Skip to content

Remove the synchronous DRM atomic page-flip wait from Flutter’s raster presentation path - #529

Merged
ardera merged 2 commits into
ardera:masterfrom
leragequit:master
Nov 8, 2026
Merged

ardera merged 2 commits into
ardera:masterfrom
leragequit:master

Conversation

@leragequit

Copy link
Copy Markdown
Contributor

Previously, FlutterCompositorPresentLayers called the blocking atomic commit path, kms_req_commit_blocking (ironically surrounded by a tracer log calling it unblocking).

The raster thread could therefore spend up to a refresh interval waiting for KMS page flip completion, inflating raster duration and causing phase-dependent frame latency.

This change moves that wait out of the raster path while keeping request and framebuffer lifetimes valid until scanout completion.

KMS presentation is now asynchronous: the raster thread submits a nonblocking commit and returns immediately, while framebuffer ownership is retained until the DRM page-flip event confirms scanout.

@leragequit

Copy link
Copy Markdown
Contributor Author

This is related to #528

@ardera

ardera commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

mh, I'm not sure about this, this is basically triple buffering

and there is already a 1 frame queue, it is in the kernel, so with this it's now a 2-frame queue, plus rendering that can basically be 48ms of latency in total... also since there's no vsync callback and you're not blocking on presents anymore, the frame timings can become totally out of sync with the actual vsync times

how much queueing you want is a tradeoff of throughput vs latency. Some graphics intensive applications might want a deeper queue, some more more interactive apps might want lower latency. I started working on proper frame scheduling here: https://github.com/ardera/flutter-pi/blob/feat/frame-scheduling/src/frame_scheduler.c but never finished it.

We can add your queueing, since it helps in your case, but I'd hide it behind a feature flag / commandline option for now and make it not the default for now

@frog-fest

Copy link
Copy Markdown

Thanks for this patch — wanted to share independent real-hardware test results, since this directly addresses the symptom described in #528 ("constant raster jank") on our hardware too.

