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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user