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
1 change: 1 addition & 0 deletions Core/GameEngine/Include/GameClient/Display.h
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ class Display : public SubsystemInterface
virtual void setCinematicTextFrames( Int frames ) { m_cinematicTextFrames = frames; }

virtual Real getAverageFPS() = 0; ///< returns the average FPS.
virtual Real getLow1PercentFPS() = 0; ///< returns the 1% low FPS.
virtual Real getCurrentFPS() = 0; ///< returns the current FPS.
virtual Int getLastFrameDrawCalls() = 0; ///< returns the number of draw calls issued in the previous frame

Expand Down
3 changes: 3 additions & 0 deletions Generals/Code/GameEngine/Include/GameClient/InGameUI.h
Original file line number Diff line number Diff line change
Expand Up @@ -747,16 +747,19 @@ friend class Drawable; // for selection/deselection transactions

// Render FPS Counter
DisplayString * m_renderFpsString;
DisplayString * m_renderFpsLowString;
DisplayString * m_renderFpsLimitString;
AsciiString m_renderFpsFont;
Int m_renderFpsPointSize;
Bool m_renderFpsBold;
Coord2D m_renderFpsPosition;
Color m_renderFpsColor;
Color m_renderFpsLowColor;
Color m_renderFpsLimitColor;
Color m_renderFpsDropColor;
UnsignedInt m_renderFpsRefreshMs;
UnsignedInt m_lastRenderFps;
UnsignedInt m_lastRenderFpsLow;
UnsignedInt m_lastRenderFpsLimit;
UnsignedInt m_lastRenderFpsUpdateMs;

Expand Down
34 changes: 31 additions & 3 deletions Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,7 @@ const FieldParse InGameUI::s_fieldParseTable[] =
{ "RenderFpsBold", INI::parseBool, nullptr, offsetof( InGameUI, m_renderFpsBold ) },
{ "RenderFpsPosition", INI::parseCoord2D, nullptr, offsetof( InGameUI, m_renderFpsPosition ) },
{ "RenderFpsColor", INI::parseColorInt, nullptr, offsetof( InGameUI, m_renderFpsColor ) },
{ "RenderFpsLowColor", INI::parseColorInt, nullptr, offsetof( InGameUI, m_renderFpsLowColor ) },
{ "RenderFpsLimitColor", INI::parseColorInt, nullptr, offsetof( InGameUI, m_renderFpsLimitColor ) },
{ "RenderFpsDropColor", INI::parseColorInt, nullptr, offsetof( InGameUI, m_renderFpsDropColor ) },
{ "RenderFpsRefreshMs", INI::parseUnsignedInt, nullptr, offsetof( InGameUI, m_renderFpsRefreshMs ) },
Expand Down Expand Up @@ -1133,17 +1134,20 @@ InGameUI::InGameUI()
m_lastNetworkLatencyFrames = ~0u;

m_renderFpsString = nullptr;
m_renderFpsLowString = nullptr;
m_renderFpsLimitString = nullptr;
m_renderFpsFont = "Tahoma";
m_renderFpsPointSize = TheGlobalData->m_renderFpsFontSize;
m_renderFpsBold = TRUE;
m_renderFpsPosition.x = kHudAnchorX;
m_renderFpsPosition.y = kHudAnchorY;
m_renderFpsColor = GameMakeColor( 255, 255, 0, 255 );
m_renderFpsLowColor = GameMakeColor( 180, 170, 120, 255 );
m_renderFpsLimitColor = GameMakeColor(119, 119, 119, 255);
m_renderFpsDropColor = GameMakeColor( 0, 0, 0, 255 );
m_renderFpsRefreshMs = 1000;
m_lastRenderFps = ~0u;
m_lastRenderFpsLow = ~0u;
m_lastRenderFpsLimit = ~0u;
m_lastRenderFpsUpdateMs = 0u;

Expand Down Expand Up @@ -2215,6 +2219,8 @@ void InGameUI::freeCustomUiResources()
m_networkLatencyString = nullptr;
TheDisplayStringManager->freeDisplayString(m_renderFpsString);
m_renderFpsString = nullptr;
TheDisplayStringManager->freeDisplayString(m_renderFpsLowString);
m_renderFpsLowString = nullptr;
TheDisplayStringManager->freeDisplayString(m_renderFpsLimitString);
m_renderFpsLimitString = nullptr;
TheDisplayStringManager->freeDisplayString(m_systemTimeString);
Expand Down Expand Up @@ -5868,6 +5874,12 @@ void InGameUI::refreshRenderFpsResources()
m_lastRenderFpsUpdateMs = 0u;
}

