The shortbread place_labels layer was never admitted, so zooming out
lost ALL labels (streets are the only other source and they fade with
length). Now: place points classify by kind and follow osm-carto's
placenames.mss gates (city z4-15.5 at 1.55x text, town z7+, village/
suburb z11.5+, quarter/hamlet/neighbourhood z13.5+), outranking every
other label source. Nature-reserve area labels also escape the z15
area-label gate — their tiles exist from z6 and the reserves are huge.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- natura2000/wetlands areas label their Dutch source names (naam_n2k /
naam) in green at their centroids
- Building age: BAG bouwjaar choropleth over the building fills (rust =
pre-1800 through blue = 2010s, gray = unknown), z13-14
- Population: CBS vierkant cells (vk500/vk100) yellow-to-deep-blue by
aantal_inwoners at 45% opacity; empty cells stay transparent
- two more checkboxes in the Layers panel
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A visible:false view that has never drawn has an empty area, so its own
redraw was a no-op — set_visible(true) only took effect on the next
unrelated full repaint, the classic "appears after hot reload" bug (hit
by the map app's layers panel; same class as the elevation graph fix).
Also: overlay_chargers_probe ignored-test builds a real Amsterdam tile
with the chargers overlay (450 icons verified).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 1 of the layers.md track:
- MapView grows overlay_mbtiles_paths (semicolon list) + set_overlay_paths
(stale-keeping rebuild). The loader opens each overlay mbtiles, clamps
to its metadata zoom range, and fetches ancestor tiles with a
quadrant transform when the overlay's maxzoom is below the requested
tile (fills/strokes ride the existing clippers; points bounds-filter)
- overlay MVT layers merge into the normal tile build, styled by layer
name: chargers = blue bolt icons from z12, transit stops dots z13 +
route shapes above the road net, natura2000/wetlands translucent
green tint (fill alpha now flows through fill groups) + outline,
gemeente/wijk/buurt purple dashed boundaries by admin level
- examples/map: Layers button + checkbox panel (EV chargers, Transit,
Nature areas, Districts; raster + rain marked as next phase)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Projected grid values are <= 2^26 at zoom 14, so i32 is exact; decode
hard-errors on out-of-range rather than ever truncating (no swimming
roads). NH output identical feature counts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Europe's ways arrive in creation order, i.e. spatially random: any node
cache that fits in a fraction of the store loses. The store is only
13.8GB compressed, so FlatNodeStore decodes ALL of it once (parallel,
~a minute for Europe) and resolver workers share it lock-free via Arc —
no cache, no eviction, no zlib on the hot path. NH pass 3: 5.6s -> 3.5s;
feature counts unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- NodeStore eviction was a min_by_key scan of the whole cache per miss;
with the Europe-sized cache that scan became the bottleneck (1.2k
ways/s). FIFO queue eviction is O(1) amortized
- way resolution now fans out over a resolver worker pool (each with
its own NodeStore handle and cache slice); geometry is localized on
the workers via prepare_lines/prepare_polygons and a single writer
thread owns the spool, so the on-disk format is untouched. The
id-ordered relation-way store stays on the ordered consumer thread
- Noord-Holland: pass 3 32.6s -> 5.6s, full conversion 25.7s; per-layer
feature counts identical (byte diff is record interleave order only)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bridge_area_fill is the gray bridge-structure color — the deck between
the rims needs the pedestrian street surface (white light / dark slab
dark). Track dots drop carto brown for the light-gray path family.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- the renderer paints all casings before all centers; the bridge rim
was a center-pass line and covered the deck (dark-filled bridges).
Rim is now the casing, the light deck the center, and the faint dots
ride a companion style one sort-rank above
- outline colors (bridge rim, platform edge, pedestrian-area edge) now
derive from theme fills via contrast_edge() — dark fills get a
lighter rim, light fills a darker one — so dark mode flips them
automatically instead of baking light-theme grays
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two fixes to the detail converter that spent 10+ hours on Europe:
- visit_pbf: ordered parallel decode — blob reads stay serial, the
zlib+protobuf decode fans out over a worker pool, and the callback
runs in exact file order on the calling thread, so id-ordered store
builders need no changes
- NodeStore group cache: 256 groups (128MB) thrashed on Europe-scale
way resolution (~115us/way of cache-miss zlib re-decode, ~8.6k
ways/s); now MAKEPAD_NODE_CACHE_MIB (default 8GB), way resolution
~370k/s
Noord-Holland: 61s end-to-end, output byte-identical to the slow
converter (7547 tiles, same bytes).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dotted centerline continues across the span (it does on osm.org,
just faint); the rim is a companion stroke job keyed off the deck color
so the two-pass StrokeStyle stays untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Matches osm.org's little outlined bridge box: dark gray casing, near-
white solid deck, no dotted centerline across the water.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- road-polygon fills (pedestrian squares, wide walking-path areas) get
carto's thin gray constant-px edge from z15
- footbridge deck narrowed (2.4x the dot width, was 3.4x) — the rope
bridge at Artis read twice as fat as osm.org
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- the animal enclosure WAYS turned out absent from the detail archive
(converter mystery, ids fine, no filter — parked), but every animal
also exists as an attraction=animal NODE: those now flow through the
micro_pois point path as label-only features (culture brown, no icon).
Verified live at Artis: Zwarte slingeraap, Roze pelikaan, Rode vari,
Goudwanggibbon, Aziatische olifant all place like osm.org
- walking paths are light gray dotted everywhere (user preference over
carto salmon); restricted-access stays gray too
- footway bridges draw a small white deck under the dots — the little
outline box carto shows at water crossings
- diagnostic probes for the Artis tiles kept as ignored tests
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- root cause found by driving a real Artis tile through the full build
in a test: admission, extraction and compaction were all fine (42/42
enclosures labeled in the tile buffers) — the labels died at placement
scoring. Point-anchored area labels have a ~zero-length path, and the
score formula's path-length term let every street name outscore them
in dense viewports. They now get a 420px length credit and green_area
ranks 4 (above pois, below streets)
- pedestrian streets/paths render carto-style: near-white core with a
gray casing (both palettes) instead of falling through to defaults
- trees 2x: canopy disc 5.4 with a 1.7 dark core, matching osm.org
proportions
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- CRITICAL: OSM's own layer=-1/1 stacking tag collided with our "layer"
source-layer key (or_insert kept the OSM value) — any layer-tagged
feature was unrecognizable downstream: the Artis tourism=zoo way
(layer=-1) never got its boundary, and detail admission missed such
features entirely. Source-layer name now owns the key; OSM's value
moves to "osm_layer"
- barrier lines from the detail archive: walls/retaining walls dark
thin, fences lighter, hedges green (the Artis perimeter wall)
- trees are now carto's light canopy disc with a dark center dot
- attraction rings force-close (tile clipping opened them — Aziatische
olifant label was dropped)
- restricted-access paths (private/no/customers) gray out instead of
public salmon inside zoos and private grounds
- micro street furniture (trees, benches, bins, dots) collides at ~45%
radius so a tree row no longer knocks out its benches
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- pattern coords now come from the vertex stage in tile-local space
scaled to view px (v_world was post-camera SCREEN space — the pattern
sat still while the map panned under it)
- courtyard gardens: smaller dots on an odd/even staggered grid;
playgrounds keep the hatch; woods/forests/cemeteries get staggered
OPEN circles (tree rings) instead of sharing the garden dots
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
carto textures its fills; ours were flat. Fill groups now carry a
pattern shape (30 = dot stipple for leisure=garden and landuse=cemetery,
31 = diagonal hatch for playgrounds) and the map shader darkens the
fill color procedurally in screen-stable world space — no textures,
pans/rotates with the map.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- named zoo enclosures/attractions (attraction=animal etc.) from the
detail archive label at their centroid in carto's culture brown; area
labels no longer require a fill color, so a named enclosure on plain
grass still gets its name
- natural=sand/beach/shingle fill carto's pale tan (theme rule both
palettes); sand-floored enclosures rank above the zoo's grass
- tourism=zoo/theme_park perimeter draws a muted purple constant-px
boundary line
- landuse=cemetery gets its proper green (was generic gray — Joodse
begraafplaats Zeeburg)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Chaikin corner-cutting now requires a GENTLE turn (< ~30 degrees) in
addition to short adjacent segments: densely sampled canal-belt quay
roads still carry sharp corners at bridge junctions between short
segments, and cutting those curved the road through the corner
buildings and off the bridges
- the "black dashed fragments" at every canal crossing were the dark
bridge casing applied to thin footway/steps bridges (a sub-px 0x4a4a4a
edge under 0.9px salmon dots); carto only cases real roads — thin
uncased paths keep their plain dotted style on bridges
- tile.rs: keep an ignored bridge_probe diagnostic test (dumps line
features near a point in a real tile with tags)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- overzoom smoothing regression: uniform Chaikin also cut real 90-degree
street corners, rounding roads through building blocks and detaching
them from their bridges. Now only vertices with BOTH adjacent segments
under ~10 screen px get cut (dense tile-quantized curves); sparse
vertices are genuine corners and stay sharp
- pedestrian squares: area=yes semantically means polygon — close the
ring unconditionally (tile clipping can leave LineString rings open)
- examples/map: headless UI smoke test (makepad_test) + local data
symlink so the in-process hub run finds local/maps
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Hella Haasseplein root cause: pedestrian squares in osm_lines arrive
as LineStrings, so `closed` was never set and the area admission
skipped them — detect implicit closure (first==last) and mark the
ring closed for the fill pass
- icon collision: doors and generic dots now yield to real symbols
(the recycling icon was losing to the building entrance beside it);
entrances gate at z18 like carto
- tools: tagprobe <pbf> <name> — dump every OSM element matching a
name, for answering "how is this actually tagged" without overpass
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- polygon POIs (parking lots, garages, shops/offices mapped on their
building) now icon at the ring centroid like carto — this is what
makes the parking P's appear; underground garages skip the centroid
(carto shows their entrance instead: amenity=parking_entrance now
draws the P) and named parkings get a blue name label
- pedestrian squares (Hella Haasseplein): admitted from the detail
archive into the street-area fill pipeline — including closed
highway=pedestrian area=yes ways that live in osm_lines, since
highway ways don't classify as polygons at conversion — and named
squares/greens both label at their centroid
- vertical street labels read TOP-TO-BOTTOM (osm.org convention, user
preference), never bottom-to-top; upside-down protection stays via
the text-span chord
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- platform polygons (railway/public_transport=platform from the detail
archive) as gray slabs with a constant-px edge, 2D and 3D, z15.5+
- new icons: traffic signals, parking P, EV charger, plus a generic
colored dot for named POIs with no dedicated symbol (offices like
TomTom get dot + name label from the detail layer); per-icon zoom
gates (entrances z17.5, signals/chargers/dots z16.5, parking z15.5)
- rail: sidings/yards (service=*) fade lighter and thinner like carto's
dead-end tracks; railway landuse gets carto's pale purple tint
- small green patches (verges, lawns, scrub) restored at street zoom
from the detail archive — z14 base generalization drops them
- overzoom Chaikin smoothing on stroke geometry: ovals and tram loops
stop reading as polygons at 8-16x magnification; roundabout/circular
junctions get oneway arrows without an explicit oneway tag
- labels: reading direction decided by the chord across the whole text
span (mid-point tangent flipped labels 180 on zigzag segments), and
small rotation deltas reuse the cached placement rigidly rotated
about the pivot — no more per-frame wiggle during heading-up nav
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- entrance door icons (entrance=*, railway=subway_entrance) from the
all-tag detail layer at door-level zoom (z17.5+); tourism=information
(i) icon in both the shortbread poi and detail micro-poi paths
- rail sleeper retune vs OSM reference: thinner band (casing 2.4, core
1.2) with longer airy stripes dash(8,8); sleeper look starts z15 so
zoomed-out station corridors stay a calm thin line; trams only darken
toward near-black at z16+
- elevation: median spike removal + ~220m smoothing + 2.5m climb
hysteresis — SRTM rooftop noise no longer fakes 50m of ascent across
pancake-flat Amsterdam (test asserts single-digit climb)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
score_search_hit: settlements are near-immune to distance ("brussels"
from Amsterdam means Bruxelles, not the closest Brusselsestraat),
POIs/streets keep strong local bias, and a number token signals
address intent. nav-build --places-only scans a pbf for settlement
nodes into a compact index (Europe: 1.31M places, 67MB, 60s); the app
merges it with the regional full index, deduping same-name near hits.
Route overlay decimation now measures against the last drawn point,
bounding the error at ~1.5px — pairwise skipping compounded and
visibly reshaped the route when zoomed out.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Search results render as two-line cards (name, then category · address
· distance) instead of one cramped line. Picking a result clears the
input — the next query no longer appends to the old text, which broke
every follow-up search. The nav worker restarts on demand if a script
hot-reload wiped the channel state.
Long-press never synthesizes from a held mouse button on desktop, so
double-click now triggers the same action (set position, then route
here); hints updated accordingly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Flat mode returns to the classic building style (base fills +
outlines) instead of a top-down view of the extruded roofs: crossing
between flat and tilted bumps the style epoch and sentinels every
resident tile's bucket, so the normal stale-bucket restyle path
rebuilds them while the previous geometry stays visible and
cross-fades.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tilted rendering now resolves occlusion in the depth buffer: map
geometry lives in a negative depth domain (UI/labels/overlay drawn
later always win by call order), view-ground y dominates so walls and
roofs occlude anything behind them under any rotation, and a baked
sort-rank micro-depth in param5 (4 depth quanta per rank step) orders
overlapping layers at equal ground without quantization flicker.
Bridges bias their micro-depth like their sort rank, the dark bridge
edge sits one step under the road fill (exact ties noise-win — black
bridges), and nested greens get distinct sub-ranks (park < garden <
grass < pitch) so parks stop shimmering. Flat mode stays on the
classic call-order path, byte-identical.
Camera ergonomics: right-mouse-drag (or Option-drag) rotates the
camera horizontally and tilts vertically, emitting ViewportChanged on
release.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Axonometric tilt as camera uniforms: screen y compresses about the
view center and building vertices carry their height in meters in
param4, lifted toward screen-top by height * px_per_m * sin(tilt) —
tilt animates freely with zero tile rebuilds. All CPU projections
(gestures, zoom anchor, screen<->lonlat, tile-selection AABB, label
placement + cache, overlay) undo the tilt alongside the rotation.
In 3D mode building footprints come from the all-tag detail archive
(real height / building:levels tags, 8m default) replacing the base
building fills: per-edge flat-shaded wall quads (NW light) painted
north-first as the painter's occlusion approximation, then the lifted
roof. The example app gets a 3D toggle with an eased tilt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The detail archive's micro-POIs never reached the collector: the
decode-time point filter only admitted pois/addresses/road-label
layers. Bellamyplein now renders its trees, benches, statues, bins,
recycling and bicycle parking like the osm.org reference.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CAMetalLayer with display sync throttles nextDrawable when the
compositor consumes frames unevenly (hardware-mirrored / scaled
displays): the main thread blocks 10-25ms in phases, felt as hiccups
in any sustained-animation app. Track in-flight presents per window
via addPresentedHandler; when two of the three pool drawables have
not reached glass, skip the paint beat and keep the pass dirty — the
next timer beat retries with the pool drained and event handling
never stalls behind vsync.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Heading-up camera: MapView rotation (bearing-up degrees) as uniforms in
the map shader — geometry and map-aligned glyphs (oneway arrows) rotate
about the view center, POI symbols stay upright billboards. Rotation-
aware tile selection AABB, pan/zoom-anchor/screen<->lonlat math, overlay
projection and puck heading, label placement (street labels follow the
rotated baseline, point labels stay horizontal) and the label pan-shift
cache. The nav app eases the camera onto the travel bearing with a
shortest-arc exponential and resets to north when navigation ends.
Micro-POIs: an optional all-tag detail archive (detail_mbtiles_path)
composes over the shortbread base at icon zooms — trees (canopy discs),
benches, waste baskets, recycling, bicycle parking, playgrounds and
artwork/memorial statues, matching osm.org's park furniture. Icons
only; base labels are never duplicated.
Simulated drive now ticks on NextFrame instead of a 20 Hz timer, so
the follow camera moves once per rendered frame.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stroke geometry now bakes the centerline anchor per vertex (svg
tessellator emits anchors; offsets ride in param1/2, width-growth
class in param3, shape_id+100 marks expand mode — the shared vertex
format is unchanged). The map vertex shader re-expands each stroke
with a per-class width correction (regular roads, thin paths/rails,
waterways, constant-px building outlines) computed from the tile's
styled bucket vs the live fractional view zoom.
Tiles rendered at a stale zoom bucket keep the exact widths a fresh
restyle would produce, through the whole gesture: no fat roads while
zooming in, no width snap when the rebuild lands, dash dots and
building outlines stay crisp. Fading outgoing generations carry their
own bucket so they correct too.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per-rule min_zoom on road/waterway rules: footway/path/steps hidden
below z15, cycleway/track below z14, streams/ditches below z13 and
canal lines below z12 — carto hides these long before roads; at city
scale they read as dot confetti over every block.
Waterway centerlines shrink faster than roads below z14
(zoom_mult^1.6): linear scaling kept Amsterdam's canal grid at near
full width at z12 and the whole city read as water; the water
polygons carry the visual weight at those zooms.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Conversion tooling (tools/map_tiles): curated Shortbread base pyramid
from a VersaTiles planet archive (bbox extract, brotli->gzip transcode
parallelized per 256x256 block), all-tag native OSM pbf detail
converter, pbf audit, and nav-build/nav-probe producing the routing
graph + search index artifacts. download_map.sh orchestrates pinned
downloads and conversions. mbtile_reader writer now skips SQLite's
lock-byte page at byte offset 1 GiB — allocating through it corrupted
every database over 1 GiB ("2nd reference to page 16385"); the 31 GB
Europe conversion passes integrity checks.
Navigation (libs/map_nav, new): region.search place/POI/street/address
index (prefix autocomplete, NL/EN category synonyms, proximity
ranking) and region.graph routing graph (CSR directed edges,
car/bike/foot speeds, oneway, turn restrictions, snap grid, A*),
maneuver generation and the NavSession map-matching state machine;
26 unit tests.
MapView interaction layer: MapViewAction, camera API + animated
fly_to, overlay.rs (route polyline with traveled dimming, markers,
position puck), per-widget mbtiles_path override, archive
minzoom/maxzoom honored for tile requests, zoom-level cross-fade over
the previous level's imagery, floating panels win hit-testing.
examples/map is the navigator app: worker-thread search, Drive/Bike/
Walk routing, simulated turn-by-turn with banner, follow camera.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- bridge roads get a dark casing edge (carto bridge style); global
casing pass keeps it under all road fills
- named parks/green areas emit centroid labels in carto green (z15+),
new LABEL_CLASS_GREEN; health-class label color wired in the view
- icon-vs-icon collision at bake time: first symbol wins within an
icon-sized neighborhood, so dense shopping streets show distinct
symbols instead of a stacked carpet
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Zoom-constant arrow glyphs (shaft+head) spaced every ~130 screen px
along oneway highways, offsets pre-rotated into the travel direction
(oneway_reverse honored), emitted into the symbol buffer so they draw
above road centers. Same anchor+screen-offset encoding as POI symbols.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Local tiles decode the MVT protobuf straight into tile-local f32
features (MvtSink trait + MvtLocalCollector with scale-aware thinning);
the MVT -> generated-Overpass-JSON -> parse detour is gone (a ~1-3MB
binary tile inflated to ~10MB of JSON re-parsed on every restyle, most
of the ~1s per-tile rebuild). The shared feature builder now takes
tile-local ways/points; the Overpass network path projects into the
same structures. MvtTileJsonBuilder deleted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Direct (zoom, column, row) tile queries so the map no longer scans an
entire zoom level per batch, plus a writer for producing mbtiles
archives.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New tile geometry fades in over 250ms via a tile_fade uniform
(multiplies the premultiplied output); the replaced generation's
geometry stays drawable underneath until the fade completes, so bucket
restyles and fresh tiles blend instead of flickering in. A 16ms timer
keeps frames coming while any fade is active (the paint beat idles
without input).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cached label placements were redrawn shifted by the raw map-offset
delta; during a zoom the offsets change by world-scale amounts, flinging
the cache thousands of px off-screen (labels vanished just off max zoom
and stayed gone while idle because the re-place rate limit counted
frames). Now: cached glyphs draw through the exact affine screen
transform s*k + (off_new - off_old*k) with glyph size scaled, so labels
track the map mid-gesture; the re-place limit is wall-time based; and
the settle timer re-arms until a budget-truncated label pass completes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The settle window counted FRAMES, but the paint beat idles when input
stops: after a zoom gesture no frames occur, the frame delta never
reaches the threshold, and stale-bucket restyles only fired when the
user wiggled the map. Settle checks now use wall time and the zoom
gesture schedules a 150ms timeout so a wake is guaranteed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- in-flight tile requests time out after 600 frames and clear stuck
Loading placeholders; a leaked key would otherwise choke the 12-slot
dispatch cap (zoom-only restyles appeared to never regenerate)
- label placement budget only applies during gestures; at rest a full
pass runs, otherwise the lowest-priority tail (house numbers) was
permanently truncated at the same point every re-place
- per-tile label cap 32768: a city-sized overzoomed tile can exceed 16K
labels and the priority sort dropped addresses first
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Single-batch dispatch meant one tile in flight at a time after a zoom
(requests only advance when a draw happens, draws only happen when a
tile lands) — restyles trickled sequentially. Now every missing/stale
tile dispatches its own worker job, up to 12 in flight, so a viewport
restyle overlaps across the pool.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gap-distribution instrumentation on the 7680x2160@120 display showed the
renderer can hold 120fps (gap_avg 8.4ms in calm stretches) but dropped
frames wherever labels are dense: full label re-placement (5-22ms) ran
almost every other frame during pan (tile arrivals + pan distance
invalidated the cache) and EVERY frame during zoom (exact zoom equality
in the cache check).
- full re-places rate-limited to ~8/s; between them the cached placement
draws shifted (~2ms), including during zoom gestures where labels stay
screen-pinned briefly (pinch behavior a la Google Maps)
- each placement pass gets a hard 7ms budget; highest-scored labels
place first, the tail defers to the next pass
- gap avg/max and >12ms counts added to the perf log
- tile restyle after zoom: one tile per worker job (10-tile batches ran
sequentially in one closure, 2-3s per batch), settle delay 150->80ms
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Instrumentation showed draw_walk at ~1ms and CPU tile insert at ~2-3ms
while inter-frame gaps hit 200-1000ms during interaction: the stall is
GPU buffer upload/pressure from oversized tiles (a bucket-17 z14 tile
holds ~6.8K outlined buildings -> tens of MB of 19-float vertices).
- scale-aware vertex thinning: drop points closer than ~0.35 screen px
at the styled zoom (invisible detail dominated vertex volume)
- building outlines use miter joins (round-join bevel expansion on ~60K
ring points per tile was the largest single buffer)
- tile GPU uploads throttled to 2 per frame via a pending queue
- resident tile cache capped at 48 (was 640) with faster idle eviction
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- perf log: draw_walk aggregates total/geometry/label ms, max frame,
inter-frame gap and full-placement count per 240 frames into
local/map_perf.log
- labels precompute name_key + tile-local bbox at tile build; the
per-frame scan does no allocation and rejects offscreen labels with
two point transforms before any path work (an overzoomed tile holds
~16K labels, mostly offscreen)
- batched glyph path no longer runs update_draw_vars per call (the open
batch already did); halo underdraw 8 -> 4 diagonal offsets, halving
glyph volume
- fill clip overlap shrunk to ~1 screen px: the wider double-drawn
strip let the later tile's land paint over the earlier tile's
buildings (pale band at tile edges at high zoom)
- point local mbtiles back at noord-holland-shortbread-1.0.mbtiles
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>