PrivatelodPrivateloadedPrivate ReadonlynPrivate ReadonlypathPrivate_Private_Private_Latched once assertWithinByteBudget has fully accounted the ladder
and admitted it. Set ONLY on that success path — never on a rejection — so
a rejection is never cached as a pass: a live instance genuinely
re-preflights on the next updateView (a failed initialize() caches
nothing), and a disposed one is terminal and simply returns from the gate's
first line. The one verdict that CANNOT come out differently is latched
separately, on _budgetRefusal, so it is not re-derived either.
Private_The sticky AGGREGATE-over-budget refusal, once thrown — deliberately the mirror image of _budgetChecked above, which is never set on a rejection.
The AGGREGATE comparison this field guards is DETERMINISTIC. By the time
assertWithinByteBudget reaches the sum, every level's
MeshPreflightResult came from an initialize() that already succeeded
and is cached (MeshWholeNodeLoader.doInitialize never re-runs once
this.handles is set) — there is no I/O left to vary, so re-running the
gate on a later updateView can only ever repeat the same verdict.
Recomputing it anyway is actively harmful: without this latch,
hasMoreLODs stays true forever (the loaded-level count never grows on
a ladder that is never allowed to fetch a single level), so
queue-next.ts keeps scheduling runMeshRefinement on every slice
scrub, which burns MAX_CONSECUTIVE_REFINEMENT_FAILURES refinement
passes per scrub and toasts "Refinement failed … — showing a partial
surface" — false, since zero triangles were ever committed.
So the refusal is computed once, cached here, and every later call
rethrows the SAME LoaderError object with no further runPreflight()
calls. hasMoreLODs also reads this field directly (see above) and
reports false once it is set, which is what actually removes the dead
node from queueNext's refinement loop rather than merely making its
gate cheap to re-fail.
A level's OWN runPreflight() rejection stays unlatched — and NOT because
it is always transient. preflightMesh raises a deterministic
LoaderError('Validation') for a genuinely malformed store, so some of
those rejections do repeat forever. The reason is that the error KIND
cannot be trusted to separate the two here, and it fails in both
directions. Over-inclusive: a 404 on additive_2/normals while a store is
still being written arrives as Validation BY DESIGN —
mesh-whole-node-loader.ts's optional-array open catch reads a zarr
not-found as "the presence flag disagrees with the store", leaves the slot
empty, and preflightMesh's flag-with-no-array check then rejectMeshes
it. Network failures now reject before zarrita can classify them as
missing, but the deterministic Validation cases remain indistinguishable
from transient publication races. Under-inclusive: an
absent REQUIRED vertices/faces array throws zarrita's NotFoundError,
which classifyLoaderError matches nowhere and files as Unexpected. So
latching by kind would strand nodes that the failed-loads banner's manual
Retry and the LOD registry's not-ready self-heal — both of which
deliberately IGNORE the kind — recover the moment the store or the
connection is fixed, and would treat two 404s a few array names apart
oppositely.
The residual cost is real and accepted: while no level has committed, the
refinement loop keeps re-firing on a deterministically broken level, and
its "showing a partial surface" toast is inaccurate there too. Separating
the two honestly would mean marking determinism where each rejection is
CONSTRUCTED (in preflight.ts and the open catches) rather than inferring
it from the kind after the fact — deliberately out of scope here.
Private_Memoized concatenation, keyed on the loaded LEVEL COUNT alone.
Its siblings additionally key on a reset generation because their ladders are rebuilt per view; a mesh ladder is view-independent (see the module docstring), so an unchanged level count returns the same object across any number of slice moves. That identity is what the commit pipeline reads to skip no-op re-commits, and what keeps the projection scratch alive.
Private_Private_Private_Private_Private_Per-pass playback budget from the CURRENT updateView; null outside playback.
Private_Pinned rung count for the current pass (ViewState.ladderDepth, the
playback "detail" setting), resolved through resolveLadderDepth; null
when the pass is not pinned. A pinned pass loads exactly this many rungs,
cold or not, and reports hasMoreLODs === false like a budgeted one — the
pinned prefix IS the target.
Private_Private ReadonlymonitorMonitor telemetry, rolled up over the levels — see the surface below.
Private_Triangles the current slice indexes, as reported by the commit
(recordVisibleElements). Node-level, so it cannot come from the per-level
roll-up — see getMetrics.
Always null — a mesh reveal ladder carries no energy stamps, by
construction at three independent layers.
The stamps exist so the display gate can release an upgrade early, and so
energyCompensation can brighten an incomplete EMISSIVE ladder by 1/e(k):
a coarse prefix of a splat cloud is a dim version of the whole, and dividing
by the committed energy fraction restores its brightness. A reveal prefix is
nothing of the kind — it is a PARTIAL OBJECT AT FULL BRIGHTNESS — so the same
multiplier would blow out the first shell by ~1/e and then fade it as the
surface completes: the exact inverse of growing in. MESH_NODE_SPEC.md §9.1
states the rule; the Python add_mesh refuses lod_stats energy keys, the
factory reads no energy table, and this getter closes the loop.
It must EXIST and return null rather than be absent, and the difference is
not cosmetic: stampLadderComplete probes
'committedEnergyFraction' in loader and takes the NON-progressive branch
when the getter is missing — stamping a half-revealed mesh
committedEnergyFraction: 1, i.e. "all of it is on screen". Returning null
makes the stamp absent instead, which is what the display gate reads as
"unstamped, fall back to committed-count crossover".
Measured footprint of the loaded ladder, for the shared sweep residency
budget (scene-loader/progressive/residency-budget). Sums real
byteLengths rather than modelling a per-element cost, so it stays correct
as payload columns come and go. Rung count is reported alongside so the
budget can estimate the next rung without needing per-rung sizes.
Discard the levels the current pass appended, restoring the ladder to the
prefix the pass started from. Called by the main-update and refinement catches; see
../loaders/progressive/pass-rollback for why a failed commit must not
leave the cursor advanced.
Levels discarded (0 when the pass appended none).
Load the mesh for the given view state
Optionalsession: UpdateSessionPrivateassertCharge the ladder's aggregate byte budget ONCE, at the ladder's first
updateView — before any level's chunks are fetched.
The obvious home for this aggregate is where the ladder is BUILT —
createProgressiveMeshLoader (../scene-loader/loaders/loader-factory.ts),
during loadMeshNodeCheap — and that spot is unusable, which is worth
recording because it is the first place a reader will look for it. The
cheap half runs OUTSIDE loadMeshNode's try/placeholder-attach and before
registerMeshLoader, so a rejection thrown from there has nowhere to go:
no placeholder to mark failed, no recordFailure, no retry, no monitor
banner — a transient blip on one level's metadata open would permanently
lose the whole node, silently (reportLoadOutcome still logs success).
Swallowing that rejection instead only trades it for the other failure:
retryFailedLoader reuses the existing loader rather than re-entering the
factory, so a swallowed level's bytes would go uncounted forever, the
level would then load fine on retry, and the ladder that motivates this
whole check would sail through over budget with nothing left to refuse it.
Both failure modes trace to the same cause: a check with no containment
around it. A leaf's own byte budget is enforced inside
MeshWholeNodeLoader.fetch(), i.e. inside loadMeshNodeExpensive's try —
so charging the LADDER at the equivalent point, its own first load, gives
it the identical containment (failure recorded, banner shown, siblings
unaffected) as well as the identical ceiling: ladder ≡ leaf in both
respects. Retryability carries over for a level's OWN rejection; the
aggregate over-budget verdict is deliberately cached and rethrown instead,
since no retry can make the sum fit (see _budgetRefusal). It is
still strictly before any chunk is fetched — runPreflight() only opens
metadata — so the two-stage gate's "refuse before allocation" property
survives the move unchanged.
Gating on EVERY level's metadata before level 0 is fetched has a real cost, and it is worth stating rather than leaving implicit:
runPreflight() rejection now costs the WHOLE first
paint. Before this change, levels 0..k-1 painted and the ladder simply
stalled at the bad level k with a "partial surface" toast; now nothing
paints until every level's metadata has opened successfully..zarray + .zattrs each) to open before
level 0 can even start fetching chunks. Behind a browser's ~6-connection
limit per origin, that is several serialized round trips — measurably
slower first paint on exactly the slow link this reveal ladder exists
to serve well.For a Luxar-written store this is close to free: the compiler writes a
.zmetadata consolidated-metadata document, and src/data/zarr.ts wraps
every store with zarrita.withMaybeConsolidatedMetadata, so every
zarr.open(..., { kind: 'array' }) above is served from an in-memory
document instead of a network round trip. The cost above is real only for
an arbitrary ?src= store that omits it.
An alternative was considered and NOT taken: charge levels 1..N-1 only
AFTER level 0 has been committed, so a slow/failing deeper level would
never block first paint (level 0 already has its own leaf-sized
runPreflight(), so the pre-refusal peak would still be bounded by one
leaf's ceiling). Rejected here because it weakens the property this gate
is FOR — "refuse the whole ladder before any chunk is fetched" — down to
"refuse before the second chunk," and because the common (consolidated)
case already pays nothing for gating everything up front. Not
implemented.
MESH_DECODE_BUDGET_BYTES is a per-NODE ceiling, and a reveal ladder is
one node (MESH_NODE_SPEC.md §9.1): this class concatenates every
level's vertices/faces/normals/colors/scalars into ONE committed buffer
set (concatenateMeshData above), and all of it stays resident for the
node's whole life. Each level's own runPreflight() only ever sees its
own ceiling, with no knowledge of its siblings — so without this, a
ladder's real ceiling was nAdditive x budget: a plain leaf with the
ladder's total geometry is refused up front, but the same geometry split
into levels sails through, N times over budget, and the tab dies on the
concatenated allocation. Summing every level's accountedBytes and
charging that once against the SAME ceiling is what turns N budgets back
into one.
The sum overcharges relative to what a single level's own preflight would
need to: it includes each level's own largest-chunk term even though the
ladder loads levels sequentially (never two chunk buffers alive at once
across levels), and an array_ref target shared between levels is
charged once per referring level rather than once total. In that sense it
is a deliberate over-estimate.
That does NOT make the charged figure a bound on true peak residency,
which is a separate quantity this sum does not track. Per level i, the
charged term is stored_i + 4·decoded_i (plus that level's own
maxChunk_i, which is transient and never resident). After each successful
concat, the level loaders release their decoded payloads and the composite
retains only the cumulative payload plus its projection scratch. Tightening
the ceiling to a true peak-residency model remains a separate concern.
initialize() routes a transient open failure (a network blip) through
classifyLoaderError precisely so it stays retryable, and that property
has to survive reaching here. Letting the rejection propagate instead of
catching it is correct HERE, and only because of where "here" is:
loadMeshNodeExpensive's catch records the failure and keeps the blip
retryable, and a later retry re-enters updateView, which re-runs this
gate — _budgetChecked is latched only on a fully successful accounting,
and the failed level's own initialize() cached nothing, so the retry
genuinely re-preflights rather than replaying a stale rejection.
Propagating is not the same as forgetting, though: the AGGREGATE over-budget refusal below IS latched on the way out and rethrown as the same error object rather than re-derived. A level's own rejection is not — see _budgetRefusal for why the error kind cannot be trusted to tell a deterministic one from a transient one here.
Re-serve the mesh for a new view state (see DataLoader.updateView for signal)
Optionalsession: UpdateSessionOptionalsignal: AbortSignalOptionalresidencyAllowanceBytes: numberPrivateconcatenateConcatenate the loaded levels, memoized on the level count.
Returning the SAME object reference when nothing has changed is safe because the result is never mutated downstream (the worker projection's inputs are structured-cloned, not transferred) and is what lets the commit pipeline skip no-op re-commits by identity.
No setPrefixParent stamp, unlike the three siblings. That lineage exists
so the commit layer can recognise a prefix EXTENSION and take the depth-sort
append fast path, which is defined over the instanced element buffers mesh
does not have — its faces are re-emitted by the projection every epoch.
Optionalsession: UpdateSessionLoaderMonitor surface (optional, for the data-loading-monitor UI).
Mirrors the surface the sibling loaders expose — implementations that
don't track metrics may omit these. Both shipped implementations
(MeshWholeNodeLoader, MeshProgressiveLoader) provide all four:
connectLoaderToMonitor duck-types the complete set, so a partial
implementation is silently skipped rather than partially reported.
Always empty — no level runs spatial queries (see MeshWholeNodeLoader).
See MeshWholeNodeLoader.recordVisibleElements.
Clean up resources
Progressive Mesh loader — the reveal ladder's composite.