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>
19 KiB
19 KiB