perf(importer): skip decoding glTF images no material references #835

Merged
SakulFlee merged 1 commit from perf/gltf-import-skip-unused-images into main 2026-09-29 16:47:54 +00:00
Owner

The importer decoded every image in the document. Real-world glTF assets
routinely ship images that no material points at - leftover art, or textures
kept for a material variant that is not used - and each one costs decode time
plus a full pixel buffer held in RAM for the duration of the import, for data
nothing ever reads.

Decode only images referenced by a material's normal, base-color,
metallic-roughness, occlusion or emissive slot. The reference scan runs over
the whole document rather than the subset a request will visit, because
GltfImport::Specific is only resolved after the document and its images have
been loaded; decoding an image nothing ends up using is cheap, whereas missing
one a later stage needs would not be.

The image vector is now Vec<Optiongltf::image::Data>: unreferenced entries
stay in place as None so the positional lookups in parse_texture and friends,
which index by document image index, keep working. A new decoded_image helper
centralises the unwrap and panics with a clear message if a material ever
references an image the scan did not decode, which would be a bug rather than
bad input.

Measured on a generated asset with 4 unreferenced 1024x1024 textures
(make_unused_texture_asset), min of 20 runs: 5.53 ms -> 2.69 ms, ~1.4-2.4x
across repeats. The saving scales with the size and count of the unreferenced
images, and is zero for an asset where every image is referenced.

Adds two regression tests covering the external-file and embedded-GLB paths.
Both point the unreferenced image at non-decodable bytes, so they fail if the
image is decoded at all; verified they fail with the skip disabled.

Also fixes build_glb in the test helper, which only produced parseable files
by luck. The reader splits the JSON chunk at exactly its declared length and
passes it to serde, so the chunk length must be the 4-byte-aligned length (as
gltf's own Glb::to_writer emits) and the JSON padding must be spaces rather
than NULs, which serde rejects as trailing characters.

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

The importer decoded every image in the document. Real-world glTF assets routinely ship images that no material points at - leftover art, or textures kept for a material variant that is not used - and each one costs decode time plus a full pixel buffer held in RAM for the duration of the import, for data nothing ever reads. Decode only images referenced by a material's normal, base-color, metallic-roughness, occlusion or emissive slot. The reference scan runs over the whole document rather than the subset a request will visit, because GltfImport::Specific is only resolved after the document and its images have been loaded; decoding an image nothing ends up using is cheap, whereas missing one a later stage needs would not be. The image vector is now Vec<Option<gltf::image::Data>>: unreferenced entries stay in place as None so the positional lookups in parse_texture and friends, which index by document image index, keep working. A new decoded_image helper centralises the unwrap and panics with a clear message if a material ever references an image the scan did not decode, which would be a bug rather than bad input. Measured on a generated asset with 4 unreferenced 1024x1024 textures (make_unused_texture_asset), min of 20 runs: 5.53 ms -> 2.69 ms, ~1.4-2.4x across repeats. The saving scales with the size and count of the unreferenced images, and is zero for an asset where every image is referenced. Adds two regression tests covering the external-file and embedded-GLB paths. Both point the unreferenced image at non-decodable bytes, so they fail if the image is decoded at all; verified they fail with the skip disabled. Also fixes build_glb in the test helper, which only produced parseable files by luck. The reader splits the JSON chunk at exactly its declared length and passes it to serde, so the chunk length must be the 4-byte-aligned length (as gltf's own Glb::to_writer emits) and the JSON padding must be spaces rather than NULs, which serde rejects as trailing characters. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
perf(importer): skip decoding glTF images no material references
All checks were successful
Lint / lint (pull_request) Successful in 5m2s
9cb2701329
The importer decoded every image in the document. Real-world glTF assets
routinely ship images that no material points at - leftover art, or textures
kept for a material variant that is not used - and each one costs decode time
plus a full pixel buffer held in RAM for the duration of the import, for data
nothing ever reads.

Decode only images referenced by a material's normal, base-color,
metallic-roughness, occlusion or emissive slot. The reference scan runs over
the whole document rather than the subset a request will visit, because
GltfImport::Specific is only resolved after the document and its images have
been loaded; decoding an image nothing ends up using is cheap, whereas missing
one a later stage needs would not be.

The image vector is now Vec<Option<gltf::image::Data>>: unreferenced entries
stay in place as None so the positional lookups in parse_texture and friends,
which index by document image index, keep working. A new decoded_image helper
centralises the unwrap and panics with a clear message if a material ever
references an image the scan did not decode, which would be a bug rather than
bad input.

Measured on a generated asset with 4 unreferenced 1024x1024 textures
(make_unused_texture_asset), min of 20 runs: 5.53 ms -> 2.69 ms, ~1.4-2.4x
across repeats. The saving scales with the size and count of the unreferenced
images, and is zero for an asset where every image is referenced.

Adds two regression tests covering the external-file and embedded-GLB paths.
Both point the unreferenced image at non-decodable bytes, so they fail if the
image is decoded at all; verified they fail with the skip disabled.

Also fixes build_glb in the test helper, which only produced parseable files
by luck. The reader splits the JSON chunk at exactly its declared length and
passes it to serde, so the chunk length must be the 4-byte-aligned length (as
gltf's own Glb::to_writer emits) and the JSON padding must be spaces rather
than NULs, which serde rejects as trailing characters.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SakulFlee deleted branch perf/gltf-import-skip-unused-images 2026-09-29 16:47:54 +00:00
Sign in to join this conversation.
No description provided.