mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-02 10:51:57 +00:00
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:
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user