perf(l2): screen sent by DMA while the next part is drawn

The panel takes each 24-line band by DMA while LVGL draws the next one
into the other buffer (two 15 KB buffers in internal RAM; LovyanGFX's
4092-byte DMA descriptors leave PSRAM unaligned for the S3's DMA, which
showed as shifted and smeared bands). LVGL draws in the panel's byte
order, and so do map tiles and the frozen screen under a popup, so
nothing is swapped on the way out. Full frames ~10-25% faster (page1
19.9 -> 14.7 ms, Home 21.4 -> 18.2 ms). Without the internal RAM the
screen is sent as before. The arduino2 env keeps the old way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-10-01 20:56:13 +02:00
co-authored by Claude Opus 5.5
parent ca5e66a4a1
commit 0a07dc3deb
5 changed files with 123 additions and 15 deletions
+83 -12
View File
@@ -43,7 +43,12 @@ static void shotCopy(const lv_area_t* a, const uint8_t* px) {
const int32_t W = lv_display_get_horizontal_resolution(NULL);
const int32_t w = a->x2 - a->x1 + 1;
const uint16_t* src = (const uint16_t*)px;
for (int32_t y = a->y1; y <= a->y2; y++, src += w) memcpy(s_shot + y * W + a->x1, src, w * 2);
for (int32_t y = a->y1; y <= a->y2; y++, src += w) {
memcpy(s_shot + y * W + a->x1, src, w * 2);
#ifdef UI_FLUSH_DMA // drawn big-endian for the panel
lv_draw_sw_rgb565_swap(s_shot + y * W + a->x1, w);
#endif
}
}
// Saved WiFi networks (Settings > WiFi), most recently saved first. Kept in RAM
@@ -99,7 +104,16 @@ static const int WIFI_SCAN_MAX = 20;
static lgfx::LGFX_Device* s_gfx = nullptr;
static bool s_swallow = false; // ignore the touch that woke the display until it lifts
static uint32_t s_flush_us = 0; // UI_PERF_TEST: time spent in flushCb
static uint32_t s_flush_us = 0; // UI_PERF_TEST: time spent in flushCb (and waiting for it)
// -D UI_FLUSH_DMA: the panel fed by DMA in the background from buffers in
// internal RAM, everything drawn in its byte order.
#ifdef UI_FLUSH_DMA
#define FLUSH_DMA 1
#else
#define FLUSH_DMA 0
#endif
static bool s_dma = false; // the buffers are DMA-able: sent in the background
static bool s_flushing = false; // a DMA transfer under way
// The CPU clock down to 80 MHz (LoRa, BLE and WiFi all run at that) with the
// screen off (powerSave()), and with it on while nobody touches it and nothing
@@ -121,16 +135,44 @@ static void flushCb(lv_display_t* disp, const lv_area_t* area, uint8_t* px_map)
shotCopy(area, px_map);
int w = area->x2 - area->x1 + 1;
int h = area->y2 - area->y1 + 1;
#if FLUSH_DMA
if (s_dma) {
// Sent by DMA while LVGL draws the next area into the other buffer (drawn
// in the panel's byte order, RGB565_SWAPPED). flushWait() (below) ends it.
s_gfx->startWrite();
display.panel().flushStart(area->x1, area->y1, w, h, px_map, (uint32_t)w * h * 2);
s_flushing = true;
return; // (UI_PERF_TEST: the wait is timed instead)
}
#endif
s_gfx->startWrite();
s_gfx->setAddrWindow(area->x1, area->y1, w, h);
s_gfx->pushPixels((uint16_t*)px_map, (uint32_t)w * h, true /* LVGL RGB565 is little-endian */);
s_gfx->pushPixels((uint16_t*)px_map, (uint32_t)w * h, !FLUSH_DMA /* LVGL RGB565 is little-endian; _SWAPPED as sent */);
s_gfx->endWrite();
lv_display_flush_ready(disp);
#ifdef UI_PERF_TEST
s_flush_us += micros() - t;
#endif
lv_display_flush_ready(disp);
}
#if FLUSH_DMA
// The area sent last, finished: before LVGL draws into its buffer again, and
// at the end of every frame -- nothing else on the bus meets a transfer.
static void flushWait(lv_display_t* disp) {
(void)disp;
if (!s_flushing) return;
#ifdef UI_PERF_TEST
uint32_t t = micros();
#endif
display.panel().flushWait();
s_gfx->endWrite();
s_flushing = false;
#ifdef UI_PERF_TEST
s_flush_us += micros() - t;
#endif
}
#endif
static void touchCb(lv_indev_t* indev, lv_indev_data_t* data) {
(void)indev;
lgfx::touch_point_t tp;
@@ -158,16 +200,45 @@ static bool begin() {
lv_display_t* disp = lv_display_create(s_gfx->width(), s_gfx->height());
lv_display_set_flush_cb(disp, flushCb);
// Two half-screen buffers (75 KB each) in PSRAM: a frame renders in two
// passes instead of six (every pass walks the whole tree and lays text out
// again) -- measured ~8% faster than 40 lines; internal RAM / DMA / -O2
// made no difference (UI_PERF_TEST).
const uint32_t buf_sz = (uint32_t)s_gfx->width() * 120 * 2;
uint8_t* buf1 = (uint8_t*)heap_caps_malloc(buf_sz, MALLOC_CAP_SPIRAM);
uint8_t* buf2 = (uint8_t*)heap_caps_malloc(buf_sz, MALLOC_CAP_SPIRAM);
if (!buf1) { buf1 = (uint8_t*)heap_caps_malloc(buf_sz, MALLOC_CAP_8BIT); buf2 = nullptr; }
// Sent by DMA (FLUSH_DMA): two 24-line buffers (15 KB each) in internal
// RAM, ten passes a frame, each drawn while the last goes out -- 7-11%
// faster screens than 48 lines in PSRAM, the map the same. Not PSRAM:
// LovyanGFX cuts a transfer into 4092-byte DMA descriptors, and from the
// second one on PSRAM isn't 16-byte aligned as the S3's DMA needs it
// there (bands shifted, or of stale pixels).
// Otherwise two half-screen buffers (75 KB each) in PSRAM: two passes
// instead of six (every pass walks the whole tree and lays text out again)
// -- measured ~8% faster than 40 lines; internal RAM / -O2 made no
// difference (UI_PERF_TEST).
uint32_t buf_sz = 0;
uint8_t *buf1 = nullptr, *buf2 = nullptr;
#if FLUSH_DMA
buf_sz = (uint32_t)s_gfx->width() * 24 * 2;
buf1 = (uint8_t*)heap_caps_malloc(buf_sz, MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA);
buf2 = buf1 ? (uint8_t*)heap_caps_malloc(buf_sz, MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA) : nullptr;
s_dma = buf2 != nullptr;
if (!s_dma && buf1) { heap_caps_free(buf1); buf1 = nullptr; } // no room: sent as it's drawn, as below
#endif
if (!s_dma) {
buf_sz = (uint32_t)s_gfx->width() * 120 * 2;
buf1 = (uint8_t*)heap_caps_malloc(buf_sz, MALLOC_CAP_SPIRAM);
buf2 = (uint8_t*)heap_caps_malloc(buf_sz, MALLOC_CAP_SPIRAM);
if (!buf1) { buf1 = (uint8_t*)heap_caps_malloc(buf_sz, MALLOC_CAP_8BIT); buf2 = nullptr; }
}
if (!buf1) return false;
lv_display_set_buffers(disp, buf1, buf2, buf_sz, LV_DISPLAY_RENDER_MODE_PARTIAL);
#if FLUSH_DMA
// Drawn big-endian, as the panel takes it: swapping a frame's bytes
// before sending cost ~6 ms. Pictures are made so too (map tiles, the
// frozen screen): an RGB565 one is swapped pixel by pixel as it's drawn.
lv_display_set_color_format(disp, LV_COLOR_FORMAT_RGB565_SWAPPED);
if (s_dma) lv_display_set_flush_wait_cb(disp, flushWait);
lv_display_add_event_cb(disp, [](lv_event_t* e) { // the frame's last area, done before anything else runs
if (!s_flushing) return;
flushWait(nullptr);
lv_display_flush_ready((lv_display_t*)lv_event_get_current_target(e));
}, LV_EVENT_REFR_READY, NULL);
#endif
lv_indev_t* indev = lv_indev_create();
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
+12 -2
View File
@@ -322,16 +322,26 @@ namespace freeze {
s_retry = nullptr;
take((lv_obj_t*)lv_timer_get_user_data(t));
}
// Darkened into the display's own byte order (UI_FLUSH_DMA: big-endian;
// lv_snapshot can't make that one itself): drawn then as a plain copy.
#ifdef UI_FLUSH_DMA
static inline uint16_t out565(uint16_t c) { return __builtin_bswap16(c); }
#else
static inline uint16_t out565(uint16_t c) { return c; }
#endif
static void darken(lv_draw_buf_t* buf) { // as the dim layer over it would
const uint32_t keep = 256 - s_dim; // what's left under the black
for (uint32_t y = 0; y < buf->header.h; y++) {
uint16_t* p = (uint16_t*)(buf->data + y * buf->header.stride);
for (uint32_t x = 0; x < buf->header.w; x++) {
uint32_t c = p[x];
p[x] = (uint16_t)(((((c >> 11) * keep) >> 8) << 11) | (((((c >> 5) & 0x3F) * keep) >> 8) << 5) |
(((c & 0x1F) * keep) >> 8));
p[x] = out565((uint16_t)(((((c >> 11) * keep) >> 8) << 11) | (((((c >> 5) & 0x3F) * keep) >> 8) << 5) |
(((c & 0x1F) * keep) >> 8)));
}
}
#ifdef UI_FLUSH_DMA
buf->header.cf = LV_COLOR_FORMAT_RGB565_SWAPPED;
#endif
}
static lv_draw_buf_t* snap(lv_obj_t* overlay, lv_obj_t* scr, bool top) {
if (!top) lv_obj_add_flag(overlay, LV_OBJ_FLAG_HIDDEN);
@@ -90,7 +90,11 @@ public:
s->present = true;
memset(&s->dsc, 0, sizeof(s->dsc));
s->dsc.header.magic = LV_IMAGE_HEADER_MAGIC;
#ifdef UI_FLUSH_DMA
s->dsc.header.cf = LV_COLOR_FORMAT_RGB565_SWAPPED;
#else
s->dsc.header.cf = LV_COLOR_FORMAT_RGB565;
#endif
s->dsc.header.w = TILE_PX;
s->dsc.header.h = TILE_PX;
s->dsc.header.stride = TILE_PX * 2;
@@ -151,6 +155,9 @@ private:
uint32_t t0 = micros();
#endif
_job.present = _spare && _job.src->renderTile(_job.z, _job.x, _job.y, _spare);
#ifdef UI_FLUSH_DMA // in the display's byte order: drawn as a plain copy (LvglPort.h)
if (_job.present) lv_draw_sw_rgb565_swap(_spare, TILE_PX * TILE_PX);
#endif
#ifdef UI_PERF_TEST
uint32_t dt = micros() - t0;
if (_job.present) { s_perf_tile_us += dt; s_perf_tile_n++; if (dt > s_perf_tile_max) s_perf_tile_max = dt; }