Setup: Embedded ARM SoM, weak Vivante/Etnaviv GPU via Mesa+DRM/KMS, 480x272 panel (~69Hz), flutter-pi with --pixelformat RGB565 already applied (a separate, already-landed optimization on our side). Remote-only testing via SSH (no physical touch interaction during measurement, but the app's gRPC backend streams kept updating the UI normally — not a frozen/static scene).

Method: App's own frame-timing instrumentation (SchedulerBinding.addTimingsCallback, split into build/UI vs raster/GPU time), plus direct kernel-level sampling of the raster thread's /proc/<pid>/task/<tid>/stat state to check for D (uninterruptible, blocked-in-kernel) state.

Structural fix, confirmed at the kernel level:

Sampling run Duration Samples Time in D state
Short 42s 1,856 0 (0%)
Long 600s 15,323 0 (0%)

Pre-patch, the raster thread reliably sat in D state waiting on the DRM atomic page-flip. Post-patch, zero D-state samples across both runs — this part of the fix is unambiguous.

Raster latency, 5 independent clean idle runs (fresh restart each time, no touch input, backend data stream still active; numbers are the final cumulative SUMMARY of each run):

Run Duration Frames raster p50 raster p95
Baseline (pre-#529, RGB565 only) — — 26.34ms 46ms
Clean run 1 ~5 min 512 8.55ms ~37ms
Clean run 2 4 min 438 7.60ms ~36ms
Clean run 3 4 min 440 12.10ms ~37ms
1-hour soak 60 min 20,557 7.32ms 34.6ms

Steady-state raster p50 consistently lands in a ~7-14ms range post-patch, vs. our pre-patch baseline of 26.34ms — roughly a 2-4x improvement in typical raster latency on our hardware.

Startup transient: every run shows elevated/variable latency for the first few minutes after a fresh restart (briefly up to ~25-27ms). This is not a deterministic "warm-up" — three independent short runs showed three different shapes (declining / flat / rising), so we treat it as run-to-run startup noise rather than a real phenomenon tied to the patch.

1-hour soak test (addressing the "tradeoff of throughput vs latency... can compound over time" concern): after the initial transient settled (~15-20 min in), raster p50/p95 stayed completely flat for the remaining ~45 minutes, with a very slight downward trend, not upward. max stayed pinned at a single early outlier (242.59ms) for the rest of the hour — no new outliers appeared. On our hardware/workload, over this duration, we did not observe the latency-queue-buildup behavior flagged as a possible tradeoff.

Caveat: per the note that FrameTiming may become less vsync-accurate post-patch, we treat these numbers as directionally trustworthy (especially the D-state elimination, which is a direct kernel-level observation, not derived from FrameTiming) rather than perfectly precise.

We haven't yet done a physical-touch responsiveness check (remote-only testing so far), but based on the above this looks like a solid win for our use case.

@ardera

ardera commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

I definitely agree this is a good change. But sorry @frog-fest that analysis is not the helpful 😅

Raster time and "d-time" don't mean anything in this case. Latency can't be measured using raster time.
If FPS improved, that would be great. And I think this might be the case for you. However in your numbers it seems like the app is running at 2-5 FPS (440 frames / 240s ~= 2FPS)

@frog-fest

frog-fest commented Oct 7, 2026 •

Copy link
Copy Markdown

Field-test results: touch/render responsiveness with & without this patch

I ran a black-box A/B comparison on real hardware (STM32MP1 DHCOM SOM, 480×272 LVDS panel, ~69Hz) between two otherwise-identical builds of our Flutter HMI app — one running flutter-pi with this PR applied, one without — to see if it measurably affects what #528 describes as "constant raster jank."

Method: A small custom tool logs evdev touch events and DRM/KMS scanout-buffer changes on a shared CLOCK_MONOTONIC timeline (PRIME-export + mmap + diff on the raw framebuffer), independent of flutter-pi/the app itself. No app or flutter-pi code was touched for measurement purposes. I had to disable a 1Hz clock widget and the text-cursor blink in the test app first — both are periodic, touch-independent redraws that otherwise swamp frame-diff-based measurements.

Test 1 — animation jank: Repeatedly tapped an isolated checkbox with a build/collapse checkmark animation, measured the max frame-to-frame gap within each tap's reaction burst (a proxy for a stalled/dropped frame mid-animation). Reproduced over 2 independent rounds:

patched (round 1 / round 2) unpatched (round 1 / round 2)
p50 99.8ms / 90.9ms 129.7ms / 125.5ms
p95 363.3ms / 303.5ms 441.2ms / 395.2ms

Patched is consistently lower on both p50 and p95 in both rounds.

Test 2 — on-screen keyboard popup latency: This device has no OS-level keyboard/input method — the "keyboard" is an in-app Flutter widget we render ourselves. Typed through a full on-screen keyboard layout (all letters + punctuation), measuring touch-release → key-popup-appear latency:

patched (n=59) unpatched (n=58)
p50 61.7ms 175.5ms
p95 214.1ms 454.2ms
max 267.6ms 507.8ms

Patched is ~3× faster at the median and >2× faster at p95.

Caveats: Manual taps by a single tester, moderate sample sizes (n≈20–59/condition), not a rigorous lab setup. A simpler "touch → first visible frame change" onset-latency metric was inconclusive/noisy early on (background redraw contamination, drag-gesture mismatches) and was dropped in favor of the two metrics above, which were more robust and reproducible.

@danielyoungx

Copy link
Copy Markdown

Independent A/B/C on different hardware, measuring FPS as you suggested.

Setup: Orange Pi 6 Plus (CIX CD8180 / Sky1), Mali-G720 via Panfrost GLES (Mesa 26.0.8), Linlon-D60 display controller + PS185 DP→HDMI bridge, 1920×1080 @ 60 Hz, -r 90, Flutter 3.47.7 release, base 8b86c52. Same app in all three runs (fullscreen fragment shader + ~140 additive particles via drawRawAtlas + a few transformed widgets), idle, 30 s each. FPS = FrameTiming callbacks per 2-second window, first window discarded. Zero failed atomic commits in all three runs.

build FPS mean FPS min p99 frame (totalSpan) worst frame
master 8b86c52 44.3 23.0 59.6 ms 59.7 ms
this PR (ca9a758) 61.1 61.0 18.0 ms 22.3 ms
alternative below 61.5 61.0 18.3 ms 19.2 ms

On this display controller the blocking commit returns two vblanks after it is issued (raster pinned at exactly 33.3 ms on master), so the raster thread misses every other vsync. GPU time itself is well under 1 ms per frame (glmark2-es2-drm), so this is purely the present path.

On the latency concern: I tried a smaller variant that keeps at most one page flip in flight. on_present_frame only waits while the previous flip is still pending, then commits non-blocking; the page-flip handler just clears the flag and signals a condvar (it must not commit, since it runs with the drmdev locked). So no queue is added on top of the kernel's, presents stay paced by page flips, but the raster thread no longer waits for its own flip. It is window.c only, ~50 lines: https://gist.github.com/danielyoungx/905aa9f5af1d271dcba8511d035aa4fb

It reaches the same FPS here. I have not measured input-to-photon latency for either variant, so I can't claim one is better on latency; only that the single-in-flight version adds no queue by construction.

Not tested: Raspberry Pi / vc4, and the Vulkan path (on this board, with master, every Vulkan present fails with drmModeAtomicCommit: Invalid argument, which looks like a separate issue).

@leragequit

Copy link
Copy Markdown
Contributor Author

@ardera the pipeline expired, but I am not getting any feedback what went wrong other than it was expired because it wasn't started I guess?

I would be 100% fine with having this as a commandline option.

@DisDis

DisDis commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

Raspberry Pi 5: higher presentation throughput with PR #529

I compared this change on our actual Raspberry Pi 5 photoframe setup, using the
production slideshow effects and media-loading paths through our benchmark app.
The results show a repeatable throughput improvement in demanding effects,
including LiquidMorph and Water Refraction.

Setup

  • Raspberry Pi 5, connected 2560 × 1600 display at approximately 50 Hz, DPR 1.
  • OpenGL, Flutter 3.47.7 release, identical engine, application bundle and media
    within each A/B series. No OS, firmware, driver or clock/governor changes.
  • Both flutter-pi binaries built on the device with identical Release settings:
    -O3 -DNDEBUG, LTO enabled, OpenGL and GStreamer enabled.
  • Baseline: integration_dpms_and_video, aa0ffcd.
    Its binary is byte-for-byte identical to our installed runtime.
  • Candidate: remove-page-flip, f7f3746,
    integrating PR #529 onto the same
    base with DPMS and video lifecycle fixes. This is an integration-tree comparison,
    rather than a comparison against clean upstream master.

The full sweep covers all 51 effects, plus a no-effect control. Mosaic variants
are reported separately, giving 58 cases per runtime. Each case has one warm-up
and three measured repetitions. A smaller control matrix also uses ABBA, with
six measured repetitions per runtime. Samples from different application bundles
are kept separate; the table below uses one identical bundle for the full sweep.

Additional runs cover 48 MP photos, Mosaic/MosaicFade grids of 100/150/200 with
Canvas and shader backends, and photo→video, video→photo and video→video transitions.
Original-resolution fixtures were retained: JPEGs up to 8000 × 6000 and two
1920 × 1080 H.264 videos. These are reproducible fixtures, not a personal photo
collection. FixedAnimationController and image/effect quality were unchanged.

Results

In addition to Flutter FrameTiming, I recorded kernel
drm_vblank_event_delivered events in a separate ftrace instance using monotonic
timestamps. The following values are median DRM completion rates across the
three measured repetitions
, restricted to each animation's timestamp window.
Intervals are calculated separately within each repetition.

Effect / variant Baseline, completions/s Patched, completions/s Change %
Tablet 25.00 44.49 +78.0%
Noise Dissolve 24.75 40.57 +63.9%
LiquidMorph 25.00 40.91 +63.6%
Accordion 25.00 35.87 +43.5%
Flip Vertical 32.78 44.95 +37.1%
Rotate Down 35.11 47.96 +36.6%
Rotate Up 35.36 47.96 +35.6%
ZoomOver 16.84 22.30 +32.4%
Background To Foreground 35.87 47.34 +32.0%
BlurFade 20.80 27.37 +31.6%
Cube 25.00 32.89 +31.5%
Depth 36.40 47.34 +30.1%
Flip Horizontal 36.13 46.28 +28.1%
Zoom Out 36.13 45.83 +26.9%
Water Refraction 13.34 15.61 +17.0%
Foreground To Background 42.31 48.86 +15.5%
No-effect control 50.00 50.00 Unchanged

Flutter-reported FPS follows the same improvement in the demanding cases:
LiquidMorph 25.00 → 41.10 FPS, Water Refraction 13.42 → 15.75 FPS.
The DRM completion rates corroborate the throughput gain seen in Flutter's timings.

The display limit matters: in the lightweight control, Flutter-reported FPS rises
from 50 to 60, while DRM completions remain at approximately 50/s. I therefore
do not interpret 60 Flutter FPS as 60 visible frames on this panel.

Tail behaviour is less uniform. For Water Refraction, the p95 interval between DRM
completions remains approximately 100 ms, despite the higher average rate.
For BlurFade, it improves from approximately 80 to 60 ms.

Validation and limits

  • 552 measured repetitions passed, plus 27/27 in a separate baseline
    confirmation. No native crashes, benchmark runtime errors or cleanup errors
    were recorded in these runs. No trace-buffer overruns or throttling occurred;
    maximum sampled CPU temperature was 63.35 °C.
  • All 174 pairs of full-resolution 2560 × 1600 control captures at 0/50/100%
    matched. Captures were taken outside the timing windows.
  • DRM completion cadence does not measure unique visible images, optical latency,
    input-to-display latency or queueing delay. Static captures do not verify every
    intermediate frame. This was a short test, not a long-term stability run.
  • One outer launcher exited with a shell error after a successful benchmark
    process completion. Its logs and measurements were retained; the additional
    baseline confirmation passed and was kept separate from the ABBA statistics.

@ardera

ardera commented Nov 8, 2026

Copy link
Copy Markdown
Owner

thanks all, I could also reproduce the improvement locally. I think it's not perfect but definitely better for most people right now, so I'll just merge directly.

Pipeline is failing because it hasn't been updated in ages, I'll have to fix that separately.

@ardera
ardera merged commit 9f95f43 into ardera:master Nov 8, 2026
DisDis added a commit to DisDis/flutter-pi that referenced this pull request Nov 8, 2026
Integrate upstream PR ardera#529 without the unrelated video changes from
remove-page-flip, preserving the published support_dpms history.

Before a blocking commit, discard the queued frame and drain the pending
page flip so it cannot re-enable the CRTC after a power change. Keep power
requests out of the page-flip path and leave mutex ownership to the common
commit wrapper.

Port the DPMS regression cases, use an error-checking test mutex, and adapt
the upstream presentation fixture to provide valid DRM property IDs.

Validation: all four CTest targets pass in Debug and Release (OpenGL/Vulkan).
DPMS passes ASan/UBSan with leak detection. The full sanitizer suite reports
316 bytes of existing command-line test leaks in flutterpi_test.
@leragequit

Copy link
Copy Markdown
Contributor Author

@ardera If you can give me some pointers on what to improve, I am happy to learn <3

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.

5 participants