feat: Step 2 colored triangle with Vulkan graphics pipeline

This commit is contained in:
emil28092005
2026-06-16 17:49:29 +03:00
parent 6397488c61
commit 4627c96814
11 changed files with 586 additions and 16 deletions
+4 -4
View File
@@ -12,10 +12,10 @@ public sealed unsafe class ClearRenderer : IDisposable
private readonly VulkanContext _context;
private readonly Swapchain _swapchain;
private VkCommandPool _commandPool;
private VkCommandBuffer[] _commandBuffers;
private VkSemaphore[] _imageAvailableSemaphores;
private VkSemaphore[] _renderFinishedSemaphores;
private VkFence[] _inFlightFences;
private VkCommandBuffer[] _commandBuffers = null!;
private VkSemaphore[] _imageAvailableSemaphores = null!;
private VkSemaphore[] _renderFinishedSemaphores = null!;
private VkFence[] _inFlightFences = null!;
private int _currentFrame;
public ClearRenderer(VulkanContext context, Swapchain swapchain)
@@ -21,6 +21,10 @@
<PackageReference Include="Vortice.Vulkan" Version="3.2.3" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Shaders\*.spv" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Engine.Core\Engine.Core.csproj" />
</ItemGroup>
+24
View File
@@ -0,0 +1,24 @@
using System;
using System.IO;
using System.Reflection;
namespace Engine.Graphics;
/// <summary>
/// Loads SPIR-V shader bytecode embedded in the assembly.
/// </summary>
public static class ShaderLoader
{
public static byte[] Load(string name)
{
var assembly = Assembly.GetExecutingAssembly();
var resourceName = $"Engine.Graphics.Shaders.{name}";
using var stream = assembly.GetManifestResourceStream(resourceName)
?? throw new InvalidOperationException($"Embedded shader resource not found: {resourceName}");
using var memory = new MemoryStream();
stream.CopyTo(memory);
return memory.ToArray();
}
}
Binary file not shown.
Binary file not shown.
+3 -3
View File
@@ -12,9 +12,9 @@ public sealed unsafe class Swapchain : IDisposable
private readonly VulkanContext _context;
private VkRenderPass _renderPass;
private VkSwapchainKHR _swapchain;
private VkImage[] _images;
private VkImageView[] _imageViews;
private VkFramebuffer[] _framebuffers;
private VkImage[] _images = null!;
private VkImageView[] _imageViews = null!;
private VkFramebuffer[] _framebuffers = null!;
private VkSurfaceFormatKHR _surfaceFormat;
private VkPresentModeKHR _presentMode;
private VkExtent2D _extent;
+217
View File
@@ -0,0 +1,217 @@
using System;
using Vortice.Vulkan;
namespace Engine.Graphics;
/// <summary>
/// Renders a colored triangle using a vertex buffer and a simple graphics pipeline.
/// </summary>
public sealed unsafe class TriangleRenderer : IDisposable
{
private readonly VulkanContext _context;
private readonly Swapchain _swapchain;
private readonly VulkanPipeline _pipeline;
private readonly VertexBuffer _vertexBuffer;
private VkCommandPool _commandPool;
private VkCommandBuffer[] _commandBuffers = null!;
private VkSemaphore[] _imageAvailableSemaphores = null!;
private VkSemaphore[] _renderFinishedSemaphores = null!;
private VkFence[] _inFlightFences = null!;
private int _currentFrame;
public TriangleRenderer(VulkanContext context, Swapchain swapchain)
{
_context = context;
_swapchain = swapchain;
_pipeline = new VulkanPipeline(context, swapchain);
_vertexBuffer = CreateTriangleBuffer();
CreateCommandPool();
CreateCommandBuffers();
CreateSyncObjects();
}
private VertexBuffer CreateTriangleBuffer()
{
var vertices = new[]
{
// Position (vec2) + Color (vec3)
0.0f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.0f, 0.0f, 1.0f
};
var bytes = new byte[vertices.Length * sizeof(float)];
fixed (byte* p = bytes)
fixed (float* v = vertices)
{
Buffer.MemoryCopy(v, p, bytes.Length, vertices.Length * sizeof(float));
}
return new VertexBuffer(_context, bytes);
}
private void CreateCommandPool()
{
var createInfo = new VkCommandPoolCreateInfo
{
sType = VkStructureType.CommandPoolCreateInfo,
queueFamilyIndex = _context.GraphicsFamilyIndex,
flags = VkCommandPoolCreateFlags.ResetCommandBuffer
};
var result = _context.DeviceApi.vkCreateCommandPool(&createInfo, null, out _commandPool);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkCreateCommandPool failed: {result}");
}
private void CreateCommandBuffers()
{
_commandBuffers = new VkCommandBuffer[2];
for (var i = 0; i < _commandBuffers.Length; i++)
{
var allocInfo = new VkCommandBufferAllocateInfo
{
sType = VkStructureType.CommandBufferAllocateInfo,
commandPool = _commandPool,
level = VkCommandBufferLevel.Primary,
commandBufferCount = 1
};
var result = _context.DeviceApi.vkAllocateCommandBuffer(&allocInfo, out _commandBuffers[i]);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkAllocateCommandBuffers failed: {result}");
}
}
private void CreateSyncObjects()
{
_imageAvailableSemaphores = new VkSemaphore[2];
_renderFinishedSemaphores = new VkSemaphore[2];
_inFlightFences = new VkFence[2];
var semaphoreInfo = new VkSemaphoreCreateInfo { sType = VkStructureType.SemaphoreCreateInfo };
var fenceInfo = new VkFenceCreateInfo
{
sType = VkStructureType.FenceCreateInfo,
flags = VkFenceCreateFlags.Signaled
};
for (var i = 0; i < 2; i++)
{
_context.DeviceApi.vkCreateSemaphore(&semaphoreInfo, null, out _imageAvailableSemaphores[i]);
_context.DeviceApi.vkCreateSemaphore(&semaphoreInfo, null, out _renderFinishedSemaphores[i]);
_context.DeviceApi.vkCreateFence(&fenceInfo, null, out _inFlightFences[i]);
}
}
public void RenderFrame()
{
var frame = _currentFrame % 2;
_context.DeviceApi.vkWaitForFences(_inFlightFences[frame], true, ulong.MaxValue);
_context.DeviceApi.vkResetFences(_inFlightFences[frame]);
var result = _context.DeviceApi.vkAcquireNextImageKHR(
_swapchain.Handle,
ulong.MaxValue,
_imageAvailableSemaphores[frame],
VkFence.Null,
out var imageIndex);
if (result == VkResult.ErrorOutOfDateKHR)
return;
var cmd = _commandBuffers[frame];
_context.DeviceApi.vkResetCommandBuffer(cmd, VkCommandBufferResetFlags.None);
var beginInfo = new VkCommandBufferBeginInfo
{
sType = VkStructureType.CommandBufferBeginInfo,
flags = VkCommandBufferUsageFlags.OneTimeSubmit
};
_context.DeviceApi.vkBeginCommandBuffer(cmd, &beginInfo);
var clearColor = new VkClearValue(0.0f, 0.0f, 0.0f, 1.0f);
var renderPassInfo = new VkRenderPassBeginInfo
{
sType = VkStructureType.RenderPassBeginInfo,
renderPass = _swapchain.RenderPass,
framebuffer = _swapchain.Framebuffers[imageIndex],
renderArea = new VkRect2D(0, 0, _swapchain.Extent.width, _swapchain.Extent.height),
clearValueCount = 1,
pClearValues = &clearColor
};
_context.DeviceApi.vkCmdBeginRenderPass(cmd, &renderPassInfo, VkSubpassContents.Inline);
_context.DeviceApi.vkCmdBindPipeline(cmd, VkPipelineBindPoint.Graphics, _pipeline.Handle);
var viewport = new VkViewport(0, 0, _swapchain.Extent.width, _swapchain.Extent.height, 0, 1);
var scissor = new VkRect2D(0, 0, _swapchain.Extent.width, _swapchain.Extent.height);
_context.DeviceApi.vkCmdSetViewport(cmd, 0, viewport);
_context.DeviceApi.vkCmdSetScissor(cmd, 0, scissor);
var vertexBuffer = _vertexBuffer.Buffer;
var offset = 0ul;
_context.DeviceApi.vkCmdBindVertexBuffers(cmd, 0, 1, &vertexBuffer, &offset);
_context.DeviceApi.vkCmdDraw(cmd, 3, 1, 0, 0);
_context.DeviceApi.vkCmdEndRenderPass(cmd);
_context.DeviceApi.vkEndCommandBuffer(cmd);
var waitSemaphore = _imageAvailableSemaphores[frame];
var signalSemaphore = _renderFinishedSemaphores[frame];
var stageMask = VkPipelineStageFlags.ColorAttachmentOutput;
var submitInfo = new VkSubmitInfo
{
sType = VkStructureType.SubmitInfo,
waitSemaphoreCount = 1,
pWaitSemaphores = &waitSemaphore,
pWaitDstStageMask = &stageMask,
commandBufferCount = 1,
pCommandBuffers = &cmd,
signalSemaphoreCount = 1,
pSignalSemaphores = &signalSemaphore
};
_context.DeviceApi.vkQueueSubmit(_context.GraphicsQueue, 1, &submitInfo, _inFlightFences[frame]);
var swapchain = _swapchain.Handle;
var presentInfo = new VkPresentInfoKHR
{
sType = VkStructureType.PresentInfoKHR,
waitSemaphoreCount = 1,
pWaitSemaphores = &signalSemaphore,
swapchainCount = 1,
pSwapchains = &swapchain,
pImageIndices = &imageIndex
};
_context.DeviceApi.vkQueuePresentKHR(_context.PresentQueue, &presentInfo);
_currentFrame++;
}
public void Dispose()
{
_context.DeviceApi.vkDeviceWaitIdle();
for (var i = 0; i < 2; i++)
{
if (_renderFinishedSemaphores[i] != VkSemaphore.Null)
_context.DeviceApi.vkDestroySemaphore(_renderFinishedSemaphores[i]);
if (_imageAvailableSemaphores[i] != VkSemaphore.Null)
_context.DeviceApi.vkDestroySemaphore(_imageAvailableSemaphores[i]);
if (_inFlightFences[i] != VkFence.Null)
_context.DeviceApi.vkDestroyFence(_inFlightFences[i]);
}
if (_commandPool != VkCommandPool.Null)
_context.DeviceApi.vkDestroyCommandPool(_commandPool);
_vertexBuffer.Dispose();
_pipeline.Dispose();
}
}
+105
View File
@@ -0,0 +1,105 @@
using System;
using Vortice.Vulkan;
namespace Engine.Graphics;
/// <summary>
/// Interleaved vertex: vec2 position + vec3 color.
/// </summary>
public sealed unsafe class VertexBuffer : IDisposable
{
private readonly VulkanContext _context;
public VkBuffer Buffer { get; }
public VkDeviceMemory Memory { get; }
public ulong Size { get; }
public VertexBuffer(VulkanContext context, ReadOnlySpan<byte> data)
{
_context = context;
Size = (ulong)data.Length;
Buffer = CreateBuffer(Size, VkBufferUsageFlags.VertexBuffer);
var memoryRequirements = GetMemoryRequirements(Buffer);
Memory = AllocateMemory(memoryRequirements, VkMemoryPropertyFlags.HostVisible | VkMemoryPropertyFlags.HostCoherent);
var result = _context.DeviceApi.vkBindBufferMemory(Buffer, Memory, 0);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkBindBufferMemory failed: {result}");
CopyData(data);
}
private VkBuffer CreateBuffer(ulong size, VkBufferUsageFlags usage)
{
var createInfo = new VkBufferCreateInfo
{
sType = VkStructureType.BufferCreateInfo,
size = size,
usage = usage,
sharingMode = VkSharingMode.Exclusive
};
var result = _context.DeviceApi.vkCreateBuffer(&createInfo, null, out var buffer);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkCreateBuffer failed: {result}");
return buffer;
}
private VkMemoryRequirements GetMemoryRequirements(VkBuffer buffer)
{
_context.DeviceApi.vkGetBufferMemoryRequirements(buffer, out var requirements);
return requirements;
}
private VkDeviceMemory AllocateMemory(VkMemoryRequirements requirements, VkMemoryPropertyFlags properties)
{
var memoryTypeIndex = FindMemoryType(requirements.memoryTypeBits, properties);
var allocateInfo = new VkMemoryAllocateInfo
{
sType = VkStructureType.MemoryAllocateInfo,
allocationSize = requirements.size,
memoryTypeIndex = memoryTypeIndex
};
var result = _context.DeviceApi.vkAllocateMemory(&allocateInfo, null, out var memory);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkAllocateMemory failed: {result}");
return memory;
}
private uint FindMemoryType(uint typeFilter, VkMemoryPropertyFlags properties)
{
_context.InstanceApi.vkGetPhysicalDeviceMemoryProperties(_context.PhysicalDevice, out var memoryProperties);
for (var i = 0; i < memoryProperties.memoryTypeCount; i++)
{
if ((typeFilter & (1u << i)) != 0 &&
(memoryProperties.memoryTypes[i].propertyFlags & properties) == properties)
{
return (uint)i;
}
}
throw new InvalidOperationException("Failed to find suitable memory type.");
}
private void CopyData(ReadOnlySpan<byte> data)
{
void* mappedData;
var result = _context.DeviceApi.vkMapMemory(Memory, 0, Size, VkMemoryMapFlags.None, &mappedData);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkMapMemory failed: {result}");
fixed (byte* src = data)
{
System.Buffer.MemoryCopy(src, mappedData, (long)Size, data.Length);
}
_context.DeviceApi.vkUnmapMemory(Memory);
}
public void Dispose()
{
_context.DeviceApi.vkDeviceWaitIdle();
_context.DeviceApi.vkDestroyBuffer(Buffer);
_context.DeviceApi.vkFreeMemory(Memory);
}
}
+225
View File
@@ -0,0 +1,225 @@
using System;
using Vortice.Vulkan;
namespace Engine.Graphics;
/// <summary>
/// A simple graphics pipeline for a single vertex/fragment shader pair.
/// Assumes a triangle with vec2 position + vec3 color per vertex.
/// </summary>
public sealed unsafe class VulkanPipeline : IDisposable
{
private readonly VulkanContext _context;
private readonly Swapchain _swapchain;
public VkPipeline Handle { get; }
public VkPipelineLayout Layout { get; }
private readonly VkShaderModule _vertexModule;
private readonly VkShaderModule _fragmentModule;
public VulkanPipeline(VulkanContext context, Swapchain swapchain)
{
_context = context;
_swapchain = swapchain;
_vertexModule = CreateShaderModule("vertex.spv");
_fragmentModule = CreateShaderModule("fragment.spv");
Layout = CreatePipelineLayout();
Handle = CreateGraphicsPipeline();
}
private VkShaderModule CreateShaderModule(string resourceName)
{
var code = ShaderLoader.Load(resourceName);
if (code.Length % 4 != 0)
throw new InvalidOperationException($"Shader {resourceName} size is not a multiple of 4.");
fixed (byte* pCode = code)
{
var createInfo = new VkShaderModuleCreateInfo
{
sType = VkStructureType.ShaderModuleCreateInfo,
codeSize = (nuint)code.Length,
pCode = (uint*)pCode
};
var result = _context.DeviceApi.vkCreateShaderModule(&createInfo, null, out var module);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkCreateShaderModule failed for {resourceName}: {result}");
return module;
}
}
private VkPipelineLayout CreatePipelineLayout()
{
var createInfo = new VkPipelineLayoutCreateInfo
{
sType = VkStructureType.PipelineLayoutCreateInfo,
setLayoutCount = 0,
pushConstantRangeCount = 0
};
var result = _context.DeviceApi.vkCreatePipelineLayout(&createInfo, null, out var layout);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkCreatePipelineLayout failed: {result}");
return layout;
}
private VkPipeline CreateGraphicsPipeline()
{
var stages = new[]
{
new VkPipelineShaderStageCreateInfo
{
sType = VkStructureType.PipelineShaderStageCreateInfo,
stage = VkShaderStageFlags.Vertex,
module = _vertexModule,
pName = VkStringInterop.ConvertToUnmanaged("main")
},
new VkPipelineShaderStageCreateInfo
{
sType = VkStructureType.PipelineShaderStageCreateInfo,
stage = VkShaderStageFlags.Fragment,
module = _fragmentModule,
pName = VkStringInterop.ConvertToUnmanaged("main")
}
};
var bindingDescription = new VkVertexInputBindingDescription
{
binding = 0,
stride = (uint)(5 * sizeof(float)),
inputRate = VkVertexInputRate.Vertex
};
var attributeDescriptions = new[]
{
new VkVertexInputAttributeDescription
{
binding = 0,
location = 0,
format = VkFormat.R32G32Sfloat,
offset = 0
},
new VkVertexInputAttributeDescription
{
binding = 0,
location = 1,
format = VkFormat.R32G32B32Sfloat,
offset = (uint)(2 * sizeof(float))
}
};
VkPipelineVertexInputStateCreateInfo vertexInputInfo;
fixed (VkVertexInputAttributeDescription* pAttributes = attributeDescriptions)
{
vertexInputInfo = new VkPipelineVertexInputStateCreateInfo
{
sType = VkStructureType.PipelineVertexInputStateCreateInfo,
vertexBindingDescriptionCount = 1,
pVertexBindingDescriptions = &bindingDescription,
vertexAttributeDescriptionCount = (uint)attributeDescriptions.Length,
pVertexAttributeDescriptions = pAttributes
};
}
var inputAssembly = new VkPipelineInputAssemblyStateCreateInfo
{
sType = VkStructureType.PipelineInputAssemblyStateCreateInfo,
topology = VkPrimitiveTopology.TriangleList,
primitiveRestartEnable = false
};
var viewport = new VkViewport(0, 0, _swapchain.Extent.width, _swapchain.Extent.height, 0, 1);
var scissor = new VkRect2D(0, 0, _swapchain.Extent.width, _swapchain.Extent.height);
var viewportState = new VkPipelineViewportStateCreateInfo
{
sType = VkStructureType.PipelineViewportStateCreateInfo,
viewportCount = 1,
pViewports = &viewport,
scissorCount = 1,
pScissors = &scissor
};
var rasterizer = new VkPipelineRasterizationStateCreateInfo
{
sType = VkStructureType.PipelineRasterizationStateCreateInfo,
polygonMode = VkPolygonMode.Fill,
cullMode = VkCullModeFlags.None,
frontFace = VkFrontFace.Clockwise,
lineWidth = 1.0f
};
var multisampling = new VkPipelineMultisampleStateCreateInfo
{
sType = VkStructureType.PipelineMultisampleStateCreateInfo,
rasterizationSamples = VkSampleCountFlags.Count1,
sampleShadingEnable = false
};
var colorBlendAttachment = new VkPipelineColorBlendAttachmentState
{
colorWriteMask = VkColorComponentFlags.R | VkColorComponentFlags.G | VkColorComponentFlags.B | VkColorComponentFlags.A
};
var colorBlending = new VkPipelineColorBlendStateCreateInfo
{
sType = VkStructureType.PipelineColorBlendStateCreateInfo,
attachmentCount = 1,
pAttachments = &colorBlendAttachment
};
var dynamicStates = new[] { VkDynamicState.Viewport, VkDynamicState.Scissor };
VkPipelineDynamicStateCreateInfo dynamicState;
fixed (VkDynamicState* pDynamic = dynamicStates)
{
dynamicState = new VkPipelineDynamicStateCreateInfo
{
sType = VkStructureType.PipelineDynamicStateCreateInfo,
dynamicStateCount = (uint)dynamicStates.Length,
pDynamicStates = pDynamic
};
}
VkPipeline pipeline;
fixed (VkPipelineShaderStageCreateInfo* pStages = stages)
{
var createInfo = new VkGraphicsPipelineCreateInfo
{
sType = VkStructureType.GraphicsPipelineCreateInfo,
stageCount = (uint)stages.Length,
pStages = pStages,
pVertexInputState = &vertexInputInfo,
pInputAssemblyState = &inputAssembly,
pViewportState = &viewportState,
pRasterizationState = &rasterizer,
pMultisampleState = &multisampling,
pColorBlendState = &colorBlending,
pDynamicState = &dynamicState,
layout = Layout,
renderPass = _swapchain.RenderPass,
subpass = 0
};
var result = _context.DeviceApi.vkCreateGraphicsPipelines(VkPipelineCache.Null, 1, &createInfo, null, &pipeline);
if (result != VkResult.Success)
throw new InvalidOperationException($"vkCreateGraphicsPipelines failed: {result}");
}
VkStringInterop.Free(stages[0].pName);
VkStringInterop.Free(stages[1].pName);
return pipeline;
}
public void Dispose()
{
_context.DeviceApi.vkDeviceWaitIdle();
_context.DeviceApi.vkDestroyPipeline(Handle);
_context.DeviceApi.vkDestroyPipelineLayout(Layout);
_context.DeviceApi.vkDestroyShaderModule(_vertexModule);
_context.DeviceApi.vkDestroyShaderModule(_fragmentModule);
}
}