Fix Vulkan capture perf collapse: read back off the present thread
Against Sphere Spectacle (144 FPS, runs without Steam) the implicit-layer
capture dropped the game to ~3 FPS. Measured cause (per-stage trace in the
layer): the read-back ran on the game's PRESENT THREAD and spent ~370 ms per
1080p frame -- not the GPU copy (~2 ms) but the CPU swizzle, because the staging
buffer was a plain HOST_VISIBLE|HOST_COHERENT type (write-combined / uncached on
a discrete GPU), where a scattered CPU read runs at PCIe latency. 3 captures/s =
the 3 FPS the user saw.
Test-first: tests/vk_capture_perf_test reproduces the stall as a deterministic
unit test (372 ms/present, ratio 1.0 -> FAIL via `--sync`), then proves the fix
(0.02 ms/present, byte-correct BGRA->RGBA, ratio ~0 -> PASS).
Fix: extract the near-identical read-back from vk_hook.cpp and coop_vk_layer.cpp
into one shared coop::hook::VkCapture that:
* has the present thread only record + submit the copy (sub-ms) and return;
* runs a dedicated reaper thread for the fence wait + swizzle + D3D upload, off
the critical path, with a ring of in-flight slots (game never waits);
* allocates HOST_CACHED staging (fast CPU read), invalidating when non-coherent;
* throttles capture to ~150 Hz (a guest stream is <= the host refresh; no point
mirroring an uncapped 400+ FPS game and burning reaper CPU).
Real-game A/B: present rate now matches the no-capture baseline (605->470 vs
593->405 over the same ramp) with the mirror at ~130 fps -- no measurable impact.
Also adds present-thread overhead guards to the other GPU backends' hook tests
(present_overhead.hpp): DX11 0.05 ms, DX12 0.34 ms, OpenGL 0.09 ms overhead, all
asserted < one 60 Hz frame, so any future synchronous-stall regression fails.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3,10 +3,9 @@
|
||||
// A real Vulkan *implicit layer* the loader inserts at vkCreateInstance -- guaranteed to be in
|
||||
// the chain before the game resolves vkQueuePresentKHR. This is the reliable early-presence path
|
||||
// for games that initialize Vulkan immediately (which inject-after-launch + the inline-hook
|
||||
// vk_hook can't catch). It does the same capture as vk_hook -- read the presented image back with
|
||||
// vkCmdCopyImageToBuffer, swizzle BGRA->RGBA, upload into the shared keyed-mutex texture on a
|
||||
// hook-owned D3D11 device, and re-chain the present's wait semaphores -- but via proper
|
||||
// layer-chain dispatch instead of an inline hook.
|
||||
// vk_hook can't catch). The actual read-back (copy the presented image into the shared keyed-mutex
|
||||
// texture, off the present thread) is the shared coop::hook::VkCapture; this file only gets the
|
||||
// layer into the dispatch chain and feeds VkCapture each present.
|
||||
//
|
||||
// Scoping: an implicit layer loads into *every* Vulkan app, so the layer only *captures* when it
|
||||
// recognises the process as the host's target -- env COOP_VK_LAYER_FORCE=1 (tests), or this
|
||||
@@ -34,6 +33,7 @@
|
||||
|
||||
#include "coop/shared_memory.hpp"
|
||||
#include "ipc_client.hpp"
|
||||
#include "vk_capture.hpp"
|
||||
|
||||
// --- Loader/layer interface (interface version 2) ---------------------------
|
||||
extern "C"
|
||||
@@ -104,64 +104,25 @@ constexpr VkStructureType kLoaderInstanceCreateInfo = static_cast<VkStructureTyp
|
||||
constexpr VkStructureType kLoaderDeviceCreateInfo = static_cast<VkStructureType>(48);
|
||||
|
||||
using coop::hook::IpcClient;
|
||||
using coop::hook::VkCapture;
|
||||
|
||||
IpcClient g_ipc;
|
||||
bool g_ipc_tried = false;
|
||||
bool g_active = false; // do we capture in this process? (scoping)
|
||||
std::atomic<bool> g_ipc_tried{false}; // reaper-thread side: connect once on the first published frame
|
||||
bool g_active = false; // do we capture in this process? (scoping)
|
||||
|
||||
// Chain dispatch.
|
||||
PFN_vkGetInstanceProcAddr g_next_gipa = nullptr;
|
||||
PFN_vkGetDeviceProcAddr g_next_gdpa = nullptr;
|
||||
PFN_vkQueuePresentKHR g_real_present = nullptr;
|
||||
PFN_vkCreateSwapchainKHR g_real_create_swapchain = nullptr;
|
||||
PFN_vkGetSwapchainImagesKHR g_get_swapchain_images = nullptr; // for our CreateSwapchain image tracking
|
||||
|
||||
VkInstance g_instance = VK_NULL_HANDLE;
|
||||
VkPhysicalDevice g_phys = VK_NULL_HANDLE;
|
||||
VkDevice g_device = VK_NULL_HANDLE;
|
||||
std::uint32_t g_qfam = 0;
|
||||
VkQueue g_queue = VK_NULL_HANDLE;
|
||||
|
||||
// Device functions for the read-back (same set as vk_hook).
|
||||
struct VkFns
|
||||
{
|
||||
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_vkQueueSubmit QueueSubmit;
|
||||
PFN_vkCreateFence CreateFence;
|
||||
PFN_vkDestroyFence DestroyFence;
|
||||
PFN_vkWaitForFences WaitForFences;
|
||||
PFN_vkResetFences ResetFences;
|
||||
PFN_vkCreateSemaphore CreateSemaphore;
|
||||
PFN_vkDestroySemaphore DestroySemaphore;
|
||||
PFN_vkCreateBuffer CreateBuffer;
|
||||
PFN_vkDestroyBuffer DestroyBuffer;
|
||||
PFN_vkGetBufferMemoryRequirements GetBufferMemoryRequirements;
|
||||
PFN_vkAllocateMemory AllocateMemory;
|
||||
PFN_vkFreeMemory FreeMemory;
|
||||
PFN_vkBindBufferMemory BindBufferMemory;
|
||||
PFN_vkMapMemory MapMemory;
|
||||
PFN_vkUnmapMemory UnmapMemory;
|
||||
PFN_vkGetSwapchainImagesKHR GetSwapchainImagesKHR;
|
||||
PFN_vkDeviceWaitIdle DeviceWaitIdle;
|
||||
PFN_vkGetPhysicalDeviceMemoryProperties GetPhysicalDeviceMemoryProperties;
|
||||
};
|
||||
VkFns g_fns{};
|
||||
|
||||
VkCommandPool g_pool = VK_NULL_HANDLE;
|
||||
VkCommandBuffer g_cmd = VK_NULL_HANDLE;
|
||||
VkFence g_fence = VK_NULL_HANDLE;
|
||||
VkSemaphore g_present_sem = VK_NULL_HANDLE;
|
||||
VkBuffer g_staging = VK_NULL_HANDLE;
|
||||
VkDeviceMemory g_staging_mem = VK_NULL_HANDLE;
|
||||
VkDeviceSize g_staging_size = 0;
|
||||
void* g_staging_mapped = nullptr;
|
||||
VkCapture g_cap; // the shared, off-present-thread read-back
|
||||
|
||||
struct SwapInfo
|
||||
{
|
||||
@@ -172,14 +133,6 @@ struct SwapInfo
|
||||
};
|
||||
std::vector<SwapInfo> g_swaps;
|
||||
|
||||
ID3D11Device* g_d3d = nullptr;
|
||||
ID3D11DeviceContext* g_d3d_ctx = nullptr;
|
||||
ID3D11Texture2D* g_shared_tex = nullptr;
|
||||
IDXGIKeyedMutex* g_shared_mutex = nullptr;
|
||||
HANDLE g_shared_handle = nullptr;
|
||||
UINT g_share_w = 0, g_share_h = 0;
|
||||
std::vector<unsigned char> g_rgba;
|
||||
|
||||
bool eq(const char* a, const char* b)
|
||||
{
|
||||
return std::strcmp(a, b) == 0;
|
||||
@@ -256,278 +209,6 @@ bool decide_active()
|
||||
return _stricmp(self8, want) == 0;
|
||||
}
|
||||
|
||||
bool ensure_d3d()
|
||||
{
|
||||
if (g_d3d != nullptr)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return SUCCEEDED(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr, 0,
|
||||
D3D11_SDK_VERSION, &g_d3d, nullptr, &g_d3d_ctx)) &&
|
||||
g_d3d != nullptr;
|
||||
}
|
||||
|
||||
void release_shared()
|
||||
{
|
||||
if (g_shared_mutex)
|
||||
{
|
||||
g_shared_mutex->Release();
|
||||
g_shared_mutex = nullptr;
|
||||
}
|
||||
if (g_shared_tex)
|
||||
{
|
||||
g_shared_tex->Release();
|
||||
g_shared_tex = nullptr;
|
||||
}
|
||||
if (g_shared_handle)
|
||||
{
|
||||
CloseHandle(g_shared_handle);
|
||||
g_shared_handle = nullptr;
|
||||
}
|
||||
g_share_w = g_share_h = 0;
|
||||
}
|
||||
|
||||
bool ensure_shared_texture(UINT w, UINT h)
|
||||
{
|
||||
if (g_shared_tex && g_share_w == w && g_share_h == h)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
release_shared();
|
||||
D3D11_TEXTURE2D_DESC d{};
|
||||
d.Width = w;
|
||||
d.Height = h;
|
||||
d.MipLevels = 1;
|
||||
d.ArraySize = 1;
|
||||
d.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
|
||||
d.SampleDesc.Count = 1;
|
||||
d.Usage = D3D11_USAGE_DEFAULT;
|
||||
d.BindFlags = D3D11_BIND_SHADER_RESOURCE;
|
||||
d.MiscFlags = D3D11_RESOURCE_MISC_SHARED_NTHANDLE | D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX;
|
||||
if (FAILED(g_d3d->CreateTexture2D(&d, nullptr, &g_shared_tex)) || !g_shared_tex)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
IDXGIResource1* res = nullptr;
|
||||
if (FAILED(g_shared_tex->QueryInterface(__uuidof(IDXGIResource1), reinterpret_cast<void**>(&res))) || !res)
|
||||
{
|
||||
release_shared();
|
||||
return false;
|
||||
}
|
||||
const std::wstring name = coop::video_share_name(GetCurrentProcessId());
|
||||
const HRESULT hr = res->CreateSharedHandle(
|
||||
nullptr, DXGI_SHARED_RESOURCE_READ | DXGI_SHARED_RESOURCE_WRITE, name.c_str(), &g_shared_handle);
|
||||
res->Release();
|
||||
if (FAILED(hr) || !g_shared_handle ||
|
||||
FAILED(g_shared_tex->QueryInterface(__uuidof(IDXGIKeyedMutex), reinterpret_cast<void**>(&g_shared_mutex))))
|
||||
{
|
||||
release_shared();
|
||||
return false;
|
||||
}
|
||||
g_share_w = w;
|
||||
g_share_h = h;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool find_host_visible_memory(std::uint32_t bits, std::uint32_t& out)
|
||||
{
|
||||
VkPhysicalDeviceMemoryProperties mp{};
|
||||
g_fns.GetPhysicalDeviceMemoryProperties(g_phys, &mp);
|
||||
const VkMemoryPropertyFlags want = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
|
||||
for (std::uint32_t i = 0; i < mp.memoryTypeCount; ++i)
|
||||
{
|
||||
if ((bits & (1u << i)) && (mp.memoryTypes[i].propertyFlags & want) == want)
|
||||
{
|
||||
out = i;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void release_staging()
|
||||
{
|
||||
if (g_staging_mapped && g_staging_mem)
|
||||
{
|
||||
g_fns.UnmapMemory(g_device, g_staging_mem);
|
||||
g_staging_mapped = nullptr;
|
||||
}
|
||||
if (g_staging)
|
||||
{
|
||||
g_fns.DestroyBuffer(g_device, g_staging, nullptr);
|
||||
g_staging = VK_NULL_HANDLE;
|
||||
}
|
||||
if (g_staging_mem)
|
||||
{
|
||||
g_fns.FreeMemory(g_device, g_staging_mem, nullptr);
|
||||
g_staging_mem = VK_NULL_HANDLE;
|
||||
}
|
||||
g_staging_size = 0;
|
||||
}
|
||||
|
||||
bool ensure_vk_resources(std::uint32_t w, std::uint32_t h)
|
||||
{
|
||||
if (g_queue == VK_NULL_HANDLE)
|
||||
{
|
||||
g_fns.GetDeviceQueue(g_device, g_qfam, 0, &g_queue);
|
||||
}
|
||||
if (g_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 = g_qfam;
|
||||
if (g_fns.CreateCommandPool(g_device, &pci, nullptr, &g_pool) != VK_SUCCESS)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
VkCommandBufferAllocateInfo ai{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
|
||||
ai.commandPool = g_pool;
|
||||
ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||
ai.commandBufferCount = 1;
|
||||
VkFenceCreateInfo fi{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
|
||||
VkSemaphoreCreateInfo si{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
|
||||
if (g_fns.AllocateCommandBuffers(g_device, &ai, &g_cmd) != VK_SUCCESS ||
|
||||
g_fns.CreateFence(g_device, &fi, nullptr, &g_fence) != VK_SUCCESS ||
|
||||
g_fns.CreateSemaphore(g_device, &si, nullptr, &g_present_sem) != VK_SUCCESS)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const VkDeviceSize need = static_cast<VkDeviceSize>(w) * h * 4;
|
||||
if (g_staging != VK_NULL_HANDLE && g_staging_size == need)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
release_staging();
|
||||
VkBufferCreateInfo bci{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
|
||||
bci.size = need;
|
||||
bci.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
||||
bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
if (g_fns.CreateBuffer(g_device, &bci, nullptr, &g_staging) != VK_SUCCESS)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
VkMemoryRequirements mr{};
|
||||
g_fns.GetBufferMemoryRequirements(g_device, g_staging, &mr);
|
||||
std::uint32_t mt = 0;
|
||||
if (!find_host_visible_memory(mr.memoryTypeBits, mt))
|
||||
{
|
||||
release_staging();
|
||||
return false;
|
||||
}
|
||||
VkMemoryAllocateInfo mai{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
||||
mai.allocationSize = mr.size;
|
||||
mai.memoryTypeIndex = mt;
|
||||
if (g_fns.AllocateMemory(g_device, &mai, nullptr, &g_staging_mem) != VK_SUCCESS ||
|
||||
g_fns.BindBufferMemory(g_device, g_staging, g_staging_mem, 0) != VK_SUCCESS ||
|
||||
g_fns.MapMemory(g_device, g_staging_mem, 0, VK_WHOLE_SIZE, 0, &g_staging_mapped) != VK_SUCCESS)
|
||||
{
|
||||
release_staging();
|
||||
return false;
|
||||
}
|
||||
g_staging_size = need;
|
||||
return true;
|
||||
}
|
||||
|
||||
void barrier(VkCommandBuffer cb, VkImage img, VkImageLayout from, VkImageLayout to, VkAccessFlags s,
|
||||
VkAccessFlags d)
|
||||
{
|
||||
VkImageMemoryBarrier b{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
|
||||
b.srcAccessMask = s;
|
||||
b.dstAccessMask = d;
|
||||
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};
|
||||
g_fns.CmdPipelineBarrier(cb, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0,
|
||||
nullptr, 0, nullptr, 1, &b);
|
||||
}
|
||||
|
||||
bool capture(VkImage image, VkFormat fmt, std::uint32_t w, std::uint32_t h, const VkSemaphore* wait,
|
||||
std::uint32_t wait_count, VkSemaphore& out_sem)
|
||||
{
|
||||
const bool bgra = fmt == VK_FORMAT_B8G8R8A8_UNORM || fmt == VK_FORMAT_B8G8R8A8_SRGB;
|
||||
const bool rgba = fmt == VK_FORMAT_R8G8B8A8_UNORM || fmt == VK_FORMAT_R8G8B8A8_SRGB;
|
||||
if ((!bgra && !rgba) || !ensure_d3d() || !ensure_shared_texture(w, h) || !ensure_vk_resources(w, h))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
g_fns.ResetCommandBuffer(g_cmd, 0);
|
||||
VkCommandBufferBeginInfo bi{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
|
||||
bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
||||
g_fns.BeginCommandBuffer(g_cmd, &bi);
|
||||
barrier(g_cmd, image, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
VK_ACCESS_MEMORY_READ_BIT, VK_ACCESS_TRANSFER_READ_BIT);
|
||||
VkBufferImageCopy region{};
|
||||
region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
|
||||
region.imageExtent = {w, h, 1};
|
||||
g_fns.CmdCopyImageToBuffer(g_cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, g_staging, 1, ®ion);
|
||||
barrier(g_cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
|
||||
VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_MEMORY_READ_BIT);
|
||||
g_fns.EndCommandBuffer(g_cmd);
|
||||
|
||||
std::vector<VkPipelineStageFlags> stages(wait_count, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT);
|
||||
VkSubmitInfo si{VK_STRUCTURE_TYPE_SUBMIT_INFO};
|
||||
si.waitSemaphoreCount = wait_count;
|
||||
si.pWaitSemaphores = wait;
|
||||
si.pWaitDstStageMask = wait_count ? stages.data() : nullptr;
|
||||
si.commandBufferCount = 1;
|
||||
si.pCommandBuffers = &g_cmd;
|
||||
si.signalSemaphoreCount = 1;
|
||||
si.pSignalSemaphores = &g_present_sem;
|
||||
g_fns.ResetFences(g_device, 1, &g_fence);
|
||||
if (g_fns.QueueSubmit(g_queue, 1, &si, g_fence) != VK_SUCCESS)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
g_fns.WaitForFences(g_device, 1, &g_fence, VK_TRUE, UINT64_MAX);
|
||||
|
||||
const size_t row = static_cast<size_t>(w) * 4;
|
||||
if (g_rgba.size() != row * h)
|
||||
{
|
||||
g_rgba.resize(row * h);
|
||||
}
|
||||
const auto* src = static_cast<const unsigned char*>(g_staging_mapped);
|
||||
for (std::uint32_t y = 0; y < h; ++y)
|
||||
{
|
||||
const unsigned char* s = src + static_cast<size_t>(y) * row;
|
||||
unsigned char* o = g_rgba.data() + static_cast<size_t>(y) * row;
|
||||
if (bgra)
|
||||
{
|
||||
for (std::uint32_t x = 0; x < w; ++x)
|
||||
{
|
||||
o[x * 4 + 0] = s[x * 4 + 2];
|
||||
o[x * 4 + 1] = s[x * 4 + 1];
|
||||
o[x * 4 + 2] = s[x * 4 + 0];
|
||||
o[x * 4 + 3] = 255;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
std::memcpy(o, s, row);
|
||||
}
|
||||
}
|
||||
out_sem = g_present_sem;
|
||||
if (g_shared_mutex->AcquireSync(coop::kVideoMutexKey, 8) == S_OK)
|
||||
{
|
||||
g_d3d_ctx->UpdateSubresource(g_shared_tex, 0, nullptr, g_rgba.data(), static_cast<UINT>(row), 0);
|
||||
g_d3d_ctx->Flush();
|
||||
g_shared_mutex->ReleaseSync(coop::kVideoMutexKey);
|
||||
if (!g_ipc_tried)
|
||||
{
|
||||
g_ipc_tried = true;
|
||||
g_ipc.connect(/*attempts=*/40, /*delay_ms=*/25);
|
||||
}
|
||||
if (g_ipc.connected())
|
||||
{
|
||||
g_ipc.publish_video_frame(w, h, static_cast<std::uint32_t>(DXGI_FORMAT_R8G8B8A8_UNORM));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const SwapInfo* find_swap(VkSwapchainKHR sc)
|
||||
{
|
||||
for (const SwapInfo& s : g_swaps)
|
||||
@@ -540,8 +221,47 @@ const SwapInfo* find_swap(VkSwapchainKHR sc)
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Per-second present-rate trace (enable with COOP_VK_LAYER_LOG): confirms the game keeps its frame
|
||||
// rate while capturing (the whole point of the off-present-thread read-back) and how many of those
|
||||
// presents the reaper actually mirrored.
|
||||
void trace_present_rate()
|
||||
{
|
||||
static int enabled = -1;
|
||||
if (enabled < 0)
|
||||
{
|
||||
enabled = GetEnvironmentVariableW(L"COOP_VK_LAYER_LOG", nullptr, 0) != 0 ? 1 : 0;
|
||||
}
|
||||
if (enabled == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
static LARGE_INTEGER freq{};
|
||||
static LARGE_INTEGER last{};
|
||||
static int count = 0;
|
||||
static std::uint64_t last_published = 0;
|
||||
if (freq.QuadPart == 0)
|
||||
{
|
||||
QueryPerformanceFrequency(&freq);
|
||||
QueryPerformanceCounter(&last);
|
||||
}
|
||||
++count;
|
||||
LARGE_INTEGER now;
|
||||
QueryPerformanceCounter(&now);
|
||||
const double sec = static_cast<double>(now.QuadPart - last.QuadPart) / static_cast<double>(freq.QuadPart);
|
||||
if (sec >= 1.0)
|
||||
{
|
||||
const std::uint64_t pub = g_cap.frames_published();
|
||||
logvk("present rate %.1f/s captured %.1f/s (game keeps its rate; capture is off the present thread)",
|
||||
count / sec, (pub - last_published) / sec);
|
||||
last = now;
|
||||
count = 0;
|
||||
last_published = pub;
|
||||
}
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL layer_QueuePresentKHR(VkQueue queue, const VkPresentInfoKHR* pi)
|
||||
{
|
||||
trace_present_rate(); // unconditional so an inactive (pass-through) run gives a baseline to compare
|
||||
if (g_active && g_ipc.connected())
|
||||
{
|
||||
g_ipc.note_present();
|
||||
@@ -552,8 +272,8 @@ VKAPI_ATTR VkResult VKAPI_CALL layer_QueuePresentKHR(VkQueue queue, const VkPres
|
||||
if (s != nullptr && pi->pImageIndices[0] < s->images.size())
|
||||
{
|
||||
VkSemaphore chained = VK_NULL_HANDLE;
|
||||
if (capture(s->images[pi->pImageIndices[0]], s->fmt, s->w, s->h, pi->pWaitSemaphores,
|
||||
pi->waitSemaphoreCount, chained))
|
||||
if (g_cap.present(s->images[pi->pImageIndices[0]], s->fmt, s->w, s->h, pi->pWaitSemaphores,
|
||||
pi->waitSemaphoreCount, chained))
|
||||
{
|
||||
VkPresentInfoKHR p = *pi;
|
||||
p.waitSemaphoreCount = 1;
|
||||
@@ -569,7 +289,7 @@ VKAPI_ATTR VkResult VKAPI_CALL layer_CreateSwapchainKHR(VkDevice device, const V
|
||||
const VkAllocationCallbacks* a, VkSwapchainKHR* out)
|
||||
{
|
||||
const VkResult r = g_real_create_swapchain(device, ci, a, out);
|
||||
if (g_active && r == VK_SUCCESS && out && g_fns.GetSwapchainImagesKHR)
|
||||
if (g_active && r == VK_SUCCESS && out && g_get_swapchain_images)
|
||||
{
|
||||
SwapInfo info{};
|
||||
info.sc = *out;
|
||||
@@ -577,17 +297,19 @@ VKAPI_ATTR VkResult VKAPI_CALL layer_CreateSwapchainKHR(VkDevice device, const V
|
||||
info.w = ci->imageExtent.width;
|
||||
info.h = ci->imageExtent.height;
|
||||
std::uint32_t n = 0;
|
||||
g_fns.GetSwapchainImagesKHR(device, *out, &n, nullptr);
|
||||
g_get_swapchain_images(device, *out, &n, nullptr);
|
||||
info.images.resize(n);
|
||||
g_fns.GetSwapchainImagesKHR(device, *out, &n, info.images.data());
|
||||
g_get_swapchain_images(device, *out, &n, info.images.data());
|
||||
g_swaps.push_back(std::move(info));
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
void load_device_fns(VkDevice dev)
|
||||
// Resolve the device functions the read-back needs and start the capture component.
|
||||
void start_capture(VkDevice dev)
|
||||
{
|
||||
#define LOAD(field, vkname) g_fns.field = reinterpret_cast<PFN_##vkname>(g_next_gdpa(dev, #vkname))
|
||||
VkCapture::Fns f{};
|
||||
#define LOAD(field, vkname) f.field = reinterpret_cast<PFN_##vkname>(g_next_gdpa(dev, #vkname))
|
||||
LOAD(GetDeviceQueue, vkGetDeviceQueue);
|
||||
LOAD(CreateCommandPool, vkCreateCommandPool);
|
||||
LOAD(DestroyCommandPool, vkDestroyCommandPool);
|
||||
@@ -602,6 +324,7 @@ void load_device_fns(VkDevice dev)
|
||||
LOAD(DestroyFence, vkDestroyFence);
|
||||
LOAD(WaitForFences, vkWaitForFences);
|
||||
LOAD(ResetFences, vkResetFences);
|
||||
LOAD(GetFenceStatus, vkGetFenceStatus);
|
||||
LOAD(CreateSemaphore, vkCreateSemaphore);
|
||||
LOAD(DestroySemaphore, vkDestroySemaphore);
|
||||
LOAD(CreateBuffer, vkCreateBuffer);
|
||||
@@ -612,11 +335,27 @@ void load_device_fns(VkDevice dev)
|
||||
LOAD(BindBufferMemory, vkBindBufferMemory);
|
||||
LOAD(MapMemory, vkMapMemory);
|
||||
LOAD(UnmapMemory, vkUnmapMemory);
|
||||
LOAD(GetSwapchainImagesKHR, vkGetSwapchainImagesKHR);
|
||||
LOAD(InvalidateMappedMemoryRanges, vkInvalidateMappedMemoryRanges);
|
||||
LOAD(DeviceWaitIdle, vkDeviceWaitIdle);
|
||||
#undef LOAD
|
||||
g_fns.GetPhysicalDeviceMemoryProperties = reinterpret_cast<PFN_vkGetPhysicalDeviceMemoryProperties>(
|
||||
f.GetPhysicalDeviceMemoryProperties = reinterpret_cast<PFN_vkGetPhysicalDeviceMemoryProperties>(
|
||||
g_next_gipa(g_instance, "vkGetPhysicalDeviceMemoryProperties"));
|
||||
g_get_swapchain_images =
|
||||
reinterpret_cast<PFN_vkGetSwapchainImagesKHR>(g_next_gdpa(dev, "vkGetSwapchainImagesKHR"));
|
||||
|
||||
g_cap.init(g_phys, dev, g_qfam, f, GetCurrentProcessId(), [](std::uint32_t w, std::uint32_t h) {
|
||||
// Runs on the reaper thread after each frame is published to the shared texture. Connect the
|
||||
// IPC channel lazily here (the host may not have created it yet at device-create time), then
|
||||
// publish the frame so the host's generation counter advances.
|
||||
if (!g_ipc_tried.exchange(true))
|
||||
{
|
||||
g_ipc.connect(/*attempts=*/40, /*delay_ms=*/25);
|
||||
}
|
||||
if (g_ipc.connected())
|
||||
{
|
||||
g_ipc.publish_video_frame(w, h, static_cast<std::uint32_t>(DXGI_FORMAT_R8G8B8A8_UNORM));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL layer_gdpa(VkDevice device, const char* name);
|
||||
@@ -651,7 +390,7 @@ VKAPI_ATTR VkResult VKAPI_CALL layer_CreateDevice(VkPhysicalDevice phys, const V
|
||||
g_real_create_swapchain = reinterpret_cast<PFN_vkCreateSwapchainKHR>(next_gdpa(*out, "vkCreateSwapchainKHR"));
|
||||
if (g_active)
|
||||
{
|
||||
load_device_fns(*out);
|
||||
start_capture(*out);
|
||||
}
|
||||
}
|
||||
return r;
|
||||
@@ -688,40 +427,11 @@ VKAPI_ATTR VkResult VKAPI_CALL layer_CreateInstance(const VkInstanceCreateInfo*
|
||||
VKAPI_ATTR void VKAPI_CALL layer_DestroyDevice(VkDevice device, const VkAllocationCallbacks* a)
|
||||
{
|
||||
auto destroy = reinterpret_cast<PFN_vkDestroyDevice>(g_next_gdpa(device, "vkDestroyDevice"));
|
||||
if (g_active && device == g_device && g_fns.DeviceWaitIdle != nullptr)
|
||||
if (g_active && device == g_device)
|
||||
{
|
||||
g_fns.DeviceWaitIdle(device);
|
||||
release_staging();
|
||||
if (g_present_sem)
|
||||
{
|
||||
g_fns.DestroySemaphore(device, g_present_sem, nullptr);
|
||||
g_present_sem = VK_NULL_HANDLE;
|
||||
}
|
||||
if (g_fence)
|
||||
{
|
||||
g_fns.DestroyFence(device, g_fence, nullptr);
|
||||
g_fence = VK_NULL_HANDLE;
|
||||
}
|
||||
if (g_pool)
|
||||
{
|
||||
g_fns.DestroyCommandPool(device, g_pool, nullptr);
|
||||
g_pool = VK_NULL_HANDLE;
|
||||
}
|
||||
release_shared();
|
||||
if (g_d3d_ctx)
|
||||
{
|
||||
g_d3d_ctx->Release();
|
||||
g_d3d_ctx = nullptr;
|
||||
}
|
||||
if (g_d3d)
|
||||
{
|
||||
g_d3d->Release();
|
||||
g_d3d = nullptr;
|
||||
}
|
||||
g_cap.shutdown(); // joins the reaper, drains the device, frees the read-back resources
|
||||
g_swaps.clear();
|
||||
g_device = VK_NULL_HANDLE;
|
||||
g_queue = VK_NULL_HANDLE;
|
||||
g_cmd = VK_NULL_HANDLE;
|
||||
}
|
||||
destroy(device, a);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user