From bfde04b38150879a10a1d45b079c95e2f551d180 Mon Sep 17 00:00:00 2001 From: emil28092005 Date: Thu, 18 Jun 2026 17:34:42 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20ImGui=20debug=20overlay=20=E2=80=94=20F?= =?UTF-8?q?PS,=20camera=20position,=20entity=20count?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ImGui.NET 1.91.6.1 integrated into Engine.Graphics.Vulkan - VulkanImGui.cs: font atlas upload (staging → VkImage), sampler, descriptor set (COMBINED_IMAGE_SAMPLER), separate pipeline with alpha blending, no depth write, dynamic scissor - ImGui shaders: imgui.vert (ortho MVP push constant) + imgui.frag (font texture sampler) - Dynamic vertex/index buffers (HOST_VISIBLE, grow on demand) - IRenderer interface: BeginImGuiFrame/EndImGuiFrame default methods - Program.cs: debug window with FPS, camera pos/target, entity count - 0 critical validation errors, ~750 FPS with physics + ImGui --- src/CortexEngine.App/Program.cs | 32 + .../Engine.Graphics.Vulkan.csproj | 4 + src/Engine.Graphics.Vulkan/Shaders/imgui.frag | 11 + .../Shaders/imgui.frag.spv | Bin 0 -> 668 bytes src/Engine.Graphics.Vulkan/Shaders/imgui.vert | 18 + .../Shaders/imgui.vert.spv | Bin 0 -> 1364 bytes src/Engine.Graphics.Vulkan/Vk.cs | 9 + src/Engine.Graphics.Vulkan/VulkanEnums.cs | 21 + src/Engine.Graphics.Vulkan/VulkanHandles.cs | 2 + src/Engine.Graphics.Vulkan/VulkanImGui.cs | 726 ++++++++++++++++++ .../VulkanRenderContext.cs | 3 +- src/Engine.Graphics.Vulkan/VulkanRenderer.cs | 19 + src/Engine.Graphics.Vulkan/VulkanStructs.cs | 51 ++ src/Engine.Graphics/IRenderer.cs | 12 + 14 files changed, 906 insertions(+), 2 deletions(-) create mode 100644 src/Engine.Graphics.Vulkan/Shaders/imgui.frag create mode 100644 src/Engine.Graphics.Vulkan/Shaders/imgui.frag.spv create mode 100644 src/Engine.Graphics.Vulkan/Shaders/imgui.vert create mode 100644 src/Engine.Graphics.Vulkan/Shaders/imgui.vert.spv create mode 100644 src/Engine.Graphics.Vulkan/VulkanImGui.cs diff --git a/src/CortexEngine.App/Program.cs b/src/CortexEngine.App/Program.cs index 516196f..7ea669d 100644 --- a/src/CortexEngine.App/Program.cs +++ b/src/CortexEngine.App/Program.cs @@ -6,6 +6,7 @@ using Engine.Graphics.Loaders; using Engine.Graphics.Vulkan; using Engine.Physics; using Flecs.NET.Core; +using ImGuiNET; namespace CortexEngine.App; @@ -41,6 +42,13 @@ class Program CreateScene(world); + var hasImGui = renderer.GetType().Name == "VulkanRenderer"; + if (hasImGui) + { + ImGui.GetIO().DisplaySize = new System.Numerics.Vector2(window.Width, window.Height); + Console.WriteLine("[App] ImGui initialized."); + } + var lastWidth = window.Width; var lastHeight = window.Height; var frames = 0; @@ -82,6 +90,30 @@ class Program physicsWorld.Update((float)timing.DeltaTime); physicsWorld.SyncTransforms(world); + + if (hasImGui) + { + renderer.BeginImGuiFrame(); + + ImGui.Begin("Cortex Engine Debug"); + ImGui.Text($"FPS: {frames}"); + ImGui.Text($"Delta: {timing.DeltaTime * 1000.0:F2} ms"); + + var cam = cameraEntity.Get(); + ImGui.Separator(); + ImGui.Text($"Camera Pos: ({cam.Position.X:F2}, {cam.Position.Y:F2}, {cam.Position.Z:F2})"); + ImGui.Text($"Camera Target: ({cam.Target.X:F2}, {cam.Target.Y:F2}, {cam.Target.Z:F2})"); + ImGui.Separator(); + + var entityCount = 0; + world.Each((Entity e, ref Transform _) => entityCount++); + ImGui.Text($"Entities: {entityCount}"); + ImGui.Text("WASD: move | RMB: look | Q/E: up/down | Shift: boost"); + ImGui.End(); + + renderer.EndImGuiFrame(); + } + renderer.RenderWorld(world); frames++; diff --git a/src/Engine.Graphics.Vulkan/Engine.Graphics.Vulkan.csproj b/src/Engine.Graphics.Vulkan/Engine.Graphics.Vulkan.csproj index 655e09e..eede5f3 100644 --- a/src/Engine.Graphics.Vulkan/Engine.Graphics.Vulkan.csproj +++ b/src/Engine.Graphics.Vulkan/Engine.Graphics.Vulkan.csproj @@ -17,6 +17,10 @@ true + + + + diff --git a/src/Engine.Graphics.Vulkan/Shaders/imgui.frag b/src/Engine.Graphics.Vulkan/Shaders/imgui.frag new file mode 100644 index 0000000..ddd58a6 --- /dev/null +++ b/src/Engine.Graphics.Vulkan/Shaders/imgui.frag @@ -0,0 +1,11 @@ +#version 450 + +layout(location = 0) in vec4 fragColor; +layout(location = 1) in vec2 fragUV; +layout(location = 0) out vec4 outColor; + +layout(set = 0, binding = 0) uniform sampler2D fontTexture; + +void main() { + outColor = fragColor * texture(fontTexture, fragUV); +} diff --git a/src/Engine.Graphics.Vulkan/Shaders/imgui.frag.spv b/src/Engine.Graphics.Vulkan/Shaders/imgui.frag.spv new file mode 100644 index 0000000000000000000000000000000000000000..e6da41945c6708e2b88d60eb59ff76d8491b2787 GIT binary patch literal 668 zcmYk2O-n*y5QV4onq~Qsr9}{0dudS-L~UBQao5*cNFyjO*cJK%{jq*ko1o{pD)_?O zdC!?M=bgEQ_0x@rwWvfn4q|>9u?`b(r9Afs_k-SN_R>4QI8#xLLM~KOqpwIdK89}- z3!6lR*dyv_t?>xA0?rXXG$ydCJqe6`=`6cT-_t2qm?ZZZPlvBdQm#&IoKCWb(N{K` zj$n%UTk0O4BKlyxjy2(koo5y6(Y462EAgYZ4Ep8JG;&vdD85o+YW7hIdW QKGtq|#s4Gs#}rq@KQv(?bpQYW literal 0 HcmV?d00001 diff --git a/src/Engine.Graphics.Vulkan/Shaders/imgui.vert b/src/Engine.Graphics.Vulkan/Shaders/imgui.vert new file mode 100644 index 0000000..62d6e16 --- /dev/null +++ b/src/Engine.Graphics.Vulkan/Shaders/imgui.vert @@ -0,0 +1,18 @@ +#version 450 + +layout(location = 0) in vec2 inPosition; +layout(location = 1) in vec2 inUV; +layout(location = 2) in vec4 inColor; + +layout(location = 0) out vec4 fragColor; +layout(location = 1) out vec2 fragUV; + +layout(push_constant) uniform PC { + mat4 mvp; +} pc; + +void main() { + gl_Position = pc.mvp * vec4(inPosition, 0.0, 1.0); + fragColor = inColor; + fragUV = inUV; +} diff --git a/src/Engine.Graphics.Vulkan/Shaders/imgui.vert.spv b/src/Engine.Graphics.Vulkan/Shaders/imgui.vert.spv new file mode 100644 index 0000000000000000000000000000000000000000..5ce60a666881a08360e80624c83d875cfef45ee4 GIT binary patch literal 1364 zcmYk4U29W85Qeu&Pg}oQYwd?xV`BWOh2n*ZASx0eUi2bT+FJ=VmILWYNTP^W{ssS% zzseiI=h>4@vtcsxzVq(x%HEXC`i`aQLFO08Yx3DJm5PO2{VO4$={2w-E?3mw^ahE5Z^*?udqm$lf()$5~ zEqP;egK<8|2LgU4``FPSTPJ6SQ9hm#p@6U1>^;x^i zcpcwqdER1-< zc_9j+&Hc{99zL?WoU(6HuEdq^vj$(p, "vkCmdEndRendering"); vkCmdPipelineBarrier2 = LoadDev(p, "vkCmdPipelineBarrier2"); vkCmdCopyBuffer = LoadDev(p, "vkCmdCopyBuffer"); + vkCmdCopyBufferToImage = LoadDev(p, "vkCmdCopyBufferToImage"); vkCmdPushConstants = LoadDev(p, "vkCmdPushConstants"); vkCmdBindDescriptorSets = LoadDev(p, "vkCmdBindDescriptorSets"); vkCreateDescriptorSetLayout = LoadDev(p, "vkCreateDescriptorSetLayout"); vkDestroyDescriptorSetLayout = LoadDev(p, "vkDestroyDescriptorSetLayout"); + vkCreateSampler = LoadDev(p, "vkCreateSampler"); + vkDestroySampler = LoadDev(p, "vkDestroySampler"); vkCreateDescriptorPool = LoadDev(p, "vkCreateDescriptorPool"); vkDestroyDescriptorPool = LoadDev(p, "vkDestroyDescriptorPool"); vkAllocateDescriptorSets = LoadDev(p, "vkAllocateDescriptorSets"); diff --git a/src/Engine.Graphics.Vulkan/VulkanEnums.cs b/src/Engine.Graphics.Vulkan/VulkanEnums.cs index 39a1292..bfa2dd9 100644 --- a/src/Engine.Graphics.Vulkan/VulkanEnums.cs +++ b/src/Engine.Graphics.Vulkan/VulkanEnums.cs @@ -55,6 +55,7 @@ public enum VkStructureType : int PipelineDynamicStateCreateInfo = 27, GraphicsPipelineCreateInfo = 28, PipelineLayoutCreateInfo = 30, + SamplerCreateInfo = 31, RenderPassCreateInfo = 38, CommandPoolCreateInfo = 39, CommandBufferAllocateInfo = 40, @@ -233,6 +234,26 @@ public enum VkImageTiling : int Linear = 1, } +public enum VkFilter : int +{ + Nearest = 0, + Linear = 1, +} + +public enum VkSamplerMipmapMode : int +{ + Nearest = 0, + Linear = 1, +} + +public enum VkSamplerAddressMode : int +{ + Repeat = 0, + MirroredRepeat = 1, + ClampToEdge = 2, + ClampToBorder = 3, +} + public enum VkBlendFactor : int { Zero = 0, diff --git a/src/Engine.Graphics.Vulkan/VulkanHandles.cs b/src/Engine.Graphics.Vulkan/VulkanHandles.cs index dd29647..3eece9f 100644 --- a/src/Engine.Graphics.Vulkan/VulkanHandles.cs +++ b/src/Engine.Graphics.Vulkan/VulkanHandles.cs @@ -44,3 +44,5 @@ public struct VkDescriptorSetLayout { public nint Handle; public static readonly public struct VkDescriptorPool { public nint Handle; public static readonly VkDescriptorPool Null = new() { Handle = 0 }; } [StructLayout(LayoutKind.Sequential)] public struct VkDescriptorSet { public nint Handle; public static readonly VkDescriptorSet Null = new() { Handle = 0 }; } +[StructLayout(LayoutKind.Sequential)] +public struct VkSampler { public nint Handle; public static readonly VkSampler Null = new() { Handle = 0 }; } diff --git a/src/Engine.Graphics.Vulkan/VulkanImGui.cs b/src/Engine.Graphics.Vulkan/VulkanImGui.cs new file mode 100644 index 0000000..9282537 --- /dev/null +++ b/src/Engine.Graphics.Vulkan/VulkanImGui.cs @@ -0,0 +1,726 @@ +using System.Numerics; +using System.Runtime.InteropServices; +using ImGuiNET; + +namespace Engine.Graphics.Vulkan; + +internal sealed unsafe class VulkanImGui : IDisposable +{ + private readonly VkDevice _device; + private readonly VkPhysicalDevice _physicalDevice; + private readonly VkQueue _queue; + private readonly VkCommandPool _commandPool; + private readonly VulkanContext _ctx; + private readonly VkFormat _colorFormat; + private readonly VkFormat _depthFormat; + + private VkPipelineLayout _pipelineLayout; + private VkPipeline _pipeline; + private VkShaderModule _vertModule; + private VkShaderModule _fragModule; + private VkDescriptorSetLayout _descriptorSetLayout; + private VkDescriptorPool _descriptorPool; + private VkDescriptorSet _descriptorSet; + + private VkImage _fontImage; + private VkDeviceMemory _fontImageMemory; + private VkImageView _fontImageView; + private VkSampler _fontSampler; + + private VkBuffer _vertexBuffer; + private VkDeviceMemory _vertexMemory; + private ulong _vertexBufferSize; + + private VkBuffer _indexBuffer; + private VkDeviceMemory _indexMemory; + private ulong _indexBufferSize; + + private bool _disposed; + + public VulkanImGui(VulkanContext ctx, VkCommandPool commandPool, VkFormat colorFormat, VkFormat depthFormat) + { + _ctx = ctx; + _device = ctx.Device; + _physicalDevice = ctx.PhysicalDevice; + _queue = ctx.GraphicsQueue; + _commandPool = commandPool; + _colorFormat = colorFormat; + _depthFormat = depthFormat; + + var imGuiContext = ImGui.CreateContext(); + ImGui.SetCurrentContext(imGuiContext); + + var io = ImGui.GetIO(); + io.ConfigFlags |= ImGuiConfigFlags.NavEnableKeyboard; + io.DisplaySize = new System.Numerics.Vector2(1280, 720); + + UploadFontAtlas(); + CreateDescriptorSetLayout(); + CreateDescriptorPoolAndSet(); + CreatePipeline(); + CreateBuffers(); + + Console.WriteLine("[Vulkan] ImGui initialized"); + } + + private void UploadFontAtlas() + { + var io = ImGui.GetIO(); + io.Fonts.AddFontDefault(); + + io.Fonts.GetTexDataAsRGBA32(out byte* pixels, out int width, out int height); + var uploadSize = (ulong)(width * height * 4); + + var stagingBuffer = VkBuffer.Null; + var stagingMemory = VkDeviceMemory.Null; + CreateBuffer(uploadSize, VkBufferUsageFlags.TransferSrc, + VkMemoryPropertyFlags.HostVisible | VkMemoryPropertyFlags.HostCoherent, + &stagingBuffer, &stagingMemory); + + void* pData = null; + Vk.vkMapMemory(_device, stagingMemory, 0, uploadSize, 0, &pData); + System.Buffer.MemoryCopy(pixels, pData, (long)uploadSize, (long)uploadSize); + Vk.vkUnmapMemory(_device, stagingMemory); + + var imageInfo = new VkImageCreateInfo + { + sType = VkStructureType.ImageCreateInfo, + imageType = VkImageType.Type2D, + format = VkFormat.R8G8B8A8Unorm, + extent = new VkExtent3D { Width = (uint)width, Height = (uint)height, Depth = 1 }, + mipLevels = 1, + arrayLayers = 1, + samples = VkSampleCountFlags.Count1, + tiling = VkImageTiling.Optimal, + usage = VkImageUsageFlags.TransferDst | VkImageUsageFlags.Sampled, + sharingMode = VkSharingMode.Exclusive, + initialLayout = VkImageLayout.Undefined, + }; + + var img = VkImage.Null; + Vk.vkCreateImage(_device, &imageInfo, 0, &img); + _fontImage = img; + + var reqs = new VkMemoryRequirements(); + Vk.vkGetImageMemoryRequirements(_device, _fontImage, &reqs); + var memTypeIndex = _ctx.FindMemoryType(reqs.memoryTypeBits, VkMemoryPropertyFlags.DeviceLocal); + + var allocInfo = new VkMemoryAllocateInfo + { + sType = VkStructureType.MemoryAllocateInfo, + allocationSize = reqs.size, + memoryTypeIndex = memTypeIndex, + }; + + var fmem = VkDeviceMemory.Null; + Vk.vkAllocateMemory(_device, &allocInfo, 0, &fmem); + _fontImageMemory = fmem; + Vk.vkBindImageMemory(_device, _fontImage, _fontImageMemory, 0); + + var cmd = BeginOneTimeCommands(); + TransitionImageLayoutCmd(cmd, _fontImage, + VkImageLayout.Undefined, VkImageLayout.TransferDstOptimal, + 0, 0, 0x1000, 0x2000); + + var region = new VkBufferImageCopyRegion + { + bufferOffset = 0, + bufferRowLength = 0, + bufferImageHeight = 0, + imageSubresource = new VkImageSubresourceLayers + { + aspectMask = VkImageAspectFlags.Color, + mipLevel = 0, + baseArrayLayer = 0, + layerCount = 1, + }, + imageOffset = new VkOffset3D { X = 0, Y = 0, Z = 0 }, + imageExtent = new VkExtent3D { Width = (uint)width, Height = (uint)height, Depth = 1 }, + }; + Vk.vkCmdCopyBufferToImage(cmd, stagingBuffer, _fontImage, VkImageLayout.TransferDstOptimal, 1, ®ion); + + TransitionImageLayoutCmd(cmd, _fontImage, + VkImageLayout.TransferDstOptimal, VkImageLayout.ShaderReadOnlyOptimal, + 0x1000, 0x2000, 0x8, 0x20); + + EndOneTimeCommands(cmd); + + Vk.vkDestroyBuffer(_device, stagingBuffer, 0); + Vk.vkFreeMemory(_device, stagingMemory, 0); + + var viewInfo = new VkImageViewCreateInfo + { + sType = VkStructureType.ImageViewCreateInfo, + image = _fontImage, + viewType = VkImageViewType.Type2D, + format = VkFormat.R8G8B8A8Unorm, + components = new VkComponentMapping { R = VkComponentSwizzle.Identity, G = VkComponentSwizzle.Identity, B = VkComponentSwizzle.Identity, A = VkComponentSwizzle.Identity }, + subresourceRange = new VkImageSubresourceRange { AspectMask = VkImageAspectFlags.Color, BaseMipLevel = 0, LevelCount = 1, BaseArrayLayer = 0, LayerCount = 1 }, + }; + + var fview = VkImageView.Null; + Vk.vkCreateImageView(_device, &viewInfo, 0, &fview); + _fontImageView = fview; + + var samplerInfo = new VkSamplerCreateInfo + { + sType = VkStructureType.SamplerCreateInfo, + magFilter = VkFilter.Linear, + minFilter = VkFilter.Linear, + mipmapMode = VkSamplerMipmapMode.Linear, + addressModeU = VkSamplerAddressMode.Repeat, + addressModeV = VkSamplerAddressMode.Repeat, + addressModeW = VkSamplerAddressMode.Repeat, + minLod = -1000, + maxLod = 1000, + }; + + var fsamp = VkSampler.Null; + Vk.vkCreateSampler(_device, &samplerInfo, 0, &fsamp); + _fontSampler = fsamp; + + io.Fonts.SetTexID((nint)1); + } + + private void CreateDescriptorSetLayout() + { + var binding = new VkDescriptorSetLayoutBinding + { + binding = 0, + descriptorType = VkDescriptorType.CombinedImageSampler, + descriptorCount = 1, + stageFlags = VkShaderStageFlags.Fragment, + }; + + var info = new VkDescriptorSetLayoutCreateInfo + { + sType = VkStructureType.DescriptorSetLayoutCreateInfo, + bindingCount = 1, + pBindings = &binding, + }; + + var layout = VkDescriptorSetLayout.Null; + Vk.vkCreateDescriptorSetLayout(_device, &info, 0, &layout); + _descriptorSetLayout = layout; + } + + private void CreateDescriptorPoolAndSet() + { + var poolSize = new VkDescriptorPoolSize + { + type = VkDescriptorType.CombinedImageSampler, + descriptorCount = 1, + }; + + var poolInfo = new VkDescriptorPoolCreateInfo + { + sType = VkStructureType.DescriptorPoolCreateInfo, + maxSets = 1, + poolSizeCount = 1, + pPoolSizes = &poolSize, + }; + + var pool = VkDescriptorPool.Null; + Vk.vkCreateDescriptorPool(_device, &poolInfo, 0, &pool); + _descriptorPool = pool; + + var layout = _descriptorSetLayout; + var allocInfo = new VkDescriptorSetAllocateInfo + { + sType = VkStructureType.DescriptorSetAllocateInfo, + descriptorPool = _descriptorPool, + descriptorSetCount = 1, + pSetLayouts = &layout, + }; + + var set = VkDescriptorSet.Null; + Vk.vkAllocateDescriptorSets(_device, &allocInfo, &set); + _descriptorSet = set; + + var imageInfo = new VkDescriptorImageInfo + { + sampler = _fontSampler, + imageView = _fontImageView, + imageLayout = VkImageLayout.ShaderReadOnlyOptimal, + }; + + var write = new VkWriteDescriptorSet + { + sType = VkStructureType.WriteDescriptorSet, + dstSet = _descriptorSet, + dstBinding = 0, + descriptorCount = 1, + descriptorType = VkDescriptorType.CombinedImageSampler, + pImageInfo = (nint)(&imageInfo), + }; + + Vk.vkUpdateDescriptorSets(_device, 1, &write, 0, 0); + } + + private void CreatePipeline() + { + _vertModule = CreateShaderModule(LoadShader("Shaders/imgui.vert.spv")); + _fragModule = CreateShaderModule(LoadShader("Shaders/imgui.frag.spv")); + + fixed (byte* pName = "main\0"u8) + { + var stages = stackalloc VkPipelineShaderStageCreateInfo[2]; + stages[0] = new VkPipelineShaderStageCreateInfo + { + sType = VkStructureType.PipelineShaderStageCreateInfo, + stage = VkShaderStageFlags.Vertex, + module = _vertModule, + pName = pName, + }; + stages[1] = new VkPipelineShaderStageCreateInfo + { + sType = VkStructureType.PipelineShaderStageCreateInfo, + stage = VkShaderStageFlags.Fragment, + module = _fragModule, + pName = pName, + }; + + var bindings = stackalloc VkVertexInputBindingDescription[1]; + bindings[0] = new VkVertexInputBindingDescription + { + binding = 0, + stride = 20, + inputRate = VkVertexInputRate.Vertex, + }; + + var attributes = stackalloc VkVertexInputAttributeDescription[3]; + attributes[0] = new VkVertexInputAttributeDescription { location = 0, binding = 0, format = VkFormat.R32G32Sfloat, offset = 0 }; + attributes[1] = new VkVertexInputAttributeDescription { location = 1, binding = 0, format = VkFormat.R32G32Sfloat, offset = 8 }; + attributes[2] = new VkVertexInputAttributeDescription { location = 2, binding = 0, format = VkFormat.R8G8B8A8Unorm, offset = 16 }; + + var vertexInputState = new VkPipelineVertexInputStateCreateInfo + { + sType = VkStructureType.PipelineVertexInputStateCreateInfo, + vertexBindingDescriptionCount = 1, + pVertexBindingDescriptions = bindings, + vertexAttributeDescriptionCount = 3, + pVertexAttributeDescriptions = attributes, + }; + + var inputAssemblyState = new VkPipelineInputAssemblyStateCreateInfo + { + sType = VkStructureType.PipelineInputAssemblyStateCreateInfo, + topology = VkPrimitiveTopology.TriangleList, + primitiveRestartEnable = VkBool32.False, + }; + + var viewportState = new VkPipelineViewportStateCreateInfo + { + sType = VkStructureType.PipelineViewportStateCreateInfo, + viewportCount = 1, + scissorCount = 1, + }; + + var rasterizationState = new VkPipelineRasterizationStateCreateInfo + { + sType = VkStructureType.PipelineRasterizationStateCreateInfo, + polygonMode = VkPolygonMode.Fill, + cullMode = VkCullModeFlags.None, + frontFace = VkFrontFace.CounterClockwise, + lineWidth = 1.0f, + }; + + var multisampleState = new VkPipelineMultisampleStateCreateInfo + { + sType = VkStructureType.PipelineMultisampleStateCreateInfo, + rasterizationSamples = VkSampleCountFlags.Count1, + }; + + var depthStencilState = new VkPipelineDepthStencilStateCreateInfo + { + sType = VkStructureType.PipelineDepthStencilStateCreateInfo, + depthTestEnable = VkBool32.False, + depthWriteEnable = VkBool32.False, + }; + + var blendAttachment = new VkPipelineColorBlendAttachmentState + { + blendEnable = VkBool32.True, + srcColorBlendFactor = VkBlendFactor.SrcAlpha, + dstColorBlendFactor = VkBlendFactor.OneMinusSrcAlpha, + colorBlendOp = VkBlendOp.Add, + srcAlphaBlendFactor = VkBlendFactor.One, + dstAlphaBlendFactor = VkBlendFactor.OneMinusSrcAlpha, + alphaBlendOp = VkBlendOp.Add, + colorWriteMask = VkColorComponentFlags.R | VkColorComponentFlags.G | VkColorComponentFlags.B | VkColorComponentFlags.A, + }; + + var colorBlendState = new VkPipelineColorBlendStateCreateInfo + { + sType = VkStructureType.PipelineColorBlendStateCreateInfo, + logicOpEnable = VkBool32.False, + attachmentCount = 1, + pAttachments = &blendAttachment, + }; + + var dynamicStates = stackalloc VkDynamicState[2]; + dynamicStates[0] = VkDynamicState.Viewport; + dynamicStates[1] = VkDynamicState.Scissor; + + var dynamicState = new VkPipelineDynamicStateCreateInfo + { + sType = VkStructureType.PipelineDynamicStateCreateInfo, + dynamicStateCount = 2, + pDynamicStates = dynamicStates, + }; + + var pushConstantRange = new VkPushConstantRange + { + stageFlags = VkShaderStageFlags.Vertex, + offset = 0, + size = 64, + }; + + var descLayout = _descriptorSetLayout; + var layoutInfo = new VkPipelineLayoutCreateInfo + { + sType = VkStructureType.PipelineLayoutCreateInfo, + setLayoutCount = 1, + pSetLayouts = &descLayout, + pushConstantRangeCount = 1, + pPushConstantRanges = (nint)(&pushConstantRange), + }; + + var pl = VkPipelineLayout.Null; + Vk.vkCreatePipelineLayout(_device, &layoutInfo, 0, &pl); + _pipelineLayout = pl; + + var cf = _colorFormat; + var df = _depthFormat; + var renderingInfo = new VkPipelineRenderingCreateInfo + { + sType = VkStructureType.PipelineRenderingCreateInfo, + colorAttachmentCount = 1, + pColorAttachmentFormats = &cf, + depthAttachmentFormat = df, + }; + + var pipelineInfo = new VkGraphicsPipelineCreateInfo + { + sType = VkStructureType.GraphicsPipelineCreateInfo, + pNext = (nint)(&renderingInfo), + stageCount = 2, + pStages = stages, + pVertexInputState = &vertexInputState, + pInputAssemblyState = &inputAssemblyState, + pViewportState = &viewportState, + pRasterizationState = &rasterizationState, + pMultisampleState = &multisampleState, + pDepthStencilState = &depthStencilState, + pColorBlendState = &colorBlendState, + pDynamicState = &dynamicState, + layout = _pipelineLayout, + renderPass = new VkRenderPass { Handle = 0 }, + subpass = 0, + }; + + var pp = VkPipeline.Null; + Vk.vkCreateGraphicsPipelines(_device, 0, 1, &pipelineInfo, 0, &pp); + _pipeline = pp; + } + } + + private void CreateBuffers() + { + _vertexBufferSize = 65536; + _indexBufferSize = 65536 * 2; + + var vb = VkBuffer.Null; + var vm = VkDeviceMemory.Null; + CreateBuffer(_vertexBufferSize, VkBufferUsageFlags.VertexBuffer, + VkMemoryPropertyFlags.HostVisible | VkMemoryPropertyFlags.HostCoherent, + &vb, &vm); + _vertexBuffer = vb; + _vertexMemory = vm; + + var ib = VkBuffer.Null; + var im = VkDeviceMemory.Null; + CreateBuffer(_indexBufferSize, VkBufferUsageFlags.IndexBuffer, + VkMemoryPropertyFlags.HostVisible | VkMemoryPropertyFlags.HostCoherent, + &ib, &im); + _indexBuffer = ib; + _indexMemory = im; + } + + public void NewFrame() + { + var io = ImGui.GetIO(); + io.DisplaySize = new System.Numerics.Vector2(1280, 720); + io.DeltaTime = 0.016f; + ImGui.NewFrame(); + } + + public void Render(VkCommandBuffer cmd, uint width, uint height) + { + var drawData = ImGui.GetDrawData(); + if (drawData.CmdListsCount == 0) + return; + + var io = ImGui.GetIO(); + var scale = Matrix4x4.CreateScale(2f / io.DisplaySize.X, 2f / io.DisplaySize.Y, 1f); + var trans = Matrix4x4.CreateTranslation(-1f, -1f, 0f); + var mvp = scale * trans; + + EnsureBufferSize(ref _vertexBuffer, ref _vertexMemory, ref _vertexBufferSize, + (ulong)(drawData.TotalVtxCount * 20), VkBufferUsageFlags.VertexBuffer); + EnsureBufferSize(ref _indexBuffer, ref _indexMemory, ref _indexBufferSize, + (ulong)(drawData.TotalIdxCount * 2), VkBufferUsageFlags.IndexBuffer); + + void* pVtx = null; + var totalVtxSize = (ulong)(drawData.TotalVtxCount * 20); + Vk.vkMapMemory(_device, _vertexMemory, 0, totalVtxSize, 0, &pVtx); + void* pIdx = null; + var totalIdxSize = (ulong)(drawData.TotalIdxCount * 2); + Vk.vkMapMemory(_device, _indexMemory, 0, totalIdxSize, 0, &pIdx); + + var vtxPtr = (byte*)pVtx; + var idxPtr = (byte*)pIdx; + for (int n = 0; n < drawData.CmdListsCount; n++) + { + var list = drawData.CmdLists[n]; + var vtxBytes = list.VtxBuffer.Size * 20; + System.Buffer.MemoryCopy((void*)list.VtxBuffer.Data, vtxPtr, vtxBytes, vtxBytes); + vtxPtr += vtxBytes; + + var idxBytes = list.IdxBuffer.Size * 2; + System.Buffer.MemoryCopy((void*)list.IdxBuffer.Data, idxPtr, idxBytes, idxBytes); + idxPtr += idxBytes; + } + + Vk.vkUnmapMemory(_device, _vertexMemory); + Vk.vkUnmapMemory(_device, _indexMemory); + + Vk.vkCmdBindPipeline(cmd, VkPipelineBindPoint.Graphics, _pipeline); + + var vp = new VkViewport { X = 0, Y = 0, Width = width, Height = height, MinDepth = 0, MaxDepth = 1 }; + Vk.vkCmdSetViewport(cmd, 0, 1, &vp); + + var descSet = _descriptorSet; + Vk.vkCmdBindDescriptorSets(cmd, VkPipelineBindPoint.Graphics, _pipelineLayout, + 0, 1, &descSet, 0, null); + + var vb = _vertexBuffer; + ulong voff = 0; + Vk.vkCmdBindVertexBuffers(cmd, 0, 1, &vb, &voff); + Vk.vkCmdBindIndexBuffer(cmd, _indexBuffer, 0, 0); + + Vk.vkCmdPushConstants(cmd, _pipelineLayout, VkShaderStageFlags.Vertex, 0, 64, &mvp); + + int vtxOffset = 0; + int idxOffset = 0; + for (int n = 0; n < drawData.CmdListsCount; n++) + { + var list = drawData.CmdLists[n]; + for (int i = 0; i < list.CmdBuffer.Size; i++) + { + var pcmd = list.CmdBuffer[i]; + var clip = pcmd.ClipRect; + var scissor = new VkRect2D + { + Offset = new VkOffset2D { X = (int)clip.X, Y = (int)clip.Y }, + Extent = new VkExtent2D + { + Width = (uint)(clip.Z - clip.X), + Height = (uint)(clip.W - clip.Y), + }, + }; + Vk.vkCmdSetScissor(cmd, 0, 1, &scissor); + Vk.vkCmdDrawIndexed(cmd, pcmd.ElemCount, 1, (uint)(idxOffset + pcmd.IdxOffset), (int)(vtxOffset + pcmd.VtxOffset), 0); + } + vtxOffset += list.VtxBuffer.Size; + idxOffset += list.IdxBuffer.Size; + } + } + + private void EnsureBufferSize(ref VkBuffer buffer, ref VkDeviceMemory memory, ref ulong currentSize, + ulong neededSize, VkBufferUsageFlags usage) + { + if (neededSize <= currentSize) return; + GrowBuffer(ref buffer, ref memory, ref currentSize, neededSize, usage); + } + + private void CreateBuffer(ulong size, VkBufferUsageFlags usage, VkMemoryPropertyFlags props, + VkBuffer* pBuffer, VkDeviceMemory* pMemory) + { + var info = new VkBufferCreateInfo + { + sType = VkStructureType.BufferCreateInfo, + size = size, + usage = usage, + sharingMode = VkSharingMode.Exclusive, + }; + + Vk.vkCreateBuffer(_device, &info, 0, pBuffer); + + var reqs = new VkMemoryRequirements(); + Vk.vkGetBufferMemoryRequirements(_device, *pBuffer, &reqs); + + var memTypeIndex = _ctx.FindMemoryType(reqs.memoryTypeBits, props); + + var allocInfo = new VkMemoryAllocateInfo + { + sType = VkStructureType.MemoryAllocateInfo, + allocationSize = reqs.size, + memoryTypeIndex = memTypeIndex, + }; + + Vk.vkAllocateMemory(_device, &allocInfo, 0, pMemory); + Vk.vkBindBufferMemory(_device, *pBuffer, *pMemory, 0); + } + + private void GrowBuffer(ref VkBuffer buffer, ref VkDeviceMemory memory, ref ulong currentSize, + ulong neededSize, VkBufferUsageFlags usage) + { + Vk.vkDeviceWaitIdle(_device); + + if (buffer.Handle != 0) Vk.vkDestroyBuffer(_device, buffer, 0); + if (memory.Handle != 0) Vk.vkFreeMemory(_device, memory, 0); + + currentSize = neededSize * 2; + var b = VkBuffer.Null; + var m = VkDeviceMemory.Null; + CreateBuffer(currentSize, usage, + VkMemoryPropertyFlags.HostVisible | VkMemoryPropertyFlags.HostCoherent, + &b, &m); + buffer = b; + memory = m; + } + private VkShaderModule CreateShaderModule(byte[] spv) + { + fixed (byte* pCode = spv) + { + var info = new VkShaderModuleCreateInfo + { + sType = VkStructureType.ShaderModuleCreateInfo, + codeSize = (nuint)spv.Length, + pCode = (uint*)pCode, + }; + + var module = VkShaderModule.Null; + Vk.vkCreateShaderModule(_device, &info, 0, &module); + return module; + } + } + + private VkCommandBuffer BeginOneTimeCommands() + { + var allocInfo = new VkCommandBufferAllocateInfo + { + sType = VkStructureType.CommandBufferAllocateInfo, + commandPool = _commandPool, + 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); + return cmd; + } + + private void EndOneTimeCommands(VkCommandBuffer cmd) + { + 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, _commandPool, 1, &cmd); + } + + private static void TransitionImageLayoutCmd(VkCommandBuffer cmd, VkImage image, + VkImageLayout oldLayout, VkImageLayout newLayout, + ulong srcStage, ulong srcAccess, ulong dstStage, ulong dstAccess) + { + var barrier = new VkImageMemoryBarrier2 + { + sType = VkStructureType.ImageMemoryBarrier2, + srcStageMask = srcStage, + srcAccessMask = srcAccess, + dstStageMask = dstStage, + dstAccessMask = dstAccess, + oldLayout = oldLayout, + newLayout = newLayout, + image = image, + subresourceRange = new VkImageSubresourceRange + { + AspectMask = VkImageAspectFlags.Color, + LevelCount = 1, + LayerCount = 1, + }, + }; + + var depInfo = new VkDependencyInfo + { + sType = VkStructureType.DependencyInfo, + imageMemoryBarrierCount = 1, + pImageMemoryBarriers = &barrier, + }; + + Vk.vkCmdPipelineBarrier2(cmd, &depInfo); + } + + private static byte[] LoadShader(string path) + { + if (!File.Exists(path)) + { + var altPath = Path.Combine(AppContext.BaseDirectory, path); + if (!File.Exists(altPath)) + { + altPath = Path.Combine(AppContext.BaseDirectory, "Shaders", Path.GetFileName(path)); + if (!File.Exists(altPath)) + throw new FileNotFoundException($"Shader file not found: {path}"); + } + return File.ReadAllBytes(altPath); + } + return File.ReadAllBytes(path); + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + Vk.vkDeviceWaitIdle(_device); + + if (_pipeline.Handle != 0) Vk.vkDestroyPipeline(_device, _pipeline, 0); + if (_pipelineLayout.Handle != 0) Vk.vkDestroyPipelineLayout(_device, _pipelineLayout, 0); + if (_descriptorPool.Handle != 0) Vk.vkDestroyDescriptorPool(_device, _descriptorPool, 0); + if (_descriptorSetLayout.Handle != 0) Vk.vkDestroyDescriptorSetLayout(_device, _descriptorSetLayout, 0); + if (_fontSampler.Handle != 0) Vk.vkDestroySampler(_device, _fontSampler, 0); + if (_fontImageView.Handle != 0) Vk.vkDestroyImageView(_device, _fontImageView, 0); + if (_fontImage.Handle != 0) Vk.vkDestroyImage(_device, _fontImage, 0); + if (_fontImageMemory.Handle != 0) Vk.vkFreeMemory(_device, _fontImageMemory, 0); + if (_vertexBuffer.Handle != 0) Vk.vkDestroyBuffer(_device, _vertexBuffer, 0); + if (_vertexMemory.Handle != 0) Vk.vkFreeMemory(_device, _vertexMemory, 0); + if (_indexBuffer.Handle != 0) Vk.vkDestroyBuffer(_device, _indexBuffer, 0); + if (_indexMemory.Handle != 0) Vk.vkFreeMemory(_device, _indexMemory, 0); + if (_vertModule.Handle != 0) Vk.vkDestroyShaderModule(_device, _vertModule, 0); + if (_fragModule.Handle != 0) Vk.vkDestroyShaderModule(_device, _fragModule, 0); + } +} diff --git a/src/Engine.Graphics.Vulkan/VulkanRenderContext.cs b/src/Engine.Graphics.Vulkan/VulkanRenderContext.cs index e08d583..aad2e3e 100644 --- a/src/Engine.Graphics.Vulkan/VulkanRenderContext.cs +++ b/src/Engine.Graphics.Vulkan/VulkanRenderContext.cs @@ -2,8 +2,7 @@ using Engine.Core; namespace Engine.Graphics.Vulkan; -internal sealed class VulkanRenderContext : IRenderContext -{ +internal sealed class VulkanRenderContext : IRenderContext{ private readonly VulkanContext _ctx; private readonly VulkanSwapchain _swapchain; private readonly IWindow _window; diff --git a/src/Engine.Graphics.Vulkan/VulkanRenderer.cs b/src/Engine.Graphics.Vulkan/VulkanRenderer.cs index 1eb8c77..647282d 100644 --- a/src/Engine.Graphics.Vulkan/VulkanRenderer.cs +++ b/src/Engine.Graphics.Vulkan/VulkanRenderer.cs @@ -13,6 +13,7 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen private readonly VulkanSwapchain _swapchain; private readonly VulkanPipeline _pipeline; private readonly VulkanFrameResources _frameResources; + internal readonly VulkanImGui? _imGui; private readonly Dictionary _meshCache = new(); @@ -37,6 +38,21 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen _frameResources = new VulkanFrameResources(ctx.Device, ctx.GraphicsQueueFamilyIndex, swapchain.ImageCount, ctx, _pipeline.DescriptorSetLayout); + + _imGui = new VulkanImGui(ctx, _frameResources.CommandPool, swapchain.Format, swapchain.DepthFormat); + } + + internal VulkanImGui? ImGuiLayer => _imGui; + + public void BeginImGuiFrame() + { + _imGui?.NewFrame(); + } + + public void EndImGuiFrame() + { + if (_imGui == null) return; + ImGuiNET.ImGui.Render(); } public void RenderWorld(World world) @@ -205,6 +221,8 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen Vk.vkCmdDrawIndexed(cmd, dc.indexCount, 1, 0, 0, 0); } + _imGui?.Render(cmd, _swapchain.Extent.Width, _swapchain.Extent.Height); + Vk.vkCmdEndRendering(cmd); TransitionImageLayout(cmd, _swapchain.Images[imageIndex], @@ -389,6 +407,7 @@ internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreen } _meshCache.Clear(); + _imGui?.Dispose(); _frameResources?.Dispose(); _pipeline?.Dispose(); } diff --git a/src/Engine.Graphics.Vulkan/VulkanStructs.cs b/src/Engine.Graphics.Vulkan/VulkanStructs.cs index 08e2d21..446cbed 100644 --- a/src/Engine.Graphics.Vulkan/VulkanStructs.cs +++ b/src/Engine.Graphics.Vulkan/VulkanStructs.cs @@ -957,6 +957,57 @@ public unsafe struct VkImageCreateInfo public VkImageLayout initialLayout; } +[StructLayout(LayoutKind.Sequential)] +public unsafe struct VkImageSubresourceLayers +{ + public VkImageAspectFlags aspectMask; + public uint mipLevel; + public uint baseArrayLayer; + public uint layerCount; +} + +[StructLayout(LayoutKind.Sequential)] +public unsafe struct VkBufferImageCopyRegion +{ + public ulong bufferOffset; + public uint bufferRowLength; + public uint bufferImageHeight; + public VkImageSubresourceLayers imageSubresource; + public VkOffset3D imageOffset; + public VkExtent3D imageExtent; +} + +[StructLayout(LayoutKind.Sequential)] +public unsafe struct VkSamplerCreateInfo +{ + public VkStructureType sType; + public nint pNext; + public uint flags; + public VkFilter magFilter; + public VkFilter minFilter; + public VkSamplerMipmapMode mipmapMode; + public VkSamplerAddressMode addressModeU; + public VkSamplerAddressMode addressModeV; + public VkSamplerAddressMode addressModeW; + public float mipLodBias; + public VkBool32 anisotropyEnable; + public float maxAnisotropy; + public VkBool32 compareEnable; + public VkCompareOp compareOp; + public float minLod; + public float maxLod; + public int borderColor; + public VkBool32 unnormalizedCoordinates; +} + +[StructLayout(LayoutKind.Sequential)] +public unsafe struct VkDescriptorImageInfo +{ + public VkSampler sampler; + public VkImageView imageView; + public VkImageLayout imageLayout; +} + [StructLayout(LayoutKind.Sequential)] public unsafe struct VkPushConstantRange { diff --git a/src/Engine.Graphics/IRenderer.cs b/src/Engine.Graphics/IRenderer.cs index b1466b3..04d59e8 100644 --- a/src/Engine.Graphics/IRenderer.cs +++ b/src/Engine.Graphics/IRenderer.cs @@ -13,4 +13,16 @@ public interface IRenderer : IDisposable void RequestScreenshot(string path); bool IsScreenshotRequested { get; } IScreenshotProvider ScreenshotProvider { get; } + + /// + /// Called before RenderWorld to begin a new ImGui frame. + /// Null if the backend doesn't support ImGui. + /// + void BeginImGuiFrame() { } + + /// + /// Called after RenderWorld to render ImGui draw data. + /// Null if the backend doesn't support ImGui. + /// + void EndImGuiFrame() { } }