From b042bf253ab6ef0d2e93f8101e4965ec87d5ebd8 Mon Sep 17 00:00:00 2001 From: AyeZeeBB Date: Sat, 10 Oct 2026 17:53:27 -0500 Subject: [PATCH] Overlay on Intel GPUs: watch the swapchain under Streamline's wrapper On Intel GPUs the overlay never drew: no Insert menu, and the pause menu's Multiplayer and Mod Options tabs were empty frames, since the overlay draws what is in them. The log stopped at "Registered DX12 swapchain" with nothing after it. The game is given Streamline's wrapper (sl.interposer.dll) around its swapchain, and the overlay hooked that wrapper's Present. On AMD and NVIDIA the game presents through the wrapper. On Intel the frames are presented on DXGI's own swapchain underneath, the wrapper is passed by, and no presentation reached the overlay. - The swapchain under a wrapper is hooked as well (Present, Present1, ResizeBuffers, ResizeBuffers1), found through Streamline's base-interface query. - It is registered beside the wrapper with the queue the wrapper was made with: it is the same chain, and it gives no queue of its own when asked. - A swapchain that presents without having been seen at creation is registered then, when it gives its queue. - The log says whose Present was hooked, when a wrapper was found, when the first presentation arrives, and why a presenting swapchain cannot be drawn on. Confirmed from an Arc A380 log up to "the first presentation reached the overlay". The last step, registering the underlying chain with the wrapper's queue, has not been run on Intel yet. Not yet checked on AMD or NVIDIA either, where both chains are now registered. --- Extension/UI/Overlay/overlay.cpp | 94 ++++++++++++++++++++++++- Extension/UI/Overlay/overlay_internal.h | 5 ++ Extension/UI/Overlay/overlay_render.cpp | 41 +++++++++++ 3 files changed, 139 insertions(+), 1 deletion(-) diff --git a/Extension/UI/Overlay/overlay.cpp b/Extension/UI/Overlay/overlay.cpp index e5e1325b..ac18dedb 100644 --- a/Extension/UI/Overlay/overlay.cpp +++ b/Extension/UI/Overlay/overlay.cpp @@ -110,6 +110,20 @@ HRESULT STDMETHODCALLTYPE present1(IDXGISwapChain1* chain, UINT interval, UINT f return result; } +template +HRESULT STDMETHODCALLTYPE present_more(IDXGISwapChain* chain, UINT interval, UINT flags) { + PresentGuard guard; + if (guard.outermost) guarded_render(chain, flags); + return original(state().present_more[I])(chain, interval, flags); +} +template +HRESULT STDMETHODCALLTYPE present1_more(IDXGISwapChain1* chain, UINT interval, UINT flags, + const DXGI_PRESENT_PARAMETERS* parameters) { + PresentGuard guard; + if (guard.outermost) guarded_render(chain, flags); + return original(state().present1_more[I])(chain, interval, flags, parameters); +} + bool before_resize(IDXGISwapChain* chain) { auto& s = state(); if (!s.swapchain) return true; @@ -131,6 +145,72 @@ HRESULT STDMETHODCALLTYPE resize(IDXGISwapChain* chain, UINT count, UINT width, if (!before_resize(chain)) return DXGI_ERROR_WAS_STILL_DRAWING; return original(state().resize)(chain, count, width, height, format, flags); } +template +HRESULT STDMETHODCALLTYPE resize_more(IDXGISwapChain* chain, UINT count, UINT width, UINT height, + DXGI_FORMAT format, UINT flags) { + std::lock_guard lock(state().render_mutex); + if (!before_resize(chain)) return DXGI_ERROR_WAS_STILL_DRAWING; + return original(state().resize_more[I])(chain, count, width, height, format, flags); +} +template +HRESULT STDMETHODCALLTYPE resize1_more(IDXGISwapChain3* chain, UINT count, UINT width, UINT height, + DXGI_FORMAT format, UINT flags, const UINT* masks, IUnknown* const* queues) { + std::lock_guard lock(state().render_mutex); + if (!before_resize(chain)) return DXGI_ERROR_WAS_STILL_DRAWING; + return original(state().resize1_more[I])(chain, count, width, height, format, flags, masks, queues); +} +// The module an address is in, for the log. +std::string module_of_address(void* address) { + HMODULE module{}; + if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(address), &module) || !module) return "unknown"; + wchar_t path[MAX_PATH]{}; + GetModuleFileNameW(module, path, MAX_PATH); + const wchar_t* name = path; + for (const wchar_t* c = path; *c; ++c) if (*c == L'\\') name = c + 1; + std::string text; + for (; *name; ++name) text += static_cast(*name); + return text; +} +// Hooks the presentation and resize of the swapchain under a wrapper, when the game was given +// one: a presentation that does not go through the wrapper still reaches the overlay then. +// Returns that swapchain's identity (null when `chain` is no wrapper): it presents with the +// queue the wrapper was made with, and does not give one itself when asked. +ComPtr hook_wrapped_swapchain(IDXGISwapChain* chain) { + auto& s = state(); + // Streamline's wrappers give the object they wrap for this interface (its + // StreamlineRetrieveBaseInterface); anything else says no. + static constexpr GUID base_interface{0xadec44e2, 0x61f0, 0x45c3, {0xad, 0x9f, 0x1b, 0x37, 0x37, 0x92, 0x84, 0xff}}; + ComPtr under; + if (FAILED(chain->QueryInterface(base_interface, reinterpret_cast(under.GetAddressOf()))) || !under) return {}; + ComPtr plain; + ComPtr extended; + if (FAILED(under.As(&plain)) || FAILED(under.As(&extended)) || plain.Get() == chain) return {}; + auto** base = *reinterpret_cast(plain.Get()); + auto** table = *reinterpret_cast(extended.Get()); + const auto add = [](Hook& first, auto& more, void* target, const auto& detours) { + if (first.target == target) return true; + for (const auto& hook : more) if (hook.target == target) return true; + for (std::size_t slot = 0; slot < more.size(); ++slot) + if (!more[slot].target) return install(more[slot], target, detours[slot]); + return false; + }; + const std::array presents{reinterpret_cast(present_more<0>), reinterpret_cast(present_more<1>), reinterpret_cast(present_more<2>)}; + const std::array presents1{reinterpret_cast(present1_more<0>), reinterpret_cast(present1_more<1>), reinterpret_cast(present1_more<2>)}; + const std::array resizes{reinterpret_cast(resize_more<0>), reinterpret_cast(resize_more<1>), reinterpret_cast(resize_more<2>)}; + const std::array resizes1{reinterpret_cast(resize1_more<0>), reinterpret_cast(resize1_more<1>), reinterpret_cast(resize1_more<2>)}; + const bool fresh = std::none_of(s.present_more.begin(), s.present_more.end(), [&](const Hook& hook) { return hook.target == base[8]; }) && + s.present.target != base[8]; + const bool okay = add(s.present, s.present_more, base[8], presents) && add(s.present1, s.present1_more, table[22], presents1) && + add(s.resize, s.resize_more, base[13], resizes) && add(s.resize1, s.resize1_more, table[39], resizes1); + if (fresh || !okay) + dingosdk::logging::printf(okay ? dingosdk::logging::Level::info : dingosdk::logging::Level::warning, dingosdk::logging::Channel::graphics, + okay ? "The game's swapchain is a wrapper (%s) over %s's: both are watched for presentations." + : "The game's swapchain is a wrapper (%s) over %s's, which could not be hooked as well.", + module_of_address((*reinterpret_cast(chain))[8]).c_str(), module_of_address(base[8]).c_str()); + return okay ? object_identity(plain.Get()) : ComPtr{}; +} + HRESULT STDMETHODCALLTYPE resize1(IDXGISwapChain3* chain, UINT count, UINT width, UINT height, DXGI_FORMAT format, UINT flags, const UINT* masks, IUnknown* const* queues) { std::lock_guard lock(state().render_mutex); @@ -400,6 +480,17 @@ extern "C" bool DingoSDKOverlayBindDx12(IDXGISwapChain* chain, ID3D12CommandQueu dingosdk::logging::write(dingosdk::logging::Level::warning, dingosdk::logging::Channel::graphics, "Swapchain hook setup incomplete; candidate ignored."); return false; } + // The swapchain under a wrapper is the same chain to draw on, by the same queue: when it is + // the one that presents (on Intel's GPUs it is), the overlay draws there. + if (const auto under = hook_wrapped_swapchain(chain)) { + if (auto* alias = find_binding(under.Get())) { + alias->queue = queue; + alias->window = description.OutputWindow; + if (s.swapchain_identity == under.Get()) s.queue = queue; + } else if (s.bindings.size() < maximum_bindings) { + s.bindings.push_back({under.Get(), queue, description.OutputWindow}); + } + } if (auto* existing = find_binding(identity.Get())) { existing->queue = queue; @@ -414,7 +505,8 @@ extern "C" bool DingoSDKOverlayBindDx12(IDXGISwapChain* chain, ID3D12CommandQueu s.bindings.erase(evict); } s.bindings.push_back({identity.Get(), queue, description.OutputWindow}); - dingosdk::logging::write(dingosdk::logging::Level::info, dingosdk::logging::Channel::graphics, "Registered DX12 swapchain and its creation queue."); + dingosdk::logging::printf(dingosdk::logging::Level::info, dingosdk::logging::Channel::graphics, + "Registered DX12 swapchain and its creation queue. (Its Present is %s's.)", module_of_address(base[8]).c_str()); return true; } diff --git a/Extension/UI/Overlay/overlay_internal.h b/Extension/UI/Overlay/overlay_internal.h index 1a445440..c0ce6f93 100644 --- a/Extension/UI/Overlay/overlay_internal.h +++ b/Extension/UI/Overlay/overlay_internal.h @@ -158,6 +158,11 @@ struct State { HANDLE provider_event = nullptr; void* dll_notification_cookie = nullptr; Hook factory, create, create_hwnd, present, present1, resize, resize1, fullscreen; + // The same four of other swapchain implementations: DXGI's own under a wrapper the game + // was given (Streamline's), which is what presents when something else presents for the + // game (Intel's frame generation does). + static constexpr std::size_t more_swapchains = 3; + std::array present_more, present1_more, resize_more, resize1_more; Hook clip_cursor, set_cursor_pos; std::vector bindings; IUnknown* swapchain_identity = nullptr; diff --git a/Extension/UI/Overlay/overlay_render.cpp b/Extension/UI/Overlay/overlay_render.cpp index fb5c6e9f..9631d0d4 100644 --- a/Extension/UI/Overlay/overlay_render.cpp +++ b/Extension/UI/Overlay/overlay_render.cpp @@ -103,11 +103,48 @@ Binding* find_binding(IUnknown* identity) { return found == bindings.end() ? nullptr : &*found; } +// A swapchain that presents without the overlay having seen it made (on some Intel systems +// the game's menus never drew: the chain that was registered at creation is not the one that +// reaches Present). A D3D12 swapchain gives the queue it was made with, which is all a binding +// needs. Render thread, with the render lock held. +Binding* bind_presented(IDXGISwapChain* presented, IUnknown* identity) { + auto& s = state(); + static std::atomic refused{}; + const auto refuse = [&](const char* why) -> Binding* { + if (refused.fetch_add(1) < 3) + dingosdk::logging::printf(dingosdk::logging::Level::warning, dingosdk::logging::Channel::graphics, + "Overlay: a swapchain it did not see created is presenting, and it cannot draw on it (%s).", why); + return nullptr; + }; + DXGI_SWAP_CHAIN_DESC description{}; + if (FAILED(presented->GetDesc(&description)) || !description.OutputWindow) return refuse("no description or window"); + if (description.BufferCount < 2 || description.BufferCount > 8) return refuse("an unusual buffer count"); + DWORD process = 0; + GetWindowThreadProcessId(description.OutputWindow, &process); + if (process != GetCurrentProcessId()) return refuse("another process's window"); + ComPtr queue; + if (FAILED(presented->GetDevice(IID_PPV_ARGS(&queue))) || !queue) return refuse("it gives no D3D12 queue"); + if (queue->GetDesc().Type != D3D12_COMMAND_LIST_TYPE_DIRECT) return refuse("its queue is not a direct one"); + if (s.bindings.size() >= maximum_bindings) { + const auto evict = std::find_if(s.bindings.begin(), s.bindings.end(), + [&](const Binding& candidate) { return candidate.identity != s.swapchain_identity; }); + if (evict == s.bindings.end()) return refuse("too many swapchains"); + s.bindings.erase(evict); + } + s.bindings.push_back({identity, queue, description.OutputWindow}); + dingosdk::logging::printf(dingosdk::logging::Level::info, dingosdk::logging::Channel::graphics, + "Registered a DX12 swapchain first seen as it presented (%ux%u, %u buffers, format %d).", + description.BufferDesc.Width, description.BufferDesc.Height, description.BufferCount, + static_cast(description.BufferDesc.Format)); + return &s.bindings.back(); +} + bool select_presented_swapchain(IDXGISwapChain* presented) { auto& s = state(); const auto identity = object_identity(presented); if (!identity) return false; Binding* binding = find_binding(identity.Get()); + if (!binding) binding = bind_presented(presented, identity.Get()); if (!binding) return false; const auto now = std::chrono::steady_clock::now(); @@ -555,6 +592,10 @@ void render(IDXGISwapChain* presented, UINT flags) { if (flags & DXGI_PRESENT_TEST) return; dingosdk::profiler::record_present(); std::lock_guard lock(s.render_mutex); + // Said once: a log without this line is a game whose presentations never reach the overlay. + if (static bool said{}; !std::exchange(said, true)) + dingosdk::logging::write(dingosdk::logging::Level::info, dingosdk::logging::Channel::graphics, + "The game's first presentation reached the overlay."); if (s.stop.load()) { if (!s.swapchain || completed(s.next_fence, 0)) { restore_input(true);