Skip to content

display_driver: present DIRECT frames without waiting inside the flush - #353

Merged
bdbarnett merged 1 commit into
mainfrom
display-driver/async-present
Oct 31, 2026
Merged

bdbarnett merged 1 commit into
mainfrom
display-driver/async-present

Conversation

@bdbarnett

@bdbarnett bdbarnett commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

On a DIRECT panel, the last flush called FBDisplay.show(), which waits up to a scanout frame for the new buffer to take over. The event loop's slow-pass rule then counted that wait as work and stayed off just as long afterwards. On an ESP32-S3-Touch-LCD-4.3 that meant a 46 ms pass, of which 29 ms was the wait, then about 46 ms off: 10.6 fps, where CircuitPython on the same board draws 23.6.

With this change, a panel that can split its present no longer waits inside the flush:

A panel without the split, including the same board on firmware without #226, keeps the old show() path. The meter still counts one frame per present.

Only excluding the wait from the slow-pass rule was measured too: 15.8 fps, but LVGL then held 73% of the thread, mostly asleep in the wait. So that alternative was dropped.

Tests: tests/test_lvgl_display_driver.py gains TestAsyncPresent. A planted fault (always calling flush_ready) fails it.

Measured on the same board with PyDevices/micropython-pydevices#226 firmware running lv_test_timer, 30 s runs:

display_driver app fps full-screen fps LVGL thread share wait per frame
main (show() in the flush) 16.7–16.8 4.9 0.49 18 ms in the flush
this branch 26.5–26.6 6.0 (one run) 0.35 0.25 ms in flush_wait_cb

Proved on hardware: all three full-screen runs on each path and a 20-minute run of this path finished with no hang (results in the comments). The earlier stop was the board's USB serial link.

On a framebuffer that can split its present (FBDisplay.show_nowait and
wait_shown, over DotClockFramebuffer.present/wait), the last area starts
the present and LVGL's flush_wait_cb finishes it just before LVGL paints
again. The LVGL pass no longer blocks up to a scanout frame per present,
and the loop's slow-pass yield no longer counts that wait as work.
Panels without the split keep presenting with show().
@bdbarnett

Copy link
Copy Markdown
Contributor Author

The remaining runs are done, on the other ESP32-S3-Touch-LCD-4.3 with PyDevices/micropython-pydevices#226 merged onto current main and this branch merged onto main, driven over WebREPL only. Same probe and lv_test_timer as the table above.

display_driver app fps full-screen fps (3 × 30 s) LVGL thread share (full) wait per frame
main (show() in the flush) 16.56 4.87, 4.87, 4.87 0.51 19.0–19.9 ms in the flush
this branch 26.33 5.95, 5.90, 5.92 0.50 0.27 ms in flush_wait_cb, max 0.95

All three full-screen runs finished on this path.

20-minute run of lv_test_timer on this branch, twenty one-minute windows: 25.75–27.13 fps (26.7–27.1 in 18 of 20), LVGL thread share 0.34–0.36, flush_wait_cb 0.25–0.27 ms on average with a worst single wait of 9.4 ms, free heap steady at 6.56–6.70 MB, and ping answered throughout. Frame gaps topped out at 62–72 ms, apart from two windows with 2.1 s and 2.6 s gaps that line up with a WebREPL login in each of those minutes. Before that, the board ran this library under lv_test_timer for 45 minutes with no hang. tests/test_lvgl_display_driver.py passes on this branch merged with main (28 tests).

@bdbarnett
bdbarnett merged commit 0c8f85e into main Oct 31, 2026
9 checks passed
@bdbarnett
bdbarnett deleted the display-driver/async-present branch October 31, 2026 01:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant