SlicePrefetcher — projected t+1 prefetch during dimension playback.
While a dimension is PLAYING, refinement is suppressed (the progressive
loaders report hasMoreLODs === false under a frame budget), leaving an
idle window between a tick's commit and the next tick. This prefetcher
uses that window to build the NEXT timepoint's decoded ladder and store
it in the shared SliceCache under the t+1 view signature, so the next
real tick's restoreLadder hits instantly and its whole frame budget
goes to DEEPENING the prefix instead of rebuilding it. Being driven by
DimensionAnimationManager.peekNextValue, it is loop/bounce/backward
aware — in particular it warms the loop WRAP (t=max → t=min), which the
linear-extrapolation chunk prefetch cannot predict.
Why shadow loader instances
The foreground loaders CANNOT be reused for a concurrent prefetch: they
hold mutable per-instance state (_activeSignal read by the RangeLoader
signal-source and the L0 proxy, a REUSED accumulator whose returned
arrays the next load overwrites, loadedLODs/lastViewState in the
progressive wrappers). And SceneLoader.updateView is single-flight —
routing a prefetch through it would abort/supersede the foreground pass
and merge t+1 into the persistent view state (stuck-display hazard).
So each registered node gets a lazily-built SHADOW loader from the same
factory helpers: own accumulator, own signal, zero shared mutable state
with the foreground. The S-cache IS the handoff — the shadow's own
updateView does restore→deepen→store (it carries a frameBudgetMs, which
bounds how far one pass deepens ≈ one cold level and makes the progressive
loaders store the deepened prefix), and the foreground restores on the real
tick. Shadows are deliberately NOT monitor-connected (the factory is
side-effect-free by design), so shadow loads never double-count metrics.
Persisting across ticks (background deepening)
A cold LOD level takes longer than one playback frame to decode, so the
shadow deepen must NOT be aborted every tick — the foreground therefore
does NOT preempt it. Instead prefetch is a no-op while a batch is
still in flight (see inFlight): the running deepen completes and
caches its level, and the next FREE tick re-targets at the then-current
predicted view. Across playback loops this fills the SliceCache toward full
ladders, so cached quality climbs while the foreground stays responsive.
This is safe because the shadow runs on its own loader instances (own
accumulator + signal), decodes on the worker pool, and only writes the
shared SliceCache under content-keyed entries — it cannot stall or corrupt
a foreground tick. prefetch() is fire-and-forget (never awaited; every
rejection, including expected AbortErrors, is swallowed); its fetches share
the global 24-slot data fetch lane and are bounded by the budget + abort.
abortInFlight() / releaseShadows() (playback end) / dispose() tear it
down.
SlicePrefetcher — projected t+1 prefetch during dimension playback.
While a dimension is PLAYING, refinement is suppressed (the progressive loaders report
hasMoreLODs === falseunder a frame budget), leaving an idle window between a tick's commit and the next tick. This prefetcher uses that window to build the NEXT timepoint's decoded ladder and store it in the shared SliceCache under the t+1 view signature, so the next real tick'srestoreLadderhits instantly and its whole frame budget goes to DEEPENING the prefix instead of rebuilding it. Being driven byDimensionAnimationManager.peekNextValue, it is loop/bounce/backward aware — in particular it warms the loop WRAP (t=max → t=min), which the linear-extrapolation chunk prefetch cannot predict.Why shadow loader instances
The foreground loaders CANNOT be reused for a concurrent prefetch: they hold mutable per-instance state (
_activeSignalread by the RangeLoader signal-source and the L0 proxy, a REUSED accumulator whose returned arrays the next load overwrites,loadedLODs/lastViewStatein the progressive wrappers). AndSceneLoader.updateViewis single-flight — routing a prefetch through it would abort/supersede the foreground pass and merge t+1 into the persistent view state (stuck-display hazard).So each registered node gets a lazily-built SHADOW loader from the same factory helpers: own accumulator, own signal, zero shared mutable state with the foreground. The S-cache IS the handoff — the shadow's own
updateViewdoes restore→deepen→store (it carries aframeBudgetMs, which bounds how far one pass deepens ≈ one cold level and makes the progressive loaders store the deepened prefix), and the foreground restores on the real tick. Shadows are deliberately NOT monitor-connected (the factory is side-effect-free by design), so shadow loads never double-count metrics.Persisting across ticks (background deepening)
A cold LOD level takes longer than one playback frame to decode, so the shadow deepen must NOT be aborted every tick — the foreground therefore does NOT preempt it. Instead prefetch is a no-op while a batch is still in flight (see inFlight): the running deepen completes and caches its level, and the next FREE tick re-targets at the then-current predicted view. Across playback loops this fills the SliceCache toward full ladders, so cached quality climbs while the foreground stays responsive.
This is safe because the shadow runs on its own loader instances (own accumulator + signal), decodes on the worker pool, and only writes the shared SliceCache under content-keyed entries — it cannot stall or corrupt a foreground tick.
prefetch()is fire-and-forget (never awaited; every rejection, including expected AbortErrors, is swallowed); its fetches share the global 24-slot data fetch lane and are bounded by the budget + abort.abortInFlight()/releaseShadows()(playback end) /dispose()tear it down.