diff --git a/Core/GameEngine/Include/GameClient/Display.h b/Core/GameEngine/Include/GameClient/Display.h index 9fc937bfaa2..df3ee681c61 100644 --- a/Core/GameEngine/Include/GameClient/Display.h +++ b/Core/GameEngine/Include/GameClient/Display.h @@ -193,6 +193,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 diff --git a/Generals/Code/GameEngine/Include/GameClient/InGameUI.h b/Generals/Code/GameEngine/Include/GameClient/InGameUI.h index 59e753c1d05..3e33b40cb04 100644 --- a/Generals/Code/GameEngine/Include/GameClient/InGameUI.h +++ b/Generals/Code/GameEngine/Include/GameClient/InGameUI.h @@ -755,16 +755,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; diff --git a/Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp b/Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp index c13b9f89b30..aa9992f11d3 100644 --- a/Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp @@ -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 ) }, @@ -1136,6 +1137,7 @@ InGameUI::InGameUI() m_lastNetworkLatencyFrames = ~0u; m_renderFpsString = nullptr; + m_renderFpsLowString = nullptr; m_renderFpsLimitString = nullptr; m_renderFpsFont = "Tahoma"; m_renderFpsPointSize = TheGlobalData->m_renderFpsFontSize; @@ -1143,10 +1145,12 @@ InGameUI::InGameUI() m_renderFpsPosition.x = kHudAnchorX; m_renderFpsPosition.y = kHudAnchorY; m_renderFpsColor = GameMakeColor( 255, 255, 0, 255 ); + m_renderFpsLowColor = GameMakeColor( 190, 180, 130, 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; @@ -2248,6 +2252,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); @@ -5912,6 +5918,12 @@ void InGameUI::refreshRenderFpsResources() m_lastRenderFpsUpdateMs = 0u; } + if (!m_renderFpsLowString) + { + m_renderFpsLowString = TheDisplayStringManager->newDisplayString(); + m_lastRenderFpsLow = ~0u; + } + if (!m_renderFpsLimitString) { m_renderFpsLimitString = TheDisplayStringManager->newDisplayString(); @@ -5922,6 +5934,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) @@ -6034,6 +6047,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"\x25BC%u", renderFpsLow); + m_renderFpsLowString->setText(lowStr); + m_lastRenderFpsLow = renderFpsLow; + } } void InGameUI::drawNetworkLatency(Int &x, Int &y) @@ -6077,19 +6099,22 @@ void InGameUI::drawRenderFps(Int &x, Int &y) updateRenderFpsString(); } - UnsignedInt renderFpsLimit = 0u; + UnsignedInt renderFpsLimit = RenderFpsPreset::UncappedFpsValue; if (TheGlobalData->m_useFpsLimit) { renderFpsLimit = (UnsignedInt)TheFramePacer->getFramesPerSecondLimit(); - if (renderFpsLimit == RenderFpsPreset::UncappedFpsValue) - { - renderFpsLimit = 0u; - } } if (renderFpsLimit != m_lastRenderFpsLimit) { UnicodeString fpsLimitStr; - fpsLimitStr.format(L"[%u]", renderFpsLimit); + if (renderFpsLimit == RenderFpsPreset::UncappedFpsValue) + { + fpsLimitStr.format(L"\x25B2X"); + } + else + { + fpsLimitStr.format(L"\x25B2%u", renderFpsLimit); + } m_renderFpsLimitString->setText(fpsLimitStr); m_lastRenderFpsLimit = renderFpsLimit; } @@ -6100,14 +6125,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); } } diff --git a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h index aa3f0b79d2c..fc03c8ae1c4 100644 --- a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h +++ b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h @@ -48,6 +48,40 @@ class RTS2DScene; class RTS3DInterfaceScene; class TextureClass; +constexpr const Real TICKS_PER_SECOND = 62500.0f; + +class QuantizedUnsignedShort +{ +public: + QuantizedUnsignedShort() : m_value(2083) {} + explicit QuantizedUnsignedShort(UnsignedShort val) : m_value(val) {} + + static QuantizedUnsignedShort fromSeconds(Real seconds) + { + Int64 ticks = (Int64)(seconds * TICKS_PER_SECOND + 0.5f); + if (ticks >= 65535) + { + return QuantizedUnsignedShort(65535); + } + if (ticks <= 0) + { + return QuantizedUnsignedShort(1); + } + return QuantizedUnsignedShort((UnsignedShort)ticks); + } + + UnsignedShort getValue() const { return m_value; } + void setValue(UnsignedShort val) { m_value = val; } + + Real toFPS() const { return TICKS_PER_SECOND / (Real)m_value; } + Real toSeconds() const { return (Real)m_value / TICKS_PER_SECOND; } + + operator UnsignedShort() const { return m_value; } + +private: + UnsignedShort m_value; +}; + //============================================================================= /** W3D implementation of the game display which is responsible for creating @@ -148,6 +182,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 @@ -161,7 +196,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; @@ -172,9 +210,17 @@ class W3DDisplay : public Display Render2DClass *m_2DRender; ///< interface for common 2D functions IRegion2D m_clipRegion; ///< the clipping region for images Bool m_isClippedEnabled; /// +#include +#include #include #include #include @@ -341,6 +343,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 @@ -361,6 +364,13 @@ W3DDisplay::W3DDisplay() m_batchGrayscale = FALSE; m_batchNeedsInit = FALSE; + m_historyOffset = 0; + m_historyCount = 1; + m_lastUpdateTime64 = 0; + m_lastLow1PercentUpdateMs = 0; + m_currentFPS = 30.0f; + std::fill(m_durationHistory, m_durationHistory + FPS_HISTORY_SIZE, QuantizedUnsignedShort()); + #ifdef PROFILER_ENABLED m_profilerFrameCapture = NEW W3DProfilerFrameCapture(); #endif @@ -916,6 +926,7 @@ void W3DDisplay::init() // we're now online m_initialized = true; + m_lastUpdateTime64 = getPerformanceCounter(); if( TheGlobalData->m_displayDebug ) { m_debugDisplayCallback = StatDebugDisplay; @@ -959,14 +970,81 @@ void W3DDisplay::reset() const UnsignedInt START_CUMU_FRAME = LOGICFRAMES_PER_SECOND / 2; // skip first half-sec -void W3DDisplay::updateAverageFPS() +void W3DDisplay::addFpsSample(Real elapsedSeconds) +{ + QuantizedUnsignedShort duration = QuantizedUnsignedShort::fromSeconds(elapsedSeconds); + + m_currentFPS = duration.toFPS(); + m_durationHistory[m_historyOffset] = duration; + + m_historyOffset = (m_historyOffset + 1) & (FPS_HISTORY_SIZE - 1); + if (m_historyCount < FPS_HISTORY_SIZE) + { + m_historyCount++; + } +} + +Real W3DDisplay::calculateAverageFPS(Real windowSeconds) { - constexpr const Int FPS_HISTORY_SIZE = 30; + UnsignedInt unitsSum = 0; + Int samples = 0; + const UnsignedInt windowUnits = (UnsignedInt)(windowSeconds * TICKS_PER_SECOND); + + for (Int i = 0; i < m_historyCount; ++i) + { + Int idx = (m_historyOffset - 1 - i) & (FPS_HISTORY_SIZE - 1); + unitsSum += m_durationHistory[idx]; + samples++; + + if (unitsSum >= windowUnits) + { + break; + } + } + + return (unitsSum > 0) ? ((Real)samples * TICKS_PER_SECOND / (Real)unitsSum) : m_currentFPS; +} + +Real W3DDisplay::calculateLow1PercentFPS(Real windowSeconds) +{ + UnsignedInt unitsSum = 0; + Int sampleCount = 0; + const UnsignedInt windowUnits = (UnsignedInt)(windowSeconds * TICKS_PER_SECOND); + QuantizedUnsignedShort sortBuffer[FPS_HISTORY_SIZE]; + + Int i; + for (i = 0; i < m_historyCount; ++i) + { + Int idx = (m_historyOffset - 1 - i) & (FPS_HISTORY_SIZE - 1); + unitsSum += m_durationHistory[idx]; + sortBuffer[sampleCount++] = m_durationHistory[idx]; - static Int64 lastUpdateTime64 = 0; - static Int historyOffset = 0; - static Real fpsHistory[FPS_HISTORY_SIZE] = {0}; + if (unitsSum >= windowUnits) + { + break; + } + } + + if (sampleCount == 0) + { + return m_currentFPS; + } + + const Int bottomSampleCount = std::max((sampleCount + 50) / 100, 1); + + std::nth_element(sortBuffer, sortBuffer + bottomSampleCount, sortBuffer + sampleCount, std::greater()); + + UnsignedInt durationUnitsSum = 0; + for (i = 0; i < bottomSampleCount; ++i) + { + durationUnitsSum += sortBuffer[i]; + } + return (durationUnitsSum > 0) ? ((Real)bottomSampleCount * TICKS_PER_SECOND / (Real)durationUnitsSum) : m_currentFPS; +} + +void W3DDisplay::updatePerformanceMetrics() +{ const Int64 freq64 = getPerformanceCounterFrequency(); const Int64 time64 = getPerformanceCounter(); @@ -977,23 +1055,13 @@ void W3DDisplay::updateAverageFPS() } #endif - const Int64 timeDiff = time64 - lastUpdateTime64; - - // convert elapsed time to seconds - Real elapsedSeconds = (Real)timeDiff/(Real)freq64; - - // append new sample to fps history. - if (historyOffset >= FPS_HISTORY_SIZE) - historyOffset = 0; - - m_currentFPS = 1.0f/elapsedSeconds; - fpsHistory[historyOffset++] = m_currentFPS; + const Int64 timeDiff = time64 - m_lastUpdateTime64; + Real elapsedSeconds = (Real)timeDiff / (Real)freq64; - // 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; + addFpsSample(elapsedSeconds); + m_averageFPS = calculateAverageFPS(1.0f); // 1.0s window for smooth Dynamic LOD tracking and UI matching - lastUpdateTime64 = time64; + m_lastUpdateTime64 = time64; } #if defined(RTS_DEBUG) //debug hack to view object under mouse stats @@ -1700,6 +1768,17 @@ Real W3DDisplay::getAverageFPS() return m_averageFPS; } +Real W3DDisplay::getLow1PercentFPS() +{ + UnsignedInt now = timeGetTime(); + if (now - m_lastLow1PercentUpdateMs >= 1000) + { + m_low1PercentFPS = calculateLow1PercentFPS(3.0f); + m_lastLow1PercentUpdateMs = now; + } + return m_low1PercentFPS; +} + Real W3DDisplay::getCurrentFPS() { return m_currentFPS; @@ -1737,7 +1816,7 @@ void W3DDisplay::draw() // TheSuperHackers @feature bobtista 10/07/2026 Show messages for screenshots finished by the screenshot thread. W3D_UpdateScreenshotMessages(); - updateAverageFPS(); + updatePerformanceMetrics(); if (TheGlobalData->m_enableDynamicLOD && TheGameLogic->getShowDynamicLOD()) { DynamicGameLODLevel lod=TheGameLODManager->findDynamicLODLevel(m_averageFPS); diff --git a/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h b/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h index c2219ec7d2e..6fae3117340 100644 --- a/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h +++ b/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h @@ -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; } diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h b/GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h index ecc1c4fdaf5..553bfb9b235 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h @@ -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; diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp index b3927ee92bd..3653a22962f 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp @@ -917,6 +917,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 ) }, @@ -1164,6 +1165,7 @@ InGameUI::InGameUI() m_lastNetworkLatencyFrames = ~0u; m_renderFpsString = nullptr; + m_renderFpsLowString = nullptr; m_renderFpsLimitString = nullptr; m_renderFpsFont = "Tahoma"; m_renderFpsPointSize = TheGlobalData->m_renderFpsFontSize; @@ -1171,10 +1173,12 @@ InGameUI::InGameUI() m_renderFpsPosition.x = kHudAnchorX; m_renderFpsPosition.y = kHudAnchorY; m_renderFpsColor = GameMakeColor( 255, 255, 0, 255 ); + m_renderFpsLowColor = GameMakeColor( 190, 180, 130, 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; @@ -2283,6 +2287,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); @@ -6045,6 +6051,12 @@ void InGameUI::refreshRenderFpsResources() m_lastRenderFpsUpdateMs = 0u; } + if (!m_renderFpsLowString) + { + m_renderFpsLowString = TheDisplayStringManager->newDisplayString(); + m_lastRenderFpsLow = ~0u; + } + if (!m_renderFpsLimitString) { m_renderFpsLimitString = TheDisplayStringManager->newDisplayString(); @@ -6055,6 +6067,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) @@ -6167,6 +6180,15 @@ void InGameUI::updateRenderFpsString() m_renderFpsString->setText(fpsStr); m_lastRenderFps = renderFps; } + + const UnsignedInt renderFpsLow = (UnsignedInt)(TheDisplay->getLow1PercentFPS() + 0.5f); + if (renderFpsLow != m_lastRenderFpsLow) + { + UnicodeString fpsLowStr; + fpsLowStr.format(L"\x25BC%u", renderFpsLow); + m_renderFpsLowString->setText(fpsLowStr); + m_lastRenderFpsLow = renderFpsLow; + } } void InGameUI::drawNetworkLatency(Int &x, Int &y) @@ -6210,19 +6232,22 @@ void InGameUI::drawRenderFps(Int &x, Int &y) updateRenderFpsString(); } - UnsignedInt renderFpsLimit = 0u; + UnsignedInt renderFpsLimit = RenderFpsPreset::UncappedFpsValue; if (TheGlobalData->m_useFpsLimit) { renderFpsLimit = (UnsignedInt)TheFramePacer->getFramesPerSecondLimit(); - if (renderFpsLimit == RenderFpsPreset::UncappedFpsValue) - { - renderFpsLimit = 0u; - } } if (renderFpsLimit != m_lastRenderFpsLimit) { UnicodeString fpsLimitStr; - fpsLimitStr.format(L"[%u]", renderFpsLimit); + if (renderFpsLimit == RenderFpsPreset::UncappedFpsValue) + { + fpsLimitStr.format(L"\x25B2X"); + } + else + { + fpsLimitStr.format(L"\x25B2%u", renderFpsLimit); + } m_renderFpsLimitString->setText(fpsLimitStr); m_lastRenderFpsLimit = renderFpsLimit; } @@ -6233,14 +6258,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); } } diff --git a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h index ade19e0ef83..28adc5fec22 100644 --- a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h +++ b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h @@ -48,6 +48,40 @@ class RTS2DScene; class RTS3DInterfaceScene; class TextureClass; +constexpr const Real TICKS_PER_SECOND = 62500.0f; + +class QuantizedUnsignedShort +{ +public: + QuantizedUnsignedShort() : m_value(2083) {} + explicit QuantizedUnsignedShort(UnsignedShort val) : m_value(val) {} + + static QuantizedUnsignedShort fromSeconds(Real seconds) + { + Int64 ticks = (Int64)(seconds * TICKS_PER_SECOND + 0.5f); + if (ticks >= 65535) + { + return QuantizedUnsignedShort(65535); + } + if (ticks <= 0) + { + return QuantizedUnsignedShort(1); + } + return QuantizedUnsignedShort((UnsignedShort)ticks); + } + + UnsignedShort getValue() const { return m_value; } + void setValue(UnsignedShort val) { m_value = val; } + + Real toFPS() const { return TICKS_PER_SECOND / (Real)m_value; } + Real toSeconds() const { return (Real)m_value / TICKS_PER_SECOND; } + + operator UnsignedShort() const { return m_value; } + +private: + UnsignedShort m_value; +}; + //============================================================================= /** W3D implementation of the game display which is responsible for creating @@ -148,6 +182,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 @@ -161,7 +196,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; @@ -172,9 +210,17 @@ class W3DDisplay : public Display Render2DClass *m_2DRender; ///< interface for common 2D functions IRegion2D m_clipRegion; ///< the clipping region for images Bool m_isClippedEnabled; /// +#include +#include #include #include #include @@ -391,6 +393,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 @@ -411,6 +414,13 @@ W3DDisplay::W3DDisplay() m_batchGrayscale = FALSE; m_batchNeedsInit = FALSE; + m_historyOffset = 0; + m_historyCount = 1; + m_lastUpdateTime64 = 0; + m_lastLow1PercentUpdateMs = 0; + m_currentFPS = 30.0f; + std::fill(m_durationHistory, m_durationHistory + FPS_HISTORY_SIZE, QuantizedUnsignedShort()); + #ifdef PROFILER_ENABLED m_profilerFrameCapture = NEW W3DProfilerFrameCapture(); #endif @@ -967,6 +977,7 @@ void W3DDisplay::init() // we're now online m_initialized = true; + m_lastUpdateTime64 = getPerformanceCounter(); if( TheGlobalData->m_displayDebug ) { m_debugDisplayCallback = StatDebugDisplay; @@ -1010,14 +1021,81 @@ void W3DDisplay::reset() const UnsignedInt START_CUMU_FRAME = LOGICFRAMES_PER_SECOND / 2; // skip first half-sec -void W3DDisplay::updateAverageFPS() +void W3DDisplay::addFpsSample(Real elapsedSeconds) +{ + QuantizedUnsignedShort duration = QuantizedUnsignedShort::fromSeconds(elapsedSeconds); + + m_currentFPS = duration.toFPS(); + m_durationHistory[m_historyOffset] = duration; + + m_historyOffset = (m_historyOffset + 1) & (FPS_HISTORY_SIZE - 1); + if (m_historyCount < FPS_HISTORY_SIZE) + { + m_historyCount++; + } +} + +Real W3DDisplay::calculateAverageFPS(Real windowSeconds) { - constexpr const Int FPS_HISTORY_SIZE = 30; + UnsignedInt unitsSum = 0; + Int samples = 0; + const UnsignedInt windowUnits = (UnsignedInt)(windowSeconds * TICKS_PER_SECOND); + + for (Int i = 0; i < m_historyCount; ++i) + { + Int idx = (m_historyOffset - 1 - i) & (FPS_HISTORY_SIZE - 1); + unitsSum += m_durationHistory[idx]; + samples++; + + if (unitsSum >= windowUnits) + { + break; + } + } + + return (unitsSum > 0) ? ((Real)samples * TICKS_PER_SECOND / (Real)unitsSum) : m_currentFPS; +} + +Real W3DDisplay::calculateLow1PercentFPS(Real windowSeconds) +{ + UnsignedInt unitsSum = 0; + Int sampleCount = 0; + const UnsignedInt windowUnits = (UnsignedInt)(windowSeconds * TICKS_PER_SECOND); + QuantizedUnsignedShort sortBuffer[FPS_HISTORY_SIZE]; + + Int i; + for (i = 0; i < m_historyCount; ++i) + { + Int idx = (m_historyOffset - 1 - i) & (FPS_HISTORY_SIZE - 1); + unitsSum += m_durationHistory[idx]; + sortBuffer[sampleCount++] = m_durationHistory[idx]; - static Int64 lastUpdateTime64 = 0; - static Int historyOffset = 0; - static Real fpsHistory[FPS_HISTORY_SIZE] = {0}; + if (unitsSum >= windowUnits) + { + break; + } + } + + if (sampleCount == 0) + { + return m_currentFPS; + } + + const Int bottomSampleCount = std::max((sampleCount + 50) / 100, 1); + + std::nth_element(sortBuffer, sortBuffer + bottomSampleCount, sortBuffer + sampleCount, std::greater()); + + UnsignedInt durationUnitsSum = 0; + for (i = 0; i < bottomSampleCount; ++i) + { + durationUnitsSum += sortBuffer[i]; + } + return (durationUnitsSum > 0) ? ((Real)bottomSampleCount * TICKS_PER_SECOND / (Real)durationUnitsSum) : m_currentFPS; +} + +void W3DDisplay::updatePerformanceMetrics() +{ const Int64 freq64 = getPerformanceCounterFrequency(); const Int64 time64 = getPerformanceCounter(); @@ -1028,23 +1106,13 @@ void W3DDisplay::updateAverageFPS() } #endif - const Int64 timeDiff = time64 - lastUpdateTime64; - - // convert elapsed time to seconds - Real elapsedSeconds = (Real)timeDiff/(Real)freq64; - - // append new sample to fps history. - if (historyOffset >= FPS_HISTORY_SIZE) - historyOffset = 0; - - m_currentFPS = 1.0f/elapsedSeconds; - fpsHistory[historyOffset++] = m_currentFPS; + const Int64 timeDiff = time64 - m_lastUpdateTime64; + Real elapsedSeconds = (Real)timeDiff / (Real)freq64; - // 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; + addFpsSample(elapsedSeconds); + m_averageFPS = calculateAverageFPS(1.0f); // 1.0s window for smooth Dynamic LOD tracking and UI matching - lastUpdateTime64 = time64; + m_lastUpdateTime64 = time64; } #if defined(RTS_DEBUG) //debug hack to view object under mouse stats @@ -1770,6 +1838,17 @@ Real W3DDisplay::getAverageFPS() return m_averageFPS; } +Real W3DDisplay::getLow1PercentFPS() +{ + UnsignedInt now = timeGetTime(); + if (now - m_lastLow1PercentUpdateMs >= 1000) + { + m_low1PercentFPS = calculateLow1PercentFPS(3.0f); + m_lastLow1PercentUpdateMs = now; + } + return m_low1PercentFPS; +} + Real W3DDisplay::getCurrentFPS() { return m_currentFPS; @@ -1807,7 +1886,7 @@ void W3DDisplay::draw() // TheSuperHackers @feature bobtista 10/07/2026 Show messages for screenshots finished by the screenshot thread. W3D_UpdateScreenshotMessages(); - updateAverageFPS(); + updatePerformanceMetrics(); if (TheGlobalData->m_enableDynamicLOD && TheGameLogic->getShowDynamicLOD()) { DynamicGameLODLevel lod=TheGameLODManager->findDynamicLODLevel(m_averageFPS); diff --git a/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h b/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h index ddd7b84bafb..7dabaad44c2 100644 --- a/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h +++ b/GeneralsMD/Code/Tools/GUIEdit/Include/GUIEditDisplay.h @@ -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; }