Synchronize and bound the Vulkan swapchain tracking (g_swaps)

g_swaps (vk_hook.cpp and the Vulkan layer) is pushed from the create-swapchain
detour and iterated by the present detour, which can run on different game
threads (Vulkan external sync is per-object, not global), and cleared on removal
from another thread -- all with no mutex. A push_back realloc could dangle the
SwapInfo* a concurrent find_swap/present is using. It was also never pruned, so a
game that recreates its swapchain each resize grew it without bound and could
match a recycled handle's stale images.

Add g_swaps_mutex around every access; the present detour now copies the matched
swapchain's fields out under the lock and captures without holding it (no GPU
submit under the lock, no dangling pointer). Create de-dups by handle and an LRU
cap (8) bounds growth -- the active swapchain is the newest, so it's never
evicted. Deliberately NOT hooking vkDestroySwapchainKHR: forwarding a destroy
incorrectly could break the game, and the de-dup + cap already bound growth and
defeat handle recycling.

Verified real by inspection (a concurrent-create+present Vulkan race isn't
deterministically reproducible in a test); validated by the full mock_game_test
Vulkan paths (capture + layer + too-late) staying green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 01:22:09 +02:00
parent bdb700ec56
commit 47462287fc
3 changed files with 98 additions and 15 deletions

View File

@@ -15,10 +15,12 @@
// 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>
@@ -132,6 +134,12 @@ struct SwapInfo
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;
bool eq(const char* a, const char* b)
{
@@ -209,6 +217,8 @@ 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)
@@ -268,12 +278,28 @@ VKAPI_ATTR VkResult VKAPI_CALL layer_QueuePresentKHR(VkQueue queue, const VkPres
}
if (g_active && pi != nullptr && pi->swapchainCount == 1)
{
const SwapInfo* s = find_swap(pi->pSwapchains[0]);
if (s != nullptr && pi->pImageIndices[0] < s->images.size())
// 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)
{
VkSemaphore chained = VK_NULL_HANDLE;
if (g_cap.present(s->images[pi->pImageIndices[0]], s->fmt, s->w, s->h, pi->pWaitSemaphores,
pi->waitSemaphoreCount, chained))
if (g_cap.present(image, fmt, w, h, pi->pWaitSemaphores, pi->waitSemaphoreCount, chained))
{
VkPresentInfoKHR p = *pi;
p.waitSemaphoreCount = 1;
@@ -305,7 +331,17 @@ VKAPI_ATTR VkResult VKAPI_CALL layer_CreateSwapchainKHR(VkDevice device, const V
g_get_swapchain_images(device, *out, &n, nullptr);
info.images.resize(n);
g_get_swapchain_images(device, *out, &n, info.images.data());
g_swaps.push_back(std::move(info));
{
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());
}
}
}
return r;
}
@@ -435,7 +471,10 @@ VKAPI_ATTR void VKAPI_CALL layer_DestroyDevice(VkDevice device, const VkAllocati
if (g_active && device == g_device)
{
g_cap.shutdown(); // joins the reaper, drains the device, frees the read-back resources
g_swaps.clear();
{
std::scoped_lock lock(g_swaps_mutex);
g_swaps.clear();
}
g_device = VK_NULL_HANDLE;
}
destroy(device, a);