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

    Module data/scene-loader/progressive/residency-budget

    Residency budget for sweep-registered progressive LOD leaves.

    An ADDITIVE ladder is prefix accumulation: its terminal state is 100% of the node, on every node, always. That bounds FIRST PAINT — the first rung is small — but it bounds nothing at rest, and the refinement loop drives every loader to its last rung with no notion of what the scene can afford.

    On the hosted cosmicflows_laniakea demo that is 10 line nodes x ~1.3M segments = 11.4M segments, and the tab dies with RangeError: Array buffer allocation failed partway up the ladders (#2426). The eager admission gate in nodes/load-children-concurrently.ts already measures each node against a budget and serialises the initial loads — but it releases in the finally around each child's load, and refinement is scheduled later (lifecycle/load-scene.ts), so by the time the ladders climb, nothing is holding a budget at all. This gate uses the device heap LIMIT already read by the cache sizing path; it does not probe current usedJSHeapSize.

    This is a REFUSAL gate, never an evictor. It declines to load MORE; it never discards what is already committed. A node that hits the ceiling simply stops at the rung it reached and stays there — a legible partial scene rather than a dead tab. Legible to a HUMAN, that is: which nodes won the remaining headroom is not deterministic, so the element counts of a stopped scene are not a property of the store and nothing downstream may compare them across builds. That is why RefinementResidencyReporter also RECORDS every refusal for RefinementResidencyStop, which the debug snapshot carries and core/app/debug/capture-readiness.ts refuses on (#2508) — a console warning is not something a harness can read. Only loaders registered in the four refinement sweep maps are covered; lazy lod_group levels are outside those maps and outside this accounting. The budget uses the eager gate's working-set calculation, including its EAGER_WORKING_SET_CAP_BYTES ceiling: sufficiently large desktop heaps all resolve to that same cap rather than scaling without bound. The GPU buffer pool instead takes the full shared non-cache remainder up to its own 2 GB ceiling and covers some of the same renderer element-row bytes. These remain independent ceilings, not additive reservations; eager in-flight bytes, pooled GPU bytes, and refinement residency may coexist transiently.

    Decoded payload bytes are measured from the typed arrays. Renderer element rows are derived from each geometry's authoritative layout constant, because those allocations are not present in the decoded payload. The renderer's two Uint32 ordering buffers (8 B/element) are intentionally omitted, as is the separately bounded slice cache. The next pass is estimated from the mean accounted rung so far — see estimateNextRungBytes. An admitted loader receives a per-pass share of the remaining headroom across tracked, non-declined paths, with enough allowance for its estimated next rung, and stops after the first level that spends it. The tracked-set denominator deliberately includes completed or currently filtered paths from the raw sweep maps, so it may under-grant relative to the loaders currently offered. This is not scene-wide round-robin fairness: geometry phases run sequentially, and an earlier phase can consume the ceiling before a later phase is offered. The allowance still prevents a warmed cache from consuming the rest of the ladder under one admission. During a fold/commit, the previous cumulative CPU payload can remain reachable while the replacement is allocated, so the transient peak may add nearly one extra decoded cumulative on top of the settled payload + element-row accounting.

    DECLINING MUST ALSO RETIRE THE LOADER FROM THE RUN. runProgressiveRefinement spins while anyHasMoreLODs() is true, and a declined loader still has more LODs. If a wrapper only returned false from processLoader the loop would re-offer it every frame forever, holding the update lock — the same trap RefinementFailureTracker avoids by excluding exhausted paths from the aggregation. So callers MUST consult RefinementResidencyBudget.isDeclined in their anyHasMoreLODs and getLoaderProgress closures, exactly as they already do for failures.isExhausted.

    RefinementResidencyReporter
    RefinementResidencyBudget
    RefinementAdmission
    RefinementPassAdmission
    LadderResidency
    RefinementResidencyStop
    RefinementAdmissionReason
    RESIDENCY_DECLINED_PATH_SAMPLE
    planRefinementAdmission
    ladderResidentBytes
    estimateNextRungBytes