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);