From cb8557e5cf34e38730323d8f7cf31c32b0e5b280 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Tue, 3 Nov 2026 13:11:11 -0600 Subject: [PATCH 1/2] feat(m5stack-tab5): tear-free LVGL flushes that keep rotation (back-buffer flush) DIRECT mode (#877) is tear-free but cannot rotate. The back-buffer flush keeps LVGL's partial rendering into draw buffers and lands every area in the frame buffer the panel is not scanning out: unrotated areas through the DMA2D engine (ESP-IDF 6.1's esp_async_color_convert window copies, the engine the DPI driver uses itself; PPA at angle 0 on older ESP-IDF), rotated areas through the PPA as before. The DPI driver's draw_bitmap cannot target the back buffer (it copies into the buffer on screen and treats a frame-buffer pointer as a switch request). At the frame's last area the flush brings the back buffer up to date from the buffer on screen - the region it has missed since it was last shown minus what this frame redrew, nothing for a full-screen redraw - then switches the panel to it with present_frame() at the frame boundary (one row of cache write-back: only DMA engines write these buffers). The panel never shows a half-drawn frame whatever the draw-buffer size, so strips are viable again: 128-row draw buffers free 1.5 MB of PSRAM against the stock example's two whole-frame ones. - DisplayConfig::flush_to_back_buffer_enabled (needs NUM_FBS >= 2 and the PPA; ignored with render_to_frame_buffers_enabled). - flush_to_back_buffer(), queue_block_copy() (DMA2D, PPA fallback), queue_ppa_block() (the PPA copy/rotation; the existing rotation path goes through it too), subtract_area(), per-buffer stale regions. - present_frame() private cache_sync_rows variant. - CMake: esp_driver_dma required on ESP-IDF >= 6.1; __has_include guard. - Example: CONFIG_M5STACK_TAB5_EXAMPLE_FLUSH_TO_BACK_BUFFER (default off) and CONFIG_M5STACK_TAB5_EXAMPLE_DRAW_BUFFER_LINES (panel rows; default 1280 = whole frame as before); GUI pacing as in DIRECT mode. - README section with the measured numbers (Tab5 ST71xx, ESP-IDF v6.1): small updates and the sweeping bar locked to the panel's 58 Hz with no tearing, full-screen redraw 28.9 fps (two panel periods; stock 38.2), 90-degree full redraw 14.4 with strips (stock 16.2). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU --- components/m5stack-tab5/CMakeLists.txt | 9 +- components/m5stack-tab5/README.md | 64 +++ .../example/main/Kconfig.projbuild | 30 ++ components/m5stack-tab5/example/main/gui.cpp | 3 +- .../example/main/m5stack_tab5_example.cpp | 34 +- .../m5stack-tab5/include/m5stack-tab5.hpp | 66 +++ components/m5stack-tab5/src/video.cpp | 413 ++++++++++++++++-- 7 files changed, 557 insertions(+), 62 deletions(-) diff --git a/components/m5stack-tab5/CMakeLists.txt b/components/m5stack-tab5/CMakeLists.txt index 0fb7ba7075..a74b380688 100755 --- a/components/m5stack-tab5/CMakeLists.txt +++ b/components/m5stack-tab5/CMakeLists.txt @@ -1,6 +1,13 @@ +# ESP-IDF 6.1 adds esp_driver_dma's async color-convert driver (DMA2D window +# copies), which the back-buffer flush uses when present (see video.cpp). +set(m5stack_tab5_extra_requires "") +if("${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR}" VERSION_GREATER_EQUAL "6.1") + list(APPEND m5stack_tab5_extra_requires esp_driver_dma) +endif() + idf_component_register( INCLUDE_DIRS "include" SRC_DIRS "src" - REQUIRES driver esp_driver_i2s esp_driver_ppa esp_driver_sdmmc esp_driver_spi esp_lcd esp_video esp_cam_sensor fatfs base_component bmi270 codec display display_drivers gt911 i2c ina226 input_drivers interrupt pi4ioe5v rx8130ce sdcard st7123touch task touch + REQUIRES driver esp_driver_i2s esp_driver_ppa esp_driver_sdmmc esp_driver_spi esp_lcd esp_video esp_cam_sensor fatfs base_component bmi270 codec display display_drivers gt911 i2c ina226 input_drivers interrupt pi4ioe5v rx8130ce sdcard st7123touch task touch ${m5stack_tab5_extra_requires} REQUIRED_IDF_TARGETS "esp32p4" ) diff --git a/components/m5stack-tab5/README.md b/components/m5stack-tab5/README.md index 62cfa80e8c..d02441cd3e 100644 --- a/components/m5stack-tab5/README.md +++ b/components/m5stack-tab5/README.md @@ -149,6 +149,70 @@ the 1.8 MB rotation scratch buffer is not needed either). The frame buffers are cache-line aligned, so they also satisfy a raised `LV_DRAW_BUF_ALIGN` (e.g. 128 for the PPA draw unit), which a plain `heap_caps_malloc()` draw buffer may not. +## Tear-free flushes with rotation: the back-buffer flush + +DIRECT mode cannot rotate. The back-buffer flush keeps LVGL's normal partial +rendering into draw buffers (so the PPA rotation of each area, and the panel's +own 180-degree flip, keep working) and gets rid of tearing differently: + +- `initialize_display({.flush_to_back_buffer_enabled = true})` (needs + `CONFIG_M5STACK_TAB5_DPI_NUM_FBS` >= 2 and the PPA). Every flushed area is + copied, rotated when needed, by the PPA into the frame buffer the panel is + *not* scanning out; nothing is drawn into the buffer on screen. +- When LVGL's last area of a frame has landed, the flush switches the panel to + that buffer with `present_frame()` at the frame boundary, then has the PPA + copy the region this frame changed from the new front buffer into the new + back buffer, left in flight while LVGL renders the next frame, so the back + buffer is current when the next frame's areas are written into it. (With + three buffers each one is brought up to date when it becomes the back + buffer.) +- The panel never shows a half-drawn frame, whatever the draw-buffer size. + That is what makes small draw buffers viable again: with a single frame + buffer a full-screen redraw flushes strip by strip into the buffer on + screen, visible as a progressive wipe, which is why the example used two + whole-frame draw buffers (3.7 MB); with the back-buffer flush the strips are + assembled off screen and shown at once. + +```cpp +// menuconfig: M5Stack Tab5 Configuration -> Number of DPI frame buffers = 2 +auto &tab5 = espp::M5StackTab5::get(); +tab5.initialize_lcd(); +tab5.initialize_display({.pixel_buffer_size = 720 * 128, // 128-row strips (184 KB each) + .flush_to_back_buffer_enabled = true}); +``` + +Costs: the flush of a frame's last area waits for the frame-done event (one +refresh, like DIRECT mode), and the PPA does one extra copy of the changed +region per frame. Pixels written with `write_lcd_lines()` land in the buffer on +screen and are not carried over to the next frame: draw through LVGL. + +Measured with the example's display benchmark (a local test aid, not in the +repository: four phases of 8 s at 0 and at 90 degrees, same unit as the earlier +tables) on a Tab5 (ST71xx, ESP-IDF v6.1). "Stock" is one frame buffer with two +whole-frame draw buffers (the example's default); the back-buffer flush uses two +frame buffers, with whole-frame or 128-row (184 KB) draw buffers: + +| phase | stock (tears) | back-buffer, whole-frame draw buffers | back-buffer, 128-row strips | +| --- | --- | --- | --- | +| small update, 0 deg | 993 fps (unthrottled) | 57.8 (locked to the 58 Hz panel) | 57.8 | +| sweeping bar, 0 deg | 350 | 57.7, no tearing | 57.5, no tearing | +| full-screen redraw, 0 deg | 38.2 | 28.9 | 28.9 | +| heavy animation, 0 deg | 15.7 | 11.5 | 11.5 | +| small update, 90 deg | 589 | 57.8 | 57.8 | +| sweeping bar, 90 deg | 260 | 56.7 | 56.8 | +| full-screen redraw, 90 deg | 16.2 | 11.6 | 14.4 | +| heavy animation, 90 deg | 9.4 | 6.4 | 7.2 | +| free PSRAM after display init | 26.36 MB | 24.52 MB | 27.89 MB | + +A full-screen redraw at 0 degrees takes two panel periods (34.5 ms): render plus +copy is about 26 ms, which misses one frame-done event and waits for the next - +the price of never showing a half-drawn frame, as in DIRECT mode. At 90 degrees +the strips are faster than whole-frame draw buffers because the PPA rotation of +one strip overlaps the rendering of the next. The 128-row configuration frees +1.5 MB of PSRAM against the stock example (two frame buffers plus two 184 KB +draw buffers, 4.1 MB, against one frame buffer plus two whole-frame draw +buffers, 5.5 MB). + ## Example The [example](./example) shows how to use the `espp::M5StackTab5` hardware abstraction component to initialize and use various subsystems of the Tab5. diff --git a/components/m5stack-tab5/example/main/Kconfig.projbuild b/components/m5stack-tab5/example/main/Kconfig.projbuild index 165e769875..add8780db8 100644 --- a/components/m5stack-tab5/example/main/Kconfig.projbuild +++ b/components/m5stack-tab5/example/main/Kconfig.projbuild @@ -16,4 +16,34 @@ menu "M5Stack Tab5 Example Configuration" The rotate button does nothing in this mode: LVGL cannot rotate in DIRECT mode. + config M5STACK_TAB5_EXAMPLE_FLUSH_TO_BACK_BUFFER + bool "Flush LVGL into the back frame buffer (tear-free, rotation works)" + depends on M5STACK_TAB5_DPI_NUM_FBS > 1 && !M5STACK_TAB5_EXAMPLE_RENDER_TO_FRAME_BUFFERS + default n + help + Initialize the display with + DisplayConfig::flush_to_back_buffer_enabled: every LVGL flush is + copied (and rotated) by the PPA into the frame buffer the panel is + not scanning out, and the panel switches to it at the frame + boundary once the frame is complete. Partial frames are never on + screen, so the draw buffers can be strips (see + M5STACK_TAB5_EXAMPLE_DRAW_BUFFER_LINES) and a full-screen redraw + still appears at once. The rotate button works. Needs + CONFIG_M5STACK_TAB5_DPI_NUM_FBS >= 2 (M5Stack Tab5 Configuration). + + config M5STACK_TAB5_EXAMPLE_DRAW_BUFFER_LINES + int "Rows per LVGL draw buffer" + depends on !M5STACK_TAB5_EXAMPLE_RENDER_TO_FRAME_BUFFERS + range 8 1280 + default 1280 + help + Height, in panel rows of 720 pixels, of each of LVGL's two draw + buffers; 1280 is a whole frame (1.8 MB of PSRAM each). With one + frame buffer a full-screen redraw flushes strip by strip into the + buffer on screen, so a whole-frame draw buffer is what makes it + appear at once; with the back-buffer flush the strips are + assembled off screen and a far smaller draw buffer (e.g. 128 rows, + 184 KB, the BSP's default strip) looks the same and leaves the + PSRAM to the application. + endmenu diff --git a/components/m5stack-tab5/example/main/gui.cpp b/components/m5stack-tab5/example/main/gui.cpp index 77dbfbaf16..1862d81454 100644 --- a/components/m5stack-tab5/example/main/gui.cpp +++ b/components/m5stack-tab5/example/main/gui.cpp @@ -427,7 +427,8 @@ bool Gui::update(std::mutex &m, std::condition_variable &cv) { update_audio_label(); } std::unique_lock lock(m); -#if CONFIG_M5STACK_TAB5_EXAMPLE_RENDER_TO_FRAME_BUFFERS +#if CONFIG_M5STACK_TAB5_EXAMPLE_RENDER_TO_FRAME_BUFFERS || \ + CONFIG_M5STACK_TAB5_EXAMPLE_FLUSH_TO_BACK_BUFFER // The flush already waits for the panel's frame-done event, which paces a // frame that has something to draw; here only sleep until LVGL's next timer // is due (at least 1 ms so an idle LVGL never spins). lv_timer_handler() diff --git a/components/m5stack-tab5/example/main/m5stack_tab5_example.cpp b/components/m5stack-tab5/example/main/m5stack_tab5_example.cpp index 1b9717261d..f46386bd2b 100644 --- a/components/m5stack-tab5/example/main/m5stack_tab5_example.cpp +++ b/components/m5stack-tab5/example/main/m5stack_tab5_example.cpp @@ -87,28 +87,32 @@ extern "C" void app_main(void) { return; } - // initialize the display with a full-frame pixel buffer (Tab5 is 1280x720 with - // 2 bytes per pixel). A full-frame LVGL draw buffer (allocated in PSRAM, see - // initialize_display) lets a full-screen redraw - e.g. a rotation change - - // flush in a single pass instead of ~72 ten-line strips, so the screen - // repaints at once rather than wiping progressively. Partial updates still - // only render/flush their dirty area, so the larger buffer costs nothing - // there (just PSRAM). logger.info("Initializing display ({} DPI frame buffers)...", tab5.frame_buffers().size()); #if CONFIG_M5STACK_TAB5_EXAMPLE_RENDER_TO_FRAME_BUFFERS - // LVGL renders straight into the panel's frame buffers (DIRECT mode): no - // draw buffers, no rotation scratch buffer, and no copy per flush. + // LVGL renders in DIRECT mode straight into the panel's frame buffers (no + // draw buffers); see the menuconfig help. if (!tab5.initialize_display({.render_to_frame_buffers_enabled = true})) { - logger.error("Failed to initialize display!"); - return; - } #else - auto pixel_buffer_size = tab5.display_width() * tab5.display_height(); - if (!tab5.initialize_display(pixel_buffer_size)) { + // LVGL draw buffers of CONFIG_M5STACK_TAB5_EXAMPLE_DRAW_BUFFER_LINES panel + // rows each. With one frame buffer a whole-frame draw buffer (the default, in + // PSRAM) lets a full-screen redraw - e.g. a rotation change - flush in a + // single pass instead of ten 128-row strips, so the screen repaints at once + // rather than wiping progressively. With the back-buffer flush + // (CONFIG_M5STACK_TAB5_EXAMPLE_FLUSH_TO_BACK_BUFFER) a full redraw is + // assembled off screen whatever the strip size, so a small draw buffer + // looks the same and frees the PSRAM. + const size_t pixel_buffer_size = + tab5.display_width() * static_cast(CONFIG_M5STACK_TAB5_EXAMPLE_DRAW_BUFFER_LINES); + if (!tab5.initialize_display({ + .pixel_buffer_size = pixel_buffer_size, +#if CONFIG_M5STACK_TAB5_EXAMPLE_FLUSH_TO_BACK_BUFFER + .flush_to_back_buffer_enabled = true, +#endif + })) { +#endif logger.error("Failed to initialize display!"); return; } -#endif // make the filter we'll use for the IMU to compute the orientation static constexpr float angle_noise = 0.001f; diff --git a/components/m5stack-tab5/include/m5stack-tab5.hpp b/components/m5stack-tab5/include/m5stack-tab5.hpp index 5d98880402..13b642874f 100644 --- a/components/m5stack-tab5/include/m5stack-tab5.hpp +++ b/components/m5stack-tab5/include/m5stack-tab5.hpp @@ -14,6 +14,11 @@ #include #include #include +#if __has_include() +// ESP-IDF 6.1+: DMA2D window copies (esp_driver_dma), used by the back-buffer flush +#include +#define M5STACK_TAB5_HAS_ASYNC_FBCPY 1 +#endif #include #include #include @@ -237,6 +242,25 @@ class M5StackTab5 : public BaseComponent { /// callback with lv_display_set_flush_wait_cb(); the transfer-done /// interrupt no longer does it. bool render_to_frame_buffers_enabled{false}; + /// Flush into the frame buffer the panel is NOT scanning out, and switch + /// the panel to it once the frame is complete (tear-free, and unlike + /// render_to_frame_buffers_enabled the display rotation keeps working). + /// Every flushed area is copied - rotated when needed - by the PPA into + /// the back frame buffer; when LVGL's last area of a frame has landed, + /// present_frame() switches the panel to that buffer at the frame + /// boundary, and the region this frame changed is copied into the new + /// back buffer (by the PPA, overlapping LVGL's next render) so it is + /// current for the next frame. Partial frames are never on screen, so + /// the draw buffers can be strips (pixel_buffer_size) instead of whole + /// frames: a full-screen redraw is assembled off screen and shown at + /// once. Needs initialize_lcd() first, CONFIG_M5STACK_TAB5_DPI_NUM_FBS >= 2 + /// and a working PPA; ignored with render_to_frame_buffers_enabled. + /// \note Costs: the flush of a frame's last area waits for the panel's + /// frame-done event (one refresh, like DIRECT mode), and the PPA + /// does an extra copy of the changed region per frame. Pixels + /// written with write_lcd_lines() land in the buffer on screen and + /// are not carried over to the next frame: draw through LVGL. + bool flush_to_back_buffer_enabled{false}; }; /// Initialize the LVGL display @@ -1233,6 +1257,48 @@ class M5StackTab5 : public BaseComponent { // initialize_display() set LVGL up in DIRECT mode on the frame buffers // (DisplayConfig::render_to_frame_buffers_enabled) bool direct_render_enabled_{false}; + // initialize_display() set flush() up to land in the back frame buffer + // and switch at the frame boundary (DisplayConfig::flush_to_back_buffer_enabled) + bool back_buffer_flush_enabled_{false}; + // Back-buffer flush bookkeeping, touched by the LVGL task only (flush() + // runs there): the panel-space region the frame in progress has changed so + // far, and per frame buffer the region it has missed since it was last on + // screen - what the sync copy after a switch brings over. + lv_area_t frame_dirty_area_{}; + bool frame_dirty_valid_{false}; + std::array stale_area_{}; + std::array stale_valid_{}; + // flush() for the back-buffer mode (see flush_to_back_buffer_enabled) + void flush_to_back_buffer(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map, + lv_display_rotation_t rotation); + // Queue a non-blocking PPA copy of a block of a source picture into a frame + // buffer, rotated by `angle`, completed through notify_ppa_trans_done(). + // Caller holds panel_op_mutex_ with no transfer in flight (and has set + // lvgl_flush_pending_ as it wants); sets transfer_in_flight_, cleared again + // if the operation is not queued (then returns false). + bool queue_ppa_block(const void *src, uint32_t src_pic_w, uint32_t src_pic_h, uint32_t src_x, + uint32_t src_y, uint32_t block_w, uint32_t block_h, void *dst_frame_buffer, + uint32_t dst_x, uint32_t dst_y, ppa_srm_rotation_angle_t angle); + // Index of a pointer inside one of frame_buffers(), or -1 + int frame_buffer_index(const void *p) const; + // Queue a non-blocking unrotated window copy of a source picture into a + // frame buffer: on the DMA2D engine (ESP-IDF 6.1+, about five times the + // PPA's throughput) when available, else the PPA at angle 0. Same contract + // as queue_ppa_block(). + bool queue_block_copy(const void *src, uint32_t src_stride_px, uint32_t src_height, + uint32_t src_x, uint32_t src_y, uint32_t w, uint32_t h, + void *dst_frame_buffer, uint32_t dst_x, uint32_t dst_y); +#if M5STACK_TAB5_HAS_ASYNC_FBCPY + // DMA2D copy-done callback (ISR) for queue_block_copy(); cb_args is the + // M5StackTab5 instance. + static bool notify_block_copy_done(async_color_convert_handle_t handle, + async_color_convert_event_data_t *edata, void *cb_args); +#endif + // present_frame() with the number of frame-buffer rows whose cache is + // written back before the switch (the driver syncs the rows of the draw): + // the public call syncs the whole buffer for CPU-rendered frames; the + // back-buffer flush, whose frame buffers only DMA engines write, syncs one. + bool present_frame(const void *frame_buffer, uint32_t timeout_ms, int cache_sync_rows); // Display controller detection DisplayController display_controller_{DisplayController::UNKNOWN}; diff --git a/components/m5stack-tab5/src/video.cpp b/components/m5stack-tab5/src/video.cpp index fe97e069dc..1476b95414 100644 --- a/components/m5stack-tab5/src/video.cpp +++ b/components/m5stack-tab5/src/video.cpp @@ -709,6 +709,10 @@ static size_t third_buffer_bytes = 0; // full-frame PSRAM buffer is slow and, because the transpose strides PSRAM, // asymmetric between 90 and 270 degrees; the PPA is fast and symmetric. static ppa_client_handle_t g_ppa_client = nullptr; +#if M5STACK_TAB5_HAS_ASYNC_FBCPY +// DMA2D window-copy driver for the back-buffer flush (installed with it) +static async_color_convert_handle_t g_fbcpy = nullptr; +#endif bool M5StackTab5::initialize_display(size_t pixel_buffer_size) { return initialize_display(DisplayConfig{.pixel_buffer_size = pixel_buffer_size}); @@ -720,6 +724,12 @@ bool M5StackTab5::initialize_display(const DisplayConfig &config) { direct_render_enabled_ ? "on" : "without"); return false; } + if (display_ && !direct_render_enabled_ && + back_buffer_flush_enabled_ != config.flush_to_back_buffer_enabled) { + logger_.error("The LVGL display is already initialized {} the back-buffer flush", + back_buffer_flush_enabled_ ? "with" : "without"); + return false; + } const Display::LvglConfig lvgl_config{ .width = display_width_, .height = display_height_, @@ -794,23 +804,6 @@ bool M5StackTab5::initialize_display(const DisplayConfig &config) { return true; } - const size_t pixel_buffer_size = config.pixel_buffer_size; - logger_.info("Initializing LVGL display with pixel buffer size: {} pixels", pixel_buffer_size); - if (!display_) { - display_ = std::make_shared>( - lvgl_config, oled_config, - Display::DynamicMemoryConfig{ - .pixel_buffer_size = pixel_buffer_size, - .double_buffered = true, - // Allocate the LVGL draw buffers in PSRAM: a full-frame double - // buffer is ~3.7 MB, far larger than internal RAM, and the DPI - // panel's draw_bitmap reads the source straight from PSRAM (the - // esp32-p4-function-ev-board uses the same PSRAM buffers). - .allocation_flags = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, - }, - Logger::Verbosity::WARN); - } - // Register a PPA client for hardware rotation, and allocate the rotation // scratch buffer that the PPA writes into. The PPA output buffer (external / // PSRAM memory) must be aligned to the data cache line size, and both the @@ -848,6 +841,74 @@ bool M5StackTab5::initialize_display(const DisplayConfig &config) { } } } + const size_t pixel_buffer_size = config.pixel_buffer_size; + if (config.flush_to_back_buffer_enabled && !display_) { + // The back-buffer flush writes every area with the PPA into the frame + // buffer the panel is not scanning out, which the DPI driver's own + // draw_bitmap cannot do (it always copies into the buffer being scanned + // out, and treats a frame-buffer pointer as a switch request). + if (num_frame_buffers_ < 2) { + logger_.error("flush_to_back_buffer_enabled needs initialize_lcd() first and " + "CONFIG_M5STACK_TAB5_DPI_NUM_FBS >= 2 ({} frame buffers)", + num_frame_buffers_); + return false; + } + if (g_ppa_client == nullptr || dpi_framebuffer_.load(std::memory_order_acquire) == nullptr) { + logger_.error("flush_to_back_buffer_enabled needs the PPA (client registration or the " + "frame-buffer alignment check failed)"); + return false; + } + // Every buffer the panel is not showing holds whatever it held before: + // bring each one fully up to date the first time it becomes the back + // buffer (one full-frame copy each, once). + const int front = frame_buffer_index(dpi_framebuffer_.load(std::memory_order_acquire)); + for (size_t i = 0; i < num_frame_buffers_; ++i) { + stale_area_[i] = lv_area_t{0, 0, static_cast(display_width_) - 1, + static_cast(display_height_) - 1}; + stale_valid_[i] = static_cast(i) != front; + } + frame_dirty_valid_ = false; +#if M5STACK_TAB5_HAS_ASYNC_FBCPY + // Unrotated copies and the frame-buffer syncs go through the DMA2D + // engine, which moves about five times what the PPA's scale-rotate path + // does (the PPA is kept for rotated areas). Same burst length as the DPI + // driver's own draw_bitmap DMA2D path. + if (g_fbcpy == nullptr) { + async_color_convert_config_t cfg = {}; + cfg.dma_burst_size = 128; + esp_err_t err = esp_async_color_convert_install_dma2d(&cfg, &g_fbcpy); + if (err != ESP_OK) { + logger_.warn("Could not install the DMA2D copy driver ({}); the back-buffer flush copies " + "with the PPA instead (slower)", + esp_err_to_name(err)); + g_fbcpy = nullptr; + } + } + const char *copy_engine = g_fbcpy != nullptr ? "DMA2D" : "PPA"; +#else + const char *copy_engine = "PPA (ESP-IDF < 6.1: no DMA2D window-copy driver)"; +#endif + back_buffer_flush_enabled_ = true; + logger_.info("LVGL flushes land in the back frame buffer ({} frame buffers, switch at the " + "frame boundary, unrotated copies on the {})", + num_frame_buffers_, copy_engine); + } + logger_.info("Initializing LVGL display with pixel buffer size: {} pixels", pixel_buffer_size); + if (!display_) { + display_ = std::make_shared>( + lvgl_config, oled_config, + Display::DynamicMemoryConfig{ + .pixel_buffer_size = pixel_buffer_size, + .double_buffered = true, + // Allocate the LVGL draw buffers in PSRAM: a full-frame double + // buffer is ~3.7 MB, far larger than internal RAM, and the DPI + // panel's draw_bitmap reads the source straight from PSRAM (the + // esp32-p4-function-ev-board uses the same PSRAM buffers). + .allocation_flags = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, + }, + Logger::Verbosity::WARN); + } + // The software-rotation scratch buffer (see ensure_rotation_scratch_buffer()) // is the size of the LVGL draw buffer, aligned to the data cache line like a // PPA output buffer (see kPpaOutBufferAlignment). It is NOT allocated here: @@ -1126,6 +1187,11 @@ void M5StackTab5::write_lcd_lines(int xs, int ys, int xe, int ye, const uint8_t } bool M5StackTab5::present_frame(const void *frame_buffer, uint32_t timeout_ms) { + return present_frame(frame_buffer, timeout_ms, static_cast(display_height_)); +} + +bool M5StackTab5::present_frame(const void *frame_buffer, uint32_t timeout_ms, + int cache_sync_rows) { // frame_buffers() is empty until the lcd_initialized_ gate is open // (acquire), so this also refuses a panel that is not fully initialized. // Any pointer inside a frame buffer selects that buffer, as the driver's @@ -1173,7 +1239,9 @@ bool M5StackTab5::present_frame(const void *frame_buffer, uint32_t timeout_ms) { logger_.error("present_frame(): a previous transfer is wedged; not switching"); return false; } - if (!draw_and_wait(0, 0, display_width_, display_height_, frame_buffer)) { + // The driver writes the cache back for the rows of the draw only, so a + // frame the CPU never wrote (the back-buffer flush) syncs a single row. + if (!draw_and_wait(0, 0, display_width_, cache_sync_rows, frame_buffer)) { logger_.error("present_frame(): draw_bitmap failed"); return false; } @@ -1323,6 +1391,11 @@ void M5StackTab5::flush(lv_display_t *disp, const lv_area_t *area, uint8_t *px_m return; } + if (back_buffer_flush_enabled_) { + flush_to_back_buffer(disp, area, px_map, rotation); + return; + } + // When the panel itself applies the rotation (ST7121, 180 degrees via the // display driver's MADCTL scan flip - see on_display_rotation()), the // logical frame is written to the framebuffer unrotated and at unrotated @@ -1367,34 +1440,15 @@ void M5StackTab5::flush(lv_display_t *disp, const lv_area_t *area, uint8_t *px_m } else if (rotation == LV_DISPLAY_ROTATION_270) { angle = PPA_SRM_ROTATION_ANGLE_270; } - ppa_srm_oper_config_t srm = {}; - srm.in.buffer = px_map; - srm.in.pic_w = ww; - srm.in.pic_h = hh; - srm.in.block_w = ww; - srm.in.block_h = hh; - srm.in.srm_cm = PPA_SRM_COLOR_MODE_RGB565; - srm.out.buffer = target_framebuffer; - srm.out.buffer_size = dpi_framebuffer_bytes_; - srm.out.pic_w = display_width_; - srm.out.pic_h = display_height_; - srm.out.block_offset_x = offsetx1; - srm.out.block_offset_y = offsety1; - srm.out.srm_cm = PPA_SRM_COLOR_MODE_RGB565; - srm.rotation_angle = angle; - srm.scale_x = 1.0f; - srm.scale_y = 1.0f; // Non-blocking, like the DMA2D copy of the unrotated path: the PPA // driver writes back the source window and invalidates the destination // window when the operation is queued, the operation is left in flight // and notify_ppa_trans_done() signals LVGL when it finishes, so LVGL // renders the next area into its other draw buffer meanwhile. The next // panel operation waits for it first (wait_for_transfer_idle()). - srm.mode = PPA_TRANS_MODE_NON_BLOCKING; - srm.user_data = this; // Serialize the framebuffer write with draw_bitmap (write_lcd_lines / // the non-PPA flush tail) so two engines never write the panel FB at once. - esp_err_t ppa_err; + bool queued = false; { std::lock_guard lock(panel_op_mutex_); if (!wait_for_transfer_idle()) { @@ -1407,15 +1461,14 @@ void M5StackTab5::flush(lv_display_t *disp, const lv_area_t *area, uint8_t *px_m // the ISR's acquire on the flag always sees the right display. lvgl_flush_disp_ = disp; lvgl_flush_pending_.store(true, std::memory_order_release); - transfer_in_flight_.store(true, std::memory_order_release); - ppa_err = ppa_do_scale_rotate_mirror(g_ppa_client, &srm); - if (ppa_err != ESP_OK) { + queued = queue_ppa_block(px_map, ww, hh, 0, 0, ww, hh, target_framebuffer, offsetx1, + offsety1, angle); + if (!queued) { // Not queued: no completion will arrive. - transfer_in_flight_.store(false, std::memory_order_release); lvgl_flush_pending_.store(false, std::memory_order_relaxed); } } - if (ppa_err == ESP_OK) { + if (queued) { // The rotated pixels land in the scanned-out framebuffer with the // cache maintenance done by the PPA driver; the espp flush path never // dirties the framebuffer with the CPU (draw_bitmap copies run on the @@ -1425,8 +1478,7 @@ void M5StackTab5::flush(lv_display_t *disp, const lv_area_t *area, uint8_t *px_m // PPA completion callback signals LVGL. return; } - logger_.warn_rate_limited("PPA rotation failed ({}); rotating in software", - esp_err_to_name(ppa_err)); + logger_.warn_rate_limited("PPA rotation failed; rotating in software"); // On failure fall through to the scratch-buffer software rotation: the // framebuffer may hold a partial block, but the fallback redraws the // full area at the same destination. @@ -1508,6 +1560,277 @@ void M5StackTab5::flush(lv_display_t *disp, const lv_area_t *area, uint8_t *px_m } } +int M5StackTab5::frame_buffer_index(const void *p) const { + const auto *b = static_cast(p); + for (size_t i = 0; i < num_frame_buffers_; ++i) { + const auto *base = static_cast(frame_buffers_[i]); + if (base != nullptr && b >= base && b < base + frame_buffer_size()) { + return static_cast(i); + } + } + return -1; +} + +bool M5StackTab5::queue_ppa_block(const void *src, uint32_t src_pic_w, uint32_t src_pic_h, + uint32_t src_x, uint32_t src_y, uint32_t block_w, + uint32_t block_h, void *dst_frame_buffer, uint32_t dst_x, + uint32_t dst_y, ppa_srm_rotation_angle_t angle) { + // The PPA's scale-rotate-mirror engine copies a block out of a source + // picture into a block of a destination picture: used for LVGL's draw + // buffers (the whole buffer is the block) rotated into a frame buffer, and + // for the frame-buffer-to-frame-buffer sync of the back-buffer flush (a + // window of one frame buffer into the same window of another, unrotated). + // The LVGL rotation maps directly onto the PPA rotation angle (LVGL 90 -> + // PPA 90, 180 -> 180, 270 -> 270); this is the mapping verified on + // hardware. If a different panel comes out turned the wrong way, swap the + // 90 and 270 cases in the callers. + ppa_srm_oper_config_t srm = {}; + srm.in.buffer = src; + srm.in.pic_w = src_pic_w; + srm.in.pic_h = src_pic_h; + srm.in.block_offset_x = src_x; + srm.in.block_offset_y = src_y; + srm.in.block_w = block_w; + srm.in.block_h = block_h; + srm.in.srm_cm = PPA_SRM_COLOR_MODE_RGB565; + srm.out.buffer = dst_frame_buffer; + srm.out.buffer_size = dpi_framebuffer_bytes_; + srm.out.pic_w = display_width_; + srm.out.pic_h = display_height_; + srm.out.block_offset_x = dst_x; + srm.out.block_offset_y = dst_y; + srm.out.srm_cm = PPA_SRM_COLOR_MODE_RGB565; + srm.rotation_angle = angle; + srm.scale_x = 1.0f; + srm.scale_y = 1.0f; + // Left in flight; notify_ppa_trans_done() completes it (see complete_transfer()). + srm.mode = PPA_TRANS_MODE_NON_BLOCKING; + srm.user_data = this; + transfer_in_flight_.store(true, std::memory_order_release); + esp_err_t err = ppa_do_scale_rotate_mirror(g_ppa_client, &srm); + if (err != ESP_OK) { + // Not queued: no completion will arrive. + transfer_in_flight_.store(false, std::memory_order_release); + logger_.warn_rate_limited("PPA block copy ({}x{} -> {},{}, angle {}) failed: {}", block_w, + block_h, dst_x, dst_y, static_cast(angle), esp_err_to_name(err)); + return false; + } + return true; +} + +bool M5StackTab5::queue_block_copy(const void *src, uint32_t src_stride_px, uint32_t src_height, + uint32_t src_x, uint32_t src_y, uint32_t w, uint32_t h, + void *dst_frame_buffer, uint32_t dst_x, uint32_t dst_y) { +#if M5STACK_TAB5_HAS_ASYNC_FBCPY + if (g_fbcpy != nullptr) { + // The driver writes the source window's cache back and invalidates the + // destination window itself; left in flight, notify_block_copy_done() + // completes it (see complete_transfer()). + async_color_convert_request_t req = {}; + req.src_buffer = src; + req.src_stride = src_stride_px; + req.src_height = src_height; + req.src_x = src_x; + req.src_y = src_y; + req.dst_buffer = dst_frame_buffer; + req.dst_stride = display_width_; + req.dst_height = display_height_; + req.dst_x = dst_x; + req.dst_y = dst_y; + req.copy_width = w; + req.copy_height = h; + req.src_color_format = ESP_COLOR_FOURCC_RGB16; + req.dst_color_format = ESP_COLOR_FOURCC_RGB16; + transfer_in_flight_.store(true, std::memory_order_release); + esp_err_t err = + esp_async_color_convert(g_fbcpy, &req, &M5StackTab5::notify_block_copy_done, this); + if (err != ESP_OK) { + transfer_in_flight_.store(false, std::memory_order_release); + logger_.warn_rate_limited("DMA2D block copy ({}x{} -> {},{}) failed: {}", w, h, dst_x, dst_y, + esp_err_to_name(err)); + return false; + } + return true; + } +#endif + return queue_ppa_block(src, src_stride_px, src_height, src_x, src_y, w, h, dst_frame_buffer, + dst_x, dst_y, PPA_SRM_ROTATION_ANGLE_0); +} + +#if M5STACK_TAB5_HAS_ASYNC_FBCPY +bool IRAM_ATTR M5StackTab5::notify_block_copy_done(async_color_convert_handle_t handle, + async_color_convert_event_data_t *edata, + void *cb_args) { + (void)handle; + (void)edata; + auto *tab5 = static_cast(cb_args); + if (tab5 == nullptr) { + return false; + } + // ISR context (the DMA2D transfer-done interrupt): the window copy the + // back-buffer flush queued is complete. + BaseType_t higher_priority_task_woken = pdFALSE; + tab5->complete_transfer(&higher_priority_task_woken); + return higher_priority_task_woken == pdTRUE; +} +#endif + +// The parts of `a` that `b` does not cover, as up to four rectangles (a band +// above and below the overlap, and the rows of the overlap left and right of +// it). Zero when `b` covers `a`, one (`a` itself) when they do not meet. +static size_t subtract_area(const lv_area_t &a, const lv_area_t &b, lv_area_t out[4]) { + if (b.x1 > a.x2 || b.x2 < a.x1 || b.y1 > a.y2 || b.y2 < a.y1) { + out[0] = a; + return 1; + } + size_t n = 0; + if (b.y1 > a.y1) { + out[n++] = lv_area_t{a.x1, a.y1, a.x2, b.y1 - 1}; + } + if (b.y2 < a.y2) { + out[n++] = lv_area_t{a.x1, b.y2 + 1, a.x2, a.y2}; + } + const int32_t y1 = std::max(a.y1, b.y1); + const int32_t y2 = std::min(a.y2, b.y2); + if (b.x1 > a.x1) { + out[n++] = lv_area_t{a.x1, y1, b.x1 - 1, y2}; + } + if (b.x2 < a.x2) { + out[n++] = lv_area_t{b.x2 + 1, y1, a.x2, y2}; + } + return n; +} + +static void join_area(lv_area_t &acc, bool &valid, const lv_area_t &a) { + if (!valid) { + acc = a; + valid = true; + return; + } + acc.x1 = std::min(acc.x1, a.x1); + acc.y1 = std::min(acc.y1, a.y1); + acc.x2 = std::max(acc.x2, a.x2); + acc.y2 = std::max(acc.y2, a.y2); +} + +void M5StackTab5::flush_to_back_buffer(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map, + lv_display_rotation_t rotation) { + // DisplayConfig::flush_to_back_buffer_enabled: every area goes, through the + // PPA, into the frame buffer the panel is not scanning out; the frame's + // last area is waited for, the panel is switched to that buffer at the + // frame boundary (present_frame()), and the region this frame changed is + // copied into the next back buffer so it is current when LVGL renders the + // next frame into it. The panel never shows a half-drawn frame, whatever + // size the draw buffers are, and rotation works as in the plain flush: + // the PPA rotates each area into its rotated place. + // + // Only the LVGL task runs this, so the dirty-region bookkeeping needs no + // lock; the frame-buffer writes are serialized with every other panel + // operation under panel_op_mutex_ and the single-flight transfer token. + lv_area_t dst = *area; + ppa_srm_rotation_angle_t angle = PPA_SRM_ROTATION_ANGLE_0; + if (rotation > LV_DISPLAY_ROTATION_0 && !panel_handles_rotation(rotation)) { + // Panel coordinates of this area (the panel's own 180-degree flip, when + // it applies one, happens at scan-out: write unrotated for that one). + lv_display_rotate_area(disp, &dst); + if (rotation == LV_DISPLAY_ROTATION_90) { + angle = PPA_SRM_ROTATION_ANGLE_90; + } else if (rotation == LV_DISPLAY_ROTATION_180) { + angle = PPA_SRM_ROTATION_ANGLE_180; + } else { + angle = PPA_SRM_ROTATION_ANGLE_270; + } + } + const uint32_t ww = lv_area_get_width(area); + const uint32_t hh = lv_area_get_height(area); + const int front = frame_buffer_index(dpi_framebuffer_.load(std::memory_order_acquire)); + if (front < 0) { + // cannot happen once initialize_display() accepted the mode + lv_display_flush_ready(disp); + return; + } + const int back = (front + 1) % static_cast(num_frame_buffers_); + void *const back_fb = frame_buffers_[back]; + const bool last = lv_display_flush_is_last(disp); + join_area(frame_dirty_area_, frame_dirty_valid_, dst); + { + std::lock_guard lock(panel_op_mutex_); + if (!wait_for_transfer_idle()) { + lv_display_flush_ready(disp); // wedged: drop this area, keep LVGL alive + return; + } + // An area that is not the frame's last releases LVGL from the PPA's + // completion ISR, like the plain flush. The last one is waited for below + // (the switch must not be queued before it has landed) and releases LVGL + // after the switch. + lvgl_flush_disp_ = disp; + lvgl_flush_pending_.store(!last, std::memory_order_release); + const bool queued = + angle == PPA_SRM_ROTATION_ANGLE_0 + ? queue_block_copy(px_map, ww, hh, 0, 0, ww, hh, back_fb, dst.x1, dst.y1) + : queue_ppa_block(px_map, ww, hh, 0, 0, ww, hh, back_fb, dst.x1, dst.y1, angle); + if (!queued) { + lvgl_flush_pending_.store(false, std::memory_order_relaxed); + lv_display_flush_ready(disp); + return; + } + } + if (!last) { + return; + } + // The frame's areas are in the back buffer (once the last copy lands). + // Everything else in that buffer is as old as the last time it was on + // screen: copy over, from the buffer on screen, the region it has missed + // since then MINUS what this frame just redrew. For a full-screen redraw + // that is nothing; for a small update it is the previous frame's small + // region. Done now rather than after the switch so the copy never races + // the next frame's areas, and so it costs only what the frame did not + // redraw. + void *const front_fb = frame_buffers_[front]; + if (stale_valid_[back]) { + lv_area_t parts[4]; + const size_t n = subtract_area(stale_area_[back], frame_dirty_area_, parts); + for (size_t i = 0; i < n; ++i) { + std::lock_guard lock(panel_op_mutex_); + if (!wait_for_transfer_idle()) { + break; // wedged: the frame is shown with that region stale + } + lvgl_flush_pending_.store(false, std::memory_order_relaxed); + if (!queue_block_copy(front_fb, display_width_, display_height_, parts[i].x1, parts[i].y1, + lv_area_get_width(&parts[i]), lv_area_get_height(&parts[i]), back_fb, + parts[i].x1, parts[i].y1)) { + break; + } + } + } + { + std::lock_guard lock(panel_op_mutex_); + if (!wait_for_transfer_idle()) { + // A copy never landed: do not show the frame (the switch would present + // a half-drawn one); its region stays in frame_dirty_area_ and is + // carried into the next frame's region. + lv_display_flush_ready(disp); + return; + } + } + // The back buffer is now complete. What this frame changed is what every + // other buffer lacks from now on; the buffer about to be shown lacks nothing. + for (size_t i = 0; i < num_frame_buffers_; ++i) { + if (static_cast(i) != back) { + join_area(stale_area_[i], stale_valid_[i], frame_dirty_area_); + } + } + stale_valid_[back] = false; + frame_dirty_valid_ = false; + // Switch at the frame boundary. The return value is deliberately not acted + // on: on a timeout the switch is still queued and LVGL carries on (that + // frame may tear); after a few consecutive timeouts present_frame() + // disables its wait itself. One row of cache write-back: only DMA engines + // write these buffers. + present_frame(back_fb, 100, 1); + lv_display_flush_ready(disp); +} + bool IRAM_ATTR M5StackTab5::notify_lvgl_flush_ready(esp_lcd_panel_handle_t panel, esp_lcd_dpi_panel_event_data_t *edata, void *user_ctx) { From be4c38fb73fb98844d3a1600a87d780961634e2b Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Tue, 3 Nov 2026 14:30:43 -0600 Subject: [PATCH 2/2] feat(m5stack-tab5): compile the back-buffer flush in only with CONFIG_M5STACK_TAB5_BACK_BUFFER_FLUSH Default off: without it nothing of the mode is built (no members, no DMA2D driver include, the flush path is what it was) and initialize_display() refuses DisplayConfig::flush_to_back_buffer_enabled with an error naming the option. With it on, the per-display flag still defaults to off and nothing is allocated or installed until it is set. The example's menuconfig option depends on it. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU --- components/m5stack-tab5/Kconfig | 18 ++++++++++++ components/m5stack-tab5/README.md | 10 +++++-- .../example/main/Kconfig.projbuild | 5 ++-- .../m5stack-tab5/include/m5stack-tab5.hpp | 29 +++++++++++-------- components/m5stack-tab5/src/video.cpp | 15 ++++++++++ 5 files changed, 60 insertions(+), 17 deletions(-) diff --git a/components/m5stack-tab5/Kconfig b/components/m5stack-tab5/Kconfig index 67f02663fa..8bb4141acc 100644 --- a/components/m5stack-tab5/Kconfig +++ b/components/m5stack-tab5/Kconfig @@ -74,4 +74,22 @@ menu "M5Stack Tab5 Configuration" application that drives the frame buffers itself. Extra buffers without DIRECT mode buy nothing: LVGL keeps copying into the scanned-out buffer. + config M5STACK_TAB5_BACK_BUFFER_FLUSH + bool "Compile in the back-buffer LVGL flush (tear-free with rotation)" + depends on M5STACK_TAB5_DPI_NUM_FBS > 1 + default n + help + Compile in DisplayConfig::flush_to_back_buffer_enabled: LVGL keeps its + draw buffers, every flushed area is copied (DMA2D; PPA when rotated) + into the frame buffer the panel is not scanning out, and the panel + switches to it at the frame boundary once the frame is complete, so + frames never tear and the display rotation keeps working (unlike + LVGL's DIRECT mode). Off, nothing of it is built: the flush path is + unchanged and initialize_display() refuses the option. On, it is still + opt-in per display (the DisplayConfig flag defaults to off; nothing is + allocated or installed until it is set). Needs 2 frame buffers (1.8 MB + of PSRAM for the second) and the PPA; a full-screen redraw then takes + two panel periods (the frame-done wait) and the draw buffers can be + small strips: see the README's measurements. + endmenu diff --git a/components/m5stack-tab5/README.md b/components/m5stack-tab5/README.md index d02441cd3e..6d4f693d43 100644 --- a/components/m5stack-tab5/README.md +++ b/components/m5stack-tab5/README.md @@ -155,8 +155,11 @@ DIRECT mode cannot rotate. The back-buffer flush keeps LVGL's normal partial rendering into draw buffers (so the PPA rotation of each area, and the panel's own 180-degree flip, keep working) and gets rid of tearing differently: -- `initialize_display({.flush_to_back_buffer_enabled = true})` (needs - `CONFIG_M5STACK_TAB5_DPI_NUM_FBS` >= 2 and the PPA). Every flushed area is +- Compiled in with `CONFIG_M5STACK_TAB5_BACK_BUFFER_FLUSH` (off by default: + an application that does not enable it carries none of the code, and its + flush path is unchanged). Then `initialize_display({.flush_to_back_buffer_enabled + = true})` (needs `CONFIG_M5STACK_TAB5_DPI_NUM_FBS` >= 2 and the PPA) turns it + on per display; nothing is allocated or installed until it is set. Every flushed area is copied, rotated when needed, by the PPA into the frame buffer the panel is *not* scanning out; nothing is drawn into the buffer on screen. - When LVGL's last area of a frame has landed, the flush switches the panel to @@ -174,7 +177,8 @@ own 180-degree flip, keep working) and gets rid of tearing differently: assembled off screen and shown at once. ```cpp -// menuconfig: M5Stack Tab5 Configuration -> Number of DPI frame buffers = 2 +// menuconfig: M5Stack Tab5 Configuration -> Number of DPI frame buffers = 2, +// Compile in the back-buffer LVGL flush = y auto &tab5 = espp::M5StackTab5::get(); tab5.initialize_lcd(); tab5.initialize_display({.pixel_buffer_size = 720 * 128, // 128-row strips (184 KB each) diff --git a/components/m5stack-tab5/example/main/Kconfig.projbuild b/components/m5stack-tab5/example/main/Kconfig.projbuild index add8780db8..1f095eb548 100644 --- a/components/m5stack-tab5/example/main/Kconfig.projbuild +++ b/components/m5stack-tab5/example/main/Kconfig.projbuild @@ -18,11 +18,12 @@ menu "M5Stack Tab5 Example Configuration" config M5STACK_TAB5_EXAMPLE_FLUSH_TO_BACK_BUFFER bool "Flush LVGL into the back frame buffer (tear-free, rotation works)" - depends on M5STACK_TAB5_DPI_NUM_FBS > 1 && !M5STACK_TAB5_EXAMPLE_RENDER_TO_FRAME_BUFFERS + depends on M5STACK_TAB5_BACK_BUFFER_FLUSH && !M5STACK_TAB5_EXAMPLE_RENDER_TO_FRAME_BUFFERS default n help Initialize the display with - DisplayConfig::flush_to_back_buffer_enabled: every LVGL flush is + DisplayConfig::flush_to_back_buffer_enabled (compiled in with + CONFIG_M5STACK_TAB5_BACK_BUFFER_FLUSH, M5Stack Tab5 Configuration): every LVGL flush is copied (and rotated) by the PPA into the frame buffer the panel is not scanning out, and the panel switches to it at the frame boundary once the frame is complete. Partial frames are never on diff --git a/components/m5stack-tab5/include/m5stack-tab5.hpp b/components/m5stack-tab5/include/m5stack-tab5.hpp index 13b642874f..e9eab76c86 100644 --- a/components/m5stack-tab5/include/m5stack-tab5.hpp +++ b/components/m5stack-tab5/include/m5stack-tab5.hpp @@ -14,7 +14,8 @@ #include #include #include -#if __has_include() +#include +#if CONFIG_M5STACK_TAB5_BACK_BUFFER_FLUSH && __has_include() // ESP-IDF 6.1+: DMA2D window copies (esp_driver_dma), used by the back-buffer flush #include #define M5STACK_TAB5_HAS_ASYNC_FBCPY 1 @@ -254,7 +255,9 @@ class M5StackTab5 : public BaseComponent { /// the draw buffers can be strips (pixel_buffer_size) instead of whole /// frames: a full-screen redraw is assembled off screen and shown at /// once. Needs initialize_lcd() first, CONFIG_M5STACK_TAB5_DPI_NUM_FBS >= 2 - /// and a working PPA; ignored with render_to_frame_buffers_enabled. + /// and a working PPA; ignored with render_to_frame_buffers_enabled. The + /// mode is compiled in with CONFIG_M5STACK_TAB5_BACK_BUFFER_FLUSH (off by + /// default: without it nothing of it is built and this flag is refused). /// \note Costs: the flush of a frame's last area waits for the panel's /// frame-done event (one refresh, like DIRECT mode), and the PPA /// does an extra copy of the changed region per frame. Pixels @@ -1257,6 +1260,17 @@ class M5StackTab5 : public BaseComponent { // initialize_display() set LVGL up in DIRECT mode on the frame buffers // (DisplayConfig::render_to_frame_buffers_enabled) bool direct_render_enabled_{false}; + // Queue a non-blocking PPA copy of a block of a source picture into a frame + // buffer, rotated by `angle`, completed through notify_ppa_trans_done(). + // Caller holds panel_op_mutex_ with no transfer in flight (and has set + // lvgl_flush_pending_ as it wants); sets transfer_in_flight_, cleared again + // if the operation is not queued (then returns false). + bool queue_ppa_block(const void *src, uint32_t src_pic_w, uint32_t src_pic_h, uint32_t src_x, + uint32_t src_y, uint32_t block_w, uint32_t block_h, void *dst_frame_buffer, + uint32_t dst_x, uint32_t dst_y, ppa_srm_rotation_angle_t angle); + // Index of a pointer inside one of frame_buffers(), or -1 + int frame_buffer_index(const void *p) const; +#if CONFIG_M5STACK_TAB5_BACK_BUFFER_FLUSH // initialize_display() set flush() up to land in the back frame buffer // and switch at the frame boundary (DisplayConfig::flush_to_back_buffer_enabled) bool back_buffer_flush_enabled_{false}; @@ -1271,16 +1285,6 @@ class M5StackTab5 : public BaseComponent { // flush() for the back-buffer mode (see flush_to_back_buffer_enabled) void flush_to_back_buffer(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map, lv_display_rotation_t rotation); - // Queue a non-blocking PPA copy of a block of a source picture into a frame - // buffer, rotated by `angle`, completed through notify_ppa_trans_done(). - // Caller holds panel_op_mutex_ with no transfer in flight (and has set - // lvgl_flush_pending_ as it wants); sets transfer_in_flight_, cleared again - // if the operation is not queued (then returns false). - bool queue_ppa_block(const void *src, uint32_t src_pic_w, uint32_t src_pic_h, uint32_t src_x, - uint32_t src_y, uint32_t block_w, uint32_t block_h, void *dst_frame_buffer, - uint32_t dst_x, uint32_t dst_y, ppa_srm_rotation_angle_t angle); - // Index of a pointer inside one of frame_buffers(), or -1 - int frame_buffer_index(const void *p) const; // Queue a non-blocking unrotated window copy of a source picture into a // frame buffer: on the DMA2D engine (ESP-IDF 6.1+, about five times the // PPA's throughput) when available, else the PPA at angle 0. Same contract @@ -1294,6 +1298,7 @@ class M5StackTab5 : public BaseComponent { static bool notify_block_copy_done(async_color_convert_handle_t handle, async_color_convert_event_data_t *edata, void *cb_args); #endif +#endif // CONFIG_M5STACK_TAB5_BACK_BUFFER_FLUSH // present_frame() with the number of frame-buffer rows whose cache is // written back before the switch (the driver syncs the rows of the draw): // the public call syncs the whole buffer for CPU-rendered frames; the diff --git a/components/m5stack-tab5/src/video.cpp b/components/m5stack-tab5/src/video.cpp index 1476b95414..266103b917 100644 --- a/components/m5stack-tab5/src/video.cpp +++ b/components/m5stack-tab5/src/video.cpp @@ -724,12 +724,21 @@ bool M5StackTab5::initialize_display(const DisplayConfig &config) { direct_render_enabled_ ? "on" : "without"); return false; } +#if CONFIG_M5STACK_TAB5_BACK_BUFFER_FLUSH if (display_ && !direct_render_enabled_ && back_buffer_flush_enabled_ != config.flush_to_back_buffer_enabled) { logger_.error("The LVGL display is already initialized {} the back-buffer flush", back_buffer_flush_enabled_ ? "with" : "without"); return false; } +#else + if (config.flush_to_back_buffer_enabled) { + logger_.error( + "flush_to_back_buffer_enabled is not compiled in: enable " + "CONFIG_M5STACK_TAB5_BACK_BUFFER_FLUSH (needs CONFIG_M5STACK_TAB5_DPI_NUM_FBS >= 2)"); + return false; + } +#endif const Display::LvglConfig lvgl_config{ .width = display_width_, .height = display_height_, @@ -842,6 +851,7 @@ bool M5StackTab5::initialize_display(const DisplayConfig &config) { } } const size_t pixel_buffer_size = config.pixel_buffer_size; +#if CONFIG_M5STACK_TAB5_BACK_BUFFER_FLUSH if (config.flush_to_back_buffer_enabled && !display_) { // The back-buffer flush writes every area with the PPA into the frame // buffer the panel is not scanning out, which the DPI driver's own @@ -893,6 +903,7 @@ bool M5StackTab5::initialize_display(const DisplayConfig &config) { "frame boundary, unrotated copies on the {})", num_frame_buffers_, copy_engine); } +#endif logger_.info("Initializing LVGL display with pixel buffer size: {} pixels", pixel_buffer_size); if (!display_) { display_ = std::make_shared>( @@ -1391,10 +1402,12 @@ void M5StackTab5::flush(lv_display_t *disp, const lv_area_t *area, uint8_t *px_m return; } +#if CONFIG_M5STACK_TAB5_BACK_BUFFER_FLUSH if (back_buffer_flush_enabled_) { flush_to_back_buffer(disp, area, px_map, rotation); return; } +#endif // When the panel itself applies the rotation (ST7121, 180 degrees via the // display driver's MADCTL scan flip - see on_display_rotation()), the @@ -1618,6 +1631,7 @@ bool M5StackTab5::queue_ppa_block(const void *src, uint32_t src_pic_w, uint32_t return true; } +#if CONFIG_M5STACK_TAB5_BACK_BUFFER_FLUSH bool M5StackTab5::queue_block_copy(const void *src, uint32_t src_stride_px, uint32_t src_height, uint32_t src_x, uint32_t src_y, uint32_t w, uint32_t h, void *dst_frame_buffer, uint32_t dst_x, uint32_t dst_y) { @@ -1830,6 +1844,7 @@ void M5StackTab5::flush_to_back_buffer(lv_display_t *disp, const lv_area_t *area present_frame(back_fb, 100, 1); lv_display_flush_ready(disp); } +#endif // CONFIG_M5STACK_TAB5_BACK_BUFFER_FLUSH bool IRAM_ATTR M5StackTab5::notify_lvgl_flush_ready(esp_lcd_panel_handle_t panel, esp_lcd_dpi_panel_event_data_t *edata,