Render matrix rain as pooled internal-RAM sprites
The rain was 40 live text labels; moving a label re-rasterizes its katakana glyphs every frame, reading the font from PSRAM and blending against the framebuffer. That per-frame load starved the DSI's framebuffer read and flashed the panel blue while a voice reply downloaded (confirmed: with the rain removed the flash vanished). Pre-render a small pool of streak sprites once into internal RAM and blit them; movement is now a cheap opaque copy that never touches the PSRAM bus. Also drain the rain during audio (listening/speaking) rather than freezing it. This clears the last of the load flicker while keeping the dense look. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKPbR6DY2JygHbyLjxm7Uu
This commit is contained in:
+8
-3
@@ -10,6 +10,11 @@ until the first tagged release.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Reduced the blue-screen flicker during voice activity — lowered the DSI pixel
|
||||
clock so the display's continuous PSRAM reads aren't starved by concurrent
|
||||
Wi-Fi and audio DMA on the same bus.
|
||||
- Fixed the blue-screen flicker during voice activity — the matrix rain now
|
||||
renders as cheap pre-rendered sprites, and the DSI clocks/FIFO are tuned, so
|
||||
the display's memory reads are no longer starved by Wi-Fi and audio DMA.
|
||||
|
||||
### Changed
|
||||
|
||||
- Matrix rain is drawn from a reused pool of pre-rendered streak sprites instead
|
||||
of live text labels, and eases off while the device is listening or speaking.
|
||||
|
||||
+99
-36
@@ -8,6 +8,7 @@
|
||||
#include <time.h>
|
||||
|
||||
#include "esp_random.h"
|
||||
#include "esp_heap_caps.h"
|
||||
#include "bsp/esp-bsp.h"
|
||||
#include "lvgl.h"
|
||||
|
||||
@@ -20,6 +21,18 @@ LV_FONT_DECLARE(font_rage_64);
|
||||
#define SCREEN 800
|
||||
#define RAIN_MAX 40
|
||||
#define RAIN_CELL 20
|
||||
|
||||
/* Rain is drawn as a small pool of pre-rendered "streak" sprites in INTERNAL RAM,
|
||||
* reused across the on-screen streams. Moving a stream is then a cheap opaque blit
|
||||
* (write-only, no per-frame glyph rasterization and no read-back-to-blend), and the
|
||||
* sprite pixels are read from internal RAM instead of PSRAM — so the rain stops
|
||||
* competing with the DSI's framebuffer read on the PSRAM bus (the "thinking"
|
||||
* underrun). One sprite is regenerated at a time to keep visual variance. */
|
||||
#define CELL_H 26 /* px per glyph row */
|
||||
#define SPRITE_GLYPHS 12 /* streak length in a sprite */
|
||||
#define SPRITE_W 24 /* sprite width (glyph ~22px) */
|
||||
#define SPRITE_H (SPRITE_GLYPHS * CELL_H)
|
||||
#define SPRITE_POOL 5 /* distinct pre-rendered streaks */
|
||||
#define RAIN_COLS (SCREEN / RAIN_CELL)
|
||||
#define TICK_MS 33
|
||||
|
||||
@@ -74,12 +87,16 @@ static const char *GLYPHS[] = {
|
||||
#define NGLYPHS (sizeof(GLYPHS) / sizeof(GLYPHS[0]))
|
||||
|
||||
typedef struct {
|
||||
lv_obj_t *tail;
|
||||
lv_obj_t *head;
|
||||
int x, y, len, speed;
|
||||
lv_obj_t *img; /* lv_image showing one of the pool sprites */
|
||||
int sprite; /* which pool sprite (index into sprite_dsc) */
|
||||
int x, y, speed;
|
||||
bool alive;
|
||||
} stream_t;
|
||||
|
||||
/* Pre-rendered streak sprite pool (RGB565, opaque, black background baked in). */
|
||||
static lv_color16_t *sprite_buf[SPRITE_POOL];
|
||||
static lv_image_dsc_t sprite_dsc[SPRITE_POOL];
|
||||
|
||||
static struct {
|
||||
face_state_t state;
|
||||
lv_obj_t *rain_layer;
|
||||
@@ -109,44 +126,68 @@ static struct {
|
||||
static uint32_t now_ms(void) { return lv_tick_get(); }
|
||||
static const char *rand_glyph(void) { return GLYPHS[esp_random() % NGLYPHS]; }
|
||||
|
||||
static void stream_fill(stream_t *s)
|
||||
/* Render a fresh random streak into pool sprite k: a katakana column, dim green
|
||||
* fading up from a bright leading glyph, on an opaque black background. Done once
|
||||
* per sprite (not per frame) via an off-screen canvas. */
|
||||
static void sprite_render(int k)
|
||||
{
|
||||
char buf[16 * 4 + 16];
|
||||
int pos = 0;
|
||||
for (int i = 0; i < s->len; i++) {
|
||||
pos += snprintf(buf + pos, sizeof(buf) - pos, "%s%s", i ? "\n" : "", rand_glyph());
|
||||
if (sprite_buf[k] == NULL) {
|
||||
return;
|
||||
}
|
||||
lv_label_set_text(s->tail, buf);
|
||||
lv_label_set_text(s->head, rand_glyph());
|
||||
lv_obj_t *cv = lv_canvas_create(F.rain_layer);
|
||||
lv_canvas_set_buffer(cv, sprite_buf[k], SPRITE_W, SPRITE_H, LV_COLOR_FORMAT_RGB565);
|
||||
lv_canvas_fill_bg(cv, lv_color_black(), LV_OPA_COVER);
|
||||
lv_layer_t layer;
|
||||
lv_canvas_init_layer(cv, &layer);
|
||||
for (int i = 0; i < SPRITE_GLYPHS; i++) {
|
||||
int from_head = SPRITE_GLYPHS - 1 - i; /* 0 = bright leading glyph */
|
||||
lv_draw_label_dsc_t dsc;
|
||||
lv_draw_label_dsc_init(&dsc);
|
||||
dsc.font = &font_rain_22;
|
||||
dsc.align = LV_TEXT_ALIGN_CENTER;
|
||||
dsc.text = rand_glyph();
|
||||
if (from_head == 0) {
|
||||
dsc.color = lv_color_hex(0xC3FFD7); /* bright head */
|
||||
} else {
|
||||
int g = 0xA0 - from_head * 9; /* fade green up the tail */
|
||||
if (g < 0x28) g = 0x28;
|
||||
dsc.color = lv_color_make(0x00, g, 0x40);
|
||||
}
|
||||
lv_area_t a = { 0, i * CELL_H, SPRITE_W - 1, i * CELL_H + CELL_H - 1 };
|
||||
lv_draw_label(&layer, &dsc, &a);
|
||||
}
|
||||
lv_canvas_finish_layer(cv, &layer);
|
||||
lv_obj_delete(cv); /* keep sprite_buf[k]; drop the canvas object */
|
||||
|
||||
sprite_dsc[k].header.magic = LV_IMAGE_HEADER_MAGIC;
|
||||
sprite_dsc[k].header.cf = LV_COLOR_FORMAT_RGB565;
|
||||
sprite_dsc[k].header.w = SPRITE_W;
|
||||
sprite_dsc[k].header.h = SPRITE_H;
|
||||
sprite_dsc[k].header.stride = SPRITE_W * 2;
|
||||
sprite_dsc[k].data = (const uint8_t *)sprite_buf[k];
|
||||
sprite_dsc[k].data_size = SPRITE_W * SPRITE_H * 2;
|
||||
}
|
||||
|
||||
static void stream_spawn(stream_t *s, int speed)
|
||||
{
|
||||
s->x = (esp_random() % RAIN_COLS) * RAIN_CELL;
|
||||
s->len = 6 + esp_random() % 9;
|
||||
s->speed = speed + esp_random() % 4;
|
||||
s->y = -(s->len + 1) * 26;
|
||||
if (s->tail == NULL) {
|
||||
s->tail = lv_label_create(F.rain_layer);
|
||||
lv_obj_set_style_text_font(s->tail, &font_rain_22, 0);
|
||||
lv_obj_set_style_text_color(s->tail, lv_color_hex(0x00A050), 0);
|
||||
lv_obj_set_style_text_opa(s->tail, 170, 0);
|
||||
s->head = lv_label_create(F.rain_layer);
|
||||
lv_obj_set_style_text_font(s->head, &font_rain_22, 0);
|
||||
lv_obj_set_style_text_color(s->head, lv_color_hex(0xC3FFD7), 0);
|
||||
s->y = -SPRITE_H;
|
||||
s->sprite = esp_random() % SPRITE_POOL;
|
||||
if (s->img == NULL) {
|
||||
s->img = lv_image_create(F.rain_layer);
|
||||
}
|
||||
lv_obj_clear_flag(s->tail, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_clear_flag(s->head, LV_OBJ_FLAG_HIDDEN);
|
||||
stream_fill(s);
|
||||
lv_image_set_src(s->img, &sprite_dsc[s->sprite]);
|
||||
lv_obj_set_pos(s->img, s->x, s->y);
|
||||
lv_obj_clear_flag(s->img, LV_OBJ_FLAG_HIDDEN);
|
||||
s->alive = true;
|
||||
}
|
||||
|
||||
static void stream_hide(stream_t *s)
|
||||
{
|
||||
s->alive = false;
|
||||
if (s->tail != NULL) {
|
||||
lv_obj_add_flag(s->tail, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_add_flag(s->head, LV_OBJ_FLAG_HIDDEN);
|
||||
if (s->img != NULL) {
|
||||
lv_obj_add_flag(s->img, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,8 +204,11 @@ static void rain_tick(void)
|
||||
return;
|
||||
}
|
||||
/* Rain is the reachability signal: it only falls when the gateway is live.
|
||||
* A disconnect drains it gracefully (streams finish falling, none respawn). */
|
||||
int target = (F.power == 2 || !gw_connected()) ? 0 : d->rain_streams;
|
||||
* A disconnect drains it gracefully (streams finish falling, none respawn).
|
||||
* Also drain it while audio is active (listening or the butler speaking):
|
||||
* the streams fall away rather than freezing (less jarring). */
|
||||
int target = (F.power == 2 || !gw_connected()
|
||||
|| audio_is_playing() || audio_capture_active()) ? 0 : d->rain_streams;
|
||||
int alive = 0;
|
||||
for (int i = 0; i < RAIN_MAX; i++) {
|
||||
stream_t *s = &F.streams[i];
|
||||
@@ -172,13 +216,9 @@ static void rain_tick(void)
|
||||
continue;
|
||||
}
|
||||
alive++;
|
||||
s->y += s->speed * 2;
|
||||
lv_obj_set_pos(s->tail, s->x, s->y);
|
||||
lv_obj_set_pos(s->head, s->x, s->y + s->len * 26);
|
||||
if (F.mutate == 0 && (esp_random() % 3) == 0) {
|
||||
lv_label_set_text(s->head, rand_glyph());
|
||||
}
|
||||
if (s->y > SCREEN + 26) {
|
||||
s->y += s->speed * 2; /* cheap blit of a pre-rendered sprite */
|
||||
lv_obj_set_pos(s->img, s->x, s->y);
|
||||
if (s->y > SCREEN) {
|
||||
if (alive > target) {
|
||||
stream_hide(s);
|
||||
alive--;
|
||||
@@ -193,7 +233,13 @@ static void rain_tick(void)
|
||||
alive++;
|
||||
}
|
||||
}
|
||||
F.mutate = (F.mutate + 1) % 4;
|
||||
/* Variance: regenerate one pool sprite periodically so the reused streaks vary
|
||||
* over time instead of looking tiled. */
|
||||
static uint16_t regen;
|
||||
if (++regen >= 90) {
|
||||
regen = 0;
|
||||
sprite_render(esp_random() % SPRITE_POOL);
|
||||
}
|
||||
}
|
||||
|
||||
static void apply_power(int level)
|
||||
@@ -368,6 +414,23 @@ void face_init(void)
|
||||
F.rain_layer = lv_obj_create(scr);
|
||||
lv_obj_remove_style_all(F.rain_layer);
|
||||
lv_obj_set_size(F.rain_layer, SCREEN, SCREEN);
|
||||
lv_obj_set_style_bg_opa(F.rain_layer, LV_OPA_TRANSP, 0);
|
||||
lv_obj_clear_flag(F.rain_layer, LV_OBJ_FLAG_SCROLLABLE);
|
||||
|
||||
/* Allocate the pre-rendered streak sprites in INTERNAL RAM (so their per-frame
|
||||
* reads stay off the PSRAM bus the DSI is reading the framebuffer from) and
|
||||
* render the initial pool. ~15 KB each. */
|
||||
for (int k = 0; k < SPRITE_POOL; k++) {
|
||||
sprite_buf[k] = heap_caps_malloc(SPRITE_W * SPRITE_H * sizeof(lv_color16_t),
|
||||
MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
|
||||
if (sprite_buf[k] == NULL) {
|
||||
/* internal RAM famine: fall back to PSRAM (still cheaper than per-frame
|
||||
* rasterization, just without the bus-relief benefit) */
|
||||
sprite_buf[k] = heap_caps_malloc(SPRITE_W * SPRITE_H * sizeof(lv_color16_t),
|
||||
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
}
|
||||
sprite_render(k);
|
||||
}
|
||||
|
||||
/* radial-ish shade so rain doesn't run through the face */
|
||||
F.shade = lv_obj_create(scr);
|
||||
|
||||
Reference in New Issue
Block a user