feat: real screenshot capture — BMP from swapchain image via vkCmdCopyImageToBuffer
- Added vkCmdCopyImageToBuffer to P/Invoke layer - Screenshot embedded in main command buffer (barrier PresentSrcKHR→TransferSrc, copy, barrier back) - Deferred read: staging buffer read on next frame after fence signaled - BMP format written directly to FileStream (row by row, avoids large heap allocations) - BGRA→BGRA direct copy for B8G8R8A8 swapchain format - All 16 camera tour screenshots captured successfully (1280x720x32bpp) - 66/66 tests pass
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
namespace Engine.Graphics.Vulkan;
|
||||
|
||||
internal static class PngEncoder
|
||||
{
|
||||
public static byte[] EncodeRgbaToPng(byte[] rgba, int width, int height)
|
||||
{
|
||||
return EncodeRgbaToBmp(rgba, width, height);
|
||||
}
|
||||
|
||||
private static byte[] EncodeRgbaToBmp(byte[] rgba, int width, int height)
|
||||
{
|
||||
var rowSize = width * 4;
|
||||
var pixelDataSize = rowSize * height;
|
||||
var fileSize = 54 + pixelDataSize;
|
||||
|
||||
var bmp = new byte[fileSize];
|
||||
|
||||
bmp[0] = (byte)'B';
|
||||
bmp[1] = (byte)'M';
|
||||
WriteUInt32LittleEndian(bmp, 2, (uint)fileSize);
|
||||
WriteUInt32LittleEndian(bmp, 10, 54u);
|
||||
WriteUInt32LittleEndian(bmp, 14, 40u);
|
||||
WriteUInt32LittleEndian(bmp, 18, (uint)width);
|
||||
WriteUInt32LittleEndian(bmp, 22, (uint)height);
|
||||
WriteUInt16LittleEndian(bmp, 26, 1);
|
||||
WriteUInt16LittleEndian(bmp, 28, 32);
|
||||
WriteUInt32LittleEndian(bmp, 34, (uint)pixelDataSize);
|
||||
|
||||
for (var y = 0; y < height; y++)
|
||||
{
|
||||
var srcRow = (height - 1 - y) * width * 4;
|
||||
var dstRow = 54 + y * rowSize;
|
||||
for (var x = 0; x < width; x++)
|
||||
{
|
||||
bmp[dstRow + x * 4 + 0] = rgba[srcRow + x * 4 + 2];
|
||||
bmp[dstRow + x * 4 + 1] = rgba[srcRow + x * 4 + 1];
|
||||
bmp[dstRow + x * 4 + 2] = rgba[srcRow + x * 4 + 0];
|
||||
bmp[dstRow + x * 4 + 3] = rgba[srcRow + x * 4 + 3];
|
||||
}
|
||||
}
|
||||
|
||||
return bmp;
|
||||
}
|
||||
|
||||
private static void WriteUInt32LittleEndian(byte[] buf, int offset, uint value)
|
||||
{
|
||||
buf[offset] = (byte)value;
|
||||
buf[offset + 1] = (byte)(value >> 8);
|
||||
buf[offset + 2] = (byte)(value >> 16);
|
||||
buf[offset + 3] = (byte)(value >> 24);
|
||||
}
|
||||
|
||||
private static void WriteUInt16LittleEndian(byte[] buf, int offset, ushort value)
|
||||
{
|
||||
buf[offset] = (byte)value;
|
||||
buf[offset + 1] = (byte)(value >> 8);
|
||||
}
|
||||
}
|
||||
@@ -80,6 +80,7 @@ internal static unsafe class Vk
|
||||
public static PFN_vkCmdPipelineBarrier vkCmdPipelineBarrier;
|
||||
public static PFN_vkCmdCopyBuffer vkCmdCopyBuffer;
|
||||
public static PFN_vkCmdCopyBufferToImage vkCmdCopyBufferToImage;
|
||||
public static PFN_vkCmdCopyImageToBuffer vkCmdCopyImageToBuffer;
|
||||
public static PFN_vkCmdClearColorImage vkCmdClearColorImage;
|
||||
public static PFN_vkCmdPushConstants vkCmdPushConstants;
|
||||
public static PFN_vkCreateSampler vkCreateSampler;
|
||||
@@ -186,6 +187,7 @@ internal static unsafe class Vk
|
||||
vkCmdPipelineBarrier = VulkanNative.LoadDeviceFunction<PFN_vkCmdPipelineBarrier>(device, "vkCmdPipelineBarrier");
|
||||
vkCmdCopyBuffer = VulkanNative.LoadDeviceFunction<PFN_vkCmdCopyBuffer>(device, "vkCmdCopyBuffer");
|
||||
vkCmdCopyBufferToImage = VulkanNative.LoadDeviceFunction<PFN_vkCmdCopyBufferToImage>(device, "vkCmdCopyBufferToImage");
|
||||
vkCmdCopyImageToBuffer = VulkanNative.LoadDeviceFunction<PFN_vkCmdCopyImageToBuffer>(device, "vkCmdCopyImageToBuffer");
|
||||
vkCmdClearColorImage = VulkanNative.LoadDeviceFunction<PFN_vkCmdClearColorImage>(device, "vkCmdClearColorImage");
|
||||
vkCmdPushConstants = VulkanNative.LoadDeviceFunction<PFN_vkCmdPushConstants>(device, "vkCmdPushConstants");
|
||||
vkCreateSampler = VulkanNative.LoadDeviceFunction<PFN_vkCreateSampler>(device, "vkCreateSampler");
|
||||
@@ -386,6 +388,8 @@ unsafe internal delegate void PFN_vkCmdCopyBuffer(VkCommandBuffer commandBuffer,
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkCmdCopyBufferToImage(VkCommandBuffer commandBuffer, VkBuffer srcBuffer, VkImage dstImage, int dstImageLayout, uint regionCount, VkBufferImageCopy* pRegions);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkCmdCopyImageToBuffer(VkCommandBuffer commandBuffer, VkImage srcImage, int srcImageLayout, VkBuffer dstBuffer, uint regionCount, VkBufferImageCopy* pRegions);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkCmdClearColorImage(VkCommandBuffer commandBuffer, VkImage image, int imageLayout, VkClearColorValue* pColor, uint rangeCount, VkImageSubresourceRange* pRanges);
|
||||
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
|
||||
unsafe internal delegate void PFN_vkCmdPushConstants(VkCommandBuffer commandBuffer, VkPipelineLayout layout, VkShaderStageFlags stageFlags, uint offset, uint size, void* pValues);
|
||||
|
||||
@@ -33,9 +33,10 @@ internal sealed unsafe class VulkanRenderer : IRenderer, IScreenshotProvider
|
||||
private bool _screenshotRequested;
|
||||
private string _screenshotPath = "";
|
||||
private TaskCompletionSource<byte[]>? _screenshotTcs;
|
||||
private byte[]? _screenshotData;
|
||||
private int _screenshotWidth;
|
||||
private int _screenshotHeight;
|
||||
|
||||
private VulkanBuffer? _screenshotStaging;
|
||||
private uint _screenshotImageIndex;
|
||||
private bool _screenshotPending;
|
||||
|
||||
public bool IsScreenshotRequested => _screenshotRequested;
|
||||
public IScreenshotProvider ScreenshotProvider => this;
|
||||
@@ -194,6 +195,11 @@ internal sealed unsafe class VulkanRenderer : IRenderer, IScreenshotProvider
|
||||
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,
|
||||
@@ -265,25 +271,92 @@ internal sealed unsafe class VulkanRenderer : IRenderer, IScreenshotProvider
|
||||
}
|
||||
|
||||
_currentFrame = (_currentFrame + 1) % MaxFramesInFlight;
|
||||
}
|
||||
|
||||
if (_screenshotRequested)
|
||||
private unsafe void FinishScreenshot()
|
||||
{
|
||||
try
|
||||
{
|
||||
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))
|
||||
{
|
||||
var dir = Path.GetDirectoryName(_screenshotPath);
|
||||
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
File.WriteAllText(_screenshotPath, "Vulkan screenshot placeholder");
|
||||
Console.WriteLine($"Screenshot saved: {_screenshotPath}");
|
||||
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);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Screenshot save failed: {ex.Message}");
|
||||
}
|
||||
_screenshotRequested = false;
|
||||
|
||||
_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)
|
||||
@@ -464,6 +537,95 @@ internal sealed unsafe class VulkanRenderer : IRenderer, IScreenshotProvider
|
||||
});
|
||||
|
||||
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, ®ion);
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user