Skip to content

feat(stats): Add 1% low FPS tracking#2661

Open
githubawn wants to merge 7 commits intoTheSuperHackers:mainfrom
githubawn:feature/fps-1percent-low-hud
Open

feat(stats): Add 1% low FPS tracking#2661
githubawn wants to merge 7 commits intoTheSuperHackers:mainfrom
githubawn:feature/fps-1percent-low-hud

Conversation

@githubawn
Copy link
Copy Markdown

@githubawn githubawn commented Apr 29, 2026

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.

lowfps

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.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 29, 2026

Greptile Summary

Adds a 1% low FPS display to the in-game HUD for both Generals and Zero Hour. The implementation replaces a 30-frame static-local rolling average with a time-bounded ring buffer (5,000 slots) and introduces calculateLow1PercentFPS via std::nth_element for an efficient O(n) partition of the bottom 1% of frames over a 3-second window.

Confidence Score: 5/5

Safe to merge; only a P2 style/consistency finding.

No P0 or P1 issues found. The algorithm is correct, lifecycle management (alloc/free) is consistent with existing patterns, and all virtual overrides are properly wired. The sole finding is a new static local variable that is inconsistent with the per-instance refactoring done in the same PR, but it has no practical runtime impact given the single-instance usage of W3DDisplay.

W3DDisplay.cpp (Generals and GeneralsMD) — the static lastLowUpdate in updatePerformanceMetrics.

Important Files Changed

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
Loading
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

Comment thread Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h Outdated
Comment thread Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp Outdated
Comment thread Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp Outdated
Comment thread Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp Outdated
Comment thread Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp Outdated
Comment thread Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp Outdated
Comment thread Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp Outdated
Comment thread Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp Outdated
Comment thread Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp Outdated
Comment thread Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp Outdated
githubawn added 4 commits May 1, 2026 01:13
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.
@githubawn githubawn requested a review from Skyaero42 April 30, 2026 23:53
Comment thread Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp Outdated
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants