Share the Vulkan swapchain tracking between the hook and the layer

vk_hook.cpp and coop_vk_layer.cpp each carried a verbatim copy of the
swap-chain registry -- the SwapInfo struct, the vector+mutex, find_swap,
the create-time de-dup + LRU cap, and the present-time lookup -- because
they are two independent early-presence paths (inline hook vs implicit
layer). The tracking logic is identical, so lift it into one
VkSwapchainRegistry (hook/src/vk_swapchain_registry.hpp); each module
owns an instance. add() de-dups + LRU-caps, lookup() copies the frame
out under the lock, clear() resets -- same behavior, one definition.

Net -45 lines. mock_game_test exercises both paths (the inline-hook vk
storm and the implicit-layer capture) and passes.
This commit is contained in:
2026-07-12 12:17:30 +02:00
parent b2b8abdc51
commit 0e57863d1f
3 changed files with 98 additions and 143 deletions

View File

@@ -15,12 +15,10 @@
// The loader/layer interface structs (VkLayer*CreateInfo, VkNegotiateLayerInterface) live in
// vk_layer.h, which Vulkan-Headers doesn't ship, so they're declared here to the stable
// loader-interface-version-2 ABI.
#include <algorithm>
#include <atomic>
#include <cstdarg>
#include <cstdio>
#include <cstring>
#include <mutex>
#include <string>
#include <vector>
@@ -36,6 +34,7 @@
#include "coop/shared_memory.hpp"
#include "ipc_client.hpp"
#include "vk_capture.hpp"
#include "vk_swapchain_registry.hpp"
// --- Loader/layer interface (interface version 2) ---------------------------
extern "C" {
@@ -99,6 +98,7 @@ constexpr VkStructureType kLoaderDeviceCreateInfo = static_cast<VkStructureType>
using coop::hook::IpcClient;
using coop::hook::VkCapture;
using coop::hook::VkSwapchainRegistry;
IpcClient g_ipc;
std::atomic<bool> g_ipc_tried{false}; // reaper-thread side: connect once on the first published frame
@@ -118,19 +118,7 @@ std::uint32_t g_qfam = 0;
VkCapture g_cap; // the shared, off-present-thread read-back
struct SwapInfo {
VkSwapchainKHR sc;
VkFormat fmt;
std::uint32_t w, h;
std::vector<VkImage> images;
};
std::vector<SwapInfo> g_swaps;
// Serialize all g_swaps access: create (push) and present (find) can run on different game threads,
// and DestroyDevice clears it. We don't track swapchain destruction (forwarding a destroy wrong could
// break the game); create de-dups by handle and an LRU cap bounds growth so repeated resize/recreate
// can't grow g_swaps unbounded or match a recycled handle's stale images.
std::mutex g_swaps_mutex;
constexpr std::size_t kMaxTrackedSwaps = 8;
VkSwapchainRegistry g_swaps; // create-swapchain records images here; present maps them back for capture
bool eq(const char* a, const char* b)
{
@@ -199,18 +187,6 @@ bool decide_active()
return _stricmp(self8, want) == 0;
}
// Caller must hold g_swaps_mutex; copy out what you need before unlocking (a concurrent push_back
// can reallocate and dangle the returned pointer).
const SwapInfo* find_swap(VkSwapchainKHR sc)
{
for (const SwapInfo& s : g_swaps) {
if (s.sc == sc) {
return &s;
}
}
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.
@@ -252,26 +228,10 @@ VKAPI_ATTR VkResult VKAPI_CALL layer_QueuePresentKHR(VkQueue queue, const VkPres
g_ipc.note_present();
}
if (g_active && pi != nullptr && pi->swapchainCount == 1) {
// Copy the matched swapchain out under the lock, then capture without holding it.
VkImage image = VK_NULL_HANDLE;
VkFormat fmt = VK_FORMAT_UNDEFINED;
std::uint32_t w = 0, h = 0;
bool matched = false;
{
std::scoped_lock lock(g_swaps_mutex);
const SwapInfo* s = find_swap(pi->pSwapchains[0]);
const std::uint32_t idx = pi->pImageIndices[0];
if (s != nullptr && idx < s->images.size()) {
image = s->images[idx];
fmt = s->fmt;
w = s->w;
h = s->h;
matched = true;
}
}
if (matched) {
VkSwapchainRegistry::Frame f;
if (g_swaps.lookup(pi->pSwapchains[0], pi->pImageIndices[0], f)) {
VkSemaphore chained = VK_NULL_HANDLE;
if (g_cap.present(image, fmt, w, h, pi->pWaitSemaphores, pi->waitSemaphoreCount, chained)) {
if (g_cap.present(f.image, f.fmt, f.w, f.h, pi->pWaitSemaphores, pi->waitSemaphoreCount, chained)) {
VkPresentInfoKHR p = *pi;
p.waitSemaphoreCount = 1;
p.pWaitSemaphores = &chained;
@@ -292,25 +252,11 @@ VKAPI_ATTR VkResult VKAPI_CALL layer_CreateSwapchainKHR(VkDevice device, const V
static_cast<int>(ci->presentMode), ci->imageExtent.width, ci->imageExtent.height, ci->minImageCount);
const VkResult r = g_real_create_swapchain(device, ci, a, out);
if (g_active && r == VK_SUCCESS && out && g_get_swapchain_images) {
SwapInfo info{};
info.sc = *out;
info.fmt = ci->imageFormat;
info.w = ci->imageExtent.width;
info.h = ci->imageExtent.height;
std::uint32_t n = 0;
g_get_swapchain_images(device, *out, &n, nullptr);
info.images.resize(n);
g_get_swapchain_images(device, *out, &n, info.images.data());
{
std::scoped_lock lock(g_swaps_mutex);
g_swaps.erase(
std::remove_if(g_swaps.begin(), g_swaps.end(), [&](const SwapInfo& e) { return e.sc == info.sc; }),
g_swaps.end());
g_swaps.push_back(std::move(info));
if (g_swaps.size() > kMaxTrackedSwaps) {
g_swaps.erase(g_swaps.begin());
}
}
std::vector<VkImage> images(n);
g_get_swapchain_images(device, *out, &n, images.data());
g_swaps.add(*out, ci->imageFormat, ci->imageExtent.width, ci->imageExtent.height, std::move(images));
}
return r;
}
@@ -428,10 +374,7 @@ VKAPI_ATTR void VKAPI_CALL layer_DestroyDevice(VkDevice device, const VkAllocati
auto destroy = reinterpret_cast<PFN_vkDestroyDevice>(g_next_gdpa(device, "vkDestroyDevice"));
if (g_active && device == g_device) {
g_cap.shutdown(); // joins the reaper, drains the device, frees the read-back resources
{
std::scoped_lock lock(g_swaps_mutex);
g_swaps.clear();
}
g_swaps.clear(); // registry locks internally
g_device = VK_NULL_HANDLE;
}
destroy(device, a);