From 2705ceda7beb763b62171dd64cc6eb6f0779cbaf Mon Sep 17 00:00:00 2001 From: GWToolbox Bot Date: Tue, 7 Jul 2026 10:55:30 +0000 Subject: [PATCH] Refuse late-attach if a device is already active RegisterExistingDevice deduped against D3D9Hooks.cpp's own g_devices map, but the cold Direct3DCreate9(Ex) path (D3D9Wrappers.cpp) never touches that map. If gMod is early-injected and something (e.g. GWToolbox adopting a pre-loaded gMod) also calls SetDevice on the resulting wrapper device, RegisterExistingDevice would treat it as new: MinHook-patch the wrapper's own vtable on top, and construct a second TextureClient, hitting its single-instance ASSERT. Check TextureClient::CurrentClient() first so any already-active device (via either path) is a no-op. --- source/D3D9Hooks.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/source/D3D9Hooks.cpp b/source/D3D9Hooks.cpp index 7b9cfda..673d14d 100644 --- a/source/D3D9Hooks.cpp +++ b/source/D3D9Hooks.cpp @@ -357,6 +357,11 @@ void InstallD3D9Hooks(IDirect3D9* d3d9, const bool is_ex) bool RegisterExistingDevice(IDirect3DDevice9* device) { if (device == nullptr) return false; + // Already active, whether registered via this path before or already wrapped by the + // cold Direct3DCreate9(Ex) path (D3D9Wrappers.cpp) - that path never touches g_devices, + // so without this check we'd double-hook the wrapper's own vtable and construct a second + // TextureClient, hitting its single-instance ASSERT. + if (TextureClient::CurrentClient() != nullptr) return false; { std::lock_guard lk(g_devices_mutex); if (g_devices.contains(device)) return false; // already known