diff --git a/Core/GameEngine/Include/Common/FramePacer.h b/Core/GameEngine/Include/Common/FramePacer.h index c8681cab7a7..bbd9cc0f6b1 100644 --- a/Core/GameEngine/Include/Common/FramePacer.h +++ b/Core/GameEngine/Include/Common/FramePacer.h @@ -47,6 +47,9 @@ class FramePacer Bool isFramesPerSecondLimitEnabled() const; ///< Returns whether the fps limit is enabled here. Bool isActualFramesPerSecondLimitEnabled() const; ///< Returns whether the fps limit is actually enabled when considering all game settings and setups. Int getActualFramesPerSecondLimit() const; // Get the actual update fps limit. +#if defined(GENERALS_ONLINE) + void invalidateWindowRefreshRate(); ///< The window moved or the display changed, query the monitor refresh rate again soon. +#endif Real getUpdateTime() const; ///< Get the last update delta time in seconds. Real getUpdateFps() const; ///< Get the last update fps. @@ -61,6 +64,11 @@ class FramePacer Int getLogicTimeScaleFps() const; ///< Get the raw logic time scale fps value. void enableLogicTimeScale( Bool enable ); ///< Enable or disable the logic time scale setup. If disabled, the simulation time scale is bound to the render frame time or network update time. Bool isLogicTimeScaleEnabled() const; ///< Check whether the logic time scale setup is enabled. +#if defined(GENERALS_ONLINE) + void setGameSpeedFps( Int fps ); ///< Set the offline game speed in logic frames per second (60 is normal, 1000 and more is unlimited). Scales the logic time scale, the render limit is untouched. + Int getGameSpeedFps() const; ///< Get the offline game speed in logic frames per second, 1000 for unlimited. + void resetGameSpeed(); ///< Back to the normal game speed. +#endif Int getActualLogicTimeScaleFps(LogicTimeQueryFlags flags = 0) const; ///< Get the real logic time scale fps, depending on the max render fps, network state and enabled state. Real getActualLogicTimeScaleRatio(LogicTimeQueryFlags flags = 0) const; ///< Get the real logic time scale ratio, depending on the max render fps, network state and enabled state. Real getActualLogicTimeScaleOverFpsRatio(LogicTimeQueryFlags flags = 0) const; ///< Get the real logic time scale over render fps ratio, used to scale down steps in render updates to match logic updates. @@ -71,6 +79,15 @@ class FramePacer FrameRateLimit m_frameRateLimit; +#if defined(GENERALS_ONLINE) + Int getWindowCappedFramesPerSecondLimit(Int fps) const; ///< Cap uncapped windowed rendering at the monitor refresh rate and rendering without focus at the logic rate. + UnsignedInt getWindowRefreshRate() const; + + mutable UnsignedInt m_windowRefreshRate; + mutable DWORD m_windowRefreshTime; + mutable Bool m_windowRefreshDirty; +#endif + Int m_maxFPS; ///< Maximum frames per second for rendering Int m_logicTimeScaleFPS; ///< Maximum frames per second for logic time scale diff --git a/Core/GameEngine/Include/Common/FrameRateLimit.h b/Core/GameEngine/Include/Common/FrameRateLimit.h index a729313d005..c9f9949f782 100644 --- a/Core/GameEngine/Include/Common/FrameRateLimit.h +++ b/Core/GameEngine/Include/Common/FrameRateLimit.h @@ -32,6 +32,8 @@ class FrameRateLimit private: Int64 m_freq; Int64 m_start; + Int64 m_lastActual; + UnsignedInt m_previousFps; }; diff --git a/Core/GameEngine/Include/Common/OptionPreferences.h b/Core/GameEngine/Include/Common/OptionPreferences.h index 63108897941..80b4fd8408c 100644 --- a/Core/GameEngine/Include/Common/OptionPreferences.h +++ b/Core/GameEngine/Include/Common/OptionPreferences.h @@ -114,6 +114,7 @@ class OptionPreferences : public UserPreferences Bool getExtraAnimationsDisabled(); Bool getUseHeatEffects(); Bool getDynamicLODEnabled(); + UnsignedInt getRenderFpsLimit(); Bool getFPSLimitEnabled(); Bool getBuildingOcclusionEnabled(); Int getParticleCap(); diff --git a/Core/GameEngine/Source/Common/FramePacer.cpp b/Core/GameEngine/Source/Common/FramePacer.cpp index d764c9c1414..ecb73f415d8 100644 --- a/Core/GameEngine/Source/Common/FramePacer.cpp +++ b/Core/GameEngine/Source/Common/FramePacer.cpp @@ -21,12 +21,19 @@ #include "GameClient/View.h" +#include "Common/GameEngine.h" + #include "GameLogic/GameLogic.h" #include "GameLogic/ScriptEngine.h" #include "GameNetwork/NetworkDefs.h" #include "GameNetwork/NetworkInterface.h" +#if defined(GENERALS_ONLINE) +#include "WW3D2/dx8wrapper.h" +#include "WW3D2/ww3d.h" +#endif + FramePacer* TheFramePacer = nullptr; @@ -42,6 +49,11 @@ FramePacer::FramePacer() m_enableLogicTimeScale = FALSE; m_isTimeFrozen = FALSE; m_isGameHalted = FALSE; +#if defined(GENERALS_ONLINE) + m_windowRefreshRate = 60; + m_windowRefreshTime = 0; + m_windowRefreshDirty = TRUE; +#endif } FramePacer::~FramePacer() @@ -117,9 +129,58 @@ Bool FramePacer::isActualFramesPerSecondLimitEnabled() const Int FramePacer::getActualFramesPerSecondLimit() const { - return isActualFramesPerSecondLimitEnabled() ? getFramesPerSecondLimit() : RenderFpsPreset::UncappedFpsValue; + const Int fps = isActualFramesPerSecondLimitEnabled() ? getFramesPerSecondLimit() : RenderFpsPreset::UncappedFpsValue; +#if defined(GENERALS_ONLINE) + return getWindowCappedFramesPerSecondLimit(fps); +#else + return fps; +#endif } +#if defined(GENERALS_ONLINE) +void FramePacer::invalidateWindowRefreshRate() +{ + m_windowRefreshDirty = TRUE; +} + +UnsignedInt FramePacer::getWindowRefreshRate() const +{ + // Re-query at most every 100 ms after a move or display change, and every second otherwise. + const DWORD now = timeGetTime(); + const DWORD age = now - m_windowRefreshTime; + if (m_windowRefreshTime == 0 || age >= 1000 || (m_windowRefreshDirty && age >= 100)) + { + m_windowRefreshRate = DX8Wrapper::Get_Window_Monitor_Refresh_Rate(); + m_windowRefreshTime = now; + m_windowRefreshDirty = FALSE; + } + return m_windowRefreshRate; +} + +Int FramePacer::getWindowCappedFramesPerSecondLimit(Int fps) const +{ + // Presenting a windowed device faster than the monitor refreshes only floods the GPU and the desktop compositor. + // Explicit limits stay as chosen, only the uncapped limit maps to the refresh rate. Exclusive fullscreen stays uncapped. + if (fps >= (Int)RenderFpsPreset::UncappedFpsValue && WW3D::Is_Windowed()) + { + fps = (Int)getWindowRefreshRate(); + } + + // Without focus nobody watches the match at a high frame rate. + if (TheGameEngine != nullptr && !TheGameEngine->isActive()) + { + fps = std::min(fps, GENERALS_ONLINE_HIGH_FPS_LIMIT); + } + + // Logic is capped by the render frame rate, so never render slower than the logic time scale or the network rate. + if (TheNetwork != nullptr || isLogicTimeScaleEnabled()) + { + fps = std::max(fps, getActualLogicTimeScaleFps(IgnoreFrozenTime | IgnoreHaltedGame)); + } + return fps; +} +#endif + Real FramePacer::getUpdateTime() const { return m_updateTime; @@ -177,6 +238,26 @@ Bool FramePacer::isLogicTimeScaleEnabled() const return m_enableLogicTimeScale; } +#if defined(GENERALS_ONLINE) +void FramePacer::setGameSpeedFps( Int fps ) +{ + const Int logicFps = fps >= 1000 ? (Int)RenderFpsPreset::UncappedFpsValue : std::max(fps, 1); + m_enableLogicTimeScale = TRUE; + m_logicTimeScaleFPS = logicFps; +} + +Int FramePacer::getGameSpeedFps() const +{ + return m_logicTimeScaleFPS >= (Int)RenderFpsPreset::UncappedFpsValue ? 1000 : m_logicTimeScaleFPS; +} + +void FramePacer::resetGameSpeed() +{ + m_enableLogicTimeScale = TRUE; + m_logicTimeScaleFPS = LOGICFRAMES_PER_SECOND; +} +#endif + Int FramePacer::getActualLogicTimeScaleFps(LogicTimeQueryFlags flags) const { if (m_isTimeFrozen && (flags & IgnoreFrozenTime) == 0) diff --git a/Core/GameEngine/Source/Common/FrameRateLimit.cpp b/Core/GameEngine/Source/Common/FrameRateLimit.cpp index a7c6776b1c2..d09cd6adf98 100644 --- a/Core/GameEngine/Source/Common/FrameRateLimit.cpp +++ b/Core/GameEngine/Source/Common/FrameRateLimit.cpp @@ -28,6 +28,8 @@ FrameRateLimit::FrameRateLimit() QueryPerformanceCounter(&start); m_freq = freq.QuadPart; m_start = start.QuadPart; + m_lastActual = m_start; + m_previousFps = 0; } Real FrameRateLimit::wait(UnsignedInt maxFps) @@ -35,6 +37,11 @@ Real FrameRateLimit::wait(UnsignedInt maxFps) PROFILER_SECTION; LARGE_INTEGER tick; QueryPerformanceCounter(&tick); + if (m_previousFps != maxFps) + { + m_start = m_lastActual; + } + m_previousFps = maxFps; double elapsedSeconds = static_cast(tick.QuadPart - m_start) / m_freq; const double targetSeconds = 1.0 / maxFps; const double sleepSeconds = targetSeconds - elapsedSeconds - 0.002; // leave ~2ms for spin wait @@ -46,6 +53,14 @@ Real FrameRateLimit::wait(UnsignedInt maxFps) Sleep(dwMilliseconds); } +#if defined(GENERALS_ONLINE) + else + { + // Give the rest of the time slice to other threads and processes once per frame + Sleep(0); + } +#endif + // Busy wait for remaining time do { @@ -54,8 +69,16 @@ Real FrameRateLimit::wait(UnsignedInt maxFps) } while (elapsedSeconds < targetSeconds); - m_start = tick.QuadPart; - return (Real)elapsedSeconds; + const Int64 period = static_cast(static_cast(m_freq) / maxFps + 0.5); + const Int64 deadline = m_start + period; + const double actualSeconds = static_cast(tick.QuadPart - m_lastActual) / m_freq; + m_lastActual = tick.QuadPart; + // Preserve small overshoot, but discard long pauses instead of banking catch-up. + if (tick.QuadPart - deadline < period) + m_start = deadline; + else + m_start = tick.QuadPart; + return static_cast(actualSeconds); } void FrameRateLimit::reset() @@ -63,6 +86,8 @@ void FrameRateLimit::reset() LARGE_INTEGER tick; QueryPerformanceCounter(&tick); m_start = tick.QuadPart; + m_lastActual = m_start; + m_previousFps = 0; } diff --git a/Core/GameEngine/Source/Common/OptionPreferences.cpp b/Core/GameEngine/Source/Common/OptionPreferences.cpp index 4a48ae6b84e..7509311d8da 100644 --- a/Core/GameEngine/Source/Common/OptionPreferences.cpp +++ b/Core/GameEngine/Source/Common/OptionPreferences.cpp @@ -744,6 +744,21 @@ Bool OptionPreferences::getDynamicLODEnabled() return FALSE; } +UnsignedInt OptionPreferences::getRenderFpsLimit() +{ + OptionPreferences::const_iterator it = find("RenderFpsLimit"); + if (it == end()) + return TheGlobalData->m_framesPerSecondLimit; + + const Int fps = atoi(it->second.str()); +#if defined(GENERALS_ONLINE) + // the logic rate is capped by the render rate, so a lower limit would slow the game down + return fps > 0 ? (UnsignedInt)std::max(fps, LOGICFRAMES_PER_SECOND) : TheGlobalData->m_framesPerSecondLimit; +#else + return fps > 0 ? (UnsignedInt)fps : TheGlobalData->m_framesPerSecondLimit; +#endif +} + Bool OptionPreferences::getFPSLimitEnabled() { OptionPreferences::const_iterator it = find("FPSLimit"); diff --git a/Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index 231a6f355b4..c47956d3d42 100644 --- a/Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -208,11 +208,11 @@ static Int applyObserverFontSizeChange(Int fontSize, ObserverFontSizeChange chan return fontSize; } -static void writeObserverFontSizePref(const char* prefKey, Int fontSize) +static void writeIntPref(const char* prefKey, Int value) { OptionPreferences optPref; AsciiString prefString; - prefString.format("%d", fontSize); + prefString.format("%d", value); optPref[prefKey] = prefString; optPref.write(); } @@ -229,7 +229,7 @@ static bool changeObserverNotificationFontSize(ObserverFontSizeChange change) if (TheInGameUI) TheInGameUI->refreshObserverNotificationResources(); - writeObserverFontSizePref("ObserverNotificationFontSize", fontSize); + writeIntPref("ObserverNotificationFontSize", fontSize); return true; } @@ -246,7 +246,7 @@ static bool changeObserverStatsFontSize(ObserverFontSizeChange change) if (TheInGameUI) TheInGameUI->initObserverOverlay(); - writeObserverFontSizePref("ObserverStatsFontSize", fontSize); + writeIntPref("ObserverStatsFontSize", fontSize); return true; } @@ -260,9 +260,9 @@ bool changeMaxRenderFps(FpsValueChange change) TheWritableGlobalData->m_useFpsLimit = (maxRenderFps != RenderFpsPreset::UncappedFpsValue); #if defined(GENERALS_ONLINE) - // Save to GO settings, SH does not save it yet - // TODO_NGMP: Remove this, SH saves it now - NGMP_OnlineServicesManager::Settings.Graphics_SetFPS(maxRenderFps, TheWritableGlobalData->m_useFpsLimit); + // the limit of the next online match, kept in Options.ini + TheWritableGlobalData->m_framesPerSecondLimit = maxRenderFps; + writeIntPref("RenderFpsLimit", maxRenderFps); #endif UnicodeString message; @@ -311,7 +311,12 @@ bool changeLogicTimeScale(FpsValueChange change) TheFramePacer->setLogicTimeScaleFps(logicTimeScaleFps); } +#if defined(GENERALS_ONLINE) + // the logic rate is capped by the render rate, so it stays enabled when it is not below it + TheFramePacer->enableLogicTimeScale(TRUE); +#else TheFramePacer->enableLogicTimeScale(logicTimeScaleFps < maxRenderFps); +#endif // Set value after potentially enabling it. if (TheFramePacer->isLogicTimeScaleEnabled()) diff --git a/Core/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp b/Core/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp index c9c7cbb7602..6d7e0f7b932 100644 --- a/Core/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp +++ b/Core/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp @@ -880,11 +880,19 @@ bool GameLogic::onNewGame(MAYBE_UNUSED GameMessage *msg) if ( msg->getArgumentCount() >= 4 ) { Int maxFPS = msg->getArgument( 3 )->integer; +#if defined(GENERALS_ONLINE) + // the game speed of the skirmish slider (logic frames per second, 60 is normal) sets the logic rate, the render limit is the one of Options.ini + if (maxFPS < 1 || maxFPS > 1000) + maxFPS = LOGICFRAMES_PER_SECOND; + DEBUG_LOG(("Setting game speed to %d FPS", maxFPS)); + TheFramePacer->setGameSpeedFps(maxFPS); +#else if (maxFPS < 1 || maxFPS > 1000) maxFPS = TheGlobalData->m_framesPerSecondLimit; DEBUG_LOG(("Setting max FPS limit to %d FPS", maxFPS)); TheFramePacer->setFramesPerSecondLimit(maxFPS); TheWritableGlobalData->m_useFpsLimit = true; +#endif } // prepare for new game diff --git a/Core/Libraries/Source/WWVegas/WW3D2/dx8wrapper.cpp b/Core/Libraries/Source/WWVegas/WW3D2/dx8wrapper.cpp index c406c783654..ba4ca12e1b7 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/dx8wrapper.cpp +++ b/Core/Libraries/Source/WWVegas/WW3D2/dx8wrapper.cpp @@ -1316,6 +1316,29 @@ bool DX8Wrapper::Set_Device_Resolution(int width, int height, int bits, int wind } } +#if defined(GENERALS_ONLINE) +unsigned DX8Wrapper::Get_Window_Monitor_Refresh_Rate() +{ + unsigned hz = 0; + MONITORINFOEX info; + memset(&info, 0, sizeof(info)); + info.cbSize = sizeof(info); + if (_Hwnd != nullptr && GetMonitorInfo(MonitorFromWindow(_Hwnd, MONITOR_DEFAULTTONEAREST), &info)) + { + DEVMODE mode; + memset(&mode, 0, sizeof(mode)); + mode.dmSize = sizeof(mode); + if (EnumDisplaySettings(info.szDevice, ENUM_CURRENT_SETTINGS, &mode)) + { + hz = mode.dmDisplayFrequency; + } + } + + // 0 and 1 mean the hardware default + return hz > 1 ? hz : 60; +} +#endif + void DX8Wrapper::Get_Device_Resolution(int& set_w, int& set_h, int& set_bits, bool& set_windowed) { WWASSERT(IsInitted); diff --git a/Core/Libraries/Source/WWVegas/WW3D2/dx8wrapper.h b/Core/Libraries/Source/WWVegas/WW3D2/dx8wrapper.h index 8a6f3d26495..1bc71b45a89 100644 --- a/Core/Libraries/Source/WWVegas/WW3D2/dx8wrapper.h +++ b/Core/Libraries/Source/WWVegas/WW3D2/dx8wrapper.h @@ -568,6 +568,10 @@ class DX8Wrapper static void Set_Draw_Polygon_Low_Bound_Limit(unsigned n) { DrawPolygonLowBoundLimit=n; } +#if defined(GENERALS_ONLINE) + static unsigned Get_Window_Monitor_Refresh_Rate(); ///< Refresh rate in Hz of the monitor the device window is on, 60 if unknown. +#endif + protected: static bool Create_Device(); diff --git a/Generals/Code/GameEngine/Source/Common/GameMain.cpp b/Generals/Code/GameEngine/Source/Common/GameMain.cpp index c9dd0a94427..2e283311879 100644 --- a/Generals/Code/GameEngine/Source/Common/GameMain.cpp +++ b/Generals/Code/GameEngine/Source/Common/GameMain.cpp @@ -42,6 +42,11 @@ Int GameMain() // initialize the game engine using factory function TheFramePacer = new FramePacer(); TheFramePacer->enableFramesPerSecondLimit(TRUE); +#if defined(GENERALS_ONLINE) + // Decouple the simulation speed from the render frame rate. Without this, raising the render FPS limit also speeds up the game logic. + // Network games are unaffected, they use TheNetwork->getFrameRate(). + TheFramePacer->enableLogicTimeScale(TRUE); +#endif TheGameEngine = CreateGameEngine(); TheGameEngine->init(); diff --git a/Generals/Code/Main/WinMain.cpp b/Generals/Code/Main/WinMain.cpp index c8e9bb9961d..e1a75f3f653 100644 --- a/Generals/Code/Main/WinMain.cpp +++ b/Generals/Code/Main/WinMain.cpp @@ -58,6 +58,7 @@ #include "GameClient/GameClient.h" #include "GameLogic/GameLogic.h" ///< @todo for demo, remove #include "GameClient/Mouse.h" +#include "Common/FramePacer.h" #include "GameClient/IMEManager.h" #include "Win32Device/GameClient/Win32Mouse.h" #include "Win32Device/Common/Win32GameEngine.h" @@ -405,9 +406,27 @@ LRESULT CALLBACK WndProc( HWND hWnd, UINT message, if (TheMouse) TheMouse->refreshCursorCapture(); +#if defined(GENERALS_ONLINE) + // the window may be on another monitor now + if (TheFramePacer) + TheFramePacer->invalidateWindowRefreshRate(); +#endif + break; } +#if defined(GENERALS_ONLINE) + //------------------------------------------------------------------------- + case WM_DISPLAYCHANGE: + case WM_EXITSIZEMOVE: + { + if (TheFramePacer) + TheFramePacer->invalidateWindowRefreshRate(); + + break; + } +#endif + //------------------------------------------------------------------------- case WM_SIZE: { diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.h index 0bcc1a2c3e3..855624f1945 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.h @@ -21,13 +21,6 @@ class GenOnlineSettings float DetermineCameraMaxHeight(); - void Graphics_SetFPS(int fpsLimit, bool bLimitFramerate) - { - m_Render_FramerateLimit_FPSVal = fpsLimit; - m_Render_LimitFramerate = bLimitFramerate; - Save(); - } - void Save_Camera_MaxHeight_WhenLobbyHost(float maxHeight) { if (maxHeight >= GENERALS_ONLINE_MIN_LOBBY_CAMERA_ZOOM && maxHeight <= GENERALS_ONLINE_MAX_LOBBY_CAMERA_ZOOM) @@ -38,16 +31,6 @@ class GenOnlineSettings } bool Graphics_DrawStatsOverlay() const { return m_Render_DrawStatsOverlay; } - bool Graphics_LimitFramerate() const { return m_Render_LimitFramerate; } - int Graphics_GetFPSLimit() const - { - if (!m_Render_LimitFramerate) - { - return 1000000; - } - - return m_Render_FramerateLimit_FPSVal; - } std::string GetAnticheatPlugin() const; bool Social_Notifications_FriendComesOnline_Menus() { return m_Social_Notification_FriendComesOnline_Menus; } @@ -126,8 +109,6 @@ class GenOnlineSettings bool m_bVerbose = false; bool m_Render_DrawStatsOverlay = true; - bool m_Render_LimitFramerate = true; - int m_Render_FramerateLimit_FPSVal = 60; int m_Chat_LifeSeconds = 30; bool m_Social_Notification_FriendComesOnline_Menus = true; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp index 7a43b85c8ca..744dc814916 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/CommandLine.cpp @@ -28,6 +28,7 @@ #include "Common/ArchiveFileSystem.h" #include "Common/CommandLine.h" #include "Common/CRCDebug.h" +#include "Common/FrameRateLimit.h" #include "Common/LocalFileSystem.h" #include "Common/Recorder.h" #include "Common/version.h" @@ -1022,7 +1023,11 @@ Int parseIgnoreStackTrace(char *args[], int num) Int parseNoFPSLimit(char *args[], int num) { TheWritableGlobalData->m_useFpsLimit = false; +#if defined(GENERALS_ONLINE) + TheWritableGlobalData->m_framesPerSecondLimit = RenderFpsPreset::UncappedFpsValue; +#else TheWritableGlobalData->m_framesPerSecondLimit = 30000; +#endif return 1; } diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp index 0c497745de0..b779d67cbc8 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp @@ -978,15 +978,18 @@ void GameEngine::update() VERIFY_CRC #if defined(GENERALS_ONLINE_HIGH_FPS_RENDER) - // NGMP_NOTE: Lock the shellmap to 30fps until we fix everything - if (TheNGMPGame != nullptr && TheGameLogic->isInGame() && !TheShell->isShellActive()) + if (TheGameLogic->isInGame() && !TheShell->isShellActive()) { - TheFramePacer->setFramesPerSecondLimit(NGMP_OnlineServicesManager::Settings.Graphics_GetFPSLimit()); - TheWritableGlobalData->m_useFpsLimit = NGMP_OnlineServicesManager::Settings.Graphics_GetFPSLimit(); + // every game (online, LAN, skirmish, campaign, replay) renders at the limit of Options.ini, the logic rate is separate: + // the network frame rate, or the logic time scale offline + TheFramePacer->setFramesPerSecondLimit(TheGlobalData->m_framesPerSecondLimit); + TheWritableGlobalData->m_useFpsLimit = TheGlobalData->m_framesPerSecondLimit < (Int)RenderFpsPreset::UncappedFpsValue; } else { + // menus and the shellmap stay at the logic rate TheFramePacer->setFramesPerSecondLimit(GENERALS_ONLINE_HIGH_FPS_LIMIT); + TheWritableGlobalData->m_useFpsLimit = true; } #endif diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp index b9a92962e05..3416467035a 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameMain.cpp @@ -42,6 +42,11 @@ Int GameMain() // initialize the game engine using factory function TheFramePacer = new FramePacer(); TheFramePacer->enableFramesPerSecondLimit(TRUE); +#if defined(GENERALS_ONLINE) + // Decouple the simulation speed from the render frame rate. Without this, raising the render FPS limit also speeds up the game logic. + // Network games are unaffected, they use TheNetwork->getFrameRate(). + TheFramePacer->enableLogicTimeScale(TRUE); +#endif TheGameEngine = CreateGameEngine(); TheGameEngine->init(); diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index 04063cf38f6..d9651abe166 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -1305,6 +1305,7 @@ void GlobalData::parseGameDataDefinition( INI* ini ) TheWritableGlobalData->m_doubleClickAttackMove = optionPref.getDoubleClickAttackMoveEnabled(); TheWritableGlobalData->m_jpegQuality = optionPref.getJpegQuality(); TheWritableGlobalData->m_keyboardScrollFactor = optionPref.getScrollFactor(); + TheWritableGlobalData->m_framesPerSecondLimit = optionPref.getRenderFpsLimit(); TheWritableGlobalData->m_defaultIP = optionPref.getLANIPAddress(); TheWritableGlobalData->m_firewallSendDelay = optionPref.getSendDelay(); TheWritableGlobalData->m_firewallBehavior = optionPref.getFirewallBehavior(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/QuitMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/QuitMenu.cpp index 26f4a684b8b..b4933e788e5 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/QuitMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/QuitMenu.cpp @@ -196,7 +196,7 @@ static void restartMissionMenu() Int rankPointsStartedWith = TheGameLogic->getRankPointsToAddAtGameStart();// must write down before reset GameDifficulty diff = TheScriptEngine->getGlobalDifficulty(); - Int fps = TheFramePacer->getFramesPerSecondLimit(); + Int fps = TheFramePacer->getGameSpeedFps(); TheGameLogic->clearGameData(FALSE); TheGameEngine->setQuitting(FALSE); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/SkirmishGameOptionsMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/SkirmishGameOptionsMenu.cpp index d071904490d..1920fbaa01c 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/SkirmishGameOptionsMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/SkirmishGameOptionsMenu.cpp @@ -150,7 +150,8 @@ static Bool buttonPushed = FALSE; static Bool stillNeedsToSetOptions = FALSE; void skirmishUpdateSlotList(); static void populateSkirmishBattleHonors(); -enum{ GREATER_NO_FPS_LIMIT = 60}; +// the slider is in logic frames per second (60 is normal, 30 to 120, one more is unlimited); the saved "FPS" pref stays in retail units, half of that +enum{ GREATER_NO_FPS_LIMIT = 120, GAME_SPEED_MIN = 30, GAME_SPEED_UNLIMITED = 121 }; Bool doUpdateSlotList = TRUE; static Int getNextSelectablePlayer(Int start) @@ -365,7 +366,7 @@ Bool SkirmishPreferences::write() // NameKeyType sliderGameSpeedID = TheNameKeyGenerator->nameToKey( "SkirmishGameOptionsMenu.wnd:SliderGameSpeed" ); GameWindow *sliderGameSpeed = TheWindowManager->winGetWindowFromId( parentSkirmishGameOptions, sliderGameSpeedID ); Int maxFPS = GadgetSliderGetPosition( sliderGameSpeed ); - setInt("FPS", maxFPS); + setInt("FPS", maxFPS > GREATER_NO_FPS_LIMIT ? 61 : (maxFPS + 1) / 2); return UserPreferences::write(); } @@ -407,7 +408,7 @@ void setFPSTextBox( Int sliderPos ) GadgetStaticTextSetText(staticTextGameSpeed, text); return; } - else if( sliderPos == TheGlobalData->m_framesPerSecondLimit ) + else if( sliderPos == LOGICFRAMES_PER_SECOND ) { // set different color staticTextGameSpeed->winEnable(FALSE); @@ -427,8 +428,8 @@ void reallyDoStart() DEBUG_LOG(("GameSpeedSlider was at %d", maxFPS)); if (maxFPS > GREATER_NO_FPS_LIMIT) maxFPS = 1000; - if (maxFPS < 15) - maxFPS = 15; + if (maxFPS < GAME_SPEED_MIN) + maxFPS = GAME_SPEED_MIN; TheWritableGlobalData->m_mapName = TheSkirmishGameInfo->getMap(); TheSkirmishGameInfo->startGame(0); @@ -1376,7 +1377,10 @@ void SkirmishGameOptionsMenuInit( WindowLayout *layout, void *userData ) // set up the game speed slider // NameKeyType sliderGameSpeedID = TheNameKeyGenerator->nameToKey( "SkirmishGameOptionsMenu.wnd:SliderGameSpeed" ); GameWindow *sliderGameSpeed = TheWindowManager->winGetWindowFromId( parentSkirmishGameOptions, sliderGameSpeedID ); - Int sliderPos = max(15,min(61,prefs.getInt("FPS", TheGlobalData->m_framesPerSecondLimit))); + // the pref is in retail units (30 is normal, 61 unlimited, retail clamps it to 15..61), doubled for the 60 Hz logic + const Int savedSpeed = max(15,min(61,prefs.getInt("FPS", LOGICFRAMES_PER_SECOND / GENERALS_ONLINE_HIGH_FPS_FRAME_MULTIPLIER))); + Int sliderPos = savedSpeed >= 61 ? GAME_SPEED_UNLIMITED : savedSpeed * GENERALS_ONLINE_HIGH_FPS_FRAME_MULTIPLIER; + TheWindowManager->winSendSystemMsg( sliderGameSpeed, GSM_SET_MIN_MAX, GAME_SPEED_MIN, GAME_SPEED_UNLIMITED ); GadgetSliderSetPosition( sliderGameSpeed, sliderPos ); setFPSTextBox(sliderPos); buttonStart->winSetText(TheGameText->fetch("GUI:Start")); @@ -1614,6 +1618,12 @@ WindowMsgHandledType SkirmishGameOptionsMenuSystem( GameWindow *window, Unsigned Int controlID = control->winGetWindowId(); if(controlID == sliderGameSpeedID) { + // steps of 2, so the speed saves in retail units and loads back the same + if (sliderPos < GAME_SPEED_UNLIMITED && (sliderPos & 1) != 0) + { + sliderPos -= 1; + GadgetSliderSetPosition( control, sliderPos ); + } setFPSTextBox(sliderPos); } break; diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptActions.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptActions.cpp index d9f10bd3221..ef27eb3b009 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptActions.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptActions.cpp @@ -7121,16 +7121,15 @@ void ScriptActions::executeAction( ScriptAction *pAction ) doSetStoppingDistance(pAction->getParameter(0)->getString(), pAction->getParameter(1)->getReal()); return; case ScriptAction::SET_FPS_LIMIT: + // the game speed of a script is in retail units (30 is normal), so it is doubled for the 60 Hz logic. The render limit is the one of Options.ini if (!pAction->getParameter(0)->getInt()) { - TheFramePacer->setFramesPerSecondLimit(TheGlobalData->m_framesPerSecondLimit); + TheFramePacer->resetGameSpeed(); } else { - TheFramePacer->setFramesPerSecondLimit(pAction->getParameter(0)->getInt()); + TheFramePacer->setGameSpeedFps(pAction->getParameter(0)->getInt() * GENERALS_ONLINE_HIGH_FPS_FRAME_MULTIPLIER); } - // Setting the fps limit doesn't do much good if we don't use it. jba. - TheWritableGlobalData->m_useFpsLimit = true; return; case ScriptAction::DISABLE_SPECIAL_POWER_DISPLAY: diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptEngine.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptEngine.cpp index 5b2fef0caa5..6facdb89561 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptEngine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/ScriptEngine/ScriptEngine.cpp @@ -5267,9 +5267,9 @@ void ScriptEngine::init() //------------------------------------------------------------------------------------------------- void ScriptEngine::reset() { - // setting FPS limit in case a script had changed it - if (TheFramePacer && TheGlobalData) - TheFramePacer->setFramesPerSecondLimit(TheGlobalData->m_framesPerSecondLimit); + // back to the normal game speed in case a script had changed it + if (TheFramePacer) + TheFramePacer->resetGameSpeed(); if (TheScriptActions) { TheScriptActions->reset(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.cpp index 347c65de981..91eeccdcc07 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.cpp @@ -2,6 +2,9 @@ #include "../json.hpp" #include "../OnlineServices_LobbyInterface.h" #include "../OnlineServices_Init.h" +#include +#include "Common/FrameRateLimit.h" +#include "Common/OptionPreferences.h" #define SETTINGS_KEY_CAMERA "camera" #define SETTINGS_KEY_CAMERA_MIN_HEIGHT "min_height" @@ -9,8 +12,8 @@ #define SETTINGS_KEY_CAMERA_MAX_HEIGHT_WHEN_LOBBY_HOST "max_height_only_when_lobby_host" #define SETTINGS_KEY_RENDER "render" -#define SETTINGS_KEY_RENDER_LIMIT_FRAMERATE "limit_framerate" -#define SETTINGS_KEY_RENDER_FRAMERATE_LIMIT_FPS_VAL "fps_limit" +#define SETTINGS_KEY_RENDER_LEGACY_LIMIT_FRAMERATE "limit_framerate" +#define SETTINGS_KEY_RENDER_LEGACY_FPS_LIMIT "fps_limit" #define SETTINGS_KEY_RENDER_DRAW_STATS_OVERLAY "stats_overlay" #define SETTINGS_KEY_CHAT "chat" @@ -94,6 +97,24 @@ std::string GenOnlineSettings::GetAnticheatPlugin() const return m_Plugins_Anticheat; } +// The render frame rate limit used to be kept here as well as in Options.ini. A value from an old settings.json moves to the +// RenderFpsLimit key of Options.ini, unless that file already has one. +static void MigrateFpsLimitToOptions(bool limited, int fps) +{ + OptionPreferences options; + if (options.find("RenderFpsLimit") != options.end()) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[Settings] render.limit_framerate and render.fps_limit dropped, Options.ini already has RenderFpsLimit"); + return; + } + const int value = limited ? std::clamp(fps, 30, 480) : static_cast(RenderFpsPreset::UncappedFpsValue); + AsciiString text; + text.format("%d", value); + options["RenderFpsLimit"] = text; + options.write(); + NetworkLog(ELogVerbosity::LOG_RELEASE, "[Settings] Moved render.limit_framerate=%d render.fps_limit=%d to Options.ini RenderFpsLimit=%d", limited ? 1 : 0, fps, value); +} + void GenOnlineSettings::Load(void) { char GameDir[MAX_PATH + 1] = {}; @@ -199,14 +220,15 @@ void GenOnlineSettings::Load(void) { auto renderSettings = jsonSettings[SETTINGS_KEY_RENDER]; - if (renderSettings.contains(SETTINGS_KEY_RENDER_LIMIT_FRAMERATE)) - { - m_Render_LimitFramerate = renderSettings[SETTINGS_KEY_RENDER_LIMIT_FRAMERATE]; - } - - if (renderSettings.contains(SETTINGS_KEY_RENDER_FRAMERATE_LIMIT_FPS_VAL)) + // the frame rate limit lives in Options.ini now: carry an old value over, the keys are dropped on the next save + const bool hasLimited = renderSettings.contains(SETTINGS_KEY_RENDER_LEGACY_LIMIT_FRAMERATE); + const bool hasFps = renderSettings.contains(SETTINGS_KEY_RENDER_LEGACY_FPS_LIMIT); + if (hasLimited || hasFps) { - m_Render_FramerateLimit_FPSVal = renderSettings[SETTINGS_KEY_RENDER_FRAMERATE_LIMIT_FPS_VAL]; + const auto& limitedValue = renderSettings[SETTINGS_KEY_RENDER_LEGACY_LIMIT_FRAMERATE]; + const auto& fpsValue = renderSettings[SETTINGS_KEY_RENDER_LEGACY_FPS_LIMIT]; + MigrateFpsLimitToOptions(!hasLimited || !limitedValue.is_boolean() || limitedValue.get(), + hasFps && fpsValue.is_number_integer() ? fpsValue.get() : 60); } if (renderSettings.contains(SETTINGS_KEY_RENDER_DRAW_STATS_OVERLAY)) @@ -343,8 +365,6 @@ void GenOnlineSettings::Load(void) m_Camera_MinHeight = m_Camera_MinHeight_default; m_Camera_MaxHeight_LobbyHost = GENERALS_ONLINE_DEFAULT_LOBBY_CAMERA_ZOOM; m_bVerbose = false; - m_Render_LimitFramerate = true; - m_Render_FramerateLimit_FPSVal = 60; m_Render_DrawStatsOverlay = true; m_Chat_LifeSeconds = 30; @@ -391,8 +411,6 @@ void GenOnlineSettings::Save() { SETTINGS_KEY_RENDER, { - {SETTINGS_KEY_RENDER_LIMIT_FRAMERATE, m_Render_LimitFramerate}, - {SETTINGS_KEY_RENDER_FRAMERATE_LIMIT_FPS_VAL, m_Render_FramerateLimit_FPSVal}, {SETTINGS_KEY_RENDER_DRAW_STATS_OVERLAY, m_Render_DrawStatsOverlay}, } }, diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMPGame.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMPGame.cpp index 62f7b703cba..51051fa392c 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMPGame.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NGMPGame.cpp @@ -1,3 +1,4 @@ +#include "Common/FrameRateLimit.h" #include "GameNetwork/GeneralsOnline/NGMPGame.h" #include "GameLogic/VictoryConditions.h" #include "Common/PlayerList.h" @@ -528,16 +529,8 @@ void NGMPGame::launchGame(void) #if defined(GENERALS_ONLINE_HIGH_FPS_RENDER) - if (NGMP_OnlineServicesManager::Settings.Graphics_LimitFramerate()) - { - TheWritableGlobalData->m_framesPerSecondLimit = NGMP_OnlineServicesManager::Settings.Graphics_GetFPSLimit(); - TheWritableGlobalData->m_useFpsLimit = true; - } - else - { - TheWritableGlobalData->m_framesPerSecondLimit = 30000; // game does this... it's not great - TheWritableGlobalData->m_useFpsLimit = false; - } + // m_framesPerSecondLimit is the render limit of Options.ini, uncapped is its largest value + TheWritableGlobalData->m_useFpsLimit = TheGlobalData->m_framesPerSecondLimit < (Int)RenderFpsPreset::UncappedFpsValue; #endif //TheWritableGlobalData->m_useFpsLimit = false; diff --git a/GeneralsMD/Code/Main/WinMain.cpp b/GeneralsMD/Code/Main/WinMain.cpp index d68a5c5d989..2074c841296 100644 --- a/GeneralsMD/Code/Main/WinMain.cpp +++ b/GeneralsMD/Code/Main/WinMain.cpp @@ -59,6 +59,7 @@ #include "GameClient/GameClient.h" #include "GameLogic/GameLogic.h" ///< @todo for demo, remove #include "GameClient/Mouse.h" +#include "Common/FramePacer.h" #include "GameClient/IMEManager.h" #include "Win32Device/GameClient/Win32Mouse.h" #include "Win32Device/Common/Win32GameEngine.h" @@ -423,9 +424,27 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, if (TheMouse) TheMouse->refreshCursorCapture(); +#if defined(GENERALS_ONLINE) + // the window may be on another monitor now + if (TheFramePacer) + TheFramePacer->invalidateWindowRefreshRate(); +#endif + break; } +#if defined(GENERALS_ONLINE) + //------------------------------------------------------------------------- + case WM_DISPLAYCHANGE: + case WM_EXITSIZEMOVE: + { + if (TheFramePacer) + TheFramePacer->invalidateWindowRefreshRate(); + + break; + } +#endif + //------------------------------------------------------------------------- case WM_SIZE: {