Skip to content

osm basemap: wire the slab straight into the renderer (OSM1 binary tiles) - #119

Merged
AdaWorldAPI merged 1 commit into
mainfrom
claude/q2-osm-map-reencoding-56p5e2
Aug 12, 2026
Merged

osm basemap: wire the slab straight into the renderer (OSM1 binary tiles)#119
AdaWorldAPI merged 1 commit into
mainfrom
claude/q2-osm-map-reencoding-56p5e2

Conversation

@AdaWorldAPI

@AdaWorldAPI AdaWorldAPI commented Aug 12, 2026

Copy link
Copy Markdown
Owner

The live deploy draws (69,125 shapes from 132,382 rows, zero rented pixels) and is "terribly slow". Asked whether ndarray was the answer: no — and that's the useful finding. Neither half of the slowness is compute, so SIMD has nothing to bite on. Two representations were wrong, which is this workspace's own measured precedent (tesseract-rs: representation is the first-order lever, SIMD second).

  1. 69k retained SVG DOM elements — the browser re-rasterizes every element on every pan frame; zoom rebuilt the layer from scratch and never evicted the old one, so memory grew per zoom visited.
  2. serde-JSON on the hot path — ~45 B/point of [lon,lat] f64 text, re-fetched on every zoom with no cache validator. Already forbidden by house doctrine (T3/ADR-022: no serialization in the hot path; to_le_bytes IS the wire).

The framing this PR is built on

The tile endpoint is render_field_view for the geo domain — an askama-style projection over data already resident in the slab. Same binary, same bytes:

  • The tile range IS the mask. slab.tile_range(z,x,y) is a Morton-prefix row range over the same mmap — surface ∩ mask, nothing copied.
  • Zoom IS the projection depth. pixel_shift(z) = 32−(z+8)−1 is the ClassView choosing a reading granularity of the same z32 register — a shift, never a branch. Zooming in narrows the mask (deeper Morton prefix) and deepens the reading (smaller shift). Nothing is re-encoded or stored twice.

So the data now goes straight through, with exactly one materialization — into the renderer's own native retained form:

slab z32 cells ──(one multiply)──▶ LE f32 pairs on the wire
  ──▶ ArrayBuffer ──(Float32Array lens; no JSON tree, no per-point objects)──▶
      Path2D per (tile,class)   ◀── the ONE materialization
  ──▶ ~150 native canvas draw calls per frame

Measured (headless Chromium, real client code, genuine OSM1 buffers)

desktop phone
canvas painted 146,199 px (960×800) 33,217 px (390×490)
SVG DOM nodes 0 0
binary tiles fetched 49 25
pan repaints yes yes
external requests 0 0

Wire size, identical tile: 2,524 B binary vs 12,873 B JSON = 5.10× smaller — before the ETag turns zoom revisits into 304s instead of re-downloads.

Server

  • OSM1 wire (/api/osm/geometry/tile-bin/:z/:x/:y): header counts + per shape (idx u32, class u8, closed u8, npoints u16) + npoints × (f32,f32) tile-relative world-pixels at z, projected straight from z32 cells — no lon/lat round-trip, none of its trig. Tile-relative so the f32 mantissa buys sub-pixel precision instead of world position (absolute world px at z19 needs 27 bits; f32 has 24).
  • ETag from the slab digest — already the cross-bake pin, so a new bake busts caches by construction and nothing else does.
  • query_tile_shapes() returns raw survivor cells; JSON and binary are both projections of it, so the two wires cannot drift in sampling or classification. The JSON endpoint is unchanged (tests, curl, click path).
  • ShapeClass::wire_code() pinned by value, not as u8 — the client's CLASS_ORDER indexes by these bytes, and an enum reorder would silently recolour the whole map with no error anywhere.

