WORKFLOW TEST — make the map better #5

Closed
opened 2026-07-30 07:38:01 +00:00 by viberfox-agent · 2 comments
Collaborator

Problem

"The map should look nicer and be faster" cannot be turned into acceptance criteria as written — it names no surface, no metric and no target device. Below is what the map actually is today and where the measurable headroom sits, so the decision is one reply rather than a design round.

What "the map" is. The default basemap source is the VersaTiles vector mirror (crates/vibe_core/src/world.rs:47), classified as vector by tile_source_kind (crates/viberfox/src/systems/tile_loader.rs:130-152), rasterised client-side to 512 px textures by render_mvt (crates/viberfox/src/systems/vector_tiles.rs:35). PAINT_ORDER (vector_tiles.rs:146-206) is the cartography. map_stream places the resulting textures as a clipmap pyramid of ground quads (crates/viberfox/src/systems/map_stream.rs). Buildings (osm_buildings.rs, lod22.rs), water (water.rs), vegetation (vegetation.rs) and roads (road_texture.rs, map_geometry.rs) are separate systems on top — whether they count as "the map" is Q3 below.

Concrete, cited candidates:

A. Ground tiles are sampled without anisotropy. apply_stream_textures builds each tile image with ImageSampler::linear() (map_stream.rs:635); same for the region path (tile_loader.rs:372) and the globe (globe.rs:986). The road-surface texture in the same codebase already asks for anisotropy_clamp: 8 (map_geometry.rs:437-442). A ground quad viewed at a grazing angle is the textbook case for anisotropic filtering, and the mip chain is already built (rendering::build_mip_chain).

B. The whole pyramid draws every frame, unculled, with a material per tile. MIN_ZOOM = 10 (map_stream.rs:59) up to base zoom 17 (map_stream.rs:248) is 8 levels; the finest ring is radius 4 → 9×9 = 81 tiles (map_stream.rs:53), each coarser ring radius 3 → 7×7 = 49 (map_stream.rs:56). That is 424 quads at low altitude, derived from those constants (not measured — see below). Every one carries NoFrustumCulling (map_stream.rs:477-479, deliberately: flat quads plus big_space rebasing confused culling) and gets its own StandardMaterial + 512² RGBA texture (map_stream.rs:638-644), so nothing batches. Nothing hides a coarse tile that a finer one fully covers — coarse levels are only pushed 0.4 m down per zoom step (map_stream.rs:438).

I cannot confirm the render cost of this from here: ShotMetrics has frame_ms but no draw-call or flat-tile count (shot_harness.rs:243-281), and there is no GPU in this container.

C. Overzoomed tiles refetch and reparse the same source body once per sub-tile. Shortbread stops at z14 (vector_tiles.rs:39), so every z15–z17 request resolves to its z14 ancestor and fetches that whole body (tile_loader.rs:240-242). load_tile_image dedups only through the on-disk cache (tile_loader.rs:169-183) — there is no in-flight map, so on a fresh anchor the 81 z17 tiles (which share only a handful of ancestors) each issue their own request. Each sub-tile then pays its own MVT parse + get_features + rasterise (tile_loader.rs:223-227): measured ~2.7 ms + ~4 ms + ~35 ms per overzoomed sub-tile, min-of-30 on the Groningen z14 fixture (docs/notes/tile-raster-cost.md). And map_stream spawns one task per wanted tile with no admission gate (map_stream.rs:486-495), unlike the region path which caps concurrency at TILE_WORKERS = 3 (tile_loader.rs:15,46).

D. Fill rate is frame-capped. MAX_UPLOADS_PER_FRAME = 8 (map_stream.rs:596) and MAX_TILE_DESPAWNS_PER_FRAME = 12 (map_stream.rs:67). Both are device-loss guards documented in CLAUDE.md, but they mean a 424-tile pyramid needs ≥53 frames to finish uploading even with every tile already decoded.

E. The map has no text. PAINT_ORDER deliberately omits *_labels, addresses and pois pending a glyph pipeline (vector_tiles.rs:144-146). This is the largest single "looks nicer" lever and by far the largest job.

F. Palette/cartography. vector_tiles::palette (vector_tiles.rs:89-123) is the entire look, deliberately flatter than osm-carto because it is lit ground texture, not a map read at arm's length (vector_tiles.rs:84-88).

