perf(box): cache Lire icons in PSRAM

The reading game referenced icons by SD file path, so LVGL re-read and decoded
each .bin from the slow microSD on every draw band (16-line buffer) and every
frame, with no image cache — hence the laggy display. Read each .bin once into
PSRAM and hand LVGL an in-RAM image descriptor: no per-band SD reads, and
repeated words are instant.
This commit is contained in:
Clément Saillant
2026-06-15 10:21:51 +02:00
parent 00596f39cd
commit a399a0f2f7
+59 -8
View File
@@ -139,17 +139,68 @@ static void play_word(int idx)
lisael_aac_play_file(p);
}
// A word's picture from the SD (or a coloured fallback with its first letter).
// --- PSRAM icon cache --------------------------------------------------------
// Referencing icons by SD file path ("S:/…") made LVGL re-read+decode each .bin
// from the slow microSD on every draw band (the draw buffer is only 16 lines)
// AND every frame, with no image cache (LV_CACHE_DEF_SIZE=0) — hence the laggy
// display. Instead we read each .bin ONCE into PSRAM and hand LVGL an in-RAM
// image descriptor: no per-band SD reads ever, and repeated words are instant.
typedef struct {
char id[28];
void *buf; // the whole .bin in PSRAM (NULL = known-missing)
lv_image_dsc_t dsc;
} lire_icon_t;
static lire_icon_t *s_icons; // PSRAM, one entry per distinct id seen
static int s_icons_n;
static const lv_image_dsc_t *icon_dsc(const char *id)
{
for (int i = 0; i < s_icons_n; i++) {
if (!strcmp(s_icons[i].id, id)) {
return s_icons[i].buf ? &s_icons[i].dsc : NULL;
}
}
if (!s_icons) {
s_icons = heap_caps_calloc(LIRE_MAX_WORDS, sizeof(lire_icon_t), MALLOC_CAP_SPIRAM);
if (!s_icons) return NULL;
}
if (s_icons_n >= LIRE_MAX_WORDS) return NULL;
lire_icon_t *c = &s_icons[s_icons_n++]; // also negatively caches misses
snprintf(c->id, sizeof(c->id), "%s", id);
c->buf = NULL;
char p[80];
snprintf(p, sizeof(p), "/sdcard/lire/img/%s.bin", id);
FILE *f = fopen(p, "rb");
if (!f) return NULL;
fseek(f, 0, SEEK_END);
long sz = ftell(f);
fseek(f, 0, SEEK_SET);
if (sz <= (long)sizeof(lv_image_header_t) || sz > 256 * 1024) { fclose(f); return NULL; }
uint8_t *buf = heap_caps_malloc((size_t)sz, MALLOC_CAP_SPIRAM);
if (!buf) { fclose(f); return NULL; }
size_t rd = fread(buf, 1, (size_t)sz, f);
fclose(f);
if (rd != (size_t)sz) { heap_caps_free(buf); return NULL; }
// The LVGL .bin layout is lv_image_header_t followed by pixel data, so the
// PSRAM copy doubles as a variable-image descriptor (same bytes LVGL would
// read from the file — which already renders correctly here).
lv_memcpy(&c->dsc.header, buf, sizeof(lv_image_header_t));
c->dsc.data = buf + sizeof(lv_image_header_t);
c->dsc.data_size = (uint32_t)(sz - (long)sizeof(lv_image_header_t));
c->buf = buf;
return &c->dsc;
}
// A word's picture (from the PSRAM cache) or a coloured fallback with its initial.
static lv_obj_t *make_word_icon(lv_obj_t *parent, int idx, int px)
{
char real[80];
snprintf(real, sizeof(real), "/sdcard/lire/img/%s.bin", s_words[idx].id);
struct stat st;
if (stat(real, &st) == 0 && st.st_size > 0) {
char src[88];
snprintf(src, sizeof(src), "S:/sdcard/lire/img/%s.bin", s_words[idx].id);
const lv_image_dsc_t *d = icon_dsc(s_words[idx].id);
if (d) {
lv_obj_t *img = lv_image_create(parent);
lv_image_set_src(img, src);
lv_image_set_src(img, d);
lv_obj_set_size(img, px, px);
lv_image_set_inner_align(img, LV_IMAGE_ALIGN_CONTAIN);
return img;