Split out of #492 as cause 2. #556 fixed cause 1 (level-wide recompute per frame); this is the remaining half and a design change rather than a regression.
Measurement
All numbers from @toycenterboss-bot's instrumented profiling in #492. macOS, Chrome, WebGPU renderer (three r185), frameloop="never" — so 50 FPS is a ceiling, and a 50.x reading means "hit the ceiling", not "hit the hardware limit".
| Scene |
Nodes |
Contents |
Draw calls |
Orbit FPS |
| empty |
0 |
grid and helpers only |
24 |
50.3 |
| zones |
142 |
flat polygons |
130 |
50.2 |
| outer walls |
88 |
perimeter only |
534 |
50.0 |
| real plan |
310 |
walls and rooms |
1362 |
50.2 |
| full import |
1089 |
outer + inner walls |
6520 |
42.7 |
Cost tracks visible meshes, not node count — a 142-node zone scene costs 130 calls, an 88-node wall scene costs 534. Every scene under roughly 1400 calls holds the ceiling; the 1089-wall import falls off it.
A scene-graph walk on that scene found 8 meshes with one material group, 1081 with three, and 24 with six: 3395 draw units, doubled by the shadow pass → 6520 calls.
Direction
Merging a level's static walls per material collapses those 3243 units to three, which took the same scene to 25 draw calls in the reporter's experiment.
The constraint to design around: the merge has to invalidate cleanly on edit, and walls are exactly the geometry users mutate most. So the win only holds if the merged batch
That ordering is why this is a follow-up to #556 rather than part of it.
Also worth checking while in here: the shadow pass doubling every unit means anything that reduces shadow-casting mesh count pays twice.
Fixture
A 1089-wall scene JSON reproducing the last row has been offered in #492 and should be added as a profiling fixture so this is measurable in CI rather than by hand.
Credit to @toycenterboss-bot for the profiling and the decomposition — separating "work repeated per frame with unchanged inputs" from "draw calls scale with mesh count" is what made both halves actionable.
Split out of #492 as cause 2. #556 fixed cause 1 (level-wide recompute per frame); this is the remaining half and a design change rather than a regression.
Measurement
All numbers from @toycenterboss-bot's instrumented profiling in #492. macOS, Chrome, WebGPU renderer (three r185),
frameloop="never"— so 50 FPS is a ceiling, and a 50.x reading means "hit the ceiling", not "hit the hardware limit".Cost tracks visible meshes, not node count — a 142-node zone scene costs 130 calls, an 88-node wall scene costs 534. Every scene under roughly 1400 calls holds the ceiling; the 1089-wall import falls off it.
A scene-graph walk on that scene found 8 meshes with one material group, 1081 with three, and 24 with six: 3395 draw units, doubled by the shadow pass → 6520 calls.
Direction
Merging a level's static walls per material collapses those 3243 units to three, which took the same scene to 25 draw calls in the reporter's experiment.
The constraint to design around: the merge has to invalidate cleanly on edit, and walls are exactly the geometry users mutate most. So the win only holds if the merged batch
That ordering is why this is a follow-up to #556 rather than part of it.
Also worth checking while in here: the shadow pass doubling every unit means anything that reduces shadow-casting mesh count pays twice.
Fixture
A 1089-wall scene JSON reproducing the last row has been offered in #492 and should be added as a profiling fixture so this is measurable in CI rather than by hand.
Credit to @toycenterboss-bot for the profiling and the decomposition — separating "work repeated per frame with unchanged inputs" from "draw calls scale with mesh count" is what made both halves actionable.