G. The perf win the notes already name is untyped serde_json::Value parsing for 3DBAG/Overpass (lod22.rs:459, osm_buildings.rs:108, osm_buildings.rs:1845), called out as "the next win if 3DBAG streaming latency matters" in docs/notes/wasm-performance.md. That is buildings, not basemap.

Already ruled out — do not re-test first. Atmosphere, env-map IBL, haze, clouds and rain were A/B'd on a real Intel Xe-LPG adapter and all landed within noise (docs/notes/wasm-performance.md). Shadows were never tested and are ~79% of submitted triangles; ?no_shadows exists to check that one. Moving rasterisation to the GPU is an explicitly deferred open question (docs/notes/tile-raster-cost.md) — it is streaming-latency cost on a worker pool, not frame-time cost, and any proposal must beat that and survive wasm32 + no-C-dependencies.

Approach

Not specifiable until Q1–Q3 are answered. For each lane, the blast radius:

Lane Files
A — anisotropy crates/viberfox/src/systems/map_stream.rs (+ tile_loader.rs, globe.rs if applied consistently)
B — cull/batch the pyramid map_stream.rs, plus one ShotMetrics field in shot_harness.rs to make it measurable
C — dedup source fetch/parse + gate admission tile_loader.rs, map_stream.rs, possibly vector_tiles.rs
D — fill-rate caps map_stream.rs only (but they are device-loss guards; raising them needs the weak-GPU check, not just a number)
E — labels vector_tiles.rs + a glyph/font path; new dependency decision
F — restyle vector_tiles.rs palette + PAINT_ORDER
G — typed parsing lod22.rs, osm_buildings.rs

Acceptance criteria

Lane-independent criteria that hold for whichever lane is chosen. The lane-specific list is blocked on Q1.

  • A before number is recorded on a workstation before any change, by the same command as the after number.
  • If the change is a perf change, the metric is frame_ms and/or settled_s from cargo shots --csv, or tools/cdp_probe.ts --gpu --profile for the web build — against the real adapter, not SwiftShader (docs/notes/wasm-performance.md).
  • Any new inspectable quantity is added as one field on ShotMetrics (shot_harness.rs:243) so it lands in the log line, --csv and --dump-state at once, per the documented pattern in CLAUDE.md.
  • cargo test -p viberfox --bin viberfox passes (note --lib does not exist).
  • Any per-frame streaming/upload path stays bounded and altitude/drift-gated — the VK_ERROR_DEVICE_LOST guards in CLAUDE.md are not to be loosened without a weak-GPU check.
  • Measurements that contradict docs/notes/tile-raster-cost.md or docs/notes/wasm-performance.md are written back into those notes in place, with date and method.

Verification

Runnable here (no GPU needed):

cargo check -p viberfox
cargo test -p viberfox --bin viberfox

Workstation only — everything visual or frame-timed:

cargo shots                                   # regenerate the README image set
cargo shots --only <preset> --csv /tmp/a.csv  # before/after frame_ms + settled_s
cargo run -p viberfox --profile dev-bevy -- --shot /tmp/s.png \
    --at 53.2194,6.5665,300 --look=-60,0 --csv /tmp/b.csv
bun tools/cdp_probe.ts --gpu --profile        # web build, real adapter

