feat: rebuild Vulkan renderer from scratch — pure P/Invoke triangle (Vulkan 1.3)

- Complete rewrite of Engine.Graphics.Vulkan with pure P/Invoke (no wrapper libs)
- Vulkan 1.3: dynamic rendering (vkCmdBeginRendering/vkCmdEndRendering),
  synchronization2 (vkQueueSubmit2, vkCmdPipelineBarrier2)
- Split types into VulkanHandles.cs, VulkanEnums.cs, VulkanStructs.cs
- Staging buffer → device-local vertex buffer pattern
- Correct swapchain semaphore indexing (per-image, not per-frame-in-flight)
- VK_EXT_debug_utils debug messenger with validation layer fallback
- Dynamic viewport/scissor (no pipeline recreation on resize)
- Simplified Program.cs to triangle-only rendering
- Removed old Silk.NET renderer, ImGui, PBR shaders, screenshot code
- Updated VULKAN_IMPLEMENTATION_PLAN.md with full architecture decisions
This commit is contained in:
emil28092005
2026-06-18 01:49:25 +03:00
parent ee98e4ad08
commit 2e0970e769
44 changed files with 3882 additions and 5273 deletions
+172 -245
View File
@@ -1,279 +1,207 @@
using System.Runtime.InteropServices;
using System.Text;
using Engine.Core;
namespace Engine.Graphics.Vulkan;
public sealed unsafe class VulkanPipeline : IDisposable
internal sealed unsafe class VulkanPipeline : IDisposable
{
public VkPipelineLayout PipelineLayout;
public VkPipeline Pipeline;
public VkDescriptorSetLayout DescriptorSetLayout;
public VkShaderModule VertexShader;
public VkShaderModule FragmentShader;
public VkShaderModule VertModule;
public VkShaderModule FragModule;
private readonly VulkanContext _ctx;
private readonly VkDevice _device;
private bool _disposed;
public const int PushConstantSize = 144;
public const int FrameUboSize = 16 + 16 + 16 * 4 * 8;
public VulkanPipeline(VulkanContext ctx, VkRenderPass renderPass)
public VulkanPipeline(VkDevice device, VkFormat colorFormat, byte[] vertSpv, byte[] fragSpv)
{
_ctx = ctx;
Create(renderPass);
}
_device = device;
VertModule = CreateShaderModule(vertSpv);
FragModule = CreateShaderModule(fragSpv);
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
fixed (byte* pName = "main\0"u8)
{
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 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 bindingDesc = new VkVertexInputBindingDescription
{
binding = 0,
stride = 36,
inputRate = 0
};
var bindings = stackalloc VkVertexInputBindingDescription[1];
bindings[0] = new VkVertexInputBindingDescription
{
binding = 0,
stride = (uint)sizeof(Vertex),
inputRate = VkVertexInputRate.Vertex,
};
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 };
var attributes = stackalloc VkVertexInputAttributeDescription[3];
attributes[0] = new VkVertexInputAttributeDescription
{
location = 0,
binding = 0,
format = VkFormat.R32G32B32Sfloat,
offset = 0,
};
attributes[1] = new VkVertexInputAttributeDescription
{
location = 1,
binding = 0,
format = VkFormat.R32G32B32Sfloat,
offset = 12,
};
attributes[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 vertexInputState = new VkPipelineVertexInputStateCreateInfo
{
sType = VkStructureType.PipelineVertexInputStateCreateInfo,
vertexBindingDescriptionCount = 1,
pVertexBindingDescriptions = bindings,
vertexAttributeDescriptionCount = 3,
pVertexAttributeDescriptions = attributes,
};
var inputAssemblyState = new VkPipelineInputAssemblyStateCreateInfo
{
sType = VkStructureType.PipelineInputAssemblyStateCreateInfo,
pNext = null,
flags = 0,
topology = VkPrimitiveTopology.TriangleList,
primitiveRestartEnable = 0
};
var inputAssemblyState = new VkPipelineInputAssemblyStateCreateInfo
{
sType = VkStructureType.PipelineInputAssemblyStateCreateInfo,
topology = VkPrimitiveTopology.TriangleList,
primitiveRestartEnable = VkBool32.False,
};
var viewport = new VkViewport { x = 0, y = 0, width = 1280, height = 720, minDepth = 0, maxDepth = 1 };
var scissor = new VkRect2D { offset = new VkOffset2D { x = 0, y = 0 }, extent = new VkExtent2D { width = 1280, height = 720 } };
var viewportState = new VkPipelineViewportStateCreateInfo
{
sType = VkStructureType.PipelineViewportStateCreateInfo,
viewportCount = 1,
pViewports = null,
scissorCount = 1,
pScissors = null,
};
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,
depthClampEnable = VkBool32.False,
rasterizerDiscardEnable = VkBool32.False,
polygonMode = VkPolygonMode.Fill,
cullMode = VkCullModeFlags.None,
frontFace = VkFrontFace.CounterClockwise,
depthBiasEnable = VkBool32.False,
lineWidth = 1.0f,
};
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,
rasterizationSamples = VkSampleCountFlags.Count1,
sampleShadingEnable = VkBool32.False,
};
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 blendAttachment = new VkPipelineColorBlendAttachmentState
{
blendEnable = VkBool32.False,
colorWriteMask = VkColorComponentFlags.R | VkColorComponentFlags.G | VkColorComponentFlags.B | VkColorComponentFlags.A,
};
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 colorBlendState = new VkPipelineColorBlendStateCreateInfo
{
sType = VkStructureType.PipelineColorBlendStateCreateInfo,
logicOpEnable = VkBool32.False,
attachmentCount = 1,
pAttachments = &blendAttachment,
};
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
};
var dynamicStates = stackalloc VkDynamicState[2];
dynamicStates[0] = VkDynamicState.Viewport;
dynamicStates[1] = VkDynamicState.Scissor;
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 dynamicState = new VkPipelineDynamicStateCreateInfo
{
sType = VkStructureType.PipelineDynamicStateCreateInfo,
dynamicStateCount = 2,
pDynamicStates = dynamicStates,
};
var dynamicStates = stackalloc VkDynamicState[2];
dynamicStates[0] = VkDynamicState.Viewport;
dynamicStates[1] = VkDynamicState.Scissor;
var layoutInfo = new VkPipelineLayoutCreateInfo
{
sType = VkStructureType.PipelineLayoutCreateInfo,
setLayoutCount = 0,
pushConstantRangeCount = 0,
};
VkPipelineDynamicStateCreateInfo dynamicState = default;
dynamicState.sType = VkStructureType.PipelineDynamicStateCreateInfo;
dynamicState.dynamicStateCount = 2;
dynamicState.pDynamicStates = dynamicStates;
fixed (VkPipelineLayout* layoutPtr = &PipelineLayout)
{
var result = Vk.vkCreatePipelineLayout(_device, &layoutInfo, 0, layoutPtr);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkCreatePipelineLayout failed: {result}");
}
var uboBinding = new VkDescriptorSetLayoutBinding
{
binding = 0,
descriptorType = VkDescriptorType.UniformBuffer,
descriptorCount = 1,
stageFlags = VkShaderStageFlags.Vertex | VkShaderStageFlags.Fragment,
pImmutableSamplers = null
};
var renderingInfo = new VkPipelineRenderingCreateInfo
{
sType = VkStructureType.PipelineRenderingCreateInfo,
colorAttachmentCount = 1,
pColorAttachmentFormats = &colorFormat,
};
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;
var pipelineInfo = new VkGraphicsPipelineCreateInfo
{
sType = VkStructureType.GraphicsPipelineCreateInfo,
pNext = (nint)(&renderingInfo),
stageCount = 2,
pStages = stages,
pVertexInputState = &vertexInputState,
pInputAssemblyState = &inputAssemblyState,
pViewportState = &viewportState,
pRasterizationState = &rasterizationState,
pMultisampleState = &multisampleState,
pColorBlendState = &colorBlendState,
pDynamicState = &dynamicState,
layout = PipelineLayout,
renderPass = new VkRenderPass { Handle = 0 },
subpass = 0,
};
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 = &dynamicState;
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;
fixed (VkPipeline* pipePtr = &Pipeline)
{
var result = Vk.vkCreateGraphicsPipelines(_device, 0, 1, &pipelineInfo, 0, pipePtr);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkCreateGraphicsPipelines failed: {result}");
}
}
Vk.FreeUtf8(mainName);
Console.WriteLine("[Vulkan] Graphics pipeline created.");
Console.WriteLine("[Vulkan] Graphics pipeline created (dynamic rendering)");
}
private VkShaderModule CreateShaderModule(string path)
private VkShaderModule CreateShaderModule(byte[] spv)
{
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)
fixed (byte* pCode = spv)
{
VkShaderModuleCreateInfo createInfo;
createInfo.sType = VkStructureType.ShaderModuleCreateInfo;
createInfo.pNext = null;
createInfo.flags = 0;
createInfo.codeSize = codeSize;
createInfo.pCode = (uint*)pCode;
var info = new VkShaderModuleCreateInfo
{
sType = VkStructureType.ShaderModuleCreateInfo,
codeSize = (nuint)spv.Length,
pCode = (uint*)pCode,
};
VkShaderModule module;
var result = Vk.vkCreateShaderModule(_ctx.Device, &createInfo, null, &module);
Vk.CheckResult(result, $"vkCreateShaderModule ({path})");
var module = VkShaderModule.Null;
var result = Vk.vkCreateShaderModule(_device, &info, 0, &module);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkCreateShaderModule failed: {result}");
return module;
}
}
@@ -283,10 +211,9 @@ public sealed unsafe class VulkanPipeline : IDisposable
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);
if (Pipeline.Handle != 0) Vk.vkDestroyPipeline(_device, Pipeline, 0);
if (PipelineLayout.Handle != 0) Vk.vkDestroyPipelineLayout(_device, PipelineLayout, 0);
if (FragModule.Handle != 0) Vk.vkDestroyShaderModule(_device, FragModule, 0);
if (VertModule.Handle != 0) Vk.vkDestroyShaderModule(_device, VertModule, 0);
}
}