diff --git a/hook/src/present_hook.cpp b/hook/src/present_hook.cpp index 02da887..aea2682 100644 --- a/hook/src/present_hook.cpp +++ b/hook/src/present_hook.cpp @@ -62,6 +62,28 @@ ID3D11Device* g_on12_d3d11 = nullptr; ID3D11DeviceContext* g_on12_ctx = nullptr; ID3D12CommandQueue* g_on12_queue = nullptr; ID3D12Device* g_on12_d3d12 = nullptr; +// The game's own present queue we built the bridge against (null = we fell back to +// creating our own queue because it hadn't been captured yet). When this changes we +// rebuild the bridge so the copy keeps running on the game's queue. +ID3D12CommandQueue* g_on12_src_queue = nullptr; + +// ExecuteCommandLists hook: recovers the game's D3D12 *present* command queue so the +// On12 copy can be submitted on the game's own queue (ordered after the frame's +// rendering, like the D3D11 immediate-context path) instead of an independent queue +// that races it -- the cause of the occasional stale-frame stutter under GPU load. +// We hook the per-frame method rather than swapchain/queue creation, so it works for +// late injection (the queue already exists). ID3D12CommandQueue method index 10: +// IUnknown 0-2, ID3D12Object 3-6, ID3D12DeviceChild 7 (ID3D12Pageable adds none), +// then ID3D12CommandQueue UpdateTileMappings 8, CopyTileMappings 9, ExecuteCommandLists 10. +constexpr unsigned kIdx_ID3D12CommandQueue_ExecuteCommandLists = 10; +safetyhook::InlineHook g_hk_ecl; +int g_id_ecl = -1; +// Last DIRECT (graphics) queue seen executing command lists. Present and its queue's +// ExecuteCommandLists run on the same render thread, so the thread-local is the most +// reliable match on engines with multiple DIRECT queues; the atomic is a cross-thread +// fallback (e.g. the first D3D12 Present beating any ExecuteCommandLists on its thread). +thread_local ID3D12CommandQueue* t_present_queue = nullptr; +std::atomic g_present_queue{nullptr}; void* vtable_method(void* obj, unsigned index) { @@ -188,32 +210,45 @@ void release_on12_locked() g_on12_d3d12->Release(); g_on12_d3d12 = nullptr; } + g_on12_src_queue = nullptr; } -// Build the D3D11On12 device for the game's D3D12 `dev` (creating our own DIRECT -// queue on it). Caller holds g_tex_mutex. Returns true when the bridge is ready. -bool ensure_on12_locked(ID3D12Device* dev) +// Build the D3D11On12 device for the game's D3D12 `dev`, submitting our copy work on +// `game_queue` (the game's present queue, captured via the ExecuteCommandLists hook) +// so it's ordered after the frame's rendering. If `game_queue` is null we fall back to +// a queue of our own (correct frame, but can race the game's render under load -- the +// old behavior) until the real queue is captured. Caller holds g_tex_mutex. Returns +// true when the bridge is ready. +bool ensure_on12_locked(ID3D12Device* dev, ID3D12CommandQueue* game_queue) { - if (g_on12 != nullptr && g_on12_d3d12 == dev) + if (g_on12 != nullptr && g_on12_d3d12 == dev && g_on12_src_queue == game_queue) { return true; } release_on12_locked(); - D3D12_COMMAND_QUEUE_DESC qd{}; - qd.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; - ID3D12CommandQueue* queue = nullptr; - HRESULT hr = dev->CreateCommandQueue(&qd, __uuidof(ID3D12CommandQueue), reinterpret_cast(&queue)); - if (FAILED(hr) || queue == nullptr) + // Hold a ref on whichever queue the bridge uses, so Release on teardown is uniform. + ID3D12CommandQueue* queue = game_queue; + if (queue != nullptr) { - logf("present(d3d12): CreateCommandQueue failed hr=0x%08lX", static_cast(hr)); - return false; + queue->AddRef(); + } + else + { + D3D12_COMMAND_QUEUE_DESC qd{}; + qd.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; + HRESULT hr = dev->CreateCommandQueue(&qd, __uuidof(ID3D12CommandQueue), reinterpret_cast(&queue)); + if (FAILED(hr) || queue == nullptr) + { + logf("present(d3d12): CreateCommandQueue failed hr=0x%08lX", static_cast(hr)); + return false; + } } IUnknown* queues[] = {queue}; ID3D11Device* d11 = nullptr; ID3D11DeviceContext* ctx = nullptr; - hr = D3D11On12CreateDevice(dev, 0, nullptr, 0, queues, 1, 0, &d11, &ctx, nullptr); + HRESULT hr = D3D11On12CreateDevice(dev, 0, nullptr, 0, queues, 1, 0, &d11, &ctx, nullptr); if (FAILED(hr) || d11 == nullptr) { logf("present(d3d12): D3D11On12CreateDevice failed hr=0x%08lX", static_cast(hr)); @@ -237,10 +272,11 @@ bool ensure_on12_locked(ID3D12Device* dev) g_on12 = on12; g_on12_d3d11 = d11; g_on12_ctx = ctx; - g_on12_queue = queue; + g_on12_queue = queue; // we hold a ref (the AddRef'd game queue, or our created one) + g_on12_src_queue = game_queue; // what we built against (null = our own queue) g_on12_d3d12 = dev; dev->AddRef(); // we hold a reference for the lifetime of the bridge - logf("present(d3d12): D3D11On12 bridge ready"); + logf("present(d3d12): D3D11On12 bridge ready (queue=%s)", game_queue != nullptr ? "game" : "own"); return true; } @@ -282,10 +318,17 @@ void capture_backbuffer_d3d12(IDXGISwapChain* sc) bool shared = false; UINT w = 0, h = 0; DXGI_FORMAT fmt = DXGI_FORMAT_UNKNOWN; + // The game's present queue, preferring the one seen on this (the render) thread. + ID3D12CommandQueue* game_queue = t_present_queue; + if (game_queue == nullptr) + { + game_queue = g_present_queue.load(std::memory_order_relaxed); + } + if (dev != nullptr) { std::scoped_lock lock(g_tex_mutex); - if (ensure_on12_locked(dev)) + if (ensure_on12_locked(dev, game_queue)) { D3D11_RESOURCE_FLAGS rf{}; rf.BindFlags = D3D11_BIND_RENDER_TARGET; @@ -413,6 +456,58 @@ void capture_backbuffer(IDXGISwapChain* sc) backbuf->Release(); } +void STDMETHODCALLTYPE hk_ExecuteCommandLists(ID3D12CommandQueue* queue, UINT num_lists, + ID3D12CommandList* const* lists) +{ + // Record the graphics queue; compute/copy queues never present, so skip them and + // keep the last DIRECT one (the present queue on single-graphics-queue engines). + if (queue != nullptr && queue->GetDesc().Type == D3D12_COMMAND_LIST_TYPE_DIRECT) + { + t_present_queue = queue; + g_present_queue.store(queue, std::memory_order_relaxed); + hook_note_call(g_id_ecl); + } + g_hk_ecl.stdcall(queue, num_lists, lists); // __stdcall, see hk_Present +} + +// Create a throwaway D3D12 device + command queue to read the address of +// ID3D12CommandQueue::ExecuteCommandLists, so we can inline-hook it (catching the +// game's pre-existing queues regardless of when we injected). Resolves D3D12CreateDevice +// dynamically: only D3D12 games have d3d12.dll loaded, and we never want to force-load +// it into a D3D11 game. Returns null when D3D12 isn't present. +void* grab_execute_command_lists_address() +{ + HMODULE d3d12 = GetModuleHandleW(L"d3d12.dll"); + if (d3d12 == nullptr) + { + return nullptr; // not a D3D12 game -> nothing to capture + } + using PFN_D3D12_CREATE_DEVICE = HRESULT(WINAPI*)(IUnknown*, D3D_FEATURE_LEVEL, REFIID, void**); + auto create = reinterpret_cast(GetProcAddress(d3d12, "D3D12CreateDevice")); + if (create == nullptr) + { + return nullptr; + } + ID3D12Device* dev = nullptr; + if (FAILED(create(nullptr, D3D_FEATURE_LEVEL_11_0, __uuidof(ID3D12Device), reinterpret_cast(&dev))) || + dev == nullptr) + { + return nullptr; + } + D3D12_COMMAND_QUEUE_DESC qd{}; + qd.Type = D3D12_COMMAND_LIST_TYPE_DIRECT; + ID3D12CommandQueue* queue = nullptr; + void* addr = nullptr; + if (SUCCEEDED(dev->CreateCommandQueue(&qd, __uuidof(ID3D12CommandQueue), reinterpret_cast(&queue))) && + queue != nullptr) + { + addr = vtable_method(queue, kIdx_ID3D12CommandQueue_ExecuteCommandLists); + queue->Release(); + } + dev->Release(); + return addr; +} + HRESULT STDMETHODCALLTYPE hk_Present(IDXGISwapChain* sc, UINT sync_interval, UINT flags) { hook_note_call(g_id_present); @@ -533,6 +628,7 @@ bool install_present_hooks(IpcClient& ipc) g_id_present = hook_register("IDXGISwapChain::Present", HookSubsys_Video); g_id_present1 = hook_register("IDXGISwapChain1::Present1", HookSubsys_Video); + g_id_ecl = hook_register("ID3D12CommandQueue::ExecuteCommandLists", HookSubsys_Video); void* present1 = nullptr; void* present = grab_present_address(&present1); @@ -552,6 +648,22 @@ bool install_present_hooks(IpcClient& ipc) hook_set_installed(g_id_present1, static_cast(g_hk_present1)); logf("install_present_hooks: present=%p hooked=%d present1=%p hooked=%d", present, static_cast(g_hk_present) ? 1 : 0, present1, static_cast(g_hk_present1) ? 1 : 0); + + // Capture the game's D3D12 present queue (D3D12 games only; null otherwise). Done + // here at injection time -- d3d12.dll is already loaded in a running D3D12 game -- + // so the queue is recovered even though we attached after it was created. + void* ecl = grab_execute_command_lists_address(); + if (ecl != nullptr) + { + g_hk_ecl = safetyhook::create_inline(ecl, reinterpret_cast(&hk_ExecuteCommandLists)); + hook_set_installed(g_id_ecl, static_cast(g_hk_ecl)); + logf("install_present_hooks: d3d12 ExecuteCommandLists=%p hooked=%d", ecl, + static_cast(g_hk_ecl) ? 1 : 0); + } + else + { + hook_set_installed(g_id_ecl, false); // not a D3D12 game; On12 path uses its own queue + } return static_cast(g_hk_present); } @@ -559,8 +671,11 @@ void remove_present_hooks() { g_hk_present = {}; g_hk_present1 = {}; + g_hk_ecl = {}; hook_set_installed(g_id_present, false); hook_set_installed(g_id_present1, false); + hook_set_installed(g_id_ecl, false); + g_present_queue.store(nullptr, std::memory_order_relaxed); { std::scoped_lock lock(g_tex_mutex); release_shared_locked();