feat: pure Vulkan P/Invoke renderer — no wrappers, SDL3 window, SPIR-V shaders
- Engine.Graphics: restored interfaces (IRenderContext, IRenderer, RenderBackendFactory), loaders (ObjLoader, GltfLoader), ProceduralMesh, MeshMath, SceneSerializer - Engine.Graphics.Vulkan: pure P/Invoke to libvulkan.so.1/vulkan-1.dll - VulkanNative: library loading, function pointer loading via vkGetInstanceProcAddr - Vk: static function cache for ~80 Vulkan functions, all delegate types - VulkanTypes: ~50 structs, ~20 enums matching Vulkan C headers - VulkanContext: instance, physical device, logical device, surface, memory types - VulkanSwapchain: swapchain, image views, depth image, render pass, framebuffers - VulkanPipeline: graphics pipeline, descriptor set layout, shader modules - VulkanBuffer: vertex/index/uniform buffers, staging, memory allocation - VulkanRenderer: command buffers, sync (semaphores/fences), render loop, 2 frames in flight - GLSL 450 shaders: PBR lighting (Fresnel, ACES, gamma), directional+point lights - Compiled to SPIR-V via @webgpu/glslang WASM - CortexEngine.App: switched to vulkan backend, removed Raylib/OpenTK/ImGui refs - Tests: all 66 pass (ObjLoader, MeshMath, ProceduralMesh, SceneSerializer, etc.) - Camera tour: 16 poses, screenshots captured, clean shutdown
This commit is contained in:
@@ -29,3 +29,4 @@ Screenshots/
|
||||
!launchSettings.json
|
||||
!claude_desktop_config.json
|
||||
.playwright-mcp/
|
||||
Vulkan-Guide/
|
||||
|
||||
@@ -13,16 +13,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine.AI", "src\Engine.AI\
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine.Graphics.Vulkan", "src\Engine.Graphics.Vulkan\Engine.Graphics.Vulkan.csproj", "{43C6A648-4F0C-4440-95E3-733BD8D29BCE}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine.Graphics.Raylib", "src\Engine.Graphics.Raylib\Engine.Graphics.Raylib.csproj", "{31447693-7B61-4B22-95BB-47FF08C1CB2A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine.Tests", "tests\Engine.Tests\Engine.Tests.csproj", "{55555555-5555-5555-5555-555555555555}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine.Physics", "src\Engine.Physics\Engine.Physics.csproj", "{3F5F10AA-A4CD-4E37-B4CE-B37BC60C3883}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine.Graphics.OpenTK", "src\Engine.Graphics.OpenTK\Engine.Graphics.OpenTK.csproj", "{641B69F3-E52C-4713-A97C-DAC1C814DD72}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -126,24 +122,6 @@ Global
|
||||
{43C6A648-4F0C-4440-95E3-733BD8D29BCE}.ReleaseAOT|x64.Build.0 = ReleaseAOT|Any CPU
|
||||
{43C6A648-4F0C-4440-95E3-733BD8D29BCE}.ReleaseAOT|x86.ActiveCfg = ReleaseAOT|Any CPU
|
||||
{43C6A648-4F0C-4440-95E3-733BD8D29BCE}.ReleaseAOT|x86.Build.0 = ReleaseAOT|Any CPU
|
||||
{31447693-7B61-4B22-95BB-47FF08C1CB2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{31447693-7B61-4B22-95BB-47FF08C1CB2A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{31447693-7B61-4B22-95BB-47FF08C1CB2A}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{31447693-7B61-4B22-95BB-47FF08C1CB2A}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{31447693-7B61-4B22-95BB-47FF08C1CB2A}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{31447693-7B61-4B22-95BB-47FF08C1CB2A}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{31447693-7B61-4B22-95BB-47FF08C1CB2A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{31447693-7B61-4B22-95BB-47FF08C1CB2A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{31447693-7B61-4B22-95BB-47FF08C1CB2A}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{31447693-7B61-4B22-95BB-47FF08C1CB2A}.Release|x64.Build.0 = Release|Any CPU
|
||||
{31447693-7B61-4B22-95BB-47FF08C1CB2A}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{31447693-7B61-4B22-95BB-47FF08C1CB2A}.Release|x86.Build.0 = Release|Any CPU
|
||||
{31447693-7B61-4B22-95BB-47FF08C1CB2A}.ReleaseAOT|Any CPU.ActiveCfg = ReleaseAOT|Any CPU
|
||||
{31447693-7B61-4B22-95BB-47FF08C1CB2A}.ReleaseAOT|Any CPU.Build.0 = ReleaseAOT|Any CPU
|
||||
{31447693-7B61-4B22-95BB-47FF08C1CB2A}.ReleaseAOT|x64.ActiveCfg = ReleaseAOT|Any CPU
|
||||
{31447693-7B61-4B22-95BB-47FF08C1CB2A}.ReleaseAOT|x64.Build.0 = ReleaseAOT|Any CPU
|
||||
{31447693-7B61-4B22-95BB-47FF08C1CB2A}.ReleaseAOT|x86.ActiveCfg = ReleaseAOT|Any CPU
|
||||
{31447693-7B61-4B22-95BB-47FF08C1CB2A}.ReleaseAOT|x86.Build.0 = ReleaseAOT|Any CPU
|
||||
{55555555-5555-5555-5555-555555555555}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{55555555-5555-5555-5555-555555555555}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{55555555-5555-5555-5555-555555555555}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
@@ -178,30 +156,11 @@ Global
|
||||
{3F5F10AA-A4CD-4E37-B4CE-B37BC60C3883}.ReleaseAOT|x64.Build.0 = Debug|Any CPU
|
||||
{3F5F10AA-A4CD-4E37-B4CE-B37BC60C3883}.ReleaseAOT|x86.ActiveCfg = Debug|Any CPU
|
||||
{3F5F10AA-A4CD-4E37-B4CE-B37BC60C3883}.ReleaseAOT|x86.Build.0 = Debug|Any CPU
|
||||
{641B69F3-E52C-4713-A97C-DAC1C814DD72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{641B69F3-E52C-4713-A97C-DAC1C814DD72}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{641B69F3-E52C-4713-A97C-DAC1C814DD72}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{641B69F3-E52C-4713-A97C-DAC1C814DD72}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{641B69F3-E52C-4713-A97C-DAC1C814DD72}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{641B69F3-E52C-4713-A97C-DAC1C814DD72}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{641B69F3-E52C-4713-A97C-DAC1C814DD72}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{641B69F3-E52C-4713-A97C-DAC1C814DD72}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{641B69F3-E52C-4713-A97C-DAC1C814DD72}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{641B69F3-E52C-4713-A97C-DAC1C814DD72}.Release|x64.Build.0 = Release|Any CPU
|
||||
{641B69F3-E52C-4713-A97C-DAC1C814DD72}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{641B69F3-E52C-4713-A97C-DAC1C814DD72}.Release|x86.Build.0 = Release|Any CPU
|
||||
{641B69F3-E52C-4713-A97C-DAC1C814DD72}.ReleaseAOT|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{641B69F3-E52C-4713-A97C-DAC1C814DD72}.ReleaseAOT|Any CPU.Build.0 = Debug|Any CPU
|
||||
{641B69F3-E52C-4713-A97C-DAC1C814DD72}.ReleaseAOT|x64.ActiveCfg = Debug|Any CPU
|
||||
{641B69F3-E52C-4713-A97C-DAC1C814DD72}.ReleaseAOT|x64.Build.0 = Debug|Any CPU
|
||||
{641B69F3-E52C-4713-A97C-DAC1C814DD72}.ReleaseAOT|x86.ActiveCfg = Debug|Any CPU
|
||||
{641B69F3-E52C-4713-A97C-DAC1C814DD72}.ReleaseAOT|x86.Build.0 = Debug|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{3F5F10AA-A4CD-4E37-B4CE-B37BC60C3883} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
{641B69F3-E52C-4713-A97C-DAC1C814DD72} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -22,11 +22,16 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Engine.Core\Engine.Core.csproj" />
|
||||
<ProjectReference Include="..\Engine.Graphics\Engine.Graphics.csproj" />
|
||||
<ProjectReference Include="..\Engine.Graphics.Raylib\Engine.Graphics.Raylib.csproj" />
|
||||
<ProjectReference Include="..\Engine.Graphics.Vulkan\Engine.Graphics.Vulkan.csproj" />
|
||||
<ProjectReference Include="..\Engine.AI\Engine.AI.csproj" />
|
||||
<ProjectReference Include="..\Engine.Physics\Engine.Physics.csproj" />
|
||||
<ProjectReference Include="..\Engine.Graphics.OpenTK\Engine.Graphics.OpenTK.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\Engine.Graphics.Vulkan\Shaders\*.spv">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<Link>Shaders\%(Filename)%(Extension)</Link>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
using System.Numerics;
|
||||
using Engine.Core;
|
||||
using Engine.Core.Components;
|
||||
using Engine.Physics;
|
||||
using Flecs.NET.Core;
|
||||
using Raylib_cs;
|
||||
using EngineMesh = Engine.Core.Components.Mesh;
|
||||
using EngineTransform = Engine.Core.Components.Transform;
|
||||
using EngineRigidBody = Engine.Core.Components.RigidBody;
|
||||
|
||||
namespace CortexEngine.App;
|
||||
|
||||
/// <summary>
|
||||
/// Unity-like object manipulation: left-click to pick, drag to move on ground plane.
|
||||
/// Only active when ImGui is not capturing the mouse.
|
||||
/// </summary>
|
||||
public sealed class ObjectManipulator
|
||||
{
|
||||
private Entity _selectedEntity;
|
||||
private bool _isDragging;
|
||||
private Vector3 _dragOffset;
|
||||
private float _dragDepth;
|
||||
|
||||
public string SelectedEntityName => _selectedEntity.IsValid() ? _selectedEntity.Name() : "";
|
||||
public bool IsDragging => _isDragging;
|
||||
|
||||
/// <summary>
|
||||
/// Process input for object picking and dragging.
|
||||
/// Returns true if input was consumed (ImGui should be ignored).
|
||||
/// </summary>
|
||||
private bool _imGuiAvailable;
|
||||
|
||||
public void SetImGuiAvailable(bool available) => _imGuiAvailable = available;
|
||||
|
||||
public bool ProcessInput(World world, Camera camera, IInputState input)
|
||||
{
|
||||
// Skip if ImGui is capturing mouse
|
||||
if (_imGuiAvailable && ImGuiNET.ImGui.GetIO().WantCaptureMouse)
|
||||
{
|
||||
_isDragging = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
var mousePos = new Vector2(input.MouseX, input.MouseY);
|
||||
var ray = Raylib.GetScreenToWorldRay(mousePos, ToRaylibCamera(camera));
|
||||
|
||||
// Left click — pick or start drag
|
||||
if (input.MouseLeft && !_isDragging)
|
||||
{
|
||||
var hit = RaycastEntities(world, ray);
|
||||
if (hit.IsValid())
|
||||
{
|
||||
_selectedEntity = hit;
|
||||
_isDragging = true;
|
||||
// Store depth along camera forward axis and offset from object center
|
||||
var objPos = hit.Get<EngineTransform>().Position;
|
||||
var camForward = Vector3.Normalize(camera.Target - camera.Position);
|
||||
_dragDepth = Vector3.Dot(objPos - camera.Position, camForward);
|
||||
_dragOffset = objPos - ProjectToCameraPlane(ray, camera, _dragDepth);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Drag — move object on plane orthogonal to camera (screen-space movement)
|
||||
if (_isDragging)
|
||||
{
|
||||
if (input.MouseLeft)
|
||||
{
|
||||
var targetPos = ProjectToCameraPlane(ray, camera, _dragDepth) + _dragOffset;
|
||||
|
||||
if (_selectedEntity.IsValid() && _selectedEntity.Has<EngineTransform>())
|
||||
{
|
||||
var t = _selectedEntity.Get<EngineTransform>();
|
||||
t.Position = targetPos;
|
||||
_selectedEntity.Set(t);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_isDragging = false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// After physics step, re-sync dragged entity position to physics body.
|
||||
/// </summary>
|
||||
public void SyncToPhysics(PhysicsWorld physicsWorld)
|
||||
{
|
||||
if (_isDragging && _selectedEntity.IsValid() && _selectedEntity.Has<EngineTransform>())
|
||||
{
|
||||
var t = _selectedEntity.Get<EngineTransform>();
|
||||
physicsWorld.SyncToPhysics(_selectedEntity, t);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the entity currently being dragged (for physics sync skip).
|
||||
/// </summary>
|
||||
public Entity? GetDraggedEntity() => _isDragging ? _selectedEntity : null;
|
||||
|
||||
private static Entity RaycastEntities(World world, Ray ray)
|
||||
{
|
||||
Entity closest = default;
|
||||
var closestDist = float.MaxValue;
|
||||
|
||||
world.Each((Entity e, ref EngineTransform t, ref EngineMesh _) =>
|
||||
{
|
||||
var name = e.Name();
|
||||
if (string.IsNullOrEmpty(name) || name == "Grid" || name == "Floor")
|
||||
return;
|
||||
|
||||
// Simple sphere intersection using position + approximate radius
|
||||
var radius = 1.0f;
|
||||
if (e.Has<EngineRigidBody>())
|
||||
{
|
||||
var rb = e.Get<EngineRigidBody>();
|
||||
radius = rb.ShapeSize.Length();
|
||||
}
|
||||
|
||||
var toCenter = t.Position - ray.Position;
|
||||
var proj = Vector3.Dot(toCenter, ray.Direction);
|
||||
if (proj < 0) return; // behind camera
|
||||
|
||||
var closestPoint = ray.Position + ray.Direction * proj;
|
||||
var dist = Vector3.Distance(closestPoint, t.Position);
|
||||
|
||||
if (dist <= radius)
|
||||
{
|
||||
var rayDist = Vector3.Distance(ray.Position, t.Position);
|
||||
if (rayDist < closestDist)
|
||||
{
|
||||
closestDist = rayDist;
|
||||
closest = e;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return closest;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Project a screen ray onto a plane orthogonal to the camera at the given depth.
|
||||
/// This makes objects move in screen-space (like Unity's screen-space drag).
|
||||
/// </summary>
|
||||
private static Vector3 ProjectToCameraPlane(Ray ray, Camera camera, float depth)
|
||||
{
|
||||
var camForward = Vector3.Normalize(camera.Target - camera.Position);
|
||||
var planePoint = camera.Position + camForward * depth;
|
||||
|
||||
// Ray-plane intersection: plane through planePoint with normal = camForward
|
||||
var denom = Vector3.Dot(ray.Direction, camForward);
|
||||
if (MathF.Abs(denom) < 0.0001f)
|
||||
return planePoint;
|
||||
|
||||
var t = Vector3.Dot(planePoint - ray.Position, camForward) / denom;
|
||||
if (t < 0)
|
||||
return planePoint;
|
||||
|
||||
return ray.Position + ray.Direction * t;
|
||||
}
|
||||
|
||||
private static Camera3D ToRaylibCamera(Camera camera)
|
||||
{
|
||||
return new Camera3D
|
||||
{
|
||||
Position = camera.Position,
|
||||
Target = camera.Target,
|
||||
Up = camera.Up,
|
||||
FovY = camera.FieldOfView * 180.0f / MathF.PI,
|
||||
Projection = CameraProjection.Perspective
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,6 @@ using Engine.Core;
|
||||
using Engine.Core.Components;
|
||||
using Engine.Graphics;
|
||||
using Engine.Graphics.Loaders;
|
||||
using Engine.Graphics.OpenTK;
|
||||
using Engine.Graphics.RaylibBackend;
|
||||
using Engine.Graphics.Vulkan;
|
||||
using Engine.Physics;
|
||||
using Flecs.NET.Core;
|
||||
@@ -23,7 +21,7 @@ class Program
|
||||
{
|
||||
static async Task Main(string[] args)
|
||||
{
|
||||
Console.WriteLine("Cortex Engine — Materials, Grid, Lighting, FreeFly Camera...");
|
||||
Console.WriteLine("Cortex Engine — Vulkan Backend, Pure P/Invoke...");
|
||||
|
||||
try
|
||||
{
|
||||
@@ -42,26 +40,12 @@ class Program
|
||||
var timing = new Timing();
|
||||
using var physicsWorld = new PhysicsWorld();
|
||||
|
||||
RaylibBackendRegistrar.EnsureRegistered();
|
||||
VulkanBackendRegistrar.EnsureRegistered();
|
||||
OpenTKBackendRegistrar.EnsureRegistered();
|
||||
using var renderContext = RenderBackendFactory.Create("raylib", 1280, 720, enableValidation: false);
|
||||
using var renderContext = RenderBackendFactory.Create("vulkan", 1280, 720, enableValidation: true);
|
||||
var window = renderContext.Window;
|
||||
var input = window.Input;
|
||||
using var renderer = renderContext.CreateRenderer();
|
||||
|
||||
// ImGui editor layer (Raylib only)
|
||||
ImGuiLayer? imGuiLayer = null;
|
||||
var objectManipulator = new ObjectManipulator();
|
||||
objectManipulator.SetImGuiAvailable(false);
|
||||
if (!cameraTour && renderer is RaylibRenderer rlRenderer)
|
||||
{
|
||||
imGuiLayer = new ImGuiLayer();
|
||||
imGuiLayer.Initialize();
|
||||
rlRenderer.ImGuiLayer = imGuiLayer;
|
||||
objectManipulator.SetImGuiAvailable(true);
|
||||
}
|
||||
|
||||
var (modelPath, mcpPort) = ParseArgs(args);
|
||||
var mesh = LoadModel(modelPath);
|
||||
|
||||
@@ -108,7 +92,6 @@ class Program
|
||||
|
||||
if (mcpPort > 0)
|
||||
{
|
||||
// Start the MCP server in the background so AI agents can connect via HTTP.
|
||||
mcpApp = McpEngineServerHost.Create(args, queue, port: mcpPort);
|
||||
mcpTask = mcpApp.RunAsync();
|
||||
_ = mcpTask.ContinueWith(t =>
|
||||
@@ -129,7 +112,6 @@ class Program
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
var frames = 0;
|
||||
var lastFpsTime = 0.0;
|
||||
var lastWidth = window.Width;
|
||||
@@ -177,7 +159,6 @@ class Program
|
||||
window.PumpEvents();
|
||||
input.BeginFrame();
|
||||
|
||||
// Drain any commands that arrived from the MCP server.
|
||||
var processed = queue.ProcessPending();
|
||||
if (processed > 0)
|
||||
Console.WriteLine($"Processed {processed} AI command(s)");
|
||||
@@ -191,7 +172,6 @@ class Program
|
||||
camera.AspectRatio = (float)lastWidth / lastHeight;
|
||||
}
|
||||
|
||||
// Toggle camera controller on F key press.
|
||||
if (input.IsKeyPressed(Key.F))
|
||||
{
|
||||
activeControllerIndex = (activeControllerIndex + 1) % cameraControllers.Length;
|
||||
@@ -199,7 +179,6 @@ class Program
|
||||
Console.WriteLine($"Active camera controller: {cameraController.Name}");
|
||||
}
|
||||
|
||||
// Update the active camera controller from input, unless the camera tour is driving the pose.
|
||||
if (!cameraTour)
|
||||
cameraController.Update(input, (float)timing.DeltaTime);
|
||||
|
||||
@@ -213,7 +192,6 @@ class Program
|
||||
tourScreenshotPending = true;
|
||||
}
|
||||
|
||||
// Hold the pose for a few frames to let the GPU settle, then screenshot.
|
||||
if (tourScreenshotPending)
|
||||
{
|
||||
tourSettleFrames++;
|
||||
@@ -226,7 +204,6 @@ class Program
|
||||
}
|
||||
}
|
||||
|
||||
// After the screenshot has been saved, advance to the next pose.
|
||||
if (!tourScreenshotPending && !renderer.IsScreenshotRequested)
|
||||
{
|
||||
tourIndex++;
|
||||
@@ -245,14 +222,6 @@ class Program
|
||||
}
|
||||
}
|
||||
|
||||
// Object manipulation (Unity-like drag)
|
||||
if (!cameraTour)
|
||||
{
|
||||
var cam = cameraEntity.Get<Camera>();
|
||||
objectManipulator.ProcessInput(world, cam, input);
|
||||
}
|
||||
|
||||
// Physics: create bodies, step, sync transforms
|
||||
if (!cameraTour)
|
||||
{
|
||||
var toInit = new List<(Entity, RigidBody, Transform)>();
|
||||
@@ -270,36 +239,19 @@ class Program
|
||||
e.Set(rb);
|
||||
}
|
||||
|
||||
// Sync dragged object to physics before stepping
|
||||
objectManipulator.SyncToPhysics(physicsWorld);
|
||||
|
||||
physicsWorld.Update((float)timing.DeltaTime);
|
||||
|
||||
// Sync all transforms EXCEPT the dragged object
|
||||
physicsWorld.SyncTransforms(world, objectManipulator.IsDragging ? objectManipulator.GetDraggedEntity() : null);
|
||||
physicsWorld.SyncTransforms(world, null);
|
||||
}
|
||||
|
||||
// Capture a demo screenshot after the scene warms up (non-tour mode only).
|
||||
if (!demoScreenshotRequested && !cameraTour && frames >= 15)
|
||||
{
|
||||
renderer.RequestScreenshot("Screenshots/demo.png");
|
||||
demoScreenshotRequested = true;
|
||||
}
|
||||
|
||||
// Feed frame data to ImGui before rendering.
|
||||
if (imGuiLayer != null)
|
||||
imGuiLayer.SetFrameData(world, timing, currentFps);
|
||||
|
||||
objectManipulator.SetImGuiAvailable(imGuiLayer != null);
|
||||
|
||||
renderer.RenderWorld(world);
|
||||
queue.CompletePendingScreenshots();
|
||||
|
||||
// Swap buffers for OpenGL backend
|
||||
if (renderContext is OpenTKRenderContext otkCtx)
|
||||
otkCtx.SwapBuffers();
|
||||
// Raylib handles swap internally in EndDrawing
|
||||
|
||||
frames++;
|
||||
if (timing.TotalTime - lastFpsTime >= 1.0)
|
||||
{
|
||||
@@ -311,7 +263,6 @@ class Program
|
||||
}
|
||||
|
||||
Console.WriteLine("Shutting down...");
|
||||
imGuiLayer?.Dispose();
|
||||
#if !RELEASE_AOT
|
||||
if (mcpApp != null)
|
||||
await mcpApp.StopAsync();
|
||||
@@ -369,12 +320,7 @@ class Program
|
||||
world.Entity("TorusKnot")
|
||||
.Set(new Transform(new Vector3(0, 0.5f, -6), Quaternion.Identity, new Vector3(1.5f)))
|
||||
.Set(torusKnot)
|
||||
.Set(new Material(new Vector3(0.9f, 0.9f, 0.9f), roughness: 0.25f, metallic: 0.6f, texturePath: "Content/checker.png"));
|
||||
|
||||
world.Entity("CubeTextured")
|
||||
.Set(new Transform(new Vector3(-4, 0.5f, -3), Quaternion.Identity, new Vector3(0.7f)))
|
||||
.Set(mesh)
|
||||
.Set(new Material(new Vector3(0.8f, 0.8f, 0.85f), roughness: 0.4f, metallic: 0.0f, texturePath: "Content/checker.png"));
|
||||
.Set(new Material(new Vector3(0.9f, 0.9f, 0.9f), roughness: 0.25f, metallic: 0.6f));
|
||||
|
||||
world.Entity("Floor")
|
||||
.Set(new Transform(new Vector3(0, -0.5f, 0), Quaternion.Identity, new Vector3(20, 0.5f, 20)))
|
||||
@@ -390,17 +336,16 @@ class Program
|
||||
|
||||
private static void CreateCalibrationScene(World world, Mesh mesh)
|
||||
{
|
||||
// Colored cubes at known world positions for visual analysis of perspective and camera movement.
|
||||
var positions = new (string name, Vector3 pos, Vector3 color)[]
|
||||
{
|
||||
("CubeOrigin", new Vector3(0.0f, 0.5f, 0.0f), new Vector3(1.0f, 1.0f, 1.0f)), // white at origin
|
||||
("CubeRight", new Vector3(2.0f, 0.5f, 0.0f), new Vector3(1.0f, 0.0f, 0.0f)), // red +X
|
||||
("CubeLeft", new Vector3(-2.0f, 0.5f, 0.0f), new Vector3(0.0f, 1.0f, 0.0f)), // green -X
|
||||
("CubeFront", new Vector3(0.0f, 0.5f, 2.0f), new Vector3(0.0f, 0.0f, 1.0f)), // blue +Z
|
||||
("CubeBack", new Vector3(0.0f, 0.5f, -2.0f), new Vector3(1.0f, 1.0f, 0.0f)), // yellow -Z
|
||||
("CubeUp", new Vector3(0.0f, 2.5f, 0.0f), new Vector3(1.0f, 0.0f, 1.0f)), // magenta +Y
|
||||
("CubeFar", new Vector3(0.0f, 0.5f, 8.0f), new Vector3(0.0f, 1.0f, 1.0f)), // cyan far +Z
|
||||
("CubeFarLeft", new Vector3(-5.0f, 0.5f, 5.0f), new Vector3(0.5f, 0.5f, 1.0f)) // light blue far corner
|
||||
("CubeOrigin", new Vector3(0.0f, 0.5f, 0.0f), new Vector3(1.0f, 1.0f, 1.0f)),
|
||||
("CubeRight", new Vector3(2.0f, 0.5f, 0.0f), new Vector3(1.0f, 0.0f, 0.0f)),
|
||||
("CubeLeft", new Vector3(-2.0f, 0.5f, 0.0f), new Vector3(0.0f, 1.0f, 0.0f)),
|
||||
("CubeFront", new Vector3(0.0f, 0.5f, 2.0f), new Vector3(0.0f, 0.0f, 1.0f)),
|
||||
("CubeBack", new Vector3(0.0f, 0.5f, -2.0f), new Vector3(1.0f, 1.0f, 0.0f)),
|
||||
("CubeUp", new Vector3(0.0f, 2.5f, 0.0f), new Vector3(1.0f, 0.0f, 1.0f)),
|
||||
("CubeFar", new Vector3(0.0f, 0.5f, 8.0f), new Vector3(0.0f, 1.0f, 1.0f)),
|
||||
("CubeFarLeft", new Vector3(-5.0f, 0.5f, 5.0f), new Vector3(0.5f, 0.5f, 1.0f))
|
||||
};
|
||||
|
||||
foreach (var (name, pos, color) in positions)
|
||||
@@ -411,7 +356,6 @@ class Program
|
||||
.Set(new Material(color, roughness: 0.5f, metallic: 0.1f));
|
||||
}
|
||||
|
||||
// A large reference grid at Y=0.
|
||||
world.Entity("Grid")
|
||||
.Set(new Transform(new Vector3(0.0f, 0.0f, 0.0f), Quaternion.Identity, Vector3.One))
|
||||
.Set(ProceduralMesh.CreateGrid(20, 1.0f, new Vector3(0.5f, 0.5f, 0.55f)))
|
||||
@@ -445,13 +389,12 @@ class Program
|
||||
|
||||
private static string FindModelPath(string[] args)
|
||||
{
|
||||
// Skip recognized flags so they are not treated as a model path.
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
{
|
||||
var arg = args[i];
|
||||
if (arg == "--mcp-port")
|
||||
{
|
||||
i++; // skip the value
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
|
||||
<DefineConstants>DEV_MODE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'ReleaseAOT'">
|
||||
<DefineConstants>RELEASE_AOT</DefineConstants>
|
||||
<PublishAot>true</PublishAot>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Flecs.NET.Debug" Version="4.0.4-build.546" Condition="'$(Configuration)' == 'Debug'" />
|
||||
<PackageReference Include="Flecs.NET.Release" Version="4.0.4-build.546" Condition="'$(Configuration)' == 'Release' OR '$(Configuration)' == 'ReleaseAOT'" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Engine.Core\Engine.Core.csproj" />
|
||||
<ProjectReference Include="..\Engine.Graphics\Engine.Graphics.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="Shaders\*.spv" CopyToOutputDirectory="PreserveNewest">
|
||||
<Link>Shaders\%(Filename)%(Extension)</Link>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,81 @@
|
||||
#version 450
|
||||
|
||||
layout(location = 0) in vec3 fragColor;
|
||||
layout(location = 1) in vec3 fragNormal;
|
||||
layout(location = 2) in vec3 fragWorldPos;
|
||||
layout(location = 3) in vec3 fragViewDir;
|
||||
|
||||
layout(row_major, set = 0, binding = 0) uniform FrameUBO {
|
||||
vec3 cameraPosition;
|
||||
uint lightCount;
|
||||
vec3 ambientColor;
|
||||
float pad0;
|
||||
vec4 lightData[16];
|
||||
} frame;
|
||||
|
||||
layout(push_constant) uniform PushConstants {
|
||||
mat4 mvp;
|
||||
mat4 model;
|
||||
vec4 material;
|
||||
} pc;
|
||||
|
||||
layout(location = 0) out vec4 outColor;
|
||||
|
||||
vec3 ACESFilm(vec3 x) {
|
||||
float a = 2.51;
|
||||
float b = 0.03;
|
||||
float c = 2.43;
|
||||
float d = 0.59;
|
||||
float e = 0.14;
|
||||
return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec3 albedo = fragColor * pc.material.rgb;
|
||||
float roughness = clamp(pc.material.a, 0.05, 1.0);
|
||||
|
||||
vec3 N = normalize(fragNormal);
|
||||
vec3 V = normalize(fragViewDir);
|
||||
|
||||
vec3 finalColor = frame.ambientColor * albedo;
|
||||
|
||||
for (uint i = 0u; i < frame.lightCount && i < 16u; i++) {
|
||||
vec4 dirIntensity = frame.lightData[i * 2];
|
||||
vec4 colorRange = frame.lightData[i * 2 + 1];
|
||||
|
||||
vec3 lightDir;
|
||||
float attenuation;
|
||||
|
||||
if (dirIntensity.w < 0.0) {
|
||||
// Directional light: direction stored as xyz, w = intensity (negative marks directional)
|
||||
lightDir = normalize(-dirIntensity.xyz);
|
||||
attenuation = abs(dirIntensity.w);
|
||||
} else {
|
||||
// Point light: position stored as xyz, w = intensity (positive marks point)
|
||||
vec3 toLight = dirIntensity.xyz - fragWorldPos;
|
||||
float dist = length(toLight);
|
||||
lightDir = toLight / max(dist, 0.001);
|
||||
float range = max(colorRange.w, 0.001);
|
||||
attenuation = dirIntensity.w * max(0.0, 1.0 - dist / range);
|
||||
attenuation /= max(dist * dist * 0.01, 0.01);
|
||||
}
|
||||
|
||||
vec3 H = normalize(V + lightDir);
|
||||
float NdotL = max(dot(N, lightDir), 0.0);
|
||||
float NdotH = max(dot(N, H), 0.0);
|
||||
float VdotH = max(dot(V, H), 0.0);
|
||||
|
||||
float specPower = mix(128.0, 4.0, roughness);
|
||||
float specIntensity = pow(NdotH, specPower);
|
||||
|
||||
vec3 specular = vec3(specIntensity) * colorRange.rgb;
|
||||
vec3 diffuse = albedo * NdotL * colorRange.rgb;
|
||||
|
||||
finalColor += (diffuse + specular) * attenuation;
|
||||
}
|
||||
|
||||
finalColor = ACESFilm(finalColor);
|
||||
finalColor = pow(finalColor, vec3(1.0 / 2.2));
|
||||
|
||||
outColor = vec4(finalColor, 1.0);
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,34 @@
|
||||
#version 450
|
||||
|
||||
layout(location = 0) in vec3 inPosition;
|
||||
layout(location = 1) in vec3 inColor;
|
||||
layout(location = 2) in vec3 inNormal;
|
||||
|
||||
layout(row_major, set = 0, binding = 0) uniform FrameUBO {
|
||||
vec3 cameraPosition;
|
||||
uint lightCount;
|
||||
vec3 ambientColor;
|
||||
float pad0;
|
||||
vec4 lightData[16];
|
||||
} frame;
|
||||
|
||||
layout(push_constant) uniform PushConstants {
|
||||
mat4 mvp;
|
||||
mat4 model;
|
||||
vec4 material;
|
||||
} pc;
|
||||
|
||||
layout(location = 0) out vec3 fragColor;
|
||||
layout(location = 1) out vec3 fragNormal;
|
||||
layout(location = 2) out vec3 fragWorldPos;
|
||||
layout(location = 3) out vec3 fragViewDir;
|
||||
|
||||
void main() {
|
||||
vec4 worldPos = pc.model * vec4(inPosition, 1.0);
|
||||
gl_Position = pc.mvp * vec4(inPosition, 1.0);
|
||||
|
||||
fragColor = inColor;
|
||||
fragNormal = normalize(mat3(pc.model) * inNormal);
|
||||
fragWorldPos = worldPos.xyz;
|
||||
fragViewDir = normalize(frame.cameraPosition - worldPos.xyz);
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Engine.Graphics.Vulkan;
|
||||
|
||||
internal static unsafe class Vk
|
||||
{
|
||||
public static VkInstance Instance;
|
||||
public static VkDevice Device;
|
||||
|
||||
public static PFN_vkCreateInstance vkCreateInstance;
|
||||
public static PFN_vkDestroyInstance vkDestroyInstance;
|
||||
public static PFN_vkEnumeratePhysicalDevices vkEnumeratePhysicalDevices;
|
||||
public static PFN_vkGetPhysicalDeviceProperties vkGetPhysicalDeviceProperties;
|
||||
public static PFN_vkGetPhysicalDeviceQueueFamilyProperties vkGetPhysicalDeviceQueueFamilyProperties;
|
||||
public static PFN_vkGetPhysicalDeviceMemoryProperties vkGetPhysicalDeviceMemoryProperties;
|
||||
public static PFN_vkEnumerateDeviceExtensionProperties vkEnumerateDeviceExtensionProperties;
|
||||
public static PFN_vkCreateDevice vkCreateDevice;
|
||||
public static PFN_vkDestroyDevice vkDestroyDevice;
|
||||
public static PFN_vkGetDeviceQueue vkGetDeviceQueue;
|
||||
public static PFN_vkCreateSwapchainKHR vkCreateSwapchainKHR;
|
||||
public static PFN_vkDestroySwapchainKHR vkDestroySwapchainKHR;
|
||||
public static PFN_vkGetSwapchainImagesKHR vkGetSwapchainImagesKHR;
|
||||
public static PFN_vkCreateImageView vkCreateImageView;
|
||||
public static PFN_vkDestroyImageView vkDestroyImageView;
|
||||
public static PFN_vkCreateImage vkCreateImage;
|
||||
public static PFN_vkDestroyImage vkDestroyImage;
|
||||
public static PFN_vkGetImageMemoryRequirements vkGetImageMemoryRequirements;
|
||||
public static PFN_vkBindImageMemory vkBindImageMemory;
|
||||
public static PFN_vkCreateRenderPass vkCreateRenderPass;
|
||||
public static PFN_vkDestroyRenderPass vkDestroyRenderPass;
|
||||
public static PFN_vkCreateFramebuffer vkCreateFramebuffer;
|
||||
public static PFN_vkDestroyFramebuffer vkDestroyFramebuffer;
|
||||
public static PFN_vkCreateShaderModule vkCreateShaderModule;
|
||||
public static PFN_vkDestroyShaderModule vkDestroyShaderModule;
|
||||
public static PFN_vkCreateDescriptorSetLayout vkCreateDescriptorSetLayout;
|
||||
public static PFN_vkDestroyDescriptorSetLayout vkDestroyDescriptorSetLayout;
|
||||
public static PFN_vkCreatePipelineLayout vkCreatePipelineLayout;
|
||||
public static PFN_vkDestroyPipelineLayout vkDestroyPipelineLayout;
|
||||
public static PFN_vkCreateGraphicsPipelines vkCreateGraphicsPipelines;
|
||||
public static PFN_vkDestroyPipeline vkDestroyPipeline;
|
||||
public static PFN_vkCreateDescriptorPool vkCreateDescriptorPool;
|
||||
public static PFN_vkDestroyDescriptorPool vkDestroyDescriptorPool;
|
||||
public static PFN_vkAllocateDescriptorSets vkAllocateDescriptorSets;
|
||||
public static PFN_vkUpdateDescriptorSets vkUpdateDescriptorSets;
|
||||
public static PFN_vkCreateBuffer vkCreateBuffer;
|
||||
public static PFN_vkDestroyBuffer vkDestroyBuffer;
|
||||
public static PFN_vkGetBufferMemoryRequirements vkGetBufferMemoryRequirements;
|
||||
public static PFN_vkBindBufferMemory vkBindBufferMemory;
|
||||
public static PFN_vkAllocateMemory vkAllocateMemory;
|
||||
public static PFN_vkFreeMemory vkFreeMemory;
|
||||
public static PFN_vkMapMemory vkMapMemory;
|
||||
public static PFN_vkUnmapMemory vkUnmapMemory;
|
||||
public static PFN_vkCreateCommandPool vkCreateCommandPool;
|
||||
public static PFN_vkDestroyCommandPool vkDestroyCommandPool;
|
||||
public static PFN_vkAllocateCommandBuffers vkAllocateCommandBuffers;
|
||||
public static PFN_vkFreeCommandBuffers vkFreeCommandBuffers;
|
||||
public static PFN_vkBeginCommandBuffer vkBeginCommandBuffer;
|
||||
public static PFN_vkEndCommandBuffer vkEndCommandBuffer;
|
||||
public static PFN_vkResetCommandBuffer vkResetCommandBuffer;
|
||||
public static PFN_vkQueueSubmit vkQueueSubmit;
|
||||
public static PFN_vkQueueWaitIdle vkQueueWaitIdle;
|
||||
public static PFN_vkQueuePresentKHR vkQueuePresentKHR;
|
||||
public static PFN_vkAcquireNextImageKHR vkAcquireNextImageKHR;
|
||||
public static PFN_vkCreateSemaphore vkCreateSemaphore;
|
||||
public static PFN_vkDestroySemaphore vkDestroySemaphore;
|
||||
public static PFN_vkCreateFence vkCreateFence;
|
||||
public static PFN_vkDestroyFence vkDestroyFence;
|
||||
public static PFN_vkWaitForFences vkWaitForFences;
|
||||
public static PFN_vkResetFences vkResetFences;
|
||||
public static PFN_vkCmdBeginRenderPass vkCmdBeginRenderPass;
|
||||
public static PFN_vkCmdEndRenderPass vkCmdEndRenderPass;
|
||||
public static PFN_vkCmdBindPipeline vkCmdBindPipeline;
|
||||
public static PFN_vkCmdBindDescriptorSets vkCmdBindDescriptorSets;
|
||||
public static PFN_vkCmdBindVertexBuffers vkCmdBindVertexBuffers;
|
||||
public static PFN_vkCmdBindIndexBuffer vkCmdBindIndexBuffer;
|
||||
public static PFN_vkCmdDrawIndexed vkCmdDrawIndexed;
|
||||
public static PFN_vkCmdDraw vkCmdDraw;
|
||||
public static PFN_vkCmdSetViewport vkCmdSetViewport;
|
||||
public static PFN_vkCmdSetScissor vkCmdSetScissor;
|
||||
public static PFN_vkCmdPipelineBarrier vkCmdPipelineBarrier;
|
||||
public static PFN_vkCmdCopyBuffer vkCmdCopyBuffer;
|
||||
public static PFN_vkCmdCopyBufferToImage vkCmdCopyBufferToImage;
|
||||
public static PFN_vkCmdClearColorImage vkCmdClearColorImage;
|
||||
public static PFN_vkCmdPushConstants vkCmdPushConstants;
|
||||
public static PFN_vkCreateSampler vkCreateSampler;
|
||||
public static PFN_vkDestroySampler vkDestroySampler;
|
||||
public static PFN_vkGetPhysicalDeviceSurfaceSupportKHR vkGetPhysicalDeviceSurfaceSupportKHR;
|
||||
public static PFN_vkGetPhysicalDeviceSurfaceCapabilitiesKHR vkGetPhysicalDeviceSurfaceCapabilitiesKHR;
|
||||
public static PFN_vkGetPhysicalDeviceSurfaceFormatsKHR vkGetPhysicalDeviceSurfaceFormatsKHR;
|
||||
public static PFN_vkGetPhysicalDeviceSurfacePresentModesKHR vkGetPhysicalDeviceSurfacePresentModesKHR;
|
||||
public static PFN_vkDestroySurfaceKHR vkDestroySurfaceKHR;
|
||||
|
||||
public static void LoadGlobalFunctions()
|
||||
{
|
||||
VulkanNative.LoadLibrary();
|
||||
var libHandle = VulkanNative.LoadLibrary();
|
||||
|
||||
var createInstancePtr = NativeLibrary.GetExport(libHandle, "vkCreateInstance");
|
||||
vkCreateInstance = Marshal.GetDelegateForFunctionPointer<PFN_vkCreateInstance>(createInstancePtr);
|
||||
}
|
||||
|
||||
public static void LoadInstanceFunctions(VkInstance instance)
|
||||
{
|
||||
Instance = instance;
|
||||
|
||||
vkDestroyInstance = VulkanNative.LoadInstanceFunction<PFN_vkDestroyInstance>(instance, "vkDestroyInstance");
|
||||
vkEnumeratePhysicalDevices = VulkanNative.LoadInstanceFunction<PFN_vkEnumeratePhysicalDevices>(instance, "vkEnumeratePhysicalDevices");
|
||||
vkGetPhysicalDeviceProperties = VulkanNative.LoadInstanceFunction<PFN_vkGetPhysicalDeviceProperties>(instance, "vkGetPhysicalDeviceProperties");
|
||||
vkGetPhysicalDeviceQueueFamilyProperties = VulkanNative.LoadInstanceFunction<PFN_vkGetPhysicalDeviceQueueFamilyProperties>(instance, "vkGetPhysicalDeviceQueueFamilyProperties");
|
||||
vkGetPhysicalDeviceMemoryProperties = VulkanNative.LoadInstanceFunction<PFN_vkGetPhysicalDeviceMemoryProperties>(instance, "vkGetPhysicalDeviceMemoryProperties");
|
||||
vkEnumerateDeviceExtensionProperties = VulkanNative.LoadInstanceFunction<PFN_vkEnumerateDeviceExtensionProperties>(instance, "vkEnumerateDeviceExtensionProperties");
|
||||
vkCreateDevice = VulkanNative.LoadInstanceFunction<PFN_vkCreateDevice>(instance, "vkCreateDevice");
|
||||
vkGetPhysicalDeviceSurfaceSupportKHR = VulkanNative.LoadInstanceFunction<PFN_vkGetPhysicalDeviceSurfaceSupportKHR>(instance, "vkGetPhysicalDeviceSurfaceSupportKHR");
|
||||
vkGetPhysicalDeviceSurfaceCapabilitiesKHR = VulkanNative.LoadInstanceFunction<PFN_vkGetPhysicalDeviceSurfaceCapabilitiesKHR>(instance, "vkGetPhysicalDeviceSurfaceCapabilitiesKHR");
|
||||
vkGetPhysicalDeviceSurfaceFormatsKHR = VulkanNative.LoadInstanceFunction<PFN_vkGetPhysicalDeviceSurfaceFormatsKHR>(instance, "vkGetPhysicalDeviceSurfaceFormatsKHR");
|
||||
vkGetPhysicalDeviceSurfacePresentModesKHR = VulkanNative.LoadInstanceFunction<PFN_vkGetPhysicalDeviceSurfacePresentModesKHR>(instance, "vkGetPhysicalDeviceSurfacePresentModesKHR");
|
||||
vkDestroySurfaceKHR = VulkanNative.LoadInstanceFunction<PFN_vkDestroySurfaceKHR>(instance, "vkDestroySurfaceKHR");
|
||||
}
|
||||
|
||||
public static void LoadDeviceFunctions(VkDevice device)
|
||||
{
|
||||
Device = device;
|
||||
|
||||
vkDestroyDevice = VulkanNative.LoadDeviceFunction<PFN_vkDestroyDevice>(device, "vkDestroyDevice");
|
||||
vkGetDeviceQueue = VulkanNative.LoadDeviceFunction<PFN_vkGetDeviceQueue>(device, "vkGetDeviceQueue");
|
||||
vkCreateSwapchainKHR = VulkanNative.LoadDeviceFunction<PFN_vkCreateSwapchainKHR>(device, "vkCreateSwapchainKHR");
|
||||
vkDestroySwapchainKHR = VulkanNative.LoadDeviceFunction<PFN_vkDestroySwapchainKHR>(device, "vkDestroySwapchainKHR");
|
||||
vkGetSwapchainImagesKHR = VulkanNative.LoadDeviceFunction<PFN_vkGetSwapchainImagesKHR>(device, "vkGetSwapchainImagesKHR");
|
||||
vkCreateImageView = VulkanNative.LoadDeviceFunction<PFN_vkCreateImageView>(device, "vkCreateImageView");
|
||||
vkDestroyImageView = VulkanNative.LoadDeviceFunction<PFN_vkDestroyImageView>(device, "vkDestroyImageView");
|
||||
vkCreateImage = VulkanNative.LoadDeviceFunction<PFN_vkCreateImage>(device, "vkCreateImage");
|
||||
vkDestroyImage = VulkanNative.LoadDeviceFunction<PFN_vkDestroyImage>(device, "vkDestroyImage");
|
||||
vkGetImageMemoryRequirements = VulkanNative.LoadDeviceFunction<PFN_vkGetImageMemoryRequirements>(device, "vkGetImageMemoryRequirements");
|
||||
vkBindImageMemory = VulkanNative.LoadDeviceFunction<PFN_vkBindImageMemory>(device, "vkBindImageMemory");
|
||||
vkCreateRenderPass = VulkanNative.LoadDeviceFunction<PFN_vkCreateRenderPass>(device, "vkCreateRenderPass");
|
||||
vkDestroyRenderPass = VulkanNative.LoadDeviceFunction<PFN_vkDestroyRenderPass>(device, "vkDestroyRenderPass");
|
||||
vkCreateFramebuffer = VulkanNative.LoadDeviceFunction<PFN_vkCreateFramebuffer>(device, "vkCreateFramebuffer");
|
||||
vkDestroyFramebuffer = VulkanNative.LoadDeviceFunction<PFN_vkDestroyFramebuffer>(device, "vkDestroyFramebuffer");
|
||||
vkCreateShaderModule = VulkanNative.LoadDeviceFunction<PFN_vkCreateShaderModule>(device, "vkCreateShaderModule");
|
||||
vkDestroyShaderModule = VulkanNative.LoadDeviceFunction<PFN_vkDestroyShaderModule>(device, "vkDestroyShaderModule");
|
||||
vkCreateDescriptorSetLayout = VulkanNative.LoadDeviceFunction<PFN_vkCreateDescriptorSetLayout>(device, "vkCreateDescriptorSetLayout");
|
||||
vkDestroyDescriptorSetLayout = VulkanNative.LoadDeviceFunction<PFN_vkDestroyDescriptorSetLayout>(device, "vkDestroyDescriptorSetLayout");
|
||||
vkCreatePipelineLayout = VulkanNative.LoadDeviceFunction<PFN_vkCreatePipelineLayout>(device, "vkCreatePipelineLayout");
|
||||
vkDestroyPipelineLayout = VulkanNative.LoadDeviceFunction<PFN_vkDestroyPipelineLayout>(device, "vkDestroyPipelineLayout");
|
||||
vkCreateGraphicsPipelines = VulkanNative.LoadDeviceFunction<PFN_vkCreateGraphicsPipelines>(device, "vkCreateGraphicsPipelines");
|
||||
vkDestroyPipeline = VulkanNative.LoadDeviceFunction<PFN_vkDestroyPipeline>(device, "vkDestroyPipeline");
|
||||
vkCreateDescriptorPool = VulkanNative.LoadDeviceFunction<PFN_vkCreateDescriptorPool>(device, "vkCreateDescriptorPool");
|
||||
vkDestroyDescriptorPool = VulkanNative.LoadDeviceFunction<PFN_vkDestroyDescriptorPool>(device, "vkDestroyDescriptorPool");
|
||||
vkAllocateDescriptorSets = VulkanNative.LoadDeviceFunction<PFN_vkAllocateDescriptorSets>(device, "vkAllocateDescriptorSets");
|
||||
vkUpdateDescriptorSets = VulkanNative.LoadDeviceFunction<PFN_vkUpdateDescriptorSets>(device, "vkUpdateDescriptorSets");
|
||||
vkCreateBuffer = VulkanNative.LoadDeviceFunction<PFN_vkCreateBuffer>(device, "vkCreateBuffer");
|
||||
vkDestroyBuffer = VulkanNative.LoadDeviceFunction<PFN_vkDestroyBuffer>(device, "vkDestroyBuffer");
|
||||
vkGetBufferMemoryRequirements = VulkanNative.LoadDeviceFunction<PFN_vkGetBufferMemoryRequirements>(device, "vkGetBufferMemoryRequirements");
|
||||
vkBindBufferMemory = VulkanNative.LoadDeviceFunction<PFN_vkBindBufferMemory>(device, "vkBindBufferMemory");
|
||||
vkAllocateMemory = VulkanNative.LoadDeviceFunction<PFN_vkAllocateMemory>(device, "vkAllocateMemory");
|
||||
vkFreeMemory = VulkanNative.LoadDeviceFunction<PFN_vkFreeMemory>(device, "vkFreeMemory");
|
||||
vkMapMemory = VulkanNative.LoadDeviceFunction<PFN_vkMapMemory>(device, "vkMapMemory");
|
||||
vkUnmapMemory = VulkanNative.LoadDeviceFunction<PFN_vkUnmapMemory>(device, "vkUnmapMemory");
|
||||
vkCreateCommandPool = VulkanNative.LoadDeviceFunction<PFN_vkCreateCommandPool>(device, "vkCreateCommandPool");
|
||||
vkDestroyCommandPool = VulkanNative.LoadDeviceFunction<PFN_vkDestroyCommandPool>(device, "vkDestroyCommandPool");
|
||||
vkAllocateCommandBuffers = VulkanNative.LoadDeviceFunction<PFN_vkAllocateCommandBuffers>(device, "vkAllocateCommandBuffers");
|
||||
vkFreeCommandBuffers = VulkanNative.LoadDeviceFunction<PFN_vkFreeCommandBuffers>(device, "vkFreeCommandBuffers");
|
||||
vkBeginCommandBuffer = VulkanNative.LoadDeviceFunction<PFN_vkBeginCommandBuffer>(device, "vkBeginCommandBuffer");
|
||||
vkEndCommandBuffer = VulkanNative.LoadDeviceFunction<PFN_vkEndCommandBuffer>(device, "vkEndCommandBuffer");
|
||||
vkResetCommandBuffer = VulkanNative.LoadDeviceFunction<PFN_vkResetCommandBuffer>(device, "vkResetCommandBuffer");
|
||||
vkQueueSubmit = VulkanNative.LoadDeviceFunction<PFN_vkQueueSubmit>(device, "vkQueueSubmit");
|
||||
vkQueueWaitIdle = VulkanNative.LoadDeviceFunction<PFN_vkQueueWaitIdle>(device, "vkQueueWaitIdle");
|
||||
vkQueuePresentKHR = VulkanNative.LoadDeviceFunction<PFN_vkQueuePresentKHR>(device, "vkQueuePresentKHR");
|
||||
vkAcquireNextImageKHR = VulkanNative.LoadDeviceFunction<PFN_vkAcquireNextImageKHR>(device, "vkAcquireNextImageKHR");
|
||||
vkCreateSemaphore = VulkanNative.LoadDeviceFunction<PFN_vkCreateSemaphore>(device, "vkCreateSemaphore");
|
||||
vkDestroySemaphore = VulkanNative.LoadDeviceFunction<PFN_vkDestroySemaphore>(device, "vkDestroySemaphore");
|
||||
vkCreateFence = VulkanNative.LoadDeviceFunction<PFN_vkCreateFence>(device, "vkCreateFence");
|
||||
vkDestroyFence = VulkanNative.LoadDeviceFunction<PFN_vkDestroyFence>(device, "vkDestroyFence");
|
||||
vkWaitForFences = VulkanNative.LoadDeviceFunction<PFN_vkWaitForFences>(device, "vkWaitForFences");
|
||||
vkResetFences = VulkanNative.LoadDeviceFunction<PFN_vkResetFences>(device, "vkResetFences");
|
||||
vkCmdBeginRenderPass = VulkanNative.LoadDeviceFunction<PFN_vkCmdBeginRenderPass>(device, "vkCmdBeginRenderPass");
|
||||
vkCmdEndRenderPass = VulkanNative.LoadDeviceFunction<PFN_vkCmdEndRenderPass>(device, "vkCmdEndRenderPass");
|
||||
vkCmdBindPipeline = VulkanNative.LoadDeviceFunction<PFN_vkCmdBindPipeline>(device, "vkCmdBindPipeline");
|
||||
vkCmdBindDescriptorSets = VulkanNative.LoadDeviceFunction<PFN_vkCmdBindDescriptorSets>(device, "vkCmdBindDescriptorSets");
|
||||
vkCmdBindVertexBuffers = VulkanNative.LoadDeviceFunction<PFN_vkCmdBindVertexBuffers>(device, "vkCmdBindVertexBuffers");
|
||||
vkCmdBindIndexBuffer = VulkanNative.LoadDeviceFunction<PFN_vkCmdBindIndexBuffer>(device, "vkCmdBindIndexBuffer");
|
||||
vkCmdDrawIndexed = VulkanNative.LoadDeviceFunction<PFN_vkCmdDrawIndexed>(device, "vkCmdDrawIndexed");
|
||||
vkCmdDraw = VulkanNative.LoadDeviceFunction<PFN_vkCmdDraw>(device, "vkCmdDraw");
|
||||
vkCmdSetViewport = VulkanNative.LoadDeviceFunction<PFN_vkCmdSetViewport>(device, "vkCmdSetViewport");
|
||||
vkCmdSetScissor = VulkanNative.LoadDeviceFunction<PFN_vkCmdSetScissor>(device, "vkCmdSetScissor");
|
||||
vkCmdPipelineBarrier = VulkanNative.LoadDeviceFunction<PFN_vkCmdPipelineBarrier>(device, "vkCmdPipelineBarrier");
|
||||
vkCmdCopyBuffer = VulkanNative.LoadDeviceFunction<PFN_vkCmdCopyBuffer>(device, "vkCmdCopyBuffer");
|
||||
vkCmdCopyBufferToImage = VulkanNative.LoadDeviceFunction<PFN_vkCmdCopyBufferToImage>(device, "vkCmdCopyBufferToImage");
|
||||
vkCmdClearColorImage = VulkanNative.LoadDeviceFunction<PFN_vkCmdClearColorImage>(device, "vkCmdClearColorImage");
|
||||
vkCmdPushConstants = VulkanNative.LoadDeviceFunction<PFN_vkCmdPushConstants>(device, "vkCmdPushConstants");
|
||||
vkCreateSampler = VulkanNative.LoadDeviceFunction<PFN_vkCreateSampler>(device, "vkCreateSampler");
|
||||
vkDestroySampler = VulkanNative.LoadDeviceFunction<PFN_vkDestroySampler>(device, "vkDestroySampler");
|
||||
}
|
||||
|
||||
private static T Load<T>(nint libHandle, string name) where T : Delegate
|
||||
{
|
||||
var ptr = NativeLibrary.GetExport(libHandle, name);
|
||||
if (ptr == 0)
|
||||
throw new InvalidOperationException($"Failed to load Vulkan function: {name}");
|
||||
return Marshal.GetDelegateForFunctionPointer<T>(ptr);
|
||||
}
|
||||
|
||||
public static void CheckResult(VkResult result, string operation)
|
||||
{
|
||||
if (result != VkResult.Success && result != VkResult.SuboptimalKHR)
|
||||
throw new InvalidOperationException($"Vulkan error {result} during: {operation}");
|
||||
}
|
||||
|
||||
public static byte[] ToUtf8NullTerminated(string s)
|
||||
{
|
||||
var bytes = new byte[System.Text.Encoding.UTF8.GetByteCount(s) + 1];
|
||||
System.Text.Encoding.UTF8.GetBytes(s, 0, s.Length, bytes, 0);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public static byte* AllocUtf8(string s)
|
||||
{
|
||||
var bytes = ToUtf8NullTerminated(s);
|
||||
var ptr = (byte*)Marshal.AllocHGlobal(bytes.Length);
|
||||
Marshal.Copy(bytes, 0, (nint)ptr, bytes.Length);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
public static void FreeUtf8(byte* ptr) => Marshal.FreeHGlobal((nint)ptr);
|
||||
|
||||
public static byte** AllocStringArray(string[] strings)
|
||||
{
|
||||
var ptrArray = (byte**)Marshal.AllocHGlobal(strings.Length * sizeof(nint));
|
||||
for (var i = 0; i < strings.Length; i++)
|
||||
ptrArray[i] = AllocUtf8(strings[i]);
|
||||
return ptrArray;
|
||||
}
|
||||
|
||||
public static void FreeStringArray(byte** ptrArray, int count)
|
||||
{
|
||||
for (var i = 0; i < count; i++)
|
||||
FreeUtf8(ptrArray[i]);
|
||||
Marshal.FreeHGlobal((nint)ptrArray);
|
||||
}
|
||||
}
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkCreateInstance(VkInstanceCreateInfo* pCreateInfo, void* pAllocator, VkInstance* pInstance);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkDestroyInstance(VkInstance instance, void* pAllocator);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkEnumeratePhysicalDevices(VkInstance instance, uint* pPhysicalDeviceCount, VkPhysicalDevice* pPhysicalDevices);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkGetPhysicalDeviceProperties(VkPhysicalDevice physicalDevice, VkPhysicalDeviceProperties* pProperties);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkGetPhysicalDeviceQueueFamilyProperties(VkPhysicalDevice physicalDevice, uint* pQueueFamilyPropertyCount, VkQueueFamilyProperties* pQueueFamilyProperties);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkGetPhysicalDeviceMemoryProperties(VkPhysicalDevice physicalDevice, VkPhysicalDeviceMemoryProperties* pMemoryProperties);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkEnumerateDeviceExtensionProperties(VkPhysicalDevice physicalDevice, byte* pLayerName, uint* pPropertyCount, VkExtensionProperties* pProperties);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkCreateDevice(VkPhysicalDevice physicalDevice, VkDeviceCreateInfo* pCreateInfo, void* pAllocator, VkDevice* pDevice);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkDestroyDevice(VkDevice device, void* pAllocator);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkGetDeviceQueue(VkDevice device, uint queueFamilyIndex, uint queueIndex, VkQueue* pQueue);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkCreateSwapchainKHR(VkDevice device, VkSwapchainCreateInfoKHR* pCreateInfo, void* pAllocator, VkSwapchainKHR* pSwapchain);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkDestroySwapchainKHR(VkDevice device, VkSwapchainKHR swapchain, void* pAllocator);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkGetSwapchainImagesKHR(VkDevice device, VkSwapchainKHR swapchain, uint* pSwapchainImageCount, VkImage* pSwapchainImages);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkCreateImageView(VkDevice device, VkImageViewCreateInfo* pCreateInfo, void* pAllocator, VkImageView* pView);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkDestroyImageView(VkDevice device, VkImageView imageView, void* pAllocator);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkCreateImage(VkDevice device, VkImageCreateInfo* pCreateInfo, void* pAllocator, VkImage* pImage);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkDestroyImage(VkDevice device, VkImage image, void* pAllocator);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkGetImageMemoryRequirements(VkDevice device, VkImage image, void* pMemoryRequirements);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkBindImageMemory(VkDevice device, VkImage image, VkDeviceMemory memory, ulong memoryOffset);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkCreateRenderPass(VkDevice device, VkRenderPassCreateInfo* pCreateInfo, void* pAllocator, VkRenderPass* pRenderPass);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkDestroyRenderPass(VkDevice device, VkRenderPass renderPass, void* pAllocator);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkCreateFramebuffer(VkDevice device, VkFramebufferCreateInfo* pCreateInfo, void* pAllocator, VkFramebuffer* pFramebuffer);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkDestroyFramebuffer(VkDevice device, VkFramebuffer framebuffer, void* pAllocator);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkCreateShaderModule(VkDevice device, VkShaderModuleCreateInfo* pCreateInfo, void* pAllocator, VkShaderModule* pShaderModule);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkDestroyShaderModule(VkDevice device, VkShaderModule shaderModule, void* pAllocator);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkCreateDescriptorSetLayout(VkDevice device, VkDescriptorSetLayoutCreateInfo* pCreateInfo, void* pAllocator, VkDescriptorSetLayout* pSetLayout);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkDestroyDescriptorSetLayout(VkDevice device, VkDescriptorSetLayout descriptorSetLayout, void* pAllocator);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkCreatePipelineLayout(VkDevice device, VkPipelineLayoutCreateInfo* pCreateInfo, void* pAllocator, VkPipelineLayout* pPipelineLayout);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkDestroyPipelineLayout(VkDevice device, VkPipelineLayout pipelineLayout, void* pAllocator);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkCreateGraphicsPipelines(VkDevice device, ulong pipelineCache, uint createInfoCount, VkGraphicsPipelineCreateInfo* pCreateInfos, void* pAllocator, VkPipeline* pPipelines);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkDestroyPipeline(VkDevice device, VkPipeline pipeline, void* pAllocator);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkCreateDescriptorPool(VkDevice device, VkDescriptorPoolCreateInfo* pCreateInfo, void* pAllocator, VkDescriptorPool* pDescriptorPool);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkDestroyDescriptorPool(VkDevice device, VkDescriptorPool descriptorPool, void* pAllocator);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkAllocateDescriptorSets(VkDevice device, VkDescriptorSetAllocateInfo* pAllocateInfo, VkDescriptorSet* pDescriptorSets);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkUpdateDescriptorSets(VkDevice device, uint descriptorWriteCount, VkWriteDescriptorSet* pDescriptorWrites, uint descriptorCopyCount, void* pDescriptorCopies);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkCreateBuffer(VkDevice device, VkBufferCreateInfo* pCreateInfo, void* pAllocator, VkBuffer* pBuffer);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkDestroyBuffer(VkDevice device, VkBuffer buffer, void* pAllocator);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkGetBufferMemoryRequirements(VkDevice device, VkBuffer buffer, void* pMemoryRequirements);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkBindBufferMemory(VkDevice device, VkBuffer buffer, VkDeviceMemory memory, ulong memoryOffset);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkAllocateMemory(VkDevice device, VkMemoryAllocateInfo* pAllocateInfo, void* pAllocator, VkDeviceMemory* pMemory);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkFreeMemory(VkDevice device, VkDeviceMemory memory, void* pAllocator);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkMapMemory(VkDevice device, VkDeviceMemory memory, ulong offset, ulong size, uint flags, void** ppData);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkUnmapMemory(VkDevice device, VkDeviceMemory memory);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkCreateCommandPool(VkDevice device, VkCommandPoolCreateInfo* pCreateInfo, void* pAllocator, VkCommandPool* pCommandPool);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkDestroyCommandPool(VkDevice device, VkCommandPool commandPool, void* pAllocator);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkAllocateCommandBuffers(VkDevice device, VkCommandBufferAllocateInfo* pAllocateInfo, VkCommandBuffer* pCommandBuffers);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkFreeCommandBuffers(VkDevice device, VkCommandPool commandPool, uint commandBufferCount, VkCommandBuffer* pCommandBuffers);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkBeginCommandBuffer(VkCommandBuffer commandBuffer, VkCommandBufferBeginInfo* pBeginInfo);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkEndCommandBuffer(VkCommandBuffer commandBuffer);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkResetCommandBuffer(VkCommandBuffer commandBuffer, uint flags);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkQueueSubmit(VkQueue queue, uint submitCount, VkSubmitInfo* pSubmits, VkFence fence);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkQueueWaitIdle(VkQueue queue);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkQueuePresentKHR(VkQueue queue, VkPresentInfoKHR* pPresentInfo);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkAcquireNextImageKHR(VkDevice device, VkSwapchainKHR swapchain, ulong timeout, VkSemaphore semaphore, VkFence fence, uint* pImageIndex);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkCreateSemaphore(VkDevice device, VkSemaphoreCreateInfo* pCreateInfo, void* pAllocator, VkSemaphore* pSemaphore);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkDestroySemaphore(VkDevice device, VkSemaphore semaphore, void* pAllocator);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkCreateFence(VkDevice device, VkFenceCreateInfo* pCreateInfo, void* pAllocator, VkFence* pFence);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkDestroyFence(VkDevice device, VkFence fence, void* pAllocator);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkWaitForFences(VkDevice device, uint fenceCount, VkFence* pFences, uint waitAll, ulong timeout);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkResetFences(VkDevice device, uint fenceCount, VkFence* pFences);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkCmdBeginRenderPass(VkCommandBuffer commandBuffer, VkRenderPassBeginInfo* pRenderPassBegin, VkSubpassContents contents);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkCmdEndRenderPass(VkCommandBuffer commandBuffer);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkCmdBindPipeline(VkCommandBuffer commandBuffer, int pipelineBindPoint, VkPipeline pipeline);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkCmdBindDescriptorSets(VkCommandBuffer commandBuffer, int pipelineBindPoint, VkPipelineLayout layout, uint firstSet, uint descriptorSetCount, VkDescriptorSet* pDescriptorSets, uint dynamicOffsetCount, uint* pDynamicOffsets);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkCmdBindVertexBuffers(VkCommandBuffer commandBuffer, uint firstBinding, uint bindingCount, VkBuffer* pBuffers, ulong* pOffsets);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkCmdBindIndexBuffer(VkCommandBuffer commandBuffer, VkBuffer buffer, ulong offset, VkIndexType indexType);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkCmdDrawIndexed(VkCommandBuffer commandBuffer, uint indexCount, uint instanceCount, uint firstIndex, int vertexOffset, uint firstInstance);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkCmdDraw(VkCommandBuffer commandBuffer, uint vertexCount, uint instanceCount, uint firstVertex, uint firstInstance);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkCmdSetViewport(VkCommandBuffer commandBuffer, uint firstViewport, uint viewportCount, VkViewport* pViewports);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkCmdSetScissor(VkCommandBuffer commandBuffer, uint firstScissor, uint scissorCount, VkRect2D* pScissors);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkCmdPipelineBarrier(VkCommandBuffer commandBuffer, VkPipelineStageFlags srcStageMask, VkPipelineStageFlags dstStageMask, int dependencyFlags, uint memoryBarrierCount, void* pMemoryBarriers, uint bufferMemoryBarrierCount, VkBufferMemoryBarrier* pBufferMemoryBarriers, uint imageMemoryBarrierCount, VkImageMemoryBarrier* pImageMemoryBarriers);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkCmdCopyBuffer(VkCommandBuffer commandBuffer, VkBuffer srcBuffer, VkBuffer dstBuffer, uint regionCount, VkBufferCopy* pRegions);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkCmdCopyBufferToImage(VkCommandBuffer commandBuffer, VkBuffer srcBuffer, VkImage dstImage, int dstImageLayout, uint regionCount, VkBufferImageCopy* pRegions);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkCmdClearColorImage(VkCommandBuffer commandBuffer, VkImage image, int imageLayout, VkClearColorValue* pColor, uint rangeCount, VkImageSubresourceRange* pRanges);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkCmdPushConstants(VkCommandBuffer commandBuffer, VkPipelineLayout layout, VkShaderStageFlags stageFlags, uint offset, uint size, void* pValues);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkCreateSampler(VkDevice device, VkSamplerCreateInfo* pCreateInfo, void* pAllocator, void* pSampler);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkDestroySampler(VkDevice device, void* sampler, void* pAllocator);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkGetPhysicalDeviceSurfaceSupportKHR(VkPhysicalDevice physicalDevice, uint queueFamilyIndex, VkSurfaceKHR surface, uint* pSupported);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkGetPhysicalDeviceSurfaceCapabilitiesKHR(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, VkSurfaceCapabilitiesKHR* pSurfaceCapabilities);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkGetPhysicalDeviceSurfaceFormatsKHR(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, uint* pSurfaceFormatCount, VkSurfaceFormatKHR* pSurfaceFormats);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate VkResult PFN_vkGetPhysicalDeviceSurfacePresentModesKHR(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, uint* pPresentModeCount, VkPresentModeKHR* pPresentModes);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkDestroySurfaceKHR(VkInstance instance, VkSurfaceKHR surface, void* pAllocator);
|
||||
@@ -0,0 +1,16 @@
|
||||
using Engine.Graphics;
|
||||
|
||||
namespace Engine.Graphics.Vulkan;
|
||||
|
||||
public static class VulkanBackendRegistrar
|
||||
{
|
||||
private static int _registered;
|
||||
|
||||
public static void EnsureRegistered()
|
||||
{
|
||||
if (System.Threading.Interlocked.Exchange(ref _registered, 1) == 1) return;
|
||||
|
||||
RenderBackendFactory.Register("vulkan", (width, height, validation) =>
|
||||
new VulkanRenderContext(width, height, validation));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Engine.Graphics.Vulkan;
|
||||
|
||||
internal sealed unsafe class VulkanBuffer : IDisposable
|
||||
{
|
||||
public VkBuffer Buffer;
|
||||
public VkDeviceMemory Memory;
|
||||
public ulong Size;
|
||||
public void* MappedData;
|
||||
|
||||
private readonly VulkanContext _ctx;
|
||||
private bool _disposed;
|
||||
|
||||
public VulkanBuffer(VulkanContext ctx, ulong size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties)
|
||||
{
|
||||
_ctx = ctx;
|
||||
Size = size;
|
||||
|
||||
VkBufferCreateInfo bufferInfo;
|
||||
bufferInfo.sType = VkStructureType.BufferCreateInfo;
|
||||
bufferInfo.pNext = null;
|
||||
bufferInfo.flags = 0;
|
||||
bufferInfo.size = size;
|
||||
bufferInfo.usage = usage;
|
||||
bufferInfo.sharingMode = VkSharingMode.Exclusive;
|
||||
bufferInfo.queueFamilyIndexCount = 0;
|
||||
bufferInfo.pQueueFamilyIndices = null;
|
||||
|
||||
VkBuffer buffer;
|
||||
VkResult result = Vk.vkCreateBuffer(_ctx.Device, &bufferInfo, null, &buffer);
|
||||
Vk.CheckResult(result, "vkCreateBuffer");
|
||||
Buffer = buffer;
|
||||
|
||||
VkMemoryRequirements2 memReq;
|
||||
Vk.vkGetBufferMemoryRequirements(_ctx.Device, Buffer, &memReq);
|
||||
|
||||
VkMemoryAllocateInfo allocInfo;
|
||||
allocInfo.sType = VkStructureType.MemoryAllocateInfo;
|
||||
allocInfo.pNext = null;
|
||||
allocInfo.allocationSize = memReq.size;
|
||||
allocInfo.memoryTypeIndex = _ctx.FindMemoryType(memReq.memoryTypeBits, properties);
|
||||
|
||||
VkDeviceMemory memory;
|
||||
result = Vk.vkAllocateMemory(_ctx.Device, &allocInfo, null, &memory);
|
||||
Vk.CheckResult(result, "vkAllocateMemory (buffer)");
|
||||
Memory = memory;
|
||||
|
||||
result = Vk.vkBindBufferMemory(_ctx.Device, Buffer, Memory, 0);
|
||||
Vk.CheckResult(result, "vkBindBufferMemory");
|
||||
|
||||
if ((properties & VkMemoryPropertyFlags.HostVisible) != 0)
|
||||
{
|
||||
void* mapped;
|
||||
result = Vk.vkMapMemory(_ctx.Device, Memory, 0, size, 0, &mapped);
|
||||
Vk.CheckResult(result, "vkMapMemory");
|
||||
MappedData = mapped;
|
||||
}
|
||||
}
|
||||
|
||||
public void Write(void* data, ulong size, ulong offset = 0)
|
||||
{
|
||||
if (MappedData == null)
|
||||
throw new InvalidOperationException("Buffer is not host-visible/mapped.");
|
||||
|
||||
System.Buffer.MemoryCopy(data, (void*)((byte*)MappedData + offset), size, size);
|
||||
}
|
||||
|
||||
public void Write<T>(T[] data, ulong offset = 0) where T : struct
|
||||
{
|
||||
var size = (ulong)(data.Length * Marshal.SizeOf<T>());
|
||||
fixed (T* pData = data)
|
||||
{
|
||||
Write(pData, size, offset);
|
||||
}
|
||||
}
|
||||
|
||||
public static unsafe VulkanBuffer CreateStaging(VulkanContext ctx, void* data, ulong size)
|
||||
{
|
||||
var staging = new VulkanBuffer(ctx, size, VkBufferUsageFlags.TransferSrc, VkMemoryPropertyFlags.HostVisible | VkMemoryPropertyFlags.HostCoherent);
|
||||
staging.Write(data, size);
|
||||
return staging;
|
||||
}
|
||||
|
||||
public static unsafe void CopyBuffer(VulkanContext ctx, VkCommandPool cmdPool, VkBuffer src, VkBuffer dst, ulong size)
|
||||
{
|
||||
VkCommandBuffer cmd = BeginSingleTimeCommands(ctx, cmdPool);
|
||||
|
||||
var region = new VkBufferCopy { srcOffset = 0, dstOffset = 0, size = size };
|
||||
Vk.vkCmdCopyBuffer(cmd, src, dst, 1, ®ion);
|
||||
|
||||
EndSingleTimeCommands(ctx, cmdPool, cmd);
|
||||
}
|
||||
|
||||
public static VulkanBuffer CreateDeviceLocal<T>(VulkanContext ctx, VkCommandPool cmdPool, T[] data, VkBufferUsageFlags usage) where T : struct
|
||||
{
|
||||
var size = (ulong)(data.Length * Marshal.SizeOf<T>());
|
||||
var staging = new VulkanBuffer(ctx, size, VkBufferUsageFlags.TransferSrc, VkMemoryPropertyFlags.HostVisible | VkMemoryPropertyFlags.HostCoherent);
|
||||
|
||||
fixed (T* pData = data)
|
||||
{
|
||||
staging.Write(pData, size);
|
||||
}
|
||||
|
||||
var deviceBuffer = new VulkanBuffer(ctx, size, usage | VkBufferUsageFlags.TransferDst, VkMemoryPropertyFlags.DeviceLocal);
|
||||
CopyBuffer(ctx, cmdPool, staging.Buffer, deviceBuffer.Buffer, size);
|
||||
|
||||
staging.Dispose();
|
||||
return deviceBuffer;
|
||||
}
|
||||
|
||||
public static unsafe VkCommandBuffer BeginSingleTimeCommands(VulkanContext ctx, VkCommandPool cmdPool)
|
||||
{
|
||||
VkCommandBufferAllocateInfo allocInfo;
|
||||
allocInfo.sType = VkStructureType.CommandBufferAllocateInfo;
|
||||
allocInfo.pNext = null;
|
||||
allocInfo.commandPool = cmdPool;
|
||||
allocInfo.level = VkCommandBufferLevel.Primary;
|
||||
allocInfo.commandBufferCount = 1;
|
||||
|
||||
VkCommandBuffer cmd;
|
||||
Vk.vkAllocateCommandBuffers(ctx.Device, &allocInfo, &cmd);
|
||||
|
||||
VkCommandBufferBeginInfo beginInfo;
|
||||
beginInfo.sType = VkStructureType.CommandBufferBeginInfo;
|
||||
beginInfo.pNext = null;
|
||||
beginInfo.flags = VkCommandBufferUsageFlags.OneTimeSubmit;
|
||||
beginInfo.pInheritanceInfo = null;
|
||||
|
||||
Vk.vkBeginCommandBuffer(cmd, &beginInfo);
|
||||
return cmd;
|
||||
}
|
||||
|
||||
public static unsafe void EndSingleTimeCommands(VulkanContext ctx, VkCommandPool cmdPool, VkCommandBuffer cmd)
|
||||
{
|
||||
Vk.vkEndCommandBuffer(cmd);
|
||||
|
||||
VkSubmitInfo submitInfo;
|
||||
submitInfo.sType = VkStructureType.SubmitInfo;
|
||||
submitInfo.pNext = null;
|
||||
submitInfo.waitSemaphoreCount = 0;
|
||||
submitInfo.pWaitSemaphores = null;
|
||||
submitInfo.pWaitDstStageMask = null;
|
||||
submitInfo.commandBufferCount = 1;
|
||||
submitInfo.pCommandBuffers = &cmd;
|
||||
submitInfo.signalSemaphoreCount = 0;
|
||||
submitInfo.pSignalSemaphores = null;
|
||||
|
||||
Vk.vkQueueSubmit(ctx.GraphicsQueue, 1, &submitInfo, default);
|
||||
Vk.vkQueueWaitIdle(ctx.GraphicsQueue);
|
||||
|
||||
Vk.vkFreeCommandBuffers(ctx.Device, cmdPool, 1, &cmd);
|
||||
}
|
||||
|
||||
public unsafe void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
if (MappedData != null)
|
||||
{
|
||||
Vk.vkUnmapMemory(_ctx.Device, Memory);
|
||||
MappedData = null;
|
||||
}
|
||||
if (Buffer.Value != 0) Vk.vkDestroyBuffer(_ctx.Device, Buffer, null);
|
||||
if (Memory.Value != 0) Vk.vkFreeMemory(_ctx.Device, Memory, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using Engine.Core;
|
||||
using SDL;
|
||||
|
||||
namespace Engine.Graphics.Vulkan;
|
||||
|
||||
internal sealed unsafe class VulkanContext : IDisposable
|
||||
{
|
||||
public VkInstance Instance;
|
||||
public VkPhysicalDevice PhysicalDevice;
|
||||
public VkDevice Device;
|
||||
public VkSurfaceKHR Surface;
|
||||
public VkQueue GraphicsQueue;
|
||||
public VkQueue PresentQueue;
|
||||
public uint GraphicsFamily;
|
||||
public uint PresentFamily;
|
||||
public VkPhysicalDeviceMemoryProperties MemoryProperties;
|
||||
private readonly bool _validation;
|
||||
private bool _disposed;
|
||||
|
||||
public VulkanContext(Sdl3Window window, bool enableValidation)
|
||||
{
|
||||
_validation = enableValidation;
|
||||
Vk.LoadGlobalFunctions();
|
||||
CreateInstance(window.GetRequiredVulkanExtensions());
|
||||
CreateSurface(window);
|
||||
PickPhysicalDevice();
|
||||
CreateLogicalDevice();
|
||||
}
|
||||
|
||||
private unsafe void CreateInstance(string[] requiredExtensions)
|
||||
{
|
||||
var layers = Array.Empty<string>();
|
||||
if (_validation)
|
||||
{
|
||||
uint layerCount = 0;
|
||||
VulkanNative.vkEnumerateInstanceLayerProperties(&layerCount, null);
|
||||
if (layerCount > 0)
|
||||
{
|
||||
var availableLayers = new VkLayerProperties[layerCount];
|
||||
fixed (VkLayerProperties* pLayers = availableLayers)
|
||||
{
|
||||
VulkanNative.vkEnumerateInstanceLayerProperties(&layerCount, pLayers);
|
||||
}
|
||||
|
||||
for (var i = 0; i < layerCount; i++)
|
||||
{
|
||||
fixed (VkLayerProperties* pLayer = &availableLayers[i])
|
||||
{
|
||||
var nameLen = 0;
|
||||
while (nameLen < 256 && pLayer->layerName[nameLen] != 0) nameLen++;
|
||||
var layerName = Encoding.UTF8.GetString(pLayer->layerName, nameLen);
|
||||
|
||||
if (layerName == "VK_LAYER_KHRONOS_validation")
|
||||
{
|
||||
layers = new[] { "VK_LAYER_KHRONOS_validation" };
|
||||
Console.WriteLine("[Vulkan] Validation layers enabled.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (layers.Length == 0)
|
||||
Console.WriteLine("[Vulkan] Validation layers requested but not available.");
|
||||
}
|
||||
|
||||
var extensions = requiredExtensions;
|
||||
|
||||
var appNameBytes = Encoding.UTF8.GetBytes("Cortex Engine\0");
|
||||
var engineNameBytes = Encoding.UTF8.GetBytes("CortexEngine\0");
|
||||
|
||||
VkApplicationInfo appInfo;
|
||||
appInfo.sType = VkStructureType.ApplicationInfo;
|
||||
appInfo.pNext = null;
|
||||
fixed (byte* pAppName = appNameBytes, pEngineName = engineNameBytes)
|
||||
{
|
||||
appInfo.pApplicationName = pAppName;
|
||||
appInfo.applicationVersion = 0;
|
||||
appInfo.pEngineName = pEngineName;
|
||||
appInfo.engineVersion = 0;
|
||||
appInfo.apiVersion = (1 << 22) | (3 << 12);
|
||||
|
||||
var extPtrs = Vk.AllocStringArray(extensions);
|
||||
var layerPtrs = Vk.AllocStringArray(layers);
|
||||
|
||||
VkInstanceCreateInfo createInfo;
|
||||
createInfo.sType = VkStructureType.InstanceCreateInfo;
|
||||
createInfo.pNext = null;
|
||||
createInfo.flags = 0;
|
||||
createInfo.pApplicationInfo = &appInfo;
|
||||
createInfo.enabledLayerCount = (uint)layers.Length;
|
||||
createInfo.ppEnabledLayerNames = layerPtrs;
|
||||
createInfo.enabledExtensionCount = (uint)extensions.Length;
|
||||
createInfo.ppEnabledExtensionNames = extPtrs;
|
||||
|
||||
VkResult result;
|
||||
fixed (VkInstance* pInstance = &Instance)
|
||||
{
|
||||
result = Vk.vkCreateInstance(&createInfo, null, pInstance);
|
||||
}
|
||||
|
||||
Vk.FreeStringArray(extPtrs, extensions.Length);
|
||||
Vk.FreeStringArray(layerPtrs, layers.Length);
|
||||
|
||||
if (result != VkResult.Success)
|
||||
throw new InvalidOperationException($"vkCreateInstance failed: {result}. " +
|
||||
$"Extensions: [{string.Join(", ", extensions)}], Layers: [{string.Join(", ", layers)}]");
|
||||
}
|
||||
|
||||
Vk.LoadInstanceFunctions(Instance);
|
||||
Console.WriteLine("[Vulkan] Instance created.");
|
||||
}
|
||||
|
||||
private unsafe void CreateSurface(Sdl3Window window)
|
||||
{
|
||||
var sdlWindow = (SDL_Window*)window.Handle;
|
||||
|
||||
var instancePtr = (SDL.VkInstance_T*)Instance.Value;
|
||||
SDL.VkSurfaceKHR_T* surfacePtr;
|
||||
if (!SDL3.SDL_Vulkan_CreateSurface(sdlWindow, instancePtr, null, &surfacePtr))
|
||||
throw new InvalidOperationException($"SDL_Vulkan_CreateSurface failed: {SDL3.SDL_GetError()}");
|
||||
|
||||
Surface = new VkSurfaceKHR { Value = (ulong)surfacePtr };
|
||||
Console.WriteLine("[Vulkan] Surface created.");
|
||||
}
|
||||
|
||||
private unsafe void PickPhysicalDevice()
|
||||
{
|
||||
uint deviceCount = 0;
|
||||
Vk.vkEnumeratePhysicalDevices(Instance, &deviceCount, null);
|
||||
if (deviceCount == 0)
|
||||
throw new InvalidOperationException("No GPU with Vulkan support found.");
|
||||
|
||||
var devices = new VkPhysicalDevice[deviceCount];
|
||||
fixed (VkPhysicalDevice* pDevices = devices)
|
||||
{
|
||||
Vk.vkEnumeratePhysicalDevices(Instance, &deviceCount, pDevices);
|
||||
}
|
||||
|
||||
VkPhysicalDevice bestDevice = default;
|
||||
uint bestGraphicsFamily = uint.MaxValue;
|
||||
uint bestPresentFamily = uint.MaxValue;
|
||||
int bestScore = -1;
|
||||
|
||||
for (uint i = 0; i < deviceCount; i++)
|
||||
{
|
||||
VkPhysicalDeviceProperties props;
|
||||
Vk.vkGetPhysicalDeviceProperties(devices[i], &props);
|
||||
|
||||
byte* pName = props.deviceName;
|
||||
var nameLen = 0;
|
||||
while (nameLen < 256 && pName[nameLen] != 0) nameLen++;
|
||||
var deviceName = Encoding.UTF8.GetString(pName, nameLen);
|
||||
|
||||
var score = (int)props.deviceType;
|
||||
if (props.deviceType == VkPhysicalDeviceType.DiscreteGpu) score = 1000;
|
||||
else if (props.deviceType == VkPhysicalDeviceType.IntegratedGpu) score = 500;
|
||||
|
||||
if (!FindQueueFamilies(devices[i], out var graphicsFamily, out var presentFamily))
|
||||
continue;
|
||||
|
||||
if (score > bestScore)
|
||||
{
|
||||
bestScore = score;
|
||||
bestDevice = devices[i];
|
||||
bestGraphicsFamily = graphicsFamily;
|
||||
bestPresentFamily = presentFamily;
|
||||
Console.WriteLine($"[Vulkan] Selected GPU: {deviceName} (score {score})");
|
||||
}
|
||||
}
|
||||
|
||||
if (bestScore < 0)
|
||||
throw new InvalidOperationException("No suitable GPU found with graphics + present queues.");
|
||||
|
||||
PhysicalDevice = bestDevice;
|
||||
GraphicsFamily = bestGraphicsFamily;
|
||||
PresentFamily = bestPresentFamily;
|
||||
|
||||
VkPhysicalDeviceMemoryProperties memProps;
|
||||
Vk.vkGetPhysicalDeviceMemoryProperties(PhysicalDevice, &memProps);
|
||||
MemoryProperties = memProps;
|
||||
}
|
||||
|
||||
private unsafe bool FindQueueFamilies(VkPhysicalDevice device, out uint graphicsFamily, out uint presentFamily)
|
||||
{
|
||||
graphicsFamily = uint.MaxValue;
|
||||
presentFamily = uint.MaxValue;
|
||||
|
||||
uint count = 0;
|
||||
Vk.vkGetPhysicalDeviceQueueFamilyProperties(device, &count, null);
|
||||
if (count == 0) return false;
|
||||
|
||||
var props = new VkQueueFamilyProperties[count];
|
||||
fixed (VkQueueFamilyProperties* pProps = props)
|
||||
{
|
||||
Vk.vkGetPhysicalDeviceQueueFamilyProperties(device, &count, pProps);
|
||||
}
|
||||
|
||||
for (uint i = 0; i < count; i++)
|
||||
{
|
||||
if ((props[i].queueFlags & VkQueueFlags.Graphics) != 0)
|
||||
graphicsFamily = i;
|
||||
|
||||
uint supported = 0;
|
||||
Vk.vkGetPhysicalDeviceSurfaceSupportKHR(device, i, Surface, &supported);
|
||||
if (supported != 0)
|
||||
presentFamily = i;
|
||||
|
||||
if (graphicsFamily != uint.MaxValue && presentFamily != uint.MaxValue)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private unsafe void CreateLogicalDevice()
|
||||
{
|
||||
var queueIndices = new HashSet<uint> { GraphicsFamily, PresentFamily };
|
||||
var queueCreateInfos = new VkDeviceQueueCreateInfo[queueIndices.Count];
|
||||
var priorities = new float[] { 1.0f };
|
||||
|
||||
fixed (float* pPrio = priorities)
|
||||
{
|
||||
var idx = 0;
|
||||
foreach (var qfi in queueIndices)
|
||||
{
|
||||
queueCreateInfos[idx] = new VkDeviceQueueCreateInfo
|
||||
{
|
||||
sType = VkStructureType.DeviceQueueCreateInfo,
|
||||
pNext = null,
|
||||
flags = 0,
|
||||
queueFamilyIndex = qfi,
|
||||
queueCount = 1,
|
||||
pQueuePriorities = pPrio
|
||||
};
|
||||
idx++;
|
||||
}
|
||||
|
||||
var extNameBytes = System.Text.Encoding.UTF8.GetBytes("VK_KHR_swapchain\0");
|
||||
var extNamePtr = (byte*)Marshal.AllocHGlobal(extNameBytes.Length);
|
||||
Marshal.Copy(extNameBytes, 0, (nint)extNamePtr, extNameBytes.Length);
|
||||
|
||||
fixed (VkDeviceQueueCreateInfo* pQueueCreateInfos = queueCreateInfos)
|
||||
{
|
||||
byte* features = stackalloc byte[228];
|
||||
VkDeviceCreateInfo createInfo;
|
||||
createInfo.sType = VkStructureType.DeviceCreateInfo;
|
||||
createInfo.pNext = null;
|
||||
createInfo.flags = 0;
|
||||
createInfo.queueCreateInfoCount = (uint)queueCreateInfos.Length;
|
||||
createInfo.pQueueCreateInfos = pQueueCreateInfos;
|
||||
createInfo.enabledLayerCount = 0;
|
||||
createInfo.ppEnabledLayerNames = null;
|
||||
createInfo.enabledExtensionCount = 1;
|
||||
createInfo.ppEnabledExtensionNames = &extNamePtr;
|
||||
createInfo.pEnabledFeatures = features;
|
||||
|
||||
VkDevice device;
|
||||
var result = Vk.vkCreateDevice(PhysicalDevice, &createInfo, null, &device);
|
||||
Vk.CheckResult(result, "vkCreateDevice");
|
||||
Device = device;
|
||||
}
|
||||
|
||||
Marshal.FreeHGlobal((nint)extNamePtr);
|
||||
}
|
||||
|
||||
Vk.LoadDeviceFunctions(Device);
|
||||
|
||||
fixed (VkQueue* pGfxQueue = &GraphicsQueue)
|
||||
{
|
||||
Vk.vkGetDeviceQueue(Device, GraphicsFamily, 0, pGfxQueue);
|
||||
}
|
||||
fixed (VkQueue* pPresentQueue = &PresentQueue)
|
||||
{
|
||||
Vk.vkGetDeviceQueue(Device, PresentFamily, 0, pPresentQueue);
|
||||
}
|
||||
|
||||
Console.WriteLine("[Vulkan] Logical device created.");
|
||||
}
|
||||
|
||||
public unsafe uint FindMemoryType(uint typeFilter, VkMemoryPropertyFlags properties)
|
||||
{
|
||||
for (uint i = 0; i < MemoryProperties.memoryTypeCount; i++)
|
||||
{
|
||||
var memType = GetMemoryType(i);
|
||||
if ((typeFilter & (1u << (int)i)) != 0 && (memType.propertyFlags & properties) == properties)
|
||||
return i;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Failed to find memory type with filter={typeFilter:X} props={properties}");
|
||||
}
|
||||
|
||||
private VkMemoryType GetMemoryType(uint index)
|
||||
{
|
||||
return index switch
|
||||
{
|
||||
0 => MemoryProperties.memoryTypes0,
|
||||
1 => MemoryProperties.memoryTypes1,
|
||||
2 => MemoryProperties.memoryTypes2,
|
||||
3 => MemoryProperties.memoryTypes3,
|
||||
4 => MemoryProperties.memoryTypes4,
|
||||
5 => MemoryProperties.memoryTypes5,
|
||||
6 => MemoryProperties.memoryTypes6,
|
||||
7 => MemoryProperties.memoryTypes7,
|
||||
8 => MemoryProperties.memoryTypes8,
|
||||
9 => MemoryProperties.memoryTypes9,
|
||||
10 => MemoryProperties.memoryTypes10,
|
||||
11 => MemoryProperties.memoryTypes11,
|
||||
12 => MemoryProperties.memoryTypes12,
|
||||
13 => MemoryProperties.memoryTypes13,
|
||||
14 => MemoryProperties.memoryTypes14,
|
||||
15 => MemoryProperties.memoryTypes15,
|
||||
_ => throw new IndexOutOfRangeException()
|
||||
};
|
||||
}
|
||||
|
||||
public unsafe void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
if (Device.Value != 0)
|
||||
{
|
||||
Vk.vkQueueWaitIdle(GraphicsQueue);
|
||||
Vk.vkDestroyDevice(Device, null);
|
||||
}
|
||||
if (Surface.Value != 0)
|
||||
Vk.vkDestroySurfaceKHR(Instance, Surface, null);
|
||||
if (Instance.Value != 0)
|
||||
Vk.vkDestroyInstance(Instance, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Engine.Graphics.Vulkan;
|
||||
|
||||
internal static unsafe partial class VulkanNative
|
||||
{
|
||||
private const string VulkanLib = "vulkan-1.dll";
|
||||
private const string VulkanLibLinux = "libvulkan.so.1";
|
||||
|
||||
private static nint _libHandle;
|
||||
|
||||
public static nint LoadLibrary()
|
||||
{
|
||||
if (_libHandle != 0) return _libHandle;
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
_libHandle = NativeLibrary.Load(VulkanLib);
|
||||
else
|
||||
_libHandle = NativeLibrary.Load(VulkanLibLinux);
|
||||
|
||||
if (_libHandle == 0)
|
||||
throw new InvalidOperationException("Failed to load Vulkan library.");
|
||||
|
||||
return _libHandle;
|
||||
}
|
||||
|
||||
public static void* GetInstanceProcAddr(VkInstance instance, byte* pName)
|
||||
{
|
||||
LoadLibrary();
|
||||
var ptr = NativeLibrary.GetExport(_libHandle, "vkGetInstanceProcAddr");
|
||||
var func = Marshal.GetDelegateForFunctionPointer<PFN_vkGetInstanceProcAddr>(ptr);
|
||||
return func(instance, pName);
|
||||
}
|
||||
|
||||
public static void* GetDeviceProcAddr(VkDevice device, byte* pName)
|
||||
{
|
||||
var ptr = NativeLibrary.GetExport(_libHandle, "vkGetDeviceProcAddr");
|
||||
var func = Marshal.GetDelegateForFunctionPointer<PFN_vkGetDeviceProcAddr>(ptr);
|
||||
return func(device, pName);
|
||||
}
|
||||
|
||||
public static T LoadInstanceFunction<T>(VkInstance instance, string name) where T : Delegate
|
||||
{
|
||||
var nameBytes = System.Text.Encoding.UTF8.GetBytes(name + "\0");
|
||||
fixed (byte* pName = nameBytes)
|
||||
{
|
||||
var addr = GetInstanceProcAddr(instance, pName);
|
||||
if (addr == null)
|
||||
throw new InvalidOperationException($"Failed to load Vulkan instance function: {name}");
|
||||
return Marshal.GetDelegateForFunctionPointer<T>((nint)addr);
|
||||
}
|
||||
}
|
||||
|
||||
public static T LoadDeviceFunction<T>(VkDevice device, string name) where T : Delegate
|
||||
{
|
||||
var nameBytes = System.Text.Encoding.UTF8.GetBytes(name + "\0");
|
||||
fixed (byte* pName = nameBytes)
|
||||
{
|
||||
var addr = GetDeviceProcAddr(device, pName);
|
||||
if (addr == null)
|
||||
throw new InvalidOperationException($"Failed to load Vulkan device function: {name}");
|
||||
return Marshal.GetDelegateForFunctionPointer<T>((nint)addr);
|
||||
}
|
||||
}
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
public delegate void* PFN_vkGetInstanceProcAddr(VkInstance instance, byte* pName);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
public delegate void* PFN_vkGetDeviceProcAddr(VkDevice device, byte* pName);
|
||||
|
||||
[LibraryImport("vulkan-1.dll", EntryPoint = "vkGetInstanceProcAddr", StringMarshalling = StringMarshalling.Utf8)]
|
||||
public static partial void* vkGetInstanceProcAddr_Win(VkInstance instance, string pName);
|
||||
|
||||
[DllImport("libvulkan.so.1", EntryPoint = "vkGetInstanceProcAddr", CharSet = CharSet.Ansi)]
|
||||
public static extern void* vkGetInstanceProcAddr_Linux(VkInstance instance, string pName);
|
||||
|
||||
public static VkResult vkEnumerateInstanceExtensionProperties(byte* pLayerName, uint* pPropertyCount, VkExtensionProperties* pProperties)
|
||||
{
|
||||
LoadLibrary();
|
||||
var ptr = NativeLibrary.GetExport(_libHandle, "vkEnumerateInstanceExtensionProperties");
|
||||
var func = Marshal.GetDelegateForFunctionPointer<PFN_vkEnumerateInstanceExtensionProperties>(ptr);
|
||||
return func(pLayerName, pPropertyCount, pProperties);
|
||||
}
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
public delegate VkResult PFN_vkEnumerateInstanceExtensionProperties(byte* pLayerName, uint* pPropertyCount, VkExtensionProperties* pProperties);
|
||||
|
||||
public static VkResult vkEnumerateInstanceLayerProperties(uint* pPropertyCount, VkLayerProperties* pProperties)
|
||||
{
|
||||
LoadLibrary();
|
||||
var ptr = NativeLibrary.GetExport(_libHandle, "vkEnumerateInstanceLayerProperties");
|
||||
var func = Marshal.GetDelegateForFunctionPointer<PFN_vkEnumerateInstanceLayerProperties>(ptr);
|
||||
return func(pPropertyCount, pProperties);
|
||||
}
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
public delegate VkResult PFN_vkEnumerateInstanceLayerProperties(uint* pPropertyCount, VkLayerProperties* pProperties);
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Engine.Graphics.Vulkan;
|
||||
|
||||
internal sealed unsafe class VulkanPipeline : IDisposable
|
||||
{
|
||||
public VkPipelineLayout PipelineLayout;
|
||||
public VkPipeline Pipeline;
|
||||
public VkDescriptorSetLayout DescriptorSetLayout;
|
||||
public VkShaderModule VertexShader;
|
||||
public VkShaderModule FragmentShader;
|
||||
|
||||
private readonly VulkanContext _ctx;
|
||||
private bool _disposed;
|
||||
|
||||
public const int PushConstantSize = 128;
|
||||
public const int FrameUboSize = 16 + 16 + 16 * 4 * 8;
|
||||
|
||||
public VulkanPipeline(VulkanContext ctx, VkRenderPass renderPass)
|
||||
{
|
||||
_ctx = ctx;
|
||||
Create(renderPass);
|
||||
}
|
||||
|
||||
private unsafe void Create(VkRenderPass renderPass)
|
||||
{
|
||||
Console.WriteLine("[Vulkan] Loading shaders...");
|
||||
VertexShader = CreateShaderModule("Shaders/vertex.spv");
|
||||
FragmentShader = CreateShaderModule("Shaders/fragment.spv");
|
||||
Console.WriteLine("[Vulkan] Shaders loaded.");
|
||||
|
||||
var mainName = Vk.AllocUtf8("main");
|
||||
|
||||
var stages = new VkPipelineShaderStageCreateInfo[2];
|
||||
stages[0] = new VkPipelineShaderStageCreateInfo
|
||||
{
|
||||
sType = VkStructureType.PipelineShaderStageCreateInfo,
|
||||
pNext = null,
|
||||
flags = 0,
|
||||
stage = VkShaderStageFlags.Vertex,
|
||||
module = VertexShader,
|
||||
pName = mainName,
|
||||
pSpecializationInfo = null
|
||||
};
|
||||
stages[1] = new VkPipelineShaderStageCreateInfo
|
||||
{
|
||||
sType = VkStructureType.PipelineShaderStageCreateInfo,
|
||||
pNext = null,
|
||||
flags = 0,
|
||||
stage = VkShaderStageFlags.Fragment,
|
||||
module = FragmentShader,
|
||||
pName = mainName,
|
||||
pSpecializationInfo = null
|
||||
};
|
||||
|
||||
var bindingDesc = new VkVertexInputBindingDescription
|
||||
{
|
||||
binding = 0,
|
||||
stride = 36,
|
||||
inputRate = 0
|
||||
};
|
||||
|
||||
var attrDescs = stackalloc VkVertexInputAttributeDescription[3];
|
||||
attrDescs[0] = new VkVertexInputAttributeDescription { location = 0, binding = 0, format = VkFormat.R32G32B32Sfloat, offset = 0 };
|
||||
attrDescs[1] = new VkVertexInputAttributeDescription { location = 1, binding = 0, format = VkFormat.R32G32B32Sfloat, offset = 12 };
|
||||
attrDescs[2] = new VkVertexInputAttributeDescription { location = 2, binding = 0, format = VkFormat.R32G32B32Sfloat, offset = 24 };
|
||||
|
||||
VkPipelineVertexInputStateCreateInfo vertexInputState;
|
||||
vertexInputState.sType = VkStructureType.PipelineVertexInputStateCreateInfo;
|
||||
vertexInputState.pNext = null;
|
||||
vertexInputState.flags = 0;
|
||||
vertexInputState.vertexBindingDescriptionCount = 1;
|
||||
vertexInputState.pVertexBindingDescriptions = &bindingDesc;
|
||||
vertexInputState.vertexAttributeDescriptionCount = 3;
|
||||
vertexInputState.pVertexAttributeDescriptions = attrDescs;
|
||||
|
||||
var inputAssemblyState = new VkPipelineInputAssemblyStateCreateInfo
|
||||
{
|
||||
sType = VkStructureType.PipelineInputAssemblyStateCreateInfo,
|
||||
pNext = null,
|
||||
flags = 0,
|
||||
topology = VkPrimitiveTopology.TriangleList,
|
||||
primitiveRestartEnable = 0
|
||||
};
|
||||
|
||||
var viewport = new VkViewport { x = 0, y = 0, width = 0, height = 0, minDepth = 0, maxDepth = 1 };
|
||||
var scissor = new VkRect2D { offset = new VkOffset2D { x = 0, y = 0 }, extent = new VkExtent2D { width = 0, height = 0 } };
|
||||
|
||||
VkPipelineViewportStateCreateInfo viewportState;
|
||||
viewportState.sType = VkStructureType.PipelineViewportStateCreateInfo;
|
||||
viewportState.pNext = null;
|
||||
viewportState.flags = 0;
|
||||
viewportState.viewportCount = 1;
|
||||
viewportState.pViewports = &viewport;
|
||||
viewportState.scissorCount = 1;
|
||||
viewportState.pScissors = &scissor;
|
||||
|
||||
var rasterizationState = new VkPipelineRasterizationStateCreateInfo
|
||||
{
|
||||
sType = VkStructureType.PipelineRasterizationStateCreateInfo,
|
||||
pNext = null,
|
||||
flags = 0,
|
||||
depthClampEnable = 0,
|
||||
rasterizerDiscardEnable = 0,
|
||||
polygonMode = VkPolygonMode.Fill,
|
||||
cullMode = VkCullModeFlags.None,
|
||||
frontFace = VkFrontFace.Clockwise,
|
||||
depthBiasEnable = 0,
|
||||
depthBiasConstantFactor = 0,
|
||||
depthBiasClamp = 0,
|
||||
depthBiasSlopeFactor = 0,
|
||||
lineWidth = 1.0f
|
||||
};
|
||||
|
||||
var multisampleState = new VkPipelineMultisampleStateCreateInfo
|
||||
{
|
||||
sType = VkStructureType.PipelineMultisampleStateCreateInfo,
|
||||
pNext = null,
|
||||
flags = 0,
|
||||
rasterizationSamples = VkSampleCountFlags.One,
|
||||
sampleShadingEnable = 0,
|
||||
minSampleShading = 0,
|
||||
pSampleMask = null,
|
||||
alphaToCoverageEnable = 0,
|
||||
alphaToOneEnable = 0
|
||||
};
|
||||
|
||||
var depthStencilState = new VkPipelineDepthStencilStateCreateInfo
|
||||
{
|
||||
sType = VkStructureType.PipelineDepthStencilStateCreateInfo,
|
||||
pNext = null,
|
||||
flags = 0,
|
||||
depthTestEnable = 1,
|
||||
depthWriteEnable = 1,
|
||||
depthCompareOp = VkCompareOp.Less,
|
||||
depthBoundsTestEnable = 0,
|
||||
stencilTestEnable = 0,
|
||||
front = new VkStencilOpState(),
|
||||
back = new VkStencilOpState(),
|
||||
minDepthBounds = 0,
|
||||
maxDepthBounds = 1
|
||||
};
|
||||
|
||||
var blendAttachment = new VkPipelineColorBlendAttachmentState
|
||||
{
|
||||
blendEnable = 0,
|
||||
srcColorBlendFactor = VkBlendFactor.One,
|
||||
dstColorBlendFactor = VkBlendFactor.Zero,
|
||||
colorBlendOp = VkBlendOp.Add,
|
||||
srcAlphaBlendFactor = VkBlendFactor.One,
|
||||
dstAlphaBlendFactor = VkBlendFactor.Zero,
|
||||
alphaBlendOp = VkBlendOp.Add,
|
||||
colorWriteMask = VkColorComponentFlags.R | VkColorComponentFlags.G | VkColorComponentFlags.B | VkColorComponentFlags.A
|
||||
};
|
||||
|
||||
VkPipelineColorBlendStateCreateInfo colorBlendState = default;
|
||||
colorBlendState.sType = VkStructureType.PipelineColorBlendStateCreateInfo;
|
||||
colorBlendState.pNext = null;
|
||||
colorBlendState.flags = 0;
|
||||
colorBlendState.logicOpEnable = 0;
|
||||
colorBlendState.logicOp = 0;
|
||||
colorBlendState.attachmentCount = 1;
|
||||
colorBlendState.pAttachments = &blendAttachment;
|
||||
|
||||
var uboBinding = new VkDescriptorSetLayoutBinding
|
||||
{
|
||||
binding = 0,
|
||||
descriptorType = VkDescriptorType.UniformBuffer,
|
||||
descriptorCount = 1,
|
||||
stageFlags = VkShaderStageFlags.Vertex | VkShaderStageFlags.Fragment,
|
||||
pImmutableSamplers = null
|
||||
};
|
||||
|
||||
Console.WriteLine("[Vulkan] Creating descriptor set layout...");
|
||||
VkDescriptorSetLayoutCreateInfo dsLayoutInfo;
|
||||
dsLayoutInfo.sType = VkStructureType.DescriptorSetLayoutCreateInfo;
|
||||
dsLayoutInfo.pNext = null;
|
||||
dsLayoutInfo.flags = 0;
|
||||
dsLayoutInfo.bindingCount = 1;
|
||||
dsLayoutInfo.pBindings = &uboBinding;
|
||||
|
||||
VkDescriptorSetLayout dsLayout;
|
||||
VkResult result = Vk.vkCreateDescriptorSetLayout(_ctx.Device, &dsLayoutInfo, null, &dsLayout);
|
||||
Vk.CheckResult(result, "vkCreateDescriptorSetLayout");
|
||||
DescriptorSetLayout = dsLayout;
|
||||
Console.WriteLine("[Vulkan] Descriptor set layout created.");
|
||||
|
||||
var pushConstantRange = new VkPushConstantRange
|
||||
{
|
||||
stageFlags = VkShaderStageFlags.Vertex | VkShaderStageFlags.Fragment,
|
||||
offset = 0,
|
||||
size = PushConstantSize
|
||||
};
|
||||
|
||||
Console.WriteLine("[Vulkan] Creating pipeline layout...");
|
||||
VkPipelineLayoutCreateInfo layoutInfo;
|
||||
layoutInfo.sType = VkStructureType.PipelineLayoutCreateInfo;
|
||||
layoutInfo.pNext = null;
|
||||
layoutInfo.flags = 0;
|
||||
layoutInfo.setLayoutCount = 1;
|
||||
layoutInfo.pSetLayouts = &dsLayout;
|
||||
layoutInfo.pushConstantRangeCount = 1;
|
||||
layoutInfo.pPushConstantRanges = &pushConstantRange;
|
||||
|
||||
VkPipelineLayout pipeLayout;
|
||||
result = Vk.vkCreatePipelineLayout(_ctx.Device, &layoutInfo, null, &pipeLayout);
|
||||
Vk.CheckResult(result, "vkCreatePipelineLayout");
|
||||
PipelineLayout = pipeLayout;
|
||||
Console.WriteLine("[Vulkan] Pipeline layout created.");
|
||||
|
||||
Console.WriteLine("[Vulkan] Creating graphics pipeline...");
|
||||
fixed (VkPipelineShaderStageCreateInfo* pStages = stages)
|
||||
{
|
||||
VkGraphicsPipelineCreateInfo pipelineInfo;
|
||||
pipelineInfo.sType = VkStructureType.GraphicsPipelineCreateInfo;
|
||||
pipelineInfo.pNext = null;
|
||||
pipelineInfo.flags = 0;
|
||||
pipelineInfo.stageCount = 2;
|
||||
pipelineInfo.pStages = pStages;
|
||||
pipelineInfo.pVertexInputState = &vertexInputState;
|
||||
pipelineInfo.pInputAssemblyState = &inputAssemblyState;
|
||||
pipelineInfo.pTessellationState = null;
|
||||
pipelineInfo.pViewportState = &viewportState;
|
||||
pipelineInfo.pRasterizationState = &rasterizationState;
|
||||
pipelineInfo.pMultisampleState = &multisampleState;
|
||||
pipelineInfo.pDepthStencilState = &depthStencilState;
|
||||
pipelineInfo.pColorBlendState = &colorBlendState;
|
||||
pipelineInfo.pDynamicState = null;
|
||||
pipelineInfo.layout = pipeLayout;
|
||||
pipelineInfo.renderPass = renderPass;
|
||||
pipelineInfo.subpass = 0;
|
||||
pipelineInfo.basePipelineHandle = default;
|
||||
pipelineInfo.basePipelineIndex = -1;
|
||||
|
||||
VkPipeline pipe;
|
||||
result = Vk.vkCreateGraphicsPipelines(_ctx.Device, 0, 1, &pipelineInfo, null, &pipe);
|
||||
Vk.CheckResult(result, "vkCreateGraphicsPipelines");
|
||||
Pipeline = pipe;
|
||||
}
|
||||
|
||||
Vk.FreeUtf8(mainName);
|
||||
|
||||
Console.WriteLine("[Vulkan] Graphics pipeline created.");
|
||||
}
|
||||
|
||||
private VkShaderModule CreateShaderModule(string path)
|
||||
{
|
||||
var fullPath = Path.Combine(AppContext.BaseDirectory, path);
|
||||
if (!File.Exists(fullPath))
|
||||
throw new FileNotFoundException($"SPIR-V shader not found: {fullPath}");
|
||||
|
||||
var code = File.ReadAllBytes(fullPath);
|
||||
var codeSize = (ulong)code.Length;
|
||||
|
||||
fixed (byte* pCode = code)
|
||||
{
|
||||
VkShaderModuleCreateInfo createInfo;
|
||||
createInfo.sType = VkStructureType.ShaderModuleCreateInfo;
|
||||
createInfo.pNext = null;
|
||||
createInfo.flags = 0;
|
||||
createInfo.codeSize = codeSize;
|
||||
createInfo.pCode = (uint*)pCode;
|
||||
|
||||
VkShaderModule module;
|
||||
var result = Vk.vkCreateShaderModule(_ctx.Device, &createInfo, null, &module);
|
||||
Vk.CheckResult(result, $"vkCreateShaderModule ({path})");
|
||||
return module;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
if (Pipeline.Value != 0) Vk.vkDestroyPipeline(_ctx.Device, Pipeline, null);
|
||||
if (PipelineLayout.Value != 0) Vk.vkDestroyPipelineLayout(_ctx.Device, PipelineLayout, null);
|
||||
if (DescriptorSetLayout.Value != 0) Vk.vkDestroyDescriptorSetLayout(_ctx.Device, DescriptorSetLayout, null);
|
||||
if (VertexShader.Value != 0) Vk.vkDestroyShaderModule(_ctx.Device, VertexShader, null);
|
||||
if (FragmentShader.Value != 0) Vk.vkDestroyShaderModule(_ctx.Device, FragmentShader, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using Engine.Core;
|
||||
using Engine.Graphics;
|
||||
|
||||
namespace Engine.Graphics.Vulkan;
|
||||
|
||||
public sealed class VulkanRenderContext : IRenderContext
|
||||
{
|
||||
private readonly VulkanContext _context;
|
||||
private readonly VulkanSwapchain _swapchain;
|
||||
private readonly VulkanPipeline _pipeline;
|
||||
private readonly VulkanRenderer _renderer;
|
||||
private readonly Sdl3Window _window;
|
||||
|
||||
public IWindow Window => _window;
|
||||
|
||||
public VulkanRenderContext(int width, int height, bool enableValidation)
|
||||
{
|
||||
_window = new Sdl3Window("Cortex Engine", width, height, vulkanSurface: true);
|
||||
_context = new VulkanContext(_window, enableValidation);
|
||||
_swapchain = new VulkanSwapchain(_context, width, height);
|
||||
_pipeline = new VulkanPipeline(_context, _swapchain.RenderPass);
|
||||
_renderer = new VulkanRenderer(_context, _swapchain, _pipeline);
|
||||
}
|
||||
|
||||
public IRenderer CreateRenderer() => _renderer;
|
||||
|
||||
public void Resize(int width, int height)
|
||||
{
|
||||
_renderer.OnResize();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_renderer.Dispose();
|
||||
_pipeline.Dispose();
|
||||
_swapchain.Dispose();
|
||||
_context.Dispose();
|
||||
_window.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,536 @@
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using Engine.Core;
|
||||
using Engine.Core.Components;
|
||||
using Engine.Graphics;
|
||||
using Flecs.NET.Core;
|
||||
|
||||
namespace Engine.Graphics.Vulkan;
|
||||
|
||||
internal sealed unsafe class VulkanRenderer : IRenderer, IScreenshotProvider
|
||||
{
|
||||
private readonly VulkanContext _ctx;
|
||||
private readonly VulkanSwapchain _swapchain;
|
||||
private readonly VulkanPipeline _pipeline;
|
||||
|
||||
private VkCommandPool _commandPool;
|
||||
private VkCommandBuffer[] _commandBuffers = Array.Empty<VkCommandBuffer>();
|
||||
private VkSemaphore[] _imageAvailableSemaphores = Array.Empty<VkSemaphore>();
|
||||
private VkSemaphore[] _renderFinishedSemaphores = Array.Empty<VkSemaphore>();
|
||||
private VkFence[] _inFlightFences = Array.Empty<VkFence>();
|
||||
|
||||
private VkDescriptorPool _descriptorPool;
|
||||
private VkDescriptorSet[] _descriptorSets = Array.Empty<VkDescriptorSet>();
|
||||
private VulkanBuffer[] _uboBuffers = Array.Empty<VulkanBuffer>();
|
||||
|
||||
private const int MaxFramesInFlight = 2;
|
||||
private int _currentFrame;
|
||||
private uint _imageIndex;
|
||||
private bool _resized;
|
||||
|
||||
private readonly Dictionary<ulong, (VulkanBuffer vertex, VulkanBuffer index, uint indexCount)> _meshCache = new();
|
||||
|
||||
private bool _screenshotRequested;
|
||||
private string _screenshotPath = "";
|
||||
private TaskCompletionSource<byte[]>? _screenshotTcs;
|
||||
private byte[]? _screenshotData;
|
||||
private int _screenshotWidth;
|
||||
private int _screenshotHeight;
|
||||
|
||||
public bool IsScreenshotRequested => _screenshotRequested;
|
||||
public IScreenshotProvider ScreenshotProvider => this;
|
||||
|
||||
public VulkanRenderer(VulkanContext ctx, VulkanSwapchain swapchain, VulkanPipeline pipeline)
|
||||
{
|
||||
_ctx = ctx;
|
||||
_swapchain = swapchain;
|
||||
_pipeline = pipeline;
|
||||
|
||||
CreateCommandPool();
|
||||
CreateSyncObjects();
|
||||
CreateDescriptorPool();
|
||||
CreateDescriptorSets();
|
||||
CreateCommandBuffers();
|
||||
}
|
||||
|
||||
private unsafe void CreateCommandPool()
|
||||
{
|
||||
VkCommandPoolCreateInfo createInfo;
|
||||
createInfo.sType = VkStructureType.CommandPoolCreateInfo;
|
||||
createInfo.pNext = null;
|
||||
createInfo.flags = 0x00000002;
|
||||
createInfo.queueFamilyIndex = _ctx.GraphicsFamily;
|
||||
|
||||
VkCommandPool pool;
|
||||
VkResult result = Vk.vkCreateCommandPool(_ctx.Device, &createInfo, null, &pool);
|
||||
Vk.CheckResult(result, "vkCreateCommandPool");
|
||||
_commandPool = pool;
|
||||
}
|
||||
|
||||
private unsafe void CreateSyncObjects()
|
||||
{
|
||||
_imageAvailableSemaphores = new VkSemaphore[MaxFramesInFlight];
|
||||
_renderFinishedSemaphores = new VkSemaphore[MaxFramesInFlight];
|
||||
_inFlightFences = new VkFence[MaxFramesInFlight];
|
||||
|
||||
for (var i = 0; i < MaxFramesInFlight; i++)
|
||||
{
|
||||
VkSemaphoreCreateInfo semInfo;
|
||||
semInfo.sType = VkStructureType.SemaphoreCreateInfo;
|
||||
semInfo.pNext = null;
|
||||
semInfo.flags = 0;
|
||||
|
||||
VkSemaphore sem1, sem2;
|
||||
Vk.CheckResult(Vk.vkCreateSemaphore(_ctx.Device, &semInfo, null, &sem1), "vkCreateSemaphore");
|
||||
Vk.CheckResult(Vk.vkCreateSemaphore(_ctx.Device, &semInfo, null, &sem2), "vkCreateSemaphore");
|
||||
_imageAvailableSemaphores[i] = sem1;
|
||||
_renderFinishedSemaphores[i] = sem2;
|
||||
|
||||
VkFenceCreateInfo fenceInfo;
|
||||
fenceInfo.sType = VkStructureType.FenceCreateInfo;
|
||||
fenceInfo.pNext = null;
|
||||
fenceInfo.flags = VkFenceCreateFlags.Signaled;
|
||||
|
||||
VkFence fence;
|
||||
Vk.CheckResult(Vk.vkCreateFence(_ctx.Device, &fenceInfo, null, &fence), "vkCreateFence");
|
||||
_inFlightFences[i] = fence;
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void CreateDescriptorPool()
|
||||
{
|
||||
var poolSize = new VkDescriptorPoolSize
|
||||
{
|
||||
type = VkDescriptorType.UniformBuffer,
|
||||
descriptorCount = (uint)MaxFramesInFlight
|
||||
};
|
||||
|
||||
VkDescriptorPoolCreateInfo createInfo;
|
||||
createInfo.sType = VkStructureType.DescriptorPoolCreateInfo;
|
||||
createInfo.pNext = null;
|
||||
createInfo.flags = VkDescriptorPoolCreateFlags.FreeDescriptorSet;
|
||||
createInfo.maxSets = (uint)MaxFramesInFlight;
|
||||
createInfo.poolSizeCount = 1;
|
||||
createInfo.pPoolSizes = &poolSize;
|
||||
|
||||
VkDescriptorPool pool;
|
||||
Vk.CheckResult(Vk.vkCreateDescriptorPool(_ctx.Device, &createInfo, null, &pool), "vkCreateDescriptorPool");
|
||||
_descriptorPool = pool;
|
||||
}
|
||||
|
||||
private unsafe void CreateDescriptorSets()
|
||||
{
|
||||
_uboBuffers = new VulkanBuffer[MaxFramesInFlight];
|
||||
_descriptorSets = new VkDescriptorSet[MaxFramesInFlight];
|
||||
|
||||
var layouts = new VkDescriptorSetLayout[MaxFramesInFlight];
|
||||
for (var i = 0; i < MaxFramesInFlight; i++)
|
||||
layouts[i] = _pipeline.DescriptorSetLayout;
|
||||
|
||||
VkDescriptorSetAllocateInfo allocInfo;
|
||||
allocInfo.sType = VkStructureType.DescriptorSetAllocateInfo;
|
||||
allocInfo.pNext = null;
|
||||
allocInfo.descriptorPool = _descriptorPool;
|
||||
allocInfo.descriptorSetCount = (uint)MaxFramesInFlight;
|
||||
|
||||
fixed (VkDescriptorSetLayout* pLayouts = layouts)
|
||||
{
|
||||
allocInfo.pSetLayouts = pLayouts;
|
||||
|
||||
fixed (VkDescriptorSet* pSets = _descriptorSets)
|
||||
{
|
||||
Vk.CheckResult(Vk.vkAllocateDescriptorSets(_ctx.Device, &allocInfo, pSets), "vkAllocateDescriptorSets");
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < MaxFramesInFlight; i++)
|
||||
{
|
||||
_uboBuffers[i] = new VulkanBuffer(_ctx, (ulong)VulkanPipeline.FrameUboSize,
|
||||
VkBufferUsageFlags.UniformBuffer,
|
||||
VkMemoryPropertyFlags.HostVisible | VkMemoryPropertyFlags.HostCoherent);
|
||||
|
||||
var bufferInfo = new VkDescriptorBufferInfo
|
||||
{
|
||||
buffer = _uboBuffers[i].Buffer,
|
||||
offset = 0,
|
||||
range = (ulong)VulkanPipeline.FrameUboSize
|
||||
};
|
||||
|
||||
VkWriteDescriptorSet writeInfo;
|
||||
writeInfo.sType = VkStructureType.WriteDescriptorSet;
|
||||
writeInfo.pNext = null;
|
||||
writeInfo.dstSet = _descriptorSets[i];
|
||||
writeInfo.dstBinding = 0;
|
||||
writeInfo.dstArrayElement = 0;
|
||||
writeInfo.descriptorCount = 1;
|
||||
writeInfo.descriptorType = VkDescriptorType.UniformBuffer;
|
||||
writeInfo.pImageInfo = null;
|
||||
writeInfo.pBufferInfo = &bufferInfo;
|
||||
writeInfo.pTexelBufferView = null;
|
||||
|
||||
Vk.vkUpdateDescriptorSets(_ctx.Device, 1, &writeInfo, 0, null);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void CreateCommandBuffers()
|
||||
{
|
||||
_commandBuffers = new VkCommandBuffer[MaxFramesInFlight];
|
||||
|
||||
VkCommandBufferAllocateInfo allocInfo;
|
||||
allocInfo.sType = VkStructureType.CommandBufferAllocateInfo;
|
||||
allocInfo.pNext = null;
|
||||
allocInfo.commandPool = _commandPool;
|
||||
allocInfo.level = VkCommandBufferLevel.Primary;
|
||||
allocInfo.commandBufferCount = (uint)MaxFramesInFlight;
|
||||
|
||||
fixed (VkCommandBuffer* pCmds = _commandBuffers)
|
||||
{
|
||||
Vk.CheckResult(Vk.vkAllocateCommandBuffers(_ctx.Device, &allocInfo, pCmds), "vkAllocateCommandBuffers");
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe void RenderWorld(World world)
|
||||
{
|
||||
VkFence fence = _inFlightFences[_currentFrame];
|
||||
Vk.CheckResult(Vk.vkWaitForFences(_ctx.Device, 1, &fence, 1, ulong.MaxValue), "vkWaitForFences");
|
||||
|
||||
uint imageIndex = 0;
|
||||
VkSemaphore imgAvailSem = _imageAvailableSemaphores[_currentFrame];
|
||||
var acquireResult = Vk.vkAcquireNextImageKHR(_ctx.Device, _swapchain.Swapchain, ulong.MaxValue,
|
||||
imgAvailSem, default, &imageIndex);
|
||||
|
||||
if (acquireResult == VkResult.ErrorOutOfDateKHR || _resized)
|
||||
{
|
||||
_resized = false;
|
||||
_swapchain.Recreate(_swapchain.Extent.width, _swapchain.Extent.height);
|
||||
RecreateCommandBuffers();
|
||||
return;
|
||||
}
|
||||
Vk.CheckResult(acquireResult, "vkAcquireNextImageKHR");
|
||||
|
||||
_imageIndex = imageIndex;
|
||||
|
||||
VkFence fenceReset = _inFlightFences[_currentFrame];
|
||||
Vk.CheckResult(Vk.vkResetFences(_ctx.Device, 1, &fenceReset), "vkResetFences");
|
||||
|
||||
var cmd = _commandBuffers[_currentFrame];
|
||||
Vk.CheckResult(Vk.vkResetCommandBuffer(cmd, 0), "vkResetCommandBuffer");
|
||||
|
||||
UpdateFrameUbo(world);
|
||||
|
||||
RecordCommandBuffer(cmd, imageIndex, world);
|
||||
|
||||
VkSemaphore renderDoneSem = _renderFinishedSemaphores[_currentFrame];
|
||||
VkSemaphore imgAvailSem2 = _imageAvailableSemaphores[_currentFrame];
|
||||
VkFence submitFence = _inFlightFences[_currentFrame];
|
||||
|
||||
VkSubmitInfo submitInfo;
|
||||
submitInfo.sType = VkStructureType.SubmitInfo;
|
||||
submitInfo.pNext = null;
|
||||
submitInfo.waitSemaphoreCount = 1;
|
||||
submitInfo.pWaitSemaphores = &imgAvailSem2;
|
||||
|
||||
var waitStage = (ulong)VkPipelineStageFlags.ColorAttachmentOutput;
|
||||
submitInfo.pWaitDstStageMask = &waitStage;
|
||||
submitInfo.commandBufferCount = 1;
|
||||
submitInfo.pCommandBuffers = &cmd;
|
||||
submitInfo.signalSemaphoreCount = 1;
|
||||
submitInfo.pSignalSemaphores = &renderDoneSem;
|
||||
|
||||
Vk.CheckResult(Vk.vkQueueSubmit(_ctx.GraphicsQueue, 1, &submitInfo, submitFence), "vkQueueSubmit");
|
||||
|
||||
VkSemaphore renderDoneSem2 = _renderFinishedSemaphores[_currentFrame];
|
||||
VkSwapchainKHR swapchain = _swapchain.Swapchain;
|
||||
|
||||
VkPresentInfoKHR presentInfo;
|
||||
presentInfo.sType = VkStructureType.PresentInfoKHR;
|
||||
presentInfo.pNext = null;
|
||||
presentInfo.waitSemaphoreCount = 1;
|
||||
presentInfo.pWaitSemaphores = &renderDoneSem2;
|
||||
presentInfo.swapchainCount = 1;
|
||||
presentInfo.pSwapchains = &swapchain;
|
||||
presentInfo.pImageIndices = &imageIndex;
|
||||
presentInfo.pResults = null;
|
||||
|
||||
var presentResult = Vk.vkQueuePresentKHR(_ctx.GraphicsQueue, &presentInfo);
|
||||
if (presentResult == VkResult.ErrorOutOfDateKHR || presentResult == VkResult.SuboptimalKHR || _resized)
|
||||
{
|
||||
_resized = false;
|
||||
_swapchain.Recreate(_swapchain.Extent.width, _swapchain.Extent.height);
|
||||
RecreateCommandBuffers();
|
||||
}
|
||||
else
|
||||
{
|
||||
Vk.CheckResult(presentResult, "vkQueuePresentKHR");
|
||||
}
|
||||
|
||||
_currentFrame = (_currentFrame + 1) % MaxFramesInFlight;
|
||||
|
||||
if (_screenshotRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dir = Path.GetDirectoryName(_screenshotPath);
|
||||
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
File.WriteAllText(_screenshotPath, "Vulkan screenshot placeholder");
|
||||
Console.WriteLine($"Screenshot saved: {_screenshotPath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Screenshot save failed: {ex.Message}");
|
||||
}
|
||||
_screenshotRequested = false;
|
||||
_screenshotTcs?.TrySetResult(Array.Empty<byte>());
|
||||
_screenshotTcs = null;
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void UpdateFrameUbo(World world)
|
||||
{
|
||||
Vector3 camPos = Vector3.Zero;
|
||||
var view = Matrix4x4.Identity;
|
||||
var proj = Matrix4x4.Identity;
|
||||
|
||||
world.Each((Entity e, ref Camera cam) =>
|
||||
{
|
||||
camPos = cam.Position;
|
||||
view = cam.GetViewMatrix();
|
||||
proj = cam.GetProjectionMatrix();
|
||||
});
|
||||
|
||||
var lights = new List<(Light light, Transform transform)>();
|
||||
world.Each((Entity e, ref Light light, ref Transform transform) =>
|
||||
{
|
||||
lights.Add((light, transform));
|
||||
});
|
||||
|
||||
var uboData = new byte[VulkanPipeline.FrameUboSize];
|
||||
fixed (byte* pUbo = uboData)
|
||||
{
|
||||
var p = (float*)pUbo;
|
||||
|
||||
p[0] = camPos.X; p[1] = camPos.Y; p[2] = camPos.Z;
|
||||
p[3] = (uint)Math.Min(lights.Count, 16);
|
||||
|
||||
p[4] = 0.15f; p[5] = 0.15f; p[6] = 0.2f; p[7] = 0f;
|
||||
|
||||
for (var i = 0; i < Math.Min(lights.Count, 16); i++)
|
||||
{
|
||||
var (light, _) = lights[i];
|
||||
var baseIdx = 8 + i * 8;
|
||||
|
||||
if (light.IsDirectional)
|
||||
{
|
||||
p[baseIdx + 0] = light.Direction.X;
|
||||
p[baseIdx + 1] = light.Direction.Y;
|
||||
p[baseIdx + 2] = light.Direction.Z;
|
||||
p[baseIdx + 3] = -light.Intensity;
|
||||
|
||||
p[baseIdx + 4] = light.Color.X;
|
||||
p[baseIdx + 5] = light.Color.Y;
|
||||
p[baseIdx + 6] = light.Color.Z;
|
||||
p[baseIdx + 7] = 0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
p[baseIdx + 0] = light.Position.X;
|
||||
p[baseIdx + 1] = light.Position.Y;
|
||||
p[baseIdx + 2] = light.Position.Z;
|
||||
p[baseIdx + 3] = light.Intensity;
|
||||
|
||||
p[baseIdx + 4] = light.Color.X;
|
||||
p[baseIdx + 5] = light.Color.Y;
|
||||
p[baseIdx + 6] = light.Color.Z;
|
||||
p[baseIdx + 7] = light.Range;
|
||||
}
|
||||
}
|
||||
|
||||
_uboBuffers[_currentFrame].Write(pUbo, (ulong)VulkanPipeline.FrameUboSize);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void RecordCommandBuffer(VkCommandBuffer cmd, uint imageIndex, World world)
|
||||
{
|
||||
VkCommandBufferBeginInfo beginInfo;
|
||||
beginInfo.sType = VkStructureType.CommandBufferBeginInfo;
|
||||
beginInfo.pNext = null;
|
||||
beginInfo.flags = 0;
|
||||
beginInfo.pInheritanceInfo = null;
|
||||
|
||||
Vk.CheckResult(Vk.vkBeginCommandBuffer(cmd, &beginInfo), "vkBeginCommandBuffer");
|
||||
|
||||
VkRenderPassBeginInfo rpBegin;
|
||||
rpBegin.sType = VkStructureType.RenderPassBeginInfo;
|
||||
rpBegin.pNext = null;
|
||||
rpBegin.renderPass = _swapchain.RenderPass;
|
||||
rpBegin.framebuffer = _swapchain.Framebuffers[imageIndex];
|
||||
rpBegin.renderArea = new VkRect2D
|
||||
{
|
||||
offset = new VkOffset2D { x = 0, y = 0 },
|
||||
extent = _swapchain.Extent
|
||||
};
|
||||
|
||||
var clearValues = new VkClearValue[2];
|
||||
clearValues[0] = new VkClearValue { color = new VkClearColorValue { r = 0.05f, g = 0.05f, b = 0.08f, a = 1.0f } };
|
||||
clearValues[1] = new VkClearValue { depthStencil = new VkClearDepthStencilValue { depth = 1.0f, stencil = 0 } };
|
||||
|
||||
fixed (VkClearValue* pClear = clearValues)
|
||||
{
|
||||
rpBegin.clearValueCount = 2;
|
||||
rpBegin.pClearValues = pClear;
|
||||
|
||||
Vk.vkCmdBeginRenderPass(cmd, &rpBegin, VkSubpassContents.Inline);
|
||||
}
|
||||
|
||||
Vk.vkCmdBindPipeline(cmd, 0, _pipeline.Pipeline);
|
||||
|
||||
var viewport = new VkViewport
|
||||
{
|
||||
x = 0, y = 0,
|
||||
width = _swapchain.Extent.width,
|
||||
height = _swapchain.Extent.height,
|
||||
minDepth = 0, maxDepth = 1
|
||||
};
|
||||
Vk.vkCmdSetViewport(cmd, 0, 1, &viewport);
|
||||
|
||||
var scissor = new VkRect2D
|
||||
{
|
||||
offset = new VkOffset2D { x = 0, y = 0 },
|
||||
extent = _swapchain.Extent
|
||||
};
|
||||
Vk.vkCmdSetScissor(cmd, 0, 1, &scissor);
|
||||
|
||||
VkDescriptorSet ds = _descriptorSets[_currentFrame];
|
||||
Vk.vkCmdBindDescriptorSets(cmd, 0, _pipeline.PipelineLayout, 0, 1, &ds, 0, null);
|
||||
|
||||
world.Each((Entity e, ref Transform transform, ref Mesh mesh, ref Material material) =>
|
||||
{
|
||||
var meshKey = (ulong)mesh.GetHashCode();
|
||||
if (!_meshCache.TryGetValue(meshKey, out var meshBuffers))
|
||||
{
|
||||
if (mesh.Vertices.Length == 0 || mesh.Indices.Length == 0) return;
|
||||
|
||||
var vertexBuffer = VulkanBuffer.CreateDeviceLocal(_ctx, _commandPool, mesh.Vertices, VkBufferUsageFlags.VertexBuffer);
|
||||
var indexBuffer = VulkanBuffer.CreateDeviceLocal(_ctx, _commandPool, mesh.Indices, VkBufferUsageFlags.IndexBuffer);
|
||||
|
||||
meshBuffers = (vertexBuffer, indexBuffer, (uint)mesh.Indices.Length);
|
||||
_meshCache[meshKey] = meshBuffers;
|
||||
}
|
||||
|
||||
var model = transform.GetMatrix();
|
||||
var view = Matrix4x4.Identity;
|
||||
var proj = Matrix4x4.Identity;
|
||||
|
||||
world.Each((Entity camE, ref Camera cam) =>
|
||||
{
|
||||
view = cam.GetViewMatrix();
|
||||
proj = cam.GetProjectionMatrix();
|
||||
});
|
||||
|
||||
var mvp = model * view * proj;
|
||||
|
||||
var pushData = new byte[VulkanPipeline.PushConstantSize];
|
||||
fixed (byte* pPush = pushData)
|
||||
{
|
||||
var p = (float*)pPush;
|
||||
|
||||
p[0] = mvp.M11; p[1] = mvp.M12; p[2] = mvp.M13; p[3] = mvp.M14;
|
||||
p[4] = mvp.M21; p[5] = mvp.M22; p[6] = mvp.M23; p[7] = mvp.M24;
|
||||
p[8] = mvp.M31; p[9] = mvp.M32; p[10] = mvp.M33; p[11] = mvp.M34;
|
||||
p[12] = mvp.M41; p[13] = mvp.M42; p[14] = mvp.M43; p[15] = mvp.M44;
|
||||
|
||||
p[16] = model.M11; p[17] = model.M12; p[18] = model.M13; p[19] = model.M14;
|
||||
p[20] = model.M21; p[21] = model.M22; p[22] = model.M23; p[23] = model.M24;
|
||||
p[24] = model.M31; p[25] = model.M32; p[26] = model.M33; p[27] = model.M34;
|
||||
p[28] = model.M41; p[29] = model.M42; p[30] = model.M43; p[31] = model.M44;
|
||||
|
||||
p[32] = material.Albedo.X;
|
||||
p[33] = material.Albedo.Y;
|
||||
p[34] = material.Albedo.Z;
|
||||
p[35] = material.Roughness;
|
||||
|
||||
var buf = meshBuffers.vertex.Buffer;
|
||||
var offset = 0ul;
|
||||
Vk.vkCmdBindVertexBuffers(cmd, 0, 1, &buf, &offset);
|
||||
Vk.vkCmdBindIndexBuffer(cmd, meshBuffers.index.Buffer, 0, VkIndexType.Uint32);
|
||||
|
||||
Vk.vkCmdPushConstants(cmd, _pipeline.PipelineLayout,
|
||||
VkShaderStageFlags.Vertex | VkShaderStageFlags.Fragment, 0,
|
||||
(uint)VulkanPipeline.PushConstantSize, pPush);
|
||||
|
||||
Vk.vkCmdDrawIndexed(cmd, meshBuffers.indexCount, 1, 0, 0, 0);
|
||||
}
|
||||
});
|
||||
|
||||
Vk.vkCmdEndRenderPass(cmd);
|
||||
Vk.CheckResult(Vk.vkEndCommandBuffer(cmd), "vkEndCommandBuffer");
|
||||
}
|
||||
|
||||
private unsafe void RecreateCommandBuffers()
|
||||
{
|
||||
fixed (VkCommandBuffer* pCmds = _commandBuffers)
|
||||
{
|
||||
Vk.vkFreeCommandBuffers(_ctx.Device, _commandPool, (uint)_commandBuffers.Length, pCmds);
|
||||
}
|
||||
|
||||
VkCommandBufferAllocateInfo allocInfo;
|
||||
allocInfo.sType = VkStructureType.CommandBufferAllocateInfo;
|
||||
allocInfo.pNext = null;
|
||||
allocInfo.commandPool = _commandPool;
|
||||
allocInfo.level = VkCommandBufferLevel.Primary;
|
||||
allocInfo.commandBufferCount = (uint)_commandBuffers.Length;
|
||||
|
||||
fixed (VkCommandBuffer* pCmds = _commandBuffers)
|
||||
{
|
||||
Vk.CheckResult(Vk.vkAllocateCommandBuffers(_ctx.Device, &allocInfo, pCmds), "vkAllocateCommandBuffers (recreate)");
|
||||
}
|
||||
}
|
||||
|
||||
public void RequestScreenshot(string outputPath)
|
||||
{
|
||||
_screenshotPath = outputPath;
|
||||
_screenshotRequested = true;
|
||||
}
|
||||
|
||||
public Task<byte[]> CaptureAsync(string outputPath)
|
||||
{
|
||||
_screenshotPath = outputPath;
|
||||
_screenshotTcs = new TaskCompletionSource<byte[]>();
|
||||
_screenshotRequested = true;
|
||||
return _screenshotTcs.Task;
|
||||
}
|
||||
public void OnResize() => _resized = true;
|
||||
|
||||
public unsafe void Dispose()
|
||||
{
|
||||
Vk.vkQueueWaitIdle(_ctx.GraphicsQueue);
|
||||
|
||||
foreach (var mesh in _meshCache.Values)
|
||||
{
|
||||
mesh.vertex.Dispose();
|
||||
mesh.index.Dispose();
|
||||
}
|
||||
_meshCache.Clear();
|
||||
|
||||
foreach (var ubo in _uboBuffers)
|
||||
ubo?.Dispose();
|
||||
|
||||
if (_descriptorPool.Value != 0)
|
||||
Vk.vkDestroyDescriptorPool(_ctx.Device, _descriptorPool, null);
|
||||
|
||||
fixed (VkCommandBuffer* pCmds = _commandBuffers)
|
||||
{
|
||||
if (_commandPool.Value != 0 && _commandBuffers.Length > 0)
|
||||
Vk.vkFreeCommandBuffers(_ctx.Device, _commandPool, (uint)_commandBuffers.Length, pCmds);
|
||||
}
|
||||
if (_commandPool.Value != 0) Vk.vkDestroyCommandPool(_ctx.Device, _commandPool, null);
|
||||
|
||||
for (var i = 0; i < MaxFramesInFlight; i++)
|
||||
{
|
||||
if (_imageAvailableSemaphores[i].Value != 0) Vk.vkDestroySemaphore(_ctx.Device, _imageAvailableSemaphores[i], null);
|
||||
if (_renderFinishedSemaphores[i].Value != 0) Vk.vkDestroySemaphore(_ctx.Device, _renderFinishedSemaphores[i], null);
|
||||
if (_inFlightFences[i].Value != 0) Vk.vkDestroyFence(_ctx.Device, _inFlightFences[i], null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Engine.Graphics.Vulkan;
|
||||
|
||||
internal sealed unsafe class VulkanSwapchain : IDisposable
|
||||
{
|
||||
public VkSwapchainKHR Swapchain;
|
||||
public VkImage[] SwapchainImages = Array.Empty<VkImage>();
|
||||
public VkImageView[] SwapchainImageViews = Array.Empty<VkImageView>();
|
||||
public VkFormat ImageFormat;
|
||||
public VkFormat DepthFormat;
|
||||
public VkExtent2D Extent;
|
||||
public VkRenderPass RenderPass;
|
||||
public VkFramebuffer[] Framebuffers = Array.Empty<VkFramebuffer>();
|
||||
|
||||
public VkImage DepthImage;
|
||||
public VkDeviceMemory DepthImageMemory;
|
||||
public VkImageView DepthImageView;
|
||||
|
||||
private readonly VulkanContext _ctx;
|
||||
private bool _disposed;
|
||||
|
||||
public VulkanSwapchain(VulkanContext ctx, int width, int height)
|
||||
{
|
||||
_ctx = ctx;
|
||||
Create(width, height);
|
||||
}
|
||||
|
||||
public unsafe void Create(int width, int height)
|
||||
{
|
||||
VkSurfaceCapabilitiesKHR caps;
|
||||
Vk.vkGetPhysicalDeviceSurfaceCapabilitiesKHR(_ctx.PhysicalDevice, _ctx.Surface, &caps);
|
||||
|
||||
uint formatCount = 0;
|
||||
Vk.vkGetPhysicalDeviceSurfaceFormatsKHR(_ctx.PhysicalDevice, _ctx.Surface, &formatCount, null);
|
||||
var formats = new VkSurfaceFormatKHR[formatCount];
|
||||
fixed (VkSurfaceFormatKHR* pFormats = formats)
|
||||
{
|
||||
Vk.vkGetPhysicalDeviceSurfaceFormatsKHR(_ctx.PhysicalDevice, _ctx.Surface, &formatCount, pFormats);
|
||||
}
|
||||
|
||||
ImageFormat = formats[0].format;
|
||||
foreach (var f in formats)
|
||||
{
|
||||
if (f.format == VkFormat.B8G8R8A8Srgb && f.colorSpace == VkColorSpaceKHR.SrgbNonlinear)
|
||||
{
|
||||
ImageFormat = f.format;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ImageFormat == VkFormat.Undefined)
|
||||
ImageFormat = VkFormat.B8G8R8A8Unorm;
|
||||
|
||||
Extent = caps.currentExtent;
|
||||
if (Extent.width == int.MaxValue || Extent.height == int.MaxValue || Extent.width <= 0 || Extent.height <= 0)
|
||||
{
|
||||
Extent.width = Math.Clamp(width, caps.minImageExtent.width, caps.maxImageExtent.width);
|
||||
Extent.height = Math.Clamp(height, caps.minImageExtent.height, caps.maxImageExtent.height);
|
||||
}
|
||||
|
||||
uint imageCount = caps.minImageCount + 1;
|
||||
if (caps.maxImageCount > 0 && imageCount > caps.maxImageCount)
|
||||
imageCount = caps.maxImageCount;
|
||||
|
||||
VkSwapchainCreateInfoKHR createInfo;
|
||||
createInfo.sType = VkStructureType.SwapchainCreateInfoKHR;
|
||||
createInfo.pNext = null;
|
||||
createInfo.flags = 0;
|
||||
createInfo.surface = _ctx.Surface;
|
||||
createInfo.minImageCount = imageCount;
|
||||
createInfo.imageFormat = ImageFormat;
|
||||
createInfo.imageColorSpace = VkColorSpaceKHR.SrgbNonlinear;
|
||||
createInfo.imageExtent = Extent;
|
||||
createInfo.imageArrayLayers = 1;
|
||||
createInfo.imageUsage = VkImageUsageFlags.ColorAttachment | VkImageUsageFlags.TransferSrc;
|
||||
createInfo.imageSharingMode = VkSharingMode.Exclusive;
|
||||
createInfo.queueFamilyIndexCount = 0;
|
||||
createInfo.pQueueFamilyIndices = null;
|
||||
createInfo.preTransform = caps.currentTransform;
|
||||
createInfo.compositeAlpha = 0x00000001;
|
||||
createInfo.presentMode = VkPresentModeKHR.Fifo;
|
||||
createInfo.clipped = 1;
|
||||
createInfo.oldSwapchain = default;
|
||||
|
||||
VkSwapchainKHR swapchain;
|
||||
VkResult result = Vk.vkCreateSwapchainKHR(_ctx.Device, &createInfo, null, &swapchain);
|
||||
Vk.CheckResult(result, "vkCreateSwapchainKHR");
|
||||
Swapchain = swapchain;
|
||||
|
||||
uint actualCount = 0;
|
||||
Vk.vkGetSwapchainImagesKHR(_ctx.Device, Swapchain, &actualCount, null);
|
||||
SwapchainImages = new VkImage[actualCount];
|
||||
fixed (VkImage* pImages = SwapchainImages)
|
||||
{
|
||||
Vk.vkGetSwapchainImagesKHR(_ctx.Device, Swapchain, &actualCount, pImages);
|
||||
}
|
||||
|
||||
SwapchainImageViews = new VkImageView[actualCount];
|
||||
for (uint i = 0; i < actualCount; i++)
|
||||
{
|
||||
VkImageViewCreateInfo viewInfo;
|
||||
viewInfo.sType = VkStructureType.ImageViewCreateInfo;
|
||||
viewInfo.pNext = null;
|
||||
viewInfo.flags = 0;
|
||||
viewInfo.image = SwapchainImages[i];
|
||||
viewInfo.viewType = VkImageViewType._2D;
|
||||
viewInfo.format = ImageFormat;
|
||||
viewInfo.components = new VkComponentMapping { r = 0, g = 0, b = 0, a = 0 };
|
||||
viewInfo.subresourceRange = new VkImageSubresourceRange
|
||||
{
|
||||
aspectMask = VkImageAspectFlags.Color,
|
||||
baseMipLevel = 0,
|
||||
levelCount = 1,
|
||||
baseArrayLayer = 0,
|
||||
layerCount = 1
|
||||
};
|
||||
|
||||
VkImageView view;
|
||||
result = Vk.vkCreateImageView(_ctx.Device, &viewInfo, null, &view);
|
||||
Vk.CheckResult(result, "vkCreateImageView (swapchain)");
|
||||
SwapchainImageViews[i] = view;
|
||||
}
|
||||
|
||||
DepthFormat = VkFormat.D32Sfloat;
|
||||
CreateDepthImage();
|
||||
|
||||
CreateRenderPass();
|
||||
CreateFramebuffers();
|
||||
|
||||
Console.WriteLine($"[Vulkan] Swapchain: {actualCount} images, {Extent.width}x{Extent.height}, format {ImageFormat}");
|
||||
}
|
||||
|
||||
private unsafe void CreateDepthImage()
|
||||
{
|
||||
VkImageCreateInfo imageInfo;
|
||||
imageInfo.sType = VkStructureType.ImageCreateInfo;
|
||||
imageInfo.pNext = null;
|
||||
imageInfo.flags = 0;
|
||||
imageInfo.imageType = VkImageType._2D;
|
||||
imageInfo.format = DepthFormat;
|
||||
imageInfo.extent = new VkExtent3D { width = Extent.width, height = Extent.height, depth = 1 };
|
||||
imageInfo.mipLevels = 1;
|
||||
imageInfo.arrayLayers = 1;
|
||||
imageInfo.samples = VkSampleCountFlags.One;
|
||||
imageInfo.tiling = 0;
|
||||
imageInfo.usage = VkImageUsageFlags.DepthStencilAttachment;
|
||||
imageInfo.sharingMode = VkSharingMode.Exclusive;
|
||||
imageInfo.queueFamilyIndexCount = 0;
|
||||
imageInfo.pQueueFamilyIndices = null;
|
||||
imageInfo.initialLayout = 0;
|
||||
|
||||
VkImage depthImage;
|
||||
VkResult result = Vk.vkCreateImage(_ctx.Device, &imageInfo, null, &depthImage);
|
||||
Vk.CheckResult(result, "vkCreateImage (depth)");
|
||||
DepthImage = depthImage;
|
||||
|
||||
VkMemoryRequirements2 memReq;
|
||||
Vk.vkGetImageMemoryRequirements(_ctx.Device, DepthImage, &memReq);
|
||||
|
||||
VkMemoryAllocateInfo allocInfo;
|
||||
allocInfo.sType = VkStructureType.MemoryAllocateInfo;
|
||||
allocInfo.pNext = null;
|
||||
allocInfo.allocationSize = memReq.size;
|
||||
allocInfo.memoryTypeIndex = _ctx.FindMemoryType(memReq.memoryTypeBits, VkMemoryPropertyFlags.DeviceLocal);
|
||||
|
||||
VkDeviceMemory depthMem;
|
||||
result = Vk.vkAllocateMemory(_ctx.Device, &allocInfo, null, &depthMem);
|
||||
Vk.CheckResult(result, "vkAllocateMemory (depth)");
|
||||
DepthImageMemory = depthMem;
|
||||
|
||||
result = Vk.vkBindImageMemory(_ctx.Device, DepthImage, DepthImageMemory, 0);
|
||||
Vk.CheckResult(result, "vkBindImageMemory (depth)");
|
||||
|
||||
VkImageViewCreateInfo viewInfo;
|
||||
viewInfo.sType = VkStructureType.ImageViewCreateInfo;
|
||||
viewInfo.pNext = null;
|
||||
viewInfo.flags = 0;
|
||||
viewInfo.image = DepthImage;
|
||||
viewInfo.viewType = VkImageViewType._2D;
|
||||
viewInfo.format = DepthFormat;
|
||||
viewInfo.components = new VkComponentMapping { r = 0, g = 0, b = 0, a = 0 };
|
||||
viewInfo.subresourceRange = new VkImageSubresourceRange
|
||||
{
|
||||
aspectMask = VkImageAspectFlags.Depth,
|
||||
baseMipLevel = 0,
|
||||
levelCount = 1,
|
||||
baseArrayLayer = 0,
|
||||
layerCount = 1
|
||||
};
|
||||
|
||||
VkImageView depthView;
|
||||
result = Vk.vkCreateImageView(_ctx.Device, &viewInfo, null, &depthView);
|
||||
Vk.CheckResult(result, "vkCreateImageView (depth)");
|
||||
DepthImageView = depthView;
|
||||
}
|
||||
|
||||
private unsafe void CreateRenderPass()
|
||||
{
|
||||
var attachments = new VkAttachmentDescription[2];
|
||||
attachments[0] = new VkAttachmentDescription
|
||||
{
|
||||
flags = 0,
|
||||
format = ImageFormat,
|
||||
samples = (uint)VkSampleCountFlags.One,
|
||||
loadOp = VkAttachmentLoadOp.Clear,
|
||||
storeOp = VkAttachmentStoreOp.Store,
|
||||
stencilLoadOp = VkAttachmentLoadOp.DontCare,
|
||||
stencilStoreOp = VkAttachmentStoreOp.DontCare,
|
||||
initialLayout = VkImageLayout.Undefined,
|
||||
finalLayout = VkImageLayout.PresentSrcKHR
|
||||
};
|
||||
attachments[1] = new VkAttachmentDescription
|
||||
{
|
||||
flags = 0,
|
||||
format = DepthFormat,
|
||||
samples = (uint)VkSampleCountFlags.One,
|
||||
loadOp = VkAttachmentLoadOp.Clear,
|
||||
storeOp = VkAttachmentStoreOp.DontCare,
|
||||
stencilLoadOp = VkAttachmentLoadOp.DontCare,
|
||||
stencilStoreOp = VkAttachmentStoreOp.DontCare,
|
||||
initialLayout = VkImageLayout.Undefined,
|
||||
finalLayout = VkImageLayout.DepthStencilAttachmentOptimal
|
||||
};
|
||||
|
||||
var colorRef = new VkAttachmentReference { attachment = 0, layout = VkImageLayout.ColorAttachmentOptimal };
|
||||
var depthRef = new VkAttachmentReference { attachment = 1, layout = VkImageLayout.DepthStencilAttachmentOptimal };
|
||||
|
||||
VkSubpassDescription subpass;
|
||||
subpass.flags = 0;
|
||||
subpass.pipelineBindPoint = 0;
|
||||
subpass.inputAttachmentCount = 0;
|
||||
subpass.pInputAttachments = null;
|
||||
subpass.colorAttachmentCount = 1;
|
||||
subpass.pColorAttachments = &colorRef;
|
||||
subpass.pResolveAttachments = null;
|
||||
subpass.pDepthStencilAttachment = &depthRef;
|
||||
subpass.preserveAttachmentCount = 0;
|
||||
subpass.pPreserveAttachments = null;
|
||||
|
||||
var dependencies = new VkSubpassDependency[2];
|
||||
dependencies[0] = new VkSubpassDependency
|
||||
{
|
||||
srcSubpass = ~0u,
|
||||
dstSubpass = 0,
|
||||
srcStageMask = VkPipelineStageFlags.ColorAttachmentOutput | VkPipelineStageFlags.EarlyFragmentTests,
|
||||
dstStageMask = VkPipelineStageFlags.ColorAttachmentOutput | VkPipelineStageFlags.EarlyFragmentTests,
|
||||
srcAccessMask = 0,
|
||||
dstAccessMask = VkAccessFlags.ColorAttachmentWrite | VkAccessFlags.DepthStencilAttachmentWrite,
|
||||
dependencyFlags = 0,
|
||||
viewOffset = 0
|
||||
};
|
||||
dependencies[1] = new VkSubpassDependency
|
||||
{
|
||||
srcSubpass = 0,
|
||||
dstSubpass = ~0u,
|
||||
srcStageMask = VkPipelineStageFlags.ColorAttachmentOutput | VkPipelineStageFlags.EarlyFragmentTests,
|
||||
dstStageMask = VkPipelineStageFlags.BottomOfPipe,
|
||||
srcAccessMask = VkAccessFlags.ColorAttachmentWrite | VkAccessFlags.DepthStencilAttachmentWrite,
|
||||
dstAccessMask = 0,
|
||||
dependencyFlags = 0,
|
||||
viewOffset = 0
|
||||
};
|
||||
|
||||
fixed (VkAttachmentDescription* pAttachments = attachments)
|
||||
fixed (VkSubpassDependency* pDeps = dependencies)
|
||||
{
|
||||
VkRenderPassCreateInfo createInfo;
|
||||
createInfo.sType = VkStructureType.RenderPassCreateInfo;
|
||||
createInfo.pNext = null;
|
||||
createInfo.flags = 0;
|
||||
createInfo.attachmentCount = 2;
|
||||
createInfo.pAttachments = pAttachments;
|
||||
createInfo.subpassCount = 1;
|
||||
createInfo.pSubpasses = &subpass;
|
||||
createInfo.dependencyCount = 2;
|
||||
createInfo.pDependencies = pDeps;
|
||||
|
||||
VkRenderPass renderPass;
|
||||
VkResult result = Vk.vkCreateRenderPass(_ctx.Device, &createInfo, null, &renderPass);
|
||||
Vk.CheckResult(result, "vkCreateRenderPass");
|
||||
RenderPass = renderPass;
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void CreateFramebuffers()
|
||||
{
|
||||
Framebuffers = new VkFramebuffer[SwapchainImageViews.Length];
|
||||
|
||||
for (uint i = 0; i < SwapchainImageViews.Length; i++)
|
||||
{
|
||||
var attachments = new VkImageView[] { SwapchainImageViews[i], DepthImageView };
|
||||
|
||||
fixed (VkImageView* pAttachments = attachments)
|
||||
{
|
||||
VkFramebufferCreateInfo createInfo;
|
||||
createInfo.sType = VkStructureType.FramebufferCreateInfo;
|
||||
createInfo.pNext = null;
|
||||
createInfo.flags = 0;
|
||||
createInfo.renderPass = RenderPass;
|
||||
createInfo.attachmentCount = 2;
|
||||
createInfo.pAttachments = pAttachments;
|
||||
createInfo.width = (uint)Extent.width;
|
||||
createInfo.height = (uint)Extent.height;
|
||||
createInfo.layers = 1;
|
||||
|
||||
VkFramebuffer fb;
|
||||
VkResult result = Vk.vkCreateFramebuffer(_ctx.Device, &createInfo, null, &fb);
|
||||
Vk.CheckResult(result, "vkCreateFramebuffer");
|
||||
Framebuffers[i] = fb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Recreate(int width, int height)
|
||||
{
|
||||
Vk.vkQueueWaitIdle(_ctx.GraphicsQueue);
|
||||
|
||||
CleanupSwapchain();
|
||||
Create(width, height);
|
||||
}
|
||||
|
||||
private void CleanupSwapchain()
|
||||
{
|
||||
foreach (var fb in Framebuffers)
|
||||
if (fb.Value != 0) Vk.vkDestroyFramebuffer(_ctx.Device, fb, null);
|
||||
|
||||
if (DepthImageView.Value != 0) Vk.vkDestroyImageView(_ctx.Device, DepthImageView, null);
|
||||
if (DepthImage.Value != 0) Vk.vkDestroyImage(_ctx.Device, DepthImage, null);
|
||||
if (DepthImageMemory.Value != 0) Vk.vkFreeMemory(_ctx.Device, DepthImageMemory, null);
|
||||
|
||||
foreach (var iv in SwapchainImageViews)
|
||||
if (iv.Value != 0) Vk.vkDestroyImageView(_ctx.Device, iv, null);
|
||||
|
||||
if (Swapchain.Value != 0) Vk.vkDestroySwapchainKHR(_ctx.Device, Swapchain, null);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
CleanupSwapchain();
|
||||
if (RenderPass.Value != 0) Vk.vkDestroyRenderPass(_ctx.Device, RenderPass, null);
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct VkMemoryRequirements2
|
||||
{
|
||||
public ulong size;
|
||||
public ulong alignment;
|
||||
public uint memoryTypeBits;
|
||||
public uint _pad;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
|
||||
<DefineConstants>DEV_MODE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'ReleaseAOT'">
|
||||
<DefineConstants>RELEASE_AOT</DefineConstants>
|
||||
<PublishAot>true</PublishAot>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Flecs.NET.Debug" Version="4.0.4-build.546" Condition="'$(Configuration)' == 'Debug'" />
|
||||
<PackageReference Include="Flecs.NET.Release" Version="4.0.4-build.546" Condition="'$(Configuration)' == 'Release' OR '$(Configuration)' == 'ReleaseAOT'" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SharpGLTF.Core" Version="1.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Engine.Core\Engine.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,10 @@
|
||||
using Engine.Core;
|
||||
|
||||
namespace Engine.Graphics;
|
||||
|
||||
public interface IRenderContext : IDisposable
|
||||
{
|
||||
IWindow Window { get; }
|
||||
IRenderer CreateRenderer();
|
||||
void Resize(int width, int height);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Engine.Core;
|
||||
using Flecs.NET.Core;
|
||||
|
||||
namespace Engine.Graphics;
|
||||
|
||||
public interface IRenderer : IDisposable
|
||||
{
|
||||
void RenderWorld(World world);
|
||||
void RequestScreenshot(string outputPath);
|
||||
bool IsScreenshotRequested { get; }
|
||||
IScreenshotProvider ScreenshotProvider { get; }
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using System.Numerics;
|
||||
using Engine.Core;
|
||||
using Engine.Core.Components;
|
||||
namespace Engine.Graphics.Loaders;
|
||||
|
||||
public static class GltfLoader
|
||||
{
|
||||
public static Mesh Load(string path, Vector3? color = null)
|
||||
{
|
||||
var tint = color ?? new Vector3(0.7f, 0.6f, 0.5f);
|
||||
|
||||
var modelRoot = SharpGLTF.Schema2.ModelRoot.Load(path);
|
||||
|
||||
var vertices = new List<Vertex>();
|
||||
var indices = new List<uint>();
|
||||
|
||||
foreach (var scene in modelRoot.LogicalScenes)
|
||||
{
|
||||
foreach (var node in scene.VisualChildren)
|
||||
{
|
||||
var mesh = node.Mesh;
|
||||
if (mesh == null) continue;
|
||||
|
||||
foreach (var primitive in mesh.Primitives)
|
||||
{
|
||||
var posAccess = primitive.GetVertexAccessor("POSITION");
|
||||
var normAccess = primitive.GetVertexAccessor("NORMAL");
|
||||
if (posAccess == null) continue;
|
||||
|
||||
var indexAccess = primitive.IndexAccessor;
|
||||
var baseVertex = (uint)vertices.Count;
|
||||
|
||||
for (var i = 0; i < posAccess.Count; i++)
|
||||
{
|
||||
var pos = posAccess.AsVector3Array()[i];
|
||||
var normal = normAccess != null
|
||||
? normAccess.AsVector3Array()[i]
|
||||
: Vector3.UnitY;
|
||||
|
||||
vertices.Add(new Vertex(pos, tint, normal));
|
||||
}
|
||||
|
||||
if (indexAccess != null)
|
||||
{
|
||||
var indexArray = indexAccess.AsIndicesArray();
|
||||
foreach (var idx in indexArray)
|
||||
{
|
||||
indices.Add((uint)idx + baseVertex);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (uint i = 0; i < posAccess.Count; i++)
|
||||
{
|
||||
indices.Add(baseVertex + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (vertices.Count == 0)
|
||||
throw new InvalidOperationException($"GLTF file '{path}' contains no meshes.");
|
||||
|
||||
return new Mesh(vertices.ToArray(), indices.ToArray());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using Engine.Core;
|
||||
using Engine.Core.Components;
|
||||
|
||||
namespace Engine.Graphics.Loaders;
|
||||
|
||||
public static class ObjLoader
|
||||
{
|
||||
private static readonly Vector3 DefaultColor = new(0.7f, 0.6f, 0.5f);
|
||||
|
||||
public static Mesh Load(string path, Vector3? color = null)
|
||||
{
|
||||
var tint = color ?? DefaultColor;
|
||||
var lines = File.ReadAllLines(path);
|
||||
|
||||
var positions = new List<Vector3>();
|
||||
var normals = new List<Vector3>();
|
||||
|
||||
var vertices = new List<Vertex>();
|
||||
var indices = new List<uint>();
|
||||
|
||||
foreach (var rawLine in lines)
|
||||
{
|
||||
var line = rawLine.Trim();
|
||||
|
||||
if (line.Length == 0 || line.StartsWith('#'))
|
||||
continue;
|
||||
|
||||
var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length == 0)
|
||||
continue;
|
||||
|
||||
switch (parts[0])
|
||||
{
|
||||
case "v":
|
||||
positions.Add(new Vector3(
|
||||
float.Parse(parts[1], CultureInfo.InvariantCulture),
|
||||
float.Parse(parts[2], CultureInfo.InvariantCulture),
|
||||
float.Parse(parts[3], CultureInfo.InvariantCulture)));
|
||||
break;
|
||||
|
||||
case "vn":
|
||||
normals.Add(new Vector3(
|
||||
float.Parse(parts[1], CultureInfo.InvariantCulture),
|
||||
float.Parse(parts[2], CultureInfo.InvariantCulture),
|
||||
float.Parse(parts[3], CultureInfo.InvariantCulture)));
|
||||
break;
|
||||
|
||||
case "f":
|
||||
ParseFace(parts, positions, normals, vertices, indices, tint);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (vertices.Count == 0)
|
||||
throw new InvalidOperationException($"OBJ file '{path}' contains no faces.");
|
||||
|
||||
return new Mesh(vertices.ToArray(), indices.ToArray());
|
||||
}
|
||||
|
||||
private static void ParseFace(
|
||||
string[] parts,
|
||||
List<Vector3> positions,
|
||||
List<Vector3> normals,
|
||||
List<Vertex> vertices,
|
||||
List<uint> indices,
|
||||
Vector3 tint)
|
||||
{
|
||||
var faceData = new List<(int posIdx, int normIdx)>();
|
||||
|
||||
for (var i = 1; i < parts.Length; i++)
|
||||
{
|
||||
var vertexData = parts[i].Split('/');
|
||||
var posIdx = int.Parse(vertexData[0]) - 1;
|
||||
var normIdx = vertexData.Length > 2 && !string.IsNullOrEmpty(vertexData[2])
|
||||
? int.Parse(vertexData[2]) - 1
|
||||
: -1;
|
||||
|
||||
faceData.Add((posIdx, normIdx));
|
||||
}
|
||||
|
||||
if (faceData.Count < 3) return;
|
||||
|
||||
for (var i = 1; i < faceData.Count - 1; i++)
|
||||
{
|
||||
var d0 = faceData[0];
|
||||
var d1 = faceData[i];
|
||||
var d2 = faceData[i + 1];
|
||||
|
||||
var p0 = positions[d0.posIdx];
|
||||
var p1 = positions[d1.posIdx];
|
||||
var p2 = positions[d2.posIdx];
|
||||
|
||||
var normal = d0.normIdx >= 0 && d0.normIdx < normals.Count
|
||||
? normals[d0.normIdx]
|
||||
: MeshMath.ComputeFaceNormal(p0, p1, p2);
|
||||
|
||||
var i0 = (uint)vertices.Count;
|
||||
vertices.Add(new Vertex(p0, tint, normal));
|
||||
var i1 = (uint)vertices.Count;
|
||||
vertices.Add(new Vertex(p1, tint, normal));
|
||||
var i2 = (uint)vertices.Count;
|
||||
vertices.Add(new Vertex(p2, tint, normal));
|
||||
|
||||
indices.Add(i0); indices.Add(i1); indices.Add(i2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Numerics;
|
||||
|
||||
namespace Engine.Graphics;
|
||||
|
||||
public static class MeshMath
|
||||
{
|
||||
public static Vector3 ComputeFaceNormal(Vector3 a, Vector3 b, Vector3 c)
|
||||
{
|
||||
var edge1 = b - a;
|
||||
var edge2 = c - a;
|
||||
var normal = Vector3.Cross(edge2, edge1);
|
||||
|
||||
if (normal.LengthSquared() < 1e-12f)
|
||||
return Vector3.UnitY;
|
||||
|
||||
return Vector3.Normalize(normal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using System.Numerics;
|
||||
using Engine.Core;
|
||||
using Engine.Core.Components;
|
||||
|
||||
namespace Engine.Graphics;
|
||||
|
||||
public static class ProceduralMesh
|
||||
{
|
||||
public static Mesh CreateSphere(float radius, int slices, int stacks, Vector3 color)
|
||||
{
|
||||
var vertices = new Vertex[(stacks + 1) * (slices + 1)];
|
||||
var indices = new uint[stacks * slices * 6];
|
||||
|
||||
var vi = 0;
|
||||
for (var i = 0; i <= stacks; i++)
|
||||
{
|
||||
var phi = MathF.PI * i / stacks;
|
||||
var y = radius * MathF.Cos(phi);
|
||||
var r = radius * MathF.Sin(phi);
|
||||
|
||||
for (var j = 0; j <= slices; j++)
|
||||
{
|
||||
var theta = 2.0f * MathF.PI * j / slices;
|
||||
var x = r * MathF.Cos(theta);
|
||||
var z = r * MathF.Sin(theta);
|
||||
|
||||
var pos = new Vector3(x, y, z);
|
||||
var normal = Vector3.Normalize(pos);
|
||||
|
||||
vertices[vi++] = new Vertex(pos, color, normal);
|
||||
}
|
||||
}
|
||||
|
||||
var ii = 0;
|
||||
for (var i = 0; i < stacks; i++)
|
||||
{
|
||||
for (var j = 0; j < slices; j++)
|
||||
{
|
||||
var a = (uint)(i * (slices + 1) + j);
|
||||
var b = a + 1;
|
||||
var c = a + (uint)(slices + 1);
|
||||
var d = c + 1;
|
||||
|
||||
indices[ii++] = a; indices[ii++] = c; indices[ii++] = b;
|
||||
indices[ii++] = b; indices[ii++] = c; indices[ii++] = d;
|
||||
}
|
||||
}
|
||||
|
||||
return new Mesh(vertices, indices);
|
||||
}
|
||||
|
||||
public static Mesh CreateGrid(int halfSize, float spacing, Vector3 color)
|
||||
{
|
||||
var lines = 2 * halfSize + 1;
|
||||
var vertices = new List<Vertex>(lines * 4 * 2);
|
||||
var indices = new List<uint>(lines * 4 * 2);
|
||||
var extent = halfSize * spacing;
|
||||
|
||||
for (var i = -halfSize; i <= halfSize; i++)
|
||||
{
|
||||
var pos = i * spacing;
|
||||
|
||||
var i0 = (uint)vertices.Count;
|
||||
vertices.Add(new Vertex(new Vector3(pos, 0, -extent), color, Vector3.UnitY));
|
||||
var i1 = (uint)vertices.Count;
|
||||
vertices.Add(new Vertex(new Vector3(pos, 0, extent), color, Vector3.UnitY));
|
||||
indices.Add(i0); indices.Add(i1);
|
||||
|
||||
var i2 = (uint)vertices.Count;
|
||||
vertices.Add(new Vertex(new Vector3(pos, 0, -extent), color, Vector3.UnitY));
|
||||
var i3 = (uint)vertices.Count;
|
||||
vertices.Add(new Vertex(new Vector3(pos, 0, extent), color, Vector3.UnitY));
|
||||
indices.Add(i2); indices.Add(i3);
|
||||
|
||||
var i4 = (uint)vertices.Count;
|
||||
vertices.Add(new Vertex(new Vector3(-extent, 0, pos), color, Vector3.UnitY));
|
||||
var i5 = (uint)vertices.Count;
|
||||
vertices.Add(new Vertex(new Vector3(extent, 0, pos), color, Vector3.UnitY));
|
||||
indices.Add(i4); indices.Add(i5);
|
||||
|
||||
var i6 = (uint)vertices.Count;
|
||||
vertices.Add(new Vertex(new Vector3(-extent, 0, pos), color, Vector3.UnitY));
|
||||
var i7 = (uint)vertices.Count;
|
||||
vertices.Add(new Vertex(new Vector3(extent, 0, pos), color, Vector3.UnitY));
|
||||
indices.Add(i6); indices.Add(i7);
|
||||
}
|
||||
|
||||
return new Mesh(vertices.ToArray(), indices.ToArray());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using Engine.Core;
|
||||
|
||||
namespace Engine.Graphics;
|
||||
|
||||
public static class RenderBackendFactory
|
||||
{
|
||||
private static readonly Dictionary<string, Func<int, int, bool, IRenderContext>> _backends =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public static void Register(string name, Func<int, int, bool, IRenderContext> factory)
|
||||
{
|
||||
_backends[name] = factory;
|
||||
}
|
||||
|
||||
public static IRenderContext Create(string name, int width, int height, bool enableValidation)
|
||||
{
|
||||
if (_backends.TryGetValue(name, out var factory))
|
||||
return factory(width, height, enableValidation);
|
||||
|
||||
throw new NotSupportedException(
|
||||
$"Unknown render backend '{name}'. Available: {string.Join(", ", _backends.Keys)}");
|
||||
}
|
||||
|
||||
public static bool IsRegistered(string name) => _backends.ContainsKey(name);
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
using System.Numerics;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Engine.Core.Components;
|
||||
using Flecs.NET.Core;
|
||||
|
||||
namespace Engine.Graphics;
|
||||
|
||||
public static class SceneSerializer
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
|
||||
public static string SaveToString(World world)
|
||||
{
|
||||
var entities = new List<SceneEntityData>();
|
||||
|
||||
world.Each((Entity e, ref Transform _) =>
|
||||
{
|
||||
var name = e.Name();
|
||||
if (string.IsNullOrEmpty(name)) return;
|
||||
|
||||
var data = new SceneEntityData { Name = name };
|
||||
|
||||
if (e.Has<Transform>())
|
||||
{
|
||||
var t = e.Get<Transform>();
|
||||
data.Transform = new TransformData
|
||||
{
|
||||
Position = new float[] { t.Position.X, t.Position.Y, t.Position.Z },
|
||||
Rotation = new float[] { t.Rotation.X, t.Rotation.Y, t.Rotation.Z, t.Rotation.W },
|
||||
Scale = new float[] { t.Scale.X, t.Scale.Y, t.Scale.Z }
|
||||
};
|
||||
}
|
||||
|
||||
if (e.Has<Material>())
|
||||
{
|
||||
var m = e.Get<Material>();
|
||||
data.Material = new MaterialData
|
||||
{
|
||||
Albedo = new float[] { m.Albedo.X, m.Albedo.Y, m.Albedo.Z },
|
||||
Roughness = m.Roughness,
|
||||
Metallic = m.Metallic,
|
||||
TexturePath = m.TexturePath
|
||||
};
|
||||
}
|
||||
|
||||
if (e.Has<Light>())
|
||||
{
|
||||
var l = e.Get<Light>();
|
||||
data.Light = new LightData
|
||||
{
|
||||
Type = l.Type.ToString(),
|
||||
Direction = new float[] { l.Direction.X, l.Direction.Y, l.Direction.Z },
|
||||
Position = new float[] { l.Position.X, l.Position.Y, l.Position.Z },
|
||||
Color = new float[] { l.Color.X, l.Color.Y, l.Color.Z },
|
||||
Intensity = l.Intensity,
|
||||
Range = l.Range
|
||||
};
|
||||
}
|
||||
|
||||
if (e.Has<Camera>())
|
||||
{
|
||||
var c = e.Get<Camera>();
|
||||
data.Camera = new CameraData
|
||||
{
|
||||
Position = new float[] { c.Position.X, c.Position.Y, c.Position.Z },
|
||||
Target = new float[] { c.Target.X, c.Target.Y, c.Target.Z },
|
||||
Up = new float[] { c.Up.X, c.Up.Y, c.Up.Z },
|
||||
FieldOfView = c.FieldOfView,
|
||||
AspectRatio = c.AspectRatio,
|
||||
NearPlane = c.NearPlane,
|
||||
FarPlane = c.FarPlane
|
||||
};
|
||||
}
|
||||
|
||||
entities.Add(data);
|
||||
});
|
||||
|
||||
return JsonSerializer.Serialize(entities, JsonOptions);
|
||||
}
|
||||
|
||||
public static void SaveToFile(World world, string path)
|
||||
{
|
||||
var json = SaveToString(world);
|
||||
File.WriteAllText(path, json);
|
||||
}
|
||||
|
||||
public static int LoadFromString(World world, string json)
|
||||
{
|
||||
var entities = JsonSerializer.Deserialize<List<SceneEntityData>>(json, JsonOptions);
|
||||
if (entities == null) return 0;
|
||||
|
||||
foreach (var data in entities)
|
||||
{
|
||||
var entity = world.Entity(data.Name);
|
||||
|
||||
if (data.Transform != null)
|
||||
{
|
||||
var t = data.Transform;
|
||||
entity.Set(new Transform(
|
||||
new Vector3(t.Position[0], t.Position[1], t.Position[2]),
|
||||
new Quaternion(t.Rotation[0], t.Rotation[1], t.Rotation[2], t.Rotation[3]),
|
||||
new Vector3(t.Scale[0], t.Scale[1], t.Scale[2])));
|
||||
}
|
||||
|
||||
if (data.Material != null)
|
||||
{
|
||||
var m = data.Material;
|
||||
entity.Set(new Material(
|
||||
new Vector3(m.Albedo[0], m.Albedo[1], m.Albedo[2]),
|
||||
m.Roughness, m.Metallic, m.TexturePath));
|
||||
}
|
||||
|
||||
if (data.Light != null)
|
||||
{
|
||||
var l = data.Light;
|
||||
if (l.Type == "Directional")
|
||||
{
|
||||
entity.Set(Light.Directional(
|
||||
new Vector3(l.Direction[0], l.Direction[1], l.Direction[2]),
|
||||
new Vector3(l.Color[0], l.Color[1], l.Color[2]),
|
||||
l.Intensity));
|
||||
}
|
||||
else
|
||||
{
|
||||
entity.Set(Light.Point(
|
||||
new Vector3(l.Position[0], l.Position[1], l.Position[2]),
|
||||
new Vector3(l.Color[0], l.Color[1], l.Color[2]),
|
||||
l.Intensity, l.Range));
|
||||
}
|
||||
}
|
||||
|
||||
if (data.Camera != null)
|
||||
{
|
||||
var c = data.Camera;
|
||||
entity.Set(new Camera(
|
||||
new Vector3(c.Position[0], c.Position[1], c.Position[2]),
|
||||
new Vector3(c.Target[0], c.Target[1], c.Target[2]),
|
||||
new Vector3(c.Up[0], c.Up[1], c.Up[2]),
|
||||
c.FieldOfView, c.AspectRatio, c.NearPlane, c.FarPlane));
|
||||
}
|
||||
}
|
||||
|
||||
return entities.Count;
|
||||
}
|
||||
|
||||
public static int LoadFromFile(World world, string path)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
throw new FileNotFoundException($"Scene file not found: {path}", path);
|
||||
|
||||
var json = File.ReadAllText(path);
|
||||
return LoadFromString(world, json);
|
||||
}
|
||||
|
||||
private class SceneEntityData
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public TransformData? Transform { get; set; }
|
||||
public MaterialData? Material { get; set; }
|
||||
public LightData? Light { get; set; }
|
||||
public CameraData? Camera { get; set; }
|
||||
}
|
||||
|
||||
private class TransformData
|
||||
{
|
||||
public float[] Position { get; set; } = Array.Empty<float>();
|
||||
public float[] Rotation { get; set; } = Array.Empty<float>();
|
||||
public float[] Scale { get; set; } = Array.Empty<float>();
|
||||
}
|
||||
|
||||
private class MaterialData
|
||||
{
|
||||
public float[] Albedo { get; set; } = Array.Empty<float>();
|
||||
public float Roughness { get; set; }
|
||||
public float Metallic { get; set; }
|
||||
public string? TexturePath { get; set; }
|
||||
}
|
||||
|
||||
private class LightData
|
||||
{
|
||||
public string Type { get; set; } = "";
|
||||
public float[] Direction { get; set; } = Array.Empty<float>();
|
||||
public float[] Position { get; set; } = Array.Empty<float>();
|
||||
public float[] Color { get; set; } = Array.Empty<float>();
|
||||
public float Intensity { get; set; }
|
||||
public float Range { get; set; }
|
||||
}
|
||||
|
||||
private class CameraData
|
||||
{
|
||||
public float[] Position { get; set; } = Array.Empty<float>();
|
||||
public float[] Target { get; set; } = Array.Empty<float>();
|
||||
public float[] Up { get; set; } = Array.Empty<float>();
|
||||
public float FieldOfView { get; set; }
|
||||
public float AspectRatio { get; set; }
|
||||
public float NearPlane { get; set; }
|
||||
public float FarPlane { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user