Two notes for whoever writes the verification steps: docs/shots.toml:23 pins the vector source, so cargo shots exercises the client-side rasteriser (CLAUDE.md's line about the built-in shot fallback being the raster basemap is stale — DEFAULT_TILE_URL_TEMPLATE is vector, vibe_core/src/world.rs:47). And a grazing-angle preset is needed to see lane A at all; a top-down shot will show nothing.

Out of scope

  • The globe/space tier (globe.rs, globe_ocean.rs) unless Q3 says otherwise — it is a separate BRDF and imagery path with its own crossfade rules.
  • Moving rasterisation to the GPU. Explicitly deferred in docs/notes/tile-raster-cost.md; re-opening it is its own issue.
  • Re-testing atmosphere / env-map / haze / clouds / rain for frame cost — already A/B'd to noise.
  • Loosening MAX_UPLOADS_PER_FRAME, MAX_TILE_DESPAWNS_PER_FRAME, MAX_GLOBE_TILE_BUILDS_PER_FRAME or the OSM detail-patch freeze as a standalone "speedup".
  • Any change verified only by a screenshot taken in this container. There is no GPU here.

Open questions

  1. Which lane? "Nicer" and "faster" are at least six unrelated workstreams (A–G above). Pick one, or rank them. If the honest answer is "whichever gives the most for the least", my recommendation is C then A: C is the one with a measured cost behind it and no visual risk, A is a few lines and is the most likely single visual improvement per unit of work.
  2. What is the target for "faster", and on what? Three different numbers, three different fixes: (a) time from teleport to a filled map — lane C/D; (b) steady-state frame time on the desktop build — lane B; (c) web-build fps on the Intel Xe-LPG the notes measure against — lane G and shadows. Which one is failing for you, and what would count as fixed?
  3. Does "the map" mean the basemap tiles only, or the whole ground scene (buildings, trees, water, roads)? Lane G only matters if buildings are in.
  4. Is a change that only a workstation can verify acceptable for this issue? Lanes A, B, E and F cannot be signed off from this container — I can implement and type-check them, but you would have to run cargo shots and look.

Branch: issue-5

Original request

The map should look nicer and be faster.

🤖 Refined by the viberfox issue agent. Reply with @agent refine and what is wrong to have this rewritten.

## Problem "The map should look nicer and be faster" cannot be turned into acceptance criteria as written — it names no surface, no metric and no target device. Below is what the map actually is today and where the measurable headroom sits, so the decision is one reply rather than a design round. **What "the map" is.** The default basemap source is the VersaTiles **vector** mirror (`crates/vibe_core/src/world.rs:47`), classified as vector by `tile_source_kind` (`crates/viberfox/src/systems/tile_loader.rs:130-152`), rasterised client-side to 512 px textures by `render_mvt` (`crates/viberfox/src/systems/vector_tiles.rs:35`). `PAINT_ORDER` (`vector_tiles.rs:146-206`) *is* the cartography. `map_stream` places the resulting textures as a clipmap pyramid of ground quads (`crates/viberfox/src/systems/map_stream.rs`). Buildings (`osm_buildings.rs`, `lod22.rs`), water (`water.rs`), vegetation (`vegetation.rs`) and roads (`road_texture.rs`, `map_geometry.rs`) are separate systems on top — whether they count as "the map" is Q3 below. Concrete, cited candidates: **A. Ground tiles are sampled without anisotropy.** `apply_stream_textures` builds each tile image with `ImageSampler::linear()` (`map_stream.rs:635`); same for the region path (`tile_loader.rs:372`) and the globe (`globe.rs:986`). The road-surface texture in the same codebase already asks for `anisotropy_clamp: 8` (`map_geometry.rs:437-442`). A ground quad viewed at a grazing angle is the textbook case for anisotropic filtering, and the mip chain is already built (`rendering::build_mip_chain`). **B. The whole pyramid draws every frame, unculled, with a material per tile.** `MIN_ZOOM = 10` (`map_stream.rs:59`) up to base zoom 17 (`map_stream.rs:248`) is 8 levels; the finest ring is radius 4 → 9×9 = 81 tiles (`map_stream.rs:53`), each coarser ring radius 3 → 7×7 = 49 (`map_stream.rs:56`). That is **424 quads** at low altitude, derived from those constants (not measured — see below). Every one carries `NoFrustumCulling` (`map_stream.rs:477-479`, deliberately: flat quads plus big_space rebasing confused culling) and gets its own `StandardMaterial` + 512² RGBA texture (`map_stream.rs:638-644`), so nothing batches. Nothing hides a coarse tile that a finer one fully covers — coarse levels are only pushed 0.4 m down per zoom step (`map_stream.rs:438`). I cannot confirm the render cost of this from here: `ShotMetrics` has `frame_ms` but no draw-call or flat-tile count (`shot_harness.rs:243-281`), and there is no GPU in this container. **C. Overzoomed tiles refetch and reparse the same source body once per sub-tile.** Shortbread stops at z14 (`vector_tiles.rs:39`), so every z15–z17 request resolves to its z14 ancestor and fetches that whole body (`tile_loader.rs:240-242`). `load_tile_image` dedups only through the on-disk cache (`tile_loader.rs:169-183`) — there is **no in-flight map**, so on a fresh anchor the 81 z17 tiles (which share only a handful of ancestors) each issue their own request. Each sub-tile then pays its own MVT parse + `get_features` + rasterise (`tile_loader.rs:223-227`): measured ~2.7 ms + ~4 ms + ~35 ms per overzoomed sub-tile, min-of-30 on the Groningen z14 fixture (`docs/notes/tile-raster-cost.md`). And `map_stream` spawns one task per wanted tile with **no admission gate** (`map_stream.rs:486-495`), unlike the region path which caps concurrency at `TILE_WORKERS = 3` (`tile_loader.rs:15,46`). **D. Fill rate is frame-capped.** `MAX_UPLOADS_PER_FRAME = 8` (`map_stream.rs:596`) and `MAX_TILE_DESPAWNS_PER_FRAME = 12` (`map_stream.rs:67`). Both are device-loss guards documented in CLAUDE.md, but they mean a 424-tile pyramid needs ≥53 frames to finish uploading even with every tile already decoded. **E. The map has no text.** `PAINT_ORDER` deliberately omits `*_labels`, `addresses` and `pois` pending a glyph pipeline (`vector_tiles.rs:144-146`). This is the largest single "looks nicer" lever and by far the largest job. **F. Palette/cartography.** `vector_tiles::palette` (`vector_tiles.rs:89-123`) is the entire look, deliberately flatter than osm-carto because it is lit ground texture, not a map read at arm's length (`vector_tiles.rs:84-88`). **G. The perf win the notes already name** is untyped `serde_json::Value` parsing for 3DBAG/Overpass (`lod22.rs:459`, `osm_buildings.rs:108`, `osm_buildings.rs:1845`), called out as "the next win if 3DBAG streaming latency matters" in `docs/notes/wasm-performance.md`. That is buildings, not basemap. **Already ruled out — do not re-test first.** Atmosphere, env-map IBL, haze, clouds and rain were A/B'd on a real Intel Xe-LPG adapter and all landed within noise (`docs/notes/wasm-performance.md`). Shadows were never tested and are ~79% of submitted triangles; `?no_shadows` exists to check that one. Moving rasterisation to the GPU is an explicitly **deferred** open question (`docs/notes/tile-raster-cost.md`) — it is streaming-latency cost on a worker pool, not frame-time cost, and any proposal must beat that and survive wasm32 + no-C-dependencies. ## Approach Not specifiable until Q1–Q3 are answered. For each lane, the blast radius: | Lane | Files | |---|---| | A — anisotropy | `crates/viberfox/src/systems/map_stream.rs` (+ `tile_loader.rs`, `globe.rs` if applied consistently) | | B — cull/batch the pyramid | `map_stream.rs`, plus one `ShotMetrics` field in `shot_harness.rs` to make it measurable | | C — dedup source fetch/parse + gate admission | `tile_loader.rs`, `map_stream.rs`, possibly `vector_tiles.rs` | | D — fill-rate caps | `map_stream.rs` only (but they are device-loss guards; raising them needs the weak-GPU check, not just a number) | | E — labels | `vector_tiles.rs` + a glyph/font path; new dependency decision | | F — restyle | `vector_tiles.rs` palette + `PAINT_ORDER` | | G — typed parsing | `lod22.rs`, `osm_buildings.rs` | ## Acceptance criteria Lane-independent criteria that hold for whichever lane is chosen. The lane-specific list is blocked on Q1. - [ ] A **before** number is recorded on a workstation before any change, by the same command as the after number. - [ ] If the change is a perf change, the metric is `frame_ms` and/or `settled_s` from `cargo shots --csv`, or `tools/cdp_probe.ts --gpu --profile` for the web build — against the real adapter, not SwiftShader (`docs/notes/wasm-performance.md`). - [ ] Any new inspectable quantity is added as one field on `ShotMetrics` (`shot_harness.rs:243`) so it lands in the log line, `--csv` and `--dump-state` at once, per the documented pattern in CLAUDE.md. - [ ] `cargo test -p viberfox --bin viberfox` passes (note `--lib` does not exist). - [ ] Any per-frame streaming/upload path stays bounded and altitude/drift-gated — the `VK_ERROR_DEVICE_LOST` guards in CLAUDE.md are not to be loosened without a weak-GPU check. - [ ] Measurements that contradict `docs/notes/tile-raster-cost.md` or `docs/notes/wasm-performance.md` are written back into those notes in place, with date and method. ## Verification Runnable here (no GPU needed): ```bash cargo check -p viberfox cargo test -p viberfox --bin viberfox ``` **Workstation only** — everything visual or frame-timed: ```bash cargo shots # regenerate the README image set cargo shots --only <preset> --csv /tmp/a.csv # before/after frame_ms + settled_s cargo run -p viberfox --profile dev-bevy -- --shot /tmp/s.png \ --at 53.2194,6.5665,300 --look=-60,0 --csv /tmp/b.csv bun tools/cdp_probe.ts --gpu --profile # web build, real adapter ``` Two notes for whoever writes the verification steps: `docs/shots.toml:23` pins the **vector** source, so `cargo shots` exercises the client-side rasteriser (CLAUDE.md's line about the built-in shot fallback being the *raster* basemap is stale — `DEFAULT_TILE_URL_TEMPLATE` is vector, `vibe_core/src/world.rs:47`). And a grazing-angle preset is needed to see lane A at all; a top-down shot will show nothing. ## Out of scope - The globe/space tier (`globe.rs`, `globe_ocean.rs`) unless Q3 says otherwise — it is a separate BRDF and imagery path with its own crossfade rules. - Moving rasterisation to the GPU. Explicitly deferred in `docs/notes/tile-raster-cost.md`; re-opening it is its own issue. - Re-testing atmosphere / env-map / haze / clouds / rain for frame cost — already A/B'd to noise. - Loosening `MAX_UPLOADS_PER_FRAME`, `MAX_TILE_DESPAWNS_PER_FRAME`, `MAX_GLOBE_TILE_BUILDS_PER_FRAME` or the OSM detail-patch freeze as a standalone "speedup". - Any change verified only by a screenshot taken in this container. There is no GPU here. ## Open questions 1. **Which lane?** "Nicer" and "faster" are at least six unrelated workstreams (A–G above). Pick one, or rank them. If the honest answer is "whichever gives the most for the least", my recommendation is **C then A**: C is the one with a measured cost behind it and no visual risk, A is a few lines and is the most likely single visual improvement per unit of work. 2. **What is the target for "faster", and on what?** Three different numbers, three different fixes: (a) time from teleport to a filled map — lane C/D; (b) steady-state frame time on the desktop build — lane B; (c) web-build fps on the Intel Xe-LPG the notes measure against — lane G and shadows. Which one is failing for you, and what would count as fixed? 3. **Does "the map" mean the basemap tiles only, or the whole ground scene** (buildings, trees, water, roads)? Lane G only matters if buildings are in. 4. **Is a change that only a workstation can verify acceptable for this issue?** Lanes A, B, E and F cannot be signed off from this container — I can implement and type-check them, but you would have to run `cargo shots` and look. --- Branch: `issue-5` <details><summary>Original request</summary> The map should look nicer and be faster. </details> <sub>🤖 Refined by the viberfox issue agent. Reply with **@agent refine** and what is wrong to have this rewritten.</sub>
Author
Collaborator

🤖 Refining this before anything gets built — reading the code, no branch, no commits. If it turns out I'd have to guess at the requirements I'll stop and ask instead.

🤖 Refining this before anything gets built — reading the code, no branch, no commits. If it turns out I'd have to guess at the requirements I'll stop and ask instead.
Author
Collaborator

🤖 This needs a decision before I build anything. The open questions are in the description above.

Answer them and reply with @agent refine to have the spec rewritten, or @agent to make me implement it as it stands.


🤖 viberfox issue agent on the VPS — 21 turns · $1.71 · session 8f53fc22-e210-4be0-b025-2a0d9e530721. Reply with @agent to continue this session.

🤖 **This needs a decision before I build anything.** The open questions are in the description above. Answer them and reply with **@agent refine** to have the spec rewritten, or **@agent** to make me implement it as it stands. --- <sub>🤖 viberfox issue agent on the VPS — 21 turns · $1.71 · session `8f53fc22-e210-4be0-b025-2a0d9e530721`. Reply with **@agent** to continue this session.</sub>
viberfox-agent removed this from the Viberfox project 2026-07-30 07:44:23 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jeroen/cartopolis#5
No description provided.