diff --git a/.gitignore b/.gitignore index 1c788f0..5367373 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ Thumbs.db !*.csproj !*.sln !launchSettings.json +.playwright-mcp/ diff --git a/src/CortexEngine.App/Program.cs b/src/CortexEngine.App/Program.cs index 49afec4..66a07ca 100644 --- a/src/CortexEngine.App/Program.cs +++ b/src/CortexEngine.App/Program.cs @@ -8,7 +8,7 @@ class Program { static void Main(string[] args) { - Console.WriteLine("Cortex Engine Step 1 — Starting up..."); + Console.WriteLine("Cortex Engine Step 2 — Starting up..."); try { @@ -17,7 +17,7 @@ class Program var input = new InputMapping(); using var vulkan = new VulkanContext(window, enableValidation: true); using var swapchain = new Swapchain(vulkan); - using var renderer = new ClearRenderer(vulkan, swapchain); + using var renderer = new TriangleRenderer(vulkan, swapchain); var frames = 0; var lastFpsTime = 0.0; @@ -30,13 +30,7 @@ class Program // Note: SDL events are already polled in PumpEvents. // In a real engine, the window would expose an event iterator. - // Animate clear color over time. - var t = (float)timing.TotalTime; - var r = MathF.Sin(t * 0.5f) * 0.5f + 0.5f; - var g = MathF.Sin(t * 0.7f + 2.0f) * 0.5f + 0.5f; - var b = MathF.Sin(t * 0.9f + 4.0f) * 0.5f + 0.5f; - - renderer.RenderFrame(r, g, b); + renderer.RenderFrame(); frames++; if (timing.TotalTime - lastFpsTime >= 1.0) diff --git a/src/Engine.Graphics/ClearRenderer.cs b/src/Engine.Graphics/ClearRenderer.cs index f63d90b..959da14 100644 --- a/src/Engine.Graphics/ClearRenderer.cs +++ b/src/Engine.Graphics/ClearRenderer.cs @@ -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) diff --git a/src/Engine.Graphics/Engine.Graphics.csproj b/src/Engine.Graphics/Engine.Graphics.csproj index 19e8ac8..b5eed86 100644 --- a/src/Engine.Graphics/Engine.Graphics.csproj +++ b/src/Engine.Graphics/Engine.Graphics.csproj @@ -21,6 +21,10 @@ + + + + diff --git a/src/Engine.Graphics/ShaderLoader.cs b/src/Engine.Graphics/ShaderLoader.cs new file mode 100644 index 0000000..4c07366 --- /dev/null +++ b/src/Engine.Graphics/ShaderLoader.cs @@ -0,0 +1,24 @@ +using System; +using System.IO; +using System.Reflection; + +namespace Engine.Graphics; + +/// +/// Loads SPIR-V shader bytecode embedded in the assembly. +/// +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(); + } +} diff --git a/src/Engine.Graphics/Shaders/fragment.spv b/src/Engine.Graphics/Shaders/fragment.spv new file mode 100644 index 0000000..a0856c9 Binary files /dev/null and b/src/Engine.Graphics/Shaders/fragment.spv differ diff --git a/src/Engine.Graphics/Shaders/vertex.spv b/src/Engine.Graphics/Shaders/vertex.spv new file mode 100644 index 0000000..f05ea45 Binary files /dev/null and b/src/Engine.Graphics/Shaders/vertex.spv differ diff --git a/src/Engine.Graphics/Swapchain.cs b/src/Engine.Graphics/Swapchain.cs index 60d27ab..9849b24 100644 --- a/src/Engine.Graphics/Swapchain.cs +++ b/src/Engine.Graphics/Swapchain.cs @@ -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; diff --git a/src/Engine.Graphics/TriangleRenderer.cs b/src/Engine.Graphics/TriangleRenderer.cs new file mode 100644 index 0000000..1b0b5f8 --- /dev/null +++ b/src/Engine.Graphics/TriangleRenderer.cs @@ -0,0 +1,217 @@ +using System; +using Vortice.Vulkan; + +namespace Engine.Graphics; + +/// +/// Renders a colored triangle using a vertex buffer and a simple graphics pipeline. +/// +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(); + } +} diff --git a/src/Engine.Graphics/VertexBuffer.cs b/src/Engine.Graphics/VertexBuffer.cs new file mode 100644 index 0000000..ba3f2a1 --- /dev/null +++ b/src/Engine.Graphics/VertexBuffer.cs @@ -0,0 +1,105 @@ +using System; +using Vortice.Vulkan; + +namespace Engine.Graphics; + +/// +/// Interleaved vertex: vec2 position + vec3 color. +/// +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 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 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); + } +} diff --git a/src/Engine.Graphics/VulkanPipeline.cs b/src/Engine.Graphics/VulkanPipeline.cs new file mode 100644 index 0000000..5a74224 --- /dev/null +++ b/src/Engine.Graphics/VulkanPipeline.cs @@ -0,0 +1,225 @@ +using System; +using Vortice.Vulkan; + +namespace Engine.Graphics; + +/// +/// A simple graphics pipeline for a single vertex/fragment shader pair. +/// Assumes a triangle with vec2 position + vec3 color per vertex. +/// +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); + } +}