From f3523458c499066d23063ab80f500177acd3b7c1 Mon Sep 17 00:00:00 2001 From: githubawn <115191165+githubawn@users.noreply.github.com> Date: Tue, 5 May 2026 23:40:48 +0200 Subject: [PATCH 1/3] chore(frame-pacer): overhaul logic and render fps limits and presets - Added 15 FPS to render presets and expanded logic presets (1 to 960 FPS). - Implemented array-based preset snapping for logic FPS. - Renamed extraStep to snapValue for clarity. - Removed redundant logic speed scaling guards and safety asserts. - Cleaned up stale comments and dead code in CommandXlat. --- .../Include/Common/FrameRateLimit.h | 18 ++---- .../Source/Common/FrameRateLimit.cpp | 63 +++++++++++++++---- .../GameClient/MessageStream/CommandXlat.cpp | 30 +++------ .../GameClient/MessageStream/CommandXlat.cpp | 30 +++------ 4 files changed, 72 insertions(+), 69 deletions(-) diff --git a/Core/GameEngine/Include/Common/FrameRateLimit.h b/Core/GameEngine/Include/Common/FrameRateLimit.h index 5bb2b5fd3a7..bda06007b3e 100644 --- a/Core/GameEngine/Include/Common/FrameRateLimit.h +++ b/Core/GameEngine/Include/Common/FrameRateLimit.h @@ -61,18 +61,12 @@ class RenderFpsPreset class LogicTimeScaleFpsPreset { public: - enum CPP_11(: UnsignedInt) - { -#if RTS_DEBUG - MinFpsValue = 5, -#else - MinFpsValue = LOGICFRAMES_PER_SECOND, -#endif - StepFpsValue = 5, - }; - static UnsignedInt getNextFpsValue(UnsignedInt value); - static UnsignedInt getPrevFpsValue(UnsignedInt value); - static UnsignedInt changeFpsValue(UnsignedInt value, FpsValueChange change); + static UnsignedInt getNextFpsValue(UnsignedInt value, UnsignedInt snapValue = 0); + static UnsignedInt getPrevFpsValue(UnsignedInt value, UnsignedInt snapValue = 0); + static UnsignedInt changeFpsValue(UnsignedInt value, FpsValueChange change, UnsignedInt snapValue = 0); + +private: + static const UnsignedInt s_fpsValues[]; }; diff --git a/Core/GameEngine/Source/Common/FrameRateLimit.cpp b/Core/GameEngine/Source/Common/FrameRateLimit.cpp index ffd76c23439..5f8710c269b 100644 --- a/Core/GameEngine/Source/Common/FrameRateLimit.cpp +++ b/Core/GameEngine/Source/Common/FrameRateLimit.cpp @@ -60,9 +60,10 @@ Real FrameRateLimit::wait(UnsignedInt maxFps) const UnsignedInt RenderFpsPreset::s_fpsValues[] = { - 30, 50, 56, 60, 65, 70, 72, 75, 80, 85, 90, 100, 110, 120, 144, 240, 480, UncappedFpsValue }; + 15, 30, 50, 56, 60, 65, 70, 72, 75, 80, 85, 90, 100, 110, 120, 144, 240, 480, UncappedFpsValue }; -static_assert(LOGICFRAMES_PER_SECOND <= 30, "Min FPS values need to be revisited!"); +const UnsignedInt LogicTimeScaleFpsPreset::s_fpsValues[] = { + 1, 5, 15, 30, 45, 60, 75, 90, 105, 120, 240, 480, 960, RenderFpsPreset::UncappedFpsValue }; UnsignedInt RenderFpsPreset::getNextFpsValue(UnsignedInt value) { @@ -102,30 +103,66 @@ UnsignedInt RenderFpsPreset::changeFpsValue(UnsignedInt value, FpsValueChange ch } } - -UnsignedInt LogicTimeScaleFpsPreset::getNextFpsValue(UnsignedInt value) +UnsignedInt LogicTimeScaleFpsPreset::getNextFpsValue(UnsignedInt value, UnsignedInt snapValue) { - return value + StepFpsValue; + UnsignedInt nextValue = s_fpsValues[ARRAY_SIZE(s_fpsValues) - 1]; // Default to Uncapped + + // Check if snapValue (e.g. current render FPS) is the next closest candidate + if (snapValue > value && snapValue < nextValue) + { + nextValue = snapValue; + } + + // Check predefined steps + for (size_t i = 0; i < ARRAY_SIZE(s_fpsValues); ++i) + { + if (s_fpsValues[i] > value) + { + if (s_fpsValues[i] < nextValue) + { + nextValue = s_fpsValues[i]; + } + break; + } + } + + return nextValue; } -UnsignedInt LogicTimeScaleFpsPreset::getPrevFpsValue(UnsignedInt value) +UnsignedInt LogicTimeScaleFpsPreset::getPrevFpsValue(UnsignedInt value, UnsignedInt snapValue) { - if (value - StepFpsValue < MinFpsValue) + UnsignedInt prevValue = s_fpsValues[0]; // Floor/seed for the search loop + + // Check if snapValue (e.g. current render FPS) is the previous closest candidate. + // Note: if snapValue == value, neither branch below fires and the snap point is + // intentionally skipped — the caller must step to a different preset. + if (snapValue < value && snapValue > prevValue) { - return MinFpsValue; + prevValue = snapValue; } - else + + // Check predefined steps + for (int i = (int)ARRAY_SIZE(s_fpsValues) - 1; i >= 0; --i) { - return value - StepFpsValue; + if (s_fpsValues[i] < value) + { + if (s_fpsValues[i] > prevValue) + { + prevValue = s_fpsValues[i]; + } + break; + } } + + return prevValue; } -UnsignedInt LogicTimeScaleFpsPreset::changeFpsValue(UnsignedInt value, FpsValueChange change) +UnsignedInt LogicTimeScaleFpsPreset::changeFpsValue(UnsignedInt value, FpsValueChange change, UnsignedInt snapValue) { switch (change) { default: - case FpsValueChange_Increase: return getNextFpsValue(value); - case FpsValueChange_Decrease: return getPrevFpsValue(value); + case FpsValueChange_Increase: return getNextFpsValue(value, snapValue); + case FpsValueChange_Decrease: return getPrevFpsValue(value, snapValue); } } diff --git a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index c6235b0f7da..d0888f462a0 100644 --- a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -211,38 +211,24 @@ bool changeLogicTimeScale(FpsValueChange change) return false; const UnsignedInt maxRenderFps = TheFramePacer->getFramesPerSecondLimit(); - UnsignedInt maxRenderRemainder = LogicTimeScaleFpsPreset::StepFpsValue; - maxRenderRemainder -= maxRenderFps % LogicTimeScaleFpsPreset::StepFpsValue; - maxRenderRemainder %= LogicTimeScaleFpsPreset::StepFpsValue; - UnsignedInt logicTimeScaleFps = TheFramePacer->getLogicTimeScaleFps(); - // Set the value to the max render fps value plus a bit when time scale is - // disabled. This ensures that the time scale does not re-enable with a - // 'surprise' value. + if (!TheFramePacer->isLogicTimeScaleEnabled()) { - logicTimeScaleFps = maxRenderFps + maxRenderRemainder; + logicTimeScaleFps = maxRenderFps; } - // Ceil the value at the max render fps value plus a bit so that the next fps - // value decrease would undercut the max render fps at the correct step value. - // Example: render fps 72 -> logic value ceiled to 75 -> decreased to 70. - logicTimeScaleFps = min(logicTimeScaleFps, maxRenderFps + maxRenderRemainder); - logicTimeScaleFps = LogicTimeScaleFpsPreset::changeFpsValue(logicTimeScaleFps, change); - // Set value before potentially disabling it. - if (TheFramePacer->isLogicTimeScaleEnabled()) + logicTimeScaleFps = LogicTimeScaleFpsPreset::changeFpsValue(logicTimeScaleFps, change, maxRenderFps); + + // Ensure logic FPS never exceeds render FPS + if (logicTimeScaleFps > maxRenderFps && logicTimeScaleFps != RenderFpsPreset::UncappedFpsValue) { - TheFramePacer->setLogicTimeScaleFps(logicTimeScaleFps); + logicTimeScaleFps = maxRenderFps; } + TheFramePacer->setLogicTimeScaleFps(logicTimeScaleFps); TheFramePacer->enableLogicTimeScale(logicTimeScaleFps < maxRenderFps); - // Set value after potentially enabling it. - if (TheFramePacer->isLogicTimeScaleEnabled()) - { - TheFramePacer->setLogicTimeScaleFps(logicTimeScaleFps); - } - logicTimeScaleFps = TheFramePacer->getLogicTimeScaleFps(); const UnsignedInt actualLogicTimeScaleFps = TheFramePacer->getActualLogicTimeScaleFps(); const Real actualLogicTimeScaleRatio = TheFramePacer->getActualLogicTimeScaleRatio(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index 830faa70ac9..d79cdfcdf5e 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -210,38 +210,24 @@ bool changeLogicTimeScale(FpsValueChange change) return false; const UnsignedInt maxRenderFps = TheFramePacer->getFramesPerSecondLimit(); - UnsignedInt maxRenderRemainder = LogicTimeScaleFpsPreset::StepFpsValue; - maxRenderRemainder -= maxRenderFps % LogicTimeScaleFpsPreset::StepFpsValue; - maxRenderRemainder %= LogicTimeScaleFpsPreset::StepFpsValue; - UnsignedInt logicTimeScaleFps = TheFramePacer->getLogicTimeScaleFps(); - // Set the value to the max render fps value plus a bit when time scale is - // disabled. This ensures that the time scale does not re-enable with a - // 'surprise' value. + if (!TheFramePacer->isLogicTimeScaleEnabled()) { - logicTimeScaleFps = maxRenderFps + maxRenderRemainder; + logicTimeScaleFps = maxRenderFps; } - // Ceil the value at the max render fps value plus a bit so that the next fps - // value decrease would undercut the max render fps at the correct step value. - // Example: render fps 72 -> logic value ceiled to 75 -> decreased to 70. - logicTimeScaleFps = min(logicTimeScaleFps, maxRenderFps + maxRenderRemainder); - logicTimeScaleFps = LogicTimeScaleFpsPreset::changeFpsValue(logicTimeScaleFps, change); - // Set value before potentially disabling it. - if (TheFramePacer->isLogicTimeScaleEnabled()) + logicTimeScaleFps = LogicTimeScaleFpsPreset::changeFpsValue(logicTimeScaleFps, change, maxRenderFps); + + // Ensure logic FPS never exceeds render FPS + if (logicTimeScaleFps > maxRenderFps && logicTimeScaleFps != RenderFpsPreset::UncappedFpsValue) { - TheFramePacer->setLogicTimeScaleFps(logicTimeScaleFps); + logicTimeScaleFps = maxRenderFps; } + TheFramePacer->setLogicTimeScaleFps(logicTimeScaleFps); TheFramePacer->enableLogicTimeScale(logicTimeScaleFps < maxRenderFps); - // Set value after potentially enabling it. - if (TheFramePacer->isLogicTimeScaleEnabled()) - { - TheFramePacer->setLogicTimeScaleFps(logicTimeScaleFps); - } - logicTimeScaleFps = TheFramePacer->getLogicTimeScaleFps(); const UnsignedInt actualLogicTimeScaleFps = TheFramePacer->getActualLogicTimeScaleFps(); const Real actualLogicTimeScaleRatio = TheFramePacer->getActualLogicTimeScaleRatio(); From 922e5d5dd78dc6ddd28ad60b5b6f1dbfa44360e8 Mon Sep 17 00:00:00 2001 From: githubawn <115191165+githubawn@users.noreply.github.com> Date: Mon, 11 May 2026 19:19:07 +0200 Subject: [PATCH 2/3] greptile fixes --- Core/GameEngine/Source/Common/FrameRateLimit.cpp | 1 + .../Source/GameClient/MessageStream/CommandXlat.cpp | 9 +++++++-- .../Source/GameClient/MessageStream/CommandXlat.cpp | 9 +++++++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/Core/GameEngine/Source/Common/FrameRateLimit.cpp b/Core/GameEngine/Source/Common/FrameRateLimit.cpp index 5f8710c269b..10c24e0fd9d 100644 --- a/Core/GameEngine/Source/Common/FrameRateLimit.cpp +++ b/Core/GameEngine/Source/Common/FrameRateLimit.cpp @@ -62,6 +62,7 @@ Real FrameRateLimit::wait(UnsignedInt maxFps) const UnsignedInt RenderFpsPreset::s_fpsValues[] = { 15, 30, 50, 56, 60, 65, 70, 72, 75, 80, 85, 90, 100, 110, 120, 144, 240, 480, UncappedFpsValue }; +// TheSuperHackers @info s_fpsValues MUST be strictly ascending; the search loops break on first match. const UnsignedInt LogicTimeScaleFpsPreset::s_fpsValues[] = { 1, 5, 15, 30, 45, 60, 75, 90, 105, 120, 240, 480, 960, RenderFpsPreset::UncappedFpsValue }; diff --git a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index d0888f462a0..adf4e56c2a8 100644 --- a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -226,8 +226,13 @@ bool changeLogicTimeScale(FpsValueChange change) logicTimeScaleFps = maxRenderFps; } - TheFramePacer->setLogicTimeScaleFps(logicTimeScaleFps); - TheFramePacer->enableLogicTimeScale(logicTimeScaleFps < maxRenderFps); + const bool enableTimescale = (logicTimeScaleFps < maxRenderFps); + // TheSuperHackers @info Preserve the last real FPS in m_logicTimeScaleFPS so re-enabling timescale resumes from a sane value. + if (enableTimescale) + { + TheFramePacer->setLogicTimeScaleFps(logicTimeScaleFps); + } + TheFramePacer->enableLogicTimeScale(enableTimescale); logicTimeScaleFps = TheFramePacer->getLogicTimeScaleFps(); const UnsignedInt actualLogicTimeScaleFps = TheFramePacer->getActualLogicTimeScaleFps(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index d79cdfcdf5e..7491c712a73 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -225,8 +225,13 @@ bool changeLogicTimeScale(FpsValueChange change) logicTimeScaleFps = maxRenderFps; } - TheFramePacer->setLogicTimeScaleFps(logicTimeScaleFps); - TheFramePacer->enableLogicTimeScale(logicTimeScaleFps < maxRenderFps); + const bool enableTimescale = (logicTimeScaleFps < maxRenderFps); + // TheSuperHackers @info Preserve the last real FPS in m_logicTimeScaleFPS so re-enabling timescale resumes from a sane value. + if (enableTimescale) + { + TheFramePacer->setLogicTimeScaleFps(logicTimeScaleFps); + } + TheFramePacer->enableLogicTimeScale(enableTimescale); logicTimeScaleFps = TheFramePacer->getLogicTimeScaleFps(); const UnsignedInt actualLogicTimeScaleFps = TheFramePacer->getActualLogicTimeScaleFps(); From 79152a9f81f8e7874455fe7122b9b188d02ebada Mon Sep 17 00:00:00 2001 From: githubawn <115191165+githubawn@users.noreply.github.com> Date: Thu, 14 May 2026 17:52:55 +0200 Subject: [PATCH 3/3] implemented feedback --- .../Source/Common/FrameRateLimit.cpp | 30 ++++++++++++------- .../GameClient/MessageStream/CommandXlat.cpp | 3 +- .../GameClient/MessageStream/CommandXlat.cpp | 3 +- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/Core/GameEngine/Source/Common/FrameRateLimit.cpp b/Core/GameEngine/Source/Common/FrameRateLimit.cpp index 10c24e0fd9d..42136119917 100644 --- a/Core/GameEngine/Source/Common/FrameRateLimit.cpp +++ b/Core/GameEngine/Source/Common/FrameRateLimit.cpp @@ -106,7 +106,7 @@ UnsignedInt RenderFpsPreset::changeFpsValue(UnsignedInt value, FpsValueChange ch UnsignedInt LogicTimeScaleFpsPreset::getNextFpsValue(UnsignedInt value, UnsignedInt snapValue) { - UnsignedInt nextValue = s_fpsValues[ARRAY_SIZE(s_fpsValues) - 1]; // Default to Uncapped + UnsignedInt nextValue = s_fpsValues[ARRAY_SIZE(s_fpsValues) - 1]; // Defaults to Uncapped // Check if snapValue (e.g. current render FPS) is the next closest candidate if (snapValue > value && snapValue < nextValue) @@ -117,11 +117,12 @@ UnsignedInt LogicTimeScaleFpsPreset::getNextFpsValue(UnsignedInt value, Unsigned // Check predefined steps for (size_t i = 0; i < ARRAY_SIZE(s_fpsValues); ++i) { - if (s_fpsValues[i] > value) + const UnsignedInt fpsValue = s_fpsValues[i]; + if (fpsValue > value) { - if (s_fpsValues[i] < nextValue) + if (fpsValue < nextValue) { - nextValue = s_fpsValues[i]; + nextValue = fpsValue; } break; } @@ -137,7 +138,7 @@ UnsignedInt LogicTimeScaleFpsPreset::getPrevFpsValue(UnsignedInt value, Unsigned // Check if snapValue (e.g. current render FPS) is the previous closest candidate. // Note: if snapValue == value, neither branch below fires and the snap point is // intentionally skipped — the caller must step to a different preset. - if (snapValue < value && snapValue > prevValue) + if (snapValue > prevValue && snapValue < value) { prevValue = snapValue; } @@ -145,11 +146,12 @@ UnsignedInt LogicTimeScaleFpsPreset::getPrevFpsValue(UnsignedInt value, Unsigned // Check predefined steps for (int i = (int)ARRAY_SIZE(s_fpsValues) - 1; i >= 0; --i) { - if (s_fpsValues[i] < value) + const UnsignedInt fpsValue = s_fpsValues[i]; + if (fpsValue < value) { - if (s_fpsValues[i] > prevValue) + if (fpsValue > prevValue) { - prevValue = s_fpsValues[i]; + prevValue = fpsValue; } break; } @@ -162,8 +164,14 @@ UnsignedInt LogicTimeScaleFpsPreset::changeFpsValue(UnsignedInt value, FpsValueC { switch (change) { - default: - case FpsValueChange_Increase: return getNextFpsValue(value, snapValue); - case FpsValueChange_Decrease: return getPrevFpsValue(value, snapValue); + case FpsValueChange_Increase: + return getNextFpsValue(value, snapValue); + + case FpsValueChange_Decrease: + return getPrevFpsValue(value, snapValue); + + default: + assert(false); + return value; } } diff --git a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index adf4e56c2a8..120faf36aef 100644 --- a/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -227,13 +227,14 @@ bool changeLogicTimeScale(FpsValueChange change) } const bool enableTimescale = (logicTimeScaleFps < maxRenderFps); + // TheSuperHackers @info Preserve the last real FPS in m_logicTimeScaleFPS so re-enabling timescale resumes from a sane value. if (enableTimescale) { TheFramePacer->setLogicTimeScaleFps(logicTimeScaleFps); } - TheFramePacer->enableLogicTimeScale(enableTimescale); + TheFramePacer->enableLogicTimeScale(enableTimescale); logicTimeScaleFps = TheFramePacer->getLogicTimeScaleFps(); const UnsignedInt actualLogicTimeScaleFps = TheFramePacer->getActualLogicTimeScaleFps(); const Real actualLogicTimeScaleRatio = TheFramePacer->getActualLogicTimeScaleRatio(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index 7491c712a73..63e5953483b 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -226,13 +226,14 @@ bool changeLogicTimeScale(FpsValueChange change) } const bool enableTimescale = (logicTimeScaleFps < maxRenderFps); + // TheSuperHackers @info Preserve the last real FPS in m_logicTimeScaleFPS so re-enabling timescale resumes from a sane value. if (enableTimescale) { TheFramePacer->setLogicTimeScaleFps(logicTimeScaleFps); } - TheFramePacer->enableLogicTimeScale(enableTimescale); + TheFramePacer->enableLogicTimeScale(enableTimescale); logicTimeScaleFps = TheFramePacer->getLogicTimeScaleFps(); const UnsignedInt actualLogicTimeScaleFps = TheFramePacer->getActualLogicTimeScaleFps(); const Real actualLogicTimeScaleRatio = TheFramePacer->getActualLogicTimeScaleRatio();