perf(viewer): pace Qt6 video frame delivery to scene-render capacity#3006
Conversation
Part of issue #2987: 1080p60 content on Pi 4 presented at 22.6 fps with the playback position falling to ~0.6x realtime (clips ended early), because every sink delivery scheduled a QML scene render on a GUI thread that sustains ~45 renders/s at 1080p — overload made throughput collapse below even the 30 fps a 1080p30 clip achieves. QMediaPlayer now renders into an intermediate QVideoSink; frames forward to the VideoOutput's sink only once the scene graph has composited the previous one (QQuickWindow::afterRendering). 30 fps sources pass untouched; 60 fps sources settle into an even ~half cadence instead of irregular drops. If the render signal is not wired the gate falls back to unpaced forwarding. Stats lines gain a frames-forwarded field between frames-delivered and frames-rendered so the gate is observable in playback-stats.log. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 1-deep gate was stop-and-wait: render -> re-arm -> idle until the next sink delivery -> render, measuring only ~23 presented fps on a GUI thread that renders faster back-to-back. Park the newest frame that arrives mid-render in a single-slot mailbox and forward it the moment afterRendering fires, so renders chain at capacity with at most one frame of latency. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR addresses jerky/high-FPS video playback on Qt6 boards by pacing QtMultimedia frame forwarding to the QML scene graph’s actual render capacity, preventing GUI-thread render backlog collapse and improving playback position accuracy.
Changes:
- Introduces an intermediate
QVideoSink(pacingSink) and a render-gated forwarding path that only forwards the newest frame when the previous one has been composited (afterRendering). - Adds a single-slot mailbox (
pendingFrame) plus newframes-forwardedstats to make the pacing behavior observable. - Extends QtTest coverage to assert the sink chaining and gating/drop behavior under the offscreen test platform.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/anthias_webview/tests/test_videoview.cpp | Adds tests asserting pacing-sink chaining and gate behavior (mailbox/drop until scene renders). |
| src/anthias_webview/src/videoview.h | Adds pacing-gate state (pacingSink, pendingFrame, sceneReadyForFrame) and a forwarded-frame counter. |
| src/anthias_webview/src/videoview.cpp | Implements render-paced forwarding via afterRendering, updates stats logging, and rewires the player to the intermediate sink. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
A frame parked in the mailbox at stop() time could be forwarded by a later afterRendering (stale-frame flash on the next reveal) and kept its decoder buffer alive between assets. Clear the mailbox, re-arm the gate, and push an empty frame to the VideoOutput so the last displayed buffer is released too (review feedback). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Independent on-device re-validation (Pi 4, 2026-06-07)Re-measured this PR from scratch on a physical Pi 4, with the exact branch point ( 1080p60 H.264 (BBB, AC3 audio, 60 s clip)
1080p30 controlPR build: exactly 300 vblanks/10 s = 30.0 fps, 1.0× realtime, The baseline failure is literally the report in #2987: a 60-second clip "finishing" in 48 s at 13 fresh fps is "jerky and cut short towards the end". A second datapoint on older master ( Two observations beyond the PR description
Verdict: the PR does what it claims on the board it claims it for. Device restored to the PR build for continued soak. |



Issues Fixed
Part of #2987 (the Pi 4 symptoms: 50/60 fps clips jerky and "freezing / cut short towards the end").
Description
On Pi 4, 1080p60 H.264 measured 22.6 fps presented (DRM vblank tracepoint) with the GUI thread saturated and — worse —
position-msadvancing at ~0.6× realtime: QtMultimedia fell behind, dropped ~70% of frames before the sink (dropped=480+per clip), and clips got killed by the slot duration before reaching their end. Root cause: every sink delivery schedules a QML scene render, and at 60 deliveries/s the GUI thread (which sustains a render only every ~40 ms at 1080p with the decoder running) collapses under the backlog.QMediaPlayernow renders into an intermediateQVideoSinkowned byVideoView. Frames forward to theVideoOutput's sink only when the scene graph has composited the previous one (QQuickWindow::afterRendering); a frame arriving mid-render parks in a single-slot mailbox and forwards the instant the render finishes, so renders chain at capacity with at most one frame of latency. If the render signal is ever un-wired, the gate falls back to unpaced forwarding.Measured on the Pi 4 testbed (BBB 1080p60 H.264, official-image lineage build
4a30bb6-pi4-64):position-msrate~23 fps is this render architecture's ceiling for 60 fps sources on Pi 4 (render cost rises to ~40 ms while the decoder saturates the memory bus); the user-visible wins are eliminating the slow-motion/cut-short behaviour and the frame staleness. Stats lines gain a
frames-forwardedfield so the gate is observable inplayback-stats.log.Affects all Qt6 boards (pi3-64/pi4-64/pi5/x86/arm64); validated on pi4-64, the board from the report.
Checklist
🤖 Generated with Claude Code