if (!m_renderFpsLowString)
{
m_renderFpsLowString = TheDisplayStringManager->newDisplayString();
m_lastRenderFpsLow = ~0u;
}

if (!m_renderFpsLimitString)
{
m_renderFpsLimitString = TheDisplayStringManager->newDisplayString();
Expand All @@ -5878,6 +5890,7 @@ void InGameUI::refreshRenderFpsResources()
Int adjustedRenderFpsFontSize = TheGlobalLanguageData->adjustFontSize(m_renderFpsPointSize);
GameFont *fpsFont = TheWindowManager->winFindFont(m_renderFpsFont, adjustedRenderFpsFontSize, m_renderFpsBold);
m_renderFpsString->setFont(fpsFont);
m_renderFpsLowString->setFont(fpsFont);
m_renderFpsLimitString->setFont(fpsFont);

if (m_renderFpsPointSize > 0)
Expand Down Expand Up @@ -5990,6 +6003,15 @@ void InGameUI::updateRenderFpsString()
m_renderFpsString->setText(fpsStr);
m_lastRenderFps = renderFps;
}

const UnsignedInt renderFpsLow = (UnsignedInt)(TheDisplay->getLow1PercentFPS() + 0.5f);
if (renderFpsLow != m_lastRenderFpsLow)
{
UnicodeString lowStr;
lowStr.format(L"(%u)", renderFpsLow);
m_renderFpsLowString->setText(lowStr);
m_lastRenderFpsLow = renderFpsLow;
}
}

void InGameUI::drawNetworkLatency(Int &x, Int &y)
Expand Down Expand Up @@ -6056,14 +6078,20 @@ void InGameUI::drawRenderFps(Int &x, Int &y)
const Int drawY = kHudAnchorY + y;

