fix: video recording — proper layout transitions + deferred buffer read

Three validation errors fixed:
1. vkCmdCopyImageToBuffer inside render pass → moved AFTER vkCmdEndRendering
2. Swapchain images missing TRANSFER_SRC usage → added to swapchain creation
3. Wrong layout transitions → ColorAttachment→TransferSrc→copy→ColorAttachment

Architecture change:
- CaptureFrame now only records commands (returns null)
- ReadCapturedBuffer called at start of NEXT frame (after WaitFrame/fence)
  GPU has finished by then, safe to map memory
- CapturedFrame available 1 frame late (acceptable for video)
- Image transitions: ColorAttachmentOptimal→TransferSrcOptimal→ColorAttachmentOptimal
  (present transition still works because image is back in ColorAttachmentOptimal)
This commit is contained in:
emil28092005
2026-06-19 08:52:41 +03:00
parent b6299e8048
commit 9b80ab7881
3 changed files with 25 additions and 13 deletions
Binary file not shown.
+24 -12
View File
@@ -107,10 +107,10 @@ public sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreensh
var w = _swapchain.Extent.Width; var w = _swapchain.Extent.Width;
var h = _swapchain.Extent.Height; var h = _swapchain.Extent.Height;
// Transition swapchain image: PresentSrc → TransferSrc // Transition: ColorAttachmentOptimal → TransferSrcOptimal
TransitionImageLayout(cmd, _swapchain.Images[imageIndex], TransitionImageLayout(cmd, _swapchain.Images[imageIndex],
VkImageLayout.PresentSrcKHR, VkImageLayout.TransferSrcOptimal, VkImageLayout.ColorAttachmentOptimal, VkImageLayout.TransferSrcOptimal,
0x8000, 0, 0x10000, 0x2000); 0x400, 0x100, 0x10000, 0x2000);
// Copy image to buffer // Copy image to buffer
var region = new VkBufferImageCopyRegion var region = new VkBufferImageCopyRegion
@@ -132,12 +132,18 @@ public sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreensh
Vk.vkCmdCopyImageToBuffer(cmd, _swapchain.Images[imageIndex], VkImageLayout.TransferSrcOptimal, Vk.vkCmdCopyImageToBuffer(cmd, _swapchain.Images[imageIndex], VkImageLayout.TransferSrcOptimal,
_screenshotBuffer, 1, &region); _screenshotBuffer, 1, &region);
// Transition back: TransferSrc → PresentSrc // Transition back: TransferSrcOptimal → ColorAttachmentOptimal
TransitionImageLayout(cmd, _swapchain.Images[imageIndex], TransitionImageLayout(cmd, _swapchain.Images[imageIndex],
VkImageLayout.TransferSrcOptimal, VkImageLayout.PresentSrcKHR, VkImageLayout.TransferSrcOptimal, VkImageLayout.ColorAttachmentOptimal,
0x10000, 0x2000, 0x8000, 0); 0x10000, 0x2000, 0x400, 0x100);
return null; // Data will be read next frame after GPU completes
}
private byte[]? ReadCapturedBuffer()
{
if (!_screenshotInitialized) return null;
// Map and read
void* pData = null; void* pData = null;
Vk.vkMapMemory(_ctx.Device, _screenshotMemory, 0, _screenshotBufferSize, 0, &pData); Vk.vkMapMemory(_ctx.Device, _screenshotMemory, 0, _screenshotBufferSize, 0, &pData);
@@ -265,6 +271,12 @@ public sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreensh
{ {
_frameResources.WaitFrame(_frameIndex); _frameResources.WaitFrame(_frameIndex);
// Read captured frame from previous render (GPU has finished by now)
if (IsRecording)
{
CapturedFrame = ReadCapturedBuffer();
}
uint imageIndex; uint imageIndex;
var acquireResult = Vk.vkAcquireNextImageKHR(_ctx.Device, _swapchain.Swapchain, var acquireResult = Vk.vkAcquireNextImageKHR(_ctx.Device, _swapchain.Swapchain,
ulong.MaxValue, _frameResources.AcquireSemaphores[_frameIndex], VkFence.Null, &imageIndex); ulong.MaxValue, _frameResources.AcquireSemaphores[_frameIndex], VkFence.Null, &imageIndex);
@@ -505,16 +517,16 @@ public sealed unsafe class VulkanRenderer : IRenderer, Engine.Graphics.IScreensh
Vk.vkCmdDrawIndexed(cmd, dc.indexCount, 1, 0, 0, 0); Vk.vkCmdDrawIndexed(cmd, dc.indexCount, 1, 0, 0, 0);
} }
// Capture frame BEFORE ImGui (video without UI) _imGui?.Render(cmd, _swapchain.Extent.Width, _swapchain.Extent.Height);
Vk.vkCmdEndRendering(cmd);
// Capture frame AFTER render pass ends, BEFORE present transition
if (IsRecording) if (IsRecording)
{ {
CapturedFrame = CaptureFrame(cmd, imageIndex); CapturedFrame = CaptureFrame(cmd, imageIndex);
} }
_imGui?.Render(cmd, _swapchain.Extent.Width, _swapchain.Extent.Height);
Vk.vkCmdEndRendering(cmd);
TransitionImageLayout(cmd, _swapchain.Images[imageIndex], TransitionImageLayout(cmd, _swapchain.Images[imageIndex],
VkImageLayout.ColorAttachmentOptimal, VkImageLayout.PresentSrcKHR, VkImageLayout.ColorAttachmentOptimal, VkImageLayout.PresentSrcKHR,
0x400, 0x100, 0x400, 0x100,
@@ -78,7 +78,7 @@ internal sealed unsafe class VulkanSwapchain : IDisposable
imageColorSpace = _surfaceFormat.colorSpace, imageColorSpace = _surfaceFormat.colorSpace,
imageExtent = Extent, imageExtent = Extent,
imageArrayLayers = 1, imageArrayLayers = 1,
imageUsage = VkImageUsageFlags.ColorAttachment | VkImageUsageFlags.TransferDst, imageUsage = VkImageUsageFlags.ColorAttachment | VkImageUsageFlags.TransferDst | VkImageUsageFlags.TransferSrc,
imageSharingMode = VkSharingMode.Exclusive, imageSharingMode = VkSharingMode.Exclusive,
preTransform = caps.currentTransform, preTransform = caps.currentTransform,
compositeAlpha = VkCompositeAlphaFlagsKHR.Opaque, compositeAlpha = VkCompositeAlphaFlagsKHR.Opaque,