feat(stats): Add 1% low FPS tracking#2661
feat(stats): Add 1% low FPS tracking#2661githubawn wants to merge 7 commits intoTheSuperHackers:mainfrom
Conversation
|
| Filename | Overview |
|---|---|
| Core/GameEngine/Include/GameClient/Display.h | Added pure virtual getLow1PercentFPS() to the base Display interface — minimal, correct change. |
| Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp | Correct INI parsing, initialization, allocation/free lifecycle, and HUD drawing for the new 1% low FPS string — follows existing patterns consistently. |
| Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h | Adds getLow1PercentFPS() override, history arrays (60 KB in-object), and helper method declarations — design is sound, enum constant is correctly placed in the header. |
| Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp | Core implementation of rolling FPS history, calculateLow1PercentFPS via nth_element, and the 1-second throttle. One static local lastLowUpdate inconsistently bypasses the per-instance refactoring done in this same PR. |
| GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp | Mirror of the Generals implementation; carries the same static local lastLowUpdate inconsistency. |
| GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp | Zero Hour mirror of the Generals InGameUI.cpp changes — consistent and correct. |
Sequence Diagram
sequenceDiagram
participant Draw as W3DDisplay::draw()
participant Metrics as updatePerformanceMetrics()
participant AddSample as addFpsSample()
participant CalcAvg as calculateAverageFPS(0.5s)
participant CalcLow as calculateLow1PercentFPS(3.0s)
participant InGameUI as InGameUI::updateRenderFpsString()
participant HUD as InGameUI::drawRenderFps()
Draw->>Metrics: called each frame
Metrics->>AddSample: elapsedSeconds → m_fpsHistory / m_durationHistory (ring buffer, 5000 slots)
Metrics->>CalcAvg: walk back up to 0.5s of history
CalcAvg-->>Metrics: m_averageFPS
Metrics->>Metrics: every 1s (via lastLowUpdate throttle)
Metrics->>CalcLow: walk back up to 3.0s, nth_element bottom 1%
CalcLow-->>Metrics: m_low1PercentFPS
InGameUI->>InGameUI: updateRenderFpsString() reads getAverageFPS() and getLow1PercentFPS()
InGameUI->>HUD: drawRenderFps() draws 60 (52) / 60fps
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 1
Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp:1079
**`lastLowUpdate` should be a member variable, not a static local**
The PR explicitly moves `lastUpdateTime64`, `historyOffset`, and `fpsHistory` from static locals to member variables to fix per-instance state isolation, but then introduces a new static local `lastLowUpdate` with the same problem. If a second `W3DDisplay` is ever constructed (e.g., during a re-init cycle), `lastLowUpdate` carries over from the previous instance, potentially suppressing the first 1% low update for up to a second. The same pattern appears in `GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp`.
Add `UnsignedInt m_lastLowUpdate;` to `W3DDisplay.h` (alongside `m_lastUpdateTime64`), initialize it to `0` in the constructor, and replace the `static` declaration with a use of the member.
```suggestion
// m_lastLowUpdate is a member variable initialized to 0 in the constructor
```
Reviews (6): Last reviewed commit: "change sampleCount from 99 to 50" | Re-trigger Greptile
Move FPS history state into W3DDisplay members. Implement accurate time-based windowing for frame metrics. Use ceiling logic for improved 1% low accuracy. Optimize percentile calculation using efficient selection algorithm. Rename and centralize performance update call sites. Increase history buffer for stable high-FPS monitoring.
Update average FPS math to use time-weighted mean. Move sortBuffer to class members for consistency.
| 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
This PR adds a 1% low FPS metric to the existing FPS counter HUD, displayed in parentheses next to the average FPS. The 1% low is a standard performance metric used to surface frame time spikes that the average FPS hides. Inspired by #1942.
Added 1% low FPS display to HUD counter
Added m_renderFpsLowString and supporting UI members
Added RenderFpsLowColor configuration to InGameUI INI
Increased history to 5,000 time-bounded frames
Implemented rolling 0.5s window for average FPS
Implemented rolling 3.0s window for 1% lows
The following screenshot from AOD Cobalt Rush shows the 1% low FPS overlay compared to CapFrameX (an external benchmarking tool, centered right), demonstrating the value of surfacing this metric separately from the average.
This change was generated with AI assistance. All generated code has been reviewed, tested, and verified for correctness. The implementation went through multiple iterations, including fundamental changes to the underlying approach, as well as passes to apply simplifications, fix inconsistencies, and optimize performance. Both Generals and GeneralsMD implementations are included in this PR with identical code.