-
Notifications
You must be signed in to change notification settings - Fork 89
tweak(fps): Decouple logic time step from render update #1451
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
8ef9d91
be00a02
90345aa
d2a6bf6
c92e7f9
20f00ce
1036d47
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
/* | ||
** Command & Conquer Generals Zero Hour(tm) | ||
** Copyright 2025 TheSuperHackers | ||
** | ||
** This program is free software: you can redistribute it and/or modify | ||
** it under the terms of the GNU General Public License as published by | ||
** the Free Software Foundation, either version 3 of the License, or | ||
** (at your option) any later version. | ||
** | ||
** This program is distributed in the hope that it will be useful, | ||
** but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
** GNU General Public License for more details. | ||
** | ||
** You should have received a copy of the GNU General Public License | ||
** along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
#pragma once | ||
|
||
class FrameRateLimit | ||
{ | ||
public: | ||
FrameRateLimit(); | ||
|
||
Real wait(UnsignedInt maxFps); | ||
|
||
private: | ||
LARGE_INTEGER m_freq; | ||
LARGE_INTEGER m_start; | ||
}; | ||
|
||
|
||
enum FpsValueChange | ||
{ | ||
FpsValueChange_Increase, | ||
FpsValueChange_Decrease, | ||
}; | ||
|
||
|
||
class RenderFpsPreset | ||
{ | ||
public: | ||
enum CPP_11(: UnsignedInt) | ||
{ | ||
UncappedFpsValue = 1000, | ||
}; | ||
|
||
static UnsignedInt getNextFpsValue(UnsignedInt value); | ||
static UnsignedInt getPrevFpsValue(UnsignedInt value); | ||
static UnsignedInt changeFpsValue(UnsignedInt value, FpsValueChange change); | ||
|
||
private: | ||
static const UnsignedInt s_fpsValues[]; | ||
}; | ||
|
||
|
||
class LogicTimeScaleFpsPreset | ||
{ | ||
public: | ||
enum CPP_11(: UnsignedInt) | ||
{ | ||
StepFpsValue = 5, | ||
}; | ||
|
||
static UnsignedInt getNextFpsValue(UnsignedInt value); | ||
static UnsignedInt getPrevFpsValue(UnsignedInt value); | ||
static UnsignedInt changeFpsValue(UnsignedInt value, FpsValueChange change); | ||
}; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
/* | ||
** Command & Conquer Generals Zero Hour(tm) | ||
** Copyright 2025 TheSuperHackers | ||
** | ||
** This program is free software: you can redistribute it and/or modify | ||
** it under the terms of the GNU General Public License as published by | ||
** the Free Software Foundation, either version 3 of the License, or | ||
** (at your option) any later version. | ||
** | ||
** This program is distributed in the hope that it will be useful, | ||
** but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
** GNU General Public License for more details. | ||
** | ||
** You should have received a copy of the GNU General Public License | ||
** along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
#include "PreRTS.h" | ||
#include "Common/FrameRateLimit.h" | ||
|
||
|
||
FrameRateLimit::FrameRateLimit() | ||
{ | ||
QueryPerformanceFrequency(&m_freq); | ||
QueryPerformanceCounter(&m_start); | ||
} | ||
|
||
Real FrameRateLimit::wait(UnsignedInt maxFps) | ||
{ | ||
LARGE_INTEGER tick; | ||
QueryPerformanceCounter(&tick); | ||
double elapsedSeconds = static_cast<double>(tick.QuadPart - m_start.QuadPart) / m_freq.QuadPart; | ||
const double targetSeconds = 1.0 / maxFps; | ||
const double sleepSeconds = targetSeconds - elapsedSeconds - 0.002; // leave ~2ms for spin wait | ||
|
||
if (sleepSeconds > 0.0) | ||
{ | ||
// Non busy wait with Munkee sleep | ||
DWORD dwMilliseconds = static_cast<DWORD>(sleepSeconds * 1000); | ||
Sleep(dwMilliseconds); | ||
} | ||
|
||
// Busy wait for remaining time | ||
do | ||
{ | ||
QueryPerformanceCounter(&tick); | ||
elapsedSeconds = static_cast<double>(tick.QuadPart - m_start.QuadPart) / m_freq.QuadPart; | ||
} | ||
while (elapsedSeconds < targetSeconds); | ||
|
||
m_start = tick; | ||
return (Real)elapsedSeconds; | ||
} | ||
|
||
|
||
const UnsignedInt RenderFpsPreset::s_fpsValues[] = { | ||
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!"); | ||
|
||
UnsignedInt RenderFpsPreset::getNextFpsValue(UnsignedInt value) | ||
{ | ||
const Int first = 0; | ||
const Int last = ARRAY_SIZE(s_fpsValues) - 1; | ||
for (Int i = first; i < last; ++i) | ||
{ | ||
if (value >= s_fpsValues[i] && value < s_fpsValues[i + 1]) | ||
{ | ||
return s_fpsValues[i + 1]; | ||
} | ||
} | ||
return s_fpsValues[last]; | ||
} | ||
|
||
UnsignedInt RenderFpsPreset::getPrevFpsValue(UnsignedInt value) | ||
{ | ||
const Int first = 0; | ||
const Int last = ARRAY_SIZE(s_fpsValues) - 1; | ||
for (Int i = last; i > first; --i) | ||
{ | ||
if (value <= s_fpsValues[i] && value > s_fpsValues[i - 1]) | ||
{ | ||
return s_fpsValues[i - 1]; | ||
} | ||
} | ||
return s_fpsValues[first]; | ||
} | ||
|
||
UnsignedInt RenderFpsPreset::changeFpsValue(UnsignedInt value, FpsValueChange change) | ||
{ | ||
switch (change) | ||
{ | ||
default: | ||
case FpsValueChange_Increase: return getNextFpsValue(value); | ||
case FpsValueChange_Decrease: return getPrevFpsValue(value); | ||
} | ||
} | ||
|
||
|
||
UnsignedInt LogicTimeScaleFpsPreset::getNextFpsValue(UnsignedInt value) | ||
{ | ||
return value + StepFpsValue; | ||
} | ||
|
||
UnsignedInt LogicTimeScaleFpsPreset::getPrevFpsValue(UnsignedInt value) | ||
{ | ||
if (value - StepFpsValue < LOGICFRAMES_PER_SECOND) | ||
{ | ||
return LOGICFRAMES_PER_SECOND; | ||
} | ||
else | ||
{ | ||
return value - StepFpsValue; | ||
} | ||
} | ||
|
||
UnsignedInt LogicTimeScaleFpsPreset::changeFpsValue(UnsignedInt value, FpsValueChange change) | ||
{ | ||
switch (change) | ||
{ | ||
default: | ||
case FpsValueChange_Increase: return getNextFpsValue(value); | ||
case FpsValueChange_Decrease: return getPrevFpsValue(value); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,8 +34,6 @@ | |
#include "Common/SubsystemInterface.h" | ||
#include "Common/GameType.h" | ||
|
||
#define DEFAULT_MAX_FPS 45 | ||
|
||
// forward declarations | ||
class AudioManager; | ||
class GameLogic; | ||
|
@@ -72,8 +70,18 @@ class GameEngine : public SubsystemInterface | |
|
||
virtual void execute( void ); /**< The "main loop" of the game engine. | ||
It will not return until the game exits. */ | ||
virtual void setFramesPerSecondLimit( Int fps ); ///< Set the maximum rate engine updates are allowed to occur | ||
virtual Int getFramesPerSecondLimit( void ); ///< Get maxFPS. Not inline since it is called from another lib. | ||
|
||
virtual void setFramesPerSecondLimit( Int fps ); ///< Set the max render and engine update fps. | ||
virtual Int getFramesPerSecondLimit( void ); ///< Get the max render fps. | ||
|
||
virtual void setLogicTimeScaleFps( Int fps ); ///< Set the logic time scale fps and therefore scale the simulation time. Is capped by the max render fps and does not apply to network matches. | ||
virtual Int getLogicTimeScaleFps(); ///< Get the raw logic time scale fps value. | ||
virtual void enableLogicTimeScale( Bool enable ); ///< Enable the logic time scale setup. If disabled, the simulation time scale is bound to the render frame time or network update time. | ||
virtual Bool isLogicTimeScaleEnabled(); ///< Check whether the logic time scale setup is enabled. | ||
Int getActualLogicTimeScaleFps(); ///< Get the real logic time scale fps, depending on the max render fps, network state and enabled state. | ||
Real getActualLogicTimeScaleRatio(); ///< Get the real logic time scale ratio, depending on the max render fps, network state and enabled state. | ||
Real getActualLogicTimeScaleOverFpsRatio(); ///< Get the real logic time scale over render fps ratio, used to scale down steps in render updates to match logic updates. | ||
|
||
virtual void setQuitting( Bool quitting ); ///< set quitting status | ||
virtual Bool getQuitting(void); ///< is app getting ready to quit. | ||
|
||
|
@@ -100,9 +108,15 @@ class GameEngine : public SubsystemInterface | |
virtual ParticleSystemManager* createParticleSystemManager( void ) = 0; | ||
virtual AudioManager *createAudioManager( void ) = 0; ///< Factory for Audio Manager | ||
|
||
Int m_maxFPS; ///< Maximum frames per second allowed | ||
Int m_maxFPS; ///< Maximum frames per second for rendering | ||
Int m_logicTimeScaleFPS; ///< Maximum frames per second for logic time scale | ||
|
||
Real m_frameTime; ///< Last render frame time | ||
Real m_logicFrameTimeAccumulator; ///< Frame time accumulated towards submitting a new logic frame | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be better to distinguish this as rendering frame time in the name of the variable There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed |
||
|
||
Bool m_quitting; ///< true when we need to quit the game | ||
Bool m_isActive; ///< app has OS focus. | ||
Bool m_enableLogicTimeScale; | ||
|
||
}; | ||
inline void GameEngine::setQuitting( Bool quitting ) { m_quitting = quitting; } | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -241,6 +241,10 @@ class GameMessage : public MemoryPoolObject | |
MSG_META_HELP, ///< bring up help screen | ||
#endif | ||
|
||
MSG_META_INCREASE_MAX_RENDER_FPS, ///< TheSuperHackers @feature Increase the max render fps | ||
MSG_META_DECREASE_MAX_RENDER_FPS, ///< TheSuperHackers @feature Decrease the max render fps | ||
MSG_META_INCREASE_MAX_LOGIC_FPS, ///< TheSuperHackers @feature Increase the max logic fps | ||
MSG_META_DECREASE_MAX_LOGIC_FPS, ///< TheSuperHackers @feature Decrease the max logic fps | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This needs renaming There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed |
||
MSG_META_TOGGLE_LOWER_DETAILS, ///< toggles graphics options to crappy mode instantly | ||
MSG_META_TOGGLE_CONTROL_BAR, ///< show/hide controlbar | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be better to rename
m_maxFPS
to make it more relevant to the rendering FPS.m_maxRenderFPS
for example.For the logic one, it would be better to have the naming match the rendering.
m_maxLogicFPS
etc.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did not intent to rename
m_maxFPS
for this change to have a bit less diff.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
m_maxLogicFPS
is not the correct terminology for this. Logic FPS is what we currently refer to as enum LOGICFRAMES_PER_SECOND=30.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's the currently set max logic frame rate,
LOGICFRAMES_PER_SECOND
is the default maximum value. But since we can / need to be able to vary the current max logic frame rate, it makes sense to call it that for the variable.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
m_logicTimeScaleFPS is conceptually not equivalent to LOGICFRAMES_PER_SECOND or m_maxLogicFPS. It effectively is a ratio that scales the logic fps.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You ask me to rename it to
m_maxLogicFPS
but I am telling you this is not the right name for it. Originally I used this exact name for it, until I realized it is misleading because it would be pretty much identical to LOGICFRAMES_PER_SECOND but are not the same thing. This is why I called it Logic Time Scale.Currently Logic Time Scale is capped by the Render Update. We could perhaps also uncap it and make it a substitute for fast forwarding globally.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not misleading though, i think you are putting too much emphasis on what
LOGICFRAMES_PER_SECOND
means compared to what it actually is meant to be.LOGICFRAMES_PER_SECOND
is the default maximum logic frame rate during normal gameplay etc. It's basically a value used for default configuration and normal configuration.While your
m_logicTimeScaleFps
is the max logic FPS that is being used within the game at runtime. Which can vary to allow fast gameplay mode in skirmish or within mod maps etc.Both values are related but mean different things.
Logic tick rate being capped by the rendering is fine, but that's a different problem overall. The logic tick rate does not need to exceed the rendering rate and probably never should. But varying the max logic rate implements the fast forward functionality, so of course the render rate has to increase if the logic rate was to exceed it.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thing is, when we make LOGICFRAMES_PER_SECOND configurable, then what will be its name? Given your name proposal, it would end up being something like:
I disagree with giving these 2 things the same name.
This will be better:
It does so in the original game, during fast forwarding. I think it is fine to do that. I can explore this in a follow up change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At that point it would be like with any other configurable variable. we have the config handling set
Int m_maxLogicFPS;
at startup etc. Or it uses the constantLOGICFRAMES_PER_SECOND
as the default.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I expect it will be possible to change LOGICFRAMES_PER_SECOND at runtime to any value above 0.