m_renderFpsString->draw(kHudAnchorX + x, drawY, m_renderFpsColor, m_renderFpsDropColor);
x += m_renderFpsString->getWidth();
x += m_renderFpsString->getWidth() + kHudGapPx / 2;
m_renderFpsLowString->draw(kHudAnchorX + x, drawY, m_renderFpsLowColor, m_renderFpsDropColor);
x += m_renderFpsLowString->getWidth() + kHudGapPx / 2;
m_renderFpsLimitString->draw(kHudAnchorX + x, drawY, m_renderFpsLimitColor, m_renderFpsDropColor);
x += m_renderFpsLimitString->getWidth() + kHudGapPx;
}
else
{
m_renderFpsString->draw(m_renderFpsPosition.x, m_renderFpsPosition.y, m_renderFpsColor, m_renderFpsDropColor);
m_renderFpsLimitString->draw(m_renderFpsPosition.x + m_renderFpsString->getWidth(), m_renderFpsPosition.y, m_renderFpsLimitColor, m_renderFpsDropColor);
Int currentX = m_renderFpsPosition.x;
m_renderFpsString->draw(currentX, m_renderFpsPosition.y, m_renderFpsColor, m_renderFpsDropColor);
currentX += m_renderFpsString->getWidth() + kHudGapPx / 2;
m_renderFpsLowString->draw(currentX, m_renderFpsPosition.y, m_renderFpsLowColor, m_renderFpsDropColor);
currentX += m_renderFpsLowString->getWidth() + kHudGapPx / 2;
m_renderFpsLimitString->draw(currentX, m_renderFpsPosition.y, m_renderFpsLimitColor, m_renderFpsDropColor);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ class W3DDisplay : public Display

void drawFPSStats(); ///< draw the fps on the screen
virtual Real getAverageFPS() override; ///< return the average FPS.
virtual Real getLow1PercentFPS() override; ///< return the 1% low FPS.
virtual Real getCurrentFPS() override; ///< return the current FPS.
virtual Int getLastFrameDrawCalls() override; ///< returns the number of draw calls issued in the previous frame

Expand All @@ -161,7 +162,10 @@ class W3DDisplay : public Display
void drawCurrentDebugDisplay(); ///< draws current debug display
void calculateTerrainLOD(); ///< Calculate terrain LOD.
void renderLetterBox(UnsignedInt time); ///< draw letter box border
void updateAverageFPS(); ///< calculate the average fps over the last 30 frames.
void updatePerformanceMetrics(); ///< update the average and 1% low fps metrics.
void addFpsSample(Real elapsedSeconds); ///< add a new sample to the history buffer.
Real calculateAverageFPS(Real windowSeconds); ///< calculate average FPS over a time window.
Real calculateLow1PercentFPS(Real windowSeconds); ///< calculate 1% low FPS over a time window.
void setup2DRenderState(TextureClass *tex, DrawImageMode mode, Bool grayscale);
virtual void onBeginBatch() override;
virtual void onEndBatch() override;
Expand All @@ -172,9 +176,18 @@ class W3DDisplay : public Display
Render2DClass *m_2DRender; ///< interface for common 2D functions
IRegion2D m_clipRegion; ///< the clipping region for images
Bool m_isClippedEnabled; ///<used by 2D drawing operations to define clip re
Real m_averageFPS; ///<average fps over the last 30 frames.
Real m_averageFPS; ///< average fps over the last 0.5s.
Real m_low1PercentFPS; ///<1% low fps.
Real m_currentFPS; ///<current fps value.

enum { FPS_HISTORY_SIZE = 5000 }; // covers 5s at 1000 FPS, degrades gracefully beyond
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this number needs evaluation but I would like input from others.

As it stands, 5000 samples in three Real arrays require 60 kB in memory - just for the FPS trackers.
I think the question should be: given 3 seconds timeframe for the 1% low FPS, At what 3-second average FPS is the low fps no longer relevant. Say you average 300 fps, what is the chance that the 1% is so low that it is still relevant as a performance metric. If 300 fps is the upper bound, only 900 samples need to be stored. That's only 18% of the memory needed compared to the current setting.

Copy link
Copy Markdown
Author

@githubawn githubawn May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Say you average 300 fps, what is the chance that the 1% is so low that it is still relevant as a performance metric.

It's pretty common to see 300 average fps and 30 fps lows in this game in large skirmish matches even on vs2022 non-retail.

But definitely would like to hear multiple inputs for this number.

Real m_fpsHistory[FPS_HISTORY_SIZE];
Real m_durationHistory[FPS_HISTORY_SIZE];
Real m_sortBuffer[FPS_HISTORY_SIZE];
Int m_historyOffset;
Int m_historyCount;
Int64 m_lastUpdateTime64;

TextureClass *m_batchTexture;
DrawImageMode m_batchMode;
Bool m_batchGrayscale;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ W3DDisplay::W3DDisplay()
m_2DScene = nullptr;
m_3DInterfaceScene = nullptr;
m_averageFPS = TheGlobalData->m_framesPerSecondLimit;
m_low1PercentFPS = TheGlobalData->m_framesPerSecondLimit;
#if defined(RTS_DEBUG)
m_timerAtCumuFPSStart = 0;
#endif
Expand All @@ -360,6 +361,15 @@ W3DDisplay::W3DDisplay()
m_batchGrayscale = FALSE;
m_batchNeedsInit = FALSE;

m_historyOffset = 0;
m_historyCount = 0;
m_lastUpdateTime64 = 0;
for (Int h = 0; h < FPS_HISTORY_SIZE; ++h)
{
m_fpsHistory[h] = 0.0f;
m_durationHistory[h] = 0.0f;
}

#ifdef PROFILER_ENABLED
m_profilerFrameCapture = NEW W3DProfilerFrameCapture();
#endif
Expand Down Expand Up @@ -958,14 +968,92 @@ void W3DDisplay::reset()

const UnsignedInt START_CUMU_FRAME = LOGICFRAMES_PER_SECOND / 2; // skip first half-sec

void W3DDisplay::updateAverageFPS()
void W3DDisplay::addFpsSample(Real elapsedSeconds)
{
constexpr const Int FPS_HISTORY_SIZE = 30;
if (elapsedSeconds <= 0.0f)
{
return;
}

m_currentFPS = 1.0f / elapsedSeconds;
m_fpsHistory[m_historyOffset] = m_currentFPS;
m_durationHistory[m_historyOffset] = elapsedSeconds;

m_historyOffset = (m_historyOffset + 1) % FPS_HISTORY_SIZE;
if (m_historyCount < FPS_HISTORY_SIZE)
{
m_historyCount++;
}
}

Real W3DDisplay::calculateAverageFPS(Real windowSeconds)
{
if (m_historyCount == 0)
{
return m_currentFPS;
}

Real timeSum = 0;
Int samples = 0;

for (Int i = 0; i < m_historyCount; ++i)
{
Int idx = (m_historyOffset - 1 - i + FPS_HISTORY_SIZE) % FPS_HISTORY_SIZE;
timeSum += m_durationHistory[idx];
samples++;

if (timeSum >= windowSeconds)
{
break;
}
}

return (timeSum > 0) ? ((Real)samples / timeSum) : m_currentFPS;
}

Real W3DDisplay::calculateLow1PercentFPS(Real windowSeconds)
{
if (m_historyCount == 0)
{
return m_currentFPS;
}

Real timeSum = 0;
Int sampleCount = 0;
Int i;

static Int64 lastUpdateTime64 = 0;
static Int historyOffset = 0;
static Real fpsHistory[FPS_HISTORY_SIZE] = {0};
for (i = 0; i < m_historyCount; ++i)
{
Int idx = (m_historyOffset - 1 - i + FPS_HISTORY_SIZE) % FPS_HISTORY_SIZE;
timeSum += m_durationHistory[idx];
m_sortBuffer[sampleCount++] = m_fpsHistory[idx];

if (timeSum >= windowSeconds)
{
break;
}
}

if (sampleCount == 0)
{
return m_currentFPS;
}

const Int bottomSampleCount = std::max((sampleCount + 50) / 100, 1);

std::nth_element(m_sortBuffer, m_sortBuffer + bottomSampleCount, m_sortBuffer + sampleCount);

Real lowSum = 0;
for (i = 0; i < bottomSampleCount; ++i)
{
lowSum += m_sortBuffer[i];
}

return lowSum / (Real)bottomSampleCount;
}

void W3DDisplay::updatePerformanceMetrics()
{
const Int64 freq64 = getPerformanceCounterFrequency();
const Int64 time64 = getPerformanceCounter();

Expand All @@ -976,23 +1064,27 @@ void W3DDisplay::updateAverageFPS()
}
#endif

const Int64 timeDiff = time64 - lastUpdateTime64;

// convert elapsed time to seconds
Real elapsedSeconds = (Real)timeDiff/(Real)freq64;
if (m_lastUpdateTime64 == 0)
{
m_lastUpdateTime64 = time64;
return;
}

// append new sample to fps history.
if (historyOffset >= FPS_HISTORY_SIZE)
historyOffset = 0;
const Int64 timeDiff = time64 - m_lastUpdateTime64;
Real elapsedSeconds = (Real)timeDiff / (Real)freq64;

m_currentFPS = 1.0f/elapsedSeconds;
fpsHistory[historyOffset++] = m_currentFPS;
addFpsSample(elapsedSeconds);
m_averageFPS = calculateAverageFPS(0.5f);

// determine average frame rate over our past history.
const Real sum = std::accumulate(fpsHistory, fpsHistory + FPS_HISTORY_SIZE, 0.0f);
m_averageFPS = sum / FPS_HISTORY_SIZE;
static UnsignedInt lastLowUpdate = 0;
UnsignedInt now = timeGetTime();
if (now - lastLowUpdate >= 1000)
{
lastLowUpdate = now;
m_low1PercentFPS = calculateLow1PercentFPS(3.0f);
}

lastUpdateTime64 = time64;
m_lastUpdateTime64 = time64;
}

