// Borderless fullscreen. // // The game renders at a fixed resolution (1024x768). Unmodified, windowed mode // shows that as a 1024x768 popup centred on the desktop; fullscreen mode // switches the display mode. With this mod the game always runs windowed, the // popup covers the whole monitor (mods/monitor.h: the preferred one), and each frame is scaled up into the largest // centred rectangle with the game's aspect ratio (black bars elsewhere). // // Mouse positions are mapped back from that rectangle to game pixels, both in // GetMouseClientPos (polled by the game) and in mouse window messages. // // sacredK.ini: // [borderless] // Enabled=1 ; 0 = original behaviour // IntegerScale=0 ; 1 = scale by whole multiples only (sharper, larger bars) // RunInBackground=1 ; keep the game running when it loses focus // // Vanilla pauses when the window loses focus (WM_ACTIVATEAPP sets bit 0x20000 in // 0x0182cdb8 and pauses sound) and stops presenting; a monitor-sized window then // goes black. With RunInBackground the deactivation is never passed to the // game. Because the game polls the keyboard with GetAsyncKeyState, which sees // keys pressed in any application, key state is hidden from it while another // window has the focus. // // Known gap: one code path (0x00610650, cursor nudging) moves the cursor with // SetCursorPos by a game-pixel delta, which is not scaled to screen pixels. #include #include #include #include #include "game/engine/input.h" #include "game/render/dx_driver7.h" #include "mods/hud.h" #include "mods/input.h" #include "mods/monitor.h" #include "mods/widescreen.h" #include "re/config.h" #include "game/addresses.h" #include "re/hook.h" #include "re/log.h" namespace { // Where a gameWidth x gameHeight image appears in the window. struct Layout { int gameWidth = 0; int gameHeight = 0; RECT client = {}; // scaled game image, window client coordinates RECT screen = {}; // same rectangle, screen coordinates }; struct Viewport { bool active = false; HWND window = nullptr; bool runInBackground = false; Layout full; // the whole back buffer }; Viewport g_viewport; const Layout& CurrentLayout() { return g_viewport.full; } bool Enabled() { return re::ConfigInt("borderless", "Enabled", 1) != 0; } // Largest rectangle with the game's aspect ratio that fits the monitor. RECT FitRect(int areaWidth, int areaHeight, int width, int height, bool integerScale) { int scaledWidth; int scaledHeight; if (integerScale) { int scale = std::max(1, std::min(areaWidth / width, areaHeight / height)); scaledWidth = width * scale; scaledHeight = height * scale; } else if (static_cast(areaWidth) * height > static_cast(areaHeight) * width) { scaledHeight = areaHeight; scaledWidth = MulDiv(width, areaHeight, height); } else { scaledWidth = areaWidth; scaledHeight = MulDiv(height, areaWidth, width); } int left = (areaWidth - scaledWidth) / 2; int top = (areaHeight - scaledHeight) / 2; return {left, top, left + scaledWidth, top + scaledHeight}; } void ApplyBorderless(game::DxDriver7* driver) { HWND window = driver->window; const RECT area = mods::monitor::GameMonitorRect(); const int areaWidth = area.right - area.left; const int areaHeight = area.bottom - area.top; SetWindowLongW(window, GWL_STYLE, WS_POPUP | WS_VISIBLE); SetWindowPos(window, HWND_TOP, area.left, area.top, areaWidth, areaHeight, SWP_FRAMECHANGED | SWP_SHOWWINDOW); const bool integerScale = re::ConfigInt("borderless", "IntegerScale", 0) != 0; auto makeLayout = [&](int width, int height) { Layout layout; layout.gameWidth = width; layout.gameHeight = height; layout.client = FitRect(areaWidth, areaHeight, width, height, integerScale); layout.screen = layout.client; OffsetRect(&layout.screen, area.left, area.top); return layout; }; Viewport viewport; viewport.window = window; viewport.runInBackground = re::ConfigInt("borderless", "RunInBackground", 1) != 0; viewport.full = makeLayout(driver->mode.width, driver->mode.height); viewport.active = true; g_viewport = viewport; // Paint the bars: the window class background brush is black. InvalidateRect(window, nullptr, TRUE); const RECT& image = viewport.full.client; re::Log("borderless: %dx%d game -> %ldx%ld monitor, image at (%ld,%ld) %ldx%ld", viewport.full.gameWidth, viewport.full.gameHeight, static_cast(areaWidth), static_cast(areaHeight), image.left, image.top, image.right - image.left, image.bottom - image.top); } // Window client coordinates -> game pixels. POINT ToGame(POINT p) { const Layout& layout = CurrentLayout(); const int width = layout.client.right - layout.client.left; const int height = layout.client.bottom - layout.client.top; POINT result; result.x = std::clamp(MulDiv(p.x - layout.client.left, layout.gameWidth, width), 0, layout.gameWidth - 1); result.y = std::clamp(MulDiv(p.y - layout.client.top, layout.gameHeight, height), 0, layout.gameHeight - 1); result.x -= mods::hud::MenuFrameOffsetX(); // the main menu's centred frame return result; } // Fills everything except the game image with black. void PaintBars(HDC dc) { RECT window; GetClientRect(g_viewport.window, &window); const RECT& image = CurrentLayout().client; const int saved = SaveDC(dc); ExcludeClipRect(dc, image.left, image.top, image.right, image.bottom); FillRect(dc, &window, static_cast(GetStockObject(BLACK_BRUSH))); RestoreDC(dc, saved); } bool IsMouseMessageWithClientCoords(UINT message) { // WM_MOUSEWHEEL and WM_MOUSEHWHEEL carry screen coordinates; leave them alone. return message >= WM_MOUSEFIRST && message <= WM_MOUSELAST && message != WM_MOUSEWHEEL && message != WM_MOUSEHWHEEL; } } // namespace namespace game { RE_HOOK(addr::DxDriver7_SetDisplayMode, DxDriver7_SetDisplayMode); RE_HOOK(addr::DxDriver7_Flip, DxDriver7_Flip); RE_HOOK(addr::GetMouseClientPos, GetMouseClientPos); RE_HOOK(addr::MainWindowProc, MainWindowProc); int __fastcall DxDriver7_SetDisplayMode(DxDriver7* self, void* edx, void* deviceCaps, const DxDisplayMode* mode, uint32_t unknown) { if (!Enabled()) { return DxDriver7_SetDisplayMode_original(self, edx, deviceCaps, mode, unknown); } DxDisplayMode windowed = *mode; windowed.fullscreen = 0; mods::widescreen::ApplyRenderSize(windowed); g_viewport.active = false; const int result = DxDriver7_SetDisplayMode_original(self, edx, deviceCaps, &windowed, unknown); if (result) { ApplyBorderless(self); } return result; } int __fastcall DxDriver7_Flip(DxDriver7* self, void* edx) { // The original computes the blit rectangles once, for a centred window of // exactly the game's size; override them on every present. if (g_viewport.active && self->window == g_viewport.window) { const Layout& layout = CurrentLayout(); self->blitSource = {0, 0, layout.gameWidth, layout.gameHeight}; self->blitDest = layout.screen; } const int result = DxDriver7_Flip_original(self, edx); static std::atomic failures = 0; if (result != 0 && failures++ < 5) { re::Log("borderless: present failed (0x%08x), source %ldx%ld -> dest (%ld,%ld)-(%ld,%ld)", static_cast(result), self->blitSource.right, self->blitSource.bottom, self->blitDest.left, self->blitDest.top, self->blitDest.right, self->blitDest.bottom); } return result; } void __cdecl GetMouseClientPos(HWND window, POINT* position) { GetMouseClientPos_original(window, position); if (window && g_viewport.active && window == g_viewport.window) { *position = ToGame(*position); position->x -= mods::hud::CursorOffsetX(); } } LRESULT CALLBACK MainWindowProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam) { if (mods::input::HandleWindowMessage(window, message, wParam, lParam)) { return 0; } if (!g_viewport.active || window != g_viewport.window) { return MainWindowProc_original(window, message, wParam, lParam); } if (IsMouseMessageWithClientCoords(message)) { POINT game = ToGame({GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)}); lParam = MAKELPARAM(game.x, game.y); } switch (message) { case WM_ERASEBKGND: PaintBars(reinterpret_cast(wParam)); return 1; case WM_ACTIVATEAPP: if (!wParam && g_viewport.runInBackground) { re::Log("borderless: focus lost, game keeps running"); return 0; } break; } return MainWindowProc_original(window, message, wParam, lParam); } } // namespace game // The game polls keys with GetAsyncKeyState, which reports keys pressed in any // application. While it runs in the background, hide them from it. SHORT WINAPI GetAsyncKeyStateWhenFocused(int key); RE_HOOK_API(GetAsyncKeyState, GetAsyncKeyStateWhenFocused); SHORT WINAPI GetAsyncKeyStateWhenFocused(int key) { if (g_viewport.active && g_viewport.runInBackground && GetForegroundWindow() != g_viewport.window) { return 0; } return GetAsyncKeyStateWhenFocused_original(key); } // The game's "bring the window to the front" (0x810af0, thiscall (window, // timeout ms), ret 8; WinMain calls it three times): ShowWindow, then // SetForegroundWindow until the window is in front and SetFocus until it has // the focus. WinMain passes 0xffffffff, which never times out. Windows refuses // the foreground to a process when the user clicked or typed elsewhere while // it started, so the game hung at startup (2026-10-20, four times; main // thread in this loop, log ending at WM_ACTIVATEAPP(1, 0)). Now it asks for a // limited time, with the input of the window in front attached (which Windows // allows), and carries on in the background otherwise; a click brings it up. constexpr DWORD kForegroundWaitMs = 2000; bool __fastcall BringWindowToFront(void* self, void* edx, HWND window, DWORD timeout); RE_HOOK(0x00810af0, BringWindowToFront); bool __fastcall BringWindowToFront(void*, void*, HWND window, DWORD timeout) { ShowWindow(window, SW_SHOW); const DWORD wait = std::min(timeout, kForegroundWaitMs); const DWORD start = GetTickCount(); while (GetForegroundWindow() != window && GetTickCount() - start < wait) { const DWORD front = GetWindowThreadProcessId(GetForegroundWindow(), nullptr); const DWORD self = GetCurrentThreadId(); const bool attached = front && front != self && AttachThreadInput(front, self, TRUE); BringWindowToTop(window); SetForegroundWindow(window); if (attached) { AttachThreadInput(front, self, FALSE); } Sleep(10); } const bool inFront = GetForegroundWindow() == window; if (inFront) { SetFocus(window); } else { re::Log("borderless: the game window did not get to the front, starting in the background"); } UpdateWindow(window); return inFront; }