Skip to content

feat(m5stack-tab5): multiple DPI frame buffers (rotate at full FPS, optional zero-copy rendering) - #847

Draft
Alaining wants to merge 3 commits into
esp-cpp:feat/tab5-st7121-rotationfrom
Alaining:feat/m5stack-tab5-multi-framebuffer
Draft

Alaining wants to merge 3 commits into
esp-cpp:feat/tab5-st7121-rotationfrom
Alaining:feat/m5stack-tab5-multi-framebuffer

Conversation

@Alaining

Copy link
Copy Markdown

Description

Tab5 screen rotation. With 2 frame buffers it rotates at 45 FPS instead of 15-27, and it can render with no copy and no tearing.

Stacked on #738. Merge order:

  1. fix(m5stack-tab5): rotation audit, experimental opt-in ST7121 panel rotation (default off), and DSI underrun fixes #738 first. This PR is built on its head, cfdbd8d9.
  2. This PR, once fix(m5stack-tab5): rotation audit, experimental opt-in ST7121 panel rotation (default off), and DSI underrun fixes #738 is in main. Because fix(m5stack-tab5): rotation audit, experimental opt-in ST7121 panel rotation (default off), and DSI underrun fixes #738 is squash-merged, we rebase this branch onto
    main (leaving only its own 3 commits) and retarget it to main.

Alternatively, this PR can be merged into #738's branch first, so #738 brings it to main.

Motivation and Context

With one frame buffer, every flush of a rotated UI has to rotate and copy into the buffer the
panel is scanning out. That costs most of the frame time and tears. With two buffers, rotated
drawing keeps the full frame rate, and a non-rotated UI can render into the hidden buffer with no
copy and no tearing, using less PSRAM.

Measured with the m5stack-tab5 example on a Tab5 (ST7123, ESP-IDF v6.0.0) while drawing on the
touch screen. "Rotated" means 90/180/270°, which all measured the same:

Config 0° FPS / CPU Rotated FPS / CPU Tearing (moving bar) Free PSRAM
main, 1 buffer 45 / 19% 27 / 55% not measured not measured
#738, 1 buffer (this PR's default) 45 / 24% 15 / 74% yes 14.2 MB
2 buffers 48 / 13% 45 / 20% less 12.4 MB
2 buffers + render_to_frame_buffers 48 / 13% n/a none 17.8 MB

How has this been tested?

  • The Tab5 example builds for esp32p4 with ESP-IDF v6.0.0 (default config, and 2 buffers with the
    option on), with no new warnings. clang-format (pre-commit) is clean.
  • On a Tab5 (ST7123), the example ran with 1, 2 and 3 buffers, with the option on and off:
  • Our application renders in DIRECT mode on 2 buffers with its own 180° flip flush. Its boot self
    test and our hardware bench suite pass.
  • Not tested: the ILI9881 / ST7121 panels, ESP-IDF v6.0.3 / v6.1.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation Update
  • Hardware (schematic, board, system design) change
  • Software change

Checklist:

  • My change requires a change to the documentation.
  • I have added / updated the documentation related to this change via either README or WIKI

Software

  • I have added tests to cover my changes. (No host harness for the BSP; tested on hardware.)
  • I have updated the .github/workflows/build.yml file to add my new test to the automated cloud build github action. (Not needed: the example is already built.)
  • All new and existing tests passed.
  • My code follows the code style of this project.

🤖 Generated with Claude Code

Alaining and others added 3 commits October 15, 2026 04:10
… 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>
@Alaining

Copy link
Copy Markdown
Author

@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!

This branch has not been deployed

No deployments
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.

1 participant