mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 10:21:59 +00:00
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:
@@ -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 = lv_display_get_horizontal_resolution(NULL);
|
||||||
const int32_t w = a->x2 - a->x1 + 1;
|
const int32_t w = a->x2 - a->x1 + 1;
|
||||||
const uint16_t* src = (const uint16_t*)px;
|
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
|
// 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 lgfx::LGFX_Device* s_gfx = nullptr;
|
||||||
static bool s_swallow = false; // ignore the touch that woke the display until it lifts
|
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
|
// 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
|
// 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);
|
shotCopy(area, px_map);
|
||||||
int w = area->x2 - area->x1 + 1;
|
int w = area->x2 - area->x1 + 1;
|
||||||
int h = area->y2 - area->y1 + 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->startWrite();
|
||||||
s_gfx->setAddrWindow(area->x1, area->y1, w, h);
|
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();
|
s_gfx->endWrite();
|
||||||
|
lv_display_flush_ready(disp);
|
||||||
#ifdef UI_PERF_TEST
|
#ifdef UI_PERF_TEST
|
||||||
s_flush_us += micros() - t;
|
s_flush_us += micros() - t;
|
||||||
#endif
|
#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) {
|
static void touchCb(lv_indev_t* indev, lv_indev_data_t* data) {
|
||||||
(void)indev;
|
(void)indev;
|
||||||
lgfx::touch_point_t tp;
|
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_t* disp = lv_display_create(s_gfx->width(), s_gfx->height());
|
||||||
lv_display_set_flush_cb(disp, flushCb);
|
lv_display_set_flush_cb(disp, flushCb);
|
||||||
|
|
||||||
// Two half-screen buffers (75 KB each) in PSRAM: a frame renders in two
|
// Sent by DMA (FLUSH_DMA): two 24-line buffers (15 KB each) in internal
|
||||||
// passes instead of six (every pass walks the whole tree and lays text out
|
// RAM, ten passes a frame, each drawn while the last goes out -- 7-11%
|
||||||
// again) -- measured ~8% faster than 40 lines; internal RAM / DMA / -O2
|
// faster screens than 48 lines in PSRAM, the map the same. Not PSRAM:
|
||||||
// made no difference (UI_PERF_TEST).
|
// LovyanGFX cuts a transfer into 4092-byte DMA descriptors, and from the
|
||||||
const uint32_t buf_sz = (uint32_t)s_gfx->width() * 120 * 2;
|
// second one on PSRAM isn't 16-byte aligned as the S3's DMA needs it
|
||||||
uint8_t* buf1 = (uint8_t*)heap_caps_malloc(buf_sz, MALLOC_CAP_SPIRAM);
|
// there (bands shifted, or of stale pixels).
|
||||||
uint8_t* buf2 = (uint8_t*)heap_caps_malloc(buf_sz, MALLOC_CAP_SPIRAM);
|
// Otherwise two half-screen buffers (75 KB each) in PSRAM: two passes
|
||||||
if (!buf1) { buf1 = (uint8_t*)heap_caps_malloc(buf_sz, MALLOC_CAP_8BIT); buf2 = nullptr; }
|
// 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;
|
if (!buf1) return false;
|
||||||
lv_display_set_buffers(disp, buf1, buf2, buf_sz, LV_DISPLAY_RENDER_MODE_PARTIAL);
|
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_t* indev = lv_indev_create();
|
||||||
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
|
lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER);
|
||||||
|
|||||||
@@ -322,16 +322,26 @@ namespace freeze {
|
|||||||
s_retry = nullptr;
|
s_retry = nullptr;
|
||||||
take((lv_obj_t*)lv_timer_get_user_data(t));
|
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
|
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
|
const uint32_t keep = 256 - s_dim; // what's left under the black
|
||||||
for (uint32_t y = 0; y < buf->header.h; y++) {
|
for (uint32_t y = 0; y < buf->header.h; y++) {
|
||||||
uint16_t* p = (uint16_t*)(buf->data + y * buf->header.stride);
|
uint16_t* p = (uint16_t*)(buf->data + y * buf->header.stride);
|
||||||
for (uint32_t x = 0; x < buf->header.w; x++) {
|
for (uint32_t x = 0; x < buf->header.w; x++) {
|
||||||
uint32_t c = p[x];
|
uint32_t c = p[x];
|
||||||
p[x] = (uint16_t)(((((c >> 11) * keep) >> 8) << 11) | (((((c >> 5) & 0x3F) * keep) >> 8) << 5) |
|
p[x] = out565((uint16_t)(((((c >> 11) * keep) >> 8) << 11) | (((((c >> 5) & 0x3F) * keep) >> 8) << 5) |
|
||||||
(((c & 0x1F) * keep) >> 8));
|
(((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) {
|
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);
|
if (!top) lv_obj_add_flag(overlay, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
|||||||
@@ -90,7 +90,11 @@ public:
|
|||||||
s->present = true;
|
s->present = true;
|
||||||
memset(&s->dsc, 0, sizeof(s->dsc));
|
memset(&s->dsc, 0, sizeof(s->dsc));
|
||||||
s->dsc.header.magic = LV_IMAGE_HEADER_MAGIC;
|
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;
|
s->dsc.header.cf = LV_COLOR_FORMAT_RGB565;
|
||||||
|
#endif
|
||||||
s->dsc.header.w = TILE_PX;
|
s->dsc.header.w = TILE_PX;
|
||||||
s->dsc.header.h = TILE_PX;
|
s->dsc.header.h = TILE_PX;
|
||||||
s->dsc.header.stride = TILE_PX * 2;
|
s->dsc.header.stride = TILE_PX * 2;
|
||||||
@@ -151,6 +155,9 @@ private:
|
|||||||
uint32_t t0 = micros();
|
uint32_t t0 = micros();
|
||||||
#endif
|
#endif
|
||||||
_job.present = _spare && _job.src->renderTile(_job.z, _job.x, _job.y, _spare);
|
_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
|
#ifdef UI_PERF_TEST
|
||||||
uint32_t dt = micros() - t0;
|
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; }
|
if (_job.present) { s_perf_tile_us += dt; s_perf_tile_n++; if (dt > s_perf_tile_max) s_perf_tile_max = dt; }
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ build_flags =
|
|||||||
-D GPS_SKYVIEW ; satellites / signal / DOPs for the GPS screen (helpers/sensors/GpsSky.h)
|
-D GPS_SKYVIEW ; satellites / signal / DOPs for the GPS screen (helpers/sensors/GpsSky.h)
|
||||||
-D ESP32_LITTLEFS ; internal flash as LittleFS, not SPIFFS (helpers/esp32/InternalFS.h)
|
-D ESP32_LITTLEFS ; internal flash as LittleFS, not SPIFFS (helpers/esp32/InternalFS.h)
|
||||||
-D ENABLE_SCREENSHOT ; the screen over USB for the site's tools (RGB565, UITask::captureFrame)
|
-D ENABLE_SCREENSHOT ; the screen over USB for the site's tools (RGB565, UITask::captureFrame)
|
||||||
|
-DUI_FLUSH_DMA ; the panel fed by DMA while LVGL draws, in its byte order (ui-lvgl/LvglPort.h)
|
||||||
build_src_filter = ${Wio_Tracker_L2.build_src_filter}
|
build_src_filter = ${Wio_Tracker_L2.build_src_filter}
|
||||||
+<helpers/esp32/*.cpp>
|
+<helpers/esp32/*.cpp>
|
||||||
+<helpers/ui/LGFXDisplay.cpp>
|
+<helpers/ui/LGFXDisplay.cpp>
|
||||||
@@ -111,6 +112,7 @@ lib_deps =
|
|||||||
[env:Wio_Tracker_L2_companion_solo_lvgl_arduino2]
|
[env:Wio_Tracker_L2_companion_solo_lvgl_arduino2]
|
||||||
extends = env:Wio_Tracker_L2_companion_solo_lvgl
|
extends = env:Wio_Tracker_L2_companion_solo_lvgl
|
||||||
platform = ${esp32_base.platform}
|
platform = ${esp32_base.platform}
|
||||||
|
build_unflags = -DARDUINO_USB_MODE=1 -DUI_FLUSH_DMA ; DMA flush untried on ESP-IDF 4.4
|
||||||
lib_deps =
|
lib_deps =
|
||||||
${Wio_Tracker_L2.lib_deps}
|
${Wio_Tracker_L2.lib_deps}
|
||||||
densaugeo/base64 @ ~1.4.0
|
densaugeo/base64 @ ~1.4.0
|
||||||
|
|||||||
@@ -88,13 +88,30 @@ public:
|
|||||||
virtual ~WioTrackerL2Backlight() = default;
|
virtual ~WioTrackerL2Backlight() = default;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The panel, plus a write that doesn't wait: an area's pixels (already in the
|
||||||
|
// panel's byte order, at most 32 KB -- one SPI transaction) go out by DMA
|
||||||
|
// while the CPU draws the next one; flushWait() ends it. The library's own
|
||||||
|
// writes wait for the wire every time (the ui-lvgl frame's ~8 ms).
|
||||||
|
struct Panel_WioTrackerL2 : public lgfx::Panel_NV3031B {
|
||||||
|
void flushStart(int x, int y, int w, int h, const uint8_t* data, uint32_t len) {
|
||||||
|
setWindow(x, y, x + w - 1, y + h - 1);
|
||||||
|
start_qspi();
|
||||||
|
_bus->writeBytes(data, len, true, true);
|
||||||
|
}
|
||||||
|
void flushWait() {
|
||||||
|
_bus->wait();
|
||||||
|
end_qspi();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
class LGFX_WioTrackerL2 : public lgfx::LGFX_Device {
|
class LGFX_WioTrackerL2 : public lgfx::LGFX_Device {
|
||||||
lgfx::Panel_NV3031B _panel_instance;
|
Panel_WioTrackerL2 _panel_instance;
|
||||||
lgfx::Bus_SPI _bus_instance;
|
lgfx::Bus_SPI _bus_instance;
|
||||||
lgfx::Touch_GT911 _touch_instance;
|
lgfx::Touch_GT911 _touch_instance;
|
||||||
WioTrackerL2Backlight _light_instance;
|
WioTrackerL2Backlight _light_instance;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
Panel_WioTrackerL2& panelL2() { return _panel_instance; }
|
||||||
bool init_impl(bool use_reset, bool use_clear) override {
|
bool init_impl(bool use_reset, bool use_clear) override {
|
||||||
// bring up backlight controller while the I2C bus is still clean
|
// bring up backlight controller while the I2C bus is still clean
|
||||||
_light_instance.init(_light_instance.getBrightness());
|
_light_instance.init(_light_instance.getBrightness());
|
||||||
@@ -182,6 +199,7 @@ public:
|
|||||||
|
|
||||||
// direct access to the LGFX device (LVGL flush/touch glue)
|
// direct access to the LGFX device (LVGL flush/touch glue)
|
||||||
lgfx::LGFX_Device* lgfxDevice() { return &disp; }
|
lgfx::LGFX_Device* lgfxDevice() { return &disp; }
|
||||||
|
Panel_WioTrackerL2& panel() { return disp.panelL2(); }
|
||||||
// LVGL draws straight to the panel: the frame sprite (76 KB of PSRAM) only
|
// LVGL draws straight to the panel: the frame sprite (76 KB of PSRAM) only
|
||||||
// carried the boot "Loading..." -- given back once LVGL takes over.
|
// carried the boot "Loading..." -- given back once LVGL takes over.
|
||||||
void releaseFrameBuffer() { buffer.deleteSprite(); }
|
void releaseFrameBuffer() { buffer.deleteSprite(); }
|
||||||
|
|||||||
Reference in New Issue
Block a user