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:
@@ -1,9 +1,7 @@
|
||||
#include "vk_hook.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
#include <windows.h>
|
||||
@@ -23,6 +21,7 @@
|
||||
#include "hook_install.hpp"
|
||||
#include "hook_registry.hpp"
|
||||
#include "vk_capture.hpp"
|
||||
#include "vk_swapchain_registry.hpp"
|
||||
|
||||
namespace coop::hook {
|
||||
|
||||
@@ -62,25 +61,7 @@ std::uint32_t g_qfam = 0;
|
||||
|
||||
VkCapture g_cap; // the shared, off-present-thread read-back (same component the layer uses)
|
||||
|
||||
// Tracked swap chains (small; engines have one or two).
|
||||
struct SwapInfo {
|
||||
VkSwapchainKHR sc;
|
||||
VkFormat fmt;
|
||||
std::uint32_t w;
|
||||
std::uint32_t h;
|
||||
std::vector<VkImage> images;
|
||||
};
|
||||
std::vector<SwapInfo> g_swaps;
|
||||
// g_swaps is pushed from the create-swapchain detour and read from the present detour, which can run
|
||||
// on different game threads (Vulkan external-sync is per-object, not global), and cleared from the
|
||||
// worker thread on removal. This mutex serializes all of that -- a push_back realloc must not race a
|
||||
// concurrent find_swap iteration. We don't hook vkDestroySwapchainKHR (failing to forward a destroy
|
||||
// could break the game); instead create de-dups by handle and an LRU cap bounds growth, so a game
|
||||
// that recreates its swapchain on every resize can't grow g_swaps without bound or match a recycled
|
||||
// handle's stale images. The active swapchain is always the most-recently created, so it's never
|
||||
// evicted.
|
||||
std::mutex g_swaps_mutex;
|
||||
constexpr std::size_t kMaxTrackedSwaps = 8; // engines use 1-3; headroom for transient resize overlap
|
||||
VkSwapchainRegistry g_swaps; // create-swapchain records images here; present maps them back for capture
|
||||
|
||||
// --- the real vkGetInstanceProcAddr, via the inline hook's trampoline --------
|
||||
PFN_vkVoidFunction real_gipa(VkInstance inst, const char* name)
|
||||
@@ -88,18 +69,6 @@ PFN_vkVoidFunction real_gipa(VkInstance inst, const char* name)
|
||||
return g_hk_gipa.stdcall<PFN_vkVoidFunction>(inst, name);
|
||||
}
|
||||
|
||||
// Caller must hold g_swaps_mutex; the returned pointer is only valid until the lock is released
|
||||
// (copy out what you need before unlocking, since another thread can push_back and reallocate).
|
||||
const SwapInfo* find_swap(VkSwapchainKHR sc)
|
||||
{
|
||||
for (const SwapInfo& s : g_swaps) {
|
||||
if (s.sc == sc) {
|
||||
return &s;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL hk_vkQueuePresentKHR(VkQueue queue, const VkPresentInfoKHR* pPresentInfo)
|
||||
{
|
||||
DetourGate::Guard guard(g_gate); // keep the read-back resources alive for this whole detour
|
||||
@@ -114,27 +83,10 @@ VKAPI_ATTR VkResult VKAPI_CALL hk_vkQueuePresentKHR(VkQueue queue, const VkPrese
|
||||
// read-back state, even though the game keeps calling this cached detour pointer.
|
||||
if (g_capture_enabled.load(std::memory_order_acquire) && g_device != VK_NULL_HANDLE && pPresentInfo != nullptr
|
||||
&& pPresentInfo->swapchainCount == 1) {
|
||||
// Copy the matched swapchain's fields out under the lock, then capture without holding it (so
|
||||
// the GPU submit can't block a concurrent create, and the SwapInfo* can't dangle on a realloc).
|
||||
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(pPresentInfo->pSwapchains[0]);
|
||||
const std::uint32_t idx = pPresentInfo->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(pPresentInfo->pSwapchains[0], pPresentInfo->pImageIndices[0], f)) {
|
||||
VkSemaphore chained = VK_NULL_HANDLE;
|
||||
if (g_cap.present(image, fmt, w, h, pPresentInfo->pWaitSemaphores, pPresentInfo->waitSemaphoreCount,
|
||||
if (g_cap.present(f.image, f.fmt, f.w, f.h, pPresentInfo->pWaitSemaphores, pPresentInfo->waitSemaphoreCount,
|
||||
chained)) {
|
||||
// Replace the present's wait with our chained semaphore (our submit consumed the
|
||||
// originals and signals this one), so the present still orders after rendering.
|
||||
@@ -154,27 +106,11 @@ VKAPI_ATTR VkResult VKAPI_CALL hk_vkCreateSwapchainKHR(VkDevice device, const Vk
|
||||
DetourGate::Guard guard(g_gate); // keep g_swaps stable while remove may be clearing it
|
||||
const VkResult r = g_real_create_swapchain(device, ci, alloc, out);
|
||||
if (r == VK_SUCCESS && out != nullptr && g_get_swapchain_images != nullptr) {
|
||||
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);
|
||||
// De-dup a recycled handle value, then bound growth (drop the oldest; the just-created
|
||||
// active swapchain is newest and stays).
|
||||
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));
|
||||
// (Don't log the VkSwapchainKHR handle: it's a uint64_t on x86, not a pointer.)
|
||||
logf("vk: swapchain %ux%u fmt=%d images=%u", ci->imageExtent.width, ci->imageExtent.height,
|
||||
static_cast<int>(ci->imageFormat), n);
|
||||
@@ -359,10 +295,7 @@ void remove_vk_hooks()
|
||||
g_cap.shutdown(); // joins the reaper, drains the device, frees the read-back resources
|
||||
|
||||
hook_set_installed(g_id_present, false);
|
||||
{
|
||||
std::scoped_lock lock(g_swaps_mutex); // drained above, but keep all g_swaps access serialized
|
||||
g_swaps.clear();
|
||||
}
|
||||
g_swaps.clear(); // registry locks internally
|
||||
g_get_swapchain_images = nullptr;
|
||||
g_device = VK_NULL_HANDLE;
|
||||
g_instance = VK_NULL_HANDLE;
|
||||
|
||||
79
hook/src/vk_swapchain_registry.hpp
Normal file
79
hook/src/vk_swapchain_registry.hpp
Normal file
@@ -0,0 +1,79 @@
|
||||
// Tracks the swap chains a Vulkan game creates so the present path can map a (swapchain, image
|
||||
// index) back to the VkImage being presented (and its format/size) for capture. Shared by BOTH
|
||||
// early-presence paths -- the inline hook (vk_hook.cpp) and the implicit layer
|
||||
// (vk_layer/coop_vk_layer.cpp) -- which each own one instance.
|
||||
//
|
||||
// Thread-safety: add() runs on the create-swapchain path and lookup() on the present path, which can
|
||||
// be different game threads (Vulkan external-sync is per-object, not global); clear() runs on the
|
||||
// worker/teardown thread. One mutex serializes all of it -- a push_back realloc must not race a
|
||||
// concurrent lookup. lookup() copies the fields out under the lock, so the caller never holds a
|
||||
// pointer that a concurrent add() could dangle.
|
||||
//
|
||||
// We deliberately do NOT track swap-chain destruction (forwarding a vkDestroySwapchainKHR wrong could
|
||||
// break the game): add() de-dups a recycled handle value and an LRU cap bounds growth, so a game that
|
||||
// recreates its swapchain every resize can neither grow the table without bound nor match a recycled
|
||||
// handle's stale images. The active swapchain is always the most-recently added, so it is never evicted.
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
namespace coop::hook {
|
||||
|
||||
class VkSwapchainRegistry {
|
||||
public:
|
||||
// Record (or refresh) a swapchain's images. Takes ownership of `images`.
|
||||
void add(VkSwapchainKHR sc, VkFormat fmt, std::uint32_t w, std::uint32_t h, std::vector<VkImage> images)
|
||||
{
|
||||
std::scoped_lock lock(mutex_);
|
||||
std::erase_if(swaps_, [&](const Entry& e) { return e.sc == sc; }); // drop a recycled handle
|
||||
swaps_.push_back({sc, fmt, w, h, std::move(images)});
|
||||
if (swaps_.size() > kMax) {
|
||||
swaps_.erase(swaps_.begin()); // bound growth; the just-added active swapchain stays
|
||||
}
|
||||
}
|
||||
|
||||
// The presented frame for (sc, image_index). False if the swapchain isn't tracked or the index is
|
||||
// out of range.
|
||||
struct Frame {
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VkFormat fmt = VK_FORMAT_UNDEFINED;
|
||||
std::uint32_t w = 0;
|
||||
std::uint32_t h = 0;
|
||||
};
|
||||
bool lookup(VkSwapchainKHR sc, std::uint32_t image_index, Frame& out) const
|
||||
{
|
||||
std::scoped_lock lock(mutex_);
|
||||
for (const Entry& e : swaps_) {
|
||||
if (e.sc == sc && image_index < e.images.size()) {
|
||||
out = {e.images[image_index], e.fmt, e.w, e.h};
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void clear()
|
||||
{
|
||||
std::scoped_lock lock(mutex_);
|
||||
swaps_.clear();
|
||||
}
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
VkSwapchainKHR sc;
|
||||
VkFormat fmt;
|
||||
std::uint32_t w;
|
||||
std::uint32_t h;
|
||||
std::vector<VkImage> images;
|
||||
};
|
||||
|
||||
static constexpr std::size_t kMax = 8; // engines use 1-3; headroom for transient resize overlap
|
||||
mutable std::mutex mutex_;
|
||||
std::vector<Entry> swaps_;
|
||||
};
|
||||
|
||||
} // namespace coop::hook
|
||||
Reference in New Issue
Block a user