[feat] 33 L1: scenes load without freezing the window, with visible progress - #261
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>
…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>
…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>
… 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>
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 L1 — scenes load without freezing the window, with visible progress
The user, on a phone: pressing Restore after reloading with Castle Courtyard open hung the whole window, and opening other scenes froze it too.
Measured on a CPU ×6 phone (412×915 isMobile, real GPU,
scripts/scene-load-trace.cjs)Castle autosave snapshot: 51 MB → ~1 MB.
What changed
sceneLoader.js(a leaf): a time-sliced build (10 ms slices, one shared clock across loops and queued work), ONE load job at a time (a new scene supersedes a running load cleanly), Cancel hooks.SceneLoadBar.svelte, non-modal, in the toast stack's first slot): "Loading Castle Courtyard — 34 / 182 objects" / "Restoring your last session — …", Cancel while building and while models arrive.toJSON()of every child, textures as PNG data URLs, for nobody); sliced with progress; Cancel clears and re-offers the prompt.afterParsehook). An edited piece is still written in full.packRefs.warmPrograms(batched twins, canvas + render-target variants, each program's first use off-frame), and the composer's pass shaders through a seam Outline registers; warm-ups bounded so a slow driver never stalls a load.packRef.box).Verification
scene-load(CPU ×6 phone, pack files on a 0.6–2.7 s link): the bar counts up; no long task > 200 ms on open, on Restore (via the real toast button), or building 1 000 ordinary meshes; the logo menu opens mid-load (66 ms vs 26 idle); another scene supersedes cleanly; Cancel takes the load back; the stub autosave; the edited piece survives. ALL PASS.Owed on a device: a real phone's feel (Restore + opening each level), the held viewport during a load as perceived on screen, the bar in non-dark themes.
🤖 Generated with Claude Code