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 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-30 00:24:57 +02:00
co-authored by Claude Opus 5.5
parent b3b77a1f6d
commit 247fab08d0
9 changed files with 230 additions and 60 deletions
+100 -27
View File
@@ -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<TileCache*>(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];
@@ -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 <root>/{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";