Per-node ceiling, in bytes, on what a mesh node is allowed to declare
before the loader will fetch any of it. Default 512 MiB.
This value is the viewer's own policy: the write side cannot know what a tab
can survive, and this gate's primary job is one the writer can never help
with. 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 — so a hostile or corrupt store could otherwise exhaust tab
memory before the per-node LoaderError containment is ever reachable.
MIRROR: MESH_DECODE_BUDGET_BYTES in
packages/luxar/src/luxar/typing_utils/constants.py holds the same value.
That is a PARTIAL write-time twin, added because the two purposes above are
separable: policing hostile stores is this gate's alone, but refusing to
AUTHOR a store that provably exceeds the ceiling is something add_mesh can
do, and should — a bound enforced only here surfaces in a browser, far from
the call that caused it. The twin is deliberately weaker: it charges only the
decoded term, so it is a strict lower bound on the accounting below and can
never refuse a node this gate would admit. See
validate_mesh_decode_budget. If you change this value, change that one.
Three quantities are checked against this, all from .zarray metadata alone:
every array's stored footprint, what each one DECODES to, and each array's
single largest per-chunk allocation.
Charging the decoded term is not belt-and-braces — the stored side can be
arbitrarily smaller than what gets allocated, so a stored-only budget is wrong
in the dangerous direction. A broadcast array stores one row and expands to
n_elements rows, so a ~12-byte declaration can materialize gigabytes; every
decoder-routed array yields a Float32Array, so a uint8 store decodes at 4x
its stored bytes; and faces widens to u32 whatever narrow dtype the INDEX
encoder chose. The decoded term is also the only thing bounding ndim, which
has no cap of its own.
The per-chunk term is likewise not redundant: zarr v2 does not require
chunks <= shape, so a "shape": [100, 3] array declaring
"chunks": [268435456, 3] would slip a multi-gigabyte first-chunk allocation
past a shape-only budget.
Be precise about what this bounds: the SUM of stored bytes, decoded bytes, and
the largest single chunk buffer — the three are added together, not checked
independently, because a chunk buffer exists during decode alongside the arrays.
(An oversized chunk is ALSO rejected on its own, so a single array cannot exceed
the ceiling even where the sum would fit.) This is still not the loader's whole
transient peak: the admission path also holds the extracted display-space
position and the driver-side GPU upload, so the real peak is a modest multiple
of this value.
512 MiB keeps that comfortably inside a 64-bit tab — which is why this must
never be raised toward "what a tab survives": the tab has to survive the
multiple, not the ceiling.
The ceiling is per NODE. N nodes can still sum to N x budget; the guarantee
it buys is "one node lost, not the scene", not an aggregate cap.
Per-node ceiling, in bytes, on what a mesh node is allowed to declare before the loader will fetch any of it. Default 512 MiB.
This value is the viewer's own policy: the write side cannot know what a tab can survive, and this gate's primary job is one the writer can never help with. 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 — so a hostile or corrupt store could otherwise exhaust tab memory before the per-nodeLoaderErrorcontainment is ever reachable.MIRROR:
MESH_DECODE_BUDGET_BYTESinpackages/luxar/src/luxar/typing_utils/constants.pyholds the same value. That is a PARTIAL write-time twin, added because the two purposes above are separable: policing hostile stores is this gate's alone, but refusing to AUTHOR a store that provably exceeds the ceiling is somethingadd_meshcan do, and should — a bound enforced only here surfaces in a browser, far from the call that caused it. The twin is deliberately weaker: it charges only the decoded term, so it is a strict lower bound on the accounting below and can never refuse a node this gate would admit. Seevalidate_mesh_decode_budget. If you change this value, change that one.Three quantities are checked against this, all from
.zarraymetadata alone: every array's stored footprint, what each one DECODES to, and each array's single largest per-chunk allocation.Charging the decoded term is not belt-and-braces — the stored side can be arbitrarily smaller than what gets allocated, so a stored-only budget is wrong in the dangerous direction. A broadcast array stores one row and expands to
n_elementsrows, so a ~12-byte declaration can materialize gigabytes; every decoder-routed array yields aFloat32Array, so auint8store decodes at 4x its stored bytes; andfaceswidens to u32 whatever narrow dtype the INDEX encoder chose. The decoded term is also the only thing boundingndim, which has no cap of its own.The per-chunk term is likewise not redundant: zarr v2 does not require
chunks <= shape, so a"shape": [100, 3]array declaring"chunks": [268435456, 3]would slip a multi-gigabyte first-chunk allocation past a shape-only budget.Be precise about what this bounds: the SUM of stored bytes, decoded bytes, and the largest single chunk buffer — the three are added together, not checked independently, because a chunk buffer exists during decode alongside the arrays. (An oversized chunk is ALSO rejected on its own, so a single array cannot exceed the ceiling even where the sum would fit.) This is still not the loader's whole transient peak: the admission path also holds the extracted display-space
positionand the driver-side GPU upload, so the real peak is a modest multiple of this value. 512 MiB keeps that comfortably inside a 64-bit tab — which is why this must never be raised toward "what a tab survives": the tab has to survive the multiple, not the ceiling.The ceiling is per NODE. N nodes can still sum to N x budget; the guarantee it buys is "one node lost, not the scene", not an aggregate cap.