perf(l2): map tiles decode with PNGdec

PNGdec (bitbank2, 1.1.6) decodes a tile a row at a time straight into
RGB565, without lodepng's 256 KB RGBA buffer; the trail overlay goes through
it too, blended by hand for palette-with-alpha and RGBA. It lives in its own
file (ui-lvgl/TilePng.cpp) because its zlib headers put DONE/EXTRA in the
global namespace. lodepng stays the fallback and the sim's decoder.

Same 20 tiles: decode 78 -> 50 ms, trails 42 -> 17.5 ms, a whole tile
170 -> 105 ms; the map with trails is pixel-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-30 08:53:15 +02:00
co-authored by Claude Opus 5.5
parent 039485bf34
commit f29c280c34
5 changed files with 116 additions and 15 deletions
+4 -3
View File
@@ -293,9 +293,10 @@ void UITask::perfMap(bool reset) {
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;
if (s_perf_tile_n) Serial.printf("PERF of which read %5.1f ms (%lu B), decode %5.1f ms, trails %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_ovl_us / 1000.0f / s_perf_tile_n);
s_raster.perf_read_us = s_raster.perf_dec_us = s_raster.perf_bytes = s_raster.perf_ovl_us = 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;
@@ -0,0 +1,78 @@
// PNGdec for the map tiles (map/TileProvider.h): a row at a time straight
// into the RGB565 tile -- no 256 KB RGBA picture in between as with lodepng,
// and about a third faster. In a file of its own because PNGdec's zlib
// headers put names like DONE and EXTRA in the global namespace, which the
// single-TU UI would trip over. Its state (~37 KB, mostly the inflate window)
// sits in PSRAM: one copy, for the tile task only (TileCache).
#if defined(ESP32)
#include <PNGdec.h>
#include <esp_heap_caps.h>
#include <new>
static PNG* s_png = nullptr;
static bool openPng(const uint8_t* png, size_t len, int side, PNG_DRAW_CALLBACK* row) {
if (!s_png) {
void* m = heap_caps_malloc(sizeof(PNG), MALLOC_CAP_SPIRAM);
if (!m) return false;
s_png = new (m) PNG();
}
if (s_png->openRAM((uint8_t*)png, (int)len, row) != PNG_SUCCESS) return false;
if (s_png->getWidth() == side && s_png->getHeight() == side) return true;
s_png->close();
return false;
}
static int onRow(PNGDRAW* d) {
s_png->getLineAsRGB565(d, (uint16_t*)d->pUser + d->y * d->iWidth, PNG_RGB565_LITTLE_ENDIAN, 0xffffffff);
return 1;
}
// `png` (len bytes) into out[side * side]; false if it isn't a side x side
// picture PNGdec can read (the caller then tries lodepng).
bool pngdecRgb565(const uint8_t* png, size_t len, uint16_t* out, int side) {
if (!openPng(png, len, side, onRow)) return false;
bool ok = s_png->decode(out, 0) == PNG_SUCCESS;
s_png->close();
return ok;
}
// One pixel of an overlay over the tile, by its alpha.
static inline void blend(uint16_t& o, unsigned r, unsigned g, unsigned b, unsigned a) {
unsigned r0 = (o >> 8) & 0xF8, g0 = (o >> 3) & 0xFC, b0 = (o << 3) & 0xF8;
r = (r * a + r0 * (255 - a)) / 255;
g = (g * a + g0 * (255 - a)) / 255;
b = (b * a + b0 * (255 - a)) / 255;
o = (uint16_t)(((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3));
}
static int onOverlayRow(PNGDRAW* d) {
uint16_t* o = (uint16_t*)d->pUser + d->y * d->iWidth;
const uint8_t* s = d->pPixels;
if (d->iPixelType == PNG_PIXEL_TRUECOLOR_ALPHA) {
for (int x = 0; x < d->iWidth; x++, s += 4)
if (s[3]) blend(o[x], s[0], s[1], s[2], s[3]);
return 1;
}
const uint8_t* pal = d->pPalette; // indexed: RGB triplets, then the alpha of each entry at 768
const int bpp = d->iBpp;
for (int x = 0; x < d->iWidth; x++) {
unsigned i = bpp == 8 ? s[x] : (s[(x * bpp) >> 3] >> (8 - bpp - ((x * bpp) & 7))) & ((1u << bpp) - 1);
if (unsigned a = pal[768 + i]) blend(o[x], pal[i * 3], pal[i * 3 + 1], pal[i * 3 + 2], a);
}
return 1;
}
// A transparent overlay (`png`) drawn over out[side * side]. Indexed or 8-bit
// RGBA pictures only (hiking trails come as either); false otherwise, with
// `out` untouched.
bool pngdecOverlayRgb565(const uint8_t* png, size_t len, uint16_t* out, int side) {
if (!openPng(png, len, side, onOverlayRow)) return false;
int t = s_png->getPixelType();
bool ok = (t == PNG_PIXEL_INDEXED || (t == PNG_PIXEL_TRUECOLOR_ALPHA && s_png->getBpp() == 8))
&& s_png->decode(out, 0) == PNG_SUCCESS;
s_png->close();
return ok;
}
#endif
+2 -1
View File
@@ -46,7 +46,8 @@
#define LV_TXT_BREAK_CHARS " ,;:-_)]}"
#define LV_USE_FONT_COMPRESSED 0 // fonts are generated uncompressed: no per-glyph decompression on every draw
// PNG decoder for raster map tiles (map/TileProvider.h calls lodepng directly).
// PNG decoder for raster map tiles: the sim's, and the fallback on the L2 behind
// PNGdec (ui-lvgl/TilePng.cpp). map/TileProvider.h calls lodepng directly.
#define LV_USE_LODEPNG 1
// Spans: message text with @[nick] mentions picked out (UITask.cpp msgText).
@@ -16,6 +16,11 @@ extern "C" {
#include "src/libs/lodepng/lodepng.h"
}
#if defined(ESP32)
bool pngdecRgb565(const uint8_t* png, size_t len, uint16_t* out, int side); // ui-lvgl/TilePng.cpp
bool pngdecOverlayRgb565(const uint8_t* png, size_t len, uint16_t* out, int side);
#endif
namespace mapview {
// OpenTopoMap answers every tile past its last zoom level (17) with the same
@@ -125,7 +130,7 @@ public:
}
#ifdef UI_PERF_TEST
uint32_t perf_read_us = 0, perf_dec_us = 0, perf_bytes = 0;
uint32_t perf_read_us = 0, perf_dec_us = 0, perf_bytes = 0, perf_ovl_us = 0;
#endif
bool renderTile(int z, int x, int y, uint16_t* out) override {
_overlay_missed = false;
@@ -142,21 +147,32 @@ public:
perf_read_us += t1 - t0; perf_bytes += len;
#endif
lv_draw_buf_t* db = decodePng(png, len);
bool ok = false;
#if defined(ESP32)
ok = pngdecRgb565(png, len, out, TILE_PX); // lodepng below if it can't
#endif
if (!ok) {
lv_draw_buf_t* db = decodePng(png, len);
if (!db) { lv_free(png); return false; }
for (int row = 0; row < TILE_PX; row++) {
const uint8_t* p = db->data + row * db->header.stride; // R, G, B, A
uint16_t* o = out + row * TILE_PX;
for (int i = 0; i < TILE_PX; i++, p += 4)
o[i] = (uint16_t)(((p[0] & 0xF8) << 8) | ((p[1] & 0xFC) << 3) | (p[2] >> 3));
}
lv_draw_buf_destroy(db);
}
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
uint16_t* o = out + row * TILE_PX;
for (int i = 0; i < TILE_PX; i++, p += 4)
o[i] = (uint16_t)(((p[0] & 0xF8) << 8) | ((p[1] & 0xFC) << 3) | (p[2] >> 3));
}
lv_draw_buf_destroy(db);
#ifdef UI_PERF_TEST
uint32_t t2 = micros();
#endif
if (s_trails_on && z <= TRAILS_MAX_Z) drawOverlay(z, x, y, out);
#ifdef UI_PERF_TEST
perf_ovl_us += micros() - t2;
#endif
return true;
}
@@ -188,6 +204,9 @@ private:
uint8_t* png = sz > 0 ? readRange(f, 0, (uint32_t)sz) : nullptr; // 0 bytes: no trail here
fclose(f);
if (!png) return;
#if defined(ESP32)
if (pngdecOverlayRgb565(png, (size_t)sz, out, TILE_PX)) { lv_free(png); return; } // else lodepng
#endif
lv_draw_buf_t* db = decodePng(png, (size_t)sz);
lv_free(png);
if (!db) return;
+2
View File
@@ -92,6 +92,7 @@ lib_deps =
${Wio_Tracker_L2.lib_deps}
densaugeo/base64 @ ~1.4.0
lvgl/lvgl @ 9.6.0 ; pinned: bump deliberately, re-test on hardware
bitbank2/PNGdec @ 1.1.6 ; map tiles: PNG straight to RGB565, a row at a time (ui-lvgl/TilePng.cpp)
SD_MMC ; map tiles on the microSD (framework libraries from here on)
FS
WiFi ; on-device map download, live tiles, OTA
@@ -111,6 +112,7 @@ lib_deps =
${Wio_Tracker_L2.lib_deps}
densaugeo/base64 @ ~1.4.0
lvgl/lvgl @ 9.6.0
bitbank2/PNGdec @ 1.1.6
SD_MMC
FS
WiFi