From 08eac48e641b5771c35e7a233aba7c6aff1dd22b Mon Sep 17 00:00:00 2001 From: Shadowtrance Date: Thu, 23 Jul 2026 07:58:34 +1000 Subject: [PATCH] Fix Tab5 Display Issues (#579) And add PPA rotation to lvgl_display --- .../Source/devices/devices_v1.cpp | 6 +- .../Source/devices/devices_v2.cpp | 6 +- Devices/m5stack-tab5/device.properties | 3 +- Drivers/ili9881c-module/source/ili9881c.cpp | 64 ++++++ Drivers/st7123-module/source/st7123.cpp | 64 ++++++ Modules/lvgl-module/CMakeLists.txt | 4 + .../lvgl-module/include/tactility/lvgl_ppa.h | 47 +++++ Modules/lvgl-module/source/lvgl_display.c | 83 ++++++-- Modules/lvgl-module/source/lvgl_ppa.c | 182 ++++++++++++++++++ 9 files changed, 440 insertions(+), 19 deletions(-) create mode 100644 Modules/lvgl-module/include/tactility/lvgl_ppa.h create mode 100644 Modules/lvgl-module/source/lvgl_ppa.c diff --git a/Devices/m5stack-tab5/Source/devices/devices_v1.cpp b/Devices/m5stack-tab5/Source/devices/devices_v1.cpp index 68a773db..280f2ef9 100644 --- a/Devices/m5stack-tab5/Source/devices/devices_v1.cpp +++ b/Devices/m5stack-tab5/Source/devices/devices_v1.cpp @@ -113,10 +113,10 @@ void tab5_create_devices_v1(Device* i2c0) { .vsync_pulse_width = 4, .vsync_back_porch = 20, .vsync_front_porch = 20, - .num_fbs = 1, - .use_dma2d = false, + .num_fbs = 2, + .use_dma2d = true, .disable_lp = false, - .allow_tearing = true, // matches old lvgl_port_display_dsi_cfg_t.avoid_tearing = 0 + .allow_tearing = true, // matches old lvgl_port_display_dsi_cfg_t.avoid_tearing = 0 (disabled = don't wait) .init_sequence = display_init_bytes.data(), .init_sequence_length = (uint32_t)display_init_bytes.size(), .backlight = backlight, diff --git a/Devices/m5stack-tab5/Source/devices/devices_v2.cpp b/Devices/m5stack-tab5/Source/devices/devices_v2.cpp index 4621d969..1d7d610c 100644 --- a/Devices/m5stack-tab5/Source/devices/devices_v2.cpp +++ b/Devices/m5stack-tab5/Source/devices/devices_v2.cpp @@ -92,10 +92,10 @@ void tab5_create_devices_v2(Device* i2c0) { .vsync_pulse_width = 2, .vsync_back_porch = 8, .vsync_front_porch = 220, - .num_fbs = 1, - .use_dma2d = false, + .num_fbs = 2, + .use_dma2d = true, .disable_lp = false, - .allow_tearing = true, + .allow_tearing = true, // matches old lvgl_port_display_dsi_cfg_t.avoid_tearing = 0 (disabled = don't wait) .init_sequence = display_init_bytes.data(), .init_sequence_length = (uint32_t)display_init_bytes.size(), .backlight = backlight, diff --git a/Devices/m5stack-tab5/device.properties b/Devices/m5stack-tab5/device.properties index a7a9ca58..8ebaaa74 100644 --- a/Devices/m5stack-tab5/device.properties +++ b/Devices/m5stack-tab5/device.properties @@ -46,8 +46,7 @@ sdkconfig.CONFIG_ESP_HOSTED_ENABLE_PEER_DATA_TRANSFER=y sdkconfig.CONFIG_ESP_HOSTED_MAX_CUSTOM_MSG_HANDLERS=8 # Performance: larger L2 cache reduces PSRAM stalls for draw/DPI buffers sdkconfig.CONFIG_CACHE_L2_CACHE_256KB=y -# Performance: use P4's PPA (pixel processing accelerator for rotation) -sdkconfig.CONFIG_LVGL_PORT_ENABLE_PPA=y +# Performance: cache-line-aligned draw buffers, required for PPA/DMA2D coherency (see lvgl-module's lvgl_display_alloc_buffer()) sdkconfig.CONFIG_LV_DRAW_BUF_ALIGN=64 sdkconfig.CONFIG_LV_DEF_REFR_PERIOD=15 # SC202CS (SC2356) MIPI CSI camera sensor + ISP pipeline (AE/AWB/demosaicing) diff --git a/Drivers/ili9881c-module/source/ili9881c.cpp b/Drivers/ili9881c-module/source/ili9881c.cpp index fd3e2ac2..3a1438c3 100644 --- a/Drivers/ili9881c-module/source/ili9881c.cpp +++ b/Drivers/ili9881c-module/source/ili9881c.cpp @@ -44,6 +44,11 @@ struct Ili9881cInternal { // draw_bitmap() when color_data is one of frame_buffers and avoid_tearing is set - see the // comment there for why. SemaphoreHandle_t frame_complete_semaphore; + // Signaled by on_color_trans_done once the panel driver's own copy/DMA2D transfer of a + // draw_bitmap() call's pixels into the target frame buffer completes. Unlike + // frame_complete_semaphore this is always waited on (regardless of allow_tearing or whether + // color_data is a real frame buffer) - see the comment on draw_bitmap() for why. + SemaphoreHandle_t color_trans_done_semaphore; // Heap-allocated only when the devicetree supplies a custom init_sequence (see // parse_init_sequence()) - nullptr otherwise, since the vendor's built-in default sequence // needs no parsing. Its .data pointers alias directly into the devicetree's static const @@ -69,6 +74,30 @@ static bool on_refresh_done(esp_lcd_panel_handle_t, esp_lcd_dpi_panel_event_data return high_task_woken == pdTRUE; } +// esp_lcd_dpi_panel_draw_bitmap() copies (memcpy, or DMA2D when use_dma2d is set) the caller's +// pixels into the target frame buffer and returns immediately - the copy itself finishes +// asynchronously. Calling draw_bitmap() again before that copy completes hits the panel driver's +// own re-entrancy guard and logs "previous draw operation is not finished" (lcd.dsi). This is +// unconditional, unlike frame_complete_semaphore's tearing-avoidance wait: it protects the copy +// itself, not scan-out, so it must be waited on regardless of allow_tearing or which buffer +// color_data is. +// +// Unlike on_refresh_done (always ISR, from the VSYNC interrupt), esp_lcd_panel_dpi.c invokes this +// callback from two different contexts depending on the copy path: synchronously from the calling +// task for the CPU-memcpy and fb-direct (zero-copy) paths, but from an ISR (async_fbcpy_done_cb, +// the DMA2D completion interrupt) when use_dma2d is set. Giving a semaphore with the ISR-only API +// from task context is undefined behavior on FreeRTOS, so the context must be checked at runtime. +static bool on_color_trans_done(esp_lcd_panel_handle_t, esp_lcd_dpi_panel_event_data_t*, void* user_ctx) { + auto* internal = static_cast(user_ctx); + if (xPortInIsrContext()) { + BaseType_t high_task_woken = pdFALSE; + xSemaphoreGiveFromISR(internal->color_trans_done_semaphore, &high_task_woken); + return high_task_woken == pdTRUE; + } + xSemaphoreGive(internal->color_trans_done_semaphore); + return false; +} + static int pin_or_unused(const GpioPinSpec& pin) { return pin.gpio_controller == nullptr ? -1 : static_cast(pin.pin); } @@ -310,10 +339,27 @@ static error_t start(Device* device) { return ERROR_OUT_OF_MEMORY; } + internal->color_trans_done_semaphore = xSemaphoreCreateBinary(); + if (internal->color_trans_done_semaphore == nullptr) { + vSemaphoreDelete(internal->frame_complete_semaphore); + esp_lcd_panel_del(internal->panel_handle); + esp_lcd_panel_io_del(internal->io_handle); + esp_lcd_del_dsi_bus(internal->dsi_bus_handle); + esp_ldo_release_channel(internal->ldo_handle); + free(internal->parsed_init_cmds); + free(internal); + return ERROR_OUT_OF_MEMORY; + } + // The panel starts out idle (no draw_bitmap() call in flight), so the first draw_bitmap() + // must not block waiting for a completion event that will never come. + xSemaphoreGive(internal->color_trans_done_semaphore); + esp_lcd_dpi_panel_event_callbacks_t callbacks = {}; callbacks.on_refresh_done = on_refresh_done; + callbacks.on_color_trans_done = on_color_trans_done; if (esp_lcd_dpi_panel_register_event_callbacks(internal->panel_handle, &callbacks, internal) != ESP_OK) { LOG_E(TAG, "Failed to register panel event callbacks"); + vSemaphoreDelete(internal->color_trans_done_semaphore); vSemaphoreDelete(internal->frame_complete_semaphore); esp_lcd_panel_del(internal->panel_handle); esp_lcd_panel_io_del(internal->io_handle); @@ -364,6 +410,7 @@ static error_t stop(Device* device) { } vSemaphoreDelete(internal->frame_complete_semaphore); + vSemaphoreDelete(internal->color_trans_done_semaphore); free(internal->parsed_init_cmds); free(internal); device_set_driver_data(device, nullptr); @@ -403,15 +450,32 @@ static bool ili9881c_color_data_is_frame_buffer(const Ili9881cInternal* internal static error_t ili9881c_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) { auto* internal = static_cast(device_get_driver_data(device)); + // Wait for the previous draw_bitmap()'s copy/DMA2D transfer to finish before issuing a new + // one - see on_color_trans_done's comment for why this is unconditional (unlike the + // tearing-avoidance wait below). The semaphore starts pre-given (see start()), so the first + // call doesn't block here. + xSemaphoreTake(internal->color_trans_done_semaphore, portMAX_DELAY); + bool wait_for_scanout = !GET_CONFIG(device)->allow_tearing && ili9881c_color_data_is_frame_buffer(internal, color_data); if (wait_for_scanout) { xSemaphoreTake(internal->frame_complete_semaphore, 0); // clear any already-pending signal } if (esp_lcd_panel_draw_bitmap(internal->panel_handle, x_start, y_start, x_end, y_end, color_data) != ESP_OK) { + xSemaphoreGive(internal->color_trans_done_semaphore); return ERROR_RESOURCE; } + // Also wait for *this* call's own copy to finish before returning: color_data may be a + // caller-owned buffer (e.g. LVGL's draw buffer in owned-buffer/PARTIAL mode), and the + // CPU-memcpy/DMA2D copy paths both read directly from it asynchronously - draw_bitmap() + // returning early would let the caller start overwriting color_data (DisplayApi's contract + // treats draw_bitmap as synchronous - see lvgl_display_flush_cb()'s lv_display_flush_ready() + // call right after) while that read is still in flight. on_color_trans_done() re-gives the + // semaphore here, restoring the pre-given/idle state for the next call. + xSemaphoreTake(internal->color_trans_done_semaphore, portMAX_DELAY); + xSemaphoreGive(internal->color_trans_done_semaphore); + if (wait_for_scanout) { xSemaphoreTake(internal->frame_complete_semaphore, portMAX_DELAY); } diff --git a/Drivers/st7123-module/source/st7123.cpp b/Drivers/st7123-module/source/st7123.cpp index f3b52096..583480f4 100644 --- a/Drivers/st7123-module/source/st7123.cpp +++ b/Drivers/st7123-module/source/st7123.cpp @@ -44,6 +44,11 @@ struct St7123Internal { // draw_bitmap() when color_data is one of frame_buffers and avoid_tearing is set - see the // comment there for why. SemaphoreHandle_t frame_complete_semaphore; + // Signaled by on_color_trans_done once the panel driver's own copy/DMA2D transfer of a + // draw_bitmap() call's pixels into the target frame buffer completes. Unlike + // frame_complete_semaphore this is always waited on (regardless of allow_tearing or whether + // color_data is a real frame buffer) - see the comment on draw_bitmap() for why. + SemaphoreHandle_t color_trans_done_semaphore; // Heap-allocated only when the devicetree supplies a custom init_sequence (see // parse_init_sequence()) - nullptr otherwise, since the vendor's built-in default sequence // needs no parsing. Its .data pointers alias directly into the devicetree's static const @@ -69,6 +74,30 @@ static bool on_refresh_done(esp_lcd_panel_handle_t, esp_lcd_dpi_panel_event_data return high_task_woken == pdTRUE; } +// esp_lcd_dpi_panel_draw_bitmap() copies (memcpy, or DMA2D when use_dma2d is set) the caller's +// pixels into the target frame buffer and returns immediately - the copy itself finishes +// asynchronously. Calling draw_bitmap() again before that copy completes hits the panel driver's +// own re-entrancy guard and logs "previous draw operation is not finished" (lcd.dsi). This is +// unconditional, unlike frame_complete_semaphore's tearing-avoidance wait: it protects the copy +// itself, not scan-out, so it must be waited on regardless of allow_tearing or which buffer +// color_data is. +// +// Unlike on_refresh_done (always ISR, from the VSYNC interrupt), esp_lcd_panel_dpi.c invokes this +// callback from two different contexts depending on the copy path: synchronously from the calling +// task for the CPU-memcpy and fb-direct (zero-copy) paths, but from an ISR (async_fbcpy_done_cb, +// the DMA2D completion interrupt) when use_dma2d is set. Giving a semaphore with the ISR-only API +// from task context is undefined behavior on FreeRTOS, so the context must be checked at runtime. +static bool on_color_trans_done(esp_lcd_panel_handle_t, esp_lcd_dpi_panel_event_data_t*, void* user_ctx) { + auto* internal = static_cast(user_ctx); + if (xPortInIsrContext()) { + BaseType_t high_task_woken = pdFALSE; + xSemaphoreGiveFromISR(internal->color_trans_done_semaphore, &high_task_woken); + return high_task_woken == pdTRUE; + } + xSemaphoreGive(internal->color_trans_done_semaphore); + return false; +} + static int pin_or_unused(const GpioPinSpec& pin) { return pin.gpio_controller == nullptr ? -1 : static_cast(pin.pin); } @@ -309,10 +338,27 @@ static error_t start(Device* device) { return ERROR_OUT_OF_MEMORY; } + internal->color_trans_done_semaphore = xSemaphoreCreateBinary(); + if (internal->color_trans_done_semaphore == nullptr) { + vSemaphoreDelete(internal->frame_complete_semaphore); + esp_lcd_panel_del(internal->panel_handle); + esp_lcd_panel_io_del(internal->io_handle); + esp_lcd_del_dsi_bus(internal->dsi_bus_handle); + esp_ldo_release_channel(internal->ldo_handle); + free(internal->parsed_init_cmds); + free(internal); + return ERROR_OUT_OF_MEMORY; + } + // The panel starts out idle (no draw_bitmap() call in flight), so the first draw_bitmap() + // must not block waiting for a completion event that will never come. + xSemaphoreGive(internal->color_trans_done_semaphore); + esp_lcd_dpi_panel_event_callbacks_t callbacks = {}; callbacks.on_refresh_done = on_refresh_done; + callbacks.on_color_trans_done = on_color_trans_done; if (esp_lcd_dpi_panel_register_event_callbacks(internal->panel_handle, &callbacks, internal) != ESP_OK) { LOG_E(TAG, "Failed to register panel event callbacks"); + vSemaphoreDelete(internal->color_trans_done_semaphore); vSemaphoreDelete(internal->frame_complete_semaphore); esp_lcd_panel_del(internal->panel_handle); esp_lcd_panel_io_del(internal->io_handle); @@ -363,6 +409,7 @@ static error_t stop(Device* device) { } vSemaphoreDelete(internal->frame_complete_semaphore); + vSemaphoreDelete(internal->color_trans_done_semaphore); free(internal->parsed_init_cmds); free(internal); device_set_driver_data(device, nullptr); @@ -402,15 +449,32 @@ static bool st7123_color_data_is_frame_buffer(const St7123Internal* internal, co static error_t st7123_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) { auto* internal = static_cast(device_get_driver_data(device)); + // Wait for the previous draw_bitmap()'s copy/DMA2D transfer to finish before issuing a new + // one - see on_color_trans_done's comment for why this is unconditional (unlike the + // tearing-avoidance wait below). The semaphore starts pre-given (see start()), so the first + // call doesn't block here. + xSemaphoreTake(internal->color_trans_done_semaphore, portMAX_DELAY); + bool wait_for_scanout = !GET_CONFIG(device)->allow_tearing && st7123_color_data_is_frame_buffer(internal, color_data); if (wait_for_scanout) { xSemaphoreTake(internal->frame_complete_semaphore, 0); // clear any already-pending signal } if (esp_lcd_panel_draw_bitmap(internal->panel_handle, x_start, y_start, x_end, y_end, color_data) != ESP_OK) { + xSemaphoreGive(internal->color_trans_done_semaphore); return ERROR_RESOURCE; } + // Also wait for *this* call's own copy to finish before returning: color_data may be a + // caller-owned buffer (e.g. LVGL's draw buffer in owned-buffer/PARTIAL mode), and the + // CPU-memcpy/DMA2D copy paths both read directly from it asynchronously - draw_bitmap() + // returning early would let the caller start overwriting color_data (DisplayApi's contract + // treats draw_bitmap as synchronous - see lvgl_display_flush_cb()'s lv_display_flush_ready() + // call right after) while that read is still in flight. on_color_trans_done() re-gives the + // semaphore here, restoring the pre-given/idle state for the next call. + xSemaphoreTake(internal->color_trans_done_semaphore, portMAX_DELAY); + xSemaphoreGive(internal->color_trans_done_semaphore); + if (wait_for_scanout) { xSemaphoreTake(internal->frame_complete_semaphore, portMAX_DELAY); } diff --git a/Modules/lvgl-module/CMakeLists.txt b/Modules/lvgl-module/CMakeLists.txt index e499a89e..cafcd03c 100644 --- a/Modules/lvgl-module/CMakeLists.txt +++ b/Modules/lvgl-module/CMakeLists.txt @@ -79,6 +79,10 @@ list(APPEND REQUIRES_LIST if (DEFINED ENV{ESP_IDF_VERSION}) list(APPEND REQUIRES_LIST esp_lvgl_port) + # required for lvgl_ppa + if (IDF_TARGET STREQUAL "esp32p4") + list(APPEND REQUIRES_LIST esp_driver_ppa) + endif () else () list(APPEND REQUIRES_LIST freertos_kernel) endif () diff --git a/Modules/lvgl-module/include/tactility/lvgl_ppa.h b/Modules/lvgl-module/include/tactility/lvgl_ppa.h new file mode 100644 index 00000000..a7f9dba1 --- /dev/null +++ b/Modules/lvgl-module/include/tactility/lvgl_ppa.h @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// True on targets with a PPA (Pixel Processing Accelerator) unit that lvgl_ppa can use to +// hardware-rotate rotated flush tiles instead of lv_draw_sw_rotate(). False (always) on the +// simulator and on ESP32 targets without a PPA (see SOC_PPA_SUPPORTED). +bool lvgl_ppa_is_supported(void); + +// True when color_format has a PPA color mode this module supports (RGB565/RGB888 - see +// lvgl_ppa_color_mode()). LV_COLOR_FORMAT_I1 and others fall back to lv_draw_sw_rotate(). +bool lvgl_ppa_supports_color_format(lv_color_format_t color_format); + +// Lazily creates (on first call) a PPA client and an output buffer at least out_buffer_size_bytes +// large, big enough for the largest tile this display will ever rotate. Returns NULL on failure - +// callers should fall back to lv_draw_sw_rotate() when that happens. Not thread-safe; callers must +// already hold the LVGL lock (true for all flush_cb callers). +void* lvgl_ppa_get_or_create(size_t out_buffer_size_bytes); + +void lvgl_ppa_delete(void* ppa_handle); + +// Rotates the tightly-packed w x h block at in_buff by rotation (LV_DISPLAY_ROTATION_90/180/270 - +// numerically identical to ppa_srm_rotation_angle_t's CCW convention, see the .c file) into the +// PPA's own output buffer and returns it. in_buff must have no row padding (PPA reads pic_w/pic_h +// in pixels, not a byte stride) - true for every lvgl_display_flush_cb() tile, which LVGL always +// renders compact. Returns NULL on failure - caller should fall back to lv_draw_sw_rotate(). +void* lvgl_ppa_rotate( + void* ppa_handle, + const uint8_t* in_buff, + int32_t w, + int32_t h, + lv_display_rotation_t rotation, + lv_color_format_t color_format, + bool swap_bytes +); + +#ifdef __cplusplus +} +#endif diff --git a/Modules/lvgl-module/source/lvgl_display.c b/Modules/lvgl-module/source/lvgl_display.c index a7c535ad..da76e111 100644 --- a/Modules/lvgl-module/source/lvgl_display.c +++ b/Modules/lvgl-module/source/lvgl_display.c @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include + #include #include #include @@ -37,6 +39,13 @@ struct LvglDisplayCtx { // display_mirror(); rotate_buf holds the rotated pixels and is sized like buf1. bool sw_rotate; void* rotate_buf; + // Lazily created on the first sw_rotate flush that needs it (see lvgl_display_rotate_tile()). + // Stays NULL - and every rotate falls back to rotate_buf/lv_draw_sw_rotate() - when the target + // has no PPA (lvgl_ppa_is_supported()), the color format has no PPA color mode + // (lvgl_ppa_supports_color_format()), or creating the PPA client/buffer failed once already. + void* ppa_handle; + bool ppa_unavailable; + bool ppa_eligible; // Size of buf1/buf2 (each) - used by lvgl_display_fb_base() to range-check which real buffer // (for the fb-direct case) a given color_map pointer falls into. size_t buf_size_bytes; @@ -166,7 +175,37 @@ static void* lvgl_display_fb_base(struct LvglDisplayCtx* ctx, const uint8_t* col return ctx->buf1; } - static void lvgl_display_flush_cb(lv_display_t* disp, const lv_area_t* area, uint8_t* color_map) { +// Tries to rotate the tightly-packed w x h block at in_buff via PPA, returning the PPA output +// buffer on success or NULL if this tile/format/target can't use it - in which case the caller +// must fall back to lv_draw_sw_rotate() into ctx->rotate_buf. Lazily creates the PPA client on the +// first eligible call, sized to ctx->buf_size_bytes (the largest tile or full-frame buffer this +// display will ever flush - see lvgl_display_add()); once creation fails once, ppa_unavailable +// latches so later tiles don't retry it. Never asks PPA to byte-swap: lvgl_display_flush_cb() +// applies ctx->byte_swap itself, once, at a single point regardless of which rotation path ran - +// simpler than tracking whether it was already done by PPA (PARTIAL mode) vs. already done +// per-tile before FULL mode's whole-frame rotate (see the two call sites). +static void* lvgl_display_try_ppa_rotate(struct LvglDisplayCtx* ctx, const uint8_t* in_buff, int32_t w, int32_t h, + lv_display_rotation_t rotation, lv_color_format_t color_format) { + if (!ctx->ppa_eligible || ctx->ppa_unavailable) { + return NULL; + } + // PPA reads pic_w/pic_h in pixels with no separate stride - only safe when the tile has no + // row padding beyond w * bytes-per-pixel (see lvgl_ppa.h). + uint32_t bpp = lv_color_format_get_size(color_format); + if (lv_draw_buf_width_to_stride(w, color_format) != (uint32_t)w * bpp) { + return NULL; + } + if (ctx->ppa_handle == NULL) { + ctx->ppa_handle = lvgl_ppa_get_or_create(ctx->buf_size_bytes); + if (ctx->ppa_handle == NULL) { + ctx->ppa_unavailable = true; + return NULL; + } + } + return lvgl_ppa_rotate(ctx->ppa_handle, in_buff, w, h, rotation, color_format, false); +} + +static void lvgl_display_flush_cb(lv_display_t* disp, const lv_area_t* area, uint8_t* color_map) { struct LvglDisplayCtx* ctx = (struct LvglDisplayCtx*)lv_display_get_driver_data(disp); bool is_i1 = lv_display_get_color_format(disp) == LV_COLOR_FORMAT_I1; @@ -190,14 +229,20 @@ static void* lvgl_display_fb_base(struct LvglDisplayCtx* ctx, const uint8_t* col lv_color_format_t color_format = lv_display_get_color_format(disp); int32_t w = x2 - x1 + 1; int32_t h = y2 - y1 + 1; - uint32_t w_stride = lv_draw_buf_width_to_stride(w, color_format); - uint32_t h_stride = lv_draw_buf_width_to_stride(h, color_format); - if (rotation == LV_DISPLAY_ROTATION_180) { - lv_draw_sw_rotate(color_map, ctx->rotate_buf, w, h, w_stride, w_stride, rotation, color_format); + + void* ppa_out = lvgl_display_try_ppa_rotate(ctx, color_map, w, h, rotation, color_format); + if (ppa_out != NULL) { + color_map = (uint8_t*)ppa_out; } else { - lv_draw_sw_rotate(color_map, ctx->rotate_buf, w, h, w_stride, h_stride, rotation, color_format); + uint32_t w_stride = lv_draw_buf_width_to_stride(w, color_format); + uint32_t h_stride = lv_draw_buf_width_to_stride(h, color_format); + if (rotation == LV_DISPLAY_ROTATION_180) { + lv_draw_sw_rotate(color_map, ctx->rotate_buf, w, h, w_stride, w_stride, rotation, color_format); + } else { + lv_draw_sw_rotate(color_map, ctx->rotate_buf, w, h, w_stride, h_stride, rotation, color_format); + } + color_map = (uint8_t*)ctx->rotate_buf; } - color_map = (uint8_t*)ctx->rotate_buf; lv_area_t rotated_area = { x1, y1, x2, y2 }; lv_display_rotate_area(disp, &rotated_area); x1 = rotated_area.x1; @@ -246,10 +291,16 @@ static void* lvgl_display_fb_base(struct LvglDisplayCtx* ctx, const uint8_t* col bool swapped_wh = rotation == LV_DISPLAY_ROTATION_90 || rotation == LV_DISPLAY_ROTATION_270; int32_t logical_w = swapped_wh ? (int32_t)vres : (int32_t)hres; int32_t logical_h = swapped_wh ? (int32_t)hres : (int32_t)vres; - uint32_t src_stride = lv_draw_buf_width_to_stride(logical_w, color_format); - uint32_t dest_stride = lv_draw_buf_width_to_stride(hres, color_format); - lv_draw_sw_rotate(fb_base, ctx->rotate_buf, logical_w, logical_h, src_stride, dest_stride, rotation, color_format); - fb_base = (uint8_t*)ctx->rotate_buf; + + void* ppa_out = lvgl_display_try_ppa_rotate(ctx, fb_base, logical_w, logical_h, rotation, color_format); + if (ppa_out != NULL) { + fb_base = (uint8_t*)ppa_out; + } else { + uint32_t src_stride = lv_draw_buf_width_to_stride(logical_w, color_format); + uint32_t dest_stride = lv_draw_buf_width_to_stride(hres, color_format); + lv_draw_sw_rotate(fb_base, ctx->rotate_buf, logical_w, logical_h, src_stride, dest_stride, rotation, color_format); + fb_base = (uint8_t*)ctx->rotate_buf; + } } display_draw_bitmap(ctx->device, 0, 0, hres, vres, fb_base); @@ -291,6 +342,13 @@ error_t lvgl_display_add(struct Device* device, const struct LvglDisplayConfig* ctx->device = device; ctx->byte_swap = config->swap_bytes; ctx->sw_rotate = config->sw_rotate; + // Only relevant when sw_rotate is set - lvgl_display_try_ppa_rotate() also checks + // ctx->ppa_eligible directly, so this is safe to compute unconditionally. + ctx->ppa_eligible = lvgl_ppa_is_supported() && lvgl_ppa_supports_color_format(lv_color_format); + if (config->sw_rotate && !ctx->ppa_eligible) { + LOG_I(TAG, "PPA not available for this display (supported=%d, color_format=%d) - using lv_draw_sw_rotate()", + (int)lvgl_ppa_is_supported(), (int)lv_color_format); + } ctx->has_swap_xy_cap = display_has_capability(device, DISPLAY_CAPABILITY_CAP_SWAP_XY); ctx->has_mirror_cap = display_has_capability(device, DISPLAY_CAPABILITY_CAP_MIRROR); ctx->has_set_gap_cap = display_has_capability(device, DISPLAY_CAPABILITY_CAP_SET_GAP); @@ -424,6 +482,9 @@ void lvgl_display_remove(lv_display_t* display) { if (ctx->rotate_buf != NULL) { lvgl_display_free_buffer(ctx->rotate_buf); } + if (ctx->ppa_handle != NULL) { + lvgl_ppa_delete(ctx->ppa_handle); + } free(ctx); } } diff --git a/Modules/lvgl-module/source/lvgl_ppa.c b/Modules/lvgl-module/source/lvgl_ppa.c new file mode 100644 index 00000000..f59ae56c --- /dev/null +++ b/Modules/lvgl-module/source/lvgl_ppa.c @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include + +#ifdef ESP_PLATFORM +#include +#endif + +#if defined(ESP_PLATFORM) && SOC_PPA_SUPPORTED + +#include +#include +#include + +#define TAG "lvgl_ppa" + +// PPA output buffer alignment requirement (address AND size - see lvgl_ppa_get_or_create()): +// cache line size is hardware-specific, but 64 bytes covers every current PPA-capable target's +// L1/L2 cache line size, so a fixed constant avoids needing per-SoC Kconfig lookups here. +#define LVGL_PPA_ALIGN 64 +#define LVGL_PPA_ALIGN_UP(n) (((n) + (LVGL_PPA_ALIGN - 1)) & ~(size_t)(LVGL_PPA_ALIGN - 1)) + +struct LvglPpa { + ppa_client_handle_t client; + uint8_t* out_buffer; + size_t out_buffer_size; + bool logged_first_rotate; +}; + +bool lvgl_ppa_is_supported(void) { + return true; +} + +bool lvgl_ppa_supports_color_format(lv_color_format_t color_format) { + return color_format == LV_COLOR_FORMAT_RGB565 || color_format == LV_COLOR_FORMAT_RGB888; +} + +static ppa_srm_color_mode_t lvgl_ppa_color_mode(lv_color_format_t color_format) { + return color_format == LV_COLOR_FORMAT_RGB888 ? PPA_SRM_COLOR_MODE_RGB888 : PPA_SRM_COLOR_MODE_RGB565; +} + +void* lvgl_ppa_get_or_create(size_t out_buffer_size_bytes) { + struct LvglPpa* ppa = (struct LvglPpa*)calloc(1, sizeof(struct LvglPpa)); + if (ppa == NULL) { + return NULL; + } + + // PPA requires the output buffer's size, not just its address, to be cache-line aligned (see + // ppa_out_pic_blk_config_t's buffer_size doc) - round up before allocating so both match. + size_t aligned_size = LVGL_PPA_ALIGN_UP(out_buffer_size_bytes); + ppa->out_buffer = (uint8_t*)heap_caps_aligned_alloc(LVGL_PPA_ALIGN, aligned_size, MALLOC_CAP_DMA | MALLOC_CAP_8BIT); + if (ppa->out_buffer == NULL) { + ppa->out_buffer = (uint8_t*)heap_caps_aligned_alloc(LVGL_PPA_ALIGN, aligned_size, MALLOC_CAP_DEFAULT); + } + if (ppa->out_buffer == NULL) { + LOG_E(TAG, "Failed to allocate PPA output buffer (%u bytes)", (unsigned)aligned_size); + free(ppa); + return NULL; + } + ppa->out_buffer_size = aligned_size; + + ppa_client_config_t client_config = { + .oper_type = PPA_OPERATION_SRM, + }; + if (ppa_register_client(&client_config, &ppa->client) != ESP_OK) { + LOG_E(TAG, "Failed to register PPA client"); + heap_caps_free(ppa->out_buffer); + free(ppa); + return NULL; + } + + LOG_I(TAG, "PPA client created (out buffer: %u bytes)", (unsigned)aligned_size); + return ppa; +} + +void lvgl_ppa_delete(void* ppa_handle) { + if (ppa_handle == NULL) { + return; + } + struct LvglPpa* ppa = (struct LvglPpa*)ppa_handle; + ppa_unregister_client(ppa->client); + heap_caps_free(ppa->out_buffer); + free(ppa); +} + +void* lvgl_ppa_rotate( + void* ppa_handle, + const uint8_t* in_buff, + int32_t w, + int32_t h, + lv_display_rotation_t rotation, + lv_color_format_t color_format, + bool swap_bytes +) { + struct LvglPpa* ppa = (struct LvglPpa*)ppa_handle; + ppa_srm_color_mode_t color_mode = lvgl_ppa_color_mode(color_format); + bool swapped_wh = rotation == LV_DISPLAY_ROTATION_90 || rotation == LV_DISPLAY_ROTATION_270; + int32_t out_w = swapped_wh ? h : w; + int32_t out_h = swapped_wh ? w : h; + + const ppa_srm_oper_config_t oper_config = { + .in = { + .buffer = in_buff, + .pic_w = (uint32_t)w, + .pic_h = (uint32_t)h, + .block_w = (uint32_t)w, + .block_h = (uint32_t)h, + .block_offset_x = 0, + .block_offset_y = 0, + .srm_cm = color_mode, + }, + .out = { + .buffer = ppa->out_buffer, + .buffer_size = ppa->out_buffer_size, + .pic_w = (uint32_t)out_w, + .pic_h = (uint32_t)out_h, + .block_offset_x = 0, + .block_offset_y = 0, + .srm_cm = color_mode, + }, + // LVGL rotation is CCW, same convention as ppa_srm_rotation_angle_t - see lvgl_ppa.h. + .rotation_angle = (ppa_srm_rotation_angle_t)rotation, + .scale_x = 1.0f, + .scale_y = 1.0f, + .byte_swap = swap_bytes, + .mode = PPA_TRANS_MODE_BLOCKING, + }; + + if (ppa_do_scale_rotate_mirror(ppa->client, &oper_config) != ESP_OK) { + LOG_E(TAG, "PPA rotate failed (%" PRId32 "x%" PRId32 " -> %" PRId32 "x%" PRId32 ")", w, h, out_w, out_h); + return NULL; + } + + if (!ppa->logged_first_rotate) { + LOG_I(TAG, "PPA rotate in use (%" PRId32 "x%" PRId32 " -> %" PRId32 "x%" PRId32 ", angle=%d)", w, h, out_w, out_h, (int)rotation); + ppa->logged_first_rotate = true; + } + + return ppa->out_buffer; +} + +#else // !(ESP_PLATFORM && SOC_PPA_SUPPORTED) + +bool lvgl_ppa_is_supported(void) { + return false; +} + +bool lvgl_ppa_supports_color_format(lv_color_format_t color_format) { + (void)color_format; + return false; +} + +void* lvgl_ppa_get_or_create(size_t out_buffer_size_bytes) { + (void)out_buffer_size_bytes; + return NULL; +} + +void lvgl_ppa_delete(void* ppa_handle) { + (void)ppa_handle; +} + +void* lvgl_ppa_rotate( + void* ppa_handle, + const uint8_t* in_buff, + int32_t w, + int32_t h, + lv_display_rotation_t rotation, + lv_color_format_t color_format, + bool swap_bytes +) { + (void)ppa_handle; + (void)in_buff; + (void)w; + (void)h; + (void)rotation; + (void)color_format; + (void)swap_bytes; + return NULL; +} + +#endif