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.
525 lines
20 KiB
C++
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
|
|
}
|