Skip to content

Commit d66c4e5

Browse files
committed
fix(alttab-fullscreen): patch SetFullscreenState to actually keep us windowed
MakeWindowAssociation(NO_ALT_ENTER) only blocks DXGI's automatic alt-enter on the IDXGIFactory we walked back to via swapchain->GetParent(). It does nothing against: - direct SetFullscreenState(TRUE,...) calls from the engine, which still happen because we silently rewrote Windowed=FALSE to TRUE and the engine's own renderer state still expects exclusive - DXGI's auto alt-enter on any other IDXGIFactory the process creates later for the same HWND (HighFPSPhysicsFix, ENB, or the engine itself enumerating adapters) When either path fires, DXGI tries to transition the swap chain back into exclusive fullscreen. Whether that succeeds, fails, or partially applies depends on monitor state and timing, which shifts the window focus events off the vanilla timeline. perchik71 hit the soft Creations load-order mismatch dialog at ~1-in-10-20 launches, bisected to this module; the working theory is that the focus-event timing shift races the Bethesda.net Creations refresh callback against the save's CC list comparison. This vtable-hooks slot 10 (SetFullscreenState) on the IDXGISwapChain returned from D3D11CreateDeviceAndSwapChain. TRUE transitions on the game's HWND are short-circuited to S_OK without forwarding; FALSE transitions forward to the original. Scoping by HWND inside the hook avoids touching other mods' swap chains (ReShade/ENB) that share the same vtable. Notes: - Pre-publish the original function pointer to std::atomic before calling DetourVTable, so a racing thread that catches the patched vtable never sees nullptr in the forward path. - Patch is once-only via compare_exchange; failure rolls the flag back so the next D3D11Create attempt can retry. - Logs MakeWindowAssociation failure (was previously silently swallowed). - Existing Windowed=TRUE rewrite and MakeWindowAssociation calls preserved as belt-and-suspenders.
1 parent 5391da2 commit d66c4e5

1 file changed

Lines changed: 62 additions & 1 deletion

File tree

Addictol/Source/Modules/AdModuleAltTabFullscreen.cpp

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,33 @@ namespace Addictol
1515
const DXGI_SWAP_CHAIN_DESC*, IDXGISwapChain**,
1616
ID3D11Device**, D3D_FEATURE_LEVEL*, ID3D11DeviceContext**);
1717

18+
using TSetFullscreenState = HRESULT(WINAPI*)(IDXGISwapChain*, BOOL, IDXGIOutput*);
19+
1820
static TD3D11CreateDeviceAndSwapChain OriginalCreate = nullptr;
21+
static std::atomic<TSetFullscreenState> OriginalSetFullscreenState{ nullptr };
22+
static std::atomic<HWND> g_gameHwnd{ nullptr };
23+
static std::atomic<bool> g_vtablePatched{ false };
24+
25+
static HRESULT WINAPI Hook_SetFullscreenState(IDXGISwapChain* a_this, BOOL a_fullscreen, IDXGIOutput* a_output) noexcept
26+
{
27+
const auto orig = OriginalSetFullscreenState.load(std::memory_order_acquire);
28+
29+
// Only block exclusive transitions on the game's own swap chain. ReShade/ENB/overlays
30+
// that create their own swap chains in-process get their calls forwarded untouched.
31+
if (a_fullscreen)
32+
{
33+
DXGI_SWAP_CHAIN_DESC desc{};
34+
const auto descHr = a_this->GetDesc(&desc);
35+
const auto gameHwnd = g_gameHwnd.load(std::memory_order_relaxed);
36+
if (SUCCEEDED(descHr) && desc.OutputWindow == gameHwnd && gameHwnd)
37+
{
38+
REX::INFO("Alt-Tab Fullscreen: SetFullscreenState(TRUE) blocked on game window."sv);
39+
return S_OK;
40+
}
41+
}
42+
43+
return orig ? orig(a_this, a_fullscreen, a_output) : S_OK;
44+
}
1945

2046
static HRESULT WINAPI Hook_D3D11Create(
2147
IDXGIAdapter* a_adapter,
@@ -58,8 +84,43 @@ namespace Addictol
5884
IDXGIFactory* factory = nullptr;
5985
if (SUCCEEDED((*a_outSwapChain)->GetParent(__uuidof(IDXGIFactory), reinterpret_cast<void**>(&factory))) && factory)
6086
{
61-
factory->MakeWindowAssociation(descToUse->OutputWindow, DXGI_MWA_NO_WINDOW_CHANGES | DXGI_MWA_NO_ALT_ENTER);
87+
const auto mwaHr = factory->MakeWindowAssociation(descToUse->OutputWindow, DXGI_MWA_NO_WINDOW_CHANGES | DXGI_MWA_NO_ALT_ENTER);
6288
factory->Release();
89+
if (FAILED(mwaHr))
90+
{
91+
REX::WARN("Alt-Tab Fullscreen: MakeWindowAssociation failed (hr=0x{:08X})."sv, static_cast<std::uint32_t>(mwaHr));
92+
}
93+
}
94+
95+
// MakeWindowAssociation only catches DXGI's auto alt-enter on this factory. Direct
96+
// SetFullscreenState(TRUE) from the engine or another mod still goes through, and a
97+
// second IDXGIFactory created later for the same HWND has its own (unset) association.
98+
// Patch the shared swap chain vtable so every path lands in our short-circuit; scope
99+
// the block by HWND inside the hook so we don't touch other mods' swap chains.
100+
g_gameHwnd.store(descToUse->OutputWindow, std::memory_order_relaxed);
101+
102+
bool expected = false;
103+
if (g_vtablePatched.compare_exchange_strong(expected, true))
104+
{
105+
constexpr std::uint32_t kSetFullscreenStateSlot = 10;
106+
auto** const vtablePtr = *reinterpret_cast<void***>(*a_outSwapChain);
107+
// Publish the original first so a racing thread that catches the patched vtable
108+
// never sees a null original pointer in the FALSE forward path.
109+
OriginalSetFullscreenState.store(
110+
reinterpret_cast<TSetFullscreenState>(vtablePtr[kSetFullscreenStateSlot]),
111+
std::memory_order_release);
112+
113+
const auto vtableAddr = reinterpret_cast<std::uintptr_t>(vtablePtr);
114+
const auto returned = RELEX::DetourVTable(
115+
vtableAddr,
116+
reinterpret_cast<std::uintptr_t>(&Hook_SetFullscreenState),
117+
kSetFullscreenStateSlot);
118+
if (!returned)
119+
{
120+
REX::WARN("Alt-Tab Fullscreen: SetFullscreenState vtable patch failed."sv);
121+
OriginalSetFullscreenState.store(nullptr, std::memory_order_release);
122+
g_vtablePatched.store(false);
123+
}
63124
}
64125

65126
return hr;

0 commit comments

Comments
 (0)