From 28e6d42f05eee057ae08ba56d95098240b8967bf Mon Sep 17 00:00:00 2001 From: Timo Niemann Date: Wed, 1 Jul 2026 17:13:11 +0200 Subject: [PATCH] opus generated a prefetch of the next image --- main/lcd/ui_image_browser.c | 196 +++++++++++++++++++++++++++++++++++- 1 file changed, 193 insertions(+), 3 deletions(-) diff --git a/main/lcd/ui_image_browser.c b/main/lcd/ui_image_browser.c index c3126e5..69a470e 100644 --- a/main/lcd/ui_image_browser.c +++ b/main/lcd/ui_image_browser.c @@ -7,7 +7,9 @@ #include #include "lvgl.h" +#include "draw/lv_draw_buf_private.h" #include "esp_log.h" +#include "esp_heap_caps.h" #include "lcd/ui_image_browser.h" #include "lcd/ui_internal.h" @@ -21,6 +23,18 @@ #define UI_IMAGE_PROBE_HEADER_LENGTH 16 #define UI_SLIDESHOW_PERIOD_MS 3000 +/* Preloading: images are pre-rendered into full RGB565 buffers in PSRAM so a + * view switch only swaps a ready-made bitmap (no decode at switch time). */ +#define UI_PREFETCH_SLOT_COUNT 2 +#define UI_PREFETCH_MAX_PIXELS (480 * 800) +#define UI_PREFETCH_BUFFER_SIZE (UI_PREFETCH_MAX_PIXELS * 2) /* RGB565 = 2 bytes/px */ + +typedef struct { + uint8_t *buffer; /* raw PSRAM pixel buffer (UI_PREFETCH_BUFFER_SIZE) */ + lv_draw_buf_t draw_buf; /* describes the rendered image for the widget src */ + int32_t image_index; /* image currently rendered here, -1 if none */ +} ui_prefetch_slot_t; + static lv_obj_t *image_view = NULL; static lv_obj_t *image_widget = NULL; static lv_obj_t *image_caption = NULL; @@ -31,13 +45,20 @@ static size_t image_browser_current_index = 0; static const char **image_browser_names = NULL; static const char *TAG = "ui"; -static void ui_image_browser_load_current(void); +static ui_prefetch_slot_t prefetch_slots[UI_PREFETCH_SLOT_COUNT]; +static lv_obj_t *prefetch_canvas = NULL; +static int32_t visible_slot = -1; +static bool prefetch_enabled = false; + +static void ui_image_browser_display_current(void); +static void ui_image_browser_load_via_path(void); +static bool ui_prefetch_render(size_t image_index, ui_prefetch_slot_t *slot); static void ui_image_browser_show_index(size_t image_index){ if(image_widget == NULL || image_browser_image_count == 0) return; image_browser_current_index = image_index % image_browser_image_count; - ui_image_browser_load_current(); + ui_image_browser_display_current(); } static void ui_image_slideshow_timer_cb(lv_timer_t *timer){ @@ -212,7 +233,127 @@ static void ui_collect_image_names(size_t image_count){ } } -static void ui_image_browser_load_current(void){ +/* Redirect the image decoder's working buffers (e.g. a full ARGB8888 PNG frame, + * ~1.5 MB) to PSRAM. Without this they come from LVGL's fixed 64 KB internal + * pool and large images fail to decode / crash. Only malloc/free are swapped; + * alignment and stride keep the default behaviour. Applied once, globally. */ +static void *ui_psram_draw_buf_malloc(size_t size, lv_color_format_t color_format){ + LV_UNUSED(color_format); + return heap_caps_malloc(size + LV_DRAW_BUF_ALIGN - 1, MALLOC_CAP_SPIRAM); +} + +static void ui_psram_draw_buf_free(void *buf){ + heap_caps_free(buf); +} + +static void ui_image_decoder_use_psram(void){ + static bool configured = false; + if(configured) return; + + lv_draw_buf_handlers_t *image_handlers = lv_draw_buf_get_image_handlers(); + if(image_handlers == NULL) return; + + image_handlers->buf_malloc_cb = ui_psram_draw_buf_malloc; + image_handlers->buf_free_cb = ui_psram_draw_buf_free; + configured = true; +} + +/* Fully decode/render an image into a slot's PSRAM RGB565 buffer once, so it can + * later be shown as a raw bitmap (a direct blit, no decoding at switch time). */ +static bool ui_prefetch_render(size_t image_index, ui_prefetch_slot_t *slot){ + if(prefetch_canvas == NULL || slot->buffer == NULL || image_browser_names == NULL) return false; + + const char *filename = image_browser_names[image_index]; + + char image_source[UI_IMAGE_PATH_MAX_LENGTH]; + snprintf(image_source, sizeof(image_source), "%s%s", UI_IMAGE_PATH_PREFIX, filename); + ui_lowercase_path_extension(image_source); + + lv_image_header_t header; + if(lv_image_decoder_get_info(image_source, &header) != LV_RESULT_OK) return false; + + const int32_t width = (int32_t)header.w; + const int32_t height = (int32_t)header.h; + if(width <= 0 || height <= 0) return false; + if((size_t)width * (size_t)height > UI_PREFETCH_MAX_PIXELS) return false; + + /* Render the image at native size into the slot buffer (black background so + * transparent PNG pixels match the app background). */ + lv_canvas_set_buffer(prefetch_canvas, slot->buffer, width, height, LV_COLOR_FORMAT_RGB565); + lv_canvas_fill_bg(prefetch_canvas, lv_color_black(), LV_OPA_COVER); + + lv_layer_t layer; + lv_canvas_init_layer(prefetch_canvas, &layer); + + lv_draw_image_dsc_t draw_dsc; + lv_draw_image_dsc_init(&draw_dsc); + draw_dsc.src = image_source; + + lv_area_t coords = { 0, 0, width - 1, height - 1 }; + lv_draw_image(&layer, &draw_dsc, &coords); + lv_canvas_finish_layer(prefetch_canvas, &layer); + + lv_draw_buf_init(&slot->draw_buf, width, height, LV_COLOR_FORMAT_RGB565, + lv_draw_buf_width_to_stride(width, LV_COLOR_FORMAT_RGB565), + slot->buffer, UI_PREFETCH_BUFFER_SIZE); + slot->image_index = (int32_t)image_index; + return true; +} + +/* Pre-render the image following current_index into the non-visible slot. */ +static void ui_prefetch_next(size_t current_index, int32_t current_slot){ + if(!prefetch_enabled || image_browser_image_count <= 1) return; + + const size_t next_index = (current_index + 1) % image_browser_image_count; + const int32_t other = (current_slot == 0) ? 1 : 0; + if(prefetch_slots[other].image_index == (int32_t)next_index) return; + + ui_prefetch_render(next_index, &prefetch_slots[other]); +} + +/* Try to show the current image from a pre-rendered PSRAM buffer. */ +static bool ui_image_browser_display_prefetched(void){ + if(!prefetch_enabled) return false; + + const size_t index = image_browser_current_index; + + int32_t slot = -1; + for(int32_t i = 0; i < UI_PREFETCH_SLOT_COUNT; i++){ + if(prefetch_slots[i].image_index == (int32_t)index){ slot = i; break; } + } + + if(slot < 0){ + /* Not prefetched yet: render on demand into the non-visible slot. */ + slot = (visible_slot == 0) ? 1 : 0; + if(!ui_prefetch_render(index, &prefetch_slots[slot])) return false; + } + + lv_image_set_src(image_widget, &prefetch_slots[slot].draw_buf); + visible_slot = slot; + + const char *filename = image_browser_names[index]; + if(image_caption != NULL){ + lv_label_set_text_fmt(image_caption, "%u/%u %s", + (unsigned int)(index + 1), + (unsigned int)image_browser_image_count, filename); + } + + lv_obj_remove_flag(image_widget, LV_OBJ_FLAG_HIDDEN); + if(image_error_label != NULL) lv_obj_add_flag(image_error_label, LV_OBJ_FLAG_HIDDEN); + + ui_log_appendf("Shown %u/%u: %s (%dx%d)", + (unsigned int)(index + 1), + (unsigned int)image_browser_image_count, filename, + (int)prefetch_slots[slot].draw_buf.header.w, + (int)prefetch_slots[slot].draw_buf.header.h); + + /* Warm up the next image so the following switch is instant too. */ + ui_prefetch_next(index, slot); + return true; +} + +/* Fallback: decode straight from the file (also drives the error diagnostics). */ +static void ui_image_browser_load_via_path(void){ if(image_widget == NULL || image_browser_names == NULL || image_browser_image_count == 0) return; const char *filename = image_browser_names[image_browser_current_index]; @@ -251,6 +392,16 @@ static void ui_image_browser_load_current(void){ } } +static void ui_image_browser_display_current(void){ + if(image_widget == NULL || image_browser_names == NULL || image_browser_image_count == 0) return; + + if(ui_image_browser_display_prefetched()) return; + + /* Prefetch unavailable or render failed: fall back to on-demand decoding. */ + visible_slot = -1; + ui_image_browser_load_via_path(); +} + void ui_image_browser_build_view(lv_obj_t *screen){ const size_t image_count = ui_count_sd_images(); @@ -315,6 +466,30 @@ void ui_image_browser_build_view(lv_obj_t *screen){ lv_obj_align(image_error_label, LV_ALIGN_CENTER, 0, -18); lv_obj_add_flag(image_error_label, LV_OBJ_FLAG_HIDDEN); + /* Preloading: route decoder buffers to PSRAM and allocate two RGB565 slots + * plus a hidden canvas used to pre-render images off-screen. */ + ui_image_decoder_use_psram(); + prefetch_enabled = false; + visible_slot = -1; + for(int32_t slot_index = 0; slot_index < UI_PREFETCH_SLOT_COUNT; slot_index++){ + prefetch_slots[slot_index].image_index = -1; + prefetch_slots[slot_index].buffer = heap_caps_malloc(UI_PREFETCH_BUFFER_SIZE, MALLOC_CAP_SPIRAM); + } + if(prefetch_slots[0].buffer != NULL && prefetch_slots[1].buffer != NULL){ + prefetch_canvas = lv_canvas_create(image_view); + lv_obj_add_flag(prefetch_canvas, LV_OBJ_FLAG_HIDDEN); + lv_obj_remove_flag(prefetch_canvas, LV_OBJ_FLAG_CLICKABLE); + prefetch_enabled = true; + } + else{ + for(int32_t slot_index = 0; slot_index < UI_PREFETCH_SLOT_COUNT; slot_index++){ + heap_caps_free(prefetch_slots[slot_index].buffer); + prefetch_slots[slot_index].buffer = NULL; + } + ESP_LOGW(TAG, "Image preloading disabled: PSRAM buffers unavailable"); + ui_log_append("Preloading disabled (no PSRAM)"); + } + ui_image_browser_show_index(0); ui_start_image_slideshow(); } @@ -325,8 +500,23 @@ void ui_image_browser_reset(void){ image_slideshow_timer = NULL; } + /* Drop the widget's reference to a prefetch buffer before freeing it. */ + if(image_widget != NULL) lv_image_set_src(image_widget, NULL); + free((void *)image_browser_names); image_browser_names = NULL; + + for(int32_t slot_index = 0; slot_index < UI_PREFETCH_SLOT_COUNT; slot_index++){ + if(prefetch_slots[slot_index].buffer != NULL){ + heap_caps_free(prefetch_slots[slot_index].buffer); + prefetch_slots[slot_index].buffer = NULL; + } + prefetch_slots[slot_index].image_index = -1; + } + prefetch_canvas = NULL; /* deleted together with the screen contents */ + prefetch_enabled = false; + visible_slot = -1; + image_view = NULL; image_widget = NULL; image_caption = NULL;