Stage 1 of the mesh loader's admission gate: the metadata preflight.
Runs purely on the node attrs and each array's zarr .zarray metadata
(declared shape, chunks, dtype). It touches no chunk data — every check
here is decidable before a single byte of geometry is fetched, and that is the
whole point of the stage existing separately.
Why a second stage isn't enough
The writer's validate_*_for_writing family protects only stores Luxar
produced. The viewer loads arbitrary ?src= URLs, and the mesh loader is
whole-node (docs/specs/MESH_NODE_SPEC.md §7): it fetches and decodes every
array in full, up front. A check that runs only after decode therefore
arrives too late for the quantities that gate admission — a hostile or
corrupt store can declare enormous arrays and exhaust tab memory before the
LoaderError containment ("one node lost, not the scene") is ever reachable.
So everything decidable from metadata is checked here, first, and the
value-level checks that genuinely need materialized arrays run afterwards in
Stage 2 (validate.ts).
What is checked, and what each check prevents
n_vertices <= MAX_MESH_VERTICES — above 2^27 the pick vote key
aliases across nodes (§6.5). Refused before allocation, not after a
multi-gigabyte fetch.
Byte budget — stored bytes, decoded bytes and the largest single chunk
allocation, SUMMED against MESH_DECODE_BUDGET_BYTES. Charged against the array
whose bytes are actually fetched, which for an array_ref is the TARGET, not the
(0, k) stub that points at it.
Shape and dtype cross-checks — against n_vertices/n_faces/ndim
and the §3.2 array table.
normal_dims well-formedness — 3 distinct integers in [0, ndim).
Which gate actually binds
At the default 512 MiB budget the byte budget is far tighter than the vertex
cap, and it is worth knowing which error to expect. A 3D float32 mesh runs
out of budget at ~22.4M vertices and a uint16-quantized one at ~29.8M, both
well under 2^27 (134.2M) — so in practice no mesh reaches the cap by growing
legitimately. (Those figures halve the stored-only arithmetic because the decoded
term is charged too: a float32 vertex costs 4 bytes stored AND 4 decoded. The
float32 number is measured, not derived — see the boundary test.)
The cap is still checked, and checked first, for two reasons. It gives a
hostile or nonsensical declaration (n_vertices: 2^30) the message that names
the real problem — pick keys aliasing, which no amount of decimation fixes —
rather than blaming bytes. And it keeps the bound enforced rather than assumed
if the budget is ever raised.
The shape checks are not cosmetic tidiness. Two of them stop a
panic = "abort" WASM trap, which escapes as an opaque uncatchable
RuntimeError: unreachable rather than a node-scoped error:
a vertices width that disagrees with ndim makes the §5.4 slab kernel
index out of slice bounds;
a faces array of the wrong shape or a float dtype either mis-strides the
topology or truncates in the u32 coercion.
And the optional-array shape checks stop a quieter failure: normals /
colors / scalars bind as enabled vertex attributes on an indexed draw
(§6.1). An undersized one doesn't trap — it makes drawElements read past
the buffer (an invalid-operation draw or silent zeros, backend-dependent) and
mis-shades every vertex it covers. A declared-undersized array is catchable
here from its shape alone.
Stage 1 of the mesh loader's admission gate: the metadata preflight.
Runs purely on the node attrs and each array's zarr
.zarraymetadata (declared shape,chunks, dtype). It touches no chunk data — every check here is decidable before a single byte of geometry is fetched, and that is the whole point of the stage existing separately.Why a second stage isn't enough
The writer's
validate_*_for_writingfamily protects only stores Luxar produced. The viewer loads arbitrary?src=URLs, and the mesh loader is whole-node (docs/specs/MESH_NODE_SPEC.md§7): it fetches and decodes every array in full, up front. A check that runs only after decode therefore arrives too late for the quantities that gate admission — a hostile or corrupt store can declare enormous arrays and exhaust tab memory before theLoaderErrorcontainment ("one node lost, not the scene") is ever reachable.So everything decidable from metadata is checked here, first, and the value-level checks that genuinely need materialized arrays run afterwards in Stage 2 (
validate.ts).What is checked, and what each check prevents
n_vertices <= MAX_MESH_VERTICES— above2^27the pick vote key aliases across nodes (§6.5). Refused before allocation, not after a multi-gigabyte fetch.MESH_DECODE_BUDGET_BYTES. Charged against the array whose bytes are actually fetched, which for anarray_refis the TARGET, not the(0, k)stub that points at it.n_vertices/n_faces/ndimand the §3.2 array table.normal_dimswell-formedness — 3 distinct integers in[0, ndim).Which gate actually binds
At the default 512 MiB budget the byte budget is far tighter than the vertex cap, and it is worth knowing which error to expect. A 3D float32 mesh runs out of budget at ~22.4M vertices and a uint16-quantized one at ~29.8M, both well under
2^27(134.2M) — so in practice no mesh reaches the cap by growing legitimately. (Those figures halve the stored-only arithmetic because the decoded term is charged too: a float32 vertex costs 4 bytes stored AND 4 decoded. The float32 number is measured, not derived — see the boundary test.)The cap is still checked, and checked first, for two reasons. It gives a hostile or nonsensical declaration (
n_vertices: 2^30) the message that names the real problem — pick keys aliasing, which no amount of decimation fixes — rather than blaming bytes. And it keeps the bound enforced rather than assumed if the budget is ever raised.The shape checks are not cosmetic tidiness. Two of them stop a
panic = "abort"WASM trap, which escapes as an opaque uncatchableRuntimeError: unreachablerather than a node-scoped error:verticeswidth that disagrees withndimmakes the §5.4 slab kernel index out of slice bounds;facesarray of the wrong shape or a float dtype either mis-strides the topology or truncates in the u32 coercion.And the optional-array shape checks stop a quieter failure:
normals/colors/scalarsbind as enabled vertex attributes on an indexed draw (§6.1). An undersized one doesn't trap — it makesdrawElementsread past the buffer (an invalid-operation draw or silent zeros, backend-dependent) and mis-shades every vertex it covers. A declared-undersized array is catchable here from its shape alone.