Skip to content
Merged
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
2 changes: 2 additions & 0 deletions Core/GameEngine/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ set(GAMEENGINE_SRC
# Include/Common/Errors.h
Include/Common/file.h
Include/Common/FileSystem.h
Include/Common/FramePacer.h
Include/Common/FrameRateLimit.h
# Include/Common/FunctionLexicon.h
Include/Common/GameAudio.h
Expand Down Expand Up @@ -570,6 +571,7 @@ set(GAMEENGINE_SRC
# Source/Common/DamageFX.cpp
# Source/Common/Dict.cpp
# Source/Common/DiscreteCircle.cpp
Source/Common/FramePacer.cpp
Source/Common/FrameRateLimit.cpp
# Source/Common/GameEngine.cpp
# Source/Common/GameLOD.cpp
Expand Down
82 changes: 82 additions & 0 deletions Core/GameEngine/Include/Common/FramePacer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
** Command & Conquer Generals Zero Hour(tm)
** Copyright 2025 Electronic Arts Inc.
**
** 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

#include "Common/FrameRateLimit.h"


// TheSuperHackers @todo Use unsigned integers for fps values
// TheSuperHackers @todo Consolidate the GlobalData::m_useFpsLimit and FramePacer::m_enableFpsLimit
// TheSuperHackers @todo Implement new fast forward in here
class FramePacer
{
public:

typedef UnsignedInt LogicTimeQueryFlags;
enum LogicTimeQueryFlags_ CPP_11(: LogicTimeQueryFlags)
{
IgnoreFrozenTime = 1<<0, ///< Ignore frozen time for the query
IgnoreHaltedGame = 1<<1, ///< Ignore halted game for the query
};

FramePacer();
~FramePacer();

void update(); ///< Signal that the app/render update is done and wait for the fps limit if applicable.

void setFramesPerSecondLimit( Int fps ); ///< Set the update fps limit.
Int getFramesPerSecondLimit() const; ///< Get the update fps limit.
void enableFramesPerSecondLimit( Bool enable ); ///< Enable or disable the update fps limit.
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.

Real getUpdateTime() const; ///< Get the last update delta time in seconds.
Real getUpdateFps() const; ///< Get the last update fps.

void setTimeFrozen(Bool frozen); ///< Set time frozen. Allows scripted camera movement.
void setGameHalted(Bool halted); ///< Set game halted. Does not allow scripted camera movement.
Bool isTimeFrozen() const;
Bool isGameHalted() const;

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.
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.
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.
Real getLogicTimeStepSeconds(LogicTimeQueryFlags flags = 0) const; ///< Get the logic time step in seconds
Real getLogicTimeStepMilliseconds(LogicTimeQueryFlags flags = 0) const; ///< Get the logic time step in milliseconds

protected:

FrameRateLimit m_frameRateLimit;

Int m_maxFPS; ///< Maximum frames per second for rendering
Int m_logicTimeScaleFPS; ///< Maximum frames per second for logic time scale

Real m_updateTime; ///< Last update delta time in seconds

Bool m_enableFpsLimit;
Bool m_enableLogicTimeScale;
Bool m_isTimeFrozen;
Bool m_isGameHalted;
};

extern FramePacer* TheFramePacer;
4 changes: 2 additions & 2 deletions Core/GameEngine/Include/Common/FrameRateLimit.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ class FrameRateLimit
Real wait(UnsignedInt maxFps);

private:
LARGE_INTEGER m_freq;
LARGE_INTEGER m_start;
Int64 m_freq;
Int64 m_start;
};


Expand Down
207 changes: 207 additions & 0 deletions Core/GameEngine/Source/Common/FramePacer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
/*
** Command & Conquer Generals Zero Hour(tm)
** Copyright 2025 Electronic Arts Inc.
**
** 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/FramePacer.h"

#include "GameClient/View.h"

#include "GameLogic/GameLogic.h"
#include "GameLogic/ScriptEngine.h"

#include "GameNetwork/NetworkDefs.h"
#include "GameNetwork/NetworkInterface.h"


FramePacer* TheFramePacer = NULL;

FramePacer::FramePacer()
{
// Set the time slice size to 1 ms.
timeBeginPeriod(1);

m_maxFPS = BaseFps;
m_logicTimeScaleFPS = LOGICFRAMES_PER_SECOND;
m_updateTime = 1.0f / (Real)BaseFps; // initialized to something to avoid division by zero on first use
m_enableFpsLimit = FALSE;
m_enableLogicTimeScale = FALSE;
m_isTimeFrozen = FALSE;
m_isGameHalted = FALSE;
}

FramePacer::~FramePacer()
{
// Restore the previous time slice for Windows.
timeEndPeriod(1);
}

void FramePacer::update()
{
// TheSuperHackers @bugfix xezon 05/08/2025 Re-implements the frame rate limiter
// with higher resolution counters to cap the frame rate more accurately to the desired limit.
const UnsignedInt maxFps = getActualFramesPerSecondLimit();// allowFpsLimit ? getFramesPerSecondLimit() : RenderFpsPreset::UncappedFpsValue;
m_updateTime = m_frameRateLimit.wait(maxFps);
}

void FramePacer::setFramesPerSecondLimit( Int fps )
{
DEBUG_LOG(("FramePacer::setFramesPerSecondLimit() - setting max fps to %d (TheGlobalData->m_useFpsLimit == %d)", fps, TheGlobalData->m_useFpsLimit));
m_maxFPS = fps;
}

Int FramePacer::getFramesPerSecondLimit() const
{
return m_maxFPS;
}

void FramePacer::enableFramesPerSecondLimit( Bool enable )
{
m_enableFpsLimit = enable;
}

Bool FramePacer::isFramesPerSecondLimitEnabled() const
{
return m_enableFpsLimit;
}

Bool FramePacer::isActualFramesPerSecondLimitEnabled() const
{
Bool allowFpsLimit = true;

if (TheTacticalView != NULL)
{
allowFpsLimit &= TheTacticalView->getTimeMultiplier()<=1 && !TheScriptEngine->isTimeFast();
}

if (TheGameLogic != NULL)
{
#if defined(_ALLOW_DEBUG_CHEATS_IN_RELEASE)
allowFpsLimit &= !(!TheGameLogic->isGamePaused() && TheGlobalData->m_TiVOFastMode);
#else //always allow this cheat key if we're in a replay game.
allowFpsLimit &= !(!TheGameLogic->isGamePaused() && TheGlobalData->m_TiVOFastMode && TheGameLogic->isInReplayGame());
#endif
}

allowFpsLimit &= TheGlobalData->m_useFpsLimit;
allowFpsLimit &= isFramesPerSecondLimitEnabled();

return allowFpsLimit;
}

Int FramePacer::getActualFramesPerSecondLimit() const
{
return isActualFramesPerSecondLimitEnabled() ? getFramesPerSecondLimit() : RenderFpsPreset::UncappedFpsValue;
}

Real FramePacer::getUpdateTime() const
{
return m_updateTime;
}

Real FramePacer::getUpdateFps() const
{
return 1.0f / m_updateTime;
}

void FramePacer::setTimeFrozen(Bool frozen)
{
m_isTimeFrozen = frozen;
}

void FramePacer::setGameHalted(Bool halted)
{
m_isGameHalted = halted;
}

Bool FramePacer::isTimeFrozen() const
{
return m_isTimeFrozen;
}

Bool FramePacer::isGameHalted() const
{
return m_isGameHalted;
}

void FramePacer::setLogicTimeScaleFps( Int fps )
{
m_logicTimeScaleFPS = fps;
}

Int FramePacer::getLogicTimeScaleFps() const
{
return m_logicTimeScaleFPS;
}

void FramePacer::enableLogicTimeScale( Bool enable )
{
m_enableLogicTimeScale = enable;
}

Bool FramePacer::isLogicTimeScaleEnabled() const
{
return m_enableLogicTimeScale;
}

Int FramePacer::getActualLogicTimeScaleFps(LogicTimeQueryFlags flags) const
{
if (m_isTimeFrozen && (flags & IgnoreFrozenTime) == 0)
{
return 0;
}

if (m_isGameHalted && (flags & IgnoreHaltedGame) == 0)
{
return 0;
}

if (TheNetwork != NULL)
{
return TheNetwork->getFrameRate();
}

if (isLogicTimeScaleEnabled())
{
return getLogicTimeScaleFps();
}

// Returns uncapped value to align with the render update as per the original game behavior.
return RenderFpsPreset::UncappedFpsValue;
}

Real FramePacer::getActualLogicTimeScaleRatio(LogicTimeQueryFlags flags) const
{
return (Real)getActualLogicTimeScaleFps(flags) / LOGICFRAMES_PER_SECONDS_REAL;
}

Real FramePacer::getActualLogicTimeScaleOverFpsRatio(LogicTimeQueryFlags flags) const
{
// TheSuperHackers @info Clamps ratio to min 1, because the logic
// frame rate is currently capped by the render frame rate.
return min(1.0f, (Real)getActualLogicTimeScaleFps(flags) / getUpdateFps());
}

Real FramePacer::getLogicTimeStepSeconds(LogicTimeQueryFlags flags) const
{
return SECONDS_PER_LOGICFRAME_REAL * getActualLogicTimeScaleOverFpsRatio(flags);
}

Real FramePacer::getLogicTimeStepMilliseconds(LogicTimeQueryFlags flags) const
{
return MSEC_PER_LOGICFRAME_REAL * getActualLogicTimeScaleOverFpsRatio(flags);
}
14 changes: 9 additions & 5 deletions Core/GameEngine/Source/Common/FrameRateLimit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,19 @@

FrameRateLimit::FrameRateLimit()
{
QueryPerformanceFrequency(&m_freq);
QueryPerformanceCounter(&m_start);
LARGE_INTEGER freq;
LARGE_INTEGER start;
QueryPerformanceFrequency(&freq);
QueryPerformanceCounter(&start);
m_freq = freq.QuadPart;
m_start = start.QuadPart;
}

Real FrameRateLimit::wait(UnsignedInt maxFps)
{
LARGE_INTEGER tick;
QueryPerformanceCounter(&tick);
double elapsedSeconds = static_cast<double>(tick.QuadPart - m_start.QuadPart) / m_freq.QuadPart;
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 @@ -45,11 +49,11 @@ Real FrameRateLimit::wait(UnsignedInt maxFps)
do
{
QueryPerformanceCounter(&tick);
elapsedSeconds = static_cast<double>(tick.QuadPart - m_start.QuadPart) / m_freq.QuadPart;
elapsedSeconds = static_cast<double>(tick.QuadPart - m_start) / m_freq;
}
while (elapsedSeconds < targetSeconds);

m_start = tick;
m_start = tick.QuadPart;
return (Real)elapsedSeconds;
}

Expand Down
3 changes: 2 additions & 1 deletion Core/Libraries/Source/WWVegas/WWLib/WWCommon.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ enum
{
// TheSuperHackers @info The original WWSync was 33 ms, ~30 fps, integer.
// Changing this will require tweaking all Drawable code that concerns the ww3d time step, including locomotion physics.
WWSyncPerSecond = 30
WWSyncPerSecond = 30,
WWSyncMilliseconds = 1000 / WWSyncPerSecond,
};

#if defined(_MSC_VER) && _MSC_VER < 1300
Expand Down
16 changes: 11 additions & 5 deletions Core/Tools/W3DView/GraphicView.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,8 @@ CGraphicView::RepaintView
// Simple check to avoid re-entrance
//
static bool _already_painting = false;
if (_already_painting) return;
if (_already_painting)
return;
_already_painting = true;

//
Expand All @@ -452,10 +453,15 @@ CGraphicView::RepaintView
m_dwLastFrameUpdate = cur_ticks;

// Update the W3D frame times according to our elapsed tick count
if (ticks_to_use == 0) {
WW3D::Sync (WW3D::Get_Sync_Time() + (ticks_elapsed * m_animationSpeed));
} else {
WW3D::Sync (WW3D::Get_Sync_Time() + ticks_to_use);
if (ticks_to_use == 0)
{
WW3D::Update_Logic_Frame_Time(ticks_elapsed * m_animationSpeed);
WW3D::Sync(WW3D::Get_Fractional_Sync_Milliseconds() >= WWSyncMilliseconds);
}
else
{
WW3D::Update_Logic_Frame_Time(ticks_to_use);
WW3D::Sync(true);
}

// Do we need to update the current animation?
Expand Down
Loading
Loading