Skip to content

Add playback performance HUD - #25

Closed
emlcpfx wants to merge 1 commit into
D-Mad:mainfrom
emlcpfx:pr13-perf-hud
Closed

Add playback performance HUD#25
emlcpfx wants to merge 1 commit into
D-Mad:mainfrom
emlcpfx:pr13-perf-hud

Conversation

@emlcpfx

@emlcpfx emlcpfx commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

View > Performance HUD (Ctrl+Alt+H) overlays measured FPS against target, frame, resolution, average decode cost and cache depth.

FPS      17.2 / 24
FRAME    1042 / 1200
RES      2048 x 1152
DECODE   58.1 ms
CACHE    48 frames

It answers the question a reviewer actually asks when playback feels wrong: is it me, or is it the machine? A supervisor calling a note on timing needs to know they're watching 24 fps and not 17 — otherwise they're grading the playback, not the shot.

playback/stats.py is pure and takes an injected clock, so the rolling averages are asserted exactly rather than slept for.

Measuring the right thing

  • Frame rate is measured where frames reach the viewer. That's the only honest definition of a displayed frame — the playback timer keeps firing whether or not a frame actually made it, so measuring there would report a healthy 24 fps while the screen sat frozen.
  • Decode cost is timed around the decode itself. For sequences that's inside the decode thread, so the queue wait in front of it isn't blamed on the decoder. For movies it covers the proxy scale, RGB conversion and OCIO — what actually stands between a decoded packet and the screen.
  • A decode slower than its frame budget is flagged. 58 ms against a 24 fps budget of ~41.7 ms cannot sustain real time however fast the rest of the pipeline is, so that row goes red on its own.

The subtle one: a stall looks exactly like a pause

Both stop frames arriving. Both empty the rolling window. So measured FPS falls to zero — which is the same reading as "nothing has played yet."

Handled naively, a total freeze renders as a calm --, which is precisely the failure this HUD exists to catch. stalled() tracks whether frames were ever flowing, and the window passes in whether the player is actually running. A freeze reads STALLED in red; a pause stays quiet. test_a_stall_is_visible_immediately_not_averaged_away and test_a_pause_is_not_a_stall pin both halves.

I only found this because a test I'd written to assert the obvious thing failed — the first cut genuinely did report a frozen player as healthy.

Only a genuinely bad row is coloured, so the eye goes to the thing that's actually wrong rather than a wall of red.

Verification

26 new tests, 264 on the branch, green three runs in a row, compile clean.

Driven end to end through a real MainWindow with a real decoded clip — the HUD reports a real measured decode cost (1.3 ms for a 1080p frame), the row set is correct, measurements reset between sources, and the refresh timer starts and stops with the toggle.

One behavioural note: SequenceDecodeThread.decoded gains a fifth argument (decode milliseconds). Timing has to be taken on the decode thread; carrying it back with the frame is the only way to keep the queue wait out of the number.

Branches off current main.

View > Performance HUD (Ctrl+Alt+H) overlays measured FPS against target,
frame, resolution, average decode cost and cache depth. It answers the
question a reviewer actually asks when playback feels wrong: is it me, or is
it the machine? A supervisor calling a note on timing needs to know they are
watching 24 fps and not 17, or they are grading the playback and not the shot.

playback/stats.py is pure and takes an injected clock, so the rolling averages
are asserted exactly instead of being slept for.

- Frame rate is measured where frames reach the viewer, which is the only
  honest definition of a displayed frame -- not at the playback timer, which
  keeps firing whether or not a frame made it.
- Decode cost is timed around the decode itself. For sequences that is inside
  the decode thread, so the queue wait in front of it is not blamed on the
  decoder; for movies it covers the proxy scale, RGB conversion and OCIO, which
  is what stands between a packet and the screen.
- A decode slower than its share of the frame budget is flagged: it cannot
  sustain real time however fast the rest of the pipeline is.

A stall and a pause both stop frames arriving and both empty the rolling
window, so measured FPS falls to zero, which is the same reading as playback
never having started. Without telling those apart, a total freeze renders as a
calm placeholder dash, and that is the exact failure this HUD exists to catch.
stalled() tracks whether frames were ever flowing, and the window passes in
whether the player is actually running, so a freeze reads STALLED in red while
a pause stays quiet.

Only a genuinely bad row is coloured, so the eye goes to the thing that is
actually wrong rather than a wall of red.

26 tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TexnzYbmCjjTDB8zzZuUPb
@D-Mad

D-Mad commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Thank you, Eric. The Performance HUD has been preserved and rebased onto Speed + Proxy in #31. Review fixed off-speed target FPS, pause/scrub false stalls, proxy labeling and actual movie drop counting. Closing this branch in favor of #31.

@D-Mad D-Mad closed this Jul 15, 2026
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