Mesh texture fetch + decode, and the Stage 2 check that the decoded surface is
the one the store declared.
Its own module rather than a branch of mesh-whole-node-loader.ts for the
reason the writer split _write_mesh_texture_arrays out on the Python side: a
texture is the only array in a mesh that is not per-vertex, so it shares none
of the loader's nVertices-shaped machinery and every one of its steps is
conditional on an encoding the other arrays do not have.
Three arms, and why they cannot share a decode
raw is a numeric (h, w, c) array, so it goes through the normal Luxar
decode stack — which matters, because the raw arm is the one that carries HDR
and an HDR texture is stored as geolog_perchannel_u16 codes under AUTO.
Reading its bytes directly would hand back quantization codes and render a
garbage image.
A codec arm (png / webp / jpeg) is an opaque blob no Luxar encoder
touches, decoded by the browser via createImageBitmap — the same mechanism
loaders/picking/image-label-loader.ts already uses for hover thumbnails.
The ktx2 arm is also an opaque blob, but it must bypass browser image decode:
the renderer-owned KTX2 decoder transcodes it directly to a native compressed
GPU texture while preserving the authored mip chain.
Stage 2 exists because Stage 1 cannot finish the job
The preflight charges the decoded surface against the byte budget from the
DECLARED texture_width/texture_height, because a compressed image's stored
size says nothing about what it expands to. That admission is therefore only as
true as the declaration. So the moment real dimensions exist — after
createImageBitmap reports them — they are compared against what was declared
and a mismatch rejects the node. Without that, a store declares 16 x 16, is
admitted for 1 KB, and detonates a 30000 x 30000 decode.
Mesh texture fetch + decode, and the Stage 2 check that the decoded surface is the one the store declared.
Its own module rather than a branch of
mesh-whole-node-loader.tsfor the reason the writer split_write_mesh_texture_arraysout on the Python side: a texture is the only array in a mesh that is not per-vertex, so it shares none of the loader'snVertices-shaped machinery and every one of its steps is conditional on an encoding the other arrays do not have.Three arms, and why they cannot share a decode
rawis a numeric(h, w, c)array, so it goes through the normal Luxar decode stack — which matters, because the raw arm is the one that carries HDR and an HDR texture is stored asgeolog_perchannel_u16codes underAUTO. Reading its bytes directly would hand back quantization codes and render a garbage image.A codec arm (
png/webp/jpeg) is an opaque blob no Luxar encoder touches, decoded by the browser viacreateImageBitmap— the same mechanismloaders/picking/image-label-loader.tsalready uses for hover thumbnails.The
ktx2arm is also an opaque blob, but it must bypass browser image decode: the renderer-owned KTX2 decoder transcodes it directly to a native compressed GPU texture while preserving the authored mip chain.Stage 2 exists because Stage 1 cannot finish the job
The preflight charges the decoded surface against the byte budget from the DECLARED
texture_width/texture_height, because a compressed image's stored size says nothing about what it expands to. That admission is therefore only as true as the declaration. So the moment real dimensions exist — aftercreateImageBitmapreports them — they are compared against what was declared and a mismatch rejects the node. Without that, a store declares16 x 16, is admitted for 1 KB, and detonates a 30000 x 30000 decode.