Node batching: items, columns, doors and windows draw through BatchedMesh containers - #760
Merged
Conversation
Feeds charter backlog #3: projected draws for per-item merge vs per-asset instancing, plus a meshes-by-kind bucket for the non-item side of the budget. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK
400-4,900 instances, two geometries, one material, shadows on. Verdict: renders/shadows/post-FX clean; ~0.3us encode per instance (~10x cheaper than a real mesh); per-instance frustum culling active (draw/tri track the camera). WebGPU counts each multi-draw segment in info.render, so the 3a metric is encode/frame-cpu ms, not draw calls. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK
…tics (charter 3a) Level-parented items render through one BatchedMesh per (level, material, attribute-signature) — membership follows the dirty signal, lit items release and draw themselves (outline/selection paint untouched), joins wait for a quiet window, isolation stands the whole thing down, and 'thumbnail:before-capture' hands every item its meshes back so exports never see a batch (belt: batches carry pascalExport='strip'). The container is incremental — instance add/delete per membership change, geometry deduped per batch, capacity grown 2x on overflow. An existing batch always accepts a rejoining item; only new batches need MIN_BATCH_ENTRIES. v1 scope: items whose parent is a level; hosted items (wall/ceiling/roof) move on host edits that never dirty the item, so they keep drawing themselves. Interactive/animated/transparent items excluded by the same rule as walls. Rich 2x: 114 of 270 items batched (304 instances, 37 batches), idle frame cpu 10.65 -> ~8.3ms. Full matrix + gates follow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK
…surface A React remount of a level subtree (thumbnail capture level shuffling, tool-state swings) replaces the registry groups: imperatively-parented batch meshes die with the old group while fresh source clones mount with no layer hold. pruneDetached() spots the orphaned batches by parent identity each frame and re-stales their items. ?perf probe gains stats/wave/census/batchRender hooks for scripted verification. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK
On WebGPU each batched instance still counts once in drawCalls (the backend loops drawIndexed per visible instance), so a batched scene looked no cheaper by the panel's DRAW number. The row now reads e.g. '581 (272 batched · 2 mesh)' — live per sample, so per-instance frustum culling is visible as the camera moves. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK
The first cut read _multiDrawCount, which snapshots whichever camera culled the batch last — shadow vs main vs outline passes made the number flip (206 -> 11 on hover) and read as items dropping out of the batch. The batch system now publishes its membership (items/instances/ containers) to the panel store; the row is stable and only moves when membership actually changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK
…3a+3b) item-batch generalizes to shared/node-batch: same wall-batch semantics, BatchedMesh containers per (level, material, attribute-signature), now covering items, columns and wall-hosted openings (resolved to the host wall's level). New release triggers: a dirty WALL cascades to its openings (the wall edit moves them without marking them), a host wall that is lit or mid-gesture releases its openings, and a door/window whose animation record appears draws itself for the tween. Hitboxes (material.visible=false) and glass (transparent) never batch. Bake pages (?disable=draw) stand batching down entirely, and the release- everything paths sweep every level subtree for stale 'batched' holds — a system can rebuild a batched node's meshes and orphan the tracked refs, and a stale hold is exactly what the GLB exporter would prune. Rich 2x: 188 nodes / 1,108 instances batched, in-page A/B frame 9.1 -> 7.4ms (-19%); rich 4x: 2,280 instances, 19.3 -> 14.1ms (-27%). Gates: e2e 8/8; bake tri+byte parity batching-on vs off; in-session pixel diff 0.4% confined to seam AA and transparent blend order. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK
|
I hit an error while handling your request (Model unavailable on AI Gateway free tier: Free tier users do not have access to this model. Upgrade to paid credits at https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%3Fmodal%3Dtop-up for unrestricted…). Please try again, rephrase, or reach out if it keeps failing. Error id: 9b34fc56-47be-4feb-acb6-7e2b2ee46476 |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK
… findings) A same-size add-and-remove slipped past the count tell and left the removed node's instances drawing as ghosts — membership now diffs ids against last frame's registry. Released geometries drop their packed mapping at refcount zero, so a rejoin re-packs current vertex content (a rebuilt geometry can reuse its uuid) instead of the copy captured at first join; the orphaned range stays until the existing overflow rebuild reclaims it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK
…ugbot round 2) The refcount fix traded one problem for another: dropping the packed mapping at zero made every hover release/rejoin cycle re-pack the same geometry, inflating used until premature overflow rebuilds. The mapping now survives cycles and carries a content stamp (position version + counts) — reuse is free, and a geometry rebuilt in place under the same uuid re-packs instead of instancing stale vertices. Whole-container frustumCulled goes off: its bounding sphere is computed at first cull, so instances joining farther out later could vanish with the whole batch — per-instance culling already owns visibility. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK
…date walk (Bugbot round 3) An item can host other items (a shelf's books) whose registered groups mount inside the host's group — the walk packed those meshes as the HOST's instances, freezing the child at join pose with no release of its own. The walk now cuts at every hosted child's registered group; hosted nodes keep drawing themselves (they are outside v1 batch scope by the level-parent rule anyway). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK
…ing (Bugbot round 4) - A hidden wall hides its openings through group visibility; batch instances hang off the level root and kept drawing them. Openings of an invisible wall are no longer candidates (the wall's visibility dirty-mark cascades the release). - ItemAnimation autoplays a GLB's first clip even without an interactive effect; such items batched and froze mid-motion. The renderer stamps clip presence on the registry group and candidates exclude it. - Below-threshold candidates were dropped after their wave, so copies placed more than a settle window apart never gathered into a batch. A new id now re-stales every unbatched node, letting the copy that crosses MIN_BATCH_ENTRIES pull earlier ones in. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK
…bot round 5) + import sort store.has locked a partially-joined node (some meshes under the new-batch threshold) out of every later wave. Partial members are now tracked; a new placement releases them for a full re-collect, so the copy that makes a leftover bucket viable pulls them in. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 393a494. Configure here.
Live overrides now release an already-batched node (a collaborator's remote drag or a programmatic move carries no local selection to tint it) and defer, rather than drop, a stale one — the commit mark re-offers it. Threshold-short candidates land in a leftover set that re-offers as a group when a NEW leftover arrives, so a copy that was deferred (selected, dirty, loading) while its peers' wave ran can still gather them into a batch; a stable leftover set re-offers nothing and small scenes stay quiet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Metric (charter backlog 3a+3b): idle frame CPU across the fixture ladder. Measured: −26% rich 2× (10.7→7.8ms), −37% Maxi 8× (21.1→13.2ms), −8..−14% everywhere else; in-page noise-cancelled A/B: rich 2× −19%, rich 4× −27%.
What
The frame's bottleneck is CPU draw submission (~3µs per mesh: pipeline/bind/uniform setup per object; the GPU idles at 1–2ms). Items, columns and wall-hosted openings now render through
THREE.BatchedMeshcontainers — one per(level, material, attribute-signature)— paying setup once per batch and ~0.3µs per instance, with per-instance frustum culling intact.Architecture mirrors the proven wall batch (
shared/node-batch/{types,store,candidates,system}):'batched'layer hold — picking, hover outlines, selection paint, measuring and GLB export are untouched by construction. Batch meshes are draw-only (noop raycast,pascalExport='strip').MIN_BATCH_ENTRIES— a scene of a few unique items creates zero batches and pays zero overhead (verified: 4-item scene, 0 batches, wave quiesces).?disable=draw) don't batch at all, and the release-everything paths sweep stale'batched'holds so exports can never lose geometry.?perfgains abatchmembership row (on WebGPU each batched instance still counts indrawCalls— the backend loopsdrawIndexedper visible instance — so the DRAW number cannot show the win; frame/encode ms is the metric).Evidence (charter
plans/performance/editor-scalable-scene-runtime.mdin private-editor)Gates
Trade-offs + the v1→v3 ladder (unmount sources / indirect multi-draw) and follow-ups (ceilings+slabs coverage, wall-mode-aware wall batch for the rich-4× <12ms target) are documented in the charter's architecture notes.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Nq2rLE18tFVES2LU6HGokK
Note
Medium Risk
Large new render-path subsystem with many release paths (export, isolation, gestures); incorrect batch membership or stale
'batched'holds could cause missing geometry or frozen poses, though capture sweeps and invariants are explicitly designed to mitigate that.Overview
Adds a node batching pipeline (
shared/node-batch) that mirrors the wall batch but usesTHREE.BatchedMeshper(level, material, attribute-signature)so static items, columns, and wall-hosted doors/windows can draw through instanced containers after a settle window, while source meshes stay mounted and are only draw-hidden with the existing'batched'layer hold.NodeBatchSystemis mounted once from the item system bundle and orchestrates join/release from dirty marks, selection/hover tint, live overrides, door/window animations, appearance changes, isolation, andthumbnail:before-capture(plus no-op on?disable=draw). Candidates skip interactive/animated items, unsettled GLBs, transparent/array materials, and hosted child nodes; the item renderer now stampsitemHasAnimationswhen a GLB has clips.The
?perfoverlay gains a batch membership row viapublishPerfBatchStats, adrawCompositionprobe, optional?spike=batchBatchedMeshspike, andwindow.__itemBatchdebugging hooks.Reviewed by Cursor Bugbot for commit 1d7a548. Bugbot is set up for automated code reviews on this repo. Configure here.