Skip to content

Commit ca8e556

Browse files
committed
bugfix(fps): Fix inaccurate frame rate cap (#1451)
1 parent 67f50d1 commit ca8e556

File tree

9 files changed

+121
-74
lines changed

9 files changed

+121
-74
lines changed

Core/GameEngine/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ set(GAMEENGINE_SRC
3939
# Include/Common/Errors.h
4040
Include/Common/file.h
4141
Include/Common/FileSystem.h
42+
Include/Common/FrameRateLimit.h
4243
# Include/Common/FunctionLexicon.h
4344
Include/Common/GameAudio.h
4445
# Include/Common/GameCommon.h
@@ -569,6 +570,7 @@ set(GAMEENGINE_SRC
569570
# Source/Common/DamageFX.cpp
570571
# Source/Common/Dict.cpp
571572
# Source/Common/DiscreteCircle.cpp
573+
Source/Common/FrameRateLimit.cpp
572574
# Source/Common/GameEngine.cpp
573575
# Source/Common/GameLOD.cpp
574576
# Source/Common/GameMain.cpp
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
** Command & Conquer Generals Zero Hour(tm)
3+
** Copyright 2025 TheSuperHackers
4+
**
5+
** This program is free software: you can redistribute it and/or modify
6+
** it under the terms of the GNU General Public License as published by
7+
** the Free Software Foundation, either version 3 of the License, or
8+
** (at your option) any later version.
9+
**
10+
** This program is distributed in the hope that it will be useful,
11+
** but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
** GNU General Public License for more details.
14+
**
15+
** You should have received a copy of the GNU General Public License
16+
** along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
#pragma once
20+
21+
class FrameRateLimit
22+
{
23+
public:
24+
FrameRateLimit();
25+
26+
Real wait(UnsignedInt maxFps);
27+
28+
private:
29+
LARGE_INTEGER m_freq;
30+
LARGE_INTEGER m_start;
31+
};
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
** Command & Conquer Generals Zero Hour(tm)
3+
** Copyright 2025 TheSuperHackers
4+
**
5+
** This program is free software: you can redistribute it and/or modify
6+
** it under the terms of the GNU General Public License as published by
7+
** the Free Software Foundation, either version 3 of the License, or
8+
** (at your option) any later version.
9+
**
10+
** This program is distributed in the hope that it will be useful,
11+
** but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
** GNU General Public License for more details.
14+
**
15+
** You should have received a copy of the GNU General Public License
16+
** along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
#include "PreRTS.h"
20+
#include "Common/FrameRateLimit.h"
21+
22+
23+
FrameRateLimit::FrameRateLimit()
24+
{
25+
QueryPerformanceFrequency(&m_freq);
26+
QueryPerformanceCounter(&m_start);
27+
}
28+
29+
Real FrameRateLimit::wait(UnsignedInt maxFps)
30+
{
31+
LARGE_INTEGER tick;
32+
QueryPerformanceCounter(&tick);
33+
double elapsedSeconds = static_cast<double>(tick.QuadPart - m_start.QuadPart) / m_freq.QuadPart;
34+
const double targetSeconds = 1.0 / maxFps;
35+
const double sleepSeconds = targetSeconds - elapsedSeconds - 0.002; // leave ~2ms for spin wait
36+
37+
if (sleepSeconds > 0.0)
38+
{
39+
// Non busy wait with Munkee sleep
40+
DWORD dwMilliseconds = static_cast<DWORD>(sleepSeconds * 1000);
41+
Sleep(dwMilliseconds);
42+
}
43+
44+
// Busy wait for remaining time
45+
do
46+
{
47+
QueryPerformanceCounter(&tick);
48+
elapsedSeconds = static_cast<double>(tick.QuadPart - m_start.QuadPart) / m_freq.QuadPart;
49+
}
50+
while (elapsedSeconds < targetSeconds);
51+
52+
m_start = tick;
53+
return (Real)elapsedSeconds;
54+
}

Generals/Code/GameEngine/Source/Common/GameEngine.cpp

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
#include "Common/ThingFactory.h"
4545
#include "Common/file.h"
4646
#include "Common/FileSystem.h"
47+
#include "Common/FrameRateLimit.h"
4748
#include "Common/ArchiveFileSystem.h"
4849
#include "Common/LocalFileSystem.h"
4950
#include "Common/CDManager.h"
@@ -668,8 +669,8 @@ extern HWND ApplicationHWnd;
668669
*/
669670
void GameEngine::execute( void )
670671
{
672+
FrameRateLimit* frameRateLimit = new FrameRateLimit();
671673

672-
DWORD prevTime = timeGetTime();
673674
#if defined(RTS_DEBUG)
674675
DWORD startTime = timeGetTime() / 1000;
675676
#endif
@@ -743,35 +744,28 @@ void GameEngine::execute( void )
743744
if (TheTacticalView->getTimeMultiplier()<=1 && !TheScriptEngine->isTimeFast())
744745
{
745746

746-
// I'm disabling this in internal because many people need alt-tab capability. If you happen to be
747+
// I'm disabling this in debug because many people need alt-tab capability. If you happen to be
747748
// doing performance tuning, please just change this on your local system. -MDC
748749
#if defined(RTS_DEBUG)
749750
::Sleep(1); // give everyone else a tiny time slice.
750751
#endif
751752

752753

753754
#if defined(_ALLOW_DEBUG_CHEATS_IN_RELEASE)
754-
if ( ! TheGlobalData->m_TiVOFastMode )
755-
#else //always allow this cheatkey if we're in a replaygame.
756-
if ( ! (TheGlobalData->m_TiVOFastMode && TheGameLogic->isInReplayGame()))
755+
if ( ! TheGlobalData->m_TiVOFastMode )
756+
#else //always allow this cheat key if we're in a replay game.
757+
if ( ! (TheGlobalData->m_TiVOFastMode && TheGameLogic->isInReplayGame()))
757758
#endif
758-
{
759-
// limit the framerate
760-
DWORD now = timeGetTime();
761-
DWORD limit = (1000.0f/m_maxFPS)-1;
762-
while (TheGlobalData->m_useFpsLimit && (now - prevTime) < limit)
763-
{
764-
::Sleep(0);
765-
now = timeGetTime();
766-
}
767-
//Int slept = now - prevTime;
768-
//DEBUG_LOG(("delayed %d",slept));
769-
770-
prevTime = now;
771-
772-
}
773-
774-
}
759+
{
760+
// TheSuperHackers @bugfix xezon 05/08/2025 Re-implements the frame rate limiter
761+
// with higher resolution counters to cap the frame rate more accurately to the desired limit.
762+
if (TheGlobalData->m_useFpsLimit)
763+
{
764+
frameRateLimit->wait(m_maxFPS);
765+
}
766+
}
767+
768+
}
775769
}
776770

777771
} // perfgather for execute_loop
@@ -787,6 +781,7 @@ void GameEngine::execute( void )
787781

788782
}
789783

