feat(ui-lvgl): VT3 vector tiles, delta varint points

Points as zigzag varint deltas, a byte length per feature to skip it,
natural land cover simplified harder: the Tatra sample goes from 4.4 MB
to 2.3 MB, panning at z14 from ~63 to ~45 ms per tile on the L2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-27 10:41:01 +02:00
co-authored by Claude Opus 5.5
parent 1973361244
commit ba4e13e2b2
2 changed files with 62 additions and 25 deletions
@@ -1,6 +1,6 @@
#pragma once #pragma once
// Vector map (spike): tiles from tools/maps/osm_vector.py under VECTOR_ROOT, // Vector map (spike): tiles from tools/maps/osm_vector.py under VECTOR_ROOT,
// {dz}/{x}/{y}.vt at data zooms 10, 12 and 14, drawn here into the 256x256 RGB565 // {dz}/{x}/{y}.vt (format VT3) at data zooms 10, 12 and 14, drawn here into the 256x256 RGB565
// buffer the map asks for -- polygons by scanline (even-odd), lines as quads // buffer the map asks for -- polygons by scanline (even-odd), lines as quads
// with round joins. No anti-aliasing, no labels yet. Where there is no vector // with round joins. No anti-aliasing, no labels yet. Where there is no vector
// data (or below zoom 10) the raster provider draws instead, so the two mix. // data (or below zoom 10) the raster provider draws instead, so the two mix.
@@ -61,6 +61,8 @@ private:
const uint8_t* _data = nullptr; // the data tile being drawn const uint8_t* _data = nullptr; // the data tile being drawn
size_t _data_len = 0; size_t _data_len = 0;
int _ox = 0, _oy = 0, _k = 0, _z = 0; int _ox = 0, _oy = 0, _k = 0, _z = 0;
const uint8_t* _end = nullptr; // the feature's parts end here
int16_t _bx = 0, _by = 0; // its bbox corner: where point deltas start
uint16_t* _out = nullptr; uint16_t* _out = nullptr;
uint32_t _last_ms = 0; uint32_t _last_ms = 0;
@@ -73,6 +75,18 @@ private:
int32_t _xs[ACTIVE]; int32_t _xs[ACTIVE];
uint32_t _t_load = 0, _t_fill = 0, _t_line = 0; // us, last tile uint32_t _t_load = 0, _t_fill = 0, _t_line = 0; // us, last tile
// Unsigned LEB128 varint / zigzag-signed (the tile format, osm_vector.py).
static inline uint32_t varint(const uint8_t*& p, const uint8_t* end) {
uint32_t v = 0;
for (int s = 0; p < end && s < 35; s += 7) {
uint8_t b = *p++;
v |= (uint32_t)(b & 0x7F) << s;
if (!(b & 0x80)) break;
}
return v;
}
static inline int32_t zigzag(uint32_t v) { return (int32_t)(v >> 1) ^ -(int32_t)(v & 1); }
static uint16_t rgb(uint32_t c) { return (uint16_t)(((c >> 8) & 0xF800) | ((c >> 5) & 0x07E0) | ((c >> 3) & 0x1F)); } static uint16_t rgb(uint32_t c) { return (uint16_t)(((c >> 8) & 0xF800) | ((c >> 5) & 0x07E0) | ((c >> 3) & 0x1F)); }
// The data tiles last read (PSRAM), least recently used replaced: the map // The data tiles last read (PSRAM), least recently used replaced: the map
@@ -109,7 +123,7 @@ private:
} }
d->len = readFast(f, d->buf, sz) ? sz : 0; d->len = readFast(f, d->buf, sz) ? sz : 0;
fclose(f); fclose(f);
d->ok = d->len == (size_t)sz && memcmp(d->buf, "VT2", 3) == 0; d->ok = d->len == (size_t)sz && memcmp(d->buf, "VT3", 3) == 0;
_data = d->buf; _data = d->buf;
_data_len = d->len; _data_len = d->len;
return d->ok; return d->ok;
@@ -165,13 +179,12 @@ private:
memcpy(&col, p + 2, 2); memcpy(&col, p + 2, 2);
memcpy(bb, p + 4, 8); memcpy(bb, p + 4, 8);
p += 12; p += 12;
uint32_t len = varint(p, end);
const uint8_t* parts = p; const uint8_t* parts = p;
for (int i = 0; i < nparts && p + 2 <= end; i++) { // skip to the next feature if (len > (uint32_t)(end - p)) return;
uint16_t n; p += len; // the next feature
memcpy(&n, p, 2); _end = p;
p += 2 + 4 * n; _bx = bb[0]; _by = bb[1];
}
if (p > end) return;
bool road = cls >= L_SERVICE && cls <= L_TRUNK; bool road = cls >= L_SERVICE && cls <= L_TRUNK;
bool want = pass == 0 ? (cls < L_SERVICE) : pass == 3 ? cls == L_ROUTE : road; bool want = pass == 0 ? (cls < L_SERVICE) : pass == 3 ? cls == L_ROUTE : road;
if (!want) continue; if (!want) continue;
@@ -268,19 +281,17 @@ private:
// Points of a part, in 1/16 px, closer than half a pixel to the last one // Points of a part, in 1/16 px, closer than half a pixel to the last one
// dropped (the data is detailed enough for zoom 18). // dropped (the data is detailed enough for zoom 18).
template <typename F> void forPoints(const uint8_t*& p, bool keep_last, F fn) { template <typename F> void forPoints(const uint8_t*& p, bool keep_last, F fn) {
uint16_t n; uint32_t n = varint(p, _end);
memcpy(&n, p, 2);
p += 2;
int32_t lx = INT32_MIN, ly = 0; int32_t lx = INT32_MIN, ly = 0;
for (int j = 0; j < n; j++) { int32_t ux = _bx, uy = _by;
int16_t ux, uy; for (uint32_t j = 0; j < n && p < _end; j++) {
memcpy(&ux, p + 4 * j, 2); memcpy(&uy, p + 4 * j + 2, 2); ux += zigzag(varint(p, _end));
int32_t x = sx(ux), y = sy(uy); uy += zigzag(varint(p, _end));
int32_t x = (ux - _ox) << _k, y = (uy - _oy) << _k;
if (lx != INT32_MIN && abs(x - lx) + abs(y - ly) < 8 && !(keep_last && j == n - 1)) continue; if (lx != INT32_MIN && abs(x - lx) + abs(y - ly) < 8 && !(keep_last && j == n - 1)) continue;
fn(x, y, lx == INT32_MIN); fn(x, y, lx == INT32_MIN);
lx = x; ly = y; lx = x; ly = y;
} }
p += 4 * n;
} }
void fillFeature(const uint8_t* p, int nparts, uint16_t col) { void fillFeature(const uint8_t* p, int nparts, uint16_t col) {
+35 -9
View File
@@ -12,12 +12,14 @@ Three data zooms: 10 (drawn at z10-11), 12 (z12-13), 14 (z14-18); the device pic
data tile covering the tile it draws and scales it. Copy the output folder to data tile covering the tile it draws and scales it. Copy the output folder to
the card as /sdcard/vmap. the card as /sdcard/vmap.
Tile format 'VT2' (little-endian): Tile format 'VT3' (little-endian):
'V' 'T' '2' dz:u8 count:u16 'V' 'T' '3' dz:u8 count:u16
count x feature, in drawing order: count x feature, in drawing order:
cls:u8 nparts:u8 colour:u16 (RGB565, 0 = the class's own) cls:u8 nparts:u8 colour:u16 (RGB565, 0 = the class's own)
bbox: x0 y0 x1 y1 (i16) -- the device skips what's off the tile unread bbox: x0 y0 x1 y1 (i16) -- the device skips what's off the tile unread
nparts x (npts:u16, npts x (x:i16, y:i16)) len: varint -- bytes of the parts that follow (to skip them)
nparts x (npts: varint, npts x (dx, dy: zigzag varint))
Points are deltas from the previous one; a part's first from (x0, y0).
Coordinates: 0..4096 across the data tile, a little past its edges. Coordinates: 0..4096 across the data tile, a little past its edges.
Polygons: rings, even-odd. Lines: polylines. Polygons: rings, even-odd. Lines: polylines.
@@ -59,12 +61,30 @@ HIGHWAY = {
# Lowest data zoom a class goes in (smaller ones would be clutter / weight). # Lowest data zoom a class goes in (smaller ones would be clutter / weight).
MIN_DZ = {P_BUILDING: 14, L_SERVICE: 14, L_PATH: 12, L_TRACK: 12, L_STREAM: 12, L_MINOR: 12} MIN_DZ = {P_BUILDING: 14, L_SERVICE: 14, L_PATH: 12, L_TRACK: 12, L_STREAM: 12, L_MINOR: 12}
# Natural land cover (not roads, buildings, water): its edges are vague
# anyway, so simplified harder -- most of the points are here.
NATURAL = (P_MEADOW, P_SCRUB, P_FOREST, P_ROCK)
NATURAL_TOL = 3
WAYMARK = { # osmc:symbol / colour -> RGB888 WAYMARK = { # osmc:symbol / colour -> RGB888
'red': 0xE0302A, 'blue': 0x2A5FE0, 'green': 0x2EA043, 'yellow': 0xE8C20E, 'black': 0x202020, 'red': 0xE0302A, 'blue': 0x2A5FE0, 'green': 0x2EA043, 'yellow': 0xE8C20E, 'black': 0x202020,
'orange': 0xF08A1C, 'purple': 0x9040C0, 'white': 0xF0F0F0, 'brown': 0x8B5A2B, 'orange': 0xF08A1C, 'purple': 0x9040C0, 'white': 0xF0F0F0, 'brown': 0x8B5A2B,
} }
def varint(v):
out = bytearray()
while v >= 0x80:
out.append((v & 0x7F) | 0x80)
v >>= 7
out.append(v)
return out
def zigzag(v):
return (v << 1) if v >= 0 else ((-v << 1) - 1)
def rgb565(c): def rgb565(c):
return ((c >> 8) & 0xF800) | ((c >> 5) & 0x07E0) | ((c >> 3) & 0x001F) return ((c >> 8) & 0xF800) | ((c >> 5) & 0x07E0) | ((c >> 3) & 0x001F)
@@ -288,11 +308,12 @@ def main():
total_bytes = total_tiles = 0 total_bytes = total_tiles = 0
for dz in DATA_ZOOMS: for dz in DATA_ZOOMS:
n = 1 << dz n = 1 << dz
tol = 1.0 if dz == DATA_ZOOMS[-1] else 4.0 # half a pixel at the deepest zoom drawn from it base_tol = 1.0 if dz == DATA_ZOOMS[-1] else 4.0 # half a pixel at the deepest zoom drawn from it
tiles = defaultdict(list) tiles = defaultdict(list)
for cls, col, kind, parts in feats: for cls, col, kind, parts in feats:
if MIN_DZ.get(cls, 0) > dz: if MIN_DZ.get(cls, 0) > dz:
continue continue
tol = base_tol * (NATURAL_TOL if cls in NATURAL else 1)
allp = [p for part in parts for p in part] allp = [p for part in parts for p in part]
if not allp: if not allp:
continue continue
@@ -318,7 +339,7 @@ def main():
tiles[(tx, ty)].append((cls, col, out)) tiles[(tx, ty)].append((cls, col, out))
for (tx, ty), fl in tiles.items(): for (tx, ty), fl in tiles.items():
fl.sort(key=lambda f: f[0]) fl.sort(key=lambda f: f[0])
buf = bytearray(b'VT2' + bytes([dz]) + struct.pack('<H', 0)) buf = bytearray(b'VT3' + bytes([dz]) + struct.pack('<H', 0))
count = 0 count = 0
for cls, col, parts in fl: for cls, col, parts in fl:
for i in range(0, len(parts), 255): # nparts is a byte for i in range(0, len(parts), 255): # nparts is a byte
@@ -332,12 +353,17 @@ def main():
q.append(p) q.append(p)
qs.append(q) qs.append(q)
allq = [p for q in qs for p in q] allq = [p for q in qs for p in q]
buf += struct.pack('<BBHhhhh', cls, len(chunk), col, min(p[0] for p in allq), min(p[1] for p in allq), bx0, by0 = min(p[0] for p in allq), min(p[1] for p in allq)
max(p[0] for p in allq), max(p[1] for p in allq)) body = bytearray()
for q in qs: for q in qs:
buf += struct.pack('<H', len(q)) body += varint(len(q))
px, py = bx0, by0
for x, y in q: for x, y in q:
buf += struct.pack('<hh', x, y) body += varint(zigzag(x - px)) + varint(zigzag(y - py))
px, py = x, y
buf += struct.pack('<BBHhhhh', cls, len(chunk), col, bx0, by0,
max(p[0] for p in allq), max(p[1] for p in allq))
buf += varint(len(body)) + body
count += 1 count += 1
struct.pack_into('<H', buf, 4, min(count, 65535)) struct.pack_into('<H', buf, 4, min(count, 65535))
d = os.path.join(a.out, str(dz), str(tx)) d = os.path.join(a.out, str(dz), str(tx))