33-scenes: Wizard's Tower + Market Town Square, the Tavern rebuilt; kit instancing for the Quest budget - #262
Merged
Merged
Conversation
…ile per long task) - scripts/scene-load-trace.cjs: a fresh phone context per scene (412x915 isMobile @2.6, real GPU, CPU throttle 6), the Templates open path and autosave -> reload -> Restore, each reporting wall time, PerformanceObserver long tasks, the longest rAF gap, the CPU profile's top self time, app call chains, and the three longest tasks broken down. - Baseline (c7018be): Castle Courtyard Restore = ONE 89 s task (91 s frozen), open 525 ms tasks / 2.1 s frozen; Tavern Restore 84 s; Forest gets NO Restore at all (its autosave is over the 50 MB cap). Table in the 33-scene-load handover. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ces warmed and placeheld - sceneLoader.js (leaf): slice(job) yields when the 10 ms budget is spent and throws LoadCancelled at a clean point; schedule(fn) runs a burst of continuations a slice at a time; ONE load job (sceneLoad store) - a new load supersedes the running one; cancel hooks; throttledPoke (250 ms) for the build loop. - sessions.applySession: the object loop is time-sliced with progress, kit stubs refill at once (counted when they land), every await re-checks the job, Cancel clears what the load added (replicated like the load), a load interrupting one mid-build writes no "Backup before" of the half scene. - packRefs: a refill's clone/attach runs through schedule() (every copy of a piece used to attach in the one task that resolved its template); warmTemplate uploads textures one per slice and compileAsync's the programs against the live scene before any copy draws; refills poke at most every 200 ms; fingerprint checksums cached per geometry by buffer versions; packRef.box (root-frame bounds, additive) drawn as ONE scene-root InstancedMesh of grey placeholder blocks while a piece is on its way. - App.svelte debug hook: + sceneLoader (220/220/220). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s", Cancel - SceneLoadBar.svelte in the toast stack's first slot (the spectator banner's spot, so arriving toasts never shove it): name, count, a progress bar (indeterminate while preparing/reading), Cancel while objects are being BUILT; once only kit models are still arriving it keeps counting without Cancel. Non-modal, shown after 250 ms so a small scene does not flash it. Ids #scene-load-bar (data-done/total/phase), #scene-load-cancel. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ubs, sliced restore
- autosave writes pristine kit pieces as STUBS (packRefs.parkPackPieces: hollowed for the
GLTF export and put back on the exporter's afterParse hook, before the async encode can
let a frame draw a hollow castle). Castle Courtyard's snapshot 51 MB -> 86 KB; Forest
Clearing gets crash recovery again (it was over the 50 MB cap).
- applyRestore: time-sliced with progress ("Restoring your last session"), the wire copy
built only when a peer is connected (stubs for pristine pieces) - every child's toJSON
re-encoded its textures as PNG data URLs for nobody, the 89 s task; Cancel clears and
re-offers the prompt; superseded by another scene cleanly.
- The debounced autosave waits out a load (saveNow does not).
- tests/e2e/scene-load.test.cjs (CPU x6 phone): open + restore with no long task > 200 ms,
the bar counts, UI answers mid-load, supersede, cancel, stub snapshot, edited piece kept.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…orce (WIP: e2e pending) - P1 contract: a pack item row's `lods` place as the object's LOD group (userData.lod, normalizeLodGroup in lodGroupCore.js is the one boundary); levels are a RENDER-TIME per-mesh geometry swap matched by node name inside lod.js's bracket (registerLodPass), drawn with LOD0's own material; lazy (built when first needed, once per file); thresholds by screen size (25% / 10% defaults, 15% hysteresis); implicit group from the pack row for pieces placed before their pack had lods - write path lodGroupActions.js: one props undo entry + one objectParameters 'lod' message (applier in commandsHandler, wireValidate shape, props kind in objectActions) - P2 LodGroupPanel.svelte in Inspector: level bar with draggable edges (one entry per drag), Force LOD, Generate levels (meshopt in the worker), replace a level with an object / Explorer drop, select-a-level preview, per-level material override, gizmo on a level (lodLevelEdit.js + Scene isLodLevelProxy branches), Show LOD level overlay, cull; api.lod handle gains force()/levels() - P3 VR props panel LOD row (readout + cycle Force LOD) - docs: PACKS.md lods field, MODULES.md - vitest lodGroupCore (22) green; svelte-check 333/47 = baseline - e2e lod-group: 22/24 on the first run, both reds fixed (test premises + block writes now take effect the same frame); re-run, lod-panel and vr-props-panel NOT yet run (slots/memory) - see the handover Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…op), no autoplay
- behaviorCore.js (pure, vitest 18): normalizeBehavior, the pose as a pure function of
{on, at, from, n} + the session clock, nextState, frameSlabs/passageThrough (the doorway
cut out of a one-piece frame, through the wall's depth)
- packBehavior.js: per-frame pose driver (rest pose in Edit whatever the shared state),
a core click handler in moduleSDK's one dispatch (desktop Interact click, Play tap, VR
laser/poke), proximity (1.5 m) and knock (VR hand >= 0.6 m/s) triggers, the `behavior`
wire message (latest-wins on sessionNow), a kinematic box per moving node that follows
the swing while a sim runs + the frame as custom slab colliders, a local Animation-panel
preview (never sent/saved), the per-item sound
- animatedImports: NOTHING AUTOPLAYS on placement any more (the first clip used to start
and loop); behavior roots skip the transport; spec (+ live state on the wire, + the
lod-editor's userData.lod) ride objectfile, the save snapshot and the undo entry
- fileHandler: animated imports land at the drop point; opts.behavior from the pack row
- packs/Explorer/explorerDrop: the row's spec travels to placement; an "animated" badge on
cards and list rows
- gameSfx: + door gate slide lever lid; wireValidate `behavior` shape; ROOM_SCOPED;
vrGameInput treats behavior roots as clickable; api.behavior {list, state, trigger}
- suites: pack-behavior (new; test-door.glb built with gltf-transform), animated-models and
animation-persist press play first now, game-audio expects 25 sounds
WIP at the budget checkpoint: pack-behavior ran 41 pass / 8 fail before the priming fix in
this commit. 7.2 (the first walk resolved on the plane tier: charController's lazy physics
import; primed now, not re-run). 7.6 (the reopen click on the frame post did not toggle)
cascades into 8.2/9.1/11.2/13.1/13.2 - next window's first job. Counterfactuals already
green: 7.7 (uncut frame = a wall when open), 2.3/12.1 (no autoplay), 14.1 (slow hand).
svelte-check 333/47 (= base), vitest 396/396, check-storage clean.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…behavior - PACKS.md: the behavior contract (types, triggers, the no-autoplay rule, shared state, frame + moving parts + collider follow, authoring notes, the Explorer badge) - MODULES.md: api.behavior list/state/trigger and the five item sounds Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…oser programs, sky first - Controls.svelte: under the virtualisation threshold the object tree MOUNTS IN CHUNKS (40, then 16 per frame) — a load handed it ~180 rows in one poke, the longest task left (434-565 ms on a CPU x6 phone). svelte-check 333 -> 332 (the inline filter callback was a counted error). - packRefs.warmTemplate compiles BOTH program variants: the canvas one and the render-target one the post-processing composer draws with (three keys a program by tone mapping + output colour space) — the composer's programs were linking inside the first frame (260 ms). - sessions/autosave: the scene's environment is restored BEFORE the objects, so pieces warming as their packs land compile for this scene's lights, and the right sky shows at once. - Cancel stays offered while kit models are still arriving; the bar reads "n / N objects" in both phases. - scene-load suite: a fresh page per measured load (parsed pack files are cached in memory, so a same-page re-open was instant), pack files routed with a staggered 0.6-2.7 s delay, store-sampled count-up, Cancel hit-tested, the menu timed against an idle reference. - scene-load-trace: PACK_DELAY=1 for the same delay. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…gh the loader - lod-real-packs (new, skips without LOD_PACKS_DIR): 33-pack-fix-lod's real level files (architecture-kit Gate + WallStone, default Duck with its camera node + parent scale, cube_diorama Cat with 3 meshes) each build as name-matched SWAPS covering every mesh and draw fewer triangles far away (Gate 5440 -> 1080, Duck 8424 -> 1684, Cat 20108 -> 4014) - lod-panel 7.3 was a check that could not fail (it compared a count with itself): it now counts lod messages before/after the overlay toggles - lodGroupStats levels carry `paired` + `error` - held battery on this head: lod-group 24/24, lod-panel 49/49, vr-props-panel, lod (31), inspector, undo, lod-real-packs all ALL PASS; vitest 400; svelte-check 333/47 = baseline; npm run build green (dev server stopped) - counterfactuals: hysteresis (vitest: a 100-frame bob on an edge switches once; band 0 pops back at once); a block write taking effect the same frame (lod-group 4.1 was red before 24fe36b's same-frame re-read); shared materials (1.8: the mesh wears its own material mid-draw; 5.2 shows the override path is a copy) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…d frame hold - packRefs.warmPrograms(root): one compile per mesh per slice, canvas AND render-target variants, then each program's FIRST USE (getUniforms) one per slice — measured: the first frames created no program the warm-up had not, the cost was three's first-use link WAIT (no parallel-compile extension) plus the environment change re-keying every existing material. warmTemplate uses it. - sceneLoader.holdFrames()/releaseFrames()/framesHeld() (bounded by HOLD_MAX_MS = 2 s): Outline.svelte skips its render while a scene load re-links programs and builds — the canvas keeps its last frame, the DOM (bar, menus) carries on. sessions + autosave hold from the environment restore until the build is in and warmed. - SceneLoadBar enters with a CSS animation, not svelte's fly (getComputedStyle on mount forced a whole-document layout inside the load's longest task). - Suite (CPU x6 phone, slow pack links): open max 220 ms (from 525), restore max 220 ms (from 89 468); menu opens in 102 ms mid-load vs 24 ms idle. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…empty frames compile it - sessions/autosave: environment AND post stack restored before the objects, then nextFrames(2): the composite shader and the helpers re-keyed by the new fog/lights compile on a frame with almost nothing to draw, not on the first frame of the full level. Then the hold + sliced warm. - sceneLoader.nextFrames(n) (rAF, 100 ms fallback where rAF does not run). - Suite: restore max 166 ms (PASS, from 89 468); open max 204 ms (from 525). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… loaded toast waits - sessions/autosave: environment -> two frames -> post stack -> two frames: each re-keys programs on its own near-empty frame (measured ~60 + ~40 ms of links together). - qualityGovernor ignores frames while a scene load runs (desktop and XR): it answered a load's slow frames by dropping the pixel ratio, and that setSize was the longest task of a phone Restore (432 ms at CPU x6, tavern). - 'Session loaded' waits for the kit models (and says nothing for a cancelled/superseded load): its fly entrance forced a whole-document layout mid-load. - CLAUDE.md: the sceneLoader architecture entry. - Suite (CPU x6 phone, slow links): open max 197 ms, restore max 148 ms, menu 66 ms mid-load (26 idle) — 28/28. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…t play schedules the clip
- physics: with nothing dynamic, a sim start simulated the (sticky) selectedObject — the
door just placed. It fell over and every Interact click on it became a carry (the
pack-behavior 7.x flake). Functional items (userData.behavior) are skipped; a door-only
scene now gets the existing "Nothing to simulate" toast.
Counterfactual: pack-behavior 7.0a (the door dynamic) and 7.3/7.7 go red without it.
- animatedImports: with registration no longer autoplaying, nothing scheduled the action
when `playing` later turned true (a press, or a restored playing save) — the state said
playing and nothing moved. setAnimationState schedules it on the first play.
Counterfactual: animated-models "clip animates on A/B", animation-persist "animates
before saving / after the round trip" go red without it.
- packBehaviorDebug grows click diagnostics (clicks, lastClick {mesh, result}).
- suites: pack-behavior primes the walker, adds a real dynamic crate + 7.0a/7.0b, samples
the preview window (10.1); animation-persist's panel section starts from a playing clip.
Battery (vite inside e2e-slot, PEER_CONFIG): pack-behavior 51/0, animated-models 15/0,
animation-persist 23/0, game-audio 26/0, packs-explorer 10/0. svelte-check 333/47 (= base),
vitest 396/396, check-storage clean, npm run build green.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… checksum cache - sessions/autosave: environment -> hold + sliced warmPrograms of the (empty) scene -> post stack -> release + two frames for the composer's own shaders (compile() cannot reach them) -> hold for the build. The env re-key no longer links inside a frame. - packRefs: the checksum cache keyed by buffer versions is GONE — pack-refs caught an in-place vertex move without needsUpdate reading as pristine, which would save an edited piece as a stub (the edit lost on reload). Counterfactual = that pack-refs check, red with the cache, green without. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…oader.within) templates-modal (software GL) went red after the whole-scene warm became awaited before the build: a software link is hundreds of ms per program, so the scene stayed empty past the check's wait. The warm carries on in the background; the load never waits on the driver. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… ordinary-objects fixture - Outline.svelte registers warmComposerPasses through sceneLoader.registerComposerWarm: every pass (and each effect's internal passes) compiled against the target it draws into, one per slice, then first use per slice. The load paths call it (bounded by within) while the viewport is held, after restoring the post stack. - scene-load section 7 builds 1200 ordinary meshes (300 measured 166 ms even unsliced — too small to prove the slicing guard). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ompiles in batches of twins - sceneLoader.pump keeps the SHARED slice clock: a loop awaiting one scheduled item at a time started a fresh pump per item through microtasks, and the whole loop ran as ONE task — measured 881 ms of program warm-up on a 1200-mesh scene, and the source of several earlier residual tasks inside pump. - packRefs.warmPrograms: compile(object, camera, scene) walks the whole scene for lights per call, so per-mesh compiles were quadratic. Plain meshes compile in batches of 48 throwaway twins sharing their geometry + material (which prepares the real material); skinned / instanced / other drawables compile as themselves. - scene-load-trace: SLUGS=many-boxes (MANY=1200) — a synthetic scene of ordinary meshes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…pheres) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…iece in one draw call The 1.18 levels drew 320-394 calls per frame in the headset analogue (Tavern 394 at the spawn, Castle 371) against the Quest budget of 150, while their 224/210 meshes come from only 45/44 distinct pack meshes. A kit level IS a few pieces repeated. - kitInstancing.js: a render-time pass (lod.js registerBatchPass, AFTER the level swaps) groups eligible meshes by (the geometry they draw, the template material, a 16 m column centred on the origin, the shadow flags), hides the members for that render call and draws one scene-root InstancedMesh per group of two or more. The tree, picking, physics, the mesh tools and every serializer never see it (the lod.js rule). - Eligible = a refilled pack piece still pristine where it shows: its own geometry (unedited), its own material, not transparent, material signature equal to its template's. Selected pieces, pieces another pass hid or re-materialled, hidden roots are drawn as themselves. - packRefs.instanceOf records which template mesh each copy was cloned from (kitMeshSource, a WeakMap). - kitInstancingCore.js (pure): the column key, the material signature, the rules. - Settings > Performance "Draw repeated kit pieces together" (LOCAL, default on). - scripts/perf-levels.cjs + level-views.cjs: the draw-call probe for the General-tab levels from named viewpoints (desktop look, and the headset analogue: post off, shadows off as the XR governor's entry floor does). Measured (perf-levels, headset analogue, 1.18 levels authored on this union): Tavern spawn 394 -> 70, Castle spawn 371 -> 75, worst view 83. Tests: vitest kitInstancingCore 12; e2e kit-instancing ALL PASS 14 (fewer calls, same picture, picking, stubs saved, selected/recoloured/hidden/moved/cleared). Counterfactuals: signature check removed -> recoloured wall draws template colour (2 red); root-visibility check removed -> hidden pieces still drawn (premise + hidden red). svelte-check 332/47 (= union), vitest 430, check-storage clean. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…thor script places them
A door from the interactive kit is an animated import (its mixer binds the parsed tree, so
it cannot be packRefs' hollow stub), and a save carried its whole GLB: 0.5-1.4 MB of base64
per door in every .tpscene and every autosave. Measured: two DoorWoods = 1,320,948 B.
- animatedImports: an animated import that IS a pack file saves `animRef = {pack, item,
path}` (PACKS_BASE-relative, the kit-stub rule) instead of `bytes`; the restore fetches
the file once per url and parses it as before; the objectfile message, the undo entry and
applyObjectFile carry the reference so a peer's or an undone copy re-saves small too. An
entry with bytes restores and re-saves exactly as before; the wire still sends bytes.
- fileHandler.importFile stamps `userData.animRef` from the drop's packRef for an animated
file (it used to drop the reference). The Explorer drop already passes it.
- author-templates: a `kit` object whose pack row carries a `behavior` (contract P2) is
placed through importFile - the Explorer drop's own call - with the row's behavior and
LOD group, rotation and scale applied after.
Tests: e2e anim-pack-refs ALL PASS 11 (two doors = 1,204 B, PACKS_BASE-relative, behavior
+ LOD kept, restored from the pack, ONE fetch for two copies, re-save stays a reference,
an entry with bytes restores as before). Counterfactual: the fileHandler stamp removed ->
6 red (1,320,948 B saved). svelte-check 332/47.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A batch is culled as one object, so an off-screen member was drawn for every on-screen member of its batch. The pass now leaves every member outside the camera frustum out of its batch (three culls it as an ordinary mesh, no call), which keeps triangles where the frustum puts them - and makes wide columns free, so CELL_METRES goes 32 -> 128. Measured (perf-levels, headset analogue, the 33 levels' worst views): Tavern 460k -> 327k triangles, Wizard's Tower 375k -> 272k, Market 302k -> 237k; calls 120 -> 112 (Tavern back corner) with the wider columns. perf-levels gains --cell to try column sizes. Tests: kit-instancing +1 check (members 12 in the full view -> 5 looking at the west end). Counterfactual: the frustum test removed -> that check red (12 -> 12). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e top of its frame Two pre-existing collider faults the new scenes' doors walked into: - charController: rapier's KinematicCharacterController swept with NO query filter, so every sensor - the grass, flowers, rugs and water the 30c levels mark pass-through, a trim, a trigger volume - was a wall in Play. Found at the Tavern's open front door: a pass-through wainscot trim held the walker at the threshold (z 4.22). Both sweeps now pass QueryFilterFlags.EXCLUDE_SENSORS. - behaviorCore (33 P2 frame colliders): a LOW leaf in a TALL frame (the town kit's garden gate, ~1 m of picket under a 1.96 m frame) left the frame box above the leaf as one solid slab across the gateway, so the open gate still stopped the walker. A vertical passage now runs to the top of its frame box when more than 0.4 m of frame stands over the leaf (a door fills its frame and keeps its lintel); and a leftover thinner than 2 cm (the gate's static iron straps, 1 cm proud of the leaf) is dropped instead of becoming a 1.76 m wide invisible wall. Tests: e2e walker-sensors (walks through a sensor wall, stopped by a solid one); vitest behaviorCore +2 (the measured FenceGate boxes; the straps). Counterfactuals: the filter flag removed -> walker-sensors red (stopped at z -2.53); each behaviorCore rule removed -> its test red. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…treet; Wizard's Tower and Market Town Square scripts/level-templates.cjs (the General-tab levels, authored by author-templates): - TAVERN INTERIOR (the architecture shell, updated): the front door and the kitchen door OPEN (interactive-kit DoorWood / DoorStudded, frames included), the street window's shutters close, the loft chest and drawers open; furnished from the Interiors kit (bar counters + back-bars under the balcony, a stone fireplace, a tavern table set, a dining set, a chandelier on a beam, a kitchen run, a double bed and a wardrobe, teal wainscot); a slate gable roof (the hall is open to it); outside a Town & Market street (sidewalks, curbs, a crossing, lamp posts, a bench, barrels, a cart) and two cottages across it. You arrive on the road facing the front door. 32 KB. - WIZARD'S TOWER (new): a three-storey 8 x 8 m stone tower at dusk - an alchemy lab (trapdoor, chest, lever), a study (rune rug, lectern, desk, armillary, drawers, shutters), a bedchamber (wardrobe that opens) and a roof terrace with a telescope; a garden gate and the tower door open; an ambient banner stirs above the door. Hero props: arcane-kit (packs feat/33-arcane-kit). 24 KB. - MARKET TOWN SQUARE (new): a 16 x 16 m paved square - fountain, three stalls with goods, a well, benches, lamps, banner poles, a clock tower, a road with a cart, cottages round it - and a bakery you walk into (its door and shutters open). 28 KB. - a shared cottage() and street() helper; every building on the kit grid. Quest budget (draw calls per frame, headset analogue, every named view in scripts/level-views.cjs): Tavern worst 135, Tower 136, Market 98 (was 394 for the 1.18 Tavern). Tests: e2e level-templates-33 ALL PASS 67 - General-tab card load, every kit piece refilled and every functional piece registered, nothing moves in Edit, a REAL click opens and shuts a door in Interact (leaf 0 -> 95 deg), the ambient banner runs in Interact, Play spawn + sim + a readable frame, each door STOPS the walker shut and lets it THROUGH open, stairs to every storey, <= 150 calls from every view. Its earlier red runs found the sensor and gate faults fixed in the previous commit and the Market's missing dynamic bodies (no sim on Play). levelTemplates unit: five levels, all on the kit grid, every item in the union packs. level-templates (30c) keeps the Castle and the Forest. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.
33-scenes (K5): two new General-tab levels, the Tavern rebuilt — and kit levels inside the Quest's 150 draw calls
Stacked on #259 (anim-core), #257 (lod-editor), #261 (scene-load) — merged into this branch with lod-editor's
documented resolutions (App tails union, packRefs union). Merge those three first; this PR's own diff is then the five
commits below. Needs packs #11 #8 #10 #9 and #12 (arcane-kit, this lane) for the levels themselves.
The levels (
scripts/level-templates.cjs, authored byauthor-templates)Interactive Kit), the street window's shutters close, the loft chest and drawers open; furnished from the Interiors kit
(bar + back-bars, fireplace, table sets, chandelier, kitchen run, bed, wardrobe, wainscot); a slate roof; outside a
Town & Market street with two cottages. You arrive on the road facing the door. 32 KB.
with a telescope; a garden gate and the tower door open; an ambient banner stirs in Interact. Hero props from arcane-kit. 24 KB.
into (door + shutters open). 28 KB.
Engine work the budget needed
kitInstancing.js+ purekitInstancingCore.js): every pristine copy of a pack piece draws as ONEInstancedMesh call, render-time only after the LOD swaps (lod.js
registerBatchPass) — the tree, picking, physics andevery serializer never see it. Selected / recoloured / edited / transparent / hidden pieces draw as themselves;
off-screen members stay out of their batch. Settings ▸ Performance toggle (LOCAL, default on).
Measured (
scripts/perf-levels.cjs, headset analogue, worst view): Tavern 309 → 137 calls on the same scene(the 1.18 Tavern was 394); Tower 277 → 136; Market 274 → 98.
animRef(its path) instead of 0.5–1.4 MB ofbase64 (two doors: 1,320,948 B → 1,204 B); restore fetches once per url. The wire still sends bytes.
query filter →
EXCLUDE_SENSORS; the 30c levels' grass/flowers/rugs too); a garden gate's frame slab above itslow leaf blocked the open gate (behaviorCore passage → frame top; <2 cm slivers dropped).
Tests (fresh server)
level-templates-33 66 (card load, nothing moves in Edit, a REAL click opens/shuts a door in Interact, doors stop the
walker shut and pass it open, stairs, ≤150 calls from every view) · kit-instancing 15 · anim-pack-refs 11 ·
walker-sensors 4 · pack-behavior 50 · char-controller 55 — ALL PASS. vitest 432; svelte-check 332/47 (= the union's);
check-storage clean; build green. Counterfactuals for every guard are in the commit bodies.
OWED on device: the three levels on a Quest 3 (fps, laser/poke on doors); the Tavern's heaviest views carry ~327k
triangles (planner target 300k) — the XR governor's LOD bias is the lever there.
Evidence:
lanes-30/after-33/33-scenes/(play / interact / per-view frames, perf tables). Forks:QUESTIONS-33-scenes.md.🤖 Generated with Claude Code