784+
delete frameRateLimit;
790785
}
791786

792787
/** -----------------------------------------------------------------------------------------------

Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1766,19 +1766,6 @@ void W3DDisplay::draw( void )
17661766

17671767
do {
17681768

1769-
{
1770-
if(TheGlobalData->m_loadScreenRender != TRUE)
1771-
{
1772-
1773-
// limit the framerate
1774-
while(TheGlobalData->m_useFpsLimit && (now - prevTime) < minTime-1)
1775-
{
1776-
now = timeGetTime();
1777-
}
1778-
prevTime = now;
1779-
}
1780-
}
1781-
17821769
// update all views of the world - recomputes data which will affect drawing
17831770
if (DX8Wrapper::_Get_D3D_Device8() && (DX8Wrapper::_Get_D3D_Device8()->TestCooperativeLevel()) == D3D_OK)
17841771
{ //Checking if we have the device before updating views because the heightmap crashes otherwise while

Generals/Code/Tools/GUIEdit/Source/GUIEdit.cpp

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,6 @@
7777

7878
#include "W3DDevice/Common/W3DFunctionLexicon.h"
7979
#include "W3DDevice/GameClient/W3DGameWindowManager.h"
80-
#include "W3DDevice/GameClient/W3DDisplay.h"
81-
#include "W3DDevice/GameClient/W3DGameWindowManager.h"
8280
#include "W3DDevice/GameClient/W3DGameFont.h"
8381
#include "W3DDevice/GameClient/W3DDisplayStringManager.h"
8482
#include "GameClient/Keyboard.h"

GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
#include "Common/ThingFactory.h"
4545
#include "Common/file.h"
4646
#include "Common/FileSystem.h"
47+
#include "Common/FrameRateLimit.h"
4748
#include "Common/ArchiveFileSystem.h"
4849
#include "Common/LocalFileSystem.h"
4950
#include "Common/CDManager.h"
@@ -842,8 +843,8 @@ extern HWND ApplicationHWnd;
842843
*/
843844
void GameEngine::execute( void )
844845
{
846+
FrameRateLimit* frameRateLimit = new FrameRateLimit();
845847

846-
DWORD prevTime = timeGetTime();
847848
#if defined(RTS_DEBUG)
848849
DWORD startTime = timeGetTime() / 1000;
849850
#endif
@@ -917,35 +918,28 @@ void GameEngine::execute( void )
917918
if (TheTacticalView->getTimeMultiplier()<=1 && !TheScriptEngine->isTimeFast())
918919
{
919920

920-
// I'm disabling this in internal because many people need alt-tab capability. If you happen to be
921+
// I'm disabling this in debug because many people need alt-tab capability. If you happen to be
921922
// doing performance tuning, please just change this on your local system. -MDC
922923
#if defined(RTS_DEBUG)
923924
::Sleep(1); // give everyone else a tiny time slice.
924925
#endif
925926

926927

927928
#if defined(_ALLOW_DEBUG_CHEATS_IN_RELEASE)
928-
if ( ! TheGlobalData->m_TiVOFastMode )
929-
#else //always allow this cheatkey if we're in a replaygame.
930-
if ( ! (TheGlobalData->m_TiVOFastMode && TheGameLogic->isInReplayGame()))
929+
if ( ! TheGlobalData->m_TiVOFastMode )
930+
#else //always allow this cheat key if we're in a replay game.
931+
if ( ! (TheGlobalData->m_TiVOFastMode && TheGameLogic->isInReplayGame()))
931932
#endif
932-
{
933-
// limit the framerate
934-
DWORD now = timeGetTime();
935-
DWORD limit = (1000.0f/m_maxFPS)-1;
936-
while (TheGlobalData->m_useFpsLimit && (now - prevTime) < limit)
937-
{
938-
::Sleep(0);
939-
now = timeGetTime();
940-
}
941-
//Int slept = now - prevTime;
942-
//DEBUG_LOG(("delayed %d",slept));
943-
944-
prevTime = now;
945-
946-
}
947-
948-
}
933+
{
934+
// TheSuperHackers @bugfix xezon 05/08/2025 Re-implements the frame rate limiter
935+
// with higher resolution counters to cap the frame rate more accurately to the desired limit.
936+
if (TheGlobalData->m_useFpsLimit)
937+
{
938+
frameRateLimit->wait(m_maxFPS);
939+
}
940+
}
941+
942+
}
949943
}
950944

