Program Listing for File FrameCapture.ixx#
↰ Return to documentation for file (Src/GraphicsEngineVulkan/renderer/FrameCapture.ixx)
module;
#include <cstdint>
#include <cstring>
#include <limits>
#include <memory>
#include <vector>
#include <vulkan/vulkan.hpp>
#include "common/FormatHelper.hpp"
#include "common/ImageBarrierHelper.hpp"
#include "spdlog/spdlog.h"
export module kataglyphis.vulkan.frame_capture;
import kataglyphis.vulkan.buffer;
import kataglyphis.vulkan.device;
import kataglyphis.vulkan.swapchain;
import kataglyphis.vulkan.texture;
export namespace Kataglyphis {
// Owns the renderer's headless frame-capture state, extracted verbatim from
// VulkanRenderer, mirroring the FrameSync/GpuTimingSubsystem extractions.
//
// The capture is recorded *inside* the frame's own command buffer, right
// after the post stage transitions the swapchain image to ePresentSrcKHR and
// before the present. Copying the image after vkQueuePresentKHR would touch
// an image owned by the presentation engine, so the copy is armed one frame
// ahead: request() arms it, record() copies the swapchain image into a
// host-visible staging buffer inside the next recorded command buffer,
// bindSubmitFence() ties that copy to the frame's in-flight fence, and take()
// waits on exactly that fence before handing back the pixels.
class FrameCapture
{
public:
[[nodiscard]] bool supportsCapture(const VulkanSwapChain &swapChain, bool deviceLostDetected) const
{
return !deviceLostDetected && swapChain.supportsTransferSrc()
&& isCapturableSwapchainFormat(swapChain.getSwapChainFormat());
}
[[nodiscard]] bool isArmed() const { return armed; }
// Arms a capture for the next recorded frame. Callers are expected to have
// already checked supportsCapture() (VulkanRenderer::requestFrameCapture
// logs a warning and skips arming when unsupported - this class stays
// silent so it has no logging policy of its own to get wrong).
void request()
{
armed = true;
pending = false;
fence = vk::Fence{};
}
// Copies the swapchain image at image_index into the staging buffer,
// recorded into commandBuffer. Growing the staging buffer only happens
// when the extent grew or it does not exist yet - safe because a resize
// goes through VulkanRenderer::recreateSwapChain(), which waits idle and
// invalidates any pending capture's fence first.
void record(const std::shared_ptr<VulkanDevice> &device,
vk::CommandBuffer &commandBuffer,
VulkanSwapChain &swapChain,
uint32_t image_index,
bool deviceLostDetected)
{
armed = false;
if (!device || !supportsCapture(swapChain, deviceLostDetected)) { return; }
const vk::Extent2D extent = swapChain.getSwapChainExtent();
if (extent.width == 0 || extent.height == 0) { return; }
const vk::DeviceSize required_size =
static_cast<vk::DeviceSize>(extent.width) * static_cast<vk::DeviceSize>(extent.height) * 4ULL;
if (buffer_size < required_size) {
// Redundant since create() now releases the previous allocation itself,
// but kept: it documents that destroying here (before the new create)
// is synchronisation-safe.
buffer.cleanUp();
buffer.create(device,
required_size,
vk::BufferUsageFlagBits::eTransferDst,
vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostCoherent);
buffer_size = required_size;
}
if (buffer.getMappedData() == nullptr) {
spdlog::error("Frame capture staging buffer is not host mapped; capture skipped.");
return;
}
vk::Image &swapchain_image = swapChain.getSwapChainImage(image_index).getImage();
const vk::ImageMemoryBarrier to_transfer_src = Kataglyphis::buildImageMemoryBarrier(swapchain_image,
vk::ImageLayout::ePresentSrcKHR,
vk::ImageLayout::eTransferSrcOptimal,
vk::AccessFlagBits::eColorAttachmentWrite,
vk::AccessFlagBits::eTransferRead);
commandBuffer.pipelineBarrier(vk::PipelineStageFlagBits::eColorAttachmentOutput,
vk::PipelineStageFlagBits::eTransfer,
vk::DependencyFlags{},
0,
nullptr,
0,
nullptr,
1,
&to_transfer_src);
vk::BufferImageCopy region{};
region.bufferOffset = 0;
region.bufferRowLength = 0;// tightly packed
region.bufferImageHeight = 0;
region.imageSubresource = vk::ImageSubresourceLayers{ vk::ImageAspectFlagBits::eColor, 0, 0, 1 };
region.imageOffset = vk::Offset3D{ 0, 0, 0 };
region.imageExtent = vk::Extent3D{ extent.width, extent.height, 1 };
commandBuffer.copyImageToBuffer(
swapchain_image, vk::ImageLayout::eTransferSrcOptimal, buffer.getBuffer(), 1, ®ion);
// Restore the layout the present expects.
const vk::ImageMemoryBarrier back_to_present = Kataglyphis::buildImageMemoryBarrier(swapchain_image,
vk::ImageLayout::eTransferSrcOptimal,
vk::ImageLayout::ePresentSrcKHR,
vk::AccessFlagBits::eTransferRead,
vk::AccessFlagBits{});
// Make the copy visible to host reads of the staging buffer as well.
vk::BufferMemoryBarrier buffer_to_host{};
buffer_to_host.srcQueueFamilyIndex = vk::QueueFamilyIgnored;
buffer_to_host.dstQueueFamilyIndex = vk::QueueFamilyIgnored;
buffer_to_host.buffer = buffer.getBuffer();
buffer_to_host.offset = 0;
buffer_to_host.size = required_size;
buffer_to_host.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
buffer_to_host.dstAccessMask = vk::AccessFlagBits::eHostRead;
commandBuffer.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
vk::PipelineStageFlagBits::eBottomOfPipe | vk::PipelineStageFlagBits::eHost,
vk::DependencyFlags{},
0,
nullptr,
1,
&buffer_to_host,
1,
&back_to_present);
width = extent.width;
height = extent.height;
format = swapChain.getSwapChainFormat();
pending = true;
// Set to the frame's in-flight fence by bindSubmitFence() right after
// the submit.
fence = vk::Fence{};
}
// Ties the just-submitted frame's in-flight fence to a pending capture, so
// take() knows exactly what to wait on. A no-op when nothing is pending.
void bindSubmitFence(vk::Fence submitFence)
{
if (pending) { fence = submitFence; }
}
// Drops a stale fence across a swapchain recreation (createSynchronization
// destroys and recreates every fence). The caller must have already
// waited idle - that already guarantees a pending capture's copy has
// completed, so the staged pixels stay readable; only the now-dangling
// fence handle goes away.
void invalidateFence() { fence = vk::Fence{}; }
// Waits on the recorded fence (never waitIdle) and returns tightly packed
// RGBA8 pixels, swizzled from BGRA when the swapchain used a B8G8R8A8
// format. Returns an empty vector when nothing is pending, the device is
// lost, or the wait fails. On eErrorDeviceLost, sets deviceLostDetected so
// the caller's own device-lost bookkeeping stays in sync.
std::vector<uint8_t> take(const std::shared_ptr<VulkanDevice> &device,
bool &deviceLostDetected,
uint32_t &outWidth,
uint32_t &outHeight)
{
outWidth = 0;
outHeight = 0;
if (!pending || deviceLostDetected || !device) { return {}; }
pending = false;
if (fence) {
const vk::Result wait_result =
device->getLogicalDevice().waitForFences(1, &fence, VK_TRUE, std::numeric_limits<uint64_t>::max());
if (wait_result != vk::Result::eSuccess) {
spdlog::error(
"Failed to wait for the frame capture fence (vk::Result={})", static_cast<int>(wait_result));
if (wait_result == vk::Result::eErrorDeviceLost) { deviceLostDetected = true; }
return {};
}
}
const void *mapped = buffer.getMappedData();
if (mapped == nullptr || width == 0 || height == 0) { return {}; }
const size_t pixel_count = static_cast<size_t>(width) * static_cast<size_t>(height);
std::vector<uint8_t> pixels(pixel_count * 4U);
std::memcpy(pixels.data(), mapped, pixels.size());
// Normalize to RGBA8 regardless of the swapchain's channel order.
if (capturedFormatIsBgra(format)) {
for (size_t i = 0; i < pixels.size(); i += 4U) { std::swap(pixels[i], pixels[i + 2U]); }
}
outWidth = width;
outHeight = height;
return pixels;
}
void cleanUp()
{
buffer.cleanUp();
buffer_size = 0;
armed = false;
pending = false;
fence = vk::Fence{};
width = 0;
height = 0;
}
private:
bool armed{ false };
bool pending{ false };
vk::Fence fence{};
VulkanBuffer buffer;
vk::DeviceSize buffer_size{ 0 };
uint32_t width{ 0 };
uint32_t height{ 0 };
vk::Format format{ vk::Format::eUndefined };
};
}// namespace Kataglyphis