#if defined(RTS_DEBUG) //debug hack to view object under mouse stats
Expand Down Expand Up @@ -1693,6 +1785,11 @@ Real W3DDisplay::getAverageFPS()
return m_averageFPS;
}

Real W3DDisplay::getLow1PercentFPS()
{
return m_low1PercentFPS;
}

Real W3DDisplay::getCurrentFPS()
{
return m_currentFPS;
Expand Down Expand Up @@ -1727,7 +1824,7 @@ void W3DDisplay::draw()
if (TheGlobalData->m_headless)
return;

updateAverageFPS();
updatePerformanceMetrics();
if (TheGlobalData->m_enableDynamicLOD && TheGameLogic->getShowDynamicLOD())
{
DynamicGameLODLevel lod=TheGameLODManager->findDynamicLODLevel(m_averageFPS);
Expand Down
1 change: 1 addition & 0 deletions Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ class GUIEditDisplay : public Display
#endif

virtual Real getAverageFPS() override { return 0; }
virtual Real getLow1PercentFPS() override { return 0; }
virtual Real getCurrentFPS() override { return 0; }
virtual Int getLastFrameDrawCalls() override { return 0; }

Expand Down
3 changes: 3 additions & 0 deletions GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h
Original file line number Diff line number Diff line change
Expand Up @@ -769,16 +769,19 @@ friend class Drawable; // for selection/deselection transactions

// Render FPS Counter
DisplayString * m_renderFpsString;
DisplayString * m_renderFpsLowString;
DisplayString * m_renderFpsLimitString;
AsciiString m_renderFpsFont;
Int m_renderFpsPointSize;
Bool m_renderFpsBold;
Coord2D m_renderFpsPosition;
Color m_renderFpsColor;
Color m_renderFpsLowColor;
Color m_renderFpsLimitColor;
Color m_renderFpsDropColor;
UnsignedInt m_renderFpsRefreshMs;
UnsignedInt m_lastRenderFps;
UnsignedInt m_lastRenderFpsLow;
UnsignedInt m_lastRenderFpsLimit;
UnsignedInt m_lastRenderFpsUpdateMs;

Expand Down
Loading
Loading