diff --git a/Core/GameEngine/Include/GameClient/Display.h b/Core/GameEngine/Include/GameClient/Display.h index 8c022244206..752ee3c5adf 100644 --- a/Core/GameEngine/Include/GameClient/Display.h +++ b/Core/GameEngine/Include/GameClient/Display.h @@ -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 diff --git a/Generals/Code/GameEngine/Include/GameClient/InGameUI.h b/Generals/Code/GameEngine/Include/GameClient/InGameUI.h index a6ecf5c4a3d..cdd923bdfa7 100644 --- a/Generals/Code/GameEngine/Include/GameClient/InGameUI.h +++ b/Generals/Code/GameEngine/Include/GameClient/InGameUI.h @@ -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; diff --git a/Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp b/Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp index b0cd79ce60e..ca34e6c6c20 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 ) }, @@ -1133,6 +1134,7 @@ InGameUI::InGameUI() m_lastNetworkLatencyFrames = ~0u; m_renderFpsString = nullptr; + m_renderFpsLowString = nullptr; m_renderFpsLimitString = nullptr; m_renderFpsFont = "Tahoma"; m_renderFpsPointSize = TheGlobalData->m_renderFpsFontSize; @@ -1140,10 +1142,12 @@ InGameUI::InGameUI() 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; @@ -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); @@ -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(); @@ -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) @@ -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) @@ -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); } } diff --git a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h index a1ae9bcc298..453a08d01ae 100644 --- a/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h +++ b/Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h @@ -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 @@ -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; @@ -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; ///m_framesPerSecondLimit; + m_low1PercentFPS = TheGlobalData->m_framesPerSecondLimit; #if defined(RTS_DEBUG) m_timerAtCumuFPSStart = 0; #endif @@ -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 @@ -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(); @@ -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 @@ -1693,6 +1785,11 @@ Real W3DDisplay::getAverageFPS() return m_averageFPS; } +Real W3DDisplay::getLow1PercentFPS() +{ + return m_low1PercentFPS; +} + Real W3DDisplay::getCurrentFPS() { return m_currentFPS; @@ -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); diff --git a/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h b/Generals/Code/Tools/GUIEdit/Include/GUIEditDisplay.h index 8679d5fabd9..eeef74278a5 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 675fd35f8de..d7609495254 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 449924ec54f..2c17c0601d8 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 ) }, @@ -1163,6 +1164,7 @@ InGameUI::InGameUI() m_lastNetworkLatencyFrames = ~0u; m_renderFpsString = nullptr; + m_renderFpsLowString = nullptr; m_renderFpsLimitString = nullptr; m_renderFpsFont = "Tahoma"; m_renderFpsPointSize = TheGlobalData->m_renderFpsFontSize; @@ -1170,10 +1172,12 @@ InGameUI::InGameUI() 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; @@ -2272,6 +2276,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); @@ -6041,6 +6047,12 @@ void InGameUI::refreshRenderFpsResources() m_lastRenderFpsUpdateMs = 0u; } + if (!m_renderFpsLowString) + { + m_renderFpsLowString = TheDisplayStringManager->newDisplayString(); + m_lastRenderFpsLow = ~0u; + } + if (!m_renderFpsLimitString) { m_renderFpsLimitString = TheDisplayStringManager->newDisplayString(); @@ -6051,6 +6063,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) @@ -6163,6 +6176,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"(%u)", renderFpsLow); + m_renderFpsLowString->setText(fpsLowStr); + m_lastRenderFpsLow = renderFpsLow; + } } void InGameUI::drawNetworkLatency(Int &x, Int &y) @@ -6229,14 +6251,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 cf2e537f9e1..344a90c9f34 100644 --- a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h +++ b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h @@ -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 @@ -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; @@ -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; ///m_framesPerSecondLimit; + m_low1PercentFPS = TheGlobalData->m_framesPerSecondLimit; #if defined(RTS_DEBUG) m_timerAtCumuFPSStart = 0; #endif @@ -410,6 +411,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 @@ -1009,14 +1019,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(); @@ -1027,23 +1115,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 @@ -1763,6 +1855,11 @@ Real W3DDisplay::getAverageFPS() return m_averageFPS; } +Real W3DDisplay::getLow1PercentFPS() +{ + return m_low1PercentFPS; +} + Real W3DDisplay::getCurrentFPS() { return m_currentFPS; @@ -1797,7 +1894,7 @@ void W3DDisplay::draw() if (TheGlobalData->m_headless) return; - 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 f554dd614c8..cfb7036f9f1 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; }