Client

  • Canvas replaces the retained SVG basemap entirely (the selection SVG stays — one shape, DOM is right for it).
  • Merged Path2D per (tile, class); fills back-to-front, strokes with roads on top.
  • rAF-throttled draw — pointermove fires faster than the display refreshes.
  • Zoom now evicts the geometry cache, fixing an unbounded-memory bug that sat underneath the perf one.

Tests

100 passed, 0 failed. Three new: wire codes pinned, empty-tile header, and a round-trip whose coordinates are cross-checked against an independent lon/lat projection — the binary encoder never touches lon/lat, so agreement is two implementations meeting rather than one echoed.

Where ndarray actually fits (recorded in the plan, not built)

Not per-request SIMD. The honest homes are bake-time pre-tiling (a .tiles sidecar so serving becomes a range memcpy, with the projection SIMD-batched once at bake) and a zero-copy chains lens (Chains::get heap-allocates a Vec<TileXy> per way per request — a lens question, not a SIMD one). Both are worth doing only if serving still profiles hot after this lands.

Not verified

No slab in this container, so every number above is synthetic-fixture through the real client code. The live deploy is the test that matters.

Plan: claude-notes/plans/2026-08-12-osm-vector-perf.md

🤖 Generated with Claude Code

https://claude.ai/code/session_01NMeiLmtDKhomJNSo2ecbJw


Generated by Claude Code

Summary by CodeRabbit

  • Performance

    • Improved vector basemap loading and rendering with a more efficient binary tile format.
    • Reduced redraw overhead for smoother map navigation and zooming.
    • Added geometry and tile caching to improve repeated map access.
  • Bug Fixes

    • Vector map tiles now handle temporary loading failures with retryable requests.
    • Improved rendering consistency across high-resolution displays.
    • Raster basemap behavior remains unchanged.

…les)

The live deploy draws (69,125 shapes from 132,382 rows, zero rented
pixels) and is "terribly slow" (operator). Neither half of that was a
compute problem, so it was never a SIMD problem: two REPRESENTATIONS
were wrong, which is this workspace's own measured precedent
(tesseract-rs: representation is the first-order lever, SIMD second).

  1. 69k retained SVG DOM elements. The browser re-rasterizes every
     element on every pan frame; zoom rebuilt the layer from scratch and
     its cache was never evicted, so memory grew per zoom visited.
  2. serde-JSON on the hot path — ~45 B per point of [lon,lat] f64 text,
     re-fetched on every zoom with no cache validator. This is exactly
     what the house doctrine already forbids (T3/ADR-022: no
     serialization in the hot path; to_le_bytes IS the wire).

Operator's framing, which is the right one: the tile endpoint is
`render_field_view` for the geo domain — an askama-style PROJECTION over
data already resident in the slab, same binary, same bytes. The tile
range IS the mask (`tile_range` = a Morton-prefix row range over the same
mmap), and ZOOM IS the projection depth: `pixel_shift(z) = 32-(z+8)-1` is
the ClassView choosing a reading granularity of the same z32 register — a
shift, never a branch, nothing re-encoded or stored twice.

So the data is now wired straight through:

  slab z32 cells --(one multiply)--> LE f32 pairs on the wire
    --> ArrayBuffer --(Float32Array lens; no JSON tree, no per-point
        objects)--> Path2D per (tile,class)  <-- the ONE materialization,
        directly into the renderer's own retained native form
    --> ~150 native canvas draw calls per frame

Server:
- OSM1 binary wire (`/api/osm/geometry/tile-bin/:z/:x/:y`): header counts
  + per shape (idx u32, class u8, closed u8, npoints u16) + npoints x
  (f32,f32) TILE-RELATIVE world-pixels at z, projected straight from the
  chain's z32 cells with no lon/lat round-trip and none of its trig.
  Tile-relative so the f32 mantissa buys sub-pixel precision instead of
  world position (absolute world px at z19 needs 27 bits; f32 has 24).
- ETag from the slab digest — already the cross-bake pin, so a new bake
  busts caches by construction and nothing else does. Zoom churn becomes
  304s instead of re-downloads.
