perf(map): stop the vector surfaces casting shadows they can't make #20
Open
jeroen
wants to merge 2 commits from
perf/render-budget into main
pull from: perf/render-budget
merge into: jeroen:main
jeroen:main
jeroen:feat/background-connection
jeroen:fix/the-boot-log-can-be-read-on-a-phone
jeroen:feat/debug-switches-on-the-device
jeroen:worktree-bridge-cse_015wTvbfKdhpnumxLptz5Vem
jeroen:feat/an-off-switch-for-the-clouds
jeroen:fix/the-web-client-dies-on-its-first-frame
jeroen:fix/road-ladder-in-depth
jeroen:feat/chat-without-a-panel
jeroen:perf/android-presentation-rate
jeroen:fix/the-avatar-walks-when-you-press-w
jeroen:feat/settings-world-and-a-panel-that-measures
jeroen:fix/one-ladder-for-the-road-stack
jeroen:fix/teleport-honours-stated-altitude
jeroen:worktree-bridge-cse_01UFjQty8rTqXWPkc6kmuoUJ
jeroen:fix/locate-keeps-the-view-mode
jeroen:fix/deploy-registry-race
jeroen:feat/overpass-street-detail
jeroen:feat/grass-lod-wild
jeroen:feat/place-card-and-wire-teleport
jeroen:fix/stay-signed-in
jeroen:perf/startup-stream-ledger
jeroen:moon-sun-disk-off
jeroen:grass-near-field
jeroen:grade-separation
jeroen:fix/adreno-gpu-driven-draws
jeroen:fix/road-junction-geometry
jeroen:device-loss-and-the-sea-seam
jeroen:handset-memory-guards
jeroen:android-atmosphere-cliff
jeroen:structural-improvements
jeroen:logo-map-towers
jeroen:worktree-bridge-cse_01G4GXKJuSSaXChKNpF9GnTE
jeroen:docs/revert-unused-capacity
jeroen:docs/unused-capacity
jeroen:map-quality
jeroen:worktree-bridge-cse_016LtLcii4wNW2UPCnRueq83
jeroen:fix/login-card-menu-only
jeroen:fix/fmt-main
jeroen:worktree-bridge-cse_01KtcXqg43mfPdN7EdPfuzRz
jeroen:worktree-bridge-cse_01L26w79mwG8PYuFA8sxsm5D
jeroen:worktree-bridge-cse_01UTR3NYNLFqVb1t1tUhWA7q
jeroen:worktree-bridge-cse_014rk39UGy6ojEfc1HxyCxpN
jeroen:fix/fmt-and-cache-on-failure
jeroen:docs/host-disk-2026-08-17
jeroen:worktree-bridge-cse_011qsp2qScTevJRNYFSP785q
jeroen:worktree-bridge-cse_01S6jQqoLKzmYAAuFkbGeTEd
jeroen:worktree-bridge-cse_01EXY5TEByuLjZZVpgPmE9QR
jeroen:worktree-bridge-cse_011aArnfDAxMrf6k3TFPeSgH
jeroen:worktree-bridge-cse_01ChWoCB2TuZmYw3xtktCoYt
jeroen:worktree-bridge-cse_01SX8YKnC8mfHwoJ2kgt5xN2
jeroen:worktree-bridge-cse_0199QpdjmHuRbzvgNHTJWNse
jeroen:worktree-bridge-cse_01VocS6mGtcYc5dQzWUKRJFB
jeroen:worktree-bridge-cse_01LRb1An23fumewbVCfXPNHw
jeroen:worktree-bridge-cse_01JoQ3KktMBwvkC4CgQTBKE3
jeroen:worktree-bridge-cse_01GKh6pjXFP9ucWw7c3HDiGT
jeroen:worktree-bridge-cse_0147AbtQqJzF6dVs6BNyHXmi
jeroen:worktree-bridge-cse_017pejrKPjzrCG77oc5gRfme
jeroen:fix/android-wake-lock
jeroen:worktree-bridge-cse_011MbGc1KjpPB8fj5eiBifyV
jeroen:fix/android-wakelock-deadlock
jeroen:feat/places-actions-and-touch-recovery
jeroen:perf/phone-frame-time
jeroen:fix/phone-render-scale
jeroen:drop-node-modules
jeroen:move-gui-to-cartopy
jeroen:tls-everywhere
jeroen:fix-ci-registry-race
jeroen:worktree-bridge-cse_012A4pKMAbPqg59fJfoZTdgz
jeroen:worktree-bridge-cse_011XzUq1BAPcLyCYTYcTx9DP
jeroen:worktree-bridge-cse_01VzhibjDRdvUUBd97jP8o8k
jeroen:docs/ci-build-cost
jeroen:worktree-bridge-cse_01MkYfqf2CgMvbp9sssbEFDx
jeroen:rebrand/cartopolis
jeroen:feat/32-proximity-voice-sim
jeroen:feat/audio-settings
jeroen:perf/37-qa-sweep-leaks-and-overzoom
jeroen:feat/prim-scripting-luau
jeroen:feat/30-avatar-game-feel
jeroen:feat/android-apk-deploy
jeroen:feat/31-pois-from-vector-tile
jeroen:feat/phone-usability
jeroen:perf/wasm-phone-render-budget
jeroen:fix/ci-fmt-and-warning-debt
jeroen:feat/cloud-scattering
jeroen:fix/foliage-on-roads
jeroen:feat/touch-input
jeroen:feat/15-retire-regions-capacity-bounded
jeroen:fix/shot-harness-software-renderer
jeroen:to-merge
jeroen:perf/11-tile-pipeline-dedup
jeroen:fix/road-joins-and-tile-decode
jeroen:feat/shared-http-agent-overpass-fallback
jeroen:issue-2
jeroen:feat/wasm-loader-sanity-checks
No reviewers
Labels
Clear labels
agent
Hand this issue to the Claude agent on the VPS
agent:ci
Branch pushed; waiting for CI. It becomes agent:done only if CI passes.
agent:done
The agent finished and pushed a branch
agent:failed
The agent errored out; see its comment
agent:needs-input
Refined, but there are blocking questions. Nothing is built until they are answered.
agent:refined
Spec written and agreed; queued for implementation.
agent:refining
The issue agent is reading the code and writing a spec.
agent:running
The agent has claimed this issue and is working
agent:skip
Keep the issue agent off this one
autonomous
Hand this to the issue agent: refine it, then implement it unattended.
driven
A person is steering this one (Zed or claude.ai/code). The issue agent leaves it alone; remove to hand over.
local
Worked on locally in Zed — the remote issue agent leaves it alone. Remove to hand over.
No labels
agent
agent:ci
agent:done
agent:failed
agent:needs-input
agent:refined
agent:refining
agent:running
agent:skip
autonomous
driven
local
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
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!20
Loading…
Reference in a new issue
No description provided.
Delete branch "perf/render-budget"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
lod22and the basemap's relief meshes both give up distant shadowcasting, and both note the same reason: geometry re-rasterised into up
to eight cascades (sun + moon x 4) is the dominant frame cost.
map_geometrynever got that treatment — every surface it spawned castunconditionally, at every distance.
Measured on the Groningen z14 fixture, tessellating one cell the way the
worker does (surfaces + bridges + roads + scatter + furniture at
tile_m = 1500):
Canopy 38,624 Road(Path) 2,786 Built 521
Furniture 13,080 Road(Minor) 2,537 Water 331
Trunk 9,248 Grass 1,695 others 815
70,450 triangles per cell, of which 60,952 — 87% — are the three groups
with actual volume. Across the CELL_RADIUS=1 ring that is ~550k
triangles going into every cascade, more shadow geometry than the
terrain-relief regression that measured ktris_shadow 1.07M -> 2.96M and
26 fps -> 10.
Two different fixes, because the groups fail differently:
The flat ground stack (water, landuse, roads) gets a permanent
NotShadowCaster. Those sheets sit 1-10 mm inside the ground they are
drawn on, so their only possible shadow is self-shadow acne, and no
camera position changes that. ~9.5k triangles per cell, ~85k across the
ring, for nothing.
SurfaceGroup::in_ground_stacknames that propertyrather than reusing
y_offset() < 0.0directly: the offsets were chosento stop z-fighting, and a future group joining the stack for depth
reasons should not silently inherit a shadow verdict. A test holds the
two matches together.
Trees and furniture get a distance LOD (SURFACE_SHADOW_RADIUS = 0), the
same shape as lod22's. A radius and not an off-switch because tree
shadows at street level are most of the reason to draw trees; it is the
ones two cells out, whose shadows fall on ground seen from a kilometre
up, that pay nothing back. Bridge decks keep casting unconditionally —
a deck spans a gap, and its shadow on the water underneath is what
reads it as a structure.
The cell rides on the entity (ShadowLodCell) rather than in a per-cell
entity list on MapSurfaces, which is the shape lod22 uses. With a list
the toggle would remove NotShadowCaster from every mesh in the cell
the moment the camera walked into it — silently handing the ground
stack back its acne-only casting, with nothing to show for it but a
frame-time regression nobody could attribute. There is a test for that
specific direction.
It is a separate system because
update_map_surfacesis already atBevy's 16-parameter ceiling, which is also why the camera cell is
published on MapSurfaces instead of recomputed: the two must agree on
which cell is "here" or the shadow boundary sits somewhere other than
the streaming ring. Current state is read with Has
rather than mirrored in a set, so there is no bookkeeping to drift and a
stationary camera issues no commands.
Note this layer escaped the policy quietly:
log_render_costbucketsbuildings, lod22, tiles and globe, but has no MapSurface line, so the
largest remaining shadow contributor was the one thing its ktris_shadow
column could not attribute.
Unverified: this container has no GPU, so the frame-time win is
argued from triangle counts, not measured. Two things want eyes on a
workstation — the size of the win (
--csv, watching frame_ms over theGroningen anchor at street level), and whether tree shadows ending at a
~1.5 km cell boundary reads badly when the camera stands near one.
Raising SURFACE_SHADOW_RADIUS to 1 is the whole fix if it does.
View command line instructions
Manual merge helper
Use this merge commit message when completing the merge manually.
Checkout
From your project repository, check out a new branch and test the changes.