mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 10:21:59 +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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -82,6 +82,7 @@ template <class T> 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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <esp_heap_caps.h>
|
||||
|
||||
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
|
||||
@@ -1,7 +0,0 @@
|
||||
#pragma once
|
||||
// LVGL's builtin allocator pool, placed in PSRAM (see lv_conf.h).
|
||||
#include <esp_heap_caps.h>
|
||||
|
||||
static inline void* lv_psram_pool_alloc(size_t sz) {
|
||||
return heap_caps_malloc(sz, MALLOC_CAP_SPIRAM);
|
||||
}
|
||||
@@ -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