- query_tile_shapes() returns RAW survivor cells; JSON and binary are
  both projections of it, so the two wires cannot drift in sampling or
  classification. JSON endpoint unchanged (tests, curl, the click path).
- ShapeClass::wire_code() is pinned by value, not `as u8`: the client's
  CLASS_ORDER indexes by these bytes, and an enum reorder would silently
  recolour the map with no error anywhere.

Client:
- Canvas replaces the retained SVG basemap entirely (the selection SVG
  stays — one shape, DOM is right for it).
- Per (tile,class) merged Path2D; fills back-to-front then strokes with
  roads on top.
- rAF-throttled draw: pointermove fires faster than the display
  refreshes.
- Zoom now EVICTS the geometry cache (paths are one zoom's pixel space) —
  fixes the unbounded-memory bug on top of the perf.

Measured, headless Chromium, real client code over a genuine OSM1 buffer:

  desktop  canvas 960x800, 146,199 px painted, SVG nodes 0, 49 bin tiles,
           pan repaints, 0 external requests
  phone    canvas 390x490, 33,217 px painted, SVG nodes 0, map 100% of vp
  wire     2,524 B binary vs 12,873 B JSON for the identical tile = 5.10x

Tests: 100 passed, 0 failed. The three new ones pin the wire codes, the
empty-tile header, and a round-trip whose coordinates are cross-checked
against an INDEPENDENT lon/lat projection (the binary encoder never
touches lon/lat, so agreement is two implementations meeting rather than
one echoed).

Not verified: the real bake. No slab in this container, so the numbers
above are synthetic-fixture through the real client code. The live
deploy is the test that matters.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NMeiLmtDKhomJNSo2ecbJw
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8053bd34-a7e2-476f-92cb-297aacd977fb

📥 Commits

Reviewing files that changed from the base of the PR and between e56324c and 6283593.

📒 Files selected for processing (4)
  • claude-notes/plans/2026-08-12-osm-vector-perf.md
  • crates/cockpit-server/src/main.rs
  • crates/cockpit-server/src/osm.rs
  • crates/cockpit-server/src/osm_features.rs

📝 Walkthrough

Walkthrough

The PR adds an OSM1 binary geometry endpoint with ETags and conditional caching. It shares raw geometry projection between JSON and binary outputs. The cockpit replaces retained SVG rendering with DPR-aware canvas rendering and class-based Path2D caches.

Changes

OSM vector tile pipeline

Layer / File(s) Summary
Raw geometry and OSM1 tile contract
crates/cockpit-server/src/osm_features.rs, crates/cockpit-server/src/main.rs, claude-notes/plans/...
Tile queries return shared raw shapes. The server encodes tile-relative f32 coordinates in OSM1 records, adds explicit wire codes, ETags, conditional 304 responses, cache headers, and binary endpoint registration. Tests validate decoding, projections, metadata, wire codes, and empty tiles.
Canvas vector rendering
crates/cockpit-server/src/osm.rs
The client fetches binary tiles, parses them into class-indexed Path2D objects, redraws through a DPR-aware canvas, evicts geometry on zoom changes, and retries failed requests. Raster rendering remains unchanged.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • AdaWorldAPI/q2#108: Extends the same slab-querying infrastructure and cockpit OSM integration.
  • AdaWorldAPI/q2#116: Shares OSM geometry handling and extends it to cached binary tile rendering.

Suggested reviewers: claude

Poem

I hop through tiles in binary light,
With canvas paths drawn crisp and bright.
Raw shapes rest in ordered rows,
While cached geometry swiftly flows.
ETags guard each little square—
A rabbit approves the lighter air.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cursor

cursor Bot commented Aug 12, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_953db3ff-f546-42ca-9545-33ddea7dd342)

@AdaWorldAPI
AdaWorldAPI marked this pull request as ready for review August 12, 2026 08:27
@AdaWorldAPI
AdaWorldAPI merged commit fcfc5a0 into main Aug 12, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants