From 247fab08d0fca3ab33fa89351b58b17f561221d4 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Wed, 30 Sep 2026 00:24:57 +0200 Subject: [PATCH] perf(l2): map tiles decode on the other core - TileCache: request() hands z/x/y to a task on core 0, which renders into a spare buffer; poll() swaps it into the LRU slot (stale results dropped after drop()/invalidate()); the sim runs the job at once - MapScreen / Home map place finished tiles as they arrive; the longest UI stall with the map opening goes from ~340 to ~170 ms - LVGL allocates from the PSRAM heap (lv_mem_psram.h), safe from both tasks; LVGL's own pool is gone - lv_conf: notes why LV_OS_FREERTOS with two draw units stays off (measured slower: drawing is bound by PSRAM bandwidth) - UI_PERF_TEST: the map (tile read/decode, pan, redraw), build time and the longest loop pass per screen Co-Authored-By: Claude Opus 5.5 --- examples/companion_radio/ui-lvgl/HomeScreen.h | 12 +- examples/companion_radio/ui-lvgl/MapScreen.h | 47 ++++++- examples/companion_radio/ui-lvgl/UITask.cpp | 21 ++- examples/companion_radio/ui-lvgl/UITask.h | 3 + examples/companion_radio/ui-lvgl/lv_conf.h | 20 +-- .../companion_radio/ui-lvgl/lv_mem_psram.h | 37 +++++ .../companion_radio/ui-lvgl/lv_psram_pool.h | 7 - .../companion_radio/ui-lvgl/map/TileCache.h | 127 ++++++++++++++---- .../ui-lvgl/map/TileProvider.h | 16 ++- 9 files changed, 230 insertions(+), 60 deletions(-) create mode 100644 examples/companion_radio/ui-lvgl/lv_mem_psram.h delete mode 100644 examples/companion_radio/ui-lvgl/lv_psram_pool.h diff --git a/examples/companion_radio/ui-lvgl/HomeScreen.h b/examples/companion_radio/ui-lvgl/HomeScreen.h index 61612988..4973e616 100644 --- a/examples/companion_radio/ui-lvgl/HomeScreen.h +++ b/examples/companion_radio/ui-lvgl/HomeScreen.h @@ -765,8 +765,8 @@ void UITask::homeMapLayout() { else lv_obj_add_flag(s_hint, LV_OBJ_FLAG_HIDDEN); } -// From loop() while the map page is up: one tile decode per pass, a re-frame -// every few seconds. +// From loop() while the map page is up: tiles one at a time (decoded on the +// other core, placed here), a re-frame every few seconds. void UITask::homeMapLoop() { using namespace home::mini; if (!s_area || home::s_touching) return; @@ -774,7 +774,8 @@ void UITask::homeMapLoop() { s_next_fit_ms = millis() + 5000; if (homeMapFit()) homeMapLayout(); } - if (!s_pending) return; + if (mapview::pollTiles()) homeMapLayout(); + if (!s_pending || mapview::s_cache.busy()) return; int w = lv_obj_get_width(s_area), h = lv_obj_get_height(s_area); double left = s_cx * mapview::TILE_PX - w / 2.0, top = s_cy * mapview::TILE_PX - h / 2.0; int tx0 = (int)floor(left / mapview::TILE_PX), ty0 = (int)floor(top / mapview::TILE_PX); @@ -785,12 +786,11 @@ void UITask::homeMapLoop() { int px = (int)lround(tx * (double)mapview::TILE_PX - left), py = (int)lround(ty * (double)mapview::TILE_PX - top); if (ty < 0 || ty >= n || px >= w || py >= h) continue; mapview::TileCache::Slot* s = mapview::s_cache.find(s_z, wx, ty); - if (!s) { mapview::s_cache.load(*mapview::s_provider, s_z, wx, ty); homeMapLayout(); return; } + if (!s) { mapview::loadTile(s_z, wx, ty); return; } if (s->present) continue; int k, wz, ax, ay; if (!mapview::ancestorFor(s_z, wx, ty, k, wz, ax, ay) && wz >= 0) { - mapview::s_cache.load(*mapview::s_provider, wz, ax, ay); - homeMapLayout(); + mapview::loadTile(wz, ax, ay); return; } } diff --git a/examples/companion_radio/ui-lvgl/MapScreen.h b/examples/companion_radio/ui-lvgl/MapScreen.h index e7a42414..c3fa4c2d 100644 --- a/examples/companion_radio/ui-lvgl/MapScreen.h +++ b/examples/companion_radio/ui-lvgl/MapScreen.h @@ -34,12 +34,17 @@ static TileDownloader s_dl("/sdcard/maps"); static AreaStore s_areas("/sdcard/maps"); static const uint32_t AVG_TILE_BYTES = 22 * 1024; // OpenTopoMap-ish, for the size estimate -// Decode a tile into the cache; with live tiles on, a base tile shown without -// its trails overlay has the overlay fetched. -static TileCache::Slot* loadTile(int z, int x, int y) { - TileCache::Slot* s = s_cache.load(*s_provider, z, x, y); - if (s_trails_on && s_provider == &s_raster && s_raster.overlayMissed()) s_dl.liveRequest(z, x, y, true); - return s; +// Hand a tile to the decoder (on the other core); false while one is in flight. +static bool loadTile(int z, int x, int y) { return s_cache.request(*s_provider, z, x, y); } + +// A decoded tile placed in the cache: true when a view has something new. +// With live tiles on, a base tile shown without its trails overlay has the +// overlay fetched. +static bool pollTiles() { + TileCache::Done d; + if (!s_cache.poll(d)) return false; + if (s_trails_on && d.src == &s_raster && d.overlay_missed) s_dl.liveRequest(d.z, d.x, d.y, true); + return true; } static const int MIN_Z = 3, MAX_Z = 18; @@ -279,6 +284,32 @@ double UITask::mapCenterBias() const { return _map_nav ? navmap::BAR_H / 2.0 / mapview::TILE_PX : 0.0; } +#ifdef UI_PERF_TEST +// UI_PERF_TEST (UITask::loop): reset = before the map opens; else report the +// tiles decoded since, then time a drag and a full redraw. +void UITask::perfMap(bool reset) { + using namespace mapview; + if (reset) { s_perf_tile_us = s_perf_tile_max = s_perf_tile_n = 0; return; } + while (s_cache.busy()) { delay(5); pollTiles(); } // a prefetch still decoding would share PSRAM with the drawing + Serial.printf("PERF tiles %lu decoded, avg %5.1f ms, max %5.1f ms (pending %d)\n", (unsigned long)s_perf_tile_n, + s_perf_tile_n ? s_perf_tile_us / 1000.0f / s_perf_tile_n : 0.0f, s_perf_tile_max / 1000.0f, (int)_map_pending); + if (s_perf_tile_n) Serial.printf("PERF of which read %5.1f ms (%lu B), decode %5.1f ms\n", s_raster.perf_read_us / 1000.0f / s_perf_tile_n, + (unsigned long)(s_raster.perf_bytes / s_perf_tile_n), s_raster.perf_dec_us / 1000.0f / s_perf_tile_n); + s_raster.perf_read_us = s_raster.perf_dec_us = s_raster.perf_bytes = 0; + uint32_t fl0 = lvport::s_flush_us, t = micros(); + for (int i = 0; i < 20; i++) { // a drag: 4 px a frame, tiles already decoded + _map_cx += (i < 10 ? 4.0 : -4.0) / TILE_PX; + layoutMap(); + lv_refr_now(NULL); + } + Serial.printf("PERF map pan frame %5.1f ms, flush %5.1f ms\n", (micros() - t) / 20000.0f, (lvport::s_flush_us - fl0) / 20000.0f); + fl0 = lvport::s_flush_us; t = micros(); + for (int i = 0; i < 10; i++) { lv_obj_invalidate(lv_screen_active()); lv_refr_now(NULL); } + Serial.printf("PERF map full redraw %5.1f ms, flush %5.1f ms\n", (micros() - t) / 10000.0f, (lvport::s_flush_us - fl0) / 10000.0f); + s_perf_tile_us = s_perf_tile_max = s_perf_tile_n = 0; +} +#endif + void UITask::openMap(bool nav) { _map_nav = nav; showMap(); @@ -552,9 +583,11 @@ void UITask::mapLoop() { refreshNavBar(); } if (!_map_area) return; + if (mapview::pollTiles()) layoutMap(); // a tile decoded on the other core uint32_t since_pan = millis() - mapview::s_last_pan_ms; if (since_pan < mapview::PAN_SETTLE_MS) return; // mid-drag: keep it smooth, decode after if (mapview::labels::loadOne()) { layoutMap(); return; } // names of the view first: a small file + if (mapview::s_cache.busy()) return; // one tile at a time int w = lv_obj_get_width(_map_area), h = lv_obj_get_height(_map_area); double left = _map_cx * mapview::TILE_PX - w / 2.0, top = _map_cy * mapview::TILE_PX - h / 2.0; if (!_map_pending) { // view complete: when idle, decode the next tile a pan would reveal @@ -599,8 +632,8 @@ void UITask::mapLoop() { } } if (bz >= 0) mapview::loadTile(bz, best_x, best_y); + else layoutMap(); // nothing left to decode: pending settles } - layoutMap(); } void UITask::rebuildMapMarkers() { diff --git a/examples/companion_radio/ui-lvgl/UITask.cpp b/examples/companion_radio/ui-lvgl/UITask.cpp index d4ac3f43..381c06c8 100644 --- a/examples/companion_radio/ui-lvgl/UITask.cpp +++ b/examples/companion_radio/ui-lvgl/UITask.cpp @@ -82,6 +82,7 @@ template static T* psramBuf(size_t n) { #include "Theme.h" #include "Anim.h" #include "LvglPort.h" +#include "lv_mem_psram.h" #include "HistoryStore.h" #include "map/LiveCache.h" static histstore::SdArchive s_archive; // message history on the SD card @@ -1031,6 +1032,10 @@ void UITask::loop() { // display refresh took during the transition that followed (render+flush). { static uint32_t next = 15000, sum_us = 0, max_us = 0, frames = 0, t0 = 0; + static uint32_t last_pass_us = 0, max_gap_us = 0; // the longest the loop was away (a blocked UI) + uint32_t now_us = micros(); + if (last_pass_us && now_us - last_pass_us > max_gap_us) max_gap_us = now_us - last_pass_us; + last_pass_us = now_us; static int step = 0; static const char* name = ""; static bool hooked = false; @@ -1047,19 +1052,25 @@ void UITask::loop() { if (millis() >= next) { if (*name) Serial.printf("PERF %-10s frames %2lu avg %5.1f ms max %5.1f ms\n", name, (unsigned long)frames, frames ? sum_us / 1000.0f / frames : 0.0f, max_us / 1000.0f); - if (*name) Serial.printf("PERF flush %5.1f ms per frame\n", frames ? lvport::s_flush_us / 1000.0f / frames : 0.0f); + if (*name) Serial.printf("PERF flush %5.1f ms per frame, longest loop pass %5.1f ms\n", + frames ? lvport::s_flush_us / 1000.0f / frames : 0.0f, max_gap_us / 1000.0f); sum_us = max_us = frames = 0; lvport::s_flush_us = 0; + max_gap_us = 0; lv_display_trigger_activity(NULL); // no sleeping mid-test next = millis() + 1200; if (step == 0) unlockScreen(); // a PIN lock would be drawn over everything - if (step % 8 == 0 && step > 0) { // a static full-screen redraw of Home, for the baseline + if (step % 10 == 0 && step > 0) { // a static full-screen redraw of Home, for the baseline uint32_t fl0 = lvport::s_flush_us, t = micros(); for (int i = 0; i < 10; i++) { lv_obj_invalidate(lv_screen_active()); lv_refr_now(NULL); } Serial.printf("PERF full redraw (Home) %5.1f ms, flush %5.1f ms\n", (micros() - t) / 10000.0f, (lvport::s_flush_us - fl0) / 10000.0f); } - switch (step++ % 8) { + if (step % 10 == 9) perfMap(); // the map: tiles decoded while it opened, panning, a full redraw + uint32_t tb = micros(); + switch (step++ % 10) { + case 8: name = "map"; perfMap(true); openMap(true); next = millis() + 8000; break; + case 9: name = "back-home"; back(); break; case 0: name = "settings"; showSettings(); break; case 1: name = "back-home"; back(); break; case 2: name = "messages"; showChats(); break; @@ -1069,6 +1080,7 @@ void UITask::loop() { case 6: name = "page-2"; setHomePage(1); break; case 7: name = "page-1"; setHomePage(0); break; } + Serial.printf("PERF build %-10s %5.1f ms\n", name, (micros() - tb) / 1000.0f); } } #endif @@ -3598,6 +3610,9 @@ void UITask::setKeyboardAlphabets(int main_idx, int alt_sel) { // a message being read. static constexpr uint32_t CPU_IDLE_MS = 2000; bool UITask::cpuNeeded() { +#ifdef UI_PERF_TEST + return true; // measured at the full clock +#endif if (lv_display_get_inactive_time(NULL) < CPU_IDLE_MS || lv_anim_count_running() > 0) return true; if (wifiInUse() || mapview::s_dl.liveQueued() > 0) return true; if (_screen == SCR_MAP && _map_pending) return true; diff --git a/examples/companion_radio/ui-lvgl/UITask.h b/examples/companion_radio/ui-lvgl/UITask.h index 70a2a4dc..1c82ba89 100644 --- a/examples/companion_radio/ui-lvgl/UITask.h +++ b/examples/companion_radio/ui-lvgl/UITask.h @@ -313,6 +313,9 @@ private: void buildHomeMap(lv_obj_t* box); void homeFieldText(uint8_t field, char* buf, int n); bool homeMapFit(); +#ifdef UI_PERF_TEST + void perfMap(bool reset = false); +#endif void homeMapLayout(); void homeMapLoop(); void refreshHome(); diff --git a/examples/companion_radio/ui-lvgl/lv_conf.h b/examples/companion_radio/ui-lvgl/lv_conf.h index 9d2e365f..bcf42dc8 100644 --- a/examples/companion_radio/ui-lvgl/lv_conf.h +++ b/examples/companion_radio/ui-lvgl/lv_conf.h @@ -6,18 +6,20 @@ #define LV_COLOR_DEPTH 16 -// Memory: LVGL's own allocator over one pool. On ESP32-S3 the pool lives in -// PSRAM so widgets / text never compete with BLE and the radio for internal RAM. -#define LV_USE_STDLIB_MALLOC LV_STDLIB_BUILTIN +// Memory. On ESP32-S3: the PSRAM heap (lv_mem_psram.h), thread-safe -- map +// tiles decode on the other core. Elsewhere (the sim): LVGL's own pool. +#if defined(ESP32) + #define LV_USE_STDLIB_MALLOC LV_STDLIB_CUSTOM +#else + #define LV_USE_STDLIB_MALLOC LV_STDLIB_BUILTIN + #define LV_MEM_SIZE (2048U * 1024U) // map tiles decode through it (~0.5 MB peak per PNG) +#endif #define LV_USE_STDLIB_STRING LV_STDLIB_CLIB #define LV_USE_STDLIB_SPRINTF LV_STDLIB_CLIB -#define LV_MEM_SIZE (2048U * 1024U) // map tiles decode through it (~0.5 MB peak per PNG) -#if defined(ESP32) - #define LV_MEM_ADR 0 - #define LV_MEM_POOL_INCLUDE "lv_psram_pool.h" - #define LV_MEM_POOL_ALLOC lv_psram_pool_alloc -#endif +// No OS: the loop draws. LV_OS_FREERTOS with two software draw units was +// measured slower on the L2 (map redraw ~105 ms against 55-80; one unit ~89): +// drawing here is bound by PSRAM bandwidth, which the two units share. #define LV_USE_OS LV_OS_NONE #define LV_DEF_REFR_PERIOD 20 // ms; the loop also services the radio #define LV_DPI_DEF 130 // 2.8" 320x240 diff --git a/examples/companion_radio/ui-lvgl/lv_mem_psram.h b/examples/companion_radio/ui-lvgl/lv_mem_psram.h new file mode 100644 index 00000000..98710a66 --- /dev/null +++ b/examples/companion_radio/ui-lvgl/lv_mem_psram.h @@ -0,0 +1,37 @@ +#pragma once +// LVGL's allocator on the ESP32-S3 (LV_STDLIB_CUSTOM in lv_conf.h): the heap +// in PSRAM, so widgets / text never compete with BLE and the radio for +// internal RAM, and -- unlike LVGL's own pool -- safe from two tasks at once: +// map tiles decode through LVGL's lodepng on the other core (TileCache). +// (LVGL's TLSF pool in PSRAM for the loop was measured: malloc+free ~3.5 us +// against ~8, yet screens built no faster -- widget cost is PSRAM and cache +// misses, not the allocator -- and it held 2 MB aside.) +// +// Single-TU fragment: included by ui-lvgl/UITask.cpp only. + +#if defined(ESP32) +#include + +extern "C" { +void lv_mem_init(void) {} +void lv_mem_deinit(void) {} +lv_mem_pool_t lv_mem_add_pool(void* mem, size_t bytes) { (void)mem; (void)bytes; return NULL; } +void lv_mem_remove_pool(lv_mem_pool_t pool) { (void)pool; } +void* lv_malloc_core(size_t size) { return heap_caps_malloc(size, MALLOC_CAP_SPIRAM); } +void* lv_realloc_core(void* p, size_t new_size) { return heap_caps_realloc(p, new_size, MALLOC_CAP_SPIRAM); } +void lv_free_core(void* p) { heap_caps_free(p); } +void lv_mem_monitor_core(lv_mem_monitor_t* mon) { + multi_heap_info_t i; + heap_caps_get_info(&i, MALLOC_CAP_SPIRAM); + mon->total_size = i.total_free_bytes + i.total_allocated_bytes; + mon->free_size = i.total_free_bytes; + mon->free_biggest_size = i.largest_free_block; + mon->free_cnt = i.free_blocks; + mon->used_cnt = i.allocated_blocks; + mon->max_used = i.total_free_bytes + i.total_allocated_bytes - i.minimum_free_bytes; + mon->used_pct = mon->total_size ? (uint8_t)(100 * i.total_allocated_bytes / mon->total_size) : 0; + mon->frag_pct = i.total_free_bytes ? (uint8_t)(100 - 100 * (uint64_t)i.largest_free_block / i.total_free_bytes) : 0; +} +lv_result_t lv_mem_test_core(void) { return LV_RESULT_OK; } +} +#endif diff --git a/examples/companion_radio/ui-lvgl/lv_psram_pool.h b/examples/companion_radio/ui-lvgl/lv_psram_pool.h deleted file mode 100644 index b55605b6..00000000 --- a/examples/companion_radio/ui-lvgl/lv_psram_pool.h +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once -// LVGL's builtin allocator pool, placed in PSRAM (see lv_conf.h). -#include - -static inline void* lv_psram_pool_alloc(size_t sz) { - return heap_caps_malloc(sz, MALLOC_CAP_SPIRAM); -} diff --git a/examples/companion_radio/ui-lvgl/map/TileCache.h b/examples/companion_radio/ui-lvgl/map/TileCache.h index 85e17868..d9bcd5d8 100644 --- a/examples/companion_radio/ui-lvgl/map/TileCache.h +++ b/examples/companion_radio/ui-lvgl/map/TileCache.h @@ -15,6 +15,10 @@ extern "C" void lv_image_cache_drop(const void* src); // not exported through namespace mapview { +#ifdef UI_PERF_TEST +static uint32_t s_perf_tile_us = 0, s_perf_tile_max = 0, s_perf_tile_n = 0; // renderTile() time +#endif + class TileCache { public: static const int SLOTS = 24; // 6 on screen at 320x218 + the 14-tile read-ahead ring + a few coarser (3 MB) @@ -38,37 +42,63 @@ public: return nullptr; } - // Render z/x/y through `src` into the least recently used slot. - Slot* load(TileProvider& src, int z, int x, int y) { - Slot* s = &_slots[0]; - for (Slot& c : _slots) if (c.used < s->used) s = &c; - if (!s->px) { + // Decoding runs off the UI loop: request() hands z/x/y to a task on core 0 + // (ESP32; a tile takes ~130 ms to read and decode), which renders it into a + // spare buffer; poll() swaps that into the least recently used slot, so a + // slot on screen is never drawn half written. One tile at a time: false + // while one is in flight. In the sim the job runs at once, the rest alike. + bool busy() const { return _job_state != JOB_IDLE; } + bool request(TileProvider& src, int z, int x, int y) { + if (_job_state != JOB_IDLE) return false; + _job.src = &src; + _job.z = (int16_t)z; _job.x = x; _job.y = y; + _job_stale = false; + _job_state = JOB_RUNNING; #if defined(ESP32) - s->px = (uint16_t*)heap_caps_malloc(TILE_PX * TILE_PX * 2, MALLOC_CAP_SPIRAM); + if (!_task && xTaskCreatePinnedToCore(jobTask, "tiles", 12288, this, tskIDLE_PRIORITY + 1, &_task, 0) != pdPASS) { + _task = nullptr; + runJob(); // no task: here, as before + return true; + } + xTaskNotifyGive(_task); #else - s->px = (uint16_t*)malloc(TILE_PX * TILE_PX * 2); + runJob(); #endif - if (!s->px) return nullptr; - } - lv_image_cache_drop(&s->dsc); // same descriptor, new pixels - s->z = z; s->x = x; s->y = y; - s->used = ++_tick; - s->present = src.renderTile(z, x, y, s->px); - if (!s->present) { // remember the miss without holding a buffer slot + return true; + } + + // A finished tile into its slot. True when one was placed (or found + // missing): the view has something new. `done` = which, and whether the + // raster provider went without its trails overlay. + struct Done { int z; int32_t x, y; TileProvider* src; bool overlay_missed; }; + bool poll(Done& done) { + if (_job_state != JOB_DONE) return false; + done = { _job.z, _job.x, _job.y, _job.src, _job.overlay_missed }; + bool keep = !_job_stale; + if (keep && !_job.present) { // remember the miss without holding a buffer slot Missing& m = _missing[_miss_next++ % MISSING]; - m.z = (int16_t)z; m.x = x; m.y = y; - s->z = -1; s->used = 0; - return &_miss_slot; + m.z = _job.z; m.x = _job.x; m.y = _job.y; + } else if (keep) { + Slot* s = &_slots[0]; + for (Slot& c : _slots) if (c.used < s->used) s = &c; + lv_image_cache_drop(&s->dsc); // same descriptor, new pixels + uint16_t* old = s->px; + s->px = _spare; + _spare = old; // nullptr while slots are still unallocated: the task makes one + s->z = _job.z; s->x = _job.x; s->y = _job.y; + s->used = ++_tick; + s->present = true; + memset(&s->dsc, 0, sizeof(s->dsc)); + s->dsc.header.magic = LV_IMAGE_HEADER_MAGIC; + s->dsc.header.cf = LV_COLOR_FORMAT_RGB565; + s->dsc.header.w = TILE_PX; + s->dsc.header.h = TILE_PX; + s->dsc.header.stride = TILE_PX * 2; + s->dsc.data_size = TILE_PX * TILE_PX * 2; + s->dsc.data = (const uint8_t*)s->px; } - memset(&s->dsc, 0, sizeof(s->dsc)); - s->dsc.header.magic = LV_IMAGE_HEADER_MAGIC; - s->dsc.header.cf = LV_COLOR_FORMAT_RGB565; - s->dsc.header.w = TILE_PX; - s->dsc.header.h = TILE_PX; - s->dsc.header.stride = TILE_PX * 2; - s->dsc.data_size = TILE_PX * TILE_PX * 2; - s->dsc.data = (const uint8_t*)s->px; - return s; + _job_state = JOB_IDLE; + return keep; } // Forget the "no tile here" answers (new tiles may have been written). @@ -82,12 +112,55 @@ public: void drop(int z, int x, int y) { for (Slot& s : _slots) if (s.z == z && s.x == x && s.y == y) { s.z = -1; s.used = 0; } forgetMissing(z, x, y); + if (_job_state != JOB_IDLE && _job.z == z && _job.x == x && _job.y == y) _job_stale = true; } // Forget everything (e.g. the card was swapped); keeps the buffers. - void invalidate() { for (Slot& s : _slots) { s.z = -1; s.used = 0; } forgetMissing(); } + void invalidate() { + for (Slot& s : _slots) { s.z = -1; s.used = 0; } + forgetMissing(); + if (_job_state != JOB_IDLE) _job_stale = true; // rendered from what's gone + } private: + enum : uint8_t { JOB_IDLE, JOB_RUNNING, JOB_DONE }; + struct Job { TileProvider* src = nullptr; int16_t z = 0; int32_t x = 0, y = 0; bool present = false, overlay_missed = false; }; + Job _job; + volatile uint8_t _job_state = JOB_IDLE; // RUNNING: the task owns _job and _spare + volatile bool _job_stale = false; // dropped / invalidated meanwhile: thrown away + uint16_t* _spare = nullptr; // the buffer the next tile is rendered into +#if defined(ESP32) + TaskHandle_t _task = nullptr; + static void jobTask(void* arg) { + for (;;) { + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + static_cast(arg)->runJob(); + if (TaskHandle_t t = lvport::s_loop_task) xTaskNotifyGive(t); // the loop places it + } + } +#endif + void runJob() { + if (!_spare) { +#if defined(ESP32) + _spare = (uint16_t*)heap_caps_malloc(TILE_PX * TILE_PX * 2, MALLOC_CAP_SPIRAM); +#else + _spare = (uint16_t*)malloc(TILE_PX * TILE_PX * 2); +#endif + } +#ifdef UI_PERF_TEST + uint32_t t0 = micros(); +#endif + _job.present = _spare && _job.src->renderTile(_job.z, _job.x, _job.y, _spare); +#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; } +#endif + _job.overlay_missed = _job.src->overlayMissed(); +#if defined(ESP32) + __sync_synchronize(); // the results before the state the loop reads +#endif + _job_state = JOB_DONE; + } struct Missing { int16_t z = -1; int32_t x = 0, y = 0; }; static const int MISSING = 48; Slot _slots[SLOTS]; diff --git a/examples/companion_radio/ui-lvgl/map/TileProvider.h b/examples/companion_radio/ui-lvgl/map/TileProvider.h index e255ae72..7d2ed315 100644 --- a/examples/companion_radio/ui-lvgl/map/TileProvider.h +++ b/examples/companion_radio/ui-lvgl/map/TileProvider.h @@ -93,6 +93,7 @@ public: virtual bool available() = 0; // anything to draw at all virtual bool renderTile(int z, int x, int y, uint16_t* out) = 0; // false: no tile there virtual const char* attribution() const = 0; + virtual bool overlayMissed() const { return false; } // the last renderTile() went without its trails overlay }; // Raster tiles under /{z}/{x}/{y}.png -- the Meshtastic MUI layout, so @@ -123,17 +124,30 @@ public: return true; } +#ifdef UI_PERF_TEST + uint32_t perf_read_us = 0, perf_dec_us = 0, perf_bytes = 0; +#endif bool renderTile(int z, int x, int y, uint16_t* out) override { _overlay_missed = false; +#ifdef UI_PERF_TEST + uint32_t t0 = micros(); +#endif uint32_t len = 0; uint8_t* png = readLoose(_root, z, x, y, len); if (!png) png = readPacked(z, x, y, len); if (!png && _live) png = readLoose(_live, z, x, y, len); if (!png) return false; +#ifdef UI_PERF_TEST + uint32_t t1 = micros(); + perf_read_us += t1 - t0; perf_bytes += len; +#endif lv_draw_buf_t* db = decodePng(png, len); lv_free(png); if (!db) return false; +#ifdef UI_PERF_TEST + perf_dec_us += micros() - t1; +#endif for (int row = 0; row < TILE_PX; row++) { const uint8_t* p = db->data + row * db->header.stride; // R, G, B, A @@ -147,7 +161,7 @@ public: } // The last renderTile() found no trails file for its tile (not fetched yet). - bool overlayMissed() const { return _overlay_missed; } + bool overlayMissed() const override { return _overlay_missed; } const char* attribution() const override { const char* base = _attr[0] ? _attr : "\xC2\xA9 OpenStreetMap contributors";