WebXR World Loading
Switching worlds in a headset is a small systems problem. The next scene may need to build geometry, load a texture and compile shaders, while the old scene is still the visitor's visual reference. Sparksbud's shared Journey renderer treats that interval as a state transition rather than replacing the frame with a blank canvas.
Show a loading cue before heavy construction, keep the current XR pose rendering, and guard asynchronous work against a later selection. Sparksbud prepares the new scene, then swaps it into one renderer. Visited worlds stay cached for quick returns during the session; teardown disposes the renderer, controls and scene resources. Cache growth and actual headset memory still need measurement.
One renderer, several world states
The public 360 worlds use a shared Three.js renderer. Each world factory returns a root group, update function and optional preparation hooks. Journey places that root in a scene and remembers it by world id. A first visit builds the scene; a later visit can reuse the cached one. The renderer, XR session and dock belong to Journey rather than to each world.
That split matters because a menu can stay in front of the visitor while the art behind it changes. On an XR switch, Sparksbud leaves the current surround world drawing and puts a loading state on the headset dock. It waits for a paint before starting synchronous geometry work. Once the next scene is prepared and compiled, it moves the camera rig and dock to the new scene and paints again before calling it ready.
| State | Visitor sees | Developer should check |
|---|---|---|
| Current | Existing scene and working dock | Pose and Exit VR still update |
| Preparing | Existing scene with loading cue | Heavy work starts after a painted cue |
| Ready | New scene after its first paint | No blank or stale world appears |
| Cached return | Revisited scene without a rebuild | Correct starting view and controls |
| Teardown | Page leaves the Journey renderer | Owned GPU resources and listeners release |
The stale-request trap
Suppose a visitor picks High Camp, then Aurorafy before High Camp finishes preparing. An asynchronous promise can resolve out of order. Sparksbud increments a loadRevision for each selection and checks it after waits and after the build promise. An older result may finish constructing and enter the bounded world cache, but it cannot replace the later choice on screen. That distinction is subtle: ignoring a stale display transition does not necessarily cancel the underlying construction.
The code also keeps one pending promise per world, so repeated taps on the same world do not start duplicate builds. If a build fails, that pending entry is removed and the dock shows an error with a reload path. A loading test should exercise success, failure, fast repeated selection and a page exit during preparation.
In a test, choose High Camp and then Aurorafy before the first load finishes. The last choice should stay on screen even if the earlier build finishes afterward.
Cache or dispose?
Sparksbud keeps visited public worlds in a session cache. That makes return trips faster and avoids rebuilding the same geometry and shaders. It also retains memory for each visited scene until the Journey object is disposed. The number of public worlds bounds how many entries can be added, but “bounded” does not mean cheap. A long session that visits many large worlds still deserves a physical-device memory check.
When Journey ends, it stops the animation loop, requests XR exit, removes its resize listener and disposes every cached scene, the headset controls, postprocessing passes and the renderer. The scene helper collects geometry and materials, then disposes textures found directly on those materials. Three.js's disposal guide explains why removing an object from the scene is not the same as releasing its GPU resources.
There is a concrete gap in this build: Journey does not call a world-specific dispose hook. The High Camp and Link Tower vista helpers expose dispose() methods for off-scene render targets, but their world factories do not pass those hooks through to Journey. We need to wire that ownership chain and check a repeated visit and exit route before claiming complete GPU cleanup.
Do not read renderer.info.memory as a perfect leak detector. Three.js notes that some internal allocations can remain after scene cleanup and be reused later. What matters is whether counts and device memory keep growing across the same repeatable route, and whether every resource has a clear owner.
| Resource | Likely owner | Release check |
|---|---|---|
| Scene geometry and materials | World or shared scene helper | Disposed at Journey teardown |
| Material textures and off-scene targets | World and shared scene helper | Direct maps released; custom targets need an explicit hook |
| XR dock canvases and meshes | Shared controls | Released once with controls |
| Timers, audio and workers | Module that creates them | Its own dispose path stops them |
Compare resource counts after a return visit and after Journey ends. Don't call every retained renderer allocation a leak; trace the world-owned objects first.
Design tests around a route
Our most useful lifecycle test is not “open one world.” It is a route: open a world, switch to another, return to the first, then exit VR or leave the page. Check that the current view stays tracked during preparation, the loading cue appears before the slow work, a later choice wins, and a return uses the expected state. Record renderer calls and memory counts at the same checkpoints. A growth trend is a lead for investigation, not proof of a leak by itself.
The browser can verify scene ownership, finite geometry and the order of visible states. A Quest emulator can verify that both eyes and the dock survive a switch. Only a physical headset can settle whether loading stalls are comfortable, whether memory pressure rises over a long visit, or whether a different browser schedules the work differently. Our Quest performance note explains that evidence boundary.
A useful limit on preloading
Loading everything at startup would shorten later switches but make the first visit heavier. Sparksbud builds public worlds on first use and caches them after that. The choice fits a catalog in which visitors may only enter one or two worlds in a session. If real usage shows people switching through the whole set, the cache policy can be revisited with memory measurements. Until then, preloading more is an assumption, not an optimization.
Questions developers ask
Does a revision check cancel scene construction?
No. Sparksbud's revision check prevents an older request from becoming the visible scene. Construction may finish and the result may enter the session cache.
Why keep the old world visible during loading?
It preserves a visual reference and headset tracking while the new scene prepares. The dock can also show progress and an exit path.
Is a cached scene a memory leak?
No. A cache is deliberately retained for reuse. It still needs a size budget and a teardown path, especially on a headset.
Does removing a mesh free its GPU buffers?
No. Three.js requires disposal of geometry, materials and textures when their owner is done with them. Custom resources need their own cleanup.
Is renderer.info.memory zero after teardown?
It need not be. Three.js may retain internal reusable resources. Compare repeatable routes and ownership, then investigate sustained growth.


