Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions Core/GameEngine/Include/Common/FramePacer.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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

Expand Down
2 changes: 2 additions & 0 deletions Core/GameEngine/Include/Common/FrameRateLimit.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ class FrameRateLimit
private:
Int64 m_freq;
Int64 m_start;
Int64 m_lastActual;
UnsignedInt m_previousFps;
};


Expand Down
1 change: 1 addition & 0 deletions Core/GameEngine/Include/Common/OptionPreferences.h
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ class OptionPreferences : public UserPreferences
Bool getExtraAnimationsDisabled();
Bool getUseHeatEffects();
Bool getDynamicLODEnabled();
UnsignedInt getRenderFpsLimit();
Bool getFPSLimitEnabled();
Bool getBuildingOcclusionEnabled();
Int getParticleCap();
Expand Down
83 changes: 82 additions & 1 deletion Core/GameEngine/Source/Common/FramePacer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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()
Expand Down Expand Up @@ -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<Int>(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<Int>(fps, getActualLogicTimeScaleFps(IgnoreFrozenTime | IgnoreHaltedGame));
}
return fps;
}
#endif

Real FramePacer::getUpdateTime() const
{
return m_updateTime;
Expand Down Expand Up @@ -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<Int>(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)
Expand Down
29 changes: 27 additions & 2 deletions Core/GameEngine/Source/Common/FrameRateLimit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,20 @@ FrameRateLimit::FrameRateLimit()
QueryPerformanceCounter(&start);
m_freq = freq.QuadPart;
m_start = start.QuadPart;
m_lastActual = m_start;
m_previousFps = 0;
}

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<double>(tick.QuadPart - m_start) / m_freq;
const double targetSeconds = 1.0 / maxFps;
const double sleepSeconds = targetSeconds - elapsedSeconds - 0.002; // leave ~2ms for spin wait
Expand All @@ -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
{
Expand All @@ -54,15 +69,25 @@ Real FrameRateLimit::wait(UnsignedInt maxFps)
}
while (elapsedSeconds < targetSeconds);

m_start = tick.QuadPart;
return (Real)elapsedSeconds;
const Int64 period = static_cast<Int64>(static_cast<double>(m_freq) / maxFps + 0.5);
const Int64 deadline = m_start + period;
const double actualSeconds = static_cast<double>(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<Real>(actualSeconds);
}

void FrameRateLimit::reset()
{
LARGE_INTEGER tick;
QueryPerformanceCounter(&tick);
m_start = tick.QuadPart;
m_lastActual = m_start;
m_previousFps = 0;
}


Expand Down
15 changes: 15 additions & 0 deletions Core/GameEngine/Source/Common/OptionPreferences.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<Int>(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");
Expand Down
19 changes: 12 additions & 7 deletions Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand All @@ -229,7 +229,7 @@ static bool changeObserverNotificationFontSize(ObserverFontSizeChange change)
if (TheInGameUI)
TheInGameUI->refreshObserverNotificationResources();

writeObserverFontSizePref("ObserverNotificationFontSize", fontSize);
writeIntPref("ObserverNotificationFontSize", fontSize);

return true;
}
Expand All @@ -246,7 +246,7 @@ static bool changeObserverStatsFontSize(ObserverFontSizeChange change)
if (TheInGameUI)
TheInGameUI->initObserverOverlay();

writeObserverFontSizePref("ObserverStatsFontSize", fontSize);
writeIntPref("ObserverStatsFontSize", fontSize);

return true;
}
Expand All @@ -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;
Expand Down Expand Up @@ -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())
Expand Down
8 changes: 8 additions & 0 deletions Core/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions Core/Libraries/Source/WWVegas/WW3D2/dx8wrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions Core/Libraries/Source/WWVegas/WW3D2/dx8wrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
5 changes: 5 additions & 0 deletions Generals/Code/GameEngine/Source/Common/GameMain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Loading
Loading