feat: Step 2 colored triangle with Vulkan graphics pipeline
This commit is contained in:
@@ -24,3 +24,4 @@ Thumbs.db
|
|||||||
!*.csproj
|
!*.csproj
|
||||||
!*.sln
|
!*.sln
|
||||||
!launchSettings.json
|
!launchSettings.json
|
||||||
|
.playwright-mcp/
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class Program
|
|||||||
{
|
{
|
||||||
static void Main(string[] args)
|
static void Main(string[] args)
|
||||||
{
|
{
|
||||||
Console.WriteLine("Cortex Engine Step 1 — Starting up...");
|
Console.WriteLine("Cortex Engine Step 2 — Starting up...");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -17,7 +17,7 @@ class Program
|
|||||||
var input = new InputMapping();
|
var input = new InputMapping();
|
||||||
using var vulkan = new VulkanContext(window, enableValidation: true);
|
using var vulkan = new VulkanContext(window, enableValidation: true);
|
||||||
using var swapchain = new Swapchain(vulkan);
|
using var swapchain = new Swapchain(vulkan);
|
||||||
using var renderer = new ClearRenderer(vulkan, swapchain);
|
using var renderer = new TriangleRenderer(vulkan, swapchain);
|
||||||
|
|
||||||
var frames = 0;
|
var frames = 0;
|
||||||
var lastFpsTime = 0.0;
|
var lastFpsTime = 0.0;
|
||||||
@@ -30,13 +30,7 @@ class Program
|
|||||||
// Note: SDL events are already polled in PumpEvents.
|
// Note: SDL events are already polled in PumpEvents.
|
||||||
// In a real engine, the window would expose an event iterator.
|
// In a real engine, the window would expose an event iterator.
|
||||||
|
|
||||||
// Animate clear color over time.
|
renderer.RenderFrame();
|
||||||
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);
|
|
||||||
|
|
||||||
frames++;
|
frames++;
|
||||||
if (timing.TotalTime - lastFpsTime >= 1.0)
|
if (timing.TotalTime - lastFpsTime >= 1.0)
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ public sealed unsafe class ClearRenderer : IDisposable
|
|||||||
private readonly VulkanContext _context;
|
private readonly VulkanContext _context;
|
||||||
private readonly Swapchain _swapchain;
|
private readonly Swapchain _swapchain;
|
||||||
private VkCommandPool _commandPool;
|
private VkCommandPool _commandPool;
|
||||||
private VkCommandBuffer[] _commandBuffers;
|
private VkCommandBuffer[] _commandBuffers = null!;
|
||||||
private VkSemaphore[] _imageAvailableSemaphores;
|
private VkSemaphore[] _imageAvailableSemaphores = null!;
|
||||||
private VkSemaphore[] _renderFinishedSemaphores;
|
private VkSemaphore[] _renderFinishedSemaphores = null!;
|
||||||
private VkFence[] _inFlightFences;
|
private VkFence[] _inFlightFences = null!;
|
||||||
private int _currentFrame;
|
private int _currentFrame;
|
||||||
|
|
||||||
public ClearRenderer(VulkanContext context, Swapchain swapchain)
|
public ClearRenderer(VulkanContext context, Swapchain swapchain)
|
||||||
|
|||||||
@@ -21,6 +21,10 @@
|
|||||||
<PackageReference Include="Vortice.Vulkan" Version="3.2.3" />
|
<PackageReference Include="Vortice.Vulkan" Version="3.2.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="Shaders\*.spv" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Engine.Core\Engine.Core.csproj" />
|
<ProjectReference Include="..\Engine.Core\Engine.Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -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.
@@ -12,9 +12,9 @@ public sealed unsafe class Swapchain : IDisposable
|
|||||||
private readonly VulkanContext _context;
|
private readonly VulkanContext _context;
|
||||||
private VkRenderPass _renderPass;
|
private VkRenderPass _renderPass;
|
||||||
private VkSwapchainKHR _swapchain;
|
private VkSwapchainKHR _swapchain;
|
||||||
private VkImage[] _images;
|
private VkImage[] _images = null!;
|
||||||
private VkImageView[] _imageViews;
|
private VkImageView[] _imageViews = null!;
|
||||||
private VkFramebuffer[] _framebuffers;
|
private VkFramebuffer[] _framebuffers = null!;
|
||||||
private VkSurfaceFormatKHR _surfaceFormat;
|
private VkSurfaceFormatKHR _surfaceFormat;
|
||||||
private VkPresentModeKHR _presentMode;
|
private VkPresentModeKHR _presentMode;
|
||||||
private VkExtent2D _extent;
|
private VkExtent2D _extent;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user