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.
This commit is contained in:
AyeZeeBB committed 2026-10-10 17:53:27 -05:00
1 parent 39897037be
commit b042bf253a
3 files changed
+139 -1

No files matched your search

+93 -1
View File
@@ -110,6 +110,20 @@ HRESULT STDMETHODCALLTYPE present1(IDXGISwapChain1* chain, UINT interval, UINT f
return result;
}
template<std::size_t I>
HRESULT STDMETHODCALLTYPE present_more(IDXGISwapChain* chain, UINT interval, UINT flags) {
PresentGuard guard;
if (guard.outermost) guarded_render(chain, flags);
return original<PresentFn>(state().present_more[I])(chain, interval, flags);
}
template<std::size_t I>
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<Present1Fn>(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<ResizeFn>(state().resize)(chain, count, width, height, format, flags);
}
template<std::size_t I>
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<ResizeFn>(state().resize_more[I])(chain, count, width, height, format, flags);
}
template<std::size_t I>
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<Resize1Fn>(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<LPCWSTR>(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<char>(*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<IUnknown> 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<IUnknown> under;
if (FAILED(chain->QueryInterface(base_interface, reinterpret_cast<void**>(under.GetAddressOf()))) || !under) return {};
ComPtr<IDXGISwapChain> plain;
ComPtr<IDXGISwapChain3> extended;
if (FAILED(under.As(&plain)) || FAILED(under.As(&extended)) || plain.Get() == chain) return {};
auto** base = *reinterpret_cast<void***>(plain.Get());
auto** table = *reinterpret_cast<void***>(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<void*>(present_more<0>), reinterpret_cast<void*>(present_more<1>), reinterpret_cast<void*>(present_more<2>)};
const std::array presents1{reinterpret_cast<void*>(present1_more<0>), reinterpret_cast<void*>(present1_more<1>), reinterpret_cast<void*>(present1_more<2>)};
const std::array resizes{reinterpret_cast<void*>(resize_more<0>), reinterpret_cast<void*>(resize_more<1>), reinterpret_cast<void*>(resize_more<2>)};
const std::array resizes1{reinterpret_cast<void*>(resize1_more<0>), reinterpret_cast<void*>(resize1_more<1>), reinterpret_cast<void*>(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<void***>(chain))[8]).c_str(), module_of_address(base[8]).c_str());
return okay ? object_identity(plain.Get()) : ComPtr<IUnknown>{};
}
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;
}
+5
View File
@@ -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<Hook, more_swapchains> present_more, present1_more, resize_more, resize1_more;
Hook clip_cursor, set_cursor_pos;
std::vector<Binding> bindings;
IUnknown* swapchain_identity = nullptr;
+41
View File
@@ -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<unsigned> 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<ID3D12CommandQueue> 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<int>(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);