Repository navigation
Conversation
… the panel's frame buffers The DPI panel was always created with one frame buffer, private to the BSP, so LVGL had to render into separate PSRAM draw buffers and every flush copied the area into the frame buffer the panel scans out. - Kconfig M5STACK_TAB5_DPI_NUM_FBS (1..3, default 1) sets num_fbs for all three panel variants (ILI9881, ST7123, ST7121). - frame_buffers() returns the panel's frame buffers (cached by initialize_lcd()), frame_buffer_size() their size, so an application can render straight into them; esp_lcd_panel_draw_bitmap() on a frame buffer switches the panel to it instead of copying. - initialize_display(const DisplayConfig &): with render_to_frame_buffers the BSP sets LVGL up in DIRECT mode on two of the frame buffers and its flush switches buffers once the frame's last area is rendered. No draw buffers and no rotation scratch buffer are allocated. initialize_display(size_t) is unchanged. - The example gains an opt-in menuconfig option for that mode; README and example README document it, with the render time measured on a Tab5 (full screen 75.6 -> 48.6 ms) and the PSRAM it saves. Defaults are unchanged: one frame buffer, draw buffers, copying flush. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…_flush_ready() itself The DPI transfer-done interrupt (notify_lvgl_flush_ready) only gives draw_done_sem_ now; the BSP's own flush() calls lv_display_flush_ready(). An application that installs its own flush callback on the frame buffers (frame_buffers(), DisplayConfig::render_to_frame_buffers) and relies on the interrupt to finish the flush hangs on its first frame. Say so in both doc comments. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…, or use a flush wait callback Make the custom-flush-callback note precise: LVGL waits for lv_display_flush_ready() after every flush-callback call (per area), and lv_display_set_flush_wait_cb() is the alternative that needs no call. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@finger563 could you take a look when you have a moment? This adds multiple DPI frame buffers to the Tab5 BSP and is stacked on #738 (details in my comment there: #738 (comment)). It's a draft until #738 lands; the default config behaves exactly as #738 alone. Thanks! |
…uffer swaps With two DPI frame buffers, esp_lcd_panel_draw_bitmap() on a frame buffer only queues the switch (cache write-back, cur_fb_index); the DMA takes the new buffer up when it restarts at the end of the frame it is sending. A renderer that starts on the previous buffer at once draws into the buffer the panel is still scanning out, and the frame tears. on_color_trans_done cannot tell when the switch lands: for a frame buffer the driver calls it synchronously, before the switch. - present_frame(frame_buffer, timeout_ms = 100): queues the switch, then waits for the DMA's frame-done event (on_frame_buf_complete; on_refresh_done before ESP-IDF v5.5.5 / v6.0.3, selected at compile time), after which the previous buffer is free. Events from before the switch was queued are dropped first. A timeout logs once and returns false instead of stalling. - set_vsync_enabled() / vsync_enabled() turn the wait off and on. - The frame-done callback and its semaphore are registered only with CONFIG_M5STACK_TAB5_DPI_NUM_FBS >= 2, so the default build is unchanged. - DisplayConfig::render_to_frame_buffers now shows each frame with present_frame(), so the BSP's DIRECT mode does not tear. README updated. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ing into the frame buffers The flush already waits for the panel's frame-done event, so the example's fixed 16 ms sleep after every pass would halve the frame rate. The default configuration keeps the fixed sleep. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Hardware results on a Tab5 (ST71xx panel, ESP-IDF v6.1)Measured with a small display benchmark added locally to the
DSI underruns were counted from the driver's "underrun happens" log lines. These are single runs. Branches tested: espp
Takeaways:
Build note: neither PR builds as-is on ESP-IDF v6.1. v6.1 added 🤖 Generated with Claude Code |
…ng into the frame buffers (supersedes #847) (#877) * feat(m5stack-tab5): configurable DPI frame-buffer count and access to the panel's frame buffers The DPI panel was always created with one frame buffer, private to the BSP, so LVGL had to render into separate PSRAM draw buffers and every flush copied the area into the frame buffer the panel scans out. - Kconfig M5STACK_TAB5_DPI_NUM_FBS (1..3, default 1) sets num_fbs for all three panel variants (ILI9881, ST7123, ST7121). - frame_buffers() returns the panel's frame buffers (cached by initialize_lcd()), frame_buffer_size() their size, so an application can render straight into them; esp_lcd_panel_draw_bitmap() on a frame buffer switches the panel to it instead of copying. - initialize_display(const DisplayConfig &): with render_to_frame_buffers the BSP sets LVGL up in DIRECT mode on two of the frame buffers and its flush switches buffers once the frame's last area is rendered. No draw buffers and no rotation scratch buffer are allocated. initialize_display(size_t) is unchanged. - The example gains an opt-in menuconfig option for that mode; README and example README document it, with the render time measured on a Tab5 (full screen 75.6 -> 48.6 ms) and the PSRAM it saves. Defaults are unchanged: one frame buffer, draw buffers, copying flush. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> (cherry picked from commit 0ebbcbd) * docs(m5stack-tab5): a custom LVGL flush callback must call lv_display_flush_ready() itself The DPI transfer-done interrupt (notify_lvgl_flush_ready) only gives draw_done_sem_ now; the BSP's own flush() calls lv_display_flush_ready(). An application that installs its own flush callback on the frame buffers (frame_buffers(), DisplayConfig::render_to_frame_buffers) and relies on the interrupt to finish the flush hangs on its first frame. Say so in both doc comments. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> (cherry picked from commit 50a6a93) * docs(m5stack-tab5): call lv_display_flush_ready() once per flush call, or use a flush wait callback Make the custom-flush-callback note precise: LVGL waits for lv_display_flush_ready() after every flush-callback call (per area), and lv_display_set_flush_wait_cb() is the alternative that needs no call. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> (cherry picked from commit c08e962) * feat(m5stack-tab5): vsync-gated present_frame() for tear-free frame-buffer swaps With two DPI frame buffers, esp_lcd_panel_draw_bitmap() on a frame buffer only queues the switch (cache write-back, cur_fb_index); the DMA takes the new buffer up when it restarts at the end of the frame it is sending. A renderer that starts on the previous buffer at once draws into the buffer the panel is still scanning out, and the frame tears. on_color_trans_done cannot tell when the switch lands: for a frame buffer the driver calls it synchronously, before the switch. - present_frame(frame_buffer, timeout_ms = 100): queues the switch, then waits for the DMA's frame-done event (on_frame_buf_complete; on_refresh_done before ESP-IDF v5.5.5 / v6.0.3, selected at compile time), after which the previous buffer is free. Events from before the switch was queued are dropped first. A timeout logs once and returns false instead of stalling. - set_vsync_enabled() / vsync_enabled() turn the wait off and on. - The frame-done callback and its semaphore are registered only with CONFIG_M5STACK_TAB5_DPI_NUM_FBS >= 2, so the default build is unchanged. - DisplayConfig::render_to_frame_buffers now shows each frame with present_frame(), so the BSP's DIRECT mode does not tear. README updated. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> (cherry picked from commit 2602cdc) * feat(m5stack-tab5): example sleeps per lv_timer_handler() when rendering into the frame buffers The flush already waits for the panel's frame-done event, so the example's fixed 16 ms sleep after every pass would halve the frame rate. The default configuration keeps the fixed sleep. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> (cherry picked from commit 6df3ece) * fix(m5stack-tab5): present_frame() takes part in the transfer accounting The merged flush path leaves its copy / rotation in flight and tracks it by a token; every panel operation waits for it first. present_frame() now does too (wait_for_transfer_idle() before its draw_bitmap, refusing to switch on a wedged transfer), and the DIRECT-mode flush documents why it never leaves a transfer in flight itself, plus the two costs inherent to DIRECT mode with two buffers that are left as they are. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU * fix(m5stack-tab5): present_frame() waits for a second frame-done event where it is the bridge VSYNC Before ESP-IDF v5.5.5 / v6.0.3 the P4 raises the DPI panel's frame-done event (on_refresh_done) from the DSI bridge's VSYNC interrupt, which is not synchronized with the DMA re-arm that actually switches buffers: the DMA fetch of a frame completes a porch before that frame's VSYNC, so the first VSYNC after a draw may belong to a frame that was re-armed on the old buffer, and LVGL would start rendering into a buffer still being scanned out (a porch-wide tearing window every frame). The DMA re-arms exactly once between consecutive VSYNCs, so on those versions wait for the second event; on_frame_buf_complete (later versions) is raised right after the re-arm and one event suffices, as before. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU * fix(m5stack-tab5): refuse DIRECT mode when LVGL's stride or the rotation would break it - LVGL's draw-buffer stride (LV_DRAW_BUF_STRIDE_ALIGN) must equal the panel's 1440-byte row for in-place rendering into the frame buffers; check it at initialize_display() and refuse instead of drawing skewed. - A non-zero display rotation (the compile-time initial one or one LVGL asked for before init) is refused at initialize_display(); LVGL cannot rotate in DIRECT mode and the old first-flush warning came too late. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU * fix(m5stack-tab5): present_frame() accepts any pointer inside a frame buffer; one presenter at a time - The buffer to show is found by range, as the DPI driver's draw_bitmap does, rather than requiring the exact start address. - present_frame() is serialized end to end by a mutex: the frame-done semaphore has a single token and a single waiter per switch, so two callers (an application task and the LVGL flush, say) could otherwise steal each other's event. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU * feat(m5stack-tab5): keep the direct-to-framebuffer PPA rotation with several frame buffers With CONFIG_M5STACK_TAB5_DPI_NUM_FBS > 1 the PPA path was disabled (dpi_framebuffer_ left null) because a cached pointer could target a buffer no longer scanned out, so rotation fell back to the slow software path and the extra buffers bought nothing without DIRECT mode. Nothing switches buffers except present_frame(): it now moves dpi_framebuffer_ to the buffer it switched to, so the PPA rotation always targets the scanned-out buffer whatever the count. The pointer is atomic (written under panel_op_mutex_, read by flush() on the LVGL task). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU * refactor(m5stack-tab5): espp bool naming for the DIRECT-mode flag; frame-buffer Kconfig help states the cost - DisplayConfig::render_to_frame_buffers -> render_to_frame_buffers_enabled and direct_render_ -> direct_render_enabled_ (espp's convention for bool config fields and members), with the README / example updated. - CONFIG_M5STACK_TAB5_DPI_NUM_FBS help now states the 1.8 MB PSRAM cost per extra buffer, that LVGL's DIRECT mode uses at most two, and that extra buffers without DIRECT mode buy nothing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU * fix(m5stack-tab5): run lv_init() before querying LVGL's stride in initialize_display() lv_draw_buf_width_to_stride() goes through the draw-buffer handlers that lv_init() installs; before the espp::Display (which calls lv_init()) is constructed the query returned 0 and the DIRECT-mode stride check refused every configuration. lv_init() is idempotent, so call it first. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU * fix(m5stack-tab5): review follow-ups on the multi-framebuffer PR - initialize_lcd() validates the frame-buffer pointers the DPI driver returns: each of the first kNumDpiFramebuffers must be non-null and distinct, else initialization fails with an error naming the index. - present_frame() counts consecutive frame-done timeouts and after three switches its wait off (with a warning naming set_vsync_enabled(true) as the re-arm), so a frame-done event that never arrives degrades to the non-blocking path instead of holding the UI at one frame per timeout; a successful wait resets the count. The README, the header doc and the DIRECT-mode flush (which deliberately ignores the return value) describe what happens on a timeout: the switch stays queued, LVGL carries on, that frame may tear. - Example: in DIRECT mode the GUI task sleeps exactly what LVGL asked for (floored at 1 ms), with LV_NO_TIMER_READY capped at LV_DEF_REFR_PERIOD, and the comment says so; next_rotation()'s DIRECT-mode early return is an #else branch now (cppcheck: statements after return). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU * fix(m5stack-tab5): present_frame() refuses a single frame buffer; re-arming vsync resets its warning Review follow-ups on #877: - present_frame() returned true with one frame buffer (nothing to switch, cache write-back only), which hid the tearing it promises to avoid. It now returns false with a rate-limited error naming CONFIG_M5STACK_TAB5_DPI_NUM_FBS; the DIRECT-mode flush never reaches it with one buffer (initialize_display() refuses that configuration). - set_vsync_enabled(true) cleared the consecutive-timeout count but not the once-only timeout warning, so a second outage episode was never logged. Re-arming now resets both. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU --------- Co-authored-by: Alaining <ee.master.alain@gmail.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Why is this PR needed: with one frame buffer, every Tab5 redraw is copied into the image the panel is sending, so the UI tears. The current code can't fix this:
num_fbs = 1;Displayonly renders in PARTIAL mode, copying from its own draw buffers.Description
Optional tear-free LVGL rendering on the Tab5, for the cost of one extra frame buffer. Off by
default: the default build behaves exactly as before.
Stacked on #738.
CONFIG_M5STACK_TAB5_DPI_NUM_FBS(1-3, default 1).initialize_display({.render_to_frame_buffers = true})(defaultfalse): LVGL draws into theframe buffer that is not on screen. The flush switches the panel to it, then waits for the panel's
frame-done event before LVGL draws again.
frame_buffers()for applications with their own renderer.Motivation and Context
Measured with the
m5stack-tab5example on a Tab5 (ST7123, ESP-IDF v6.0.0), BSP default drawbuffers, with the same LVGL loop in both builds:
mainlonger needed. Net: 1.2 MB.
refresh. Heavy animations therefore run at half the panel rate (29 FPS) instead of tearing.
How has this been tested?
clang-format clean.
as the base.
Types of changes
🤖 Generated with Claude Code