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:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user