opus generated a prefetch of the next image

This commit is contained in:
Timo Niemann
2026-07-01 17:13:11 +02:00
parent a53303e862
commit 28e6d42f05
+193 -3
View File
@@ -7,7 +7,9 @@
#include <string.h>
#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;