951945
} // perfgather for execute_loop
@@ -961,6 +955,7 @@ void GameEngine::execute( void )
961955

962956
}
963957

958+
delete frameRateLimit;
964959
}
965960

966961
/** -----------------------------------------------------------------------------------------------

GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1851,19 +1851,6 @@ void W3DDisplay::draw( void )
18511851

18521852
do {
18531853

1854-
{
1855-
if(TheGlobalData->m_loadScreenRender != TRUE)
1856-
{
1857-
1858-
// limit the framerate
1859-
while(TheGlobalData->m_useFpsLimit && (now - prevTime) < minTime-1)
1860-
{
1861-
now = timeGetTime();
1862-
}
1863-
prevTime = now;
1864-
}
1865-
}
1866-
18671854
// update all views of the world - recomputes data which will affect drawing
18681855
if (DX8Wrapper::_Get_D3D_Device8() && (DX8Wrapper::_Get_D3D_Device8()->TestCooperativeLevel()) == D3D_OK)
18691856
{ //Checking if we have the device before updating views because the heightmap crashes otherwise while

GeneralsMD/Code/Tools/GUIEdit/Source/GUIEdit.cpp

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,6 @@
7777

7878
#include "W3DDevice/Common/W3DFunctionLexicon.h"
7979
#include "W3DDevice/GameClient/W3DGameWindowManager.h"
80-
#include "W3DDevice/GameClient/W3DDisplay.h"
81-
#include "W3DDevice/GameClient/W3DGameWindowManager.h"
8280
#include "W3DDevice/GameClient/W3DGameFont.h"
8381
#include "W3DDevice/GameClient/W3DDisplayStringManager.h"
8482
#include "GameClient/Keyboard.h"

0 commit comments

Comments
 (0)