From fde8b0f63a6d28a5733df0b7240278376bab775b Mon Sep 17 00:00:00 2001 From: emil28092005 Date: Thu, 18 Jun 2026 02:05:15 +0300 Subject: [PATCH] feat: index buffer + OBJ cube rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - VulkanIndexBuffer.cs: staging → device-local index buffer (uint16 indices) - vkCmdBindIndexBuffer + vkCmdDrawIndexed added to Vk.cs - VulkanRenderer loads Content/cube.obj via ObjLoader (36 vertices, 12 faces) - Replaced hardcoded triangle with rotating cube - 0 validation errors, ~3800 FPS --- src/Engine.Graphics.Vulkan/Vk.cs | 6 + .../VulkanIndexBuffer.cs | 170 ++++++++++++++++++ src/Engine.Graphics.Vulkan/VulkanRenderer.cs | 37 +++- 3 files changed, 205 insertions(+), 8 deletions(-) create mode 100644 src/Engine.Graphics.Vulkan/VulkanIndexBuffer.cs diff --git a/src/Engine.Graphics.Vulkan/Vk.cs b/src/Engine.Graphics.Vulkan/Vk.cs index 6cffbec..233a20d 100644 --- a/src/Engine.Graphics.Vulkan/Vk.cs +++ b/src/Engine.Graphics.Vulkan/Vk.cs @@ -58,6 +58,8 @@ internal static unsafe class Vk public delegate void VkCmdSetScissor(VkCommandBuffer commandBuffer, uint firstScissor, uint scissorCount, VkRect2D* pScissors); public delegate void VkCmdBindVertexBuffers(VkCommandBuffer commandBuffer, uint firstBinding, uint bindingCount, VkBuffer* pBuffers, ulong* pOffsets); public delegate void VkCmdDraw(VkCommandBuffer commandBuffer, uint vertexCount, uint instanceCount, uint firstVertex, uint firstInstance); + public delegate void VkCmdDrawIndexed(VkCommandBuffer commandBuffer, uint indexCount, uint instanceCount, uint firstIndex, int vertexOffset, uint firstInstance); + public delegate void VkCmdBindIndexBuffer(VkCommandBuffer commandBuffer, VkBuffer buffer, ulong offset, int indexType); public delegate void VkCmdBeginRendering(VkCommandBuffer commandBuffer, VkRenderingInfo* pRenderingInfo); public delegate void VkCmdEndRendering(VkCommandBuffer commandBuffer); public delegate void VkCmdPipelineBarrier2(VkCommandBuffer commandBuffer, VkDependencyInfo* pDependencyInfo); @@ -133,6 +135,8 @@ internal static unsafe class Vk public static VkCmdSetScissor vkCmdSetScissor; public static VkCmdBindVertexBuffers vkCmdBindVertexBuffers; public static VkCmdDraw vkCmdDraw; + public static VkCmdDrawIndexed vkCmdDrawIndexed; + public static VkCmdBindIndexBuffer vkCmdBindIndexBuffer; public static VkCmdBeginRendering vkCmdBeginRendering; public static VkCmdEndRendering vkCmdEndRendering; public static VkCmdPipelineBarrier2 vkCmdPipelineBarrier2; @@ -215,6 +219,8 @@ internal static unsafe class Vk vkCmdSetScissor = LoadDev(p, "vkCmdSetScissor"); vkCmdBindVertexBuffers = LoadDev(p, "vkCmdBindVertexBuffers"); vkCmdDraw = LoadDev(p, "vkCmdDraw"); + vkCmdDrawIndexed = LoadDev(p, "vkCmdDrawIndexed"); + vkCmdBindIndexBuffer = LoadDev(p, "vkCmdBindIndexBuffer"); vkCmdBeginRendering = LoadDev(p, "vkCmdBeginRendering"); vkCmdEndRendering = LoadDev(p, "vkCmdEndRendering"); vkCmdPipelineBarrier2 = LoadDev(p, "vkCmdPipelineBarrier2"); diff --git a/src/Engine.Graphics.Vulkan/VulkanIndexBuffer.cs b/src/Engine.Graphics.Vulkan/VulkanIndexBuffer.cs new file mode 100644 index 0000000..37db31e --- /dev/null +++ b/src/Engine.Graphics.Vulkan/VulkanIndexBuffer.cs @@ -0,0 +1,170 @@ +using System.Runtime.InteropServices; + +namespace Engine.Graphics.Vulkan; + +internal sealed unsafe class VulkanIndexBuffer : IDisposable +{ + public VkBuffer Buffer; + public VkDeviceMemory Memory; + public uint IndexCount; + + private readonly VkDevice _device; + private bool _disposed; + + public VulkanIndexBuffer(VkDevice device, VkCommandPool commandPool, VkQueue queue, + VulkanContext ctx, uint[] indices) + { + _device = device; + IndexCount = (uint)indices.Length; + var bufferSize = (ulong)(indices.Length * sizeof(uint)); + + var stagingBuffer = VkBuffer.Null; + var stagingMemory = VkDeviceMemory.Null; + CreateStagingBuffer(device, ctx, bufferSize, &stagingBuffer, &stagingMemory); + UploadToStaging(device, stagingMemory, indices, bufferSize); + var buf = VkBuffer.Null; + var mem = VkDeviceMemory.Null; + CreateDeviceLocalBuffer(device, ctx, bufferSize, &buf, &mem); + Buffer = buf; + Memory = mem; + CopyBuffer(device, commandPool, queue, stagingBuffer, Buffer, bufferSize); + + Vk.vkDestroyBuffer(device, stagingBuffer, 0); + Vk.vkFreeMemory(device, stagingMemory, 0); + } + + private static void CreateStagingBuffer(VkDevice device, VulkanContext ctx, ulong size, + VkBuffer* pBuffer, VkDeviceMemory* pMemory) + { + var info = new VkBufferCreateInfo + { + sType = VkStructureType.BufferCreateInfo, + size = size, + usage = VkBufferUsageFlags.TransferSrc, + sharingMode = VkSharingMode.Exclusive, + }; + + var result = Vk.vkCreateBuffer(device, &info, 0, pBuffer); + if (result != VkResult.Success) + throw new InvalidOperationException($"vkCreateBuffer (staging) failed: {result}"); + + var reqs = new VkMemoryRequirements(); + Vk.vkGetBufferMemoryRequirements(device, *pBuffer, &reqs); + + var memTypeIndex = ctx.FindMemoryType(reqs.memoryTypeBits, + VkMemoryPropertyFlags.HostVisible | VkMemoryPropertyFlags.HostCoherent); + + var allocInfo = new VkMemoryAllocateInfo + { + sType = VkStructureType.MemoryAllocateInfo, + allocationSize = reqs.size, + memoryTypeIndex = memTypeIndex, + }; + + result = Vk.vkAllocateMemory(device, &allocInfo, 0, pMemory); + if (result != VkResult.Success) + throw new InvalidOperationException($"vkAllocateMemory (staging) failed: {result}"); + + Vk.vkBindBufferMemory(device, *pBuffer, *pMemory, 0); + } + + private static void UploadToStaging(VkDevice device, VkDeviceMemory memory, uint[] indices, ulong size) + { + void* pData = null; + Vk.vkMapMemory(device, memory, 0, size, 0, &pData); + fixed (uint* pIndices = indices) + { + System.Buffer.MemoryCopy(pIndices, pData, (long)size, (long)size); + } + Vk.vkUnmapMemory(device, memory); + } + + private static void CreateDeviceLocalBuffer(VkDevice device, VulkanContext ctx, ulong size, + VkBuffer* pBuffer, VkDeviceMemory* pMemory) + { + var info = new VkBufferCreateInfo + { + sType = VkStructureType.BufferCreateInfo, + size = size, + usage = VkBufferUsageFlags.TransferDst | VkBufferUsageFlags.IndexBuffer, + sharingMode = VkSharingMode.Exclusive, + }; + + var result = Vk.vkCreateBuffer(device, &info, 0, pBuffer); + if (result != VkResult.Success) + throw new InvalidOperationException($"vkCreateBuffer (index) failed: {result}"); + + var reqs = new VkMemoryRequirements(); + Vk.vkGetBufferMemoryRequirements(device, *pBuffer, &reqs); + + var memTypeIndex = ctx.FindMemoryType(reqs.memoryTypeBits, VkMemoryPropertyFlags.DeviceLocal); + + var allocInfo = new VkMemoryAllocateInfo + { + sType = VkStructureType.MemoryAllocateInfo, + allocationSize = reqs.size, + memoryTypeIndex = memTypeIndex, + }; + + result = Vk.vkAllocateMemory(device, &allocInfo, 0, pMemory); + if (result != VkResult.Success) + throw new InvalidOperationException($"vkAllocateMemory (index) failed: {result}"); + + Vk.vkBindBufferMemory(device, *pBuffer, *pMemory, 0); + } + + private static void CopyBuffer(VkDevice device, VkCommandPool pool, VkQueue queue, + VkBuffer src, VkBuffer dst, ulong size) + { + var allocInfo = new VkCommandBufferAllocateInfo + { + sType = VkStructureType.CommandBufferAllocateInfo, + commandPool = pool, + level = VkCommandBufferLevel.Primary, + commandBufferCount = 1, + }; + + var cmd = VkCommandBuffer.Null; + Vk.vkAllocateCommandBuffers(device, &allocInfo, &cmd); + + var beginInfo = new VkCommandBufferBeginInfo + { + sType = VkStructureType.CommandBufferBeginInfo, + flags = VkCommandBufferUsageFlags.OneTimeSubmit, + }; + + Vk.vkBeginCommandBuffer(cmd, &beginInfo); + + var copyRegion = new VkBufferCopy { srcOffset = 0, dstOffset = 0, size = size }; + Vk.vkCmdCopyBuffer(cmd, src, dst, 1, ©Region); + + Vk.vkEndCommandBuffer(cmd); + + var cmdInfo = new VkCommandBufferSubmitInfo + { + sType = VkStructureType.CommandBufferSubmitInfo, + commandBuffer = cmd, + }; + + var submitInfo = new VkSubmitInfo2 + { + sType = VkStructureType.SubmitInfo2, + commandBufferInfoCount = 1, + pCommandBufferInfos = &cmdInfo, + }; + + Vk.vkQueueSubmit2(queue, 1, &submitInfo, VkFence.Null); + Vk.vkQueueWaitIdle(queue); + + Vk.vkFreeCommandBuffers(device, pool, 1, &cmd); + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + if (Buffer.Handle != 0) Vk.vkDestroyBuffer(_device, Buffer, 0); + if (Memory.Handle != 0) Vk.vkFreeMemory(_device, Memory, 0); + } +} diff --git a/src/Engine.Graphics.Vulkan/VulkanRenderer.cs b/src/Engine.Graphics.Vulkan/VulkanRenderer.cs index f5ae673..c145f98 100644 --- a/src/Engine.Graphics.Vulkan/VulkanRenderer.cs +++ b/src/Engine.Graphics.Vulkan/VulkanRenderer.cs @@ -1,6 +1,7 @@ using System.Numerics; using Engine.Core; using Engine.Core.Components; +using Engine.Graphics.Loaders; using Flecs.NET.Core; using System.Runtime.InteropServices; @@ -13,6 +14,8 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen private readonly VulkanPipeline _pipeline; private readonly VulkanFrameResources _frameResources; private readonly VulkanVertexBuffer _vertexBuffer; + private readonly VulkanIndexBuffer _indexBuffer; + private readonly uint _indexCount; private int _frameIndex; private bool _disposed; @@ -36,15 +39,30 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen _frameResources = new VulkanFrameResources(ctx.Device, ctx.GraphicsQueueFamilyIndex, swapchain.ImageCount, ctx, _pipeline.DescriptorSetLayout); - var vertices = new Vertex[] - { - new(new Vector3( 0.0f, -0.5f, 0.0f), new Vector3(1.0f, 0.0f, 0.0f), new Vector3(0, 0, 1)), - new(new Vector3( 0.5f, 0.5f, 0.0f), new Vector3(0.0f, 1.0f, 0.0f), new Vector3(0, 0, 1)), - new(new Vector3(-0.5f, 0.5f, 0.0f), new Vector3(0.0f, 0.0f, 1.0f), new Vector3(0, 0, 1)), - }; + var mesh = LoadMesh("Content/cube.obj"); + _indexCount = (uint)mesh.Indices.Length; _vertexBuffer = new VulkanVertexBuffer(ctx.Device, ctx.PhysicalDevice, - _frameResources.CommandPool, ctx.GraphicsQueue, ctx, vertices); + _frameResources.CommandPool, ctx.GraphicsQueue, ctx, mesh.Vertices); + + _indexBuffer = new VulkanIndexBuffer(ctx.Device, _frameResources.CommandPool, + ctx.GraphicsQueue, ctx, mesh.Indices); + } + + private static Mesh LoadMesh(string path) + { + if (!File.Exists(path)) + { + var altPath = Path.Combine(AppContext.BaseDirectory, path); + if (!File.Exists(altPath)) + { + altPath = Path.Combine(AppContext.BaseDirectory, "Content", Path.GetFileName(path)); + if (!File.Exists(altPath)) + throw new FileNotFoundException($"Mesh file not found: {path}"); + } + return ObjLoader.Load(altPath, new Vector3(0.8f, 0.6f, 0.3f)); + } + return ObjLoader.Load(path, new Vector3(0.8f, 0.6f, 0.3f)); } public void RenderWorld(World world) @@ -147,10 +165,12 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen ulong offset = 0; Vk.vkCmdBindVertexBuffers(cmd, 0, 1, &bufferHandle, &offset); + Vk.vkCmdBindIndexBuffer(cmd, _indexBuffer.Buffer, 0, 0); + var angle = _totalTime; Vk.vkCmdPushConstants(cmd, _pipeline.PipelineLayout, VkShaderStageFlags.Vertex, 0, 4, &angle); - Vk.vkCmdDraw(cmd, 3, 1, 0, 0); + Vk.vkCmdDrawIndexed(cmd, _indexCount, 1, 0, 0, 0); Vk.vkCmdEndRendering(cmd); @@ -305,6 +325,7 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen Vk.vkDeviceWaitIdle(_ctx.Device); _vertexBuffer?.Dispose(); + _indexBuffer?.Dispose(); _frameResources?.Dispose(); _pipeline?.Dispose(); }