Skip to content

Commit 128c71c

Browse files
authored
fix(alttab-fullscreen): block direct SetFullscreenState(TRUE) (#28)
MakeWindowAssociation only catches DXGI's auto alt-enter; explicit calls bypassed it. Vtable-hook slot 10 on the returned swap chain, scoped to the game HWND so other mods' swap chains forward untouched.
1 parent 5391da2 commit 128c71c

1 file changed

Lines changed: 56 additions & 1 deletion

File tree

Addictol/Source/Modules/AdModuleAltTabFullscreen.cpp

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,32 @@ 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 neuter our own swap chain; ReShade/ENB/overlay swap chains get forwarded.
30+
if (a_fullscreen)
31+
{
32+
DXGI_SWAP_CHAIN_DESC desc{};
33+
const auto descHr = a_this->GetDesc(&desc);
34+
const auto gameHwnd = g_gameHwnd.load(std::memory_order_relaxed);
35+
if (SUCCEEDED(descHr) && desc.OutputWindow == gameHwnd && gameHwnd)
36+
{
37+
REX::INFO("Alt-Tab Fullscreen: SetFullscreenState(TRUE) blocked on game window."sv);
38+
return S_OK;
39+
}
40+
}
41+
42+
return orig ? orig(a_this, a_fullscreen, a_output) : S_OK;
43+
}
1944

2045
static HRESULT WINAPI Hook_D3D11Create(
2146
IDXGIAdapter* a_adapter,
@@ -58,8 +83,38 @@ namespace Addictol
5883
IDXGIFactory* factory = nullptr;
5984
if (SUCCEEDED((*a_outSwapChain)->GetParent(__uuidof(IDXGIFactory), reinterpret_cast<void**>(&factory))) && factory)
6085
{
61-
factory->MakeWindowAssociation(descToUse->OutputWindow, DXGI_MWA_NO_WINDOW_CHANGES | DXGI_MWA_NO_ALT_ENTER);
86+
const auto mwaHr = factory->MakeWindowAssociation(descToUse->OutputWindow, DXGI_MWA_NO_WINDOW_CHANGES | DXGI_MWA_NO_ALT_ENTER);
6287
factory->Release();
88+
if (FAILED(mwaHr))
89+
{
90+
REX::WARN("Alt-Tab Fullscreen: MakeWindowAssociation failed (hr=0x{:08X})."sv, static_cast<std::uint32_t>(mwaHr));
91+
}
92+
}
93+
94+
// MakeWindowAssociation isn't enough; explicit SetFullscreenState(TRUE) bypasses it.
95+
g_gameHwnd.store(descToUse->OutputWindow, std::memory_order_relaxed);
96+
97+
bool expected = false;
98+
if (g_vtablePatched.compare_exchange_strong(expected, true))
99+
{
100+
constexpr std::uint32_t kSetFullscreenStateSlot = 10;
101+
auto** const vtablePtr = *reinterpret_cast<void***>(*a_outSwapChain);
102+
// Publish original before the swap so the forward path never sees nullptr.
103+
OriginalSetFullscreenState.store(
104+
reinterpret_cast<TSetFullscreenState>(vtablePtr[kSetFullscreenStateSlot]),
105+
std::memory_order_release);
106+
107+
const auto vtableAddr = reinterpret_cast<std::uintptr_t>(vtablePtr);
108+
const auto returned = RELEX::DetourVTable(
109+
vtableAddr,
110+
reinterpret_cast<std::uintptr_t>(&Hook_SetFullscreenState),
111+
kSetFullscreenStateSlot);
112+
if (!returned)
113+
{
114+
REX::WARN("Alt-Tab Fullscreen: SetFullscreenState vtable patch failed."sv);
115+
OriginalSetFullscreenState.store(nullptr, std::memory_order_release);
116+
g_vtablePatched.store(false);
117+
}
63118
}
64119

65120
return hr;

0 commit comments

Comments
 (0)