Skip to content

perf: pace the playback clock's ticks by the screen's frames - #569

Merged
Tiaxi merged 2 commits into
mainfrom
perf/clock-tick-pacing
Nov 10, 2026
Merged

Tiaxi merged 2 commits into
mainfrom
perf/clock-tick-pacing

Conversation

@Tiaxi

@Tiaxi Tiaxi commented Nov 10, 2026 •

Copy link
Copy Markdown
Owner

Closes #553

What changes

The transport bar's PlaybackClock woke at every device pixel of playhead travel, at least 1 ms apart. That made about 1000 UI-thread wakeups a second for a 1 s clip. It happened at every track start too, while the duration was unknown and the seek bar fell back to a 1 s range.

The scheduling rule in PlaybackClock::scheduleTick now works like this:

  • Slow movement: unchanged. The clock still ticks exactly at each pixel boundary when a pixel takes longer than a frame.
  • Fast movement: a step of positionSeconds comes no sooner than one frame after it last changed. The frame is set by the refresh rate of the window's screen. When several pixels pass within a frame, the playhead moves by all of them at once, as the screen would show them anyway.
  • Unknown duration: only the time text's second boundaries tick. The seek bar's playhead waits at the start then (fix: show the seek bar's playhead for very short tracks #541/fix: show the seek bar's playhead for very short tracks #552), so there is nothing else to move.
  • Text and exact reads: the text's change still comes exactly when its second is due, without frame pacing. currentPositionSeconds(), seeks, pauses and jumps still show the exact position.

The refresh rate comes from public API only. WindowPresence gets a screenRefreshRate property that follows QWindow::screenChanged and QScreen::refreshRateChanged, because QML's Screen attached type has no refresh rate. Main.qml passes it to PlaybackController.frameRate, which binds it into the new PlaybackClock.frameRate property. If the screen reports no rate, the clock uses 60 Hz.

Tests

  • tst_playback_clock:
    • shortTrackTicksNoFasterThanTheScreen: a 1 s clip on a 1598 px bar, across a pause/resume and a seek. Moves of the playhead stay at least a frame apart, and total ticks stay within 60 Hz plus the text's ticks. The position and text are right at every tick, the text turns over on time, and ticks stop at the end.
    • unknownDurationTicksOnlyAtSeconds: with the 1 s fallback step, exactly three ticks in 3 s, at the second boundaries, including after a seek and a pause/resume. Once the duration is known, ticks are frame-paced again.
    • frameRateFollowsTheScreen: 0, NaN and negative rates fall back to 60 Hz. At 144 Hz ticks come 7 ms apart, and a change of rate reschedules the next tick.
    • The existing step-0 check now expects one 60 Hz frame instead of the removed kFrameIntervalMs.
  • tst_qml_smoke_transport_input:
    • windowPresenceFollowsTheScreensRefreshRate: the presence's rate follows its window's screen, and goes to 0 without a window or after the window is destroyed.
    • The controller passes frameRate to the clock.
    • The main window's clock is paced at its screen's refresh rate.
  • The existing pixel-precision tests pass unchanged, including the accessibility test that requires seek-bar moves within 1 ms of each pixel boundary on a 20 s track.

./scripts/run-tests.sh --ui-only: 102/102 passed, no build warnings. No Rust changes.

Review round 1

  • Sample corrections are paced too: every step of the playhead waits a frame after the last one, whether it comes from a tick, the text's tick or a sample correction. A step that has to wait is shown by the tick at that frame.
  • Exact changes restart the pacing: a seek, a pause or resume, a track change or a duration change starts it over. A slow track's next pixel boundary after a seek now comes when it is due, not a frame later.
  • New tests:
    • samplesKeepAShortTracksPlayheadToItsFrames: a 1 s clip with jittered samples every 20–50 ms. Every move is at least a frame apart, and the tick bound holds.
    • sparseStepsAfterASeekComeAtTheirBoundaries: a 100 ms step after a seek 1 ms before a boundary. The boundary and the ones after it come within 1 ms.
    • Both short-track tests fail with the pacing check in update() removed.
  • ./scripts/run-tests.sh --ui-only: 102/102 passed.

The transport bar's clock woke at every device pixel of playhead travel,
at least 1 ms apart: about 1000 ticks a second for a 1 s clip, and the
same at every track start while the duration was unknown and the seek
bar fell back to a 1 s range.

A step of positionSeconds now comes no sooner than a frame after it last
changed, at the refresh rate of the window's screen, which WindowPresence
reads from QScreen; slow tracks keep their exact pixel-boundary ticks.
Without a duration only the text's second boundaries tick. The text's
change and exact position reads are unchanged.

Closes #553
A sample that corrected the position moved the playhead whenever it
crossed a pixel, however soon after the last move, so a short track's
playhead could move twice within a frame. Every step, from a tick, a
text tick or a sample, now waits a frame after the last one; a step
that waits is shown by the tick at that frame.

An exact change, such as a seek, a pause or resume, or a duration
change, starts the pacing over, so a slow track's next pixel boundary
after a seek comes when it is due rather than a frame later.
@Tiaxi
Tiaxi merged commit 6037ecd into main Nov 10, 2026
5 checks passed
@Tiaxi
Tiaxi deleted the perf/clock-tick-pacing branch November 10, 2026 08:28
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.

perf: don't tick the playback clock every millisecond for short or unknown-length tracks

1 participant