feat: rebuild Vulkan renderer from scratch — pure P/Invoke triangle (Vulkan 1.3)

- Complete rewrite of Engine.Graphics.Vulkan with pure P/Invoke (no wrapper libs)
- Vulkan 1.3: dynamic rendering (vkCmdBeginRendering/vkCmdEndRendering),
  synchronization2 (vkQueueSubmit2, vkCmdPipelineBarrier2)
- Split types into VulkanHandles.cs, VulkanEnums.cs, VulkanStructs.cs
- Staging buffer → device-local vertex buffer pattern
- Correct swapchain semaphore indexing (per-image, not per-frame-in-flight)
- VK_EXT_debug_utils debug messenger with validation layer fallback
- Dynamic viewport/scissor (no pipeline recreation on resize)
- Simplified Program.cs to triangle-only rendering
- Removed old Silk.NET renderer, ImGui, PBR shaders, screenshot code
- Updated VULKAN_IMPLEMENTATION_PLAN.md with full architecture decisions
This commit is contained in:
emil28092005
2026-06-18 01:49:25 +03:00
parent ee98e4ad08
commit 2e0970e769
44 changed files with 3882 additions and 5273 deletions
+204 -619
View File
@@ -1,704 +1,289 @@
using System.Numerics;
using System.Runtime.InteropServices;
using Engine.Core;
using Engine.Core.Components;
using Engine.Graphics;
using Flecs.NET.Core;
using System.Runtime.InteropServices;
namespace Engine.Graphics.Vulkan;
public sealed unsafe class VulkanRenderer : IRenderer, IScreenshotProvider
internal sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreenshotProvider
{
public readonly VulkanContext _ctx;
public readonly VulkanSwapchain _swapchain;
private readonly VulkanContext _ctx;
private readonly VulkanSwapchain _swapchain;
private readonly VulkanPipeline _pipeline;
public VulkanImGui? ImGuiLayer;
private VkCommandPool _commandPool;
private VkCommandBuffer[] _commandBuffers = Array.Empty<VkCommandBuffer>();
private VkSemaphore[] _imageAvailableSemaphores = Array.Empty<VkSemaphore>();
private VkSemaphore[] _renderFinishedSemaphores = Array.Empty<VkSemaphore>();
private VkFence[] _inFlightFences = Array.Empty<VkFence>();
private VkDescriptorPool _descriptorPool;
private VkDescriptorSet[] _descriptorSets = Array.Empty<VkDescriptorSet>();
private VulkanBuffer[] _uboBuffers = Array.Empty<VulkanBuffer>();
private const int MaxFramesInFlight = 2;
private int _currentFrame;
private uint _imageIndex;
private bool _resized;
private readonly Dictionary<ulong, (VulkanBuffer vertex, VulkanBuffer index, uint indexCount)> _meshCache = new();
private readonly VulkanFrameResources _frameResources;
private readonly VulkanVertexBuffer _vertexBuffer;
private int _frameIndex;
private bool _disposed;
private bool _screenshotRequested;
private string _screenshotPath = "";
private TaskCompletionSource<byte[]>? _screenshotTcs;
private VulkanBuffer? _screenshotStaging;
private uint _screenshotImageIndex;
private bool _screenshotPending;
private string? _screenshotPath;
public bool IsScreenshotRequested => _screenshotRequested;
public IScreenshotProvider ScreenshotProvider => this;
public VulkanRenderer(VulkanContext ctx, VulkanSwapchain swapchain, VulkanPipeline pipeline)
public VulkanRenderer(VulkanContext ctx, VulkanSwapchain swapchain)
{
_ctx = ctx;
_swapchain = swapchain;
_pipeline = pipeline;
CreateCommandPool();
CreateSyncObjects();
CreateDescriptorPool();
CreateDescriptorSets();
CreateCommandBuffers();
}
var vertSpv = LoadShader("Shaders/triangle.vert.spv");
var fragSpv = LoadShader("Shaders/triangle.frag.spv");
private unsafe void CreateCommandPool()
{
VkCommandPoolCreateInfo createInfo;
createInfo.sType = VkStructureType.CommandPoolCreateInfo;
createInfo.pNext = null;
createInfo.flags = 0x00000002;
createInfo.queueFamilyIndex = _ctx.GraphicsFamily;
_pipeline = new VulkanPipeline(ctx.Device, swapchain.Format, vertSpv, fragSpv);
VkCommandPool pool;
VkResult result = Vk.vkCreateCommandPool(_ctx.Device, &createInfo, null, &pool);
Vk.CheckResult(result, "vkCreateCommandPool");
_commandPool = pool;
}
_frameResources = new VulkanFrameResources(ctx.Device, ctx.GraphicsQueueFamilyIndex, swapchain.ImageCount);
private unsafe void CreateSyncObjects()
{
_imageAvailableSemaphores = new VkSemaphore[MaxFramesInFlight];
_renderFinishedSemaphores = new VkSemaphore[MaxFramesInFlight];
_inFlightFences = new VkFence[MaxFramesInFlight];
for (var i = 0; i < MaxFramesInFlight; i++)
var vertices = new Vertex[]
{
VkSemaphoreCreateInfo semInfo;
semInfo.sType = VkStructureType.SemaphoreCreateInfo;
semInfo.pNext = null;
semInfo.flags = 0;
VkSemaphore sem1, sem2;
Vk.CheckResult(Vk.vkCreateSemaphore(_ctx.Device, &semInfo, null, &sem1), "vkCreateSemaphore");
Vk.CheckResult(Vk.vkCreateSemaphore(_ctx.Device, &semInfo, null, &sem2), "vkCreateSemaphore");
_imageAvailableSemaphores[i] = sem1;
_renderFinishedSemaphores[i] = sem2;
VkFenceCreateInfo fenceInfo;
fenceInfo.sType = VkStructureType.FenceCreateInfo;
fenceInfo.pNext = null;
fenceInfo.flags = VkFenceCreateFlags.Signaled;
VkFence fence;
Vk.CheckResult(Vk.vkCreateFence(_ctx.Device, &fenceInfo, null, &fence), "vkCreateFence");
_inFlightFences[i] = fence;
}
}
private unsafe void CreateDescriptorPool()
{
var poolSize = new VkDescriptorPoolSize
{
type = VkDescriptorType.UniformBuffer,
descriptorCount = (uint)MaxFramesInFlight
new(new Vector3( 0.0f, -0.5f, 0.0f), new Vector3(1.0f, 0.0f, 0.0f), new Vector3(0, 0, 1)),
new(new Vector3( 0.5f, 0.5f, 0.0f), new Vector3(0.0f, 1.0f, 0.0f), new Vector3(0, 0, 1)),
new(new Vector3(-0.5f, 0.5f, 0.0f), new Vector3(0.0f, 0.0f, 1.0f), new Vector3(0, 0, 1)),
};
VkDescriptorPoolCreateInfo createInfo;
createInfo.sType = VkStructureType.DescriptorPoolCreateInfo;
createInfo.pNext = null;
createInfo.flags = VkDescriptorPoolCreateFlags.FreeDescriptorSet;
createInfo.maxSets = (uint)MaxFramesInFlight;
createInfo.poolSizeCount = 1;
createInfo.pPoolSizes = &poolSize;
VkDescriptorPool pool;
Vk.CheckResult(Vk.vkCreateDescriptorPool(_ctx.Device, &createInfo, null, &pool), "vkCreateDescriptorPool");
_descriptorPool = pool;
_vertexBuffer = new VulkanVertexBuffer(ctx.Device, ctx.PhysicalDevice,
_frameResources.CommandPool, ctx.GraphicsQueue, ctx, vertices);
}
private unsafe void CreateDescriptorSets()
public void RenderWorld(World world)
{
_uboBuffers = new VulkanBuffer[MaxFramesInFlight];
_descriptorSets = new VkDescriptorSet[MaxFramesInFlight];
var layouts = new VkDescriptorSetLayout[MaxFramesInFlight];
for (var i = 0; i < MaxFramesInFlight; i++)
layouts[i] = _pipeline.DescriptorSetLayout;
VkDescriptorSetAllocateInfo allocInfo;
allocInfo.sType = VkStructureType.DescriptorSetAllocateInfo;
allocInfo.pNext = null;
allocInfo.descriptorPool = _descriptorPool;
allocInfo.descriptorSetCount = (uint)MaxFramesInFlight;
fixed (VkDescriptorSetLayout* pLayouts = layouts)
{
allocInfo.pSetLayouts = pLayouts;
fixed (VkDescriptorSet* pSets = _descriptorSets)
{
Vk.CheckResult(Vk.vkAllocateDescriptorSets(_ctx.Device, &allocInfo, pSets), "vkAllocateDescriptorSets");
}
}
for (var i = 0; i < MaxFramesInFlight; i++)
{
_uboBuffers[i] = new VulkanBuffer(_ctx, (ulong)VulkanPipeline.FrameUboSize,
VkBufferUsageFlags.UniformBuffer,
VkMemoryPropertyFlags.HostVisible | VkMemoryPropertyFlags.HostCoherent);
var bufferInfo = new VkDescriptorBufferInfo
{
buffer = _uboBuffers[i].Buffer,
offset = 0,
range = (ulong)VulkanPipeline.FrameUboSize
};
VkWriteDescriptorSet writeInfo;
writeInfo.sType = VkStructureType.WriteDescriptorSet;
writeInfo.pNext = null;
writeInfo.dstSet = _descriptorSets[i];
writeInfo.dstBinding = 0;
writeInfo.dstArrayElement = 0;
writeInfo.descriptorCount = 1;
writeInfo.descriptorType = VkDescriptorType.UniformBuffer;
writeInfo.pImageInfo = null;
writeInfo.pBufferInfo = &bufferInfo;
writeInfo.pTexelBufferView = null;
Vk.vkUpdateDescriptorSets(_ctx.Device, 1, &writeInfo, 0, null);
}
Render();
}
private unsafe void CreateCommandBuffers()
private void Render()
{
_commandBuffers = new VkCommandBuffer[MaxFramesInFlight];
_frameResources.WaitFrame(_frameIndex);
VkCommandBufferAllocateInfo allocInfo;
allocInfo.sType = VkStructureType.CommandBufferAllocateInfo;
allocInfo.pNext = null;
allocInfo.commandPool = _commandPool;
allocInfo.level = VkCommandBufferLevel.Primary;
allocInfo.commandBufferCount = (uint)MaxFramesInFlight;
uint imageIndex;
var acquireResult = Vk.vkAcquireNextImageKHR(_ctx.Device, _swapchain.Swapchain,
ulong.MaxValue, _frameResources.AcquireSemaphores[_frameIndex], VkFence.Null, &imageIndex);
fixed (VkCommandBuffer* pCmds = _commandBuffers)
if (acquireResult == VkResult.ErrorOutOfDateKHR || acquireResult == VkResult.SuboptimalKHR)
{
Vk.CheckResult(Vk.vkAllocateCommandBuffers(_ctx.Device, &allocInfo, pCmds), "vkAllocateCommandBuffers");
}
}
public unsafe void RenderWorld(World world)
{
VkFence fence = _inFlightFences[_currentFrame];
Vk.CheckResult(Vk.vkWaitForFences(_ctx.Device, 1, &fence, 1, ulong.MaxValue), "vkWaitForFences");
if (_screenshotPending && _screenshotStaging != null)
{
FinishScreenshot();
}
uint imageIndex = 0;
VkSemaphore imgAvailSem = _imageAvailableSemaphores[_currentFrame];
var acquireResult = Vk.vkAcquireNextImageKHR(_ctx.Device, _swapchain.Swapchain, ulong.MaxValue,
imgAvailSem, default, &imageIndex);
if (acquireResult == VkResult.ErrorOutOfDateKHR || _resized)
{
_resized = false;
_swapchain.Recreate(_swapchain.Extent.width, _swapchain.Extent.height);
RecreateCommandBuffers();
_swapchain.Recreate(_ctx.SurfaceExtent.Width == 0 ? 1280 : (int)_ctx.SurfaceExtent.Width,
_ctx.SurfaceExtent.Height == 0 ? 720 : (int)_ctx.SurfaceExtent.Height);
Render();
return;
}
Vk.CheckResult(acquireResult, "vkAcquireNextImageKHR");
_imageIndex = imageIndex;
if (acquireResult != VkResult.Success)
throw new InvalidOperationException($"vkAcquireNextImageKHR failed: {acquireResult}");
VkFence fenceReset = _inFlightFences[_currentFrame];
Vk.CheckResult(Vk.vkResetFences(_ctx.Device, 1, &fenceReset), "vkResetFences");
var cmd = _frameResources.CommandBuffers[_frameIndex];
Vk.vkResetCommandBuffer(cmd, 0);
var cmd = _commandBuffers[_currentFrame];
Vk.CheckResult(Vk.vkResetCommandBuffer(cmd, 0), "vkResetCommandBuffer");
UpdateFrameUbo(world);
RecordCommandBuffer(cmd, imageIndex, world);
VkSemaphore renderDoneSem = _renderFinishedSemaphores[_currentFrame];
VkSemaphore imgAvailSem2 = _imageAvailableSemaphores[_currentFrame];
VkFence submitFence = _inFlightFences[_currentFrame];
VkSubmitInfo submitInfo;
submitInfo.sType = VkStructureType.SubmitInfo;
submitInfo.pNext = null;
submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores = &imgAvailSem2;
var waitStage = (ulong)VkPipelineStageFlags.ColorAttachmentOutput;
submitInfo.pWaitDstStageMask = &waitStage;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &cmd;
submitInfo.signalSemaphoreCount = 1;
submitInfo.pSignalSemaphores = &renderDoneSem;
Vk.CheckResult(Vk.vkQueueSubmit(_ctx.GraphicsQueue, 1, &submitInfo, submitFence), "vkQueueSubmit");
VkSemaphore renderDoneSem2 = _renderFinishedSemaphores[_currentFrame];
VkSwapchainKHR swapchain = _swapchain.Swapchain;
VkPresentInfoKHR presentInfo;
presentInfo.sType = VkStructureType.PresentInfoKHR;
presentInfo.pNext = null;
presentInfo.waitSemaphoreCount = 1;
presentInfo.pWaitSemaphores = &renderDoneSem2;
presentInfo.swapchainCount = 1;
presentInfo.pSwapchains = &swapchain;
presentInfo.pImageIndices = &imageIndex;
presentInfo.pResults = null;
var presentResult = Vk.vkQueuePresentKHR(_ctx.GraphicsQueue, &presentInfo);
if (presentResult == VkResult.ErrorOutOfDateKHR || presentResult == VkResult.SuboptimalKHR || _resized)
var beginInfo = new VkCommandBufferBeginInfo
{
_resized = false;
_swapchain.Recreate(_swapchain.Extent.width, _swapchain.Extent.height);
RecreateCommandBuffers();
}
else
sType = VkStructureType.CommandBufferBeginInfo,
flags = VkCommandBufferUsageFlags.OneTimeSubmit,
};
Vk.vkBeginCommandBuffer(cmd, &beginInfo);
TransitionImageLayout(cmd, _swapchain.Images[imageIndex],
VkImageLayout.Undefined, VkImageLayout.ColorAttachmentOptimal,
0, 0,
0x400, 0x100);
var clearValue = new VkClearValue
{
Vk.CheckResult(presentResult, "vkQueuePresentKHR");
}
_currentFrame = (_currentFrame + 1) % MaxFramesInFlight;
}
private unsafe void FinishScreenshot()
{
try
{
var width = _swapchain.Extent.width;
var height = _swapchain.Extent.height;
var dir = Path.GetDirectoryName(_screenshotPath);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
var src = (byte*)_screenshotStaging!.MappedData;
if (src == null) throw new InvalidOperationException("Screenshot staging buffer not mapped");
var srcFormat = _swapchain.ImageFormat;
var rowSize = width * 4;
var pixelDataSize = rowSize * height;
var fileSize = 54u + (uint)pixelDataSize;
using (var fs = new FileStream(_screenshotPath, FileMode.Create))
using (var bw = new BinaryWriter(fs))
{
bw.Write((byte)'B');
bw.Write((byte)'M');
bw.Write(fileSize);
bw.Write(0u);
bw.Write(54u);
bw.Write(40u);
bw.Write((uint)width);
bw.Write((uint)height);
bw.Write((ushort)1);
bw.Write((ushort)32);
bw.Write((uint)pixelDataSize);
bw.Write(0u);
bw.Write(0u);
bw.Write(0u);
bw.Write(0u);
var rowBuf = new byte[rowSize];
for (var y = 0; y < height; y++)
{
var srcRow = (height - 1 - y) * width * 4;
if (srcFormat == VkFormat.B8G8R8A8Srgb || srcFormat == VkFormat.B8G8R8A8Unorm)
{
for (var x = 0; x < width; x++)
{
rowBuf[x * 4 + 0] = src[srcRow + x * 4 + 0];
rowBuf[x * 4 + 1] = src[srcRow + x * 4 + 1];
rowBuf[x * 4 + 2] = src[srcRow + x * 4 + 2];
rowBuf[x * 4 + 3] = src[srcRow + x * 4 + 3];
}
}
else
{
for (var x = 0; x < width; x++)
{
rowBuf[x * 4 + 0] = src[srcRow + x * 4 + 2];
rowBuf[x * 4 + 1] = src[srcRow + x * 4 + 1];
rowBuf[x * 4 + 2] = src[srcRow + x * 4 + 0];
rowBuf[x * 4 + 3] = src[srcRow + x * 4 + 3];
}
}
bw.Write(rowBuf, 0, rowSize);
}
}
_screenshotStaging.Dispose();
_screenshotStaging = null;
_screenshotPending = false;
Console.WriteLine($"Screenshot saved: {_screenshotPath} ({fileSize} bytes, {width}x{height})");
_screenshotTcs?.TrySetResult(Array.Empty<byte>());
_screenshotTcs = null;
}
catch (Exception ex)
{
Console.WriteLine($"Screenshot capture failed: {ex}");
_screenshotStaging?.Dispose();
_screenshotStaging = null;
_screenshotPending = false;
_screenshotTcs?.TrySetException(ex);
_screenshotTcs = null;
}
}
private unsafe void UpdateFrameUbo(World world)
{
Vector3 camPos = Vector3.Zero;
var view = Matrix4x4.Identity;
var proj = Matrix4x4.Identity;
world.Each((Entity e, ref Camera cam) =>
{
camPos = cam.Position;
view = cam.GetViewMatrix();
proj = cam.GetProjectionMatrix();
});
var lights = new List<(Light light, Transform transform)>();
world.Each((Entity e, ref Light light, ref Transform transform) =>
{
lights.Add((light, transform));
});
var uboData = new byte[VulkanPipeline.FrameUboSize];
fixed (byte* pUbo = uboData)
{
var p = (float*)pUbo;
p[0] = camPos.X; p[1] = camPos.Y; p[2] = camPos.Z;
p[3] = (uint)Math.Min(lights.Count, 16);
p[4] = 0.15f; p[5] = 0.15f; p[6] = 0.2f; p[7] = 0f;
for (var i = 0; i < Math.Min(lights.Count, 16); i++)
{
var (light, _) = lights[i];
var baseIdx = 8 + i * 8;
if (light.IsDirectional)
{
p[baseIdx + 0] = light.Direction.X;
p[baseIdx + 1] = light.Direction.Y;
p[baseIdx + 2] = light.Direction.Z;
p[baseIdx + 3] = -light.Intensity;
p[baseIdx + 4] = light.Color.X;
p[baseIdx + 5] = light.Color.Y;
p[baseIdx + 6] = light.Color.Z;
p[baseIdx + 7] = 0f;
}
else
{
p[baseIdx + 0] = light.Position.X;
p[baseIdx + 1] = light.Position.Y;
p[baseIdx + 2] = light.Position.Z;
p[baseIdx + 3] = light.Intensity;
p[baseIdx + 4] = light.Color.X;
p[baseIdx + 5] = light.Color.Y;
p[baseIdx + 6] = light.Color.Z;
p[baseIdx + 7] = light.Range;
}
}
_uboBuffers[_currentFrame].Write(pUbo, (ulong)VulkanPipeline.FrameUboSize);
}
}
private unsafe void RecordCommandBuffer(VkCommandBuffer cmd, uint imageIndex, World world)
{
VkCommandBufferBeginInfo beginInfo;
beginInfo.sType = VkStructureType.CommandBufferBeginInfo;
beginInfo.pNext = null;
beginInfo.flags = 0;
beginInfo.pInheritanceInfo = null;
Vk.CheckResult(Vk.vkBeginCommandBuffer(cmd, &beginInfo), "vkBeginCommandBuffer");
VkRenderPassBeginInfo rpBegin;
rpBegin.sType = VkStructureType.RenderPassBeginInfo;
rpBegin.pNext = null;
rpBegin.renderPass = _swapchain.RenderPass;
rpBegin.framebuffer = _swapchain.Framebuffers[imageIndex];
rpBegin.renderArea = new VkRect2D
{
offset = new VkOffset2D { x = 0, y = 0 },
extent = _swapchain.Extent
Color = new VkClearColorValue { Float0 = 0.02f, Float1 = 0.02f, Float2 = 0.02f, Float3 = 1.0f },
};
var clearValues = new VkClearValue[2];
clearValues[0] = new VkClearValue { color = new VkClearColorValue { r = 0.05f, g = 0.05f, b = 0.08f, a = 1.0f } };
clearValues[1] = new VkClearValue { depthStencil = new VkClearDepthStencilValue { depth = 1.0f, stencil = 0 } };
fixed (VkClearValue* pClear = clearValues)
var colorAttachment = new VkRenderingAttachmentInfo
{
rpBegin.clearValueCount = 2;
rpBegin.pClearValues = pClear;
sType = VkStructureType.RenderingAttachmentInfo,
imageView = _swapchain.ImageViews[imageIndex],
imageLayout = VkImageLayout.ColorAttachmentOptimal,
loadOp = VkAttachmentLoadOp.Clear,
storeOp = VkAttachmentStoreOp.Store,
clearValue = clearValue,
};
Vk.vkCmdBeginRenderPass(cmd, &rpBegin, VkSubpassContents.Inline);
}
var renderingInfo = new VkRenderingInfo
{
sType = VkStructureType.RenderingInfo,
renderArea = new VkRect2D
{
Offset = new VkOffset2D { X = 0, Y = 0 },
Extent = _swapchain.Extent,
},
layerCount = 1,
colorAttachmentCount = 1,
pColorAttachments = &colorAttachment,
};
Vk.vkCmdBindPipeline(cmd, 0, _pipeline.Pipeline);
Vk.vkCmdBeginRendering(cmd, &renderingInfo);
Vk.vkCmdBindPipeline(cmd, VkPipelineBindPoint.Graphics, _pipeline.Pipeline);
var viewport = new VkViewport
{
x = 0, y = 0,
width = _swapchain.Extent.width,
height = _swapchain.Extent.height,
minDepth = 0, maxDepth = 1
X = 0, Y = 0,
Width = _swapchain.Extent.Width,
Height = _swapchain.Extent.Height,
MinDepth = 0, MaxDepth = 1,
};
Vk.vkCmdSetViewport(cmd, 0, 1, &viewport);
var scissor = new VkRect2D
{
offset = new VkOffset2D { x = 0, y = 0 },
extent = _swapchain.Extent
Offset = new VkOffset2D { X = 0, Y = 0 },
Extent = _swapchain.Extent,
};
Vk.vkCmdSetScissor(cmd, 0, 1, &scissor);
var ds = _descriptorSets[_currentFrame];
Vk.vkCmdBindDescriptorSets(cmd, 0, _pipeline.PipelineLayout, 0, 1, &ds, 0, null);
var bufferHandle = _vertexBuffer.Buffer;
ulong offset = 0;
Vk.vkCmdBindVertexBuffers(cmd, 0, 1, &bufferHandle, &offset);
world.Each((Entity e, ref Transform transform, ref Mesh mesh, ref Material material) =>
Vk.vkCmdDraw(cmd, 3, 1, 0, 0);
Vk.vkCmdEndRendering(cmd);
TransitionImageLayout(cmd, _swapchain.Images[imageIndex],
VkImageLayout.ColorAttachmentOptimal, VkImageLayout.PresentSrcKHR,
0x400, 0x100,
0x8000, 0);
Vk.vkEndCommandBuffer(cmd);
var waitInfo = new VkSemaphoreSubmitInfo
{
var meshKey = (ulong)mesh.GetHashCode();
if (!_meshCache.TryGetValue(meshKey, out var meshBuffers))
{
if (mesh.Vertices.Length == 0 || mesh.Indices.Length == 0) return;
sType = VkStructureType.SemaphoreSubmitInfo,
semaphore = _frameResources.AcquireSemaphores[_frameIndex],
stageMask = 0x400,
};
var vertexBuffer = VulkanBuffer.CreateDeviceLocal(_ctx, _commandPool, mesh.Vertices, VkBufferUsageFlags.VertexBuffer);
var indexBuffer = VulkanBuffer.CreateDeviceLocal(_ctx, _commandPool, mesh.Indices, VkBufferUsageFlags.IndexBuffer);
meshBuffers = (vertexBuffer, indexBuffer, (uint)mesh.Indices.Length);
_meshCache[meshKey] = meshBuffers;
}
var model = transform.GetMatrix();
var view = Matrix4x4.Identity;
var proj = Matrix4x4.Identity;
world.Each((Entity camE, ref Camera cam) =>
{
view = cam.GetViewMatrix();
proj = cam.GetProjectionMatrix();
});
var mvp = proj * view * model;
var pushData = new byte[VulkanPipeline.PushConstantSize];
fixed (byte* pPush = pushData)
{
var p = (float*)pPush;
p[0] = mvp.M11; p[1] = mvp.M12; p[2] = mvp.M13; p[3] = mvp.M14;
p[4] = mvp.M21; p[5] = mvp.M22; p[6] = mvp.M23; p[7] = mvp.M24;
p[8] = mvp.M31; p[9] = mvp.M32; p[10] = mvp.M33; p[11] = mvp.M34;
p[12] = mvp.M41; p[13] = mvp.M42; p[14] = mvp.M43; p[15] = mvp.M44;
p[16] = model.M11; p[17] = model.M12; p[18] = model.M13; p[19] = model.M14;
p[20] = model.M21; p[21] = model.M22; p[22] = model.M23; p[23] = model.M24;
p[24] = model.M31; p[25] = model.M32; p[26] = model.M33; p[27] = model.M34;
p[28] = model.M41; p[29] = model.M42; p[30] = model.M43; p[31] = model.M44;
p[32] = material.Albedo.X;
p[33] = material.Albedo.Y;
p[34] = material.Albedo.Z;
p[35] = material.Roughness;
var buf = meshBuffers.vertex.Buffer;
var offset = 0ul;
Vk.vkCmdBindVertexBuffers(cmd, 0, 1, &buf, &offset);
Vk.vkCmdBindIndexBuffer(cmd, meshBuffers.index.Buffer, 0, VkIndexType.Uint32);
Vk.vkCmdPushConstants(cmd, _pipeline.PipelineLayout,
VkShaderStageFlags.Vertex | VkShaderStageFlags.Fragment, 0,
(uint)VulkanPipeline.PushConstantSize, pPush);
Vk.vkCmdDrawIndexed(cmd, meshBuffers.indexCount, 1, 0, 0, 0);
}
});
if (ImGuiLayer != null)
var cmdInfo = new VkCommandBufferSubmitInfo
{
ImGuiLayer.Render(cmd);
sType = VkStructureType.CommandBufferSubmitInfo,
commandBuffer = cmd,
};
var signalInfo = new VkSemaphoreSubmitInfo
{
sType = VkStructureType.SemaphoreSubmitInfo,
semaphore = _frameResources.SubmitSemaphores[imageIndex],
stageMask = 0x8000,
};
var submitInfo = new VkSubmitInfo2
{
sType = VkStructureType.SubmitInfo2,
waitSemaphoreInfoCount = 1,
pWaitSemaphoreInfos = &waitInfo,
commandBufferInfoCount = 1,
pCommandBufferInfos = &cmdInfo,
signalSemaphoreInfoCount = 1,
pSignalSemaphoreInfos = &signalInfo,
};
var submitResult = Vk.vkQueueSubmit2(_ctx.GraphicsQueue, 1, &submitInfo,
_frameResources.FrameFences[_frameIndex]);
if (submitResult != VkResult.Success)
throw new InvalidOperationException($"vkQueueSubmit2 failed: {submitResult}");
var presentSwapchain = _swapchain.Swapchain;
var presentSemaphore = _frameResources.SubmitSemaphores[imageIndex];
var presentInfo = new VkPresentInfoKHR
{
sType = VkStructureType.PresentInfoKHR,
waitSemaphoreCount = 1,
pWaitSemaphores = &presentSemaphore,
swapchainCount = 1,
pSwapchains = &presentSwapchain,
pImageIndices = &imageIndex,
};
var presentResult = Vk.vkQueuePresentKHR(_ctx.GraphicsQueue, &presentInfo);
if (presentResult == VkResult.ErrorOutOfDateKHR || presentResult == VkResult.SuboptimalKHR)
{
_swapchain.Recreate(_ctx.SurfaceExtent.Width == 0 ? 1280 : (int)_ctx.SurfaceExtent.Width,
_ctx.SurfaceExtent.Height == 0 ? 720 : (int)_ctx.SurfaceExtent.Height);
}
Vk.vkCmdEndRenderPass(cmd);
if (_screenshotRequested)
{
var width = _swapchain.Extent.width;
var height = _swapchain.Extent.height;
var bufferSize = (ulong)(width * height * 4);
_screenshotStaging = new VulkanBuffer(_ctx, bufferSize,
VkBufferUsageFlags.TransferDst,
VkMemoryPropertyFlags.HostVisible | VkMemoryPropertyFlags.HostCoherent);
_screenshotImageIndex = imageIndex;
_screenshotPending = true;
_screenshotRequested = false;
var barrier = new VkImageMemoryBarrier
{
sType = VkStructureType.ImageMemoryBarrier,
pNext = null,
srcAccessMask = VkAccessFlags.ColorAttachmentWrite,
dstAccessMask = VkAccessFlags.TransferRead,
oldLayout = VkImageLayout.PresentSrcKHR,
newLayout = VkImageLayout.TransferSrcOptimal,
srcQueueFamilyIndex = ~0u,
dstQueueFamilyIndex = ~0u,
image = _swapchain.SwapchainImages[imageIndex],
subresourceRange = new VkImageSubresourceRange
{
aspectMask = VkImageAspectFlags.Color,
baseMipLevel = 0,
levelCount = 1,
baseArrayLayer = 0,
layerCount = 1
}
};
Vk.vkCmdPipelineBarrier(cmd,
VkPipelineStageFlags.ColorAttachmentOutput,
VkPipelineStageFlags.Transfer,
0, 0, null, 0, null, 1, &barrier);
var region = new VkBufferImageCopy
{
bufferOffset = 0,
bufferRowLength = (uint)width,
bufferImageHeight = (uint)height,
imageSubresource = new VkImageSubresourceLayers
{
aspectMask = VkImageAspectFlags.Color,
mipLevel = 0,
baseArrayLayer = 0,
layerCount = 1
},
imageOffset = new VkOffset3D { x = 0, y = 0, z = 0 },
imageExtent = new VkExtent3D { width = width, height = height, depth = 1 }
};
var stagingBuf = _screenshotStaging.Buffer;
Vk.vkCmdCopyImageToBuffer(cmd,
_swapchain.SwapchainImages[imageIndex],
(int)VkImageLayout.TransferSrcOptimal,
stagingBuf, 1, &region);
var barrier2 = new VkImageMemoryBarrier
{
sType = VkStructureType.ImageMemoryBarrier,
pNext = null,
srcAccessMask = VkAccessFlags.TransferRead,
dstAccessMask = VkAccessFlags.MemoryRead,
oldLayout = VkImageLayout.TransferSrcOptimal,
newLayout = VkImageLayout.PresentSrcKHR,
srcQueueFamilyIndex = ~0u,
dstQueueFamilyIndex = ~0u,
image = _swapchain.SwapchainImages[imageIndex],
subresourceRange = new VkImageSubresourceRange
{
aspectMask = VkImageAspectFlags.Color,
baseMipLevel = 0,
levelCount = 1,
baseArrayLayer = 0,
layerCount = 1
}
};
Vk.vkCmdPipelineBarrier(cmd,
VkPipelineStageFlags.Transfer,
VkPipelineStageFlags.BottomOfPipe,
0, 0, null, 0, null, 1, &barrier2);
}
Vk.CheckResult(Vk.vkEndCommandBuffer(cmd), "vkEndCommandBuffer");
_frameIndex = (_frameIndex + 1) % VulkanFrameResources.MaxFramesInFlight;
}
private unsafe void RecreateCommandBuffers()
private static void TransitionImageLayout(VkCommandBuffer cmd, VkImage image,
VkImageLayout oldLayout, VkImageLayout newLayout,
ulong srcStage, ulong srcAccess,
ulong dstStage, ulong dstAccess)
{
fixed (VkCommandBuffer* pCmds = _commandBuffers)
var barrier = new VkImageMemoryBarrier2
{
Vk.vkFreeCommandBuffers(_ctx.Device, _commandPool, (uint)_commandBuffers.Length, pCmds);
}
sType = VkStructureType.ImageMemoryBarrier2,
srcStageMask = srcStage,
srcAccessMask = srcAccess,
dstStageMask = dstStage,
dstAccessMask = dstAccess,
oldLayout = oldLayout,
newLayout = newLayout,
image = image,
subresourceRange = new VkImageSubresourceRange
{
AspectMask = VkImageAspectFlags.Color,
LevelCount = 1,
LayerCount = 1,
},
};
VkCommandBufferAllocateInfo allocInfo;
allocInfo.sType = VkStructureType.CommandBufferAllocateInfo;
allocInfo.pNext = null;
allocInfo.commandPool = _commandPool;
allocInfo.level = VkCommandBufferLevel.Primary;
allocInfo.commandBufferCount = (uint)_commandBuffers.Length;
fixed (VkCommandBuffer* pCmds = _commandBuffers)
var depInfo = new VkDependencyInfo
{
Vk.CheckResult(Vk.vkAllocateCommandBuffers(_ctx.Device, &allocInfo, pCmds), "vkAllocateCommandBuffers (recreate)");
}
sType = VkStructureType.DependencyInfo,
imageMemoryBarrierCount = 1,
pImageMemoryBarriers = &barrier,
};
Vk.vkCmdPipelineBarrier2(cmd, &depInfo);
}
public void RequestScreenshot(string outputPath)
private static byte[] LoadShader(string path)
{
if (!File.Exists(path))
{
var altPath = Path.Combine(AppContext.BaseDirectory, path);
if (!File.Exists(altPath))
{
altPath = Path.Combine(AppContext.BaseDirectory, "Shaders", Path.GetFileName(path));
if (!File.Exists(altPath))
throw new FileNotFoundException($"Shader file not found: {path}");
}
return File.ReadAllBytes(altPath);
}
return File.ReadAllBytes(path);
}
public void RequestScreenshot(string path)
{
_screenshotPath = outputPath;
_screenshotRequested = true;
_screenshotPath = path;
}
public Task<byte[]> CaptureAsync(string outputPath)
{
_screenshotPath = outputPath;
_screenshotTcs = new TaskCompletionSource<byte[]>();
_screenshotRequested = true;
return _screenshotTcs.Task;
_screenshotRequested = false;
return Task.FromResult(Array.Empty<byte>());
}
public void OnResize() => _resized = true;
public unsafe void Dispose()
public string? TryTakeScreenshotPath()
{
Vk.vkQueueWaitIdle(_ctx.GraphicsQueue);
var path = _screenshotRequested ? _screenshotPath : null;
_screenshotRequested = false;
return path;
}
foreach (var mesh in _meshCache.Values)
{
mesh.vertex.Dispose();
mesh.index.Dispose();
}
_meshCache.Clear();
public void Dispose()
{
if (_disposed) return;
_disposed = true;
foreach (var ubo in _uboBuffers)
ubo?.Dispose();
Vk.vkDeviceWaitIdle(_ctx.Device);
if (_descriptorPool.Value != 0)
Vk.vkDestroyDescriptorPool(_ctx.Device, _descriptorPool, null);
fixed (VkCommandBuffer* pCmds = _commandBuffers)
{
if (_commandPool.Value != 0 && _commandBuffers.Length > 0)
Vk.vkFreeCommandBuffers(_ctx.Device, _commandPool, (uint)_commandBuffers.Length, pCmds);
}
if (_commandPool.Value != 0) Vk.vkDestroyCommandPool(_ctx.Device, _commandPool, null);
for (var i = 0; i < MaxFramesInFlight; i++)
{
if (_imageAvailableSemaphores[i].Value != 0) Vk.vkDestroySemaphore(_ctx.Device, _imageAvailableSemaphores[i], null);
if (_renderFinishedSemaphores[i].Value != 0) Vk.vkDestroySemaphore(_ctx.Device, _renderFinishedSemaphores[i], null);
if (_inFlightFences[i].Value != 0) Vk.vkDestroyFence(_ctx.Device, _inFlightFences[i], null);
}
_vertexBuffer?.Dispose();
_frameResources?.Dispose();
_pipeline?.Dispose();
}
}