Files
Cortex_Engine/src/Engine.Graphics.Vulkan/VulkanPipeline.cs
T
emil28092005 749fbc8df4 feat: ImGui integration + real screenshot capture via Vulkan
ImGui:
- ImGui.NET 1.91.6.1 NuGet package
- VulkanImGui: font texture upload, ImGui pipeline (blending, no depth), vertex/index buffer streaming
- GLSL 450 ImGui shaders (vert: scale/translate push constants, frag: font texture sampler)
- Compiled to SPIR-V via @webgpu/glslang
- Debug overlay: FPS, camera position, entity count, light list
- 1600+ FPS with ImGui active

Screenshot:
- vkCmdCopyImageToBuffer embedded in main command buffer
- Layout transition PresentSrcKHR→TransferSrcOptimal→PresentSrcKHR
- Deferred read: staging buffer read on next frame after fence
- BMP format written directly to FileStream (row by row)
- All 16 camera tour screenshots captured (1280x720x32bpp)

All 66 tests pass.
2026-06-17 23:39:38 +03:00

284 lines
11 KiB
C#

using System.Runtime.InteropServices;
using System.Text;
namespace Engine.Graphics.Vulkan;
public 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);
}
}