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);
+ }
+}