diff --git a/src/core/Frontend.cpp b/src/core/Frontend.cpp index 7977b0f..a8c6851 100644 --- a/src/core/Frontend.cpp +++ b/src/core/Frontend.cpp @@ -5681,6 +5681,7 @@ CMenuManager::ProcessUserInput(uint8 goDown, uint8 goUp, uint8 optionSelected, u m_PrefsShowLegends = true; m_PrefsVsyncDisp = true; m_PrefsFrameLimiter = true; + RsGlobal.maxFPS = 30; m_PrefsRadarMode = 0; m_PrefsShowHud = true; m_nDisplayVideoMode = m_nPrefsVideoMode; diff --git a/src/core/MenuScreensCustom.cpp b/src/core/MenuScreensCustom.cpp index 6c80fb3..4e37f2d 100644 --- a/src/core/MenuScreensCustom.cpp +++ b/src/core/MenuScreensCustom.cpp @@ -28,6 +28,7 @@ #include "Text.h" #include "crossplatform.h" #include "custompipes.h" +#include "framepacing.h" #include "main.h" #include "platform.h" #include "postfx.h" @@ -38,6 +39,9 @@ #ifdef CUSTOM_FRONTEND_OPTIONS +#define FRAME_LIMIT_SELECTOR \ + MENUACTION_CFO_DYNAMIC, "FEM_FRM", {new CCFODynamic(nil, nil, nil, FrameLimitDraw, FrameLimitButtonPress)}, 0, 0, MENUALIGN_LEFT, + #if defined(IMPROVED_VIDEOMODE) && !defined(GTA_HANDHELD) #ifdef UNLOCK_MENU_OPTIONS_INGAME #define VIDEOMODE_SELECTOR \ @@ -146,6 +150,58 @@ const char *filterNames[] = { "FEM_NON", "FEM_SIM", "FEM_NRM", "FEM_MOB" }; const char *off_on[] = { "FEM_OFF", "FEM_ON" }; + +static const int frameLimits[] = {30, 40, 50, 60, 75, 90, 100, 120, 144, 165, 180, 200, 240}; + +static wchar * +FrameLimitDraw(bool *disabled, bool userHovering) +{ + (void)userHovering; + *disabled = false; + if(!FrontEndMenuManager.m_PrefsFrameLimiter) return TheText.Get("FEM_OFF"); + + static wchar text[16]; + char value[16]; + snprintf(value, sizeof(value), "%d FPS", RsFramePacer::SanitizeLimit(RsGlobal.maxFPS)); + AsciiToUnicode(value, text); + return text; +} + +static void +FrameLimitButtonPress(int8 action) +{ + if(action != FEOPTION_ACTION_LEFT && action != FEOPTION_ACTION_RIGHT && action != FEOPTION_ACTION_SELECT) return; + + const bool increase = action != FEOPTION_ACTION_LEFT; + if(!FrontEndMenuManager.m_PrefsFrameLimiter) { + RsGlobal.maxFPS = increase ? frameLimits[0] : frameLimits[ARRAY_SIZE(frameLimits) - 1]; + FrontEndMenuManager.m_PrefsFrameLimiter = true; + } else { + const int current = RsFramePacer::SanitizeLimit(RsGlobal.maxFPS); + int next = 0; + if(increase) { + for(int i = 0; i < ARRAY_SIZE(frameLimits); i++) { + if(frameLimits[i] > current) { + next = frameLimits[i]; + break; + } + } + } else { + for(int i = ARRAY_SIZE(frameLimits) - 1; i >= 0; i--) { + if(frameLimits[i] < current) { + next = frameLimits[i]; + break; + } + } + } + + // Keep the last valid limit while disabled; zero is never a frame interval. + if(next) RsGlobal.maxFPS = next; + else FrontEndMenuManager.m_PrefsFrameLimiter = false; + } + FrontEndMenuManager.SaveSettings(); +} + #ifdef RW_WGPU // WebGPU backend choices. Index maps to rw::wgpu::BACKEND_* in the startup // plumbing (glfw.cpp). D3D12 is Windows-only; OpenGL is the desktop-GL path. @@ -161,6 +217,7 @@ RestoreDefGraphics(int8 action) { if(action != FEOPTION_ACTION_SELECT) return; + RsGlobal.maxFPS = 30; #ifdef PS2_ALPHA_TEST gPS2alphaTest = false; #endif @@ -748,12 +805,7 @@ CMenuScreenCustom aScreens[] = { 0, MENUALIGN_LEFT, #endif - MENUACTION_FRAMELIMIT, - "FEM_FRM", - {nil, SAVESLOT_NONE, MENUPAGE_DISPLAY_SETTINGS}, - 0, - 0, - MENUALIGN_LEFT, + FRAME_LIMIT_SELECTOR #if defined LEGACY_MENU_OPTIONS && !defined EXTENDED_COLOURFILTER MENUACTION_TRAILS, "FED_TRA", @@ -1953,12 +2005,7 @@ CMenuScreenCustom aScreens[] = { 0, MENUALIGN_LEFT, #endif - MENUACTION_FRAMELIMIT, - "FEM_FRM", - {nil, SAVESLOT_NONE, MENUPAGE_GRAPHICS_SETTINGS}, - 0, - 0, - MENUALIGN_LEFT, + FRAME_LIMIT_SELECTOR MULTISAMPLING_SELECTOR ISLAND_LOADING_SELECTOR DUALPASS_SELECTOR WGPU_BACKEND_SELECTOR #ifdef EXTENDED_COLOURFILTER diff --git a/src/core/re3.cpp b/src/core/re3.cpp index e930d2a..9e6c3aa 100644 --- a/src/core/re3.cpp +++ b/src/core/re3.cpp @@ -50,6 +50,7 @@ #include "Zones.h" #include "crossplatform.h" +#include "framepacing.h" #ifndef _WIN32 #include "assert.h" @@ -560,6 +561,7 @@ bool LoadINISettings() ReadIniIfExists("Graphics", "AspectRatio", &FrontEndMenuManager.m_PrefsUseWideScreen); ReadIniIfExists("Graphics", "FrameLimiter", &FrontEndMenuManager.m_PrefsFrameLimiter); ReadIniIfExists("Graphics", "MaxFPS", &RsGlobal.maxFPS); + RsGlobal.maxFPS = RsFramePacer::SanitizeLimit(RsGlobal.maxFPS); #ifdef LEGACY_MENU_OPTIONS ReadIniIfExists("Graphics", "VSync", &FrontEndMenuManager.m_PrefsVsyncDisp); ReadIniIfExists("Graphics", "Trails", &CMBlur::BlurOn); diff --git a/src/skel/framepacing.h b/src/skel/framepacing.h new file mode 100644 index 0000000..69c544a --- /dev/null +++ b/src/skel/framepacing.h @@ -0,0 +1,71 @@ +#ifndef RS_FRAMEPACING_H +#define RS_FRAMEPACING_H + +#include +#include + +// One policy and sub-millisecond clock for every desktop rendering backend. +// The platform performs the interruptible wait, then checks input/focus again. +class RsFramePacer +{ + bool m_started; + int m_targetFPS; + double m_lastFrame; + +public: + RsFramePacer() { Reset(); } + + static int SanitizeLimit(int fps) + { + return fps <= 0 ? 30 : (fps > 1000 ? 1000 : fps); + } + + static int GetTargetFPS(bool limiterEnabled, int maxFPS, bool menuActive) + { + if (!limiterEnabled) + return menuActive ? 60 : 0; + const int limit = SanitizeLimit(maxFPS); + return menuActive && limit > 60 ? 60 : limit; + } + + static double CurrentTimeSeconds() + { + return std::chrono::duration(std::chrono::steady_clock::now().time_since_epoch()).count(); + } + + void Reset() + { + m_started = false; + m_targetFPS = 0; + m_lastFrame = 0.0; + } + + // A zero result permits and records this frame. Never accumulate overdue + // deadlines: slow rendering, loading or focus loss must not cause a burst. + double RemainingSeconds(double now, int targetFPS) + { + if (!std::isfinite(now)) { + Reset(); + return 0.0; + } + + targetFPS = targetFPS > 0 ? SanitizeLimit(targetFPS) : 0; + if (!m_started || targetFPS == 0 || targetFPS != m_targetFPS || now < m_lastFrame) { + m_started = true; + m_targetFPS = targetFPS; + m_lastFrame = now; + return 0.0; + } + + const double remaining = 1.0 / (double)targetFPS - (now - m_lastFrame); + // Arithmetic at an exact deadline can differ by a fraction of a + // nanosecond. Do not dispatch another OS wait for that rounding noise. + if (remaining <= 1.0e-9) { + m_lastFrame = now; + return 0.0; + } + return remaining; + } +}; + +#endif diff --git a/src/skel/glfw/glfw.cpp b/src/skel/glfw/glfw.cpp index 0528ff9..0ecbc98 100644 --- a/src/skel/glfw/glfw.cpp +++ b/src/skel/glfw/glfw.cpp @@ -38,6 +38,11 @@ long _dwOperatingSystemVersion; #include "platform.h" #include "rwcore.h" #include "skeleton.h" +#include "framepacing.h" +#ifdef _WIN32 +#include "../win/framewait.h" +#include "../win/windowssession.h" +#endif #include #include "AnimViewer.h" @@ -79,6 +84,7 @@ static RwBool WindowFocused = TRUE; static RwBool WindowFocusLostLatch = FALSE; static RwBool WindowPlatformPaused = FALSE; static RwInt32 WindowActivePollCount = 2; +static RsFramePacer FramePacer; static RwBool RwInitialised = FALSE; @@ -158,6 +164,7 @@ ApplyWindowMinimizedPause(RwBool paused) WindowPlatformPaused = paused; ForegroundApp = !paused; + FramePacer.Reset(); CTimer::SetWindowMinimizedPause(!!paused); MoviePlayer::SetPaused(!!paused); @@ -182,6 +189,47 @@ MarkWindowFocusLost(void) ForegroundApp = FALSE; } +#ifdef _WIN32 +static void +MinimizeInactiveFullscreenWindow(void) +{ + GLFWwindow *window = PSGLOBAL(window); + if(window == nil) return; + HWND nativeWindow = glfwGetWin32Window(window); + static HWND activityWindow = nil; + static RwBool WindowHasBeenActive = FALSE; + if(activityWindow != nativeWindow) { + activityWindow = nativeWindow; + WindowHasBeenActive = FALSE; + } + + HWND foreground = GetForegroundWindow(); + if(foreground == nativeWindow) { + WindowHasBeenActive = TRUE; + return; + } + + // GLFW auto-iconifies monitor-attached fullscreen windows only. Borderless + // is deliberately a regular window, so pause alone leaves its last frame + // covering the desktop when Alt+Tab selects another application. + if(!WindowHasBeenActive || !IsFullscreenLikeWindowMode() || foreground == nil || + RsGlobal.quit || glfwWindowShouldClose(window) || + !IsWindowVisible(nativeWindow) || IsIconic(nativeWindow)) return; + + DWORD foregroundProcess = 0; + if(!GetWindowThreadProcessId(foreground, &foregroundProcess) || + foregroundProcess == GetCurrentProcessId() || + GetAncestor(foreground, GA_ROOTOWNER) == GetAncestor(nativeWindow, GA_ROOTOWNER)) return; + + // Run only after glfwPollEvents, never from a focus callback or during a + // mode change. Return the desktop before any audio/GPU pause work; restore + // remains an explicit user action through the taskbar or Alt+Tab. + glfwIconifyWindow(window); + WindowIconified = glfwGetWindowAttrib(window, GLFW_ICONIFIED) == GLFW_TRUE; + if(WindowIconified) MarkWindowFocusLost(); +} +#endif + bool psRefreshAndGetWindowMinimizedPause() { @@ -332,6 +380,23 @@ psCameraShowRaster(RwCamera *camera) return; } +static bool +IsFrameReady(int targetFPS) +{ + const double remaining = FramePacer.RemainingSeconds(glfwGetTime(), targetFPS); + if(remaining <= 0.0) return true; + + // Windows GLFW truncates its ordinary timeout to whole milliseconds; + // use the same precise, message-aware wait as the D3D9 skeleton there. +#ifdef _WIN32 + RsWaitForFrameWin32(remaining); +#else + glfwWaitEventsTimeout(remaining); +#endif + // Always recheck close/focus/input in the main loop, even after a timeout. + return false; +} + /* ***************************************************************************** */ @@ -2171,7 +2236,6 @@ WinMain(HINSTANCE instance, HINSTANCE prevInstance __RWUNUSED__, CMDSTR cmdLine, SetGameWorkingDirectory(); RwInt32 argc; RwChar **argv; - SystemParametersInfo(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, nil, SPIF_SENDCHANGE); #ifndef MASTER if(strstr(cmdLine, "-console")) { @@ -2292,19 +2356,7 @@ main(int argc, char *argv[]) RsEventHandler(rsCAMERASIZE, &r); } #ifdef _WIN32 - SystemParametersInfo(SPI_SETPOWEROFFACTIVE, FALSE, nil, SPIF_SENDCHANGE); - SystemParametersInfo(SPI_SETLOWPOWERACTIVE, FALSE, nil, SPIF_SENDCHANGE); - - STICKYKEYS SavedStickyKeys; - SavedStickyKeys.cbSize = sizeof(STICKYKEYS); - - SystemParametersInfo(SPI_GETSTICKYKEYS, sizeof(STICKYKEYS), &SavedStickyKeys, SPIF_SENDCHANGE); - - STICKYKEYS NewStickyKeys; - NewStickyKeys.cbSize = sizeof(STICKYKEYS); - NewStickyKeys.dwFlags = SKF_TWOKEYSOFF; - - SystemParametersInfo(SPI_SETSTICKYKEYS, sizeof(STICKYKEYS), &NewStickyKeys, SPIF_SENDCHANGE); + RsWindowsSession windowsSession; #endif { @@ -2360,6 +2412,7 @@ main(int argc, char *argv[]) while(TRUE) { RwInitialised = TRUE; + FramePacer.Reset(); /* * Set the initial mouse position... @@ -2399,10 +2452,28 @@ main(int argc, char *argv[]) #endif { glfwPollEvents(); + // Alt+F4/close received above must not start one last GPU frame or + // apply a queued resize before we leave the render loop. + if(RsGlobal.quit || glfwWindowShouldClose(PSGLOBAL(window))) break; +#ifdef _WIN32 + MinimizeInactiveFullscreenWindow(); +#endif +#if defined(_WIN32) && defined(RW_WGPU) + // A fast CPU must not queue frames faster than D3D12/the compositor + // can retire them. Keep pumping focus/minimize/close messages while + // the previous frame finishes, before resize/acquire can block in DXGI. + psRefreshAndGetWindowMinimizedPause(); + if(RwInitialised && !rw::wgpu::pollFrameReady()) { + RsWaitForFrameWin32(0.001); + continue; + } +#endif ApplyPendingRenderSize(); // Callbacks above only latch raw state. Apply the transition here, // outside GLFW's native message-dispatch stack. +#if !defined(_WIN32) || !defined(RW_WGPU) psRefreshAndGetWindowMinimizedPause(); +#endif #ifdef GET_KEYBOARD_INPUT_FROM_X11 checkKeyPresses(); #endif @@ -2516,6 +2587,9 @@ main(int argc, char *argv[]) } case GS_FRONTEND: { + if(!IsFrameReady(RsFramePacer::GetTargetFPS(FrontEndMenuManager.m_PrefsFrameLimiter, + RsGlobal.maxFPS, true))) + break; if(!WindowIconified) RsEventHandler(rsFRONTENDIDLE, nil); #ifdef PS2_MENU @@ -2578,9 +2652,10 @@ main(int argc, char *argv[]) } case GS_PLAYING_GAME: { - float ms = (float)CTimer::GetCurrentTimeInCycles() / (float)CTimer::GetCyclesPerMillisecond(); if(RwInitialised) { - if(!FrontEndMenuManager.m_PrefsFrameLimiter || (1000.0f / (float)RsGlobal.maxFPS) < ms) + const int targetFPS = RsFramePacer::GetTargetFPS(FrontEndMenuManager.m_PrefsFrameLimiter, + RsGlobal.maxFPS, FrontEndMenuManager.m_bMenuActive || CGame::IsWindowPauseMenuActive()); + if(IsFrameReady(targetFPS)) RsEventHandler(rsIDLE, (void *)TRUE); } break; @@ -2597,6 +2672,15 @@ main(int argc, char *argv[]) */ RwInitialised = FALSE; +#if defined(_WIN32) && defined(RW_WGPU) + if(RsGlobal.quit || glfwWindowShouldClose(PSGLOBAL(window))) { + // Return the desktop and cursor before resource/driver teardown. + // A borderless window must not cover other apps during cleanup. + _InputShutdownMouse(); + glfwHideWindow(PSGLOBAL(window)); + } +#endif + FrontEndMenuManager.UnloadTextures(); #ifdef PS2_MENU if(!(FrontEndMenuManager.m_bWantToRestart || TheMemoryCard.b_FoundRecentSavedGameWantToLoad)) break; @@ -2695,9 +2779,6 @@ main(int argc, char *argv[]) */ free(argv); - SystemParametersInfo(SPI_SETSTICKYKEYS, sizeof(STICKYKEYS), &SavedStickyKeys, SPIF_SENDCHANGE); - SystemParametersInfo(SPI_SETPOWEROFFACTIVE, TRUE, nil, SPIF_SENDCHANGE); - SystemParametersInfo(SPI_SETLOWPOWERACTIVE, TRUE, nil, SPIF_SENDCHANGE); SetErrorMode(0); #endif diff --git a/src/skel/win/framewait.h b/src/skel/win/framewait.h new file mode 100644 index 0000000..c2a747c --- /dev/null +++ b/src/skel/win/framewait.h @@ -0,0 +1,60 @@ +#ifndef RS_FRAMEWAIT_WIN32_H +#define RS_FRAMEWAIT_WIN32_H + +#ifdef _WIN32 +#include +#include +#include + +// Wait without dispatching callbacks. Both desktop skeletons recheck their +// native message/focus state in the main loop before beginning another frame. +inline void +RsWaitForFrameWin32(double remainingSeconds) +{ + if (!std::isfinite(remainingSeconds) || remainingSeconds <= 0.0) + return; + // The supported minimum limit is one FPS. Bound conversions defensively + // and allow the caller to recheck its deadline after at most one second. + const double remaining = 1000.0 * (remainingSeconds > 1.0 ? 1.0 : remainingSeconds); + struct FrameWaitTimer { + HANDLE handle; + FrameWaitTimer() : handle(nullptr) + { + typedef HANDLE (WINAPI *CreateTimerFn)(LPSECURITY_ATTRIBUTES, LPCWSTR, DWORD, DWORD); + CreateTimerFn createTimer = (CreateTimerFn)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "CreateWaitableTimerExW"); + if (createTimer) + // CREATE_WAITABLE_TIMER_HIGH_RESOLUTION (Windows 10 1803+). + handle = createTimer(nullptr, nullptr, 0x00000002, SYNCHRONIZE | TIMER_MODIFY_STATE); + } + ~FrameWaitTimer() + { + if (handle) { + CancelWaitableTimer(handle); + CloseHandle(handle); + } + } + }; + static FrameWaitTimer timer; + if (timer.handle) { + LARGE_INTEGER dueTime; + dueTime.QuadPart = -(LONGLONG)std::ceil(remaining * 10000.0); + if (SetWaitableTimer(timer.handle, &dueTime, 0, nullptr, nullptr, FALSE)) { + DWORD result = MsgWaitForMultipleObjectsEx(1, &timer.handle, INFINITE, QS_ALLINPUT, MWMO_INPUTAVAILABLE); + CancelWaitableTimer(timer.handle); + if (result != WAIT_FAILED) + return; + } + } + + // Older Windows versions lack high-resolution waitable timers. Request a + // finer timeout only for this wait, and release it before returning. + const DWORD waitMs = (DWORD)std::ceil(remaining); + const bool raisedTimerResolution = timeBeginPeriod(1) == TIMERR_NOERROR; + if (MsgWaitForMultipleObjectsEx(0, nullptr, waitMs, QS_ALLINPUT, MWMO_INPUTAVAILABLE) == WAIT_FAILED) + Sleep(waitMs); + if (raisedTimerResolution) + timeEndPeriod(1); +} +#endif + +#endif diff --git a/src/skel/win/win.cpp b/src/skel/win/win.cpp index 0d99bf2..4144d3e 100644 --- a/src/skel/win/win.cpp +++ b/src/skel/win/win.cpp @@ -49,6 +49,9 @@ #include "rwcore.h" #include "resource.h" #include "skeleton.h" +#include "framepacing.h" +#include "framewait.h" +#include "windowssession.h" #include "platform.h" #include "crossplatform.h" @@ -60,6 +63,7 @@ static RwBool WindowMinimized = FALSE; static RwBool WindowFocusLostLatch = FALSE; static RwBool WindowPlatformPaused = FALSE; static RwInt32 WindowActivePollCount = 2; +static RsFramePacer FramePacer; static RwBool RwInitialised = FALSE; static bool WindowModeChangeInProgress; @@ -172,6 +176,7 @@ ApplyWindowMinimizedPause(RwBool paused) WindowPlatformPaused = paused; ForegroundApp = !paused; + FramePacer.Reset(); CTimer::SetWindowMinimizedPause(!!paused); if (paused) { @@ -401,56 +406,13 @@ psCameraShowRaster(RwCamera *camera) } static bool -IsFrameReady(float maxFPS) +IsFrameReady(int targetFPS) { - if (maxFPS <= 0.0f) - return true; - - const double elapsed = (double)CTimer::GetCurrentTimeInCycles() / (double)CTimer::GetCyclesPerMillisecond(); - const double remaining = 1000.0 / (double)maxFPS - elapsed; + const double remaining = FramePacer.RemainingSeconds(RsFramePacer::CurrentTimeSeconds(), targetFPS); if (remaining <= 0.0) return true; - // Yield between frames, but wake immediately for input or focus changes. - // Recheck those messages in the main loop before starting another frame. - struct MenuFrameTimer { - HANDLE handle; - MenuFrameTimer() : handle(nil) - { - typedef HANDLE (WINAPI *CreateTimerFn)(LPSECURITY_ATTRIBUTES, LPCWSTR, DWORD, DWORD); - CreateTimerFn createTimer = (CreateTimerFn)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "CreateWaitableTimerExW"); - if (createTimer) - // CREATE_WAITABLE_TIMER_HIGH_RESOLUTION (Windows 10 1803+). - handle = createTimer(nil, nil, 0x00000002, SYNCHRONIZE | TIMER_MODIFY_STATE); - } - ~MenuFrameTimer() - { - if (handle) { - CancelWaitableTimer(handle); - CloseHandle(handle); - } - } - }; - static MenuFrameTimer timer; - if (timer.handle) { - LARGE_INTEGER dueTime; - dueTime.QuadPart = -(LONGLONG)ceil(remaining * 10000.0); - if (SetWaitableTimer(timer.handle, &dueTime, 0, nil, nil, FALSE)) { - DWORD result = MsgWaitForMultipleObjectsEx(1, &timer.handle, INFINITE, QS_ALLINPUT, MWMO_INPUTAVAILABLE); - CancelWaitableTimer(timer.handle); - if (result != WAIT_FAILED) - return false; - } - } - - // Older Windows versions lack high-resolution waitable timers. Request a - // finer timeout only for this wait, and release it before returning. - const DWORD waitMs = (DWORD)ceil(remaining); - const bool raisedTimerResolution = timeBeginPeriod(1) == TIMERR_NOERROR; - if (MsgWaitForMultipleObjectsEx(0, nil, waitMs, QS_ALLINPUT, MWMO_INPUTAVAILABLE) == WAIT_FAILED) - Sleep(waitMs); - if (raisedTimerResolution) - timeEndPeriod(1); + RsWaitForFrameWin32(remaining); return false; } @@ -1216,6 +1178,10 @@ MainWndProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam) { case WM_SYSCOMMAND: { + // Keep this active game window awake without changing the desktop's + // screensaver/power policy (and broadcasting changes to other apps). + if (((wParam & 0xFFF0) == SC_SCREENSAVE || (wParam & 0xFFF0) == SC_MONITORPOWER) && IsWindowActuallyActive()) + return 0L; if ((wParam & 0xFFF0) == SC_KEYMENU && lParam == 0) return 0L; break; @@ -2355,7 +2321,6 @@ WinMain(HINSTANCE instance, RwV2d pos; RwInt32 argc, i; RwChar **argv; - SystemParametersInfo(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, nil, SPIF_SENDCHANGE); #ifndef MASTER if (strstr(cmdLine, "-console")) @@ -2474,21 +2439,7 @@ WinMain(HINSTANCE instance, RsEventHandler(rsCAMERASIZE, &r); } - SystemParametersInfo(SPI_SETSCREENSAVEACTIVE, FALSE, nil, SPIF_SENDCHANGE); - SystemParametersInfo(SPI_SETPOWEROFFACTIVE, FALSE, nil, SPIF_SENDCHANGE); - SystemParametersInfo(SPI_SETLOWPOWERACTIVE, FALSE, nil, SPIF_SENDCHANGE); - - - STICKYKEYS SavedStickyKeys; - SavedStickyKeys.cbSize = sizeof(STICKYKEYS); - - SystemParametersInfo(SPI_GETSTICKYKEYS, sizeof(STICKYKEYS), &SavedStickyKeys, SPIF_SENDCHANGE); - - STICKYKEYS NewStickyKeys; - NewStickyKeys.cbSize = sizeof(STICKYKEYS); - NewStickyKeys.dwFlags = SKF_TWOKEYSOFF; - - SystemParametersInfo(SPI_SETSTICKYKEYS, sizeof(STICKYKEYS), &NewStickyKeys, SPIF_SENDCHANGE); + RsWindowsSession windowsSession; /* @@ -2562,6 +2513,7 @@ WinMain(HINSTANCE instance, while ( TRUE ) { RwInitialised = TRUE; + FramePacer.Reset(); /* * Set the initial mouse position... @@ -2766,7 +2718,8 @@ WinMain(HINSTANCE instance, case GS_FRONTEND: { - if (!IsFrameReady(60.0f)) + if (!IsFrameReady(RsFramePacer::GetTargetFPS(FrontEndMenuManager.m_PrefsFrameLimiter, + RsGlobal.maxFPS, true))) break; GetWindowPlacement(PSGLOBAL(window), &wp); @@ -2843,16 +2796,10 @@ WinMain(HINSTANCE instance, { if ( RwInitialised ) { - if (FrontEndMenuManager.m_bMenuActive || CGame::IsWindowPauseMenuActive()) { - float maxFPS = FrontEndMenuManager.m_PrefsFrameLimiter ? (float)RsGlobal.maxFPS : 0.0f; - maxFPS = maxFPS > 0.0f ? Min(maxFPS, 60.0f) : 60.0f; - if (IsFrameReady(maxFPS)) - RsEventHandler(rsIDLE, (void *)TRUE); - } else { - float ms = (float)CTimer::GetCurrentTimeInCycles() / (float)CTimer::GetCyclesPerMillisecond(); - if (!FrontEndMenuManager.m_PrefsFrameLimiter || (1000.0f / (float)RsGlobal.maxFPS) < ms) - RsEventHandler(rsIDLE, (void *)TRUE); - } + const int targetFPS = RsFramePacer::GetTargetFPS(FrontEndMenuManager.m_PrefsFrameLimiter, + RsGlobal.maxFPS, FrontEndMenuManager.m_bMenuActive || CGame::IsWindowPauseMenuActive()); + if (IsFrameReady(targetFPS)) + RsEventHandler(rsIDLE, (void *)TRUE); } break; } @@ -2987,11 +2934,6 @@ WinMain(HINSTANCE instance, ShowCursor(TRUE); - SystemParametersInfo(SPI_SETSTICKYKEYS, sizeof(STICKYKEYS), &SavedStickyKeys, SPIF_SENDCHANGE); - SystemParametersInfo(SPI_SETPOWEROFFACTIVE, TRUE, nil, SPIF_SENDCHANGE); - SystemParametersInfo(SPI_SETLOWPOWERACTIVE, TRUE, nil, SPIF_SENDCHANGE); - SystemParametersInfo(SPI_SETSCREENSAVEACTIVE, TRUE, nil, SPIF_SENDCHANGE); - SetErrorMode(0); return message.wParam; diff --git a/src/skel/win/windowssession.h b/src/skel/win/windowssession.h new file mode 100644 index 0000000..50c86a2 --- /dev/null +++ b/src/skel/win/windowssession.h @@ -0,0 +1,60 @@ +#ifndef RS_WINDOWSSESSION_H +#define RS_WINDOWSSESSION_H + +#ifdef _WIN32 +#include + +// Keep game-only requests scoped to this main thread. Never alter the user's +// foreground-lock timeout, screensaver or power policy, or broadcast settings +// changes to every other application during startup/shutdown. +class RsWindowsSession +{ + EXECUTION_STATE m_previousExecutionState; + DWORD m_savedStickyHotkeys; + bool m_stickyKeysChanged; + +public: + RsWindowsSession() + : m_previousExecutionState(SetThreadExecutionState(ES_CONTINUOUS | ES_DISPLAY_REQUIRED)), + m_savedStickyHotkeys(0), m_stickyKeysChanged(false) + { + STICKYKEYS sticky = {}; + sticky.cbSize = sizeof(sticky); + if(!SystemParametersInfo(SPI_GETSTICKYKEYS, sizeof(sticky), &sticky, 0) || + (sticky.dwFlags & SKF_STICKYKEYSON)) + return; + + const DWORD hotkeyFlags = SKF_HOTKEYACTIVE | SKF_CONFIRMHOTKEY; + m_savedStickyHotkeys = sticky.dwFlags & hotkeyFlags; + if(m_savedStickyHotkeys == 0) return; + + // Suppress only the five-Shift activation prompt, never an enabled + // accessibility feature or any of its other user preferences. + sticky.dwFlags &= ~hotkeyFlags; + m_stickyKeysChanged = SystemParametersInfo(SPI_SETSTICKYKEYS, sizeof(sticky), &sticky, 0) != FALSE; + } + + ~RsWindowsSession() + { + if(m_previousExecutionState) + SetThreadExecutionState(m_previousExecutionState | ES_CONTINUOUS); + if(!m_stickyKeysChanged) return; + + STICKYKEYS sticky = {}; + sticky.cbSize = sizeof(sticky); + if(!SystemParametersInfo(SPI_GETSTICKYKEYS, sizeof(sticky), &sticky, 0)) return; + + const DWORD hotkeyFlags = SKF_HOTKEYACTIVE | SKF_CONFIRMHOTKEY; + // If the user enabled StickyKeys or changed either managed hotkey bit + // while playing, their current choice takes precedence over our snapshot. + if(sticky.dwFlags & (SKF_STICKYKEYSON | hotkeyFlags)) return; + sticky.dwFlags |= m_savedStickyHotkeys; + SystemParametersInfo(SPI_SETSTICKYKEYS, sizeof(sticky), &sticky, 0); + } + + RsWindowsSession(const RsWindowsSession&) = delete; + RsWindowsSession& operator=(const RsWindowsSession&) = delete; +}; +#endif + +#endif diff --git a/vendor/librw/src/wgpu/rwwgpu.h b/vendor/librw/src/wgpu/rwwgpu.h index ca0894e..670cfe8 100644 --- a/vendor/librw/src/wgpu/rwwgpu.h +++ b/vendor/librw/src/wgpu/rwwgpu.h @@ -36,6 +36,12 @@ namespace rw int getBackend(void); // the requested preference const char* getBackendName(void); // the backend actually in use (after open) +#ifdef _WIN32 + // Nonblocking backpressure for the Win32 UI/render thread. Recheck + // window messages while GPU work is pending instead of piling up frames. + bool pollFrameReady(void); +#endif + void registerPlatformPlugins(void); void initMatFX(void); void initSkin(void); diff --git a/vendor/librw/src/wgpu/wgpudevice.cpp b/vendor/librw/src/wgpu/wgpudevice.cpp index 4d1eee8..57f371a 100644 --- a/vendor/librw/src/wgpu/wgpudevice.cpp +++ b/vendor/librw/src/wgpu/wgpudevice.cpp @@ -38,6 +38,17 @@ namespace rw FrameState s_frame = {}; RenderState s_renderState = {}; +#ifdef _WIN32 + bool pollFrameReady(void) + { + if (!wgpuGlobals.device) return true; + const bool idle = wgpuDevicePoll(wgpuGlobals.device, false, nullptr) != 0; + // Offscreen passes can interrupt one display frame. Let its existing + // encoder finish; only throttle the start of a new display frame. + return s_frame.inFrame || idle; + } +#endif + // The acquired swapchain texture, held across mid-frame submits. A frame can be // interrupted by offscreen render-to-texture passes (e.g. the neo env map): // the main frame's commands are submitted so the offscreen target can take @@ -2257,6 +2268,20 @@ struct VSOut { static int termWGPU(void) { + // Closing can interrupt a display frame. Do not submit more GPU work + // while shutting down, and release encoder-held texture references + // before destroying the pipelines and persistent attachments. + if (s_frame.inRenderPass && s_frame.renderPass) + wgpuRenderPassEncoderEnd(s_frame.renderPass); + if (s_frame.renderPass) wgpuRenderPassEncoderRelease(s_frame.renderPass); + if (s_frame.encoder) wgpuCommandEncoderRelease(s_frame.encoder); + if (s_frame.isOffscreen) + { + if (s_frame.depthView) wgpuTextureViewRelease(s_frame.depthView); + if (s_frame.depthTexture) wgpuTextureRelease(s_frame.depthTexture); + } + s_frame = {}; + closeAtomic(); closeIm3D(); closeIm2DText(); @@ -2309,8 +2334,8 @@ struct VSOut { s_backbuffer.resolveTexture = nullptr; } s_backbuffer = {}; - if (s_swap.texture) { wgpuTextureRelease(s_swap.texture); } if (s_swap.view) { wgpuTextureViewRelease(s_swap.view); } + if (s_swap.texture) { wgpuTextureRelease(s_swap.texture); } s_swap.view = nullptr; s_swap.texture = nullptr; s_swap.acquired = false; diff --git a/vendor/librw/src/wgpu/wgpuraster.cpp b/vendor/librw/src/wgpu/wgpuraster.cpp index bb05322..ac6fe94 100644 --- a/vendor/librw/src/wgpu/wgpuraster.cpp +++ b/vendor/librw/src/wgpu/wgpuraster.cpp @@ -15,6 +15,7 @@ #include #include +#include #include "wgpu_internal.h" @@ -338,30 +339,55 @@ namespace rw struct UD { std::atomic done{false}; - } ud; + std::atomic references{2}; // caller and map callback + WGPUMapAsyncStatus status = WGPUMapAsyncStatus_Error; + + void release() + { + if (references.fetch_sub(1, std::memory_order_acq_rel) == 1) + delete this; + } + }; + // Map callbacks can run after a timeout (including cancellation). + // Their userdata must outlive this stack frame until they return. + UD* ud = new UD; auto cb = [](WGPUMapAsyncStatus s, WGPUStringView msg, void* u1, void* u2) { - (void)s; (void)msg; - ((UD*)u1)->done.store(true, std::memory_order_release); + (void)u2; + UD* state = (UD*)u1; + state->status = s; + state->done.store(true, std::memory_order_release); + state->release(); }; WGPUBufferMapCallbackInfo ci = {}; ci.mode = WGPUCallbackMode_AllowSpontaneous; ci.callback = cb; - ci.userdata1 = &ud; + ci.userdata1 = ud; wgpuBufferMapAsync(buf, WGPUMapMode_Read, 0, size, ci); auto t0 = std::chrono::steady_clock::now(); - while (!ud.done.load(std::memory_order_acquire)) + while (!ud->done.load(std::memory_order_acquire)) { + // Native backends need device polling to retire submissions and + // deliver map callbacks. Poll without a blocking device wait. + wgpuDevicePoll(wgpuGlobals.device, false, nullptr); wgpuInstanceProcessEvents(wgpuGlobals.instance); + if (ud->done.load(std::memory_order_acquire)) break; if (std::chrono::steady_clock::now() - t0 > std::chrono::seconds(15)) { fprintf(stderr, "[wgpu] mapBufferSync: no answer after 15s (driver hang?) — giving up\n"); + wgpuBufferUnmap(buf); // cancel a map that has not completed + ud->release(); return nullptr; } + // A stalled driver must not spin a CPU core for the whole timeout. + std::this_thread::sleep_for(std::chrono::milliseconds(1)); } - const WGPUBufferMapState mapState = wgpuBufferGetMapState(buf); - if (mapState != WGPUBufferMapState_Mapped) return nullptr; + const bool succeeded = ud->status == WGPUMapAsyncStatus_Success; + ud->release(); + if (!succeeded) return nullptr; + // Success already guarantees mapped storage. Some supported native + // releases do not implement the optional map-state query. return (uint8_t*)wgpuBufferGetMappedRange(buf, 0, size); } @@ -769,6 +795,14 @@ namespace rw wgpuBufferRelease(staging); return nullptr; } + img->allocate(); + if (!img->pixels) + { + img->destroy(); + wgpuBufferUnmap(staging); + wgpuBufferRelease(staging); + return nullptr; + } // Repack the padded rows, converting from RGBA8 (BGRA for camera // textures on a BGRA surface — P2-12) into the target depth.