Skip to content

Commit f3bf262

Browse files
committed
fix(AltTabFullscreen): force borderless-windowed at swap-chain creation
The previous approach subclassed the engine WndProc and called SetFullscreenState(FALSE) on focus loss, but never restored fullscreen on focus gain. After alt-tab back, the swap chain was windowed while the engine believed it was still in exclusive mode, causing a permanent black-screen lockup on the render thread. Switch to the same approach used by SSE Engine Fixes / Display Tweaks: at D3D11CreateDeviceAndSwapChain, force Windowed=TRUE and clear ALLOW_MODE_SWITCH whenever the engine requested exclusive. Keep MakeWindowAssociation(NO_WINDOW_CHANGES | NO_ALT_ENTER) so DXGI cannot promote us back into exclusive on Alt+Enter. Drop the WndProc subclass and all g_swapChain/g_hwnd/g_origWndProc state.
1 parent 572e256 commit f3bf262

2 files changed

Lines changed: 22 additions & 44 deletions

File tree

.Build/F4SE/Plugins/Addictol.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ bPipBoyCursorConstraints = true
165165
# Fixes a bug where the muzzle-flash light keeps illuminating the scene after the flash ends.
166166
bMuzzleFlashLight = true
167167

168-
# Fixes the exclusive-fullscreen Alt-Tab hang by dropping fullscreen state on focus loss and blocking DXGI's auto Alt+Enter handler.
168+
# Fixes the exclusive-fullscreen Alt-Tab hang by forcing the swap chain to borderless-windowed at creation and blocking DXGI's auto Alt+Enter handler.
169169
bAltTabFullscreen = true
170170

171171
# Fixes a CTD when scrapping or wiring after a settlement mod has been removed, by cleaning up orphan power-grid entries left behind by deleted references.

Addictol/Source/Modules/AdModuleAltTabFullscreen.cpp

Lines changed: 21 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -16,31 +16,6 @@ namespace Addictol
1616
ID3D11Device**, D3D_FEATURE_LEVEL*, ID3D11DeviceContext**);
1717

1818
static TD3D11CreateDeviceAndSwapChain OriginalCreate = nullptr;
19-
static IDXGISwapChain* g_swapChain = nullptr;
20-
static HWND g_hwnd = nullptr;
21-
static WNDPROC g_origWndProc = nullptr;
22-
23-
static LRESULT CALLBACK Hook_WndProc(HWND a_hwnd, UINT a_msg, WPARAM a_wp, LPARAM a_lp) noexcept
24-
{
25-
if (g_swapChain)
26-
{
27-
const bool focusLost =
28-
a_msg == WM_KILLFOCUS ||
29-
(a_msg == WM_ACTIVATEAPP && a_wp == FALSE) ||
30-
(a_msg == WM_ACTIVATE && LOWORD(a_wp) == WA_INACTIVE);
31-
32-
if (focusLost)
33-
{
34-
BOOL isFullscreen = FALSE;
35-
if (SUCCEEDED(g_swapChain->GetFullscreenState(&isFullscreen, nullptr)) && isFullscreen)
36-
g_swapChain->SetFullscreenState(FALSE, nullptr);
37-
}
38-
}
39-
40-
if (g_origWndProc)
41-
return CallWindowProcA(g_origWndProc, a_hwnd, a_msg, a_wp, a_lp);
42-
return DefWindowProcA(a_hwnd, a_msg, a_wp, a_lp);
43-
}
4419

4520
static HRESULT WINAPI Hook_D3D11Create(
4621
IDXGIAdapter* a_adapter,
@@ -56,36 +31,39 @@ namespace Addictol
5631
D3D_FEATURE_LEVEL* a_outFeatureLevel,
5732
ID3D11DeviceContext** a_outContext) noexcept
5833
{
34+
// Force borderless-windowed when the engine requested exclusive fullscreen.
35+
// The vanilla alt-tab hang is rooted in exclusive-mode ResizeBuffers re-entry;
36+
// never entering exclusive eliminates the bug class entirely.
37+
DXGI_SWAP_CHAIN_DESC patchedDesc{};
38+
const DXGI_SWAP_CHAIN_DESC* descToUse = a_desc;
39+
if (a_desc && !a_desc->Windowed)
40+
{
41+
patchedDesc = *a_desc;
42+
patchedDesc.Windowed = TRUE;
43+
patchedDesc.Flags &= ~DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
44+
descToUse = &patchedDesc;
45+
REX::INFO("Alt-Tab Fullscreen: forcing exclusive swap chain to windowed ({}x{}, hwnd {})"sv,
46+
a_desc->BufferDesc.Width, a_desc->BufferDesc.Height,
47+
reinterpret_cast<void*>(a_desc->OutputWindow));
48+
}
49+
5950
const auto hr = OriginalCreate
6051
? OriginalCreate(a_adapter, a_driverType, a_software, a_flags, a_featureLevels,
61-
a_numFeatureLevels, a_sdkVersion, a_desc, a_outSwapChain, a_outDevice,
52+
a_numFeatureLevels, a_sdkVersion, descToUse, a_outSwapChain, a_outDevice,
6253
a_outFeatureLevel, a_outContext)
6354
: E_FAIL;
6455

65-
if (FAILED(hr) || !a_outSwapChain || !*a_outSwapChain || !a_desc)
56+
if (FAILED(hr) || !a_outSwapChain || !*a_outSwapChain || !descToUse)
6657
return hr;
6758

68-
// AddRef so WndProc dispatched during engine teardown won't dereference a freed swap chain.
69-
g_swapChain = *a_outSwapChain;
70-
g_swapChain->AddRef();
71-
g_hwnd = a_desc->OutputWindow;
72-
59+
// Block DXGI's auto Alt+Enter; without this, DXGI can transition us back into exclusive.
7360
IDXGIFactory* factory = nullptr;
74-
if (SUCCEEDED(g_swapChain->GetParent(__uuidof(IDXGIFactory), reinterpret_cast<void**>(&factory))) && factory)
61+
if (SUCCEEDED((*a_outSwapChain)->GetParent(__uuidof(IDXGIFactory), reinterpret_cast<void**>(&factory))) && factory)
7562
{
76-
factory->MakeWindowAssociation(g_hwnd, DXGI_MWA_NO_WINDOW_CHANGES | DXGI_MWA_NO_ALT_ENTER);
63+
factory->MakeWindowAssociation(descToUse->OutputWindow, DXGI_MWA_NO_WINDOW_CHANGES | DXGI_MWA_NO_ALT_ENTER);
7764
factory->Release();
7865
}
7966

80-
if (g_hwnd && !g_origWndProc)
81-
{
82-
if (auto* prior = reinterpret_cast<WNDPROC>(GetWindowLongPtrA(g_hwnd, GWLP_WNDPROC)))
83-
{
84-
g_origWndProc = prior;
85-
SetWindowLongPtrA(g_hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(&Hook_WndProc));
86-
}
87-
}
88-
8967
return hr;
9068
}
9169

0 commit comments

Comments
 (0)