Files
CoopAllTheThings/tests/vk_capture_perf_test.cpp
BlackMark 30eccf749d Apply clang-format across the whole tree
Run clang-format (the repo's .clang-format: LLVM base, 120 cols, tabs,
Allman functions) over every source file so the tree is formatter-clean.
Whitespace only -- no behavior change; full x64 + x86 suites pass.

Also set SortIncludes: false in .clang-format. Windows include order is
load-bearing (windows.h must precede tlhelp32.h / mmreg.h / xinput.h /
dinput.h; winsock2.h must precede windows.h), and the default
alphabetical sort reorders tlhelp32.h ahead of windows.h -- a build
break. Leaving order alone keeps the manual, correct grouping.
2026-07-12 11:52:53 +02:00

525 lines
20 KiB
C++

// Reproducer + regression guard for the Vulkan capture performance collapse.
//
// Against a real 144 FPS game (Sphere Spectacle) the implicit-layer / inline-hook capture dropped
// the game to ~3 FPS. Measured cause: the read-back ran on the game's PRESENT THREAD and spent
// ~370 ms per 1080p frame reading the mapped staging buffer -- because the staging memory was a
// plain HOST_VISIBLE|HOST_COHERENT type (write-combined / uncached on a discrete GPU), where a
// scattered CPU read runs at PCIe latency.
//
// This test builds a known gradient image (in PRESENT_SRC layout) on a real device and measures the
// time the *present thread* spends per capture for two implementations:
// * a synchronous reference (copy + WaitForFences + read the HOST_COHERENT mapping + swizzle, all
// inline) -> reproduces the stall, and
// * coop::hook::VkCapture (present thread only records+submits; a reaper thread does the
// HOST_CACHED read-back + swizzle + upload off the critical path).
// It asserts VkCapture's present-thread cost is a small fraction of the synchronous cost, and that
// the captured image is byte-correct (BGRA->RGBA swizzle). `--sync` routes the measured path through
// the synchronous reference so the same assertion FAILS, demonstrating the test catches the regression.
//
// Needs a working Vulkan ICD (the dev box has one). With no vulkan-1.dll / no device it SKIPs.
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
#include <windows.h>
#define VK_NO_PROTOTYPES
#include <vulkan/vulkan.h>
#include "vk_capture.hpp"
namespace {
double now_ms()
{
LARGE_INTEGER f, c;
QueryPerformanceFrequency(&f);
QueryPerformanceCounter(&c);
return 1000.0 * static_cast<double>(c.QuadPart) / static_cast<double>(f.QuadPart);
}
int g_failures = 0;
void check(bool ok, const char* what)
{
std::printf("%s %s\n", ok ? " ok:" : "FAIL:", what);
if (!ok) {
++g_failures;
}
}
// Everything the test resolves from the device (superset of VkCapture::Fns + image-build helpers).
struct DevFns {
PFN_vkGetDeviceProcAddr GetDeviceProcAddr;
PFN_vkGetDeviceQueue GetDeviceQueue;
PFN_vkCreateCommandPool CreateCommandPool;
PFN_vkDestroyCommandPool DestroyCommandPool;
PFN_vkAllocateCommandBuffers AllocateCommandBuffers;
PFN_vkBeginCommandBuffer BeginCommandBuffer;
PFN_vkEndCommandBuffer EndCommandBuffer;
PFN_vkResetCommandBuffer ResetCommandBuffer;
PFN_vkCmdPipelineBarrier CmdPipelineBarrier;
PFN_vkCmdCopyImageToBuffer CmdCopyImageToBuffer;
PFN_vkCmdCopyBufferToImage CmdCopyBufferToImage;
PFN_vkQueueSubmit QueueSubmit;
PFN_vkQueueWaitIdle QueueWaitIdle;
PFN_vkCreateFence CreateFence;
PFN_vkDestroyFence DestroyFence;
PFN_vkWaitForFences WaitForFences;
PFN_vkResetFences ResetFences;
PFN_vkGetFenceStatus GetFenceStatus;
PFN_vkCreateSemaphore CreateSemaphore;
PFN_vkDestroySemaphore DestroySemaphore;
PFN_vkCreateBuffer CreateBuffer;
PFN_vkDestroyBuffer DestroyBuffer;
PFN_vkGetBufferMemoryRequirements GetBufferMemoryRequirements;
PFN_vkCreateImage CreateImage;
PFN_vkDestroyImage DestroyImage;
PFN_vkGetImageMemoryRequirements GetImageMemoryRequirements;
PFN_vkAllocateMemory AllocateMemory;
PFN_vkFreeMemory FreeMemory;
PFN_vkBindBufferMemory BindBufferMemory;
PFN_vkBindImageMemory BindImageMemory;
PFN_vkMapMemory MapMemory;
PFN_vkUnmapMemory UnmapMemory;
PFN_vkInvalidateMappedMemoryRanges InvalidateMappedMemoryRanges;
PFN_vkDeviceWaitIdle DeviceWaitIdle;
PFN_vkGetPhysicalDeviceMemoryProperties GetPhysicalDeviceMemoryProperties;
};
VkPhysicalDeviceMemoryProperties g_memprops{};
bool find_mem(std::uint32_t type_bits, VkMemoryPropertyFlags want, std::uint32_t& out)
{
for (std::uint32_t i = 0; i < g_memprops.memoryTypeCount; ++i) {
if ((type_bits & (1u << i)) && (g_memprops.memoryTypes[i].propertyFlags & want) == want) {
out = i;
return true;
}
}
return false;
}
coop::hook::VkCapture::Fns capture_fns(const DevFns& d)
{
coop::hook::VkCapture::Fns f{};
f.GetDeviceQueue = d.GetDeviceQueue;
f.CreateCommandPool = d.CreateCommandPool;
f.DestroyCommandPool = d.DestroyCommandPool;
f.AllocateCommandBuffers = d.AllocateCommandBuffers;
f.BeginCommandBuffer = d.BeginCommandBuffer;
f.EndCommandBuffer = d.EndCommandBuffer;
f.ResetCommandBuffer = d.ResetCommandBuffer;
f.CmdPipelineBarrier = d.CmdPipelineBarrier;
f.CmdCopyImageToBuffer = d.CmdCopyImageToBuffer;
f.QueueSubmit = d.QueueSubmit;
f.CreateFence = d.CreateFence;
f.DestroyFence = d.DestroyFence;
f.WaitForFences = d.WaitForFences;
f.ResetFences = d.ResetFences;
f.GetFenceStatus = d.GetFenceStatus;
f.CreateSemaphore = d.CreateSemaphore;
f.DestroySemaphore = d.DestroySemaphore;
f.CreateBuffer = d.CreateBuffer;
f.DestroyBuffer = d.DestroyBuffer;
f.GetBufferMemoryRequirements = d.GetBufferMemoryRequirements;
f.AllocateMemory = d.AllocateMemory;
f.FreeMemory = d.FreeMemory;
f.BindBufferMemory = d.BindBufferMemory;
f.MapMemory = d.MapMemory;
f.UnmapMemory = d.UnmapMemory;
f.InvalidateMappedMemoryRanges = d.InvalidateMappedMemoryRanges;
f.DeviceWaitIdle = d.DeviceWaitIdle;
f.GetPhysicalDeviceMemoryProperties = d.GetPhysicalDeviceMemoryProperties;
return f;
}
} // namespace
int main(int argc, char** argv)
{
const bool sync_repro = argc > 1 && std::strcmp(argv[1], "--sync") == 0;
const std::uint32_t W = 1920, H = 1080; // the resolution where the stall was measured
HMODULE vk = LoadLibraryW(L"vulkan-1.dll");
if (vk == nullptr) {
std::printf("SKIP vk_capture_perf_test (no vulkan-1.dll)\n");
return 0;
}
auto gipa = reinterpret_cast<PFN_vkGetInstanceProcAddr>(GetProcAddress(vk, "vkGetInstanceProcAddr"));
if (gipa == nullptr) {
std::printf("SKIP vk_capture_perf_test (no vkGetInstanceProcAddr)\n");
return 0;
}
#define IFN(name) reinterpret_cast<PFN_vk##name>(gipa(instance, "vk" #name))
VkInstance instance = VK_NULL_HANDLE;
{
auto create = reinterpret_cast<PFN_vkCreateInstance>(gipa(nullptr, "vkCreateInstance"));
VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO};
app.apiVersion = VK_API_VERSION_1_1;
VkInstanceCreateInfo ci{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO};
ci.pApplicationInfo = &app;
if (create == nullptr || create(&ci, nullptr, &instance) != VK_SUCCESS) {
std::printf("SKIP vk_capture_perf_test (vkCreateInstance failed)\n");
return 0;
}
}
auto EnumeratePhysicalDevices = IFN(EnumeratePhysicalDevices);
auto GetPhysicalDeviceQueueFamilyProperties = IFN(GetPhysicalDeviceQueueFamilyProperties);
auto CreateDevice = IFN(CreateDevice);
auto DestroyDevice = IFN(DestroyDevice);
auto DestroyInstance = IFN(DestroyInstance);
auto gdpa = IFN(GetDeviceProcAddr);
std::uint32_t n = 0;
EnumeratePhysicalDevices(instance, &n, nullptr);
if (n == 0) {
std::printf("SKIP vk_capture_perf_test (no physical devices)\n");
DestroyInstance(instance, nullptr);
return 0;
}
std::vector<VkPhysicalDevice> phys(n);
EnumeratePhysicalDevices(instance, &n, phys.data());
VkPhysicalDevice gpu = phys[0];
std::uint32_t qn = 0;
GetPhysicalDeviceQueueFamilyProperties(gpu, &qn, nullptr);
std::vector<VkQueueFamilyProperties> qf(qn);
GetPhysicalDeviceQueueFamilyProperties(gpu, &qn, qf.data());
std::uint32_t qfam = UINT32_MAX;
for (std::uint32_t i = 0; i < qn; ++i) {
if (qf[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) {
qfam = i;
break;
}
}
if (qfam == UINT32_MAX) {
std::printf("SKIP vk_capture_perf_test (no graphics queue)\n");
DestroyInstance(instance, nullptr);
return 0;
}
const float prio = 1.0f;
VkDeviceQueueCreateInfo qci{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO};
qci.queueFamilyIndex = qfam;
qci.queueCount = 1;
qci.pQueuePriorities = &prio;
VkDeviceCreateInfo dci{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO};
dci.queueCreateInfoCount = 1;
dci.pQueueCreateInfos = &qci;
VkDevice device = VK_NULL_HANDLE;
if (CreateDevice(gpu, &dci, nullptr, &device) != VK_SUCCESS) {
std::printf("SKIP vk_capture_perf_test (vkCreateDevice failed)\n");
DestroyInstance(instance, nullptr);
return 0;
}
#define DFN(name) reinterpret_cast<PFN_vk##name>(gdpa(device, "vk" #name))
DevFns d{};
d.GetDeviceProcAddr = gdpa;
d.GetDeviceQueue = DFN(GetDeviceQueue);
d.CreateCommandPool = DFN(CreateCommandPool);
d.DestroyCommandPool = DFN(DestroyCommandPool);
d.AllocateCommandBuffers = DFN(AllocateCommandBuffers);
d.BeginCommandBuffer = DFN(BeginCommandBuffer);
d.EndCommandBuffer = DFN(EndCommandBuffer);
d.ResetCommandBuffer = DFN(ResetCommandBuffer);
d.CmdPipelineBarrier = DFN(CmdPipelineBarrier);
d.CmdCopyImageToBuffer = DFN(CmdCopyImageToBuffer);
d.CmdCopyBufferToImage = DFN(CmdCopyBufferToImage);
d.QueueSubmit = DFN(QueueSubmit);
d.QueueWaitIdle = DFN(QueueWaitIdle);
d.CreateFence = DFN(CreateFence);
d.DestroyFence = DFN(DestroyFence);
d.WaitForFences = DFN(WaitForFences);
d.ResetFences = DFN(ResetFences);
d.GetFenceStatus = DFN(GetFenceStatus);
d.CreateSemaphore = DFN(CreateSemaphore);
d.DestroySemaphore = DFN(DestroySemaphore);
d.CreateBuffer = DFN(CreateBuffer);
d.DestroyBuffer = DFN(DestroyBuffer);
d.GetBufferMemoryRequirements = DFN(GetBufferMemoryRequirements);
d.CreateImage = DFN(CreateImage);
d.DestroyImage = DFN(DestroyImage);
d.GetImageMemoryRequirements = DFN(GetImageMemoryRequirements);
d.AllocateMemory = DFN(AllocateMemory);
d.FreeMemory = DFN(FreeMemory);
d.BindBufferMemory = DFN(BindBufferMemory);
d.BindImageMemory = DFN(BindImageMemory);
d.MapMemory = DFN(MapMemory);
d.UnmapMemory = DFN(UnmapMemory);
d.InvalidateMappedMemoryRanges = DFN(InvalidateMappedMemoryRanges);
d.DeviceWaitIdle = DFN(DeviceWaitIdle);
d.GetPhysicalDeviceMemoryProperties = reinterpret_cast<PFN_vkGetPhysicalDeviceMemoryProperties>(
gipa(instance, "vkGetPhysicalDeviceMemoryProperties"));
d.GetPhysicalDeviceMemoryProperties(gpu, &g_memprops);
VkQueue queue = VK_NULL_HANDLE;
d.GetDeviceQueue(device, qfam, 0, &queue);
// A small command pool + fence the test uses for setup and for the synchronous reference.
VkCommandPool pool = VK_NULL_HANDLE;
VkCommandPoolCreateInfo pci{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
pci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
pci.queueFamilyIndex = qfam;
d.CreateCommandPool(device, &pci, nullptr, &pool);
VkCommandBuffer cb = VK_NULL_HANDLE;
VkCommandBufferAllocateInfo cbai{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
cbai.commandPool = pool;
cbai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
cbai.commandBufferCount = 1;
d.AllocateCommandBuffers(device, &cbai, &cb);
VkFence fence = VK_NULL_HANDLE;
VkFenceCreateInfo fci{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
d.CreateFence(device, &fci, nullptr, &fence);
auto submit_wait = [&](VkCommandBuffer c) {
VkSubmitInfo si{VK_STRUCTURE_TYPE_SUBMIT_INFO};
si.commandBufferCount = 1;
si.pCommandBuffers = &c;
d.ResetFences(device, 1, &fence);
d.QueueSubmit(queue, 1, &si, fence);
d.WaitForFences(device, 1, &fence, VK_TRUE, UINT64_MAX);
};
// --- Build the known source image (BGRA gradient) in PRESENT_SRC layout ------------------------
const VkDeviceSize bytes = static_cast<VkDeviceSize>(W) * H * 4;
std::vector<unsigned char> gradient(bytes);
for (std::uint32_t y = 0; y < H; ++y) {
for (std::uint32_t x = 0; x < W; ++x) {
unsigned char* p = &gradient[(static_cast<size_t>(y) * W + x) * 4];
p[0] = static_cast<unsigned char>(x & 0xFF); // B
p[1] = static_cast<unsigned char>(y & 0xFF); // G
p[2] = static_cast<unsigned char>((x + y) & 0xFF); // R
p[3] = 255;
}
}
// Upload buffer (host-visible).
VkBuffer upbuf = VK_NULL_HANDLE;
VkDeviceMemory upmem = VK_NULL_HANDLE;
{
VkBufferCreateInfo bci{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
bci.size = bytes;
bci.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
d.CreateBuffer(device, &bci, nullptr, &upbuf);
VkMemoryRequirements mr{};
d.GetBufferMemoryRequirements(device, upbuf, &mr);
std::uint32_t mt = 0;
find_mem(mr.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, mt);
VkMemoryAllocateInfo mai{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
mai.allocationSize = mr.size;
mai.memoryTypeIndex = mt;
d.AllocateMemory(device, &mai, nullptr, &upmem);
d.BindBufferMemory(device, upbuf, upmem, 0);
void* mp = nullptr;
d.MapMemory(device, upmem, 0, VK_WHOLE_SIZE, 0, &mp);
std::memcpy(mp, gradient.data(), bytes);
d.UnmapMemory(device, upmem);
}
// Device-local source image.
VkImage img = VK_NULL_HANDLE;
VkDeviceMemory imgmem = VK_NULL_HANDLE;
{
VkImageCreateInfo ici{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
ici.imageType = VK_IMAGE_TYPE_2D;
ici.format = VK_FORMAT_B8G8R8A8_UNORM;
ici.extent = {W, H, 1};
ici.mipLevels = 1;
ici.arrayLayers = 1;
ici.samples = VK_SAMPLE_COUNT_1_BIT;
ici.tiling = VK_IMAGE_TILING_OPTIMAL;
ici.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
ici.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
if (d.CreateImage(device, &ici, nullptr, &img) != VK_SUCCESS) {
std::printf("SKIP vk_capture_perf_test (CreateImage failed)\n");
return 0;
}
VkMemoryRequirements mr{};
d.GetImageMemoryRequirements(device, img, &mr);
std::uint32_t mt = 0;
find_mem(mr.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, mt);
VkMemoryAllocateInfo mai{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
mai.allocationSize = mr.size;
mai.memoryTypeIndex = mt;
d.AllocateMemory(device, &mai, nullptr, &imgmem);
d.BindImageMemory(device, img, imgmem, 0);
}
auto barrier = [&](VkCommandBuffer c, VkImageLayout from, VkImageLayout to, VkAccessFlags src, VkAccessFlags dst) {
VkImageMemoryBarrier b{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
b.srcAccessMask = src;
b.dstAccessMask = dst;
b.oldLayout = from;
b.newLayout = to;
b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
b.image = img;
b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
d.CmdPipelineBarrier(c, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr,
0, nullptr, 1, &b);
};
{
VkCommandBufferBeginInfo bi{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
d.BeginCommandBuffer(cb, &bi);
barrier(cb, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, VK_ACCESS_TRANSFER_WRITE_BIT);
VkBufferImageCopy r{};
r.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
r.imageExtent = {W, H, 1};
d.CmdCopyBufferToImage(cb, upbuf, img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &r);
barrier(cb, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_ACCESS_TRANSFER_WRITE_BIT,
VK_ACCESS_MEMORY_READ_BIT);
d.EndCommandBuffer(cb);
submit_wait(cb);
}
// --- Synchronous reference (HOST_COHERENT staging, inline read-back on the present thread) ------
VkBuffer ref_buf = VK_NULL_HANDLE;
VkDeviceMemory ref_mem = VK_NULL_HANDLE;
void* ref_mapped = nullptr;
{
VkBufferCreateInfo bci{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
bci.size = bytes;
bci.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
d.CreateBuffer(device, &bci, nullptr, &ref_buf);
VkMemoryRequirements mr{};
d.GetBufferMemoryRequirements(device, ref_buf, &mr);
std::uint32_t mt = 0; // OLD selection: first HOST_VISIBLE|HOST_COHERENT (often write-combined)
find_mem(mr.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, mt);
VkMemoryAllocateInfo mai{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
mai.allocationSize = mr.size;
mai.memoryTypeIndex = mt;
d.AllocateMemory(device, &mai, nullptr, &ref_mem);
d.BindBufferMemory(device, ref_buf, ref_mem, 0);
d.MapMemory(device, ref_mem, 0, VK_WHOLE_SIZE, 0, &ref_mapped);
}
std::vector<unsigned char> ref_rgba(bytes);
auto sync_capture = [&]() {
VkCommandBufferBeginInfo bi{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
d.ResetCommandBuffer(cb, 0);
d.BeginCommandBuffer(cb, &bi);
barrier(cb, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_ACCESS_MEMORY_READ_BIT,
VK_ACCESS_TRANSFER_READ_BIT);
VkBufferImageCopy r{};
r.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
r.imageExtent = {W, H, 1};
d.CmdCopyImageToBuffer(cb, img, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, ref_buf, 1, &r);
barrier(cb, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_ACCESS_TRANSFER_READ_BIT,
VK_ACCESS_MEMORY_READ_BIT);
d.EndCommandBuffer(cb);
submit_wait(cb);
const auto* src = static_cast<const unsigned char*>(ref_mapped);
const size_t row = static_cast<size_t>(W) * 4;
for (std::uint32_t y = 0; y < H; ++y) {
const unsigned char* in = src + static_cast<size_t>(y) * row;
unsigned char* o = ref_rgba.data() + static_cast<size_t>(y) * row;
for (std::uint32_t x = 0; x < W; ++x) {
o[x * 4 + 0] = in[x * 4 + 2];
o[x * 4 + 1] = in[x * 4 + 1];
o[x * 4 + 2] = in[x * 4 + 0];
o[x * 4 + 3] = 255;
}
}
};
// Time the synchronous reference (a few iterations; this is the per-present cost an inline
// read-back puts on the game's present thread).
sync_capture(); // warm
double sync_ms = 0;
const int iters = 8;
for (int i = 0; i < iters; ++i) {
const double t0 = now_ms();
sync_capture();
sync_ms += now_ms() - t0;
}
sync_ms /= iters;
std::printf("synchronous reference (old design): %.2f ms per present-thread capture (%ux%u)\n", sync_ms, W, H);
double sut_ms = sync_ms; // in --sync repro mode the measured path IS the synchronous one
bool image_ok = true;
if (!sync_repro) {
// --- The fix under test: VkCapture (async reaper) -----------------------------------------
coop::hook::VkCapture cap;
cap.init(gpu, device, qfam, capture_fns(d), GetCurrentProcessId(), nullptr);
// Consume the present semaphore VkCapture hands back, exactly like a real vkQueuePresentKHR.
auto consume = [&](VkSemaphore sem) {
VkPipelineStageFlags stage = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT;
VkSubmitInfo si{VK_STRUCTURE_TYPE_SUBMIT_INFO};
si.waitSemaphoreCount = 1;
si.pWaitSemaphores = &sem;
si.pWaitDstStageMask = &stage;
d.QueueSubmit(queue, 1, &si, VK_NULL_HANDLE);
};
// Warm up (first calls allocate staging / create the D3D texture on the reaper).
for (int i = 0; i < 16; ++i) {
VkSemaphore sem = VK_NULL_HANDLE;
if (cap.present(img, VK_FORMAT_B8G8R8A8_UNORM, W, H, nullptr, 0, sem)) {
consume(sem);
}
Sleep(2);
}
// Steady-state present-thread cost.
int queued = 0;
double t = 0;
const int loop = 240;
for (int i = 0; i < loop; ++i) {
VkSemaphore sem = VK_NULL_HANDLE;
const double t0 = now_ms();
const bool did = cap.present(img, VK_FORMAT_B8G8R8A8_UNORM, W, H, nullptr, 0, sem);
t += now_ms() - t0;
if (did) {
consume(sem);
++queued;
}
Sleep(1); // ~1 kHz present loop; lets the reaper drain
}
sut_ms = t / loop;
std::printf("VkCapture (fix): %.3f ms per present-thread call (queued %d/%d, published %llu)\n", sut_ms,
queued, loop, static_cast<unsigned long long>(cap.frames_published()));
// Let the reaper finish, then verify the captured image is byte-correct (BGRA->RGBA swizzle).
Sleep(50);
std::vector<unsigned char> got;
std::uint32_t gw = 0, gh = 0;
if (cap.last_frame(got, gw, gh) && gw == W && gh == H) {
image_ok = std::memcmp(got.data(), ref_rgba.data(), bytes) == 0;
} else {
image_ok = false;
}
check(cap.frames_published() > 0, "VkCapture published frames");
check(image_ok, "VkCapture image matches the source gradient (BGRA->RGBA swizzle correct)");
d.DeviceWaitIdle(device);
cap.shutdown();
}
// The core assertion: the measured present-thread cost must be a small fraction of the synchronous
// read-back cost (VkCapture moves the read-back off the present thread). In --sync repro mode the
// measured path IS the synchronous one, so this fails -- demonstrating the test catches the bug.
std::printf("present-thread cost: measured %.3f ms vs synchronous %.2f ms (ratio %.3f)\n", sut_ms, sync_ms,
sut_ms / sync_ms);
check(sut_ms * 4.0 < sync_ms, "capture stays off the present thread (measured << synchronous)");
// cleanup
d.DeviceWaitIdle(device);
d.UnmapMemory(device, ref_mem);
d.DestroyBuffer(device, ref_buf, nullptr);
d.FreeMemory(device, ref_mem, nullptr);
d.DestroyImage(device, img, nullptr);
d.FreeMemory(device, imgmem, nullptr);
d.DestroyBuffer(device, upbuf, nullptr);
d.FreeMemory(device, upmem, nullptr);
d.DestroyFence(device, fence, nullptr);
d.DestroyCommandPool(device, pool, nullptr);
DestroyDevice(device, nullptr);
DestroyInstance(instance, nullptr);
std::printf(g_failures == 0 ? "PASS vk_capture_perf_test\n" : "FAILED vk_capture_perf_test (%d)\n", g_failures);
return g_failures == 0 ? 1 - 1 : 1; // 0 on pass
}