ifcviewer: bound the CPU triangle shadow and the cull scratch to residency

The wasm heap grew past 2 GB on a 66-model session (surfacing first as
the setBindGroup 2 GB TypeError, fixed separately) because CPU memory
attached to loaded geometry never shrank while the GPU pool did:

- mesh_triangles_cache — the dequantised positions + LOD0 indices the
  surface raycasts and measurement tools read — was filled once per mesh
  on first residency (gated on mesh_local_volumes == 0) and never
  released, converging over a session to the whole federation's geometry
  on the heap: 12 B/vertex + 4 B/index, 400 MB - 1 GB at this scale. And
  on web nothing reads it at all (no measurement tools yet).
- Every chunk's cull scratch was reserved at model load (20 B/instance
  scene-wide) and the scratch + uploaded mirrors survived eviction.
- Cull ran the HiZ test and emitted VisibleDrawGpu entries — then
  uploaded them — for non-resident chunks render() cannot draw.

Now the shadow follows GPU residency: a per-mesh resident-chunk refcount
(the spatial planner may duplicate a mesh into several chunks) is
counted up in applyStreamedChunk and down in unloadChunk, releasing the
mesh's entry at zero and refilling from the chunk bytes on the next
residency. mesh_local_volumes (8 B/mesh) is kept across eviction so the
Volume tool still covers evicted meshes. Hosts opt in via
ViewportHost::wantsCpuMeshTriangles(): Qt yes, web no until the tools
are ported — so on web the shadow costs nothing.

Cull stops at the streaming counters for non-resident chunks, the eager
scratch reserve is gone, and unloadChunk releases the scratch and
uploaded mirrors. Clearing the mirrors also fixes a real staleness bug
in unload/load: the model's cull buffers are recreated on load, and a
stale mirror would make the memcmp dirty-check skip the first upload
into the fresh (garbage) buffer.

The heartbeat log reports the shadow (cpuTris). Measured on a 3-model /
990 MB scene: shadow tracks residency (493 MB at a 530 MB resident set,
flat over minutes of streaming churn; previously monotonic), unload
drops it to zero, reload refills it (verified via readbackMeshTriangles
round trip).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-08-23 22:38:02 +10:00
parent 06d87e21a7
commit 091b4d4113
4 changed files with 101 additions and 10 deletions
+5
View File
@@ -58,6 +58,11 @@ public:
// here so the page can read them whenever it likes (ifcv_get_frame_stats_c)
// instead of being called back every frame across the wasm boundary.
void onFrameStats(const FrameStats& stats) override { last_stats_ = stats; }
// No measurement tools on web yet, so nothing reads the CPU triangle
// shadow — and at 12 B/vertex it is a large slice of a 4 GB-capped
// wasm heap. Flip when the tools are ported.
bool wantsCpuMeshTriangles() const override { return false; }
const FrameStats& lastFrameStats() const { return last_stats_; }
private: