Luxar Viewer API Documentation - v2026.10.19
    Preparing search index...

    Class SceneLoader

    Main scene loader that handles the complete loading pipeline.

    Features:

    • Spatial index-based loading for efficient nD queries
    • Hierarchical scene graph construction
    • Transform and rendering attribute inheritance
    • Dimension metadata management
    • Memory-efficient loading with proper caching

    Lifecycle: one-shot. A loader loads exactly one dataset: a second loadScene() — or one after dispose(), which is terminal — throws. Use SceneLoaderManager.createLoaderAsync() (the canonical entry point), which awaits the previous loader's teardown and constructs a fresh loader per dataset.

    Index
    isUpdateInProgress isLoadPassInProgress prefetchSlice activateAndWarmNextSlice releasePrefetchResources setLeafMaterializedListener onArchiveFault notifyArchiveFault reportArchiveFault invokeArchiveFaultListener getCacheStats listCachedDatasets clearL0Cache clearL1Cache clearL2Cache clearAllCaches getCacheBudgets applyEffectiveAttrs setRequestRender setDepthSortCoordinator setMaterialManager commitHost commitAndRequestRender setAutoRetryableFailureCallback setRefinementDensityProvider refinementHoldReason resumeDensityDeferredRefinement loadScene makeLoadSceneCtx deriveNodeViewState runLoaderUpdates sweepLoaders registeredLoaderPaths sweepActivatedParts requestReprocess resetRefinementFailures sweepLoaderEntries mergeViewState matchesRunningPass updateView runPassCommit progressiveLadderResidencies kickRefinementIfIdle scheduleProgressiveRefinement beginRefinementBudget refinementPhases noteRefinementOutcome scheduleAbandonedRungRetry abandonedLaddersStillOwedARound cancelAbandonedRungRetry clearCommittedDataStamp updateVisibleCountsInMonitor refreshVisibleCounts processLinesData withDatasetSignal commitLinesGeometry processGSplatsData commitGSplatsGeometry processMeshData commitMeshGeometry makeNodeBuildCtx factoryDeps connectLoaderToMonitor updatePointsGeometry processPointsData commitPointsGeometry initializeSceneDimensions isAtViewState normalizeURL showMonitor hideMonitor toggleMonitor getFailedLoaders hasNetworkFailureUnder getFailedLoadsProvider getMonitorFailedPaths clearArchiveFaultForRetry finishRetry hasFailures hasAutoRetryableFailures clearFailures retryFailedLoader makeRetryCtx retryAllFailedLoaders dispose
    _zarrStore: Readable | null = null
    cachingStore: MultiLevelCachingStore | null = null
    l0Cache: DecompressedChunkCache | null = null
    sliceCache: SliceCache | null = null
    cacheBudgets: CacheBudgets | null = null
    registry: LoaderRegistry = ...
    lineWorkingSetGate: LineWorkingSetGate
    refinementResidencyReporter: RefinementResidencyReporter = ...

    Scene-wide byte-ceiling record for progressive refinement, surfaced on the debug snapshot (#2508); lifetime contract on RefinementResidencyStop.

    DELIBERATELY NOT CLEARED BY dispose(): a loader loads one dataset and dispose() is terminal (a second loadScene() throws), so a new scene always gets a fresh loader and reporter through SceneLoaderManager.createLoaderAsync.

    refinementDensityGate: RefinementDensityGate | null = null
    lastResidencyBudget: RefinementResidencyBudget | null = null

    The last refinement run's residency budget, kept so the data monitor can tell a rung held at the residency ceiling from one still streaming (refinementHoldReason). Replaced at every run start.

    viewState: ViewState
    _committedViewState: ViewState | null = null

    See committedViewState; null until a load or pass commits.

    config: LoaderConfig
    rootGroup: Group<Object3DEventMap> | null = null
    monitor: SceneLoaderMonitorPort | null = null

    Optional monitor port — populated at construction by the factory passed in via SceneLoaderManager.setMonitorFactory. Null when no UI is wired up (tests, embedders), in which case all monitor calls become no-ops at the call sites.

    arrayRefRegistry: ArrayRefRegistry
    _gpuBufferPool: GPUBufferPool | null = null
    nodeFactory: NodeFactory = ...
    profiler: UpdateProfiler | null = null
    _identityWatchdog: SceneIdentityWatchdog | null = null
    _archiveFault: ArchiveFaultError | null = null
    archiveFaultListeners: Set<(error: ArchiveFaultError) => void> = ...
    _lastUpdateWasFrameBudgeted: boolean = false
    _updateVersion: number = 0

    Monotonic view-generation counter, read by the LOD registry as currentViewVersion and stamped onto every committed leaf as userData.loadedViewVersion (freshness is EXACT equality — see scene/lod-freshness.ts).

    CONTRACT: it bumps ONLY when a query-determinant of the view changes (viewStatesEqual: displayDims / slicePosition / tolerance / the dimensions query signature). A pass whose merged view state equals the current one — a partition resync, a retry re-run, a depth-sort re-commit — re-sweeps under the SAME version. This matters because lazy lod_group levels are NOT in the sweep and are never re-stamped by it: bumping on an unchanged view invalidated every resident fine level scene-wide, so a partition part crossing the screen edge dropped ALL LOD groups to coarse and re-streamed them (the #2366 regression on the h2afva 44-part scene).

    _passCount: number = 0

    Passes actually run (every updateView that reached the sweep). The per-geometry processors gate their first-update info logs on this being <= 1; it used to be _updateVersion, which now only advances on a view CHANGE and would keep those logs on forever for a static scene.

    _abandonedRetryTimer: Timeout | null = null
    _abandonedRetryRounds: Map<string, number> = ...
    _disposed: boolean = false
    _loadStarted: boolean = false

    Set by the first loadScene; a loader loads exactly one dataset.

    _sceneGraph: SceneNode | null = null
    _sceneNodeIndex: SceneNodeIndex | null = null

    path → node + world nD transform index of _sceneGraph, built once with it (the graph never changes afterwards). Every per-node derivation and prefetch lookup reads it instead of walking the graph.

    _leafMaterializedListener: LeafMaterializedListener | null = null
    viewStateQueue: ViewStateQueue = ...

    View-state queue: the pending slot (whose lifecycle passes drives) and the per-loader previous view-state map used by predictive prefetch. See ./scene-loader/view-state/view-state-queue.ts.

    The pending-state slot is overwritten on every queued update, so a burst of view changes during an in-flight retry collapses to a single drained call (latest-wins). The per-node prev-state map is cleared on dispose (the loader is one-shot, so there is no dataset switch to reset it for); skipped paths forget their snapshot so the next non-skip update re-baselines.

    _datasetAbortController: AbortController | null = null

    The dataset's AbortController. Created by the (one-shot) loadScene and aborted on dispose — which a dataset switch runs on this loader before creating a fresh one — so worker tasks it queued settle immediately instead of running to completion against a superseded scene. The loader threads it into its own worker projections (withDatasetSignal); the pool itself is shared by every host on the page and carries no dataset signal. WASM execution itself cannot be cancelled, but the orphan results are discarded — see WorkerAbortError.

    passes: PassScheduler = ...

    Serialization of view passes, refinement runs and retries: the lock, the pending slot's lifecycle, request generations and their waiters, the resync stash and the B5 drag commit guarantee. See scene-loader/update-view/pass-scheduler.ts.

    _slicePrefetcher: SlicePrefetcher | null = null

    Background t+1 slice prefetcher (dimension playback). Lazily created on the first prefetchSlice call; aborted at the top of every updateView (foreground always preempts); disposed with the loader. Its SHADOW loader instances share nothing mutable with the foreground loaders — the S-cache is the only handoff (see slice-prefetcher.ts).

    lodGroupRegistry: LODGroupRegistry | null

    Per-loader LOD-group registry. Constructed via the factory passed by SceneLoaderManager so the registry's camera / viewport / displayDims getters can close over the live SceneManager — which the data/ layer must not import directly. null when the host (e.g. headless tests) doesn't supply a factory; in that case the scene loader still loads lod_group nodes but the per-frame selector is a no-op (default level renders).

    _requestRender: ((drawn: boolean) => void) | null = null

    Optional render-loop wake-up, wired by SceneLoaderManager from the app pipeline (→ AnimationController.startAnimation). Fired after every geometry commit so late commits — progressive-refinement passes, failed-load retries, the online auto-retry, lazy LOD loads — repaint even when the rAF loop has idle-paused meanwhile. The callback is idempotent on the receiving side (startAnimation early-outs while animating and re-arms the idle timer), so per-node calls inside an atomic sweep are harmless. Null in bare/test loaders → no-op.

    drawn is false when the committed node was not drawn either side of the commit (itself or an ancestor hidden: an LOD level the registry keeps off screen, a hidden layer), so the frame cannot have changed and the receiver only keeps the loop ticking. It must still wake it: the LOD registry polls per frame and may now show the level that just committed, and that visibility flip requests its own redraw.

    decodeKTX2: KTX2TextureDecoder | null
    _depthSort: DepthSortCoordinator | null = null

    The host's depth-sort coordinator, which every commit reports to. Set by loadScene (data/zarr-loader.ts) before the scene loads, from the coordinator the caller owns — the LuxarApp's SceneManager.depthSort, or a LuxarLayer's own. null (tests, a bare loader) leaves the nodes untracked.

    _materials: MaterialManager | null = null

    The host's material manager, or null for the LuxarApp's. Installed by the host's SceneLoaderManager; node creation (the factory) and the commit stage both run against it, so a LuxarLayer's materials never join the app's registry (capabilities, camera broadcast, disposal).

    • get zarrStore(): Readable | null

      Public accessor for the zarr store (needed by LabelLoader).

      Returns Readable | null

    • get sceneNodeIndex(): SceneNodeIndex | null

      Path index over sceneGraph (path → node + world nD transform), built with it in setSceneGraph. Consumers outside the loader that derive per-node view states (the audio slab rule) read nD transforms from here in O(1) instead of walking the graph from the root on every derivation.

      Returns SceneNodeIndex | null

    • get hasCachingStore(): boolean

      True if the L1/L2 caching store is configured for this loader.

      Returns boolean

    • get gpuBufferPool(): GPUBufferPool | null

      The GPU buffer pool, or null when pooling is disabled. Exposed so the LOD-group registry's resident-byte query (getResidentBytes) can read the single VRAM truth; callers treat a disabled pool as 0 resident bytes, so it is never over budget and never evicts.

      Returns GPUBufferPool | null

    • get refinementResidencyStop(): RefinementResidencyStop | undefined

      This scene's progressive-refinement BYTE-ceiling stop, or undefined when refinement never declined a rung.

      The reporter is scene-scoped (one per loader) while the residency budget is rebuilt per refinement run, so this accumulates across runs — which is what makes it answerable at capture time, long after the run that stopped. Read by the debug snapshot through SceneLoaderManager.refinementResidencyStop().

      Returns RefinementResidencyStop | undefined

    • get currentViewVersion(): number

      Monotonic view-generation version (bumped by updateView only when the merged view state differs from the current one — see _updateVersion). The LOD registry reads this to decide whether a level's committed geometry is fresh for the CURRENT view — a mesh stamped with an older version (its data still reflects a previous slice/displayDims) is treated as stale so the registry can show a coarser fresh level until the re-slice commits.

      Returns number

    • get committedViewState(): ViewState

      The view the drawn geometry was last committed for (B4) — set when a load starts and by every committed pass. Before the first commit, the live view.

      Returns ViewState

    • Whether an updateView sweep (fetch/decode/upload) is currently in flight. Exposed for consumers that must treat loading-time frame jank as unrepresentative — e.g. the adaptive DPR manager suppresses probe learning while this is true. Covers the serialized update sweep, not late lazy-LOD commits (those surface as content-change notifications instead).

      Returns boolean

    • Whether a LOAD PASS is in flight. Three parts:

      1. a RUNNING sweep — an updateView pass (fetch / decode / upload) up to its geometry commit, or a failed-loader retry sweep, which takes the same lock;
      2. MINUS the progressive-LOD refinement drain, which inherits that same lock (see PassScheduler.refining);
      3. PLUS a sweep that is QUEUED but has not started yet.

      Together they answer "has the data for the view the user asked for arrived yet?".

      Refinement is excluded because it runs after the current view has already been committed, so folding it in would turn this into full-ladder latency instead of first-commit latency. That is the same distinction update-view/pass-scheduler.ts already draws where it resolves the pass waiters at refinement ENTRY rather than completion — the pacing gate there needs first-commit latency too.

      The queued slot counts because the refinement exclusion would otherwise open a hole big enough to drive a test through. The steady state on any laddered dataset right after a commit is "lock held, refinement live"; an updateView arriving then is parked in the pending slot by PassScheduler.request without touching either flag. The requested slice has not begun loading, yet both flags still describe the refinement that preceded it — so without this clause a poller would read idle and conclude the new slice had rendered. hasPending() is true across exactly that window: the slot is filled by the request and cleared by takePending() at the moment the next pass starts (the scheduler's frame yield before it re-enters updateView, or its release drain), so the flag cannot latch busy after a pass begins.

      Also outside its scope: the initial loadScene (which only touches the lock at its very end, to hand it to the post-load refinement kick) and lazy substitutive-LOD / deferred-partition ensureLoaded promotions, which run outside any updateView cycle and surface as content-change notifications instead.

      isUpdateInProgress keeps its existing, broader meaning ("the serialization lock is held, refinement included") for its existing consumers — the adaptive-DPR manager and core/app/init/pipeline.ts, both of which want to discount loading-time frame jank for as long as the loader is doing work of any kind.

      Returns boolean

    • Fire one background prefetch pass for the PREDICTED next view (t+1 during playback). Fire-and-forget: returns immediately. The shadow pass deliberately persists across foreground ticks (a cold level outlives one frame); it is aborted only on playback end / dispose / its stall guard, and a foreground pass for the same slice waits for it rather than re-assembling that slice (see slice-prefetcher.ts). The partial is merged onto a COPY of the current view state — never persisted (a prefetch must not move the real view; see the stuck-display hazard in slice-prefetcher.ts).

      Parameters

      • viewState: Partial<ViewState>
      • budgetMs: number
      • OptionalladderDepth: number | "auto"

      Returns void

    • Deferred partition parts the predicted slice needs (B4) are activated ahead of it, so its foreground pass finds them registered — and, once they are, a targeted shadow pass warms them: a shadow can only warm a REGISTERED node, so the batch prefetchSlice just started cannot reach them. Fire-and-forget: the activation never rejects.

      Parameters

      • predicted: ViewState
      • budgetMs: number
      • ladderDepth: number | "auto" | undefined

      Returns void

    • Release the prefetcher's shadow loaders (frees their accumulators). Called when playback ends; shadows rebuild lazily on the next play.

      Returns void

    • Connect the owner of per-leaf LIVE appearance state (the Layers panel) to every data leaf this loader attaches from now on — partition parts the LOD registry activates, lazily built levels, anything built after load. Each leaf is reported as its placeholder attaches, before any data reaches it, together with this loader's scene graph. null disconnects.

      Parameters

      Returns void

    • Subscribe to archive-fault episodes for this loader. An explicit retry clears the latch; if the archive fails again, listeners are notified again. Listener exceptions are logged and do not propagate to the caller.

      Parameters

      • listener: (error: ArchiveFaultError) => void
      • options: { replayCurrent?: boolean } = {}
        • OptionalreplayCurrent?: boolean

          Replay the current fault immediately when one is latched.

      Returns () => void

    • Clear the in-memory L0 decompressed-chunk cache. No-op if absent.

      Returns void

    • Clear the persistent L2 OPFS cache. No-op if absent.

      Returns Promise<void>

    • Clear ALL cache tiers (L0 + L1 + L2 + the decoded-slice S-cache).

      Returns Promise<void>

    • Resolved per-tier cache budgets from the last setupCaches run (source: heap / explicit / device-class / fixed), or null before the first scene load / after dispose. Surfaced for the Settings popover's budget readout.

      Returns CacheBudgets | null

    • Return a node-attrs record with rendering attributes replaced by the effective values composed along the scene-graph ancestry (root → leaf). Hierarchical composition: opacity/gamma/intensity multiply, offset adds, blending_mode uses the nearest ancestor's choice.

      If the scene graph is unavailable, falls back to the node's raw attrs.

      Parameters

      Returns {
          transform?: readonly number[];
          nd_transform?: NdTransformMap;
          opacity?: number;
          absorption?: number;
          gamma?: number;
          intensity?: number;
          offset?: number;
          blending_mode?: string;
          join?: string;
          max_radius?: number;
          n_points?: number;
          layer?: boolean;
          visible?: boolean;
          color_data_range?: [number, number];
          amplitude_data_range?: [number, number];
          scalar_data_range?: [number, number];
          colormap?: string;
          has_scalars?: boolean;
          extend_to_all?: string[];
          [key: string]: unknown;
      }

      • [key: string]: unknown

        Any additional attributes

      • Optionaltransform?: readonly number[]

        Transformation matrix (16 elements for a column-major 4x4 matrix). Typed as readonly number[] because zarr metadata is parsed dynamically; see import('../types/zarr').Matrix4x4 for the narrowed 16-tuple shape.

      • Optionalnd_transform?: NdTransformMap

        Per-dimension transforms for non-displayed dimensions

      • Optionalopacity?: number

        Rendering attributes

      • Optionalabsorption?: number
      • Optionalgamma?: number
      • Optionalintensity?: number
      • Optionaloffset?: number
      • Optionalblending_mode?: string
      • Optionaljoin?: string

        Lines-only join style at degree-2 polyline joints: 'none' | 'miter' (#790).

      • Optionalmax_radius?: number
      • Optionaln_points?: number
      • Optionallayer?: boolean

        Whether this node is exposed as a layer in the Layers panel

      • Optionalvisible?: boolean

        Initial visibility when the scene loads (default: true). Authoring-time only.

      • Optionalcolor_data_range?: [number, number]

        Min/max of color data, computed at encoding time

      • Optionalamplitude_data_range?: [number, number]

        Min/max of amplitude data (GSplats), computed at encoding time

      • Optionalscalar_data_range?: [number, number]

        Min/max of scalar data (Points/Lines), computed at encoding time

      • Optionalcolormap?: string

        Colormap name for scalar-to-color mapping (e.g., "viridis", "green", "custom")

      • Optionalhas_scalars?: boolean

        Whether this node has scalar values for colormap lookup

      • Optionalextend_to_all?: string[]

        Dimensions to extend visibility across (points visible at all values)

    • Install (or clear) the render-loop wake-up callback.

      Parameters

      • callback: ((drawn: boolean) => void) | null

      Returns void

    • Run a commit of the node at path, then wake the render loop (see _requestRender), saying whether the commit can have changed the drawn frame: the node was effectively visible before or after it. A node that cannot be resolved counts as drawn. Returns that verdict.

      Parameters

      • path: string
      • commit: () => void

      Returns boolean

    • Install (or clear) the notification used to arm online failure retries.

      Parameters

      • callback: (() => void) | null

      Returns void

    • Why this node's next rung is held back, if it is: 'density' when the density gate deferred it at the current framing, 'budget' when the last run's residency budget declined it, null otherwise (streaming, or nothing pending). Density first: a density refusal is also folded into the budget's declined set, and the camera-dependent reason is the actionable one.

      Parameters

      • path: string

      Returns RefinementHoldReason | null

    • Per-frame hook from the density walk: if any rung the gate deferred now fits (the camera moved in), re-kick refinement. Cheap when nothing is deferred (the common case). Returns how many paths resumed.

      Returns number

    • Load a complete scene from a Zarr store using chunk-based spatial indexing.

      Orchestrates the loading of hierarchical scene graphs, managing spatial indices, attribute inheritance, and dimension metadata. Supports both points and lines with automatic fallback for datasets without spatial ordering.

      The loading process:

      1. Opens Zarr store with optional two-level caching (L1 memory + L2 OPFS)
      2. Loads scene metadata and initializes dimensions
      3. Recursively constructs THREE.js scene graph from Zarr group hierarchy
      4. Creates spatial index loaders for efficient nD queries
      5. Connects loaders to data monitor for debugging

      Parameters

      Returns Promise<Group<Object3DEventMap>>

      Promise resolving to a THREE.Group containing the complete scene graph. The group's userData contains: - sceneDimensions: Dimension metadata if available - bounds: AABB of all points - nodeCount: Total number of leaf nodes

      If this loader already loaded a scene or was disposed (one-shot — see the class doc)

      If the Zarr store cannot be opened or is invalid

      If consolidated metadata (.zmetadata) is malformed

      If required arrays (positions) are missing from point nodes

      // Load a scene from HTTP URL
      const scene = await sceneLoader.loadScene('https://example.com/data.zarr');
      threeScene.add(scene);
      console.log(`Loaded ${scene.children.length} top-level nodes`);
      // Load with error handling
      try {
      const scene = await sceneLoader.loadScene(url);
      if (scene.children.length === 0) {
      console.warn('Scene is empty');
      }
      } catch (error) {
      console.error('Failed to load scene:', error);
      // Fallback to default visualization
      }
      // Access scene metadata after loading
      const scene = await sceneLoader.loadScene(url);
      const dims = scene.userData.sceneDimensions;
      if (dims) {
      console.log(`${dims.length}D dataset:`, dims.map(d => d.name).join(', '));
      }

      MultiLevelCachingStore for caching implementation

    • Derive a per-node view state from this.viewState, folding in extend_to_all tolerance overrides and the inverse nd_transform for the node's path.

      The single source of truth for query-state derivation: both the main update path and retryFailedLoader go through this method so a retry cannot load a different query region than a fresh update would. The pre-extraction retry skipped both adjustments, which could produce a "successful" retry rendering incorrect data.

      A fully-extended node (extend_to_all covers all non-displayed dims) is returned as a NORMAL { skip: false } node whose query is made slice-INVARIANT (extend-to-all tolerance sentinel + extended dims' slicePosition pinned to 0), so per-sweep re-queries hit the loader's same-view no-op. No extend_to_all skip is produced.

      Parameters

      • path: string

        Scene-graph path of the node, used to compose the world nd_transform from this node up to the root.

      • attrs: { extend_to_all?: string[] } | undefined

        Node attrs (only extend_to_all is read here).

      • opts: {
            applyPartialExtendTolerance: boolean;
            extendedToleranceCache?: Map<string, number[]>;
        }
        • applyPartialExtendTolerance: boolean

          When true, partial extend_to_all coverage triggers a tolerance override via getOrComputeExtendedTolerance. Points/GSplats: true. Lines: false because line bounds already encode their non-displayed spatial extent.

        • OptionalextendedToleranceCache?: Map<string, number[]>

          Optional cross-node cache for the partial-extend tolerance array. Main update path passes one cache per cycle; retry passes nothing (fresh).

      Returns { skip: false; viewState: ViewState }

    • Shared scaffolding for the per-geometry update loops. The Points / Lines / GSplats branches in updateView differ only in the type-specific work (deriveNodeViewState, call loader.updateView, post-process, setMetadata, return staged) — the surrounding try/catch with failedLoaders bookkeeping + retryCount tracking, profiler dispatch, and Promise.all are all identical and live here.

      Type Parameters

      • TLoader
      • TStaged

      Parameters

      • loaders: Map<string, TLoader>

        The map of (path → loader) for one geometry type.

      • loaderType: "Mesh" | "Points" | "Lines" | "GSplats"

        Human-readable type for profiler label + error log.

      • updateFn: (
            path: string,
            loader: TLoader,
            session: UpdateSession,
        ) => Promise<TStaged | null>

        Per-loader work; returns staged commit data or null when there's nothing to commit.

      • onArchiveFault: (fault: ArchiveFaultError) => void
      • OptionalresyncPaths: ReadonlySet<string>

      Returns Promise<{ staged: TStaged | null; session: UpdateSession }[]>

    • Every registered loader path, of every geometry type.

      Returns Set<string>

    • Re-run the current view state without blocking the caller. With paths (the partition parts that just re-entered the frustum) the sweep is restricted to loaders at/under them; otherwise every loader re-runs. Either way the view state is unchanged, so the view version is NOT bumped.

      Parameters

      • Optionalpaths: readonly string[]

      Returns void

    • Re-open exhausted progressive ladders after connectivity is restored.

      Returns boolean

    • The view half of a JOIN (#2943): the request names a slice (a dims-driven request, not a {} reprocess that exists to force a re-commit), neither it nor the running pass carries a playback directive (a budget-free refine must still re-run a budgeted pass), and it merges to exactly the view the running pass is loading. The scheduler adds that a pass is running, unaborted, with nothing newer queued.

      Parameters

      Returns boolean

    • Update all points and lines for a new view state.

      Uses serialized execution to prevent race conditions: only one update runs at a time. If a new update arrives while one is in progress, it's queued as "pending" and processed after the current update completes. Only the LATEST pending state is kept (older ones are discarded), ensuring eventual convergence without starvation.

      Parameters

      Returns Promise<void>

    • Kick the progressive refinement orchestrator from OUTSIDE an update pass.

      Refinement is normally scheduled only at view-pass tails and after the initial scene load — loaders that register OUTSIDE those moments otherwise sit at their first additive chunk until the next slice change. The one such registration path is a deferred lod_group SUBTREE activation (e.g. the overview recipe's fine kind=partition branch): its part leaves join the sweep maps mid-session, so the deferred-group ensureLoaded calls this after loadChildren settles. Two recovery paths use it too (#2975): a failed-loader retry that committed only part of a ladder, and the backoff re-drain of ladders the consecutive-failure cap retired.

      When idle it takes the lock and runs the orchestrator; when busy it re-checks on a timer. "Busy" is the lock OR a live refinement run: the final phase's release opens the lock while the run is still unwinding, and a second run started in that window would share the first one's flags. See PassScheduler.kickRefinementIfIdle.

      Returns void

    • Drain every progressive ladder (points, lines, gsplats and mesh) one rung per frame, under the serialization lock the caller handed over.

      The phases run in sequence — gsplats first (its loader was the original template), then points, lines and mesh — each over the load-eligible loaders of its type at the moment it starts (kickRefinementIfIdle covers loaders registered later). On cancellation (a view queued during the run) the cancelling phase hands the lock to the next pass across a frame and the later phases are skipped. A failed-loader retry pre-empts the run the same way (A10): isActive turns false, every phase exits, and the run's end hands the still-held lock to the retry. Only the LAST phase completing releases the lock. The per-pass timing — frame yield, cancellation check, stall exit — lives in progressive/refinement.ts.

      Returns Promise<void>

    • Open the run's shared residency budget: ONE budget for all four phases, seeded from every sweep-registered ladder. Completed loaders return before their wrapper calls admit(), so seeding is what keeps their resident bytes in later view-triggered runs instead of ratcheting the ceiling upward. Lazy lod_group levels are not in these sweep maps. The density gate is re-evaluated from scratch each run: a deferral is camera-dependent, never sticky across runs.

      Returns RefinementResidencyBudget

    • The four refinement phases in run order, each resolving its type's load-eligible loaders when it STARTS (a snapshot, not the live map).

      Mesh runs LAST and therefore releases the lock. Order within the four is otherwise historical (gsplats was the template), but the final slot is not arbitrary: a mesh reveal is the cheapest of the four to interrupt — its levels are already-decoded whole-node payloads, so a cancelled pass loses at most one projection rather than an in-flight chunk fetch.

      Returns ((shared: RefinementPhaseShared) => Promise<void>)[]

    • Stamp the load timeline's refinementComplete milestone when the final geometry phase ran every ladder to completion — not on a cancellation hand-off (the next update re-kicks refinement) and not on a dead loader.

      Parameters

      • cancelled: boolean

      Returns void

    • Re-drain ladders the consecutive-failure cap retired (#2975).

      A chunk fetch that stalls to give-up fails the refinement step that needs it, and three such failures retire the ladder for this loader instance. Only an online event used to re-open it, so a session that stayed online sat idle at a coarser rung until the user moved something. Instead, after a drain that ends with such a ladder, wait ABANDONED_RUNG_RETRY_BASE_MS · 2^round (capped), re-open the retired ladders and kick a fresh drain. A view change, resetRefinementFailures or dispose cancels the wait; after MAX_ABANDONED_RUNG_RETRY_ROUNDS rounds the ladder is left, and the drain reports complete.

      Returns boolean

      Whether a re-drain is pending (the drain is not complete).

    • Drop a pending re-drain and its backoff (the retired ladders are re-opened elsewhere).

      Returns void

    • Clear the committedData identity stamp on a node's mesh. Called when a lazy LOD level is demoted: its geometry returned to the evictable pool, so the stamp (a) no longer describes what's on the GPU and (b) would pin the released node's large CPU arrays in memory. Re-promotion builds a fresh loader → new data reference → full recommit either way.

      Parameters

      • path: string

      Returns void

    • Aggregate visible counts from all lines and gsplats meshes and update monitor. This should be called after view updates to report accurate visible counts.

      Returns void

    • Recompute the monitor's visible-element tally outside the data-load cycle. Substitutive LOD selection (LODGroupRegistry.evaluatePerFrame) swaps which level renders on camera moves with no reload, so the per-frame callback calls this after a LOD switch — otherwise the monitor's "visible" counts stay pinned to the level that was active at the last updateView (e.g. the coarsest default level).

      Returns void

    • Process lines data: compute tolerance, project to 3D (async). Returns staged commit data without mutating any mesh geometry.

      Implementation lives in scene-loader/process/data-processor-lines.ts; this method is a thin delegate so the pipeline can be tested in isolation without instantiating a SceneLoader.

      Parameters

      Returns Promise<StagedLinesCommit | null>

    • Run a worker-backed step under signal AND this loader's dataset signal (dataset, the current one by default), so disposing the dataset settles it even when no per-update signal was given (initial build, retry). The worker pool is a page singleton shared by every host, so the dataset signal travels with the call rather than living on the pool.

      Type Parameters

      • T

      Parameters

      • signal: AbortSignal | undefined
      • run: (signal: AbortSignal | undefined) => Promise<T>
      • dataset: AbortController | null = ...

      Returns Promise<T>

    • Commit lines geometry to GPU buffers (synchronous). Called as part of the atomic commit stage — no async operations allowed. The optional session is forwarded so the helper can record an "Update Buffers" child entry under the per-node profiler session (parity with Points and GSplats).

      Parameters

      Returns boolean

    • Commit gsplats geometry to GPU buffers (synchronous). Called as part of the atomic commit stage — no async operations allowed. The optional session is forwarded so the helper can record an "Update Buffers" child entry under the per-node profiler session (parity with Points and Lines).

      Parameters

      Returns boolean

    • Project + stage a mesh for commit.

      Implementation lives in scene-loader/process/data-processor-mesh.ts. Async only because backend selection is, so unlike the lines/gsplats twins this never resolves to null — there is no worker projection that can decline.

      Parameters

      • path: string
      • data: LoadedMeshData
      • viewState: ViewState
      • attrs: Pick<
            MeshMetadata,
            "normal_dims"
            | "double_sided"
            | "extend_to_all"
            | "slab_tolerance",
        >

      Returns Promise<StagedMeshCommit>

    • Commit mesh geometry (synchronous).

      Implementation lives in scene-loader/commit/commit-mesh-geometry.ts. Mesh buffers are not pooled, but each commit registers their resident bytes for the shared GPU budget.

      Parameters

      Returns boolean

    • Build the per-call NodeBuildCtx for the initial-load leaf helpers. Snapshots viewState + factoryDeps so a concurrent updateView can't mutate state mid-flight. Never passes this.

      Returns NodeBuildCtx

    • Initialize scene dimensions from metadata. Implementation lives in scene-loader/nodes/initialize-scene-dimensions.ts; null return means validation failed and the existing viewState stays.

      Parameters

      • sceneDims: unknown

      Returns void

    • Whether candidate describes the view this loader has ALREADY committed (same displayed dims, slice, tolerances and per-dim query signature, by viewStatesEqual). Used by updateSceneForDimensions to turn the post-load updateAllNDNodes — fired unconditionally after loadScene has fetched, decoded, projected and committed every node at exactly this state — into a no-op instead of a second full pass (measured: L0 hits == misses on every 3-D scene, and ~1 s of extra main-thread work on a 29.6 M-splat slide). A real slider change compares unequal and proceeds.

      False while a requested main pass has not finished: this.viewState names a pass's view as soon as it starts. This also covers the frame boundary between a superseded pass and its queued replacement, when no main pass is running but the old view never committed (#2943).

      Parameters

      Returns boolean

    • Normalize URL for zarr store access. Guards window so this works in non-DOM contexts (tests, embed-in-Worker scenarios). Absolute URLs ignore the origin entirely; the fallback only matters for relative paths.

      Parameters

      • url: string

      Returns string

    • Get information about failed loaders

      Returns ReadonlyMap<string, { error: Error; timestamp: number; retryCount: number }>

      Map of loader paths to error information

    • Whether a path or one of its descendants has a recorded network failure.

      Parameters

      • path: string

      Returns boolean

    • Build a FailedLoadsProviderPort over the failed-load set for a UI consumer — the data-monitor's failure banner (wired in monitor-wiring.ts) and the layers panel's per-row error badge (wired in core/app/dataset/load-dataset.ts). Each call returns a NEW provider object (the monitor and the panel hold distinct instances), but all of them read the SAME live loader failures, archive-fault latch, and latched lazy branches through the one retryAllFailedLoaders entry point, so they always agree. getFailedReason powers the layers-panel tooltip.

      Returns FailedLoadsProviderPort

    • Release the retry's lock and resume the view: run what queued meanwhile (including a resync parked during the retry), else reload the whole view after a cleared archive fault, else resume any ladder the retry left short.

      Parameters

      • hadArchiveFault: boolean

      Returns void

    • Whether any failure is worth an AUTOMATIC retry: a latched archive fault, a transient loader cause still under the attempt cap (see LoaderRegistry.autoRetryablePaths), or a lazy branch latched by an archive fault. The connectivity-triggered retry gates on this so deterministic ordinary loader failures remain quiet while reconnecting can re-open the loader and deferred LOD work.

      Returns boolean

    • Clear failed loader tracking Useful for retry operations or after user acknowledges errors

      Returns void

    • Retry loading a specific failed loader.

      Re-triggers the update for a failed loader using the current view state. Useful for recovering from transient network errors or after connectivity is restored.

      Parameters

      • path: string

        The path of the failed loader to retry

      Returns Promise<boolean>

      Promise resolving to true if retry succeeded, false if failed or not found. For a LAZY substitutive LOD level (not in the sweep maps), true means the deferred reload was KICKED (fire-and-forget) — the lazy thunk owns the eventual ready/failed outcome, and a repeat failure re-records itself for another retry. Registered lines paths first wait for the session working-set gate, so this call can remain pending behind an eager scene walk.

      // Retry a specific loader after network recovery
      const success = await sceneLoader.retryFailedLoader('/points/cloud1');
      if (success) {
      console.log('Loader recovered successfully');
      }
    • Retry all failed loaders. Useful for batch recovery after network connectivity is restored.

      Parameters

      • opts: { onlyAutoRetryable?: boolean } = {}
        • OptionalonlyAutoRetryable?: boolean

          Retry only entries that pass the automatic-retry filter (a latched archive fault, or a transient cause under the attempt cap). Set by the connectivity-triggered retry. A manual Retry omits it and forces every failed path.

      Returns Promise<{ succeeded: string[]; failed: string[]; deferred?: boolean }>

      Promise resolving to { succeeded, failed, deferred? }. deferred: true means NOTHING was retried — a main update held the serialization lock, so the batch was refused (every path is reported in failed for compatibility, but none genuinely re-failed). Callers must not present a deferred result as a failed re-attempt; retry again once the update settles.

      // Retry all failed loaders after network recovery
      const result = await sceneLoader.retryAllFailedLoaders();
      console.log(`Recovered: ${result.succeeded.length}, Still failing: ${result.failed.length}`);
    • Dispose of all resources.

      Async because the caching store dispose path drains the prefetcher, cancels in-flight validation, and flushes OPFS metadata — work that a dataset switch should wait for before constructing the next loader. Existing sync callers (the beforeunload path, SceneLoaderManager.destroyLoader/destroyAll) still work; the returned promise just unwinds in the background. Callers that need deterministic teardown should await this method or use SceneLoaderManager.destroyLoaderAsync (added in a follow-up commit).

      Worker pool policy: Web Workers used for projection/decoding live in a MODULE-LEVEL singleton (workers/worker-pool.ts:getWorkerPool), not per-SceneLoader. Dataset switches deliberately do NOT terminate workers — the pool is bounded, and tearing it down per switch would force a fresh worker spin-up on the next load (10s of ms of WASM re-init on each cycle). Workers are terminated when the last host releases its lease via releaseWorkerPool() (LuxarApp in core/app/lifecycle/dispose-pipeline.ts, or LuxarLayer).

      Returns Promise<void>