@woosh/meep-engine 3.14.1 → 3.14.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1282) hide show
  1. package/README.md +31 -24
  2. package/editor/prototypeEditorShell.js +6 -0
  3. package/package.json +1 -1
  4. package/src/CODEC_LAYOUT_VERIFICATION.md +420 -0
  5. package/src/core/binary/ByteCursor.d.ts +138 -0
  6. package/src/core/binary/ByteCursor.d.ts.map +1 -0
  7. package/src/core/binary/ByteCursor.js +325 -0
  8. package/src/core/binary/zstd/ZstdReverseBitReader.d.ts +76 -0
  9. package/src/core/binary/zstd/ZstdReverseBitReader.d.ts.map +1 -0
  10. package/src/core/binary/zstd/ZstdReverseBitReader.js +223 -0
  11. package/src/core/binary/zstd/zstd_decompress.d.ts +31 -0
  12. package/src/core/binary/zstd/zstd_decompress.d.ts.map +1 -0
  13. package/src/core/binary/zstd/zstd_decompress.js +628 -0
  14. package/src/core/binary/zstd/zstd_fse.d.ts +83 -0
  15. package/src/core/binary/zstd/zstd_fse.d.ts.map +1 -0
  16. package/src/core/binary/zstd/zstd_fse.js +265 -0
  17. package/src/core/binary/zstd/zstd_huffman.d.ts +56 -0
  18. package/src/core/binary/zstd/zstd_huffman.d.ts.map +1 -0
  19. package/src/core/binary/zstd/zstd_huffman.js +244 -0
  20. package/src/core/binary/zstd/zstd_sequence_tables.d.ts +62 -0
  21. package/src/core/binary/zstd/zstd_sequence_tables.d.ts.map +1 -0
  22. package/src/core/binary/zstd/zstd_sequence_tables.js +98 -0
  23. package/src/core/color/COLOR_REVIEW_2026_08_28.md +1 -1
  24. package/src/core/color/PQ/PQ_constants.d.ts +1 -1
  25. package/src/core/color/PQ/PQ_constants.js +1 -1
  26. package/src/engine/EngineHarness.js +1 -1
  27. package/src/engine/asset/GameAssetType.d.ts +1 -0
  28. package/src/engine/asset/GameAssetType.js +8 -0
  29. package/src/engine/asset/codec/Codec.d.ts.map +1 -0
  30. package/src/engine/asset/codec/CodecWithFallback.d.ts.map +1 -0
  31. package/src/engine/asset/codec/CodecWithFallback.js +105 -0
  32. package/src/engine/asset/codec/DecodedImage.d.ts +39 -0
  33. package/src/engine/asset/codec/DecodedImage.d.ts.map +1 -0
  34. package/src/engine/asset/codec/DecodedImage.js +26 -0
  35. package/src/engine/asset/guessAssetType.d.ts.map +1 -1
  36. package/src/engine/asset/guessAssetType.js +10 -0
  37. package/src/engine/asset/load_model_scene_bundle.d.ts +1 -1
  38. package/src/engine/asset/load_model_scene_bundle.d.ts.map +1 -1
  39. package/src/engine/asset/load_model_scene_bundle.js +6 -2
  40. package/src/engine/asset/loaders/USDSceneBundleAssetLoader.d.ts +37 -0
  41. package/src/engine/asset/loaders/USDSceneBundleAssetLoader.d.ts.map +1 -0
  42. package/src/engine/asset/loaders/USDSceneBundleAssetLoader.js +104 -0
  43. package/src/engine/asset/loaders/image/IMAGE_CODEC_REVIEW_2026_08_28.md +87 -75
  44. package/src/engine/asset/loaders/image/ImageDecoderWorker.js +2 -2
  45. package/src/engine/asset/loaders/image/ImageRGBADataLoader.d.ts +9 -10
  46. package/src/engine/asset/loaders/image/ImageRGBADataLoader.d.ts.map +1 -1
  47. package/src/engine/asset/loaders/image/ImageRGBADataLoader.js +13 -4
  48. package/src/engine/asset/loaders/image/avif/encode_image_source.d.ts +70 -0
  49. package/src/engine/asset/loaders/image/avif/encode_image_source.d.ts.map +1 -0
  50. package/src/engine/asset/loaders/image/avif/encode_image_source.js +117 -0
  51. package/src/engine/asset/loaders/image/avif/encoder_worker.d.ts.map +1 -0
  52. package/src/engine/asset/loaders/image/avif/threaded_image_encoder.d.ts.map +1 -0
  53. package/src/engine/asset/loaders/image/codec/AvifCodec.d.ts +34 -0
  54. package/src/engine/asset/loaders/image/codec/AvifCodec.d.ts.map +1 -0
  55. package/src/engine/asset/loaders/image/codec/AvifCodec.js +85 -0
  56. package/src/engine/asset/loaders/image/codec/JpegCodec.d.ts +26 -0
  57. package/src/engine/asset/loaders/image/codec/JpegCodec.d.ts.map +1 -0
  58. package/src/engine/asset/loaders/image/codec/JpegCodec.js +66 -0
  59. package/src/engine/asset/loaders/image/codec/NativeImageDecoder.d.ts +5 -17
  60. package/src/engine/asset/loaders/image/codec/NativeImageDecoder.d.ts.map +1 -1
  61. package/src/engine/asset/loaders/image/codec/NativeImageDecoder.js +3 -3
  62. package/src/engine/asset/loaders/image/codec/ThreadedImageDecoder.d.ts +22 -2
  63. package/src/engine/asset/loaders/image/codec/ThreadedImageDecoder.d.ts.map +1 -1
  64. package/src/engine/asset/loaders/image/codec/ThreadedImageDecoder.js +22 -2
  65. package/src/engine/asset/loaders/image/prototypePNG.d.ts.map +1 -0
  66. package/src/engine/asset/loaders/image/prototypePNG.js +37 -0
  67. package/src/engine/asset/preloader/extractAssetListFromManager.d.ts.map +1 -1
  68. package/src/engine/asset/preloader/extractAssetListFromManager.js +2 -1
  69. package/src/engine/graphics/ecs/mesh/assetTypeByPath.d.ts.map +1 -1
  70. package/src/engine/graphics/ecs/mesh/assetTypeByPath.js +6 -0
  71. package/src/engine/graphics/texture/atlas/TextureAtlas.d.ts.map +1 -1
  72. package/src/engine/graphics/texture/atlas/TextureAtlas.js +13 -0
  73. package/src/engine/graphics/texture/sampler/avif_to_sampler2d.d.ts +25 -0
  74. package/src/engine/graphics/texture/sampler/avif_to_sampler2d.d.ts.map +1 -0
  75. package/src/engine/graphics/texture/sampler/avif_to_sampler2d.js +42 -0
  76. package/src/engine/graphics/texture/sampler/compare/sampler2d_compare.d.ts.map +1 -0
  77. package/src/engine/graphics/texture/sampler/compare/sampler2d_compare.js +94 -0
  78. package/src/engine/graphics/texture/sampler/compare/sampler2d_max_absolute_difference.d.ts +19 -0
  79. package/src/engine/graphics/texture/sampler/compare/sampler2d_max_absolute_difference.d.ts.map +1 -0
  80. package/src/engine/graphics/texture/sampler/compare/sampler2d_max_absolute_difference.js +62 -0
  81. package/src/engine/graphics/texture/sampler/compare/sampler2d_psnr.d.ts +30 -0
  82. package/src/engine/graphics/texture/sampler/compare/sampler2d_psnr.d.ts.map +1 -0
  83. package/src/engine/graphics/texture/sampler/compare/sampler2d_psnr.js +115 -0
  84. package/src/engine/graphics/texture/sampler/sampler2d_to_avif.d.ts.map +1 -0
  85. package/src/engine/graphics/texture/sampler/sampler2d_to_avif.js +108 -0
  86. package/src/engine/graphics/texture/sampler/sampler2d_to_f16.d.ts +7 -1
  87. package/src/engine/graphics/texture/sampler/sampler2d_to_f16.d.ts.map +1 -1
  88. package/src/engine/graphics/texture/sampler/sampler2d_to_f16.js +28 -21
  89. package/src/engine/graphics/texture/sampler/sampler2d_to_uint8_RGBA.d.ts.map +1 -1
  90. package/src/engine/graphics/texture/sampler/sampler2d_to_uint8_RGBA.js +3 -0
  91. package/src/engine/physics/broadphase/generate_pairs.d.ts.map +1 -1
  92. package/src/engine/physics/broadphase/generate_pairs.js +7 -1
  93. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  94. package/src/format/image/avif/DECISIONS.md +545 -0
  95. package/src/format/image/avif/api/AvifDecoder.d.ts.map +1 -0
  96. package/src/format/image/avif/api/apply_transformations.d.ts.map +1 -0
  97. package/src/format/image/avif/api/convert_to_rgba.d.ts.map +1 -0
  98. package/src/format/image/avif/api/convert_to_rgba.js +204 -0
  99. package/src/format/image/avif/api/decode_avif.d.ts.map +1 -0
  100. package/src/format/image/avif/api/decode_image_item.d.ts.map +1 -0
  101. package/src/format/image/avif/api/encode_avif.d.ts.map +1 -0
  102. package/src/format/image/avif/av1/decode/Av1FrameContext.d.ts.map +1 -0
  103. package/src/format/image/avif/av1/decode/block_decoded.d.ts.map +1 -0
  104. package/src/format/image/avif/av1/decode/decode_av1_still.d.ts.map +1 -0
  105. package/src/format/image/avif/av1/decode/decode_coefficients.d.ts.map +1 -0
  106. package/src/format/image/avif/av1/decode/decode_palette.d.ts.map +1 -0
  107. package/src/format/image/avif/av1/decode/decode_palette.js +442 -0
  108. package/src/format/image/avif/av1/decode/decode_tile.d.ts.map +1 -0
  109. package/src/format/image/avif/av1/decode/decode_tile.js +1213 -0
  110. package/src/format/image/avif/av1/decode/get_scan.d.ts.map +1 -0
  111. package/src/format/image/avif/av1/decode/read_lr.d.ts.map +1 -0
  112. package/src/format/image/avif/av1/decode/read_lr.js +277 -0
  113. package/src/format/image/avif/av1/decode/read_transform_type.d.ts.map +1 -0
  114. package/src/format/image/avif/av1/decode/read_transform_type.js +92 -0
  115. package/src/format/image/avif/av1/decode/reconstruct.d.ts.map +1 -0
  116. package/src/format/image/avif/av1/decode/reconstruct.js +241 -0
  117. package/src/format/image/avif/av1/decode/transform_type.d.ts.map +1 -0
  118. package/src/format/image/avif/av1/encode/Av1BlockSnapshot.d.ts.map +1 -0
  119. package/src/format/image/avif/av1/encode/Av1EncodeContext.d.ts.map +1 -0
  120. package/src/format/image/avif/av1/encode/build_headers.d.ts.map +1 -0
  121. package/src/format/image/avif/av1/encode/choose_chroma_alpha.d.ts.map +1 -0
  122. package/src/format/image/avif/av1/encode/choose_filters.d.ts.map +1 -0
  123. package/src/format/image/avif/av1/encode/choose_restoration.d.ts.map +1 -0
  124. package/src/format/image/avif/av1/encode/choose_restoration.js +616 -0
  125. package/src/format/image/avif/av1/encode/decode_effort.d.ts.map +1 -0
  126. package/src/format/image/avif/av1/encode/encode_av1_still.d.ts.map +1 -0
  127. package/src/format/image/avif/av1/encode/encode_effort.d.ts.map +1 -0
  128. package/src/format/image/avif/av1/encode/encode_tile.d.ts.map +1 -0
  129. package/src/format/image/avif/av1/encode/encode_tile.js +2331 -0
  130. package/src/format/image/avif/av1/encode/quantise.d.ts.map +1 -0
  131. package/src/format/image/avif/av1/encode/write_coefficients.d.ts.map +1 -0
  132. package/src/format/image/avif/av1/encode/write_lr.d.ts.map +1 -0
  133. package/src/format/image/avif/av1/encode/write_transform_type.d.ts.map +1 -0
  134. package/src/format/image/avif/av1/entropy/CdfContext.d.ts.map +1 -0
  135. package/src/format/image/avif/av1/entropy/SymbolReader.d.ts.map +1 -0
  136. package/src/format/image/avif/av1/entropy/SymbolWriter.d.ts.map +1 -0
  137. package/src/format/image/avif/av1/entropy/coefficient_context.d.ts.map +1 -0
  138. package/src/format/image/avif/av1/entropy/partition_cdf.d.ts.map +1 -0
  139. package/src/format/image/avif/av1/entropy/symbol_cost.d.ts.map +1 -0
  140. package/src/format/image/avif/av1/filter/cdef.d.ts.map +1 -0
  141. package/src/format/image/avif/av1/filter/cdef.js +313 -0
  142. package/src/format/image/avif/av1/filter/loop_filter.d.ts.map +1 -0
  143. package/src/format/image/avif/av1/filter/loop_filter.js +377 -0
  144. package/src/format/image/avif/av1/filter/loop_restoration.d.ts.map +1 -0
  145. package/src/format/image/avif/av1/filter/loop_restoration.js +451 -0
  146. package/src/format/image/avif/av1/filter/superres.d.ts.map +1 -0
  147. package/src/format/image/avif/av1/filter/superres.js +86 -0
  148. package/src/format/image/avif/av1/grain/FilmGrainState.d.ts.map +1 -0
  149. package/src/format/image/avif/av1/grain/film_grain.d.ts.map +1 -0
  150. package/src/format/image/avif/av1/grain/film_grain.js +623 -0
  151. package/src/format/image/avif/av1/obu/FrameHeader.d.ts.map +1 -0
  152. package/src/format/image/avif/av1/obu/ObuHeader.d.ts.map +1 -0
  153. package/src/format/image/avif/av1/obu/SequenceHeader.d.ts.map +1 -0
  154. package/src/format/image/avif/av1/obu/for_each_obu.d.ts.map +1 -0
  155. package/src/format/image/avif/av1/obu/parse_frame_header.d.ts.map +1 -0
  156. package/src/format/image/avif/av1/obu/parse_frame_header.js +841 -0
  157. package/src/format/image/avif/av1/obu/parse_obu_header.d.ts.map +1 -0
  158. package/src/format/image/avif/av1/obu/parse_sequence_header.d.ts.map +1 -0
  159. package/src/format/image/avif/av1/obu/write_frame_header.d.ts.map +1 -0
  160. package/src/format/image/avif/av1/obu/write_sequence_header.d.ts.map +1 -0
  161. package/src/format/image/avif/av1/predict/IntraPredictionState.d.ts.map +1 -0
  162. package/src/format/image/avif/av1/predict/intra_filter_type.d.ts.map +1 -0
  163. package/src/format/image/avif/av1/predict/predict_chroma_from_luma.d.ts.map +1 -0
  164. package/src/format/image/avif/av1/predict/predict_chroma_from_luma.js +153 -0
  165. package/src/format/image/avif/av1/predict/predict_intra.d.ts.map +1 -0
  166. package/src/format/image/avif/av1/predict/predict_intra.js +803 -0
  167. package/src/format/image/avif/av1/predict/predict_palette.d.ts.map +1 -0
  168. package/src/format/image/avif/av1/tables/av1_symbols.d.ts.map +1 -0
  169. package/src/format/image/avif/av1/tables/av1_symbols.js +419 -0
  170. package/src/format/image/avif/av1/tables/block_tables.d.ts.map +1 -0
  171. package/src/format/image/avif/av1/tables/block_tables.js +270 -0
  172. package/src/format/image/avif/av1/tables/cdf_tables.d.ts.map +1 -0
  173. package/src/format/image/avif/av1/tables/cdf_tables.js +1893 -0
  174. package/src/format/image/avif/av1/tables/coefficient_tables.d.ts.map +1 -0
  175. package/src/format/image/avif/av1/tables/coefficient_tables.js +82 -0
  176. package/src/format/image/avif/av1/tables/derived_block_tables.d.ts.map +1 -0
  177. package/src/format/image/avif/av1/tables/filter_tables.d.ts.map +1 -0
  178. package/src/format/image/avif/av1/tables/filter_tables.js +313 -0
  179. package/src/format/image/avif/av1/tables/grain_tables.d.ts.map +1 -0
  180. package/src/format/image/avif/av1/tables/grain_tables.js +190 -0
  181. package/src/format/image/avif/av1/tables/prediction_tables.d.ts.map +1 -0
  182. package/src/format/image/avif/av1/tables/prediction_tables.js +223 -0
  183. package/src/format/image/avif/av1/tables/quantizer_matrix_tables.d.ts.map +1 -0
  184. package/src/format/image/avif/av1/tables/quantizer_matrix_tables.js +38 -0
  185. package/src/format/image/avif/av1/tables/quantizer_tables.d.ts.map +1 -0
  186. package/src/format/image/avif/av1/tables/quantizer_tables.js +160 -0
  187. package/src/format/image/avif/av1/tables/scan_tables.d.ts.map +1 -0
  188. package/src/format/image/avif/av1/tables/scan_tables.js +727 -0
  189. package/src/format/image/avif/av1/tables/segmentation_tables.d.ts.map +1 -0
  190. package/src/format/image/avif/av1/tables/segmentation_tables.js +48 -0
  191. package/src/format/image/avif/av1/tables/transform_tables.d.ts.map +1 -0
  192. package/src/format/image/avif/av1/tables/transform_tables.js +140 -0
  193. package/src/format/image/avif/av1/tables/unpack_table.d.ts.map +1 -0
  194. package/src/format/image/avif/av1/tables/unpack_table.js +73 -0
  195. package/src/format/image/avif/av1/transform/forward_transform_2d.d.ts.map +1 -0
  196. package/src/format/image/avif/av1/transform/inverse_transform_1d.d.ts.map +1 -0
  197. package/src/format/image/avif/av1/transform/inverse_transform_1d.js +590 -0
  198. package/src/format/image/avif/av1/transform/inverse_transform_2d.d.ts.map +1 -0
  199. package/src/format/image/avif/av1/transform/inverse_transform_2d.js +180 -0
  200. package/src/format/image/avif/av1/util/ceil_log2.d.ts.map +1 -0
  201. package/src/format/image/avif/av1/util/floor_log2.d.ts +20 -0
  202. package/src/format/image/avif/av1/util/floor_log2.d.ts.map +1 -0
  203. package/src/format/image/avif/av1/util/floor_log2.js +20 -0
  204. package/src/format/image/avif/av1/util/round2.d.ts.map +1 -0
  205. package/src/format/image/avif/av1/util/round2_signed.d.ts.map +1 -0
  206. package/src/format/image/avif/av1/util/tile_log2.d.ts.map +1 -0
  207. package/src/format/image/avif/bitstream/BitReader.d.ts.map +1 -0
  208. package/src/format/image/avif/bitstream/BitWriter.d.ts.map +1 -0
  209. package/src/format/image/avif/color/ColourTransform.d.ts.map +1 -0
  210. package/src/format/image/avif/color/YuvImage.d.ts.map +1 -0
  211. package/src/format/image/avif/color/clamp_sample.d.ts.map +1 -0
  212. package/src/format/image/avif/color/linear_to_transfer.d.ts.map +1 -0
  213. package/src/format/image/avif/color/linear_to_transfer.js +126 -0
  214. package/src/format/image/avif/color/primaries.d.ts.map +1 -0
  215. package/src/format/image/avif/color/rgb_row_to_ycbcr.d.ts.map +1 -0
  216. package/src/format/image/avif/color/transfer_to_linear.d.ts.map +1 -0
  217. package/src/format/image/avif/color/transfer_to_linear.js +171 -0
  218. package/src/format/image/avif/color/upsample_chroma_row.d.ts.map +1 -0
  219. package/src/format/image/avif/color/upsample_chroma_row.js +89 -0
  220. package/src/format/image/avif/color/ycbcr_row_to_rgb.d.ts.map +1 -0
  221. package/src/format/image/avif/color/ycbcr_row_to_rgb.js +78 -0
  222. package/src/format/image/avif/heif/AvifFile.d.ts.map +1 -0
  223. package/src/format/image/avif/heif/ItemProperty.d.ts.map +1 -0
  224. package/src/format/image/avif/heif/find_item_property.d.ts.map +1 -0
  225. package/src/format/image/avif/heif/find_item_references.d.ts.map +1 -0
  226. package/src/format/image/avif/heif/find_items_referencing.d.ts.map +1 -0
  227. package/src/format/image/avif/heif/parse_avif_file.d.ts.map +1 -0
  228. package/src/format/image/avif/heif/parse_avif_file.js +384 -0
  229. package/src/format/image/avif/heif/parse_image_grid.d.ts.map +1 -0
  230. package/src/format/image/avif/heif/parse_image_grid.js +49 -0
  231. package/src/format/image/avif/heif/parse_item_property.d.ts.map +1 -0
  232. package/src/format/image/avif/heif/read_item_data.d.ts.map +1 -0
  233. package/src/format/image/avif/heif/write_avif_file.d.ts.map +1 -0
  234. package/src/format/image/avif/heif/write_item_property.d.ts.map +1 -0
  235. package/src/format/image/avif/index.d.ts +12 -0
  236. package/src/format/image/avif/index.d.ts.map +1 -0
  237. package/src/format/image/avif/index.js +58 -0
  238. package/src/format/image/avif/isobmff/BoxHeader.d.ts.map +1 -0
  239. package/src/format/image/avif/isobmff/BoxWriter.d.ts +118 -0
  240. package/src/format/image/avif/isobmff/BoxWriter.d.ts.map +1 -0
  241. package/src/format/image/avif/isobmff/BoxWriter.js +264 -0
  242. package/src/format/image/avif/isobmff/for_each_box.d.ts.map +1 -0
  243. package/src/format/image/avif/isobmff/read_box_header.d.ts.map +1 -0
  244. package/src/format/image/avif/isobmff/read_full_box_header.d.ts.map +1 -0
  245. package/src/format/image/jpeg/JpegFrame.d.ts.map +1 -0
  246. package/src/format/image/jpeg/JpegFrameComponent.d.ts.map +1 -0
  247. package/src/format/image/jpeg/JpegImage.d.ts.map +1 -0
  248. package/src/format/image/jpeg/JpegImage.js +664 -0
  249. package/src/format/image/jpeg/buildComponentData.d.ts.map +1 -0
  250. package/src/format/image/jpeg/buildHuffmanTable.d.ts.map +1 -0
  251. package/src/format/image/jpeg/decodeScan.d.ts.map +1 -0
  252. package/src/format/image/jpeg/idct8x8_float.d.ts.map +1 -0
  253. package/src/format/image/jpeg/idct8x8_float.js +31 -0
  254. package/src/format/image/jpeg/jpeg_component_row.d.ts.map +1 -0
  255. package/src/format/image/jpeg/jpeg_decode.d.ts.map +1 -0
  256. package/src/format/image/png/PNG.d.ts.map +1 -0
  257. package/src/format/image/png/PNGReader.d.ts +158 -0
  258. package/src/format/image/png/PNGReader.d.ts.map +1 -0
  259. package/src/format/image/png/PNGReader.js +656 -0
  260. package/src/format/image/png/PNG_HEADER_BYTES.d.ts.map +1 -0
  261. package/src/format/image/png/chunk/png_chunk_decode_iTXt.d.ts.map +1 -0
  262. package/src/format/image/png/chunk/png_chunk_decode_iTXt.js +53 -0
  263. package/src/format/image/png/chunk/png_chunk_decode_zTXt.d.ts.map +1 -0
  264. package/src/format/image/png/chunk/png_chunk_decode_zTXt.js +42 -0
  265. package/src/format/image/png/crc32.d.ts +2 -0
  266. package/src/format/image/png/crc32.d.ts.map +1 -0
  267. package/src/format/image/png/crc32.js +8 -0
  268. package/src/format/image/png/filter/png_filter_unFilterAverage.d.ts.map +1 -0
  269. package/src/format/image/png/filter/png_filter_unFilterNone.d.ts.map +1 -0
  270. package/src/format/image/png/filter/png_filter_unFilterPaeth.d.ts.map +1 -0
  271. package/src/format/image/png/filter/png_filter_unFilterSub.d.ts.map +1 -0
  272. package/src/format/image/png/filter/png_filter_unFilterUp.d.ts.map +1 -0
  273. package/src/format/image/png/filter/png_unfilter.d.ts.map +1 -0
  274. package/src/format/image/png/filter/png_unfilter.js +68 -0
  275. package/src/format/image/png/png_inflate.d.ts.map +1 -0
  276. package/src/format/image/png/png_unpack_samples.d.ts.map +1 -0
  277. package/src/format/scene/gltf/GLTF_PRIMITIVE_MODE.d.ts.map +1 -0
  278. package/src/format/scene/gltf/MESHOPT_COMPRESSION_PLAN.md +793 -0
  279. package/src/format/scene/gltf/coalesce_array_duplicates.d.ts.map +1 -0
  280. package/src/format/scene/gltf/ext/GltfBufferViewExtension.d.ts.map +1 -0
  281. package/src/format/scene/gltf/ext/GltfBufferViewExtensionSet.d.ts.map +1 -0
  282. package/src/format/scene/gltf/ext/GltfBufferViewExtensionSet.js +109 -0
  283. package/src/format/scene/gltf/ext/gltf_apply_buffer_view_extensions.d.ts.map +1 -0
  284. package/src/format/scene/gltf/ext/gltf_buffer_view_extensions.d.ts.map +1 -0
  285. package/src/format/scene/gltf/ext/gltf_geometry_extensions.d.ts.map +1 -0
  286. package/src/format/scene/gltf/ext/meshopt/MeshoptBufferViewExtension.d.ts.map +1 -0
  287. package/src/format/scene/gltf/ext/meshopt/MeshoptBufferViewExtension.js +301 -0
  288. package/src/format/scene/gltf/gltf_component_type.d.ts.map +1 -0
  289. package/src/format/scene/gltf/gltf_node_world_matrices.d.ts.map +1 -0
  290. package/src/format/scene/gltf/gltf_node_world_matrices.js +137 -0
  291. package/src/format/scene/gltf/gltf_parse_container.d.ts.map +1 -0
  292. package/src/format/scene/gltf/gltf_read_accessor.d.ts.map +1 -0
  293. package/src/format/scene/gltf/gltf_read_accessor.js +294 -0
  294. package/src/format/scene/gltf/gltf_used_image_indices.d.ts +51 -0
  295. package/src/format/scene/gltf/gltf_used_image_indices.d.ts.map +1 -0
  296. package/src/format/scene/gltf/gltf_used_image_indices.js +122 -0
  297. package/src/format/scene/usd/UsdError.d.ts.map +1 -0
  298. package/src/format/scene/usd/parse_usda.d.ts.map +1 -0
  299. package/src/format/scene/usd/unpack_usdz.d.ts.map +1 -0
  300. package/src/format/scene/usd/unpack_usdz.js +106 -0
  301. package/src/format/scene/usd/usd_compose_transform.d.ts.map +1 -0
  302. package/src/format/scene/usd/usd_compose_transform.js +258 -0
  303. package/src/format/scene/usd/usd_lz4.d.ts.map +1 -0
  304. package/src/format/scene/usd/usd_lz4.js +182 -0
  305. package/src/format/scene/usd/usd_triangulate.d.ts.map +1 -0
  306. package/src/format/texture/TexelPayload.d.ts +55 -0
  307. package/src/format/texture/TexelPayload.d.ts.map +1 -0
  308. package/src/format/texture/TexelPayload.js +39 -0
  309. package/src/format/texture/basis/BasisBitReader.d.ts +77 -0
  310. package/src/format/texture/basis/BasisBitReader.d.ts.map +1 -0
  311. package/src/format/texture/basis/BasisBitReader.js +231 -0
  312. package/src/format/texture/basis/BasisHuffmanTable.d.ts +53 -0
  313. package/src/format/texture/basis/BasisHuffmanTable.d.ts.map +1 -0
  314. package/src/format/texture/basis/BasisHuffmanTable.js +263 -0
  315. package/src/format/texture/basis/astc_block.d.ts +92 -0
  316. package/src/format/texture/basis/astc_block.d.ts.map +1 -0
  317. package/src/format/texture/basis/astc_block.js +164 -0
  318. package/src/format/texture/basis/astc_integer_sequence.d.ts +59 -0
  319. package/src/format/texture/basis/astc_integer_sequence.d.ts.map +1 -0
  320. package/src/format/texture/basis/astc_integer_sequence.js +251 -0
  321. package/src/format/texture/basis/astc_partition.d.ts +56 -0
  322. package/src/format/texture/basis/astc_partition.d.ts.map +1 -0
  323. package/src/format/texture/basis/astc_partition.js +199 -0
  324. package/src/format/texture/basis/basis_read_huffman_table.d.ts +20 -0
  325. package/src/format/texture/basis/basis_read_huffman_table.d.ts.map +1 -0
  326. package/src/format/texture/basis/basis_read_huffman_table.js +187 -0
  327. package/src/format/texture/basis/etc1s/Etc1sSelectorHistory.d.ts +50 -0
  328. package/src/format/texture/basis/etc1s/Etc1sSelectorHistory.d.ts.map +1 -0
  329. package/src/format/texture/basis/etc1s/Etc1sSelectorHistory.js +89 -0
  330. package/src/format/texture/basis/etc1s/etc1s_block_colors.d.ts +14 -0
  331. package/src/format/texture/basis/etc1s/etc1s_block_colors.d.ts.map +1 -0
  332. package/src/format/texture/basis/etc1s/etc1s_block_colors.js +75 -0
  333. package/src/format/texture/basis/etc1s/etc1s_decode_slice.d.ts +57 -0
  334. package/src/format/texture/basis/etc1s/etc1s_decode_slice.d.ts.map +1 -0
  335. package/src/format/texture/basis/etc1s/etc1s_decode_slice.js +290 -0
  336. package/src/format/texture/basis/etc1s/etc1s_read_endpoint_codebook.d.ts +48 -0
  337. package/src/format/texture/basis/etc1s/etc1s_read_endpoint_codebook.d.ts.map +1 -0
  338. package/src/format/texture/basis/etc1s/etc1s_read_endpoint_codebook.js +111 -0
  339. package/src/format/texture/basis/etc1s/etc1s_read_selector_codebook.d.ts +42 -0
  340. package/src/format/texture/basis/etc1s/etc1s_read_selector_codebook.d.ts.map +1 -0
  341. package/src/format/texture/basis/etc1s/etc1s_read_selector_codebook.js +127 -0
  342. package/src/format/texture/basis/etc1s/etc1s_read_slice_tables.d.ts +54 -0
  343. package/src/format/texture/basis/etc1s/etc1s_read_slice_tables.d.ts.map +1 -0
  344. package/src/format/texture/basis/etc1s/etc1s_read_slice_tables.js +47 -0
  345. package/src/format/texture/basis/transcode/build_range_plan.d.ts +46 -0
  346. package/src/format/texture/basis/transcode/build_range_plan.d.ts.map +1 -0
  347. package/src/format/texture/basis/transcode/build_range_plan.js +54 -0
  348. package/src/format/texture/basis/transcode/etc1s_to_astc.d.ts +30 -0
  349. package/src/format/texture/basis/transcode/etc1s_to_astc.d.ts.map +1 -0
  350. package/src/format/texture/basis/transcode/etc1s_to_astc.js +328 -0
  351. package/src/format/texture/basis/transcode/etc1s_to_bc1.d.ts +34 -0
  352. package/src/format/texture/basis/transcode/etc1s_to_bc1.d.ts.map +1 -0
  353. package/src/format/texture/basis/transcode/etc1s_to_bc1.js +486 -0
  354. package/src/format/texture/basis/transcode/etc1s_to_bc7.d.ts +27 -0
  355. package/src/format/texture/basis/transcode/etc1s_to_bc7.d.ts.map +1 -0
  356. package/src/format/texture/basis/transcode/etc1s_to_bc7.js +496 -0
  357. package/src/format/texture/basis/transcode/etc1s_to_etc2.d.ts +31 -0
  358. package/src/format/texture/basis/transcode/etc1s_to_etc2.d.ts.map +1 -0
  359. package/src/format/texture/basis/transcode/etc1s_to_etc2.js +332 -0
  360. package/src/format/texture/basis/transcode/etc1s_to_rgba8.d.ts +39 -0
  361. package/src/format/texture/basis/transcode/etc1s_to_rgba8.d.ts.map +1 -0
  362. package/src/format/texture/basis/transcode/etc1s_to_rgba8.js +115 -0
  363. package/src/format/texture/basis/transcode/fit_endpoints.d.ts +79 -0
  364. package/src/format/texture/basis/transcode/fit_endpoints.d.ts.map +1 -0
  365. package/src/format/texture/basis/transcode/fit_endpoints.js +297 -0
  366. package/src/format/texture/basis/transcode_basis.d.ts +109 -0
  367. package/src/format/texture/basis/transcode_basis.d.ts.map +1 -0
  368. package/src/format/texture/basis/transcode_basis.js +250 -0
  369. package/src/format/texture/basis/transcode_uastc.d.ts +55 -0
  370. package/src/format/texture/basis/transcode_uastc.d.ts.map +1 -0
  371. package/src/format/texture/basis/transcode_uastc.js +117 -0
  372. package/src/format/texture/basis/uastc/bc7_tables.d.ts +111 -0
  373. package/src/format/texture/basis/uastc/bc7_tables.d.ts.map +1 -0
  374. package/src/format/texture/basis/uastc/bc7_tables.js +276 -0
  375. package/src/format/texture/basis/uastc/bc7_write_block.d.ts +124 -0
  376. package/src/format/texture/basis/uastc/bc7_write_block.d.ts.map +1 -0
  377. package/src/format/texture/basis/uastc/bc7_write_block.js +227 -0
  378. package/src/format/texture/basis/uastc/uastc_decode_block.d.ts +105 -0
  379. package/src/format/texture/basis/uastc/uastc_decode_block.d.ts.map +1 -0
  380. package/src/format/texture/basis/uastc/uastc_decode_block.js +480 -0
  381. package/src/format/texture/basis/uastc/uastc_tables.d.ts +240 -0
  382. package/src/format/texture/basis/uastc/uastc_tables.d.ts.map +1 -0
  383. package/src/format/texture/basis/uastc/uastc_tables.js +383 -0
  384. package/src/format/texture/basis/uastc/uastc_to_astc.d.ts +30 -0
  385. package/src/format/texture/basis/uastc/uastc_to_astc.d.ts.map +1 -0
  386. package/src/format/texture/basis/uastc/uastc_to_astc.js +421 -0
  387. package/src/format/texture/basis/uastc/uastc_to_bc5.d.ts +26 -0
  388. package/src/format/texture/basis/uastc/uastc_to_bc5.d.ts.map +1 -0
  389. package/src/format/texture/basis/uastc/uastc_to_bc5.js +115 -0
  390. package/src/format/texture/basis/uastc/uastc_to_bc7.d.ts +36 -0
  391. package/src/format/texture/basis/uastc/uastc_to_bc7.d.ts.map +1 -0
  392. package/src/format/texture/basis/uastc/uastc_to_bc7.js +602 -0
  393. package/src/format/texture/basis/uastc/uastc_to_rgba8.d.ts +37 -0
  394. package/src/format/texture/basis/uastc/uastc_to_rgba8.d.ts.map +1 -0
  395. package/src/format/texture/basis/uastc/uastc_to_rgba8.js +87 -0
  396. package/src/format/texture/bc/bc1_write_block.d.ts +28 -0
  397. package/src/format/texture/bc/bc1_write_block.d.ts.map +1 -0
  398. package/src/format/texture/bc/bc1_write_block.js +41 -0
  399. package/src/format/texture/bc/bc4_write_block.d.ts +32 -0
  400. package/src/format/texture/bc/bc4_write_block.d.ts.map +1 -0
  401. package/src/format/texture/bc/bc4_write_block.js +50 -0
  402. package/src/format/texture/bc/bc6h_write_block_mode11.d.ts +30 -0
  403. package/src/format/texture/bc/bc6h_write_block_mode11.d.ts.map +1 -0
  404. package/src/format/texture/bc/bc6h_write_block_mode11.js +62 -0
  405. package/src/format/texture/ktx2/KTX2_IDENTIFIER.d.ts +14 -0
  406. package/src/format/texture/ktx2/KTX2_IDENTIFIER.d.ts.map +1 -0
  407. package/src/format/texture/ktx2/KTX2_IDENTIFIER.js +15 -0
  408. package/src/format/texture/ktx2/KhrDfModel.d.ts +35 -0
  409. package/src/format/texture/ktx2/KhrDfModel.d.ts.map +1 -0
  410. package/src/format/texture/ktx2/KhrDfModel.js +49 -0
  411. package/src/format/texture/ktx2/SupercompressionScheme.d.ts +16 -0
  412. package/src/format/texture/ktx2/SupercompressionScheme.d.ts.map +1 -0
  413. package/src/format/texture/ktx2/SupercompressionScheme.js +23 -0
  414. package/src/format/texture/ktx2/ktx2_read.d.ts +105 -0
  415. package/src/format/texture/ktx2/ktx2_read.d.ts.map +1 -0
  416. package/src/format/texture/ktx2/ktx2_read.js +909 -0
  417. package/src/format/texture/ktx2/ktx2_read_basis_lz_global_data.d.ts +27 -0
  418. package/src/format/texture/ktx2/ktx2_read_basis_lz_global_data.d.ts.map +1 -0
  419. package/src/format/texture/ktx2/ktx2_read_basis_lz_global_data.js +189 -0
  420. package/src/format/texture/ktx2/vk_format_to_texture_format.d.ts +55 -0
  421. package/src/format/texture/ktx2/vk_format_to_texture_format.d.ts.map +1 -0
  422. package/src/format/texture/ktx2/vk_format_to_texture_format.js +290 -0
  423. package/src/shade/descriptor/GPUTextureUsage.d.ts +23 -0
  424. package/src/shade/descriptor/GPUTextureUsage.d.ts.map +1 -0
  425. package/src/shade/descriptor/GPUTextureUsage.js +24 -0
  426. package/src/shade/descriptor/WebGPUExtensionType.d.ts +3 -0
  427. package/src/shade/descriptor/WebGPUExtensionType.js +26 -0
  428. package/src/shade/descriptor/texture/TextureDescriptor.d.ts +2 -6
  429. package/src/shade/descriptor/texture/TextureDescriptor.d.ts.map +1 -1
  430. package/src/shade/descriptor/texture/TextureDescriptor.js +24 -40
  431. package/src/shade/descriptor/texture/format/TextureFormat.d.ts +125 -0
  432. package/src/shade/descriptor/texture/format/TextureFormat.d.ts.map +1 -0
  433. package/src/shade/descriptor/texture/format/TextureFormat.js +146 -0
  434. package/src/shade/descriptor/texture/format/gpu_texture_format_feature.d.ts +16 -0
  435. package/src/shade/descriptor/texture/format/gpu_texture_format_feature.d.ts.map +1 -0
  436. package/src/shade/descriptor/texture/format/gpu_texture_format_feature.js +44 -0
  437. package/src/shade/descriptor/texture/format/gpu_texture_format_info.d.ts +76 -0
  438. package/src/shade/descriptor/texture/format/gpu_texture_format_info.d.ts.map +1 -1
  439. package/src/shade/descriptor/texture/format/gpu_texture_format_info.js +126 -0
  440. package/src/shade/descriptor/texture/format/gpu_texture_format_srgb.d.ts +19 -0
  441. package/src/shade/descriptor/texture/format/gpu_texture_format_srgb.d.ts.map +1 -0
  442. package/src/shade/descriptor/texture/format/gpu_texture_format_srgb.js +50 -0
  443. package/src/shade/device/COPY_BYTES_PER_ROW_ALIGNMENT.d.ts +14 -0
  444. package/src/shade/device/COPY_BYTES_PER_ROW_ALIGNMENT.d.ts.map +1 -0
  445. package/src/shade/device/COPY_BYTES_PER_ROW_ALIGNMENT.js +13 -0
  446. package/src/shade/device/graph/TextureResourceDescriptor.d.ts.map +1 -1
  447. package/src/shade/device/graph/TextureResourceDescriptor.js +160 -155
  448. package/src/shade/device/mock/SoftwareGPUDevice.d.ts.map +1 -1
  449. package/src/shade/device/mock/SoftwareGPUDevice.js +37 -3
  450. package/src/shade/device/mock/SoftwareGPUQueue.d.ts +11 -4
  451. package/src/shade/device/mock/SoftwareGPUQueue.d.ts.map +1 -1
  452. package/src/shade/device/mock/SoftwareGPUQueue.js +31 -37
  453. package/src/shade/device/mock/SoftwareGPUTexture.d.ts +4 -2
  454. package/src/shade/device/mock/SoftwareGPUTexture.d.ts.map +1 -1
  455. package/src/shade/device/mock/SoftwareGPUTexture.js +47 -7
  456. package/src/shade/device/mock/SoftwareGPUTextureView.d.ts +7 -0
  457. package/src/shade/device/mock/SoftwareGPUTextureView.d.ts.map +1 -1
  458. package/src/shade/device/mock/SoftwareGPUTextureView.js +8 -0
  459. package/src/shade/device/mock/texture_copy.d.ts.map +1 -1
  460. package/src/shade/device/mock/texture_copy.js +74 -12
  461. package/src/shade/device/timing/profile/frame_graph_extract_topology.d.ts.map +1 -1
  462. package/src/shade/device/timing/profile/frame_graph_extract_topology.js +164 -174
  463. package/src/shade/playground/basis_textures/README.md +105 -0
  464. package/src/shade/playground/basis_textures/index.html +141 -0
  465. package/src/shade/playground/basis_textures/main.d.ts +8 -0
  466. package/src/shade/playground/basis_textures/main.d.ts.map +1 -0
  467. package/src/shade/playground/basis_textures/main.js +351 -0
  468. package/src/shade/playground/basis_textures/make_basis_gltf.d.mts +2 -0
  469. package/src/shade/playground/basis_textures/make_basis_gltf.d.mts.map +1 -0
  470. package/src/shade/playground/basis_textures/make_basis_gltf.mjs +197 -0
  471. package/src/shade/playground/basis_textures/node_modules/basis_universal/bin/basisu.exe +0 -0
  472. package/src/shade/playground/basis_textures/texture_inventory.d.ts +28 -0
  473. package/src/shade/playground/basis_textures/texture_inventory.d.ts.map +1 -0
  474. package/src/shade/playground/basis_textures/texture_inventory.js +98 -0
  475. package/src/shade/playground/ground_seam/README.md +235 -0
  476. package/src/shade/playground/ground_seam/capture_scene_color.d.ts +45 -0
  477. package/src/shade/playground/ground_seam/capture_scene_color.d.ts.map +1 -0
  478. package/src/shade/playground/ground_seam/capture_scene_color.js +242 -0
  479. package/src/shade/playground/ground_seam/depth_step_across_seam.d.ts +11 -0
  480. package/src/shade/playground/ground_seam/depth_step_across_seam.d.ts.map +1 -0
  481. package/src/shade/playground/ground_seam/depth_step_across_seam.js +185 -0
  482. package/src/shade/playground/ground_seam/index.html +195 -0
  483. package/src/shade/playground/ground_seam/main.d.ts +2 -0
  484. package/src/shade/playground/ground_seam/main.d.ts.map +1 -0
  485. package/src/shade/playground/ground_seam/main.js +879 -0
  486. package/src/shade/playground/ground_seam/make_grid_texture.d.ts +29 -0
  487. package/src/shade/playground/ground_seam/make_grid_texture.d.ts.map +1 -0
  488. package/src/shade/playground/ground_seam/make_grid_texture.js +105 -0
  489. package/src/shade/playground/ground_seam/make_ground.d.ts +70 -0
  490. package/src/shade/playground/ground_seam/make_ground.d.ts.map +1 -0
  491. package/src/shade/playground/ground_seam/make_ground.js +161 -0
  492. package/src/shade/playground/ground_seam/measure_flicker.d.ts +71 -0
  493. package/src/shade/playground/ground_seam/measure_flicker.d.ts.map +1 -0
  494. package/src/shade/playground/ground_seam/measure_flicker.js +531 -0
  495. package/src/shade/playground/image_decode_bench/README.md +42 -0
  496. package/src/shade/playground/image_decode_bench/decode_worker.d.ts +2 -0
  497. package/src/shade/playground/image_decode_bench/decode_worker.d.ts.map +1 -0
  498. package/src/shade/playground/image_decode_bench/decode_worker.js +19 -0
  499. package/src/shade/playground/image_decode_bench/index.html +34 -0
  500. package/src/shade/playground/image_decode_bench/main.d.ts +2 -0
  501. package/src/shade/playground/image_decode_bench/main.d.ts.map +1 -0
  502. package/src/shade/playground/image_decode_bench/main.js +211 -0
  503. package/src/shade/playground/skinned_blas_refit/README.md +1 -1
  504. package/src/shade/playground/skinned_blas_refit/index.html +15 -1
  505. package/src/shade/playground/skinned_blas_refit/main.d.ts.map +1 -1
  506. package/src/shade/playground/skinned_blas_refit/main.js +339 -3
  507. package/src/shade/playground/skinned_blas_refit/shader_blas_audit_leaf_triangles.d.ts +8 -1
  508. package/src/shade/playground/skinned_blas_refit/shader_blas_audit_leaf_triangles.d.ts.map +1 -1
  509. package/src/shade/playground/skinned_blas_refit/shader_blas_audit_leaf_triangles.js +27 -5
  510. package/src/shade/playground/skinned_blas_refit/verify_clone_leaves.d.ts +27 -0
  511. package/src/shade/playground/skinned_blas_refit/verify_clone_leaves.d.ts.map +1 -0
  512. package/src/shade/playground/skinned_blas_refit/verify_clone_leaves.js +127 -0
  513. package/src/shade/playground/skinned_blas_refit/verify_leaf_triangles.d.ts.map +1 -1
  514. package/src/shade/playground/skinned_blas_refit/verify_leaf_triangles.js +3 -3
  515. package/src/shade/playground/skinned_blas_refit/verify_skinned_blas_refit.d.ts.map +1 -1
  516. package/src/shade/playground/skinned_blas_refit/verify_skinned_blas_refit.js +10 -8
  517. package/src/shade/playground/texture_encode/README.md +109 -0
  518. package/src/shade/playground/texture_encode/index.html +133 -0
  519. package/src/shade/playground/texture_encode/main.d.ts +24 -0
  520. package/src/shade/playground/texture_encode/main.d.ts.map +1 -0
  521. package/src/shade/playground/texture_encode/main.js +1430 -0
  522. package/src/shade/playground/texture_encode/panel_view.d.ts +118 -0
  523. package/src/shade/playground/texture_encode/panel_view.d.ts.map +1 -0
  524. package/src/shade/playground/texture_encode/panel_view.js +363 -0
  525. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  526. package/src/shade/renderer/Renderer.js +2519 -2511
  527. package/src/shade/renderer/animation/GPUAnimationManager.d.ts.map +1 -1
  528. package/src/shade/renderer/animation/GPUAnimationManager.js +49 -5
  529. package/src/shade/renderer/animation/skinning/GPUMeshSkinningContext.d.ts.map +1 -1
  530. package/src/shade/renderer/animation/skinning/GPUMeshSkinningContext.js +32 -14
  531. package/src/shade/renderer/geometry/GPUGeometryManager.d.ts.map +1 -1
  532. package/src/shade/renderer/geometry/GPUGeometryManager.js +591 -497
  533. package/src/shade/renderer/geometry/GPUGeometryMetadata.d.ts +9 -0
  534. package/src/shade/renderer/geometry/GPUGeometryMetadata.d.ts.map +1 -1
  535. package/src/shade/renderer/geometry/GPUGeometryMetadata.js +10 -0
  536. package/src/shade/renderer/geometry/bvh/BLAS_REFIT_BATCH_PLAN.md +606 -0
  537. package/src/shade/renderer/geometry/bvh/BLAS_REFIT_TARGET_STRUCT.d.ts +17 -0
  538. package/src/shade/renderer/geometry/bvh/BLAS_REFIT_TARGET_STRUCT.d.ts.map +1 -0
  539. package/src/shade/renderer/geometry/bvh/BLAS_REFIT_TARGET_STRUCT.js +44 -0
  540. package/src/shade/renderer/geometry/bvh/GPUBLASRefitBatch.d.ts +58 -0
  541. package/src/shade/renderer/geometry/bvh/GPUBLASRefitBatch.d.ts.map +1 -0
  542. package/src/shade/renderer/geometry/bvh/GPUBLASRefitBatch.js +362 -0
  543. package/src/shade/renderer/geometry/bvh/GPUGeometryBVHManager.d.ts +38 -0
  544. package/src/shade/renderer/geometry/bvh/GPUGeometryBVHManager.d.ts.map +1 -1
  545. package/src/shade/renderer/geometry/bvh/GPUGeometryBVHManager.js +730 -617
  546. package/src/shade/renderer/geometry/bvh/GPUGeometryMeshletsBVH.d.ts +15 -2
  547. package/src/shade/renderer/geometry/bvh/GPUGeometryMeshletsBVH.d.ts.map +1 -1
  548. package/src/shade/renderer/geometry/bvh/GPUGeometryMeshletsBVH.js +15 -1
  549. package/src/shade/renderer/geometry/bvh/shader_blas_refit_batch.d.ts +39 -0
  550. package/src/shade/renderer/geometry/bvh/shader_blas_refit_batch.d.ts.map +1 -0
  551. package/src/shade/renderer/geometry/bvh/shader_blas_refit_batch.js +134 -0
  552. package/src/shade/renderer/geometry/virtual/build/gltf/gltf_collect_geometries.js +3 -3
  553. package/src/shade/renderer/geometry/virtual/build/gltf/gltf_open_document.js +3 -3
  554. package/src/shade/renderer/geometry/virtual/build/gltf/gltf_read_geometry.js +1 -1
  555. package/src/shade/renderer/gpu_primitive/bvh/BVH_NODE_LAYOUT_DEFINITION.d.ts +17 -0
  556. package/src/shade/renderer/gpu_primitive/bvh/BVH_NODE_LAYOUT_DEFINITION.d.ts.map +1 -0
  557. package/src/shade/renderer/gpu_primitive/bvh/BVH_NODE_LAYOUT_DEFINITION.js +30 -0
  558. package/src/shade/renderer/gpu_primitive/bvh/GPU_BVH_BUILD_PLAN.md +8 -1
  559. package/src/shade/renderer/gpu_primitive/bvh/chunk_bvh_refit_climb.d.ts +34 -0
  560. package/src/shade/renderer/gpu_primitive/bvh/chunk_bvh_refit_climb.d.ts.map +1 -0
  561. package/src/shade/renderer/gpu_primitive/bvh/chunk_bvh_refit_climb.js +102 -0
  562. package/src/shade/renderer/gpu_primitive/bvh/shader_bvh_refit.d.ts +11 -9
  563. package/src/shade/renderer/gpu_primitive/bvh/shader_bvh_refit.d.ts.map +1 -1
  564. package/src/shade/renderer/gpu_primitive/bvh/shader_bvh_refit.js +24 -86
  565. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts.map +1 -1
  566. package/src/shade/renderer/hiz/HierarchicalZBuffer.js +275 -274
  567. package/src/shade/renderer/light/GPULightCollection.d.ts.map +1 -1
  568. package/src/shade/renderer/light/GPULightCollection.js +323 -314
  569. package/src/shade/renderer/lightmap/GPULightMap.js +336 -336
  570. package/src/shade/renderer/loader/gltf/gltf_create_texture.d.ts +18 -0
  571. package/src/shade/renderer/loader/gltf/gltf_create_texture.d.ts.map +1 -0
  572. package/src/shade/renderer/loader/gltf/gltf_create_texture.js +103 -0
  573. package/src/shade/renderer/loader/gltf/gltf_image_roles.d.ts +26 -0
  574. package/src/shade/renderer/loader/gltf/gltf_image_roles.d.ts.map +1 -0
  575. package/src/shade/renderer/loader/gltf/gltf_image_roles.js +108 -0
  576. package/src/shade/renderer/loader/gltf/load_gltf.d.ts +10 -3
  577. package/src/shade/renderer/loader/gltf/load_gltf.d.ts.map +1 -1
  578. package/src/shade/renderer/loader/gltf/load_gltf.js +1179 -1253
  579. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +18 -1
  580. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts.map +1 -1
  581. package/src/shade/renderer/loader/gltf/tiny-gltf.js +837 -646
  582. package/src/shade/renderer/loader/usd/load_usd.js +3 -3
  583. package/src/shade/renderer/loader/usd/usd_build_scene.js +2 -2
  584. package/src/shade/renderer/loader/usd/usd_decode_image.d.ts +6 -5
  585. package/src/shade/renderer/loader/usd/usd_decode_image.d.ts.map +1 -1
  586. package/src/shade/renderer/loader/usd/usd_decode_image.js +197 -384
  587. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts.map +1 -1
  588. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.js +12 -2
  589. package/src/shade/renderer/material/standard/fragment_gbuffer.d.ts.map +1 -1
  590. package/src/shade/renderer/material/standard/fragment_gbuffer.js +3 -1
  591. package/src/shade/renderer/material/standard/fragment_gbuffer_vt.js +169 -169
  592. package/src/shade/renderer/path_tracer/shader/chunk_sample_material_data.d.ts.map +1 -1
  593. package/src/shade/renderer/path_tracer/shader/chunk_sample_material_data.js +3 -1
  594. package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
  595. package/src/shade/renderer/postprocess/taa/TAA.d.ts +3 -3
  596. package/src/shade/renderer/postprocess/taa/TAA.d.ts.map +1 -1
  597. package/src/shade/renderer/postprocess/taa/TAA.js +2 -6
  598. package/src/shade/renderer/postprocess/taa/chunk_depth_clip_offset_codec.d.ts +18 -0
  599. package/src/shade/renderer/postprocess/taa/chunk_depth_clip_offset_codec.d.ts.map +1 -0
  600. package/src/shade/renderer/postprocess/taa/chunk_depth_clip_offset_codec.js +36 -0
  601. package/src/shade/renderer/postprocess/taa/chunk_taa_history_color_bounding_box_YCoCg.d.ts.map +1 -1
  602. package/src/shade/renderer/postprocess/taa/chunk_taa_history_color_bounding_box_YCoCg.js +4 -1
  603. package/src/shade/renderer/postprocess/taa/shader_depth_occlusion_clip.d.ts.map +1 -1
  604. package/src/shade/renderer/postprocess/taa/shader_depth_occlusion_clip.js +13 -8
  605. package/src/shade/renderer/postprocess/taa/shader_taa.d.ts +1 -1
  606. package/src/shade/renderer/postprocess/taa/shader_taa.d.ts.map +1 -1
  607. package/src/shade/renderer/postprocess/taa/shader_taa.js +52 -38
  608. package/src/shade/renderer/rasterize/native/oit/chunk_forward_shade_standard_fragment_brick4.d.ts.map +1 -1
  609. package/src/shade/renderer/rasterize/native/oit/chunk_forward_shade_standard_fragment_brick4.js +3 -1
  610. package/src/shade/renderer/rasterize/native/oit/chunk_forward_shade_standard_fragment_ibl.d.ts.map +1 -1
  611. package/src/shade/renderer/rasterize/native/oit/chunk_forward_shade_standard_fragment_ibl.js +3 -1
  612. package/src/shade/renderer/restir/di/chunk_restir_temporal_reuse.d.ts +4 -2
  613. package/src/shade/renderer/restir/di/chunk_restir_temporal_reuse.d.ts.map +1 -1
  614. package/src/shade/renderer/restir/di/chunk_restir_temporal_reuse.js +9 -6
  615. package/src/shade/renderer/scene/optimization/deduplicate_images.js +93 -93
  616. package/src/shade/renderer/scene/optimization/downscale_images.js +216 -216
  617. package/src/shade/renderer/scene/serialization/decode_image_source_hdr.js +1 -1
  618. package/src/shade/renderer/scene/serialization/deserialize_scene.js +1 -1
  619. package/src/shade/renderer/scene/serialization/write_image_source.js +1 -1
  620. package/src/shade/renderer/shader/chunk/color/iCtCp/chunk_eotf_st2084.d.ts +1 -1
  621. package/src/shade/renderer/shader/chunk/color/iCtCp/chunk_eotf_st2084.js +1 -1
  622. package/src/shade/renderer/shader/chunk/geometry/normal/chunk_decode_tangent_normal.d.ts +28 -0
  623. package/src/shade/renderer/shader/chunk/geometry/normal/chunk_decode_tangent_normal.d.ts.map +1 -0
  624. package/src/shade/renderer/shader/chunk/geometry/normal/chunk_decode_tangent_normal.js +39 -0
  625. package/src/shade/renderer/shader/chunk/geometry/triangle/chunk_barycentric_full.d.ts +45 -0
  626. package/src/shade/renderer/shader/chunk/geometry/triangle/chunk_barycentric_full.d.ts.map +1 -1
  627. package/src/shade/renderer/shader/chunk/geometry/triangle/chunk_barycentric_full.js +94 -61
  628. package/src/shade/renderer/shader/chunk/geometry/triangle/chunk_barycentric_solve.d.ts +24 -0
  629. package/src/shade/renderer/shader/chunk/geometry/triangle/chunk_barycentric_solve.d.ts.map +1 -0
  630. package/src/shade/renderer/shader/chunk/geometry/triangle/chunk_barycentric_solve.js +43 -0
  631. package/src/shade/renderer/shader/chunk/texture/sample/chunk_texture_sample_catmullrom_hardware_5tap_uv.d.ts +2 -1
  632. package/src/shade/renderer/shader/chunk/texture/sample/chunk_texture_sample_catmullrom_hardware_5tap_uv.d.ts.map +1 -1
  633. package/src/shade/renderer/shader/chunk/texture/sample/chunk_texture_sample_catmullrom_hardware_5tap_uv.js +2 -1
  634. package/src/shade/renderer/texture/COMPRESSED_TEXTURE_PLAN_2026_09_01.md +2345 -0
  635. package/src/shade/renderer/texture/TextureManager.d.ts.map +1 -1
  636. package/src/shade/renderer/texture/TextureManager.js +20 -8
  637. package/src/shade/renderer/texture/TextureRole.d.ts +19 -0
  638. package/src/shade/renderer/texture/TextureRole.d.ts.map +1 -0
  639. package/src/shade/renderer/texture/TextureRole.js +26 -0
  640. package/src/shade/renderer/texture/TextureVariantSet.d.ts +71 -0
  641. package/src/shade/renderer/texture/TextureVariantSet.d.ts.map +1 -0
  642. package/src/shade/renderer/texture/TextureVariantSet.js +105 -0
  643. package/src/shade/renderer/texture/download_texture_data.d.ts.map +1 -1
  644. package/src/shade/renderer/texture/download_texture_data.js +22 -0
  645. package/src/shade/renderer/texture/encode/ENCODE_NOTES_2026_09_02.md +462 -0
  646. package/src/shade/renderer/texture/encode/chunk_astc_encode_block_4x4_rgb.d.ts +33 -0
  647. package/src/shade/renderer/texture/encode/chunk_astc_encode_block_4x4_rgb.d.ts.map +1 -0
  648. package/src/shade/renderer/texture/encode/chunk_astc_encode_block_4x4_rgb.js +310 -0
  649. package/src/shade/renderer/texture/encode/chunk_astc_pack_4x4_rgb.d.ts +17 -0
  650. package/src/shade/renderer/texture/encode/chunk_astc_pack_4x4_rgb.d.ts.map +1 -0
  651. package/src/shade/renderer/texture/encode/chunk_astc_pack_4x4_rgb.js +51 -0
  652. package/src/shade/renderer/texture/encode/chunk_bc1_encode_block.d.ts +42 -0
  653. package/src/shade/renderer/texture/encode/chunk_bc1_encode_block.d.ts.map +1 -0
  654. package/src/shade/renderer/texture/encode/chunk_bc1_encode_block.js +375 -0
  655. package/src/shade/renderer/texture/encode/chunk_bc1_pack_block.d.ts +12 -0
  656. package/src/shade/renderer/texture/encode/chunk_bc1_pack_block.d.ts.map +1 -0
  657. package/src/shade/renderer/texture/encode/chunk_bc1_pack_block.js +24 -0
  658. package/src/shade/renderer/texture/encode/chunk_bc4_encode_block.d.ts +45 -0
  659. package/src/shade/renderer/texture/encode/chunk_bc4_encode_block.d.ts.map +1 -0
  660. package/src/shade/renderer/texture/encode/chunk_bc4_encode_block.js +348 -0
  661. package/src/shade/renderer/texture/encode/chunk_bc4_pack_block.d.ts +16 -0
  662. package/src/shade/renderer/texture/encode/chunk_bc4_pack_block.d.ts.map +1 -0
  663. package/src/shade/renderer/texture/encode/chunk_bc4_pack_block.js +33 -0
  664. package/src/shade/renderer/texture/encode/chunk_bc6h_encode_block_mode11.d.ts +32 -0
  665. package/src/shade/renderer/texture/encode/chunk_bc6h_encode_block_mode11.d.ts.map +1 -0
  666. package/src/shade/renderer/texture/encode/chunk_bc6h_encode_block_mode11.js +346 -0
  667. package/src/shade/renderer/texture/encode/chunk_bc6h_pack_mode11.d.ts +10 -0
  668. package/src/shade/renderer/texture/encode/chunk_bc6h_pack_mode11.d.ts.map +1 -0
  669. package/src/shade/renderer/texture/encode/chunk_bc6h_pack_mode11.js +54 -0
  670. package/src/shade/renderer/texture/encode/chunk_bc7_encode_block_mode6.d.ts +36 -0
  671. package/src/shade/renderer/texture/encode/chunk_bc7_encode_block_mode6.d.ts.map +1 -0
  672. package/src/shade/renderer/texture/encode/chunk_bc7_encode_block_mode6.js +327 -0
  673. package/src/shade/renderer/texture/encode/chunk_bc7_pack_mode6.d.ts +22 -0
  674. package/src/shade/renderer/texture/encode/chunk_bc7_pack_mode6.d.ts.map +1 -0
  675. package/src/shade/renderer/texture/encode/chunk_bc7_pack_mode6.js +71 -0
  676. package/src/shade/renderer/texture/encode/chunk_texture_encode_level.d.ts +20 -0
  677. package/src/shade/renderer/texture/encode/chunk_texture_encode_level.d.ts.map +1 -0
  678. package/src/shade/renderer/texture/encode/chunk_texture_encode_level.js +73 -0
  679. package/src/shade/renderer/texture/encode/chunk_texture_encode_load_block.d.ts +16 -0
  680. package/src/shade/renderer/texture/encode/chunk_texture_encode_load_block.d.ts.map +1 -0
  681. package/src/shade/renderer/texture/encode/chunk_texture_encode_load_block.js +36 -0
  682. package/src/shade/renderer/texture/encode/shader_texture_encode_astc_4x4.d.ts +13 -0
  683. package/src/shade/renderer/texture/encode/shader_texture_encode_astc_4x4.d.ts.map +1 -0
  684. package/src/shade/renderer/texture/encode/shader_texture_encode_astc_4x4.js +63 -0
  685. package/src/shade/renderer/texture/encode/shader_texture_encode_bc1.d.ts +12 -0
  686. package/src/shade/renderer/texture/encode/shader_texture_encode_bc1.d.ts.map +1 -0
  687. package/src/shade/renderer/texture/encode/shader_texture_encode_bc1.js +60 -0
  688. package/src/shade/renderer/texture/encode/shader_texture_encode_bc4.d.ts +28 -0
  689. package/src/shade/renderer/texture/encode/shader_texture_encode_bc4.d.ts.map +1 -0
  690. package/src/shade/renderer/texture/encode/shader_texture_encode_bc4.js +76 -0
  691. package/src/shade/renderer/texture/encode/shader_texture_encode_bc4_snorm.d.ts +16 -0
  692. package/src/shade/renderer/texture/encode/shader_texture_encode_bc4_snorm.d.ts.map +1 -0
  693. package/src/shade/renderer/texture/encode/shader_texture_encode_bc4_snorm.js +64 -0
  694. package/src/shade/renderer/texture/encode/shader_texture_encode_bc5.d.ts +14 -0
  695. package/src/shade/renderer/texture/encode/shader_texture_encode_bc5.d.ts.map +1 -0
  696. package/src/shade/renderer/texture/encode/shader_texture_encode_bc5.js +67 -0
  697. package/src/shade/renderer/texture/encode/shader_texture_encode_bc5_snorm.d.ts +10 -0
  698. package/src/shade/renderer/texture/encode/shader_texture_encode_bc5_snorm.d.ts.map +1 -0
  699. package/src/shade/renderer/texture/encode/shader_texture_encode_bc5_snorm.js +63 -0
  700. package/src/shade/renderer/texture/encode/shader_texture_encode_bc6h.d.ts +12 -0
  701. package/src/shade/renderer/texture/encode/shader_texture_encode_bc6h.d.ts.map +1 -0
  702. package/src/shade/renderer/texture/encode/shader_texture_encode_bc6h.js +62 -0
  703. package/src/shade/renderer/texture/encode/shader_texture_encode_bc7.d.ts +14 -0
  704. package/src/shade/renderer/texture/encode/shader_texture_encode_bc7.d.ts.map +1 -0
  705. package/src/shade/renderer/texture/encode/shader_texture_encode_bc7.js +58 -0
  706. package/src/shade/renderer/texture/encode/texture_encode.d.ts +84 -0
  707. package/src/shade/renderer/texture/encode/texture_encode.d.ts.map +1 -0
  708. package/src/shade/renderer/texture/encode/texture_encode.js +168 -0
  709. package/src/shade/renderer/texture/encode/texture_encode_layout.d.ts +196 -0
  710. package/src/shade/renderer/texture/encode/texture_encode_layout.d.ts.map +1 -0
  711. package/src/shade/renderer/texture/encode/texture_encode_layout.js +248 -0
  712. package/src/shade/renderer/texture/encode/texture_encode_resources.d.ts +15 -0
  713. package/src/shade/renderer/texture/encode/texture_encode_resources.d.ts.map +1 -0
  714. package/src/shade/renderer/texture/encode/texture_encode_resources.js +25 -0
  715. package/src/shade/renderer/texture/encode/texture_encode_shader.d.ts +30 -0
  716. package/src/shade/renderer/texture/encode/texture_encode_shader.d.ts.map +1 -0
  717. package/src/shade/renderer/texture/encode/texture_encode_shader.js +108 -0
  718. package/src/shade/renderer/texture/format/gpu_texture_compression_support.d.ts +17 -0
  719. package/src/shade/renderer/texture/format/gpu_texture_compression_support.d.ts.map +1 -0
  720. package/src/shade/renderer/texture/format/gpu_texture_compression_support.js +33 -0
  721. package/src/shade/renderer/texture/format/gpu_texture_format_can_generate_mipmaps.d.ts +27 -0
  722. package/src/shade/renderer/texture/format/gpu_texture_format_can_generate_mipmaps.d.ts.map +1 -0
  723. package/src/shade/renderer/texture/format/gpu_texture_format_can_generate_mipmaps.js +65 -0
  724. package/src/shade/renderer/texture/format/infer_format_from_parameters.d.ts +11 -0
  725. package/src/shade/renderer/texture/format/infer_format_from_parameters.d.ts.map +1 -0
  726. package/src/shade/renderer/texture/format/infer_format_from_parameters.js +88 -0
  727. package/src/shade/renderer/texture/format/texture_format_from_shade_image.d.ts +24 -0
  728. package/src/shade/renderer/texture/format/texture_format_from_shade_image.d.ts.map +1 -0
  729. package/src/shade/renderer/texture/format/texture_format_from_shade_image.js +95 -0
  730. package/src/shade/renderer/texture/format/texture_format_preference.d.ts +22 -0
  731. package/src/shade/renderer/texture/format/texture_format_preference.d.ts.map +1 -0
  732. package/src/shade/renderer/texture/format/texture_format_preference.js +89 -0
  733. package/src/shade/renderer/texture/format/texture_variant_select.d.ts +24 -0
  734. package/src/shade/renderer/texture/format/texture_variant_select.d.ts.map +1 -0
  735. package/src/shade/renderer/texture/format/texture_variant_select.js +38 -0
  736. package/src/shade/renderer/texture/shade_texture_might_have_alpha.d.ts +9 -1
  737. package/src/shade/renderer/texture/shade_texture_might_have_alpha.d.ts.map +1 -1
  738. package/src/shade/renderer/texture/shade_texture_might_have_alpha.js +73 -8
  739. package/src/shade/renderer/texture/source/AlphaMode.d.ts +27 -0
  740. package/src/shade/renderer/texture/source/AlphaMode.d.ts.map +1 -0
  741. package/src/shade/renderer/texture/source/AlphaMode.js +52 -0
  742. package/src/shade/renderer/texture/source/ShadeImage.d.ts +33 -0
  743. package/src/shade/renderer/texture/source/ShadeImage.d.ts.map +1 -1
  744. package/src/shade/renderer/texture/source/ShadeImage.js +74 -0
  745. package/src/shade/renderer/texture/source/ShadeTexelData.d.ts +78 -0
  746. package/src/shade/renderer/texture/source/ShadeTexelData.d.ts.map +1 -0
  747. package/src/shade/renderer/texture/source/ShadeTexelData.js +158 -0
  748. package/src/shade/renderer/texture/source/texel_data_from_ktx2.d.ts +32 -0
  749. package/src/shade/renderer/texture/source/texel_data_from_ktx2.d.ts.map +1 -0
  750. package/src/shade/renderer/texture/source/texel_data_from_ktx2.js +48 -0
  751. package/src/shade/renderer/texture/texture_descriptor_from_texture_shade.d.ts +0 -10
  752. package/src/shade/renderer/texture/texture_descriptor_from_texture_shade.d.ts.map +1 -1
  753. package/src/shade/renderer/texture/texture_descriptor_from_texture_shade.js +73 -175
  754. package/src/shade/renderer/texture/texture_write_level.d.ts +32 -0
  755. package/src/shade/renderer/texture/texture_write_level.d.ts.map +1 -0
  756. package/src/shade/renderer/texture/texture_write_level.js +87 -0
  757. package/src/shade/renderer/texture/texture_write_to_gpu.d.ts +8 -1
  758. package/src/shade/renderer/texture/texture_write_to_gpu.d.ts.map +1 -1
  759. package/src/shade/renderer/texture/texture_write_to_gpu.js +62 -37
  760. package/src/shade/renderer/texture/virtual/source/VTSourceImage.d.ts +8 -0
  761. package/src/shade/renderer/texture/virtual/source/VTSourceImage.d.ts.map +1 -1
  762. package/src/shade/renderer/texture/virtual/source/VTSourceImage.js +8 -0
  763. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts.map +1 -1
  764. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.js +20 -0
  765. package/src/shade/renderer/volumetrics/NOTES.md +28 -0
  766. package/src/shade/renderer/volumetrics/taa/chunk_volumetric_history_bounding_box.d.ts.map +1 -1
  767. package/src/shade/renderer/volumetrics/taa/chunk_volumetric_history_bounding_box.js +7 -2
  768. package/src/shade/renderer/volumetrics/taa/chunk_volumetrics_taa_reproject.d.ts +17 -0
  769. package/src/shade/renderer/volumetrics/taa/chunk_volumetrics_taa_reproject.d.ts.map +1 -0
  770. package/src/shade/renderer/volumetrics/taa/chunk_volumetrics_taa_reproject.js +168 -0
  771. package/src/shade/renderer/volumetrics/taa/chunk_volumetrics_taa_resolve.d.ts +13 -0
  772. package/src/shade/renderer/volumetrics/taa/chunk_volumetrics_taa_resolve.d.ts.map +1 -0
  773. package/src/shade/renderer/volumetrics/taa/chunk_volumetrics_taa_resolve.js +56 -0
  774. package/src/shade/renderer/volumetrics/taa/shader_volumetrics_taa.d.ts.map +1 -1
  775. package/src/shade/renderer/volumetrics/taa/shader_volumetrics_taa.js +134 -334
  776. package/src/shade/util/texture_mip_extent.d.ts +49 -0
  777. package/src/shade/util/texture_mip_extent.d.ts.map +1 -0
  778. package/src/shade/util/texture_mip_extent.js +87 -0
  779. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.d.ts.map +1 -1
  780. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +49 -21
  781. package/src/avif/encode_image_source.d.ts +0 -70
  782. package/src/avif/encode_image_source.d.ts.map +0 -1
  783. package/src/avif/encode_image_source.js +0 -117
  784. package/src/avif/encoder_worker.d.ts.map +0 -1
  785. package/src/avif/index.d.ts +0 -5
  786. package/src/avif/index.d.ts.map +0 -1
  787. package/src/avif/index.js +0 -21
  788. package/src/avif/native/DECISIONS.md +0 -537
  789. package/src/avif/native/api/AvifDecoder.d.ts.map +0 -1
  790. package/src/avif/native/api/apply_transformations.d.ts.map +0 -1
  791. package/src/avif/native/api/avif_to_sampler2d.d.ts +0 -25
  792. package/src/avif/native/api/avif_to_sampler2d.d.ts.map +0 -1
  793. package/src/avif/native/api/avif_to_sampler2d.js +0 -42
  794. package/src/avif/native/api/convert_to_rgba.d.ts.map +0 -1
  795. package/src/avif/native/api/convert_to_rgba.js +0 -204
  796. package/src/avif/native/api/decode_avif.d.ts.map +0 -1
  797. package/src/avif/native/api/decode_image_item.d.ts.map +0 -1
  798. package/src/avif/native/api/encode_avif.d.ts.map +0 -1
  799. package/src/avif/native/api/sampler2d_to_avif.d.ts.map +0 -1
  800. package/src/avif/native/api/sampler2d_to_avif.js +0 -108
  801. package/src/avif/native/av1/decode/Av1FrameContext.d.ts.map +0 -1
  802. package/src/avif/native/av1/decode/block_decoded.d.ts.map +0 -1
  803. package/src/avif/native/av1/decode/decode_av1_still.d.ts.map +0 -1
  804. package/src/avif/native/av1/decode/decode_coefficients.d.ts.map +0 -1
  805. package/src/avif/native/av1/decode/decode_palette.d.ts.map +0 -1
  806. package/src/avif/native/av1/decode/decode_palette.js +0 -442
  807. package/src/avif/native/av1/decode/decode_tile.d.ts.map +0 -1
  808. package/src/avif/native/av1/decode/decode_tile.js +0 -1213
  809. package/src/avif/native/av1/decode/get_scan.d.ts.map +0 -1
  810. package/src/avif/native/av1/decode/read_lr.d.ts.map +0 -1
  811. package/src/avif/native/av1/decode/read_lr.js +0 -277
  812. package/src/avif/native/av1/decode/read_transform_type.d.ts.map +0 -1
  813. package/src/avif/native/av1/decode/read_transform_type.js +0 -92
  814. package/src/avif/native/av1/decode/reconstruct.d.ts.map +0 -1
  815. package/src/avif/native/av1/decode/reconstruct.js +0 -241
  816. package/src/avif/native/av1/decode/transform_type.d.ts.map +0 -1
  817. package/src/avif/native/av1/encode/Av1BlockSnapshot.d.ts.map +0 -1
  818. package/src/avif/native/av1/encode/Av1EncodeContext.d.ts.map +0 -1
  819. package/src/avif/native/av1/encode/build_headers.d.ts.map +0 -1
  820. package/src/avif/native/av1/encode/choose_chroma_alpha.d.ts.map +0 -1
  821. package/src/avif/native/av1/encode/choose_filters.d.ts.map +0 -1
  822. package/src/avif/native/av1/encode/choose_restoration.d.ts.map +0 -1
  823. package/src/avif/native/av1/encode/choose_restoration.js +0 -616
  824. package/src/avif/native/av1/encode/decode_effort.d.ts.map +0 -1
  825. package/src/avif/native/av1/encode/encode_av1_still.d.ts.map +0 -1
  826. package/src/avif/native/av1/encode/encode_effort.d.ts.map +0 -1
  827. package/src/avif/native/av1/encode/encode_tile.d.ts.map +0 -1
  828. package/src/avif/native/av1/encode/encode_tile.js +0 -2331
  829. package/src/avif/native/av1/encode/quantise.d.ts.map +0 -1
  830. package/src/avif/native/av1/encode/write_coefficients.d.ts.map +0 -1
  831. package/src/avif/native/av1/encode/write_lr.d.ts.map +0 -1
  832. package/src/avif/native/av1/encode/write_transform_type.d.ts.map +0 -1
  833. package/src/avif/native/av1/entropy/CdfContext.d.ts.map +0 -1
  834. package/src/avif/native/av1/entropy/SymbolReader.d.ts.map +0 -1
  835. package/src/avif/native/av1/entropy/SymbolWriter.d.ts.map +0 -1
  836. package/src/avif/native/av1/entropy/coefficient_context.d.ts.map +0 -1
  837. package/src/avif/native/av1/entropy/partition_cdf.d.ts.map +0 -1
  838. package/src/avif/native/av1/entropy/symbol_cost.d.ts.map +0 -1
  839. package/src/avif/native/av1/filter/cdef.d.ts.map +0 -1
  840. package/src/avif/native/av1/filter/cdef.js +0 -313
  841. package/src/avif/native/av1/filter/loop_filter.d.ts.map +0 -1
  842. package/src/avif/native/av1/filter/loop_filter.js +0 -377
  843. package/src/avif/native/av1/filter/loop_restoration.d.ts.map +0 -1
  844. package/src/avif/native/av1/filter/loop_restoration.js +0 -451
  845. package/src/avif/native/av1/filter/superres.d.ts.map +0 -1
  846. package/src/avif/native/av1/filter/superres.js +0 -86
  847. package/src/avif/native/av1/grain/FilmGrainState.d.ts.map +0 -1
  848. package/src/avif/native/av1/grain/film_grain.d.ts.map +0 -1
  849. package/src/avif/native/av1/grain/film_grain.js +0 -623
  850. package/src/avif/native/av1/obu/FrameHeader.d.ts.map +0 -1
  851. package/src/avif/native/av1/obu/ObuHeader.d.ts.map +0 -1
  852. package/src/avif/native/av1/obu/SequenceHeader.d.ts.map +0 -1
  853. package/src/avif/native/av1/obu/for_each_obu.d.ts.map +0 -1
  854. package/src/avif/native/av1/obu/parse_frame_header.d.ts.map +0 -1
  855. package/src/avif/native/av1/obu/parse_frame_header.js +0 -841
  856. package/src/avif/native/av1/obu/parse_obu_header.d.ts.map +0 -1
  857. package/src/avif/native/av1/obu/parse_sequence_header.d.ts.map +0 -1
  858. package/src/avif/native/av1/obu/write_frame_header.d.ts.map +0 -1
  859. package/src/avif/native/av1/obu/write_sequence_header.d.ts.map +0 -1
  860. package/src/avif/native/av1/predict/IntraPredictionState.d.ts.map +0 -1
  861. package/src/avif/native/av1/predict/intra_filter_type.d.ts.map +0 -1
  862. package/src/avif/native/av1/predict/predict_chroma_from_luma.d.ts.map +0 -1
  863. package/src/avif/native/av1/predict/predict_chroma_from_luma.js +0 -153
  864. package/src/avif/native/av1/predict/predict_intra.d.ts.map +0 -1
  865. package/src/avif/native/av1/predict/predict_intra.js +0 -803
  866. package/src/avif/native/av1/predict/predict_palette.d.ts.map +0 -1
  867. package/src/avif/native/av1/tables/av1_symbols.d.ts.map +0 -1
  868. package/src/avif/native/av1/tables/av1_symbols.js +0 -419
  869. package/src/avif/native/av1/tables/block_tables.d.ts.map +0 -1
  870. package/src/avif/native/av1/tables/block_tables.js +0 -270
  871. package/src/avif/native/av1/tables/cdf_tables.d.ts.map +0 -1
  872. package/src/avif/native/av1/tables/cdf_tables.js +0 -1893
  873. package/src/avif/native/av1/tables/coefficient_tables.d.ts.map +0 -1
  874. package/src/avif/native/av1/tables/coefficient_tables.js +0 -82
  875. package/src/avif/native/av1/tables/derived_block_tables.d.ts.map +0 -1
  876. package/src/avif/native/av1/tables/filter_tables.d.ts.map +0 -1
  877. package/src/avif/native/av1/tables/filter_tables.js +0 -313
  878. package/src/avif/native/av1/tables/grain_tables.d.ts.map +0 -1
  879. package/src/avif/native/av1/tables/grain_tables.js +0 -190
  880. package/src/avif/native/av1/tables/prediction_tables.d.ts.map +0 -1
  881. package/src/avif/native/av1/tables/prediction_tables.js +0 -223
  882. package/src/avif/native/av1/tables/quantizer_matrix_tables.d.ts.map +0 -1
  883. package/src/avif/native/av1/tables/quantizer_matrix_tables.js +0 -38
  884. package/src/avif/native/av1/tables/quantizer_tables.d.ts.map +0 -1
  885. package/src/avif/native/av1/tables/quantizer_tables.js +0 -160
  886. package/src/avif/native/av1/tables/scan_tables.d.ts.map +0 -1
  887. package/src/avif/native/av1/tables/scan_tables.js +0 -727
  888. package/src/avif/native/av1/tables/segmentation_tables.d.ts.map +0 -1
  889. package/src/avif/native/av1/tables/segmentation_tables.js +0 -48
  890. package/src/avif/native/av1/tables/transform_tables.d.ts.map +0 -1
  891. package/src/avif/native/av1/tables/transform_tables.js +0 -140
  892. package/src/avif/native/av1/tables/unpack_table.d.ts.map +0 -1
  893. package/src/avif/native/av1/tables/unpack_table.js +0 -73
  894. package/src/avif/native/av1/transform/forward_transform_2d.d.ts.map +0 -1
  895. package/src/avif/native/av1/transform/inverse_transform_1d.d.ts.map +0 -1
  896. package/src/avif/native/av1/transform/inverse_transform_1d.js +0 -590
  897. package/src/avif/native/av1/transform/inverse_transform_2d.d.ts.map +0 -1
  898. package/src/avif/native/av1/transform/inverse_transform_2d.js +0 -180
  899. package/src/avif/native/av1/util/ceil_log2.d.ts.map +0 -1
  900. package/src/avif/native/av1/util/floor_log2.d.ts +0 -20
  901. package/src/avif/native/av1/util/floor_log2.d.ts.map +0 -1
  902. package/src/avif/native/av1/util/floor_log2.js +0 -20
  903. package/src/avif/native/av1/util/round2.d.ts.map +0 -1
  904. package/src/avif/native/av1/util/round2_signed.d.ts.map +0 -1
  905. package/src/avif/native/av1/util/tile_log2.d.ts.map +0 -1
  906. package/src/avif/native/bitstream/BitReader.d.ts.map +0 -1
  907. package/src/avif/native/bitstream/BitWriter.d.ts.map +0 -1
  908. package/src/avif/native/color/ColourTransform.d.ts.map +0 -1
  909. package/src/avif/native/color/YuvImage.d.ts.map +0 -1
  910. package/src/avif/native/color/clamp_sample.d.ts.map +0 -1
  911. package/src/avif/native/color/linear_to_transfer.d.ts.map +0 -1
  912. package/src/avif/native/color/linear_to_transfer.js +0 -126
  913. package/src/avif/native/color/primaries.d.ts.map +0 -1
  914. package/src/avif/native/color/rgb_row_to_ycbcr.d.ts.map +0 -1
  915. package/src/avif/native/color/transfer_to_linear.d.ts.map +0 -1
  916. package/src/avif/native/color/transfer_to_linear.js +0 -171
  917. package/src/avif/native/color/upsample_chroma_row.d.ts.map +0 -1
  918. package/src/avif/native/color/upsample_chroma_row.js +0 -89
  919. package/src/avif/native/color/ycbcr_row_to_rgb.d.ts.map +0 -1
  920. package/src/avif/native/color/ycbcr_row_to_rgb.js +0 -78
  921. package/src/avif/native/heif/AvifFile.d.ts.map +0 -1
  922. package/src/avif/native/heif/ItemProperty.d.ts.map +0 -1
  923. package/src/avif/native/heif/find_item_property.d.ts.map +0 -1
  924. package/src/avif/native/heif/find_item_references.d.ts.map +0 -1
  925. package/src/avif/native/heif/find_items_referencing.d.ts.map +0 -1
  926. package/src/avif/native/heif/parse_avif_file.d.ts.map +0 -1
  927. package/src/avif/native/heif/parse_avif_file.js +0 -383
  928. package/src/avif/native/heif/parse_image_grid.d.ts.map +0 -1
  929. package/src/avif/native/heif/parse_image_grid.js +0 -48
  930. package/src/avif/native/heif/parse_item_property.d.ts.map +0 -1
  931. package/src/avif/native/heif/read_item_data.d.ts.map +0 -1
  932. package/src/avif/native/heif/write_avif_file.d.ts.map +0 -1
  933. package/src/avif/native/heif/write_item_property.d.ts.map +0 -1
  934. package/src/avif/native/index.d.ts +0 -14
  935. package/src/avif/native/index.d.ts.map +0 -1
  936. package/src/avif/native/index.js +0 -56
  937. package/src/avif/native/isobmff/BoxHeader.d.ts.map +0 -1
  938. package/src/avif/native/isobmff/BoxWriter.d.ts +0 -118
  939. package/src/avif/native/isobmff/BoxWriter.d.ts.map +0 -1
  940. package/src/avif/native/isobmff/BoxWriter.js +0 -264
  941. package/src/avif/native/isobmff/ByteCursor.d.ts +0 -125
  942. package/src/avif/native/isobmff/ByteCursor.d.ts.map +0 -1
  943. package/src/avif/native/isobmff/ByteCursor.js +0 -298
  944. package/src/avif/native/isobmff/for_each_box.d.ts.map +0 -1
  945. package/src/avif/native/isobmff/read_box_header.d.ts.map +0 -1
  946. package/src/avif/native/isobmff/read_full_box_header.d.ts.map +0 -1
  947. package/src/avif/threaded_image_encoder.d.ts.map +0 -1
  948. package/src/engine/asset/loaders/gltf/computeObjectBoundingSphere.d.ts +0 -8
  949. package/src/engine/asset/loaders/gltf/computeObjectBoundingSphere.d.ts.map +0 -1
  950. package/src/engine/asset/loaders/gltf/computeObjectBoundingSphere.js +0 -70
  951. package/src/engine/asset/loaders/gltf/isMesh.d.ts +0 -7
  952. package/src/engine/asset/loaders/gltf/isMesh.d.ts.map +0 -1
  953. package/src/engine/asset/loaders/gltf/isMesh.js +0 -8
  954. package/src/engine/asset/loaders/image/codec/Codec.d.ts.map +0 -1
  955. package/src/engine/asset/loaders/image/codec/CodecWithFallback.d.ts.map +0 -1
  956. package/src/engine/asset/loaders/image/codec/CodecWithFallback.js +0 -105
  957. package/src/engine/asset/loaders/image/jpeg/JpegFrame.d.ts.map +0 -1
  958. package/src/engine/asset/loaders/image/jpeg/JpegFrameComponent.d.ts.map +0 -1
  959. package/src/engine/asset/loaders/image/jpeg/JpegImage.d.ts.map +0 -1
  960. package/src/engine/asset/loaders/image/jpeg/JpegImage.js +0 -664
  961. package/src/engine/asset/loaders/image/jpeg/buildComponentData.d.ts.map +0 -1
  962. package/src/engine/asset/loaders/image/jpeg/buildHuffmanTable.d.ts.map +0 -1
  963. package/src/engine/asset/loaders/image/jpeg/decodeScan.d.ts.map +0 -1
  964. package/src/engine/asset/loaders/image/jpeg/idct8x8_float.d.ts.map +0 -1
  965. package/src/engine/asset/loaders/image/jpeg/idct8x8_float.js +0 -31
  966. package/src/engine/asset/loaders/image/jpeg/jpeg_component_row.d.ts.map +0 -1
  967. package/src/engine/asset/loaders/image/jpeg/jpeg_decode.d.ts.map +0 -1
  968. package/src/engine/asset/loaders/image/png/PNG.d.ts.map +0 -1
  969. package/src/engine/asset/loaders/image/png/PNGReader.d.ts +0 -158
  970. package/src/engine/asset/loaders/image/png/PNGReader.d.ts.map +0 -1
  971. package/src/engine/asset/loaders/image/png/PNGReader.js +0 -656
  972. package/src/engine/asset/loaders/image/png/PNG_HEADER_BYTES.d.ts.map +0 -1
  973. package/src/engine/asset/loaders/image/png/chunk/png_chunk_decode_iTXt.d.ts.map +0 -1
  974. package/src/engine/asset/loaders/image/png/chunk/png_chunk_decode_iTXt.js +0 -53
  975. package/src/engine/asset/loaders/image/png/chunk/png_chunk_decode_zTXt.d.ts.map +0 -1
  976. package/src/engine/asset/loaders/image/png/chunk/png_chunk_decode_zTXt.js +0 -42
  977. package/src/engine/asset/loaders/image/png/crc32.d.ts +0 -2
  978. package/src/engine/asset/loaders/image/png/crc32.d.ts.map +0 -1
  979. package/src/engine/asset/loaders/image/png/crc32.js +0 -8
  980. package/src/engine/asset/loaders/image/png/filter/png_filter_unFilterAverage.d.ts.map +0 -1
  981. package/src/engine/asset/loaders/image/png/filter/png_filter_unFilterNone.d.ts.map +0 -1
  982. package/src/engine/asset/loaders/image/png/filter/png_filter_unFilterPaeth.d.ts.map +0 -1
  983. package/src/engine/asset/loaders/image/png/filter/png_filter_unFilterSub.d.ts.map +0 -1
  984. package/src/engine/asset/loaders/image/png/filter/png_filter_unFilterUp.d.ts.map +0 -1
  985. package/src/engine/asset/loaders/image/png/filter/png_unfilter.d.ts.map +0 -1
  986. package/src/engine/asset/loaders/image/png/filter/png_unfilter.js +0 -68
  987. package/src/engine/asset/loaders/image/png/png_inflate.d.ts.map +0 -1
  988. package/src/engine/asset/loaders/image/png/png_unpack_samples.d.ts.map +0 -1
  989. package/src/engine/asset/loaders/image/png/prototypePNG.d.ts.map +0 -1
  990. package/src/engine/asset/loaders/image/png/prototypePNG.js +0 -37
  991. package/src/shade/renderer/geometry/bvh/record_blas_refit.d.ts +0 -25
  992. package/src/shade/renderer/geometry/bvh/record_blas_refit.d.ts.map +0 -1
  993. package/src/shade/renderer/geometry/bvh/record_blas_refit.js +0 -102
  994. package/src/shade/renderer/geometry/bvh/shader_blas_refit_leaf_bounds.d.ts +0 -30
  995. package/src/shade/renderer/geometry/bvh/shader_blas_refit_leaf_bounds.d.ts.map +0 -1
  996. package/src/shade/renderer/geometry/bvh/shader_blas_refit_leaf_bounds.js +0 -125
  997. package/src/shade/renderer/loader/gltf/GLTF_PRIMITIVE_MODE.d.ts.map +0 -1
  998. package/src/shade/renderer/loader/gltf/MESHOPT_COMPRESSION_PLAN.md +0 -793
  999. package/src/shade/renderer/loader/gltf/coalesce_array_duplicates.d.ts.map +0 -1
  1000. package/src/shade/renderer/loader/gltf/ext/GltfBufferViewExtension.d.ts.map +0 -1
  1001. package/src/shade/renderer/loader/gltf/ext/GltfBufferViewExtensionSet.d.ts.map +0 -1
  1002. package/src/shade/renderer/loader/gltf/ext/GltfBufferViewExtensionSet.js +0 -109
  1003. package/src/shade/renderer/loader/gltf/ext/gltf_apply_buffer_view_extensions.d.ts.map +0 -1
  1004. package/src/shade/renderer/loader/gltf/ext/gltf_buffer_view_extensions.d.ts.map +0 -1
  1005. package/src/shade/renderer/loader/gltf/ext/gltf_geometry_extensions.d.ts.map +0 -1
  1006. package/src/shade/renderer/loader/gltf/ext/meshopt/MeshoptBufferViewExtension.d.ts.map +0 -1
  1007. package/src/shade/renderer/loader/gltf/ext/meshopt/MeshoptBufferViewExtension.js +0 -301
  1008. package/src/shade/renderer/loader/gltf/gltf_component_type.d.ts.map +0 -1
  1009. package/src/shade/renderer/loader/gltf/gltf_node_world_matrices.d.ts.map +0 -1
  1010. package/src/shade/renderer/loader/gltf/gltf_node_world_matrices.js +0 -137
  1011. package/src/shade/renderer/loader/gltf/gltf_parse_container.d.ts.map +0 -1
  1012. package/src/shade/renderer/loader/gltf/gltf_read_accessor.d.ts.map +0 -1
  1013. package/src/shade/renderer/loader/gltf/gltf_read_accessor.js +0 -294
  1014. package/src/shade/renderer/loader/gltf/gltf_used_image_indices.d.ts +0 -25
  1015. package/src/shade/renderer/loader/gltf/gltf_used_image_indices.d.ts.map +0 -1
  1016. package/src/shade/renderer/loader/gltf/gltf_used_image_indices.js +0 -49
  1017. package/src/shade/renderer/loader/usd/UsdError.d.ts.map +0 -1
  1018. package/src/shade/renderer/loader/usd/parse_usda.d.ts.map +0 -1
  1019. package/src/shade/renderer/loader/usd/unpack_usdz.d.ts.map +0 -1
  1020. package/src/shade/renderer/loader/usd/unpack_usdz.js +0 -106
  1021. package/src/shade/renderer/loader/usd/usd_compose_transform.d.ts.map +0 -1
  1022. package/src/shade/renderer/loader/usd/usd_compose_transform.js +0 -258
  1023. package/src/shade/renderer/loader/usd/usd_lz4.d.ts.map +0 -1
  1024. package/src/shade/renderer/loader/usd/usd_lz4.js +0 -182
  1025. package/src/shade/renderer/loader/usd/usd_triangulate.d.ts.map +0 -1
  1026. package/src/shade/renderer/postprocess/taa/chunk_taa_sample_history_color.d.ts +0 -2
  1027. package/src/shade/renderer/postprocess/taa/chunk_taa_sample_history_color.d.ts.map +0 -1
  1028. package/src/shade/renderer/postprocess/taa/chunk_taa_sample_history_color.js +0 -43
  1029. package/src/shade/renderer/scene/optimization/sampler2d_compare.d.ts.map +0 -1
  1030. package/src/shade/renderer/scene/optimization/sampler2d_compare.js +0 -94
  1031. package/src/shade/util/compute_max_mip_count.d.ts +0 -8
  1032. package/src/shade/util/compute_max_mip_count.d.ts.map +0 -1
  1033. package/src/shade/util/compute_max_mip_count.js +0 -27
  1034. /package/src/engine/asset/{loaders/image/codec → codec}/Codec.d.ts +0 -0
  1035. /package/src/engine/asset/{loaders/image/codec → codec}/Codec.js +0 -0
  1036. /package/src/engine/asset/{loaders/image/codec → codec}/CodecWithFallback.d.ts +0 -0
  1037. /package/src/{avif → engine/asset/loaders/image/avif}/encoder_worker.d.ts +0 -0
  1038. /package/src/{avif → engine/asset/loaders/image/avif}/encoder_worker.js +0 -0
  1039. /package/src/{avif → engine/asset/loaders/image/avif}/threaded_image_encoder.d.ts +0 -0
  1040. /package/src/{avif → engine/asset/loaders/image/avif}/threaded_image_encoder.js +0 -0
  1041. /package/src/engine/asset/loaders/image/{png/prototypePNG.d.ts → prototypePNG.d.ts} +0 -0
  1042. /package/src/{shade/renderer/scene/optimization → engine/graphics/texture/sampler/compare}/sampler2d_compare.d.ts +0 -0
  1043. /package/src/{avif/native/api → engine/graphics/texture/sampler}/sampler2d_to_avif.d.ts +0 -0
  1044. /package/src/{avif/native → format/image/avif}/NOTICE.md +0 -0
  1045. /package/src/{avif/native → format/image/avif}/api/AvifDecoder.d.ts +0 -0
  1046. /package/src/{avif/native → format/image/avif}/api/AvifDecoder.js +0 -0
  1047. /package/src/{avif/native → format/image/avif}/api/apply_transformations.d.ts +0 -0
  1048. /package/src/{avif/native → format/image/avif}/api/apply_transformations.js +0 -0
  1049. /package/src/{avif/native → format/image/avif}/api/convert_to_rgba.d.ts +0 -0
  1050. /package/src/{avif/native → format/image/avif}/api/decode_avif.d.ts +0 -0
  1051. /package/src/{avif/native → format/image/avif}/api/decode_avif.js +0 -0
  1052. /package/src/{avif/native → format/image/avif}/api/decode_image_item.d.ts +0 -0
  1053. /package/src/{avif/native → format/image/avif}/api/decode_image_item.js +0 -0
  1054. /package/src/{avif/native → format/image/avif}/api/encode_avif.d.ts +0 -0
  1055. /package/src/{avif/native → format/image/avif}/api/encode_avif.js +0 -0
  1056. /package/src/{avif/native → format/image/avif}/av1/decode/Av1FrameContext.d.ts +0 -0
  1057. /package/src/{avif/native → format/image/avif}/av1/decode/Av1FrameContext.js +0 -0
  1058. /package/src/{avif/native → format/image/avif}/av1/decode/block_decoded.d.ts +0 -0
  1059. /package/src/{avif/native → format/image/avif}/av1/decode/block_decoded.js +0 -0
  1060. /package/src/{avif/native → format/image/avif}/av1/decode/decode_av1_still.d.ts +0 -0
  1061. /package/src/{avif/native → format/image/avif}/av1/decode/decode_av1_still.js +0 -0
  1062. /package/src/{avif/native → format/image/avif}/av1/decode/decode_coefficients.d.ts +0 -0
  1063. /package/src/{avif/native → format/image/avif}/av1/decode/decode_coefficients.js +0 -0
  1064. /package/src/{avif/native → format/image/avif}/av1/decode/decode_palette.d.ts +0 -0
  1065. /package/src/{avif/native → format/image/avif}/av1/decode/decode_tile.d.ts +0 -0
  1066. /package/src/{avif/native → format/image/avif}/av1/decode/get_scan.d.ts +0 -0
  1067. /package/src/{avif/native → format/image/avif}/av1/decode/get_scan.js +0 -0
  1068. /package/src/{avif/native → format/image/avif}/av1/decode/read_lr.d.ts +0 -0
  1069. /package/src/{avif/native → format/image/avif}/av1/decode/read_transform_type.d.ts +0 -0
  1070. /package/src/{avif/native → format/image/avif}/av1/decode/reconstruct.d.ts +0 -0
  1071. /package/src/{avif/native → format/image/avif}/av1/decode/transform_type.d.ts +0 -0
  1072. /package/src/{avif/native → format/image/avif}/av1/decode/transform_type.js +0 -0
  1073. /package/src/{avif/native → format/image/avif}/av1/encode/Av1BlockSnapshot.d.ts +0 -0
  1074. /package/src/{avif/native → format/image/avif}/av1/encode/Av1BlockSnapshot.js +0 -0
  1075. /package/src/{avif/native → format/image/avif}/av1/encode/Av1EncodeContext.d.ts +0 -0
  1076. /package/src/{avif/native → format/image/avif}/av1/encode/Av1EncodeContext.js +0 -0
  1077. /package/src/{avif/native → format/image/avif}/av1/encode/build_headers.d.ts +0 -0
  1078. /package/src/{avif/native → format/image/avif}/av1/encode/build_headers.js +0 -0
  1079. /package/src/{avif/native → format/image/avif}/av1/encode/choose_chroma_alpha.d.ts +0 -0
  1080. /package/src/{avif/native → format/image/avif}/av1/encode/choose_chroma_alpha.js +0 -0
  1081. /package/src/{avif/native → format/image/avif}/av1/encode/choose_filters.d.ts +0 -0
  1082. /package/src/{avif/native → format/image/avif}/av1/encode/choose_filters.js +0 -0
  1083. /package/src/{avif/native → format/image/avif}/av1/encode/choose_restoration.d.ts +0 -0
  1084. /package/src/{avif/native → format/image/avif}/av1/encode/decode_effort.d.ts +0 -0
  1085. /package/src/{avif/native → format/image/avif}/av1/encode/decode_effort.js +0 -0
  1086. /package/src/{avif/native → format/image/avif}/av1/encode/encode_av1_still.d.ts +0 -0
  1087. /package/src/{avif/native → format/image/avif}/av1/encode/encode_av1_still.js +0 -0
  1088. /package/src/{avif/native → format/image/avif}/av1/encode/encode_effort.d.ts +0 -0
  1089. /package/src/{avif/native → format/image/avif}/av1/encode/encode_effort.js +0 -0
  1090. /package/src/{avif/native → format/image/avif}/av1/encode/encode_tile.d.ts +0 -0
  1091. /package/src/{avif/native → format/image/avif}/av1/encode/quantise.d.ts +0 -0
  1092. /package/src/{avif/native → format/image/avif}/av1/encode/quantise.js +0 -0
  1093. /package/src/{avif/native → format/image/avif}/av1/encode/write_coefficients.d.ts +0 -0
  1094. /package/src/{avif/native → format/image/avif}/av1/encode/write_coefficients.js +0 -0
  1095. /package/src/{avif/native → format/image/avif}/av1/encode/write_lr.d.ts +0 -0
  1096. /package/src/{avif/native → format/image/avif}/av1/encode/write_lr.js +0 -0
  1097. /package/src/{avif/native → format/image/avif}/av1/encode/write_transform_type.d.ts +0 -0
  1098. /package/src/{avif/native → format/image/avif}/av1/encode/write_transform_type.js +0 -0
  1099. /package/src/{avif/native → format/image/avif}/av1/entropy/CdfContext.d.ts +0 -0
  1100. /package/src/{avif/native → format/image/avif}/av1/entropy/CdfContext.js +0 -0
  1101. /package/src/{avif/native → format/image/avif}/av1/entropy/SymbolReader.d.ts +0 -0
  1102. /package/src/{avif/native → format/image/avif}/av1/entropy/SymbolReader.js +0 -0
  1103. /package/src/{avif/native → format/image/avif}/av1/entropy/SymbolWriter.d.ts +0 -0
  1104. /package/src/{avif/native → format/image/avif}/av1/entropy/SymbolWriter.js +0 -0
  1105. /package/src/{avif/native → format/image/avif}/av1/entropy/coefficient_context.d.ts +0 -0
  1106. /package/src/{avif/native → format/image/avif}/av1/entropy/coefficient_context.js +0 -0
  1107. /package/src/{avif/native → format/image/avif}/av1/entropy/partition_cdf.d.ts +0 -0
  1108. /package/src/{avif/native → format/image/avif}/av1/entropy/partition_cdf.js +0 -0
  1109. /package/src/{avif/native → format/image/avif}/av1/entropy/symbol_cost.d.ts +0 -0
  1110. /package/src/{avif/native → format/image/avif}/av1/entropy/symbol_cost.js +0 -0
  1111. /package/src/{avif/native → format/image/avif}/av1/filter/cdef.d.ts +0 -0
  1112. /package/src/{avif/native → format/image/avif}/av1/filter/loop_filter.d.ts +0 -0
  1113. /package/src/{avif/native → format/image/avif}/av1/filter/loop_restoration.d.ts +0 -0
  1114. /package/src/{avif/native → format/image/avif}/av1/filter/superres.d.ts +0 -0
  1115. /package/src/{avif/native → format/image/avif}/av1/grain/FilmGrainState.d.ts +0 -0
  1116. /package/src/{avif/native → format/image/avif}/av1/grain/FilmGrainState.js +0 -0
  1117. /package/src/{avif/native → format/image/avif}/av1/grain/film_grain.d.ts +0 -0
  1118. /package/src/{avif/native → format/image/avif}/av1/obu/FrameHeader.d.ts +0 -0
  1119. /package/src/{avif/native → format/image/avif}/av1/obu/FrameHeader.js +0 -0
  1120. /package/src/{avif/native → format/image/avif}/av1/obu/ObuHeader.d.ts +0 -0
  1121. /package/src/{avif/native → format/image/avif}/av1/obu/ObuHeader.js +0 -0
  1122. /package/src/{avif/native → format/image/avif}/av1/obu/SequenceHeader.d.ts +0 -0
  1123. /package/src/{avif/native → format/image/avif}/av1/obu/SequenceHeader.js +0 -0
  1124. /package/src/{avif/native → format/image/avif}/av1/obu/for_each_obu.d.ts +0 -0
  1125. /package/src/{avif/native → format/image/avif}/av1/obu/for_each_obu.js +0 -0
  1126. /package/src/{avif/native → format/image/avif}/av1/obu/parse_frame_header.d.ts +0 -0
  1127. /package/src/{avif/native → format/image/avif}/av1/obu/parse_obu_header.d.ts +0 -0
  1128. /package/src/{avif/native → format/image/avif}/av1/obu/parse_obu_header.js +0 -0
  1129. /package/src/{avif/native → format/image/avif}/av1/obu/parse_sequence_header.d.ts +0 -0
  1130. /package/src/{avif/native → format/image/avif}/av1/obu/parse_sequence_header.js +0 -0
  1131. /package/src/{avif/native → format/image/avif}/av1/obu/write_frame_header.d.ts +0 -0
  1132. /package/src/{avif/native → format/image/avif}/av1/obu/write_frame_header.js +0 -0
  1133. /package/src/{avif/native → format/image/avif}/av1/obu/write_sequence_header.d.ts +0 -0
  1134. /package/src/{avif/native → format/image/avif}/av1/obu/write_sequence_header.js +0 -0
  1135. /package/src/{avif/native → format/image/avif}/av1/predict/IntraPredictionState.d.ts +0 -0
  1136. /package/src/{avif/native → format/image/avif}/av1/predict/IntraPredictionState.js +0 -0
  1137. /package/src/{avif/native → format/image/avif}/av1/predict/intra_filter_type.d.ts +0 -0
  1138. /package/src/{avif/native → format/image/avif}/av1/predict/intra_filter_type.js +0 -0
  1139. /package/src/{avif/native → format/image/avif}/av1/predict/predict_chroma_from_luma.d.ts +0 -0
  1140. /package/src/{avif/native → format/image/avif}/av1/predict/predict_intra.d.ts +0 -0
  1141. /package/src/{avif/native → format/image/avif}/av1/predict/predict_palette.d.ts +0 -0
  1142. /package/src/{avif/native → format/image/avif}/av1/predict/predict_palette.js +0 -0
  1143. /package/src/{avif/native → format/image/avif}/av1/tables/av1_symbols.d.ts +0 -0
  1144. /package/src/{avif/native → format/image/avif}/av1/tables/block_tables.d.ts +0 -0
  1145. /package/src/{avif/native → format/image/avif}/av1/tables/cdf_tables.d.ts +0 -0
  1146. /package/src/{avif/native → format/image/avif}/av1/tables/coefficient_tables.d.ts +0 -0
  1147. /package/src/{avif/native → format/image/avif}/av1/tables/derived_block_tables.d.ts +0 -0
  1148. /package/src/{avif/native → format/image/avif}/av1/tables/derived_block_tables.js +0 -0
  1149. /package/src/{avif/native → format/image/avif}/av1/tables/filter_tables.d.ts +0 -0
  1150. /package/src/{avif/native → format/image/avif}/av1/tables/grain_tables.d.ts +0 -0
  1151. /package/src/{avif/native → format/image/avif}/av1/tables/prediction_tables.d.ts +0 -0
  1152. /package/src/{avif/native → format/image/avif}/av1/tables/quantizer_matrix_tables.d.ts +0 -0
  1153. /package/src/{avif/native → format/image/avif}/av1/tables/quantizer_tables.d.ts +0 -0
  1154. /package/src/{avif/native → format/image/avif}/av1/tables/scan_tables.d.ts +0 -0
  1155. /package/src/{avif/native → format/image/avif}/av1/tables/segmentation_tables.d.ts +0 -0
  1156. /package/src/{avif/native → format/image/avif}/av1/tables/transform_tables.d.ts +0 -0
  1157. /package/src/{avif/native → format/image/avif}/av1/tables/unpack_table.d.ts +0 -0
  1158. /package/src/{avif/native → format/image/avif}/av1/transform/forward_transform_2d.d.ts +0 -0
  1159. /package/src/{avif/native → format/image/avif}/av1/transform/forward_transform_2d.js +0 -0
  1160. /package/src/{avif/native → format/image/avif}/av1/transform/inverse_transform_1d.d.ts +0 -0
  1161. /package/src/{avif/native → format/image/avif}/av1/transform/inverse_transform_2d.d.ts +0 -0
  1162. /package/src/{avif/native → format/image/avif}/av1/util/ceil_log2.d.ts +0 -0
  1163. /package/src/{avif/native → format/image/avif}/av1/util/ceil_log2.js +0 -0
  1164. /package/src/{avif/native → format/image/avif}/av1/util/round2.d.ts +0 -0
  1165. /package/src/{avif/native → format/image/avif}/av1/util/round2.js +0 -0
  1166. /package/src/{avif/native → format/image/avif}/av1/util/round2_signed.d.ts +0 -0
  1167. /package/src/{avif/native → format/image/avif}/av1/util/round2_signed.js +0 -0
  1168. /package/src/{avif/native → format/image/avif}/av1/util/tile_log2.d.ts +0 -0
  1169. /package/src/{avif/native → format/image/avif}/av1/util/tile_log2.js +0 -0
  1170. /package/src/{avif/native → format/image/avif}/bitstream/BitReader.d.ts +0 -0
  1171. /package/src/{avif/native → format/image/avif}/bitstream/BitReader.js +0 -0
  1172. /package/src/{avif/native → format/image/avif}/bitstream/BitWriter.d.ts +0 -0
  1173. /package/src/{avif/native → format/image/avif}/bitstream/BitWriter.js +0 -0
  1174. /package/src/{avif/native → format/image/avif}/color/ColourTransform.d.ts +0 -0
  1175. /package/src/{avif/native → format/image/avif}/color/ColourTransform.js +0 -0
  1176. /package/src/{avif/native → format/image/avif}/color/YuvImage.d.ts +0 -0
  1177. /package/src/{avif/native → format/image/avif}/color/YuvImage.js +0 -0
  1178. /package/src/{avif/native → format/image/avif}/color/clamp_sample.d.ts +0 -0
  1179. /package/src/{avif/native → format/image/avif}/color/clamp_sample.js +0 -0
  1180. /package/src/{avif/native → format/image/avif}/color/linear_to_transfer.d.ts +0 -0
  1181. /package/src/{avif/native → format/image/avif}/color/primaries.d.ts +0 -0
  1182. /package/src/{avif/native → format/image/avif}/color/primaries.js +0 -0
  1183. /package/src/{avif/native → format/image/avif}/color/rgb_row_to_ycbcr.d.ts +0 -0
  1184. /package/src/{avif/native → format/image/avif}/color/rgb_row_to_ycbcr.js +0 -0
  1185. /package/src/{avif/native → format/image/avif}/color/transfer_to_linear.d.ts +0 -0
  1186. /package/src/{avif/native → format/image/avif}/color/upsample_chroma_row.d.ts +0 -0
  1187. /package/src/{avif/native → format/image/avif}/color/ycbcr_row_to_rgb.d.ts +0 -0
  1188. /package/src/{avif/native → format/image/avif}/heif/AvifFile.d.ts +0 -0
  1189. /package/src/{avif/native → format/image/avif}/heif/AvifFile.js +0 -0
  1190. /package/src/{avif/native → format/image/avif}/heif/ItemProperty.d.ts +0 -0
  1191. /package/src/{avif/native → format/image/avif}/heif/ItemProperty.js +0 -0
  1192. /package/src/{avif/native → format/image/avif}/heif/find_item_property.d.ts +0 -0
  1193. /package/src/{avif/native → format/image/avif}/heif/find_item_property.js +0 -0
  1194. /package/src/{avif/native → format/image/avif}/heif/find_item_references.d.ts +0 -0
  1195. /package/src/{avif/native → format/image/avif}/heif/find_item_references.js +0 -0
  1196. /package/src/{avif/native → format/image/avif}/heif/find_items_referencing.d.ts +0 -0
  1197. /package/src/{avif/native → format/image/avif}/heif/find_items_referencing.js +0 -0
  1198. /package/src/{avif/native → format/image/avif}/heif/parse_avif_file.d.ts +0 -0
  1199. /package/src/{avif/native → format/image/avif}/heif/parse_image_grid.d.ts +0 -0
  1200. /package/src/{avif/native → format/image/avif}/heif/parse_item_property.d.ts +0 -0
  1201. /package/src/{avif/native → format/image/avif}/heif/parse_item_property.js +0 -0
  1202. /package/src/{avif/native → format/image/avif}/heif/read_item_data.d.ts +0 -0
  1203. /package/src/{avif/native → format/image/avif}/heif/read_item_data.js +0 -0
  1204. /package/src/{avif/native → format/image/avif}/heif/write_avif_file.d.ts +0 -0
  1205. /package/src/{avif/native → format/image/avif}/heif/write_avif_file.js +0 -0
  1206. /package/src/{avif/native → format/image/avif}/heif/write_item_property.d.ts +0 -0
  1207. /package/src/{avif/native → format/image/avif}/heif/write_item_property.js +0 -0
  1208. /package/src/{avif/native → format/image/avif}/isobmff/BoxHeader.d.ts +0 -0
  1209. /package/src/{avif/native → format/image/avif}/isobmff/BoxHeader.js +0 -0
  1210. /package/src/{avif/native → format/image/avif}/isobmff/for_each_box.d.ts +0 -0
  1211. /package/src/{avif/native → format/image/avif}/isobmff/for_each_box.js +0 -0
  1212. /package/src/{avif/native → format/image/avif}/isobmff/read_box_header.d.ts +0 -0
  1213. /package/src/{avif/native → format/image/avif}/isobmff/read_box_header.js +0 -0
  1214. /package/src/{avif/native → format/image/avif}/isobmff/read_full_box_header.d.ts +0 -0
  1215. /package/src/{avif/native → format/image/avif}/isobmff/read_full_box_header.js +0 -0
  1216. /package/src/{engine/asset/loaders → format}/image/jpeg/JpegFrame.d.ts +0 -0
  1217. /package/src/{engine/asset/loaders → format}/image/jpeg/JpegFrame.js +0 -0
  1218. /package/src/{engine/asset/loaders → format}/image/jpeg/JpegFrameComponent.d.ts +0 -0
  1219. /package/src/{engine/asset/loaders → format}/image/jpeg/JpegFrameComponent.js +0 -0
  1220. /package/src/{engine/asset/loaders → format}/image/jpeg/JpegImage.d.ts +0 -0
  1221. /package/src/{engine/asset/loaders → format}/image/jpeg/NOTES.md +0 -0
  1222. /package/src/{engine/asset/loaders → format}/image/jpeg/buildComponentData.d.ts +0 -0
  1223. /package/src/{engine/asset/loaders → format}/image/jpeg/buildComponentData.js +0 -0
  1224. /package/src/{engine/asset/loaders → format}/image/jpeg/buildHuffmanTable.d.ts +0 -0
  1225. /package/src/{engine/asset/loaders → format}/image/jpeg/buildHuffmanTable.js +0 -0
  1226. /package/src/{engine/asset/loaders → format}/image/jpeg/decodeScan.d.ts +0 -0
  1227. /package/src/{engine/asset/loaders → format}/image/jpeg/decodeScan.js +0 -0
  1228. /package/src/{engine/asset/loaders → format}/image/jpeg/idct8x8_float.d.ts +0 -0
  1229. /package/src/{engine/asset/loaders → format}/image/jpeg/jpeg_component_row.d.ts +0 -0
  1230. /package/src/{engine/asset/loaders → format}/image/jpeg/jpeg_component_row.js +0 -0
  1231. /package/src/{engine/asset/loaders → format}/image/jpeg/jpeg_decode.d.ts +0 -0
  1232. /package/src/{engine/asset/loaders → format}/image/jpeg/jpeg_decode.js +0 -0
  1233. /package/src/{engine/asset/loaders → format}/image/png/PNG.d.ts +0 -0
  1234. /package/src/{engine/asset/loaders → format}/image/png/PNG.js +0 -0
  1235. /package/src/{engine/asset/loaders → format}/image/png/PNG_HEADER_BYTES.d.ts +0 -0
  1236. /package/src/{engine/asset/loaders → format}/image/png/PNG_HEADER_BYTES.js +0 -0
  1237. /package/src/{engine/asset/loaders → format}/image/png/chunk/png_chunk_decode_iTXt.d.ts +0 -0
  1238. /package/src/{engine/asset/loaders → format}/image/png/chunk/png_chunk_decode_zTXt.d.ts +0 -0
  1239. /package/src/{engine/asset/loaders → format}/image/png/filter/png_filter_unFilterAverage.d.ts +0 -0
  1240. /package/src/{engine/asset/loaders → format}/image/png/filter/png_filter_unFilterAverage.js +0 -0
  1241. /package/src/{engine/asset/loaders → format}/image/png/filter/png_filter_unFilterNone.d.ts +0 -0
  1242. /package/src/{engine/asset/loaders → format}/image/png/filter/png_filter_unFilterNone.js +0 -0
  1243. /package/src/{engine/asset/loaders → format}/image/png/filter/png_filter_unFilterPaeth.d.ts +0 -0
  1244. /package/src/{engine/asset/loaders → format}/image/png/filter/png_filter_unFilterPaeth.js +0 -0
  1245. /package/src/{engine/asset/loaders → format}/image/png/filter/png_filter_unFilterSub.d.ts +0 -0
  1246. /package/src/{engine/asset/loaders → format}/image/png/filter/png_filter_unFilterSub.js +0 -0
  1247. /package/src/{engine/asset/loaders → format}/image/png/filter/png_filter_unFilterUp.d.ts +0 -0
  1248. /package/src/{engine/asset/loaders → format}/image/png/filter/png_filter_unFilterUp.js +0 -0
  1249. /package/src/{engine/asset/loaders → format}/image/png/filter/png_unfilter.d.ts +0 -0
  1250. /package/src/{engine/asset/loaders → format}/image/png/png_inflate.d.ts +0 -0
  1251. /package/src/{engine/asset/loaders → format}/image/png/png_inflate.js +0 -0
  1252. /package/src/{engine/asset/loaders → format}/image/png/png_unpack_samples.d.ts +0 -0
  1253. /package/src/{engine/asset/loaders → format}/image/png/png_unpack_samples.js +0 -0
  1254. /package/src/{shade/renderer/loader → format/scene}/gltf/GLTF_PRIMITIVE_MODE.d.ts +0 -0
  1255. /package/src/{shade/renderer/loader → format/scene}/gltf/GLTF_PRIMITIVE_MODE.js +0 -0
  1256. /package/src/{shade/renderer/loader → format/scene}/gltf/coalesce_array_duplicates.d.ts +0 -0
  1257. /package/src/{shade/renderer/loader → format/scene}/gltf/coalesce_array_duplicates.js +0 -0
  1258. /package/src/{shade/renderer/loader → format/scene}/gltf/ext/GltfBufferViewExtension.d.ts +0 -0
  1259. /package/src/{shade/renderer/loader → format/scene}/gltf/ext/GltfBufferViewExtension.js +0 -0
  1260. /package/src/{shade/renderer/loader → format/scene}/gltf/ext/GltfBufferViewExtensionSet.d.ts +0 -0
  1261. /package/src/{shade/renderer/loader → format/scene}/gltf/ext/gltf_apply_buffer_view_extensions.d.ts +0 -0
  1262. /package/src/{shade/renderer/loader → format/scene}/gltf/ext/gltf_apply_buffer_view_extensions.js +0 -0
  1263. /package/src/{shade/renderer/loader → format/scene}/gltf/ext/gltf_buffer_view_extensions.d.ts +0 -0
  1264. /package/src/{shade/renderer/loader → format/scene}/gltf/ext/gltf_buffer_view_extensions.js +0 -0
  1265. /package/src/{shade/renderer/loader → format/scene}/gltf/ext/gltf_geometry_extensions.d.ts +0 -0
  1266. /package/src/{shade/renderer/loader → format/scene}/gltf/ext/gltf_geometry_extensions.js +0 -0
  1267. /package/src/{shade/renderer/loader → format/scene}/gltf/ext/meshopt/MeshoptBufferViewExtension.d.ts +0 -0
  1268. /package/src/{shade/renderer/loader → format/scene}/gltf/gltf_component_type.d.ts +0 -0
  1269. /package/src/{shade/renderer/loader → format/scene}/gltf/gltf_component_type.js +0 -0
  1270. /package/src/{shade/renderer/loader → format/scene}/gltf/gltf_node_world_matrices.d.ts +0 -0
  1271. /package/src/{shade/renderer/loader → format/scene}/gltf/gltf_parse_container.d.ts +0 -0
  1272. /package/src/{shade/renderer/loader → format/scene}/gltf/gltf_parse_container.js +0 -0
  1273. /package/src/{shade/renderer/loader → format/scene}/gltf/gltf_read_accessor.d.ts +0 -0
  1274. /package/src/{shade/renderer/loader → format/scene}/usd/UsdError.d.ts +0 -0
  1275. /package/src/{shade/renderer/loader → format/scene}/usd/UsdError.js +0 -0
  1276. /package/src/{shade/renderer/loader → format/scene}/usd/parse_usda.d.ts +0 -0
  1277. /package/src/{shade/renderer/loader → format/scene}/usd/parse_usda.js +0 -0
  1278. /package/src/{shade/renderer/loader → format/scene}/usd/unpack_usdz.d.ts +0 -0
  1279. /package/src/{shade/renderer/loader → format/scene}/usd/usd_compose_transform.d.ts +0 -0
  1280. /package/src/{shade/renderer/loader → format/scene}/usd/usd_lz4.d.ts +0 -0
  1281. /package/src/{shade/renderer/loader → format/scene}/usd/usd_triangulate.d.ts +0 -0
  1282. /package/src/{shade/renderer/loader → format/scene}/usd/usd_triangulate.js +0 -0
@@ -0,0 +1,2345 @@
1
+ # Block-compressed textures — implementation plan
2
+
3
+ **Status:** proposal — revision 6
4
+ **Date:** 2026-09-01
5
+ **Author:** Alex Goldring / Company Named Limited
6
+ **Scope:** `src/shade/renderer/texture/`, `src/shade/descriptor/texture/format/`,
7
+ `src/shade/device/mock/`, `src/engine/asset/`
8
+
9
+ > **Revision 2** rewrites §4 and §5, which had the wrong shape. Revision 1 treated transcoding as a
10
+ > rejected alternative and per-format delivery as a container feature; both were wrong. Transcoding
11
+ > is a **supported path** — KTX2/Basis is what the web's model pipeline emits, and the engine has to
12
+ > read it — and format selection is a **network concern**, not a packaging one, which also reverses
13
+ > revision 1's "do not ship ETC2" (§5.2). §3.4 gains one-level-at-a-time upload, which is what
14
+ > streaming actually requires, and §7 is new: what compressed virtual textures need the rest of the
15
+ > design not to preclude.
16
+ >
17
+ > **Revision 3** fixes §3.2, which was wrong in the way this document spends §0 warning against.
18
+ > Revision 2 had `GenerateMipMaps` mean "generate, unless levels were supplied, in which case
19
+ > quietly do nothing" — an explicit instruction silently reinterpreted from the data. It is now
20
+ > honoured or it throws (§3.2), which also removes revision 2's separate rule about single-level
21
+ > compressed payloads by subsuming it. §2 loses a module that turned out to already exist (§5.3).
22
+ >
23
+ > **Revision 4** adds §2.1. A `TextureFormat` enum is worth having, though not for the reason it was
24
+ > asked for — `GPUTextureFormat` is a string and never throws. The load-time `GPUTextureUsage` read
25
+ > in `TextureDescriptor` does throw, outside vitest, which is where the node-side consumers this
26
+ > plan adds will all be standing.
27
+ >
28
+ > **Revision 5** records an adversarial pass (§11) and applies it. Six findings: "no shader changes"
29
+ > was false (§11.1 — BC5 normals need Z reconstruction at five sites), `ShadeImage.format` cannot be
30
+ > memoised (§11.2), `alpha_mode` needed a third state or it would premultiply every data texture
31
+ > (§11.3), the uploader could not see the flag it was told to honour (§11.4), per-format resolution
32
+ > needs a manifest (§11.5), plus corrections of fact (§11.6). §0's central move survived all six.
33
+ >
34
+ > **Revision 6** adds step 11, the GPU block encoder of §4.5 (§9.20), and corrects §4.4's item 2: a
35
+ > copy of a block-compressed level is sized in whole blocks against its physical extent, which the
36
+ > first real `copyBufferToTexture` found and which `texture_write_level` had been getting wrong on
37
+ > hardware since step 3.
38
+
39
+ Assumes the payload is already an `ArrayBuffer` in JS memory. Fetching is not discussed, except in
40
+ §5, where *which* bytes to fetch is the whole point.
41
+
42
+ ---
43
+
44
+ ## 0. The one thing this document is about
45
+
46
+ There is an obvious way to do this and it is wrong: give `ShadeImage` an `is_compressed` flag, add
47
+ `if (compressed)` to the descriptor builder and the uploader, and let the two halves drift for a
48
+ year. The reason that is tempting is that the engine currently decides a texture's **format at
49
+ upload time** by inspecting the pixel data — and compressed data has no pixels to inspect.
50
+
51
+ So the plan is one structural move, and the rest follows from it:
52
+
53
+ > **`ShadeImage` stops describing pixels and starts describing texels.** It carries a
54
+ > `GPUTextureFormat` and a list of mip levels. Uncompressed data is the degenerate case — a
55
+ > 1×1 block — exactly as [`gpu_texture_format_info`](../../descriptor/texture/format/gpu_texture_format_info.js)
56
+ > already models it.
57
+
58
+ The invariant that keeps this honest:
59
+
60
+ > **Below `ShadeImage`, nothing knows whether a texture is compressed.** Every difference is either
61
+ > (a) a value read out of the one format table, or (b) a decision taken *above* `ShadeImage` — in
62
+ > the asset layer, before the image exists.
63
+
64
+ **The axis that legitimately forks is not compressed/uncompressed.** It is *how the bytes reach the
65
+ GPU*, and it forks into three (§4): an external image, host bytes, or a device-side producer. That
66
+ split exists today, has nothing to do with compression, and is where transcoding and — later —
67
+ runtime compression belong.
68
+
69
+ **No shader *variants*.** A `bc7-rgba-unorm` texture binds as `texture_2d<f32>` and samples like
70
+ `rgba8unorm` — no pipeline variants, no codegen, nothing in [wgsl](../../wgsl) that branches on
71
+ format. One shared decode change is needed, and only because two-channel normal formats exist:
72
+ Z reconstruction, applied identically to compressed and uncompressed sources (§11.1).
73
+
74
+ ---
75
+
76
+ ## 1. What is there now
77
+
78
+ Five places encode "a texture's bytes are `channel_count` samples of `data_type` per texel".
79
+
80
+ | Place | Assumption |
81
+ |---|---|
82
+ | [`ShadeImage`](source/ShadeImage.js) | pixels are `(data_type, channel_count, normalized)` plus a `color_space`. No format, no mip levels. |
83
+ | [`inferFormatFromImage`](texture_descriptor_from_texture_shade.js:99) | the format is *derived at upload* from those four fields, by string-building `${prefix}${bit_size}${ending}` |
84
+ | [`texture_descriptor_from_texture_shade`](texture_descriptor_from_texture_shade.js:149) | `GenerateMipMaps` implies `RENDER_ATTACHMENT` **and** a full generated chain. Both are illegal for a block format. |
85
+ | [`texture_write_raw`](texture_write_to_gpu.js:93) | one `writeTexture` hard-coded to `mipLevel: 0`, with `bytesPerRow = channels × bytes × width`; plus RGB→RGBA padding and CPU premultiplication, neither of which can be done to a block |
86
+ | [`TextureDescriptor.bits_per_sample`](../../descriptor/texture/TextureDescriptor.js:100) | a per-channel bit table. Every block format misses it, so `memory_footprint` warns and reports **0 bytes**. |
87
+
88
+ Second order, all of them one-liners once the above is fixed: the resident material atlas
89
+ ([`GPUResidentMaterialContext.js:338`](../material/resident/GPUResidentMaterialContext.js:338)), the
90
+ software device ([`SoftwareGPUQueue.js:220`](../../device/mock/SoftwareGPUQueue.js:220)), the
91
+ bindless allocator ([`GPUBindlessTextureManager.js:114`](bindless/GPUBindlessTextureManager.js:114)),
92
+ [`shade_texture_might_have_alpha`](shade_texture_might_have_alpha.js), and the VT cache's
93
+ `bytes_per_row ?? this.slot_size * 4` ([`VTPhysicalCache.js:324`](virtual/VTPhysicalCache.js:324)).
94
+
95
+ **What is already right, and is the reason this is cheap:**
96
+
97
+ - [`gpu_texture_format_info`](../../descriptor/texture/format/gpu_texture_format_info.js) already
98
+ tabulates every BC, ETC2/EAC and ASTC format with correct block dimensions and byte sizes, and
99
+ already exposes `gpu_texture_format_row_bytes` / `gpu_texture_format_row_count`, which round up to
100
+ whole blocks. This is the table the whole plan runs on. It has three consumers today.
101
+ - [`texture_copy.js`](../../device/mock/texture_copy.js:221) in the software device is already
102
+ block-aware, and the mock already implements `copyBufferToTexture`
103
+ ([`SoftwareGPUCommandEncoder.js:329`](../../device/mock/SoftwareGPUCommandEncoder.js:329)) — which
104
+ no engine code uses yet, and which the GPU transcode path (§4.4) will be the first to want.
105
+ - [`VTPhysicalCache`](virtual/VTPhysicalCache.js:69) is **already parameterised by a per-layer
106
+ `GPUTextureFormat`**. §7 is smaller than it looks.
107
+ - [`SamplerDescriptor`](../../descriptor/texture/SamplerDescriptor.js:17) already carries
108
+ `lodMinClamp`, which is the mechanism mip streaming needs (§3.4).
109
+ - [`gpu_texture_format_srg_to_linear`](format/gpu_texture_format_srg_to_linear.js) and
110
+ [`gpu_texture_format_to_color_space`](format/gpu_texture_format_to_color_space.js) already
111
+ enumerate every BC, ETC2 and ASTC srgb pair — written compressed-aware, never yet handed a
112
+ compressed format (§5.3).
113
+
114
+ ---
115
+
116
+ ## 2. Modules
117
+
118
+ ```
119
+ shade/renderer/texture/source/
120
+ ShadeTexelData.js NEW format + size + mip levels. The compressed payload,
121
+ and the general case of the uncompressed one.
122
+ ShadeImage.js CHG gains .format, .level_count, .alpha_mode
123
+ AlphaMode.js NEW @enum Opaque | Straight | Premultiplied (§11.3)
124
+
125
+ shade/renderer/texture/format/
126
+ gpu_texture_format_is_block_compressed.js NEW one-liner over gpu_texture_format_info
127
+ gpu_texture_format_can_generate_mipmaps.js NEW can the generator render into it (§3.2)
128
+ gpu_texture_format_srg_to_linear.js CHG gains its inverse; already knows BC/ETC2/ASTC
129
+ gpu_texture_compression_support.js NEW device -> Set<GPUTextureFormat>
130
+ texture_format_preference.js NEW (role, support) -> ordered format list
131
+
132
+ shade/renderer/texture/
133
+ texture_write_level.js NEW one level, block-driven. The primitive.
134
+ texture_write_to_gpu.js CHG dispatch on source kind; loops levels
135
+ texture_descriptor_from_texture_shade.js CHG reads image.format; stops inventing one
136
+ shade_texture_might_have_alpha.js CHG answers from the format
137
+
138
+ shade/descriptor/texture/
139
+ TextureDescriptor.js CHG memory_footprint via gpu_texture_format_info
140
+
141
+ shade/device/mock/
142
+ SoftwareGPUQueue.js CHG block-aware writeTexture
143
+ ```
144
+
145
+ Container readers (KTX2, DDS, whatever ships next) are **not** engine code in this list: each is a
146
+ free function `bytes -> ShadeTexelData` living with its container, the way
147
+ [`GltfBufferViewExtension`](../../../format/scene/gltf/ext/GltfBufferViewExtension.js) keeps meshopt out of the
148
+ glTF parser. The engine names no container. Transcoders (§4) are likewise their own tree.
149
+
150
+ ### 2.1 `TextureFormat` — and the load-time globals underneath it
151
+
152
+ ```
153
+ shade/descriptor/texture/format/
154
+ TextureFormat.js NEW @enum {string} — every GPUTextureFormat, by name
155
+ shade/descriptor/
156
+ GPUTextureUsage.js NEW the usage bits, beside the existing GPUShaderStage.js
157
+ ```
158
+
159
+ The precise state of this, because the failure is not quite where it looks:
160
+
161
+ **`GPUTextureFormat` never throws.** It is a WebIDL enum, which in JavaScript is a plain string — no
162
+ global is read, and `gpu_texture_format_info("bc7-rgba-unorm")` runs in node today. A grep confirms
163
+ it: the identifier appears in this tree only inside JSDoc and inside one error message. So a
164
+ `TextureFormat` enum is not what keeps node running.
165
+
166
+ **What it does buy is worth having anyway**, and the third item is a real hole:
167
+
168
+ 1. One enumeration instead of a convention. The names already exist as the keys of `FORMATS` in
169
+ [`gpu_texture_format_info.js`](../../descriptor/texture/format/gpu_texture_format_info.js), which
170
+ even exports `gpu_texture_format_names()` for tests to sweep. Promoting that key set to a named
171
+ enum makes it the source rather than an implementation detail of one table.
172
+ 2. A typo becomes `undefined` at the call site instead of `'rgba8'` reaching
173
+ `gpu_texture_format_info` and throwing one layer down — which is a thing that has already been
174
+ worth writing a test for ([`SoftwareGPUValidation.spec.js:95`](../../device/mock/SoftwareGPUValidation.spec.js:95)).
175
+ 3. **`@webgpu/types` is not a dependency of this package.** Every `@param {GPUTextureFormat}` in the
176
+ tree therefore resolves to nothing, and `npm run generate-types` emits `any` for all of them. An
177
+ in-tree `@enum {string}` resolves without taking that dependency, which is the cheaper of the two
178
+ fixes and the one that does not put a browser typings package in a node tool's path.
179
+
180
+ **The enum must be a proxy, not a parallel identifier space.** Its values are the WebGPU strings
181
+ verbatim, so `TextureFormat.BC7_RGBA_UNORM` is passable straight to `createTexture` and no
182
+ translation exists at any boundary — the same discipline
183
+ [`WebGPUExtensionType`](../WebGPUExtensionType.js) already keeps (`Subgroups: "subgroups"`). A spec
184
+ asserting `Object.values(TextureFormat)` and `gpu_texture_format_names()` are the same set is what
185
+ stops the two drifting (§10.1).
186
+
187
+ **Keys are `SCREAMING_SNAKE_CASE`** — `BC7_RGBA_UNORM`, `ASTC_6X6_UNORM_SRGB`. Settled, so it is not
188
+ re-argued at review: this enum mirrors a specification's namespace rather than naming an engine
189
+ concept, which is the case [`GPUShaderStage`](../GPUShaderStage.js) already answers the same way,
190
+ and `Bc7RgbaUnorm` reads worse than the name every specification and tool writes.
191
+ [`WebGPUExtensionType`](../WebGPUExtensionType.js)'s PascalCase is the deliberate exception, not the
192
+ rule this follows — its keys are engine-facing labels for capabilities, not format identifiers.
193
+
194
+ **Now the thing that actually throws in node.** [`TextureDescriptor.js:61`](../../descriptor/texture/TextureDescriptor.js:61)
195
+ reads `GPUTextureUsage.TEXTURE_BINDING` as a **class field initializer** — at module load, not at
196
+ call time. Under vitest that is covered, by a global stubbed in
197
+ [`vitest.setup.mjs`](../../vitest.setup.mjs). Outside vitest nothing stubs it, so `import`ing
198
+ `TextureDescriptor` from a CLI, an asset-pipeline script, or the offline encoder of §8 throws
199
+ `ReferenceError` before a line of it runs.
200
+
201
+ That matters *because of this plan*: §4.3's CPU transcoder, the container readers, and the offline
202
+ encoder are all node-side consumers of the texture stack, and none of them is a test. The existing
203
+ in-tree precedent is [`descriptor/GPUShaderStage.js`](../GPUShaderStage.js) — "useful for when WebGPU
204
+ constants are not available" — which does exactly this for shader stages and was never extended to
205
+ the other three namespaces. Add `GPUTextureUsage.js` beside it and import it rather than reading the
206
+ global.
207
+
208
+ The vitest stub stays regardless: it covers modules this plan does not touch, and a shim that is
209
+ imported and a global that is stubbed do not conflict.
210
+
211
+ ---
212
+
213
+ ## 3. The design
214
+
215
+ ### 3.1 `ShadeTexelData` — the payload, and `ShadeImage.format` — the seam
216
+
217
+ ```js
218
+ export class ShadeTexelData {
219
+ /** @type {GPUTextureFormat} */
220
+ format;
221
+
222
+ /** texels, of the base level */
223
+ width; height; depth;
224
+
225
+ /**
226
+ * One entry per mip, index 0 = base. Rows of blocks, tightly packed, no padding
227
+ * between rows or levels — exactly what gpu_texture_format_row_bytes describes.
228
+ * A hole is a level that has not arrived yet (§3.4).
229
+ * @type {(Uint8Array|undefined)[]}
230
+ */
231
+ levels;
232
+ }
233
+ ```
234
+
235
+ `ShadeImage.source` may now also be a `ShadeTexelData`, alongside the existing
236
+ `ImageBitmap | Sampler2D | ArrayBuffer`. Two new members on `ShadeImage`, and one free function:
237
+
238
+ ```js
239
+ get level_count() // 1 for everything that exists today
240
+ get alpha_mode() // §3.3
241
+
242
+ texture_format_from_shade_image(image) -> GPUTextureFormat // format/, not a member
243
+ ```
244
+
245
+ **The format is a function, not a getter.** `ShadeImage` describes data — a channel count, a data
246
+ type, a colour space, some bytes — and what WebGPU calls that arrangement is not the data's business.
247
+ Keeping it off the class is what lets the encoder, the container readers and the asset pipeline hold
248
+ an image without any of them importing a graphics API, and it is the same rule the colour package
249
+ already follows: free functions over the model, not methods on it.
250
+
251
+ For a `ShadeTexelData`, the answer is the payload's. For everything else it is exactly what
252
+ [`inferFormatFromImage`](texture_descriptor_from_texture_shade.js:99) computes today — both of its
253
+ arms, the `ImageBitmap` one included — [moved from the descriptor builder to the
254
+ image](texture_descriptor_from_texture_shade.js:12). That move is the whole trick: format stops being
255
+ something the upload path *guesses from the data* and becomes something the image *states*, and a
256
+ compressed image states it the same way an uncompressed one does.
257
+
258
+ **A plain getter, not memoised.** `color_space` is mutated after construction — see §11.2, where
259
+ caching it silently strips sRGB from every glTF albedo map.
260
+
261
+ [`texture_descriptor_from_texture_shade`](texture_descriptor_from_texture_shade.js) then loses
262
+ `inferFormatFromImage` entirely and reads `image.format`. It is the only caller.
263
+
264
+ Keeping `ShadeTexelData` as an object in `source` rather than as fields on `ShadeImage` is
265
+ deliberate: [`optimize_texture_set`](../scene/optimization/optimize_texture_set.js:15) and
266
+ [`serialize_scene`](../scene/serialization/serialize_scene.js:321) key maps on `image.source` by
267
+ identity to deduplicate. A payload is one object, so both keep working untouched.
268
+
269
+ ### 3.2 Mip levels are data; `GenerateMipMaps` is an instruction
270
+
271
+ Two independent facts, currently welded together.
272
+
273
+ **Where a texture's mip levels come from** is a property of the image: it has `level_count` of them,
274
+ and that is how many the texture gets. Nothing infers, nothing fills in.
275
+
276
+ **Whether to run the mip generator** is an instruction the caller gives, and
277
+ `ShadeTextureFlags.GenerateMipMaps` is that instruction. It is honoured or it throws:
278
+
279
+ ```js
280
+ // texture_descriptor_from_texture_shade
281
+ const generate = (shade.flags & ShadeTextureFlags.GenerateMipMaps) !== 0;
282
+
283
+ let mipLevelCount;
284
+
285
+ if (generate) {
286
+ if (!gpu_texture_format_can_generate_mipmaps(format)) {
287
+ throw new Error(
288
+ `${shade.label}: mipmap generation requested for '${format}', which cannot be a render`
289
+ + ` attachment. Mips for this format come from the encoder — supply them in the payload`
290
+ + ` and clear ShadeTextureFlags.GenerateMipMaps.`
291
+ );
292
+ }
293
+
294
+ mipLevelCount = compute_max_mip_count(image.width, image.height);
295
+ usage |= GPUTextureUsage.RENDER_ATTACHMENT;
296
+ } else {
297
+ mipLevelCount = image.level_count;
298
+ }
299
+ ```
300
+
301
+ and [`TextureManager.contextFromShadeTexture`](TextureManager.js:128) schedules the generator if and
302
+ only if `generate` — not if some level count comparison suggests it might be useful.
303
+
304
+ Three consequences, and none of them is a fallback:
305
+
306
+ - **The flag always means what it says.** Set it on an image that already has a chain and the chain
307
+ is regenerated; the supplied levels above 0 are not uploaded, because you asked for generated ones
308
+ instead. That is a real use — a DDS or KTX2 whose mips were built with a filter you do not want,
309
+ re-derived through [`mipmaps/filters/`](mipmaps/filters) — and not merely a pedantic reading.
310
+ - **The flag on a block format is an error, always**, whatever the level count. Nothing on the device
311
+ turns a BC7 base into BC7 mips (that is §4.5, and it is not built). The message names the format
312
+ and says what to do instead.
313
+ - **An uncompressed image carrying its own mips works**, which the engine cannot do today, by simply
314
+ not setting the flag.
315
+
316
+ `gpu_texture_format_can_generate_mipmaps` is the honesty predicate: the generator renders between mip
317
+ levels, so the format must be usable as a colour attachment. Block-compressed is the case this plan
318
+ adds; `rgb9e5ufloat` and the `snorm` formats are non-renderable *today* and would already fail deeper
319
+ in [`WebGPUMipmapGenerator`](mipmaps/WebGPUMipmapGenerator.js) with a worse message. One predicate
320
+ over the format table closes both.
321
+
322
+ **On `DEFAULT_FLAGS`.** [`ShadeTexture`](ShadeTexture.js:8) defaults the flag *on*, so a loader
323
+ building a compressed texture must clear it, and forgetting to is a hard error naming the texture.
324
+ The tempting alternative — have `ShadeTexture.from` derive the default from the image, the way it
325
+ already derives `dimensions` — is rejected for the same reason revision 2's §3.2 was: it makes
326
+ behaviour depend on data. An uncompressed asset that ships with mips would silently stop generating
327
+ them, and re-exporting it without mips would silently start again. A two-line `clearFlag` at each
328
+ compressed loader is the cheaper price.
329
+
330
+ **Rule, unchanged:** `level_count` is either `1` or the full chain for the base size. A partial chain
331
+ is a malformed payload, rejected in `ShadeTexelData`, which keeps
332
+ [`WebGPUMipmapGenerator`](mipmaps/WebGPUMipmapGenerator.js) unchanged — it is never asked to start
333
+ from a level other than 0. Not to be confused with a chain whose *levels have not all arrived yet*,
334
+ which is §3.4 and is fine.
335
+
336
+ A single-level block-compressed payload is **not** an error. A LUT, a UI sprite, a 4×4 icon
337
+ legitimately has one level, and rejecting content because it might alias somewhere is the engine
338
+ overruling the asset pipeline about a decision that is not its own. Whether a wall texture should
339
+ have shipped with mips is a validator's question.
340
+
341
+ ### 3.3 Alpha mode is declared, not inferred
342
+
343
+ [`texture_write_to_gpu:217`](texture_write_to_gpu.js:217) decides to premultiply from
344
+ `image.color_space !== ColorSpace.None`. That is a proxy for "this is colour, so it probably has
345
+ straight alpha", and it is already wrong in one direction — an `ArrayBuffer` image that arrives
346
+ already premultiplied gets premultiplied twice. For a block payload it is unfixable: you cannot
347
+ premultiply BC7 without decoding it.
348
+
349
+ Make it a property of the data:
350
+
351
+ ```js
352
+ export const AlphaMode = {
353
+ Opaque: 0, // no meaningful alpha — data. Never premultiply.
354
+ Straight: 1, // colour with straight alpha
355
+ Premultiplied: 2, // already done, by the encoder or by copyExternalImageToTexture
356
+ };
357
+ ```
358
+
359
+ Three states, not two: premultiplication is not a property of the alpha channel but of whether that
360
+ channel *means* opacity, and collapsing `Opaque` into `Straight` premultiplies every height field and
361
+ ORM map on upload (§11.3). `ShadeImage.alpha_mode` defaults from `color_space` where the image is
362
+ built — `ColorSpace.None` to `Opaque`, otherwise `Straight` — which is today's rule, preserved. At
363
+ upload:
364
+
365
+ | declared | format | action |
366
+ |---|---|---|
367
+ | `Opaque` | either | nothing. Today's `color_space === None` behaviour. |
368
+ | `Straight` | uncompressed | premultiply on the CPU — today's code, unchanged |
369
+ | `Straight` | block | **throw.** The producer had to do this; nothing here can. |
370
+ | `Premultiplied` | either | nothing |
371
+
372
+ `ImageBitmap` sources keep using `copyExternalImageToTexture`'s `premultipliedAlpha`, which is the
373
+ same decision expressed through the API that owns it.
374
+
375
+ This is the one place the compressed path imposes a requirement on the *producer* rather than on the
376
+ engine, and it is better stated loudly than discovered as dark fringes on foliage.
377
+
378
+ ### 3.4 One level at a time — the primitive, and what streaming needs
379
+
380
+ [`texture_write_raw`](texture_write_to_gpu.js:93) computes its own layout from
381
+ `channel_count × dataTypeByteSize(data_type)` and writes exactly one thing, at `mipLevel: 0`. Both
382
+ have to go, and the replacement is not "a loop over levels" but **a function that writes one level**,
383
+ with the loop as its caller:
384
+
385
+ `texture_write_to_gpu` gains a level range alongside it — without one it cannot honour §3.2's rule
386
+ that generated mips replace supplied ones, because the flag lives on the `ShadeTexture` and the
387
+ uploader never receives it (§11.4). The same parameter is what lets a streaming arrival write exactly
388
+ one level, so it is one addition serving both.
389
+
390
+ ```js
391
+ // texture_write_level.js — the primitive
392
+ export function texture_write_level(queue, texture, format, level, bytes, size) {
393
+ const w = Math.max(size[0] >> level, 1);
394
+ const h = Math.max(size[1] >> level, 1);
395
+ const d = is_3d ? Math.max(size[2] >> level, 1) : size[2];
396
+
397
+ queue.writeTexture(
398
+ { texture, mipLevel: level, origin: { x: 0, y: 0, z: 0 } },
399
+ bytes,
400
+ {
401
+ offset: 0,
402
+ bytesPerRow: gpu_texture_format_row_bytes(format, w),
403
+ rowsPerImage: gpu_texture_format_row_count(format, h),
404
+ },
405
+ [w, h, d]
406
+ );
407
+ }
408
+ ```
409
+
410
+ The level, not the image, is the unit — because **meep streams**. A texture is not a thing that
411
+ arrives; it is a thing that keeps arriving. The descriptor is built from a header (format, base size,
412
+ level count) before any texel bytes exist, the texture is allocated once at full size, and levels
413
+ land as the network delivers them, coarsest first.
414
+
415
+ Two mechanics make that work, and both already exist:
416
+
417
+ - **Sampling a partly-resident texture.** Unwritten mips read as zero, so a texture with only its
418
+ coarse levels present must not be sampled at its fine ones. Clamp with
419
+ [`SamplerDescriptor.lodMinClamp`](../../descriptor/texture/SamplerDescriptor.js:17), lowered as
420
+ levels arrive. This keeps the texture's dimensions and UV mapping correct, which a narrowed
421
+ `TextureViewDescriptor` would not — a view starting at `baseMipLevel: 8` makes the texture claim to
422
+ be 1/256 the size and puts every LOD computation downstream out by eight.
423
+ - **Level-tail ordering.** The coarse levels of a mip chain are a rounding error in bytes — level 0
424
+ is 3/4 of the pyramid — so "everything but level 0" is a cheap first request that makes a texture
425
+ usable, and level 0 is a second one. Block formats make that tail *exact*: the 1×1 mip of a BC7
426
+ texture is one 16-byte block, which `gpu_texture_format_row_bytes` already rounds up to correctly
427
+ and a hand-rolled `width * bytes_per_texel` does not.
428
+
429
+ Two properties of `writeTexture` worth recording, because the GPU transcode path (§4.4) has neither:
430
+
431
+ - **No `bytesPerRow` alignment requirement.** That is `copyBufferToTexture`'s rule (256 bytes), not
432
+ `writeTexture`'s. Tightly-packed levels upload directly, with no staging buffer and no padding.
433
+ This is what makes "the bytes are already in JS memory" the easy case it should be.
434
+ - **`rowsPerImage` is now always supplied**; today it is set only when `depth > 1`
435
+ ([:165](texture_write_to_gpu.js:165)). Harmless for 2D, required for arrays.
436
+
437
+ The RGB→RGBA padding and the premultiply pass stop being branches *inside* the writer and become a
438
+ transform producing a payload *before* it.
439
+
440
+ ### 3.5 Capability is the engine's; selection is the asset layer's
441
+
442
+ Add to [`WebGPUExtensionType`](../../descriptor/WebGPUExtensionType.js):
443
+
444
+ ```js
445
+ TextureCompressionBC: "texture-compression-bc",
446
+ TextureCompressionETC2: "texture-compression-etc2",
447
+ TextureCompressionASTC: "texture-compression-astc",
448
+ ```
449
+
450
+ and to `optional_desired` in [`Renderer.js:1165`](../Renderer.js:1165) — all three, taken when
451
+ offered, exactly as `TextureFormatsTier1` already is. None is required.
452
+
453
+ The engine then exposes two things and stops:
454
+
455
+ ```js
456
+ // what this device can sample
457
+ gpu_texture_compression_support(device) -> Set<GPUTextureFormat>
458
+
459
+ // what this engine would prefer for this kind of content, best first,
460
+ // filtered to what the device has
461
+ texture_format_preference(role, support) -> GPUTextureFormat[]
462
+ ```
463
+
464
+ `role` is albedo / normal / orm / emissive — the same distinction
465
+ [`image_compression_quality_from_usage`](../scene/serialization/serialize_scene.js:168) already
466
+ draws. The renderer owns this because the renderer is what knows BC5 is right for a normal map and
467
+ BC1 is not.
468
+
469
+ **It does not own what happens next.** The asset layer takes that ordered list, resolves the first
470
+ entry to a concrete resource, and fetches only that one (§5). The engine never sees the formats it
471
+ did not ask for.
472
+
473
+ ---
474
+
475
+ ## 4. Three ways bytes reach a texture
476
+
477
+ ### 4.1 The axis that actually forks
478
+
479
+ `texture_write_to_gpu` already dispatches on *source kind*: an `ImageBitmap` goes through
480
+ `copyExternalImageToTexture`, a `Sampler2D` or `ArrayBuffer` through `writeTexture`. So does
481
+ [`VTPhysicalCache.upload`](virtual/VTPhysicalCache.js:306), independently, in the same shape.
482
+
483
+ Adding compression does not add a fourth kind. It adds **texels the host cannot look inside**, which
484
+ the `writeTexture` arm handles by asking the format table for the layout instead of computing it.
485
+ What *does* add a third kind is a producer that runs on the device:
486
+
487
+ | kind | mechanism | why it exists |
488
+ |---|---|---|
489
+ | external image | `copyExternalImageToTexture` | the browser decoded it; never compressed |
490
+ | host bytes | `writeTexture` | the common case, compressed or not |
491
+ | device-side producer | compute pass → `copyBufferToTexture` | GPU transcode (§4.4), and later runtime compression (§4.5) |
492
+
493
+ The third row is not a compressed-texture feature. Procedural and GPU-generated content wants the
494
+ same shape, and the mock device already implements the API for it
495
+ ([`SoftwareGPUCommandEncoder.js:329`](../../device/mock/SoftwareGPUCommandEncoder.js:329)) while no
496
+ engine code uses it yet. Naming the three explicitly is worth doing regardless of this plan; doing it
497
+ here is what keeps the transcoder from becoming a private path inside `TextureManager`.
498
+
499
+ ### 4.2 Transcoding is supported, and is not the main path
500
+
501
+ KTX2 with Basis Universal (ETC1S or UASTC) is what the web's model pipeline emits.
502
+ `KHR_texture_basisu` is a ratified glTF extension, `gltf-transform` and `toktx` produce it by
503
+ default, and a viewer that cannot read it cannot read the ecosystem's models. Support it.
504
+
505
+ But it is a **compatibility path, not the delivery path**. Transcoding pays CPU or GPU time on load
506
+ to recover a format the device could have been handed directly, and meep's own pipeline hands it
507
+ directly (§5). The distinction to hold on to:
508
+
509
+ - **Third-party content** — a glTF someone exported, a model dropped on the canvas — arrives as
510
+ KTX2/Basis and is transcoded.
511
+ - **First-party content** — anything meep's own tools produced — arrives already in a device format
512
+ and is never transcoded.
513
+
514
+ Both produce a compressed texture, and below that they are the same texture.
515
+
516
+ ### 4.3 The CPU transcoder
517
+
518
+ Signature, and the whole of its contract:
519
+
520
+ ```js
521
+ // bytes in, texels out. No device, no queue, no engine.
522
+ transcode_basis(payload, target_format) -> ShadeTexelData
523
+ ```
524
+
525
+ That is the §3.1 seam exactly, so a CPU-transcoded texture enters through the ordinary door: a
526
+ `ShadeImage` over a `ShadeTexelData`, uploaded by `texture_write_level` with no knowledge that a
527
+ transcoder was involved. It is worker-safe by construction — it imports nothing outside `core/` —
528
+ which is where it should run.
529
+
530
+ ETC1S→BC1/BC7 and UASTC→BC7/ASTC are block-to-block transforms with no cross-block dependencies, and
531
+ the Basis format specification is public. This repo's convention — meshopt, AVIF — is to implement
532
+ from a specification rather than vendor a wasm blob. That is a real cost, and it should be paid
533
+ deliberately, which is why §9 sequences it last.
534
+
535
+ ### 4.4 The GPU transcoder
536
+
537
+ The same transform, per block, with no dependencies between blocks — which is a compute shader with
538
+ one invocation per output block. It cannot write its result as a storage texture, because block
539
+ formats are not storage-bindable, so it writes a buffer and copies:
540
+
541
+ ```
542
+ source blocks (storage buffer, uploaded once)
543
+ -> compute pass, one invocation per output block
544
+ -> output buffer
545
+ -> copyBufferToTexture, one call per mip level
546
+ -> the ordinary compressed texture
547
+ ```
548
+
549
+ Three constraints shape the output buffer's layout, and are worth writing down now rather than
550
+ discovering at the copy:
551
+
552
+ 1. **`copyBufferToTexture` requires `bytesPerRow % 256 === 0`.** `writeTexture` does not (§3.4). For
553
+ BC7 a row is `ceil(w / 4) × 16` bytes, so alignment needs `ceil(w / 4)` to be a multiple of 16 —
554
+ which for the power-of-two widths of a mip chain means `w >= 64`, and every level below that needs
555
+ its rows padded. Lay the buffer out padded from the start.
556
+ 2. **Origin and extent must be block multiples**, as everywhere else. Trivially satisfied for a
557
+ whole-level copy, since the physical extent of a mip is its size rounded up to whole blocks.
558
+ 3. **One `copyBufferToTexture` per level**, since each level has its own row padding. They share one
559
+ encoder and one buffer.
560
+
561
+ The output of this path is a `GPUTexture`, not a `ShadeTexelData` — it never touches host memory,
562
+ which is the point. So it enters as §4.1's third kind, not through `ShadeImage`. That is a genuine
563
+ second route into a texture, and it is justified by *where the bytes are*, not by whether they are
564
+ compressed: an uncompressed procedural texture takes the same route.
565
+
566
+ ### 4.5 Runtime compression, later
567
+
568
+ > **Landed 2026-09-02, as step 11** — [`texture_encode`](encode/texture_encode.js), with BC4 as the
569
+ > first encoder and the rest refused by name; see §9.20 and
570
+ > [`encode/ENCODE_NOTES_2026_09_02.md`](encode/ENCODE_NOTES_2026_09_02.md). Everything below held,
571
+ > with one correction: the copy extent for a partial mip is the level's *physical* size, rounded up to
572
+ > whole blocks, not its logical one.
573
+
574
+ Not being built now. Recorded because the seam either admits it or does not, and it costs nothing to
575
+ make sure it does.
576
+
577
+ Compressing at runtime — an ordinary decoded image in, block data out — is the same shape as §4.4
578
+ with a different shader: a compute pass over 4×4 texel neighbourhoods, an output buffer of blocks, a
579
+ `copyBufferToTexture`. Everything in §4.4's numbered list applies unchanged. Fixed-rate formats make
580
+ this tractable in a way variable-rate ones never are: the output size is known before the shader
581
+ runs, so the buffer is allocated up front and every invocation knows exactly where to write.
582
+
583
+ The thing that would preclude it is treating "compressed" as a property of *how a texture was
584
+ loaded*. It is a property of the texture's format, and a texture compressed on the device five
585
+ seconds ago is indistinguishable from one that arrived that way. §3.1 is what buys that.
586
+
587
+ ---
588
+
589
+ ## 5. Delivery: one format per resource, chosen at request time
590
+
591
+ ### 5.1 The philosophy point
592
+
593
+ meep streams. It is not a packaged engine that loads a level file and starts; it fetches what it
594
+ needs, when it needs it, at the granularity it needs. The SHADE scene container
595
+ ([`serialize_scene.js`](../scene/serialization/serialize_scene.js)) is one delivery mechanism among
596
+ several — useful, and not the model to design around.
597
+
598
+ So: **a texture is not one resource with variants inside it. It is several resources, one per format,
599
+ and the client fetches the one it wants.** The engine's part is §3.5's ordered preference list; the
600
+ asset layer's part is turning the first entry into a URL.
601
+
602
+ ```js
603
+ const formats = texture_format_preference(role, support); // e.g. [bc7, astc-4x4]
604
+
605
+ for (const format of formats) {
606
+ const description = resolve(logical_name, format); // may not exist
607
+ if (description !== undefined) {
608
+ return load(description);
609
+ }
610
+ }
611
+
612
+ return load(resolve(logical_name, undefined)); // raw — AVIF, PNG, whatever ships
613
+ ```
614
+
615
+ `resolve` must answer from a **manifest known before the first request** — which formats were built
616
+ for this texture — or a device whose preferred format was never built pays a 404 round-trip per
617
+ preference-list entry before reaching the fallback (§11.5). That list belongs beside whatever already
618
+ names the texture.
619
+
620
+ [`AssetManager.assignAlias`](../../../engine/asset/AssetManager.js:1396) is the mechanism that
621
+ already exists for exactly this indirection — a logical name resolving to a concrete
622
+ `AssetDescription`. Making that resolution format-aware, rather than inventing a parallel one, is the
623
+ smaller change and the one that lets a texture be requested by name from anywhere.
624
+
625
+ The uncompressed resource is not a fallback bolted on: it is the entry with no format constraint, and
626
+ it is what a device with no compression support, a tool, or a test asks for.
627
+
628
+ ### 5.2 Which formats to build — and why revision 1 had this backwards
629
+
630
+ Revision 1 argued against shipping ETC2 because "every variant added is bytes every user downloads".
631
+ Under per-format delivery that is simply false: **a format nobody requests costs origin storage and
632
+ build time, and zero bytes of anyone's download.** The calculus is entirely different, and the answer
633
+ becomes "build whatever the audience's devices can sample":
634
+
635
+ | Content | Format | Note |
636
+ |---|---|---|
637
+ | albedo, emissive | `bc7-rgba-unorm` | desktop. Best quality-per-byte for colour with alpha. |
638
+ | normal maps | `bc5-rg-unorm` | two channels, **and Z reconstruction in the shader** — the one shader change this plan carries, §11.1 |
639
+ | ORM, single-channel masks | `bc4-r-unorm` / `bc7-rgba-unorm` | linear |
640
+ | anything, mobile / Apple | `astc-4x4-unorm`, coarser blocks per role | see §7.2 for why 4×4 specifically |
641
+ | ETC2 | `etc2-rgba8unorm` | build it if the audience has it — it costs everyone else nothing |
642
+
643
+ ### 5.3 sRGB is not a separate resource
644
+
645
+ `bc7-rgba-unorm` and `bc7-rgba-unorm-srgb` are **the same encoded bytes**; the suffix changes how the
646
+ sampler decodes them, not what is stored. The same holds for every BC/ETC2/ASTC pair.
647
+
648
+ So the delivery key is the non-srgb name, and the `-srgb` variant is chosen when the texture is
649
+ created, from the image's `color_space` — which `ShadeImage` already carries and
650
+ [`gltf_create_material.js:54`](../loader/gltf/gltf_create_material.js:54) already sets. This halves
651
+ the artifact count for free, and keeps the sRGB decision where it belongs: with the content role, not
652
+ with the bytes.
653
+
654
+ The mapping that does it already exists.
655
+ [`gpu_texture_format_srg_to_linear`](format/gpu_texture_format_srg_to_linear.js) enumerates every
656
+ BC/ETC2/ASTC srgb pair, and so does
657
+ [`gpu_texture_format_to_color_space`](format/gpu_texture_format_to_color_space.js) — both were
658
+ written compressed-aware and neither has ever been handed a compressed format. What is missing is
659
+ only the inverse direction, in the file that already holds the forward one.
660
+
661
+ ---
662
+
663
+ ## 6. Everything that breaks, and what it becomes
664
+
665
+ ### 6.1 Memory accounting reports zero
666
+
667
+ [`TextureDescriptor.bits_per_sample`](../../descriptor/texture/TextureDescriptor.js:100) misses every
668
+ block format, so `memory_footprint` warns and returns 0 — a compressed scene would report an empty
669
+ texture budget through [`TextureManager.gpu_memory_usage`](TextureManager.js:172) and through the
670
+ transient pool's `sizeOf` in [`GPUTextureAllocator`](GPUTextureAllocator.js).
671
+
672
+ Rewrite `memory_footprint` on `gpu_texture_format_info`:
673
+
674
+ ```js
675
+ const info = gpu_texture_format_info(this.format);
676
+
677
+ total += Math.ceil(w / info.block_width) * Math.ceil(h / info.block_height) * d * info.bytes_per_block;
678
+ ```
679
+
680
+ which is verbatim what
681
+ [`frame_graph_extract_topology.js:42`](../../device/timing/profile/frame_graph_extract_topology.js:42)
682
+ already computes. Two implementations of one thing become one — as
683
+ `gpu_texture_format_memory_footprint`, exported from the table's own module beside
684
+ `gpu_texture_format_row_bytes`, since that is where this file already keeps derived helpers.
685
+
686
+ **`bits_per_sample` is deleted, not kept.** An earlier revision said it stays for
687
+ [`ColorTargetState`](../../descriptor/pipeline/render/ColorTargetState.js:49); that was a misreading.
688
+ `ColorTargetState` computes its own local sum straight from
689
+ `gpu_texture_format_channel_bit_count` and has never called the getter, whose only caller was
690
+ `memory_footprint` itself. A public accessor that warns and returns 0 for sixty-odd formats is worth
691
+ removing rather than leaving for someone to find. The table it read stays, for `ColorTargetState`.
692
+
693
+ **The signature is positional and every argument is required** —
694
+ `(format, size, dimension, mip_level_count, sample_count)`. Not an options object: the two callers
695
+ hold their fields under different names (`size` against `resolution`), so a destructured parameter
696
+ would have been satisfied by passing a whole descriptor and would have coupled this function to the
697
+ shape of both. And no defaults, because there is no sensible default for a mip or sample count — a
698
+ caller that does not know them is not in a position to ask the question, and a defaulted `1` answers
699
+ a different one convincingly. The counts are asserted instead, so a missing argument is loud rather
700
+ than `NaN`.
701
+
702
+ **A second bug, found while writing it.** Both implementations halved the *third size component*
703
+ per mip level. That is right for a 3D texture and wrong for an array one, whose layers each carry the
704
+ whole chain — a six-layer cube map was under-reported by nearly half. The shared function takes
705
+ `dimension` and halves only for `"3d"`. Neither descriptor had to change to supply it; both already
706
+ carry the field.
707
+
708
+ ### 6.2 The resident material atlas's fast path
709
+
710
+ [`GPUResidentMaterialContext.js:338`](../material/resident/GPUResidentMaterialContext.js:338) takes
711
+ `copyTextureToTexture` from the source into an `rgba8unorm` array when the source is square, POT and
712
+ not sRGB — otherwise it re-renders through `render_copy_texture`. The `!is_srgb` term is standing in
713
+ for "the formats are copy-compatible", which happens to be true today because every material texture
714
+ is `rgba8unorm`. A BC7 source would reach it and fail validation.
715
+
716
+ Replace the proxy with the fact: `source_ctx.gpu_texture.format === target.format`. Compressed
717
+ sources then take the render path, which is correct and already exists — sampling a BC7 texture into
718
+ an `rgba8unorm` attachment is legal, and re-encoding to fit the atlas is what that path is for.
719
+
720
+ ### 6.3 The software device cannot write blocks
721
+
722
+ [`SoftwareGPUQueue.writeTexture:220`](../../device/mock/SoftwareGPUQueue.js:220) computes
723
+ `texture.bytes_per_texel`, which
724
+ [throws for block formats by design](../../device/mock/SoftwareGPUTexture.js:105). Restate the row
725
+ loop in blocks using `gpu_texture_format_row_bytes` / `gpu_texture_format_row_count` — the same shape
726
+ [`texture_copy.js:221`](../../device/mock/texture_copy.js:221) already uses for
727
+ `copyTextureToTexture`. This is what makes the orchestration tier testable without a GPU, and it is
728
+ why step 5 of §9 is not optional.
729
+
730
+ ### 6.4 Things that want CPU pixels
731
+
732
+ | Caller | Resolution |
733
+ |---|---|
734
+ | [`shade_texture_might_have_alpha`](shade_texture_might_have_alpha.js) | **Improved, not broken.** It currently guesses from `source.itemSize` and otherwise defaults to `true`. The format answers exactly: `bc4`/`bc5`/`etc2-rgb8unorm` have no alpha, while `bc7`/`bc3`/`astc` may — and `bc1-rgba-unorm` and `etc2-rgb8a1unorm` carry 1-bit alpha and must answer **true** (§11.6). |
735
+ | [`download_texture_data`](download_texture_data.js) | Debug readback, via `parse_gpu_texture_format`, whose regex cannot parse a block name. Throw a message naming the format rather than fail inside a regex. |
736
+ | [`VTSourceImage`](virtual/source/VTSourceImage.js) | Builds mip pyramids on `OffscreenCanvas` from `ImageBitmap`. Structurally incompatible with block data — §7.3. |
737
+
738
+ ### 6.5 Bindless
739
+
740
+ [`GPUBindlessTextureManager.js:114`](bindless/GPUBindlessTextureManager.js:114) sizes its allocation
741
+ via `parse_gpu_texture_format(...).getByteSize()`. Its `upload` throws `Not implemented`, so nothing
742
+ regresses today — but the arithmetic should move to `gpu_texture_format_info` when it is finished,
743
+ for the same reason as §6.1.
744
+
745
+ ---
746
+
747
+ ## 7. Virtual textures — what this must not preclude
748
+
749
+ Compressed VT is **out of scope to build** and in scope to not design out. What follows is the list
750
+ of things that would have made it impossible, and what the plan does about each.
751
+
752
+ ### 7.1 WebGPU permits it
753
+
754
+ Writing compressed tiles into a compressed atlas is legal. The rules, from the copy-validation
755
+ sections of the specification, are the same three for `writeTexture`, `copyBufferToTexture` and
756
+ `copyTextureToTexture`:
757
+
758
+ - `origin.x` a multiple of the format's `block_width`, `origin.y` of `block_height`
759
+ - `copySize.width` a multiple of `block_width`, `copySize.height` of `block_height`
760
+ - the copy fits within the *physical* extent of the subresource, which for a compressed format is the
761
+ level's size rounded up to whole blocks
762
+
763
+ No decompression, no intermediate, no per-tile texture. A tile upload stays exactly what it is today:
764
+ one `writeTexture` at an offset.
765
+
766
+ ### 7.2 The cache is already most of the way there
767
+
768
+ [`VTPhysicalCache`](virtual/VTPhysicalCache.js:69) takes a `VTCacheLayer[]`, each with its own
769
+ `GPUTextureFormat`, and creates one atlas texture per layer. Albedo as BC7 beside normals as BC5 is
770
+ already expressible. Three things stand between that and working:
771
+
772
+ 1. **`bytes_per_row ?? this.slot_size * 4`** ([:324](virtual/VTPhysicalCache.js:324)) — the one
773
+ uncompressed assumption in the upload. Becomes
774
+ `gpu_texture_format_row_bytes(layer.format, this.slot_size)`, the same call §3.4 makes.
775
+ 2. **`RENDER_ATTACHMENT`** ([:164](virtual/VTPhysicalCache.js:164)) is requested unconditionally, for
776
+ the `copyExternalImageToTexture` path. Illegal on a compressed format, and unnecessary — a
777
+ compressed layer has no external-image path to serve. Condition it on the layer's format.
778
+ 3. **`slot_size = page_size + border × 2` must be a multiple of the block size.** Slot origins are
779
+ `slot_index × slot_size`, so aligning the slot size aligns every origin for free — one assertion
780
+ in the constructor covers the whole system.
781
+
782
+ That last constraint binds unevenly, which is worth spelling out. For a 4×4 block and the default
783
+ 128-texel page, any even border works (128 + 2·4 = 136 = 34 blocks). For `astc-6x6` it needs
784
+ `(128 + 2·border) ≡ 0 (mod 6)`, i.e. `border ≡ 2 (mod 3)` — satisfiable, and a trap. **This is the
785
+ argument for `astc-4x4` in the VT path** even where a coarser block would be chosen for an ordinary
786
+ texture: it makes the alignment rule the same one BC already imposes.
787
+
788
+ ### 7.3 Borders must be baked in, which rules out one source
789
+
790
+ [`VTSourceImage`](virtual/source/VTSourceImage.js) builds pyramids on `OffscreenCanvas` and resolves
791
+ each page's border at runtime by blitting from neighbouring content. That is impossible for block
792
+ data — border and interior share blocks at the seam, and re-encoding a block at runtime is precisely
793
+ the machinery this is not building.
794
+
795
+ So a compressed stack must come from a source whose tiles are **produced with their borders already
796
+ inside them**, which is what [`VTSourceTiled`](virtual/source/VTSourceTiled.js) is for and what
797
+ `VTPhysicalCache.upload` already documents ("border already baked in"). `VTSourceImage` stays the
798
+ uncompressed runtime-virtualization path and should say so rather than produce blank pages.
799
+
800
+ This is not a new limitation so much as §3.2's rule restated at tile granularity: **blocks are
801
+ produced by an encoder that can see the texels, and nothing downstream re-derives them.**
802
+
803
+ ### 7.4 What this plan owes VT
804
+
805
+ Nothing structural — which is the finding. Block-aware row arithmetic through
806
+ `gpu_texture_format_row_bytes` (§3.4), format-carrying payloads (§3.1), and per-format delivery (§5)
807
+ are what the tile path needs, and it needs them in the same form the texture path does. The tile
808
+ becomes the streaming unit instead of the mip level, and §3.4's primitive is already written at the
809
+ right granularity for that.
810
+
811
+ ---
812
+
813
+ ## 8. Out of scope
814
+
815
+ - **Compressed 3D textures** — they need `texture-compression-bc-sliced-3d` or
816
+ `texture-compression-astc-sliced-3d`, and nothing in the engine wants one.
817
+ - **Compressed virtual textures** — §7. Not built; not designed out.
818
+ - **Runtime compression** — §4.5. Not built; not designed out.
819
+ - **On-GPU BC/ASTC encoders** — the shader behind §4.5.
820
+ - **The offline encoder.** First-party content has to be compressed by something, and that something
821
+ is a tool, not the engine. It belongs beside the AVIF encoder
822
+ ([`avif/threaded_image_encoder.js`](../../../engine/asset/loaders/image/avif/threaded_image_encoder.js)), reusing its worker
823
+ pool. Mips are generated *before* compression, from the source texels, with the same filters
824
+ [`mipmaps/filters/`](mipmaps/filters) uses — compressing level 0 and decompressing it to build
825
+ level 1 is how a pyramid rots.
826
+
827
+ ---
828
+
829
+ ## 9. Staging
830
+
831
+ Each step is independently committable and leaves the engine working.
832
+
833
+ | # | Change | Risk |
834
+ |---|---|---|
835
+ | 0 | **Landed.** `TextureFormat` enum; `GPUTextureUsage.js` shim beside `GPUShaderStage.js`; `TextureDescriptor` **and `TextureResourceDescriptor`** import it (§2.1, §9.1) | none. Unblocks every node-side consumer this plan later adds. The `{GPUTextureFormat}` → `{TextureFormat}` JSDoc sweep is mechanical, changes no behaviour — both are strings — and still lags. |
836
+ | 1 | **Landed.** `memory_footprint` onto `gpu_texture_format_info`; duplicate in `frame_graph_extract_topology` dropped; `bits_per_sample` deleted; array-vs-3D mip depth fixed (§6.1) | none — it fixes two wrong numbers |
837
+ | 2 | **Landed.** `AlphaMode`, `ShadeTexelData`, `ShadeImage.format` / `.level_count` / `.alpha_mode`; descriptor builder and uploader read them (§3.1, §3.3, §9.2) | pure refactor. **No new formats.** Every existing texture test passed with its assertions untouched — `"color_space None skips premultiplication entirely"` included, which is the §11.3 guard. |
838
+ | 3 | **Landed.** `texture_write_level` as the primitive; `texture_write_to_gpu` becomes its caller (§3.4, §11.4, §9.3) | as above — the existing upload tests pass with their assertions untouched |
839
+ | 3b | **Landed.** `chunk_decode_tangent_normal` at the five normal-sample sites; `texture_format_preference` now leads with BC5 (§11.1, §9.8) | one shared chunk, verified on today's RGB normal maps — the stored blue channel is redundant, so the decode agrees with it to 5 decimal places |
840
+ | 4 | **Landed.** `GenerateMipMaps` honoured or thrown; levels otherwise come from the image; `texture_write_to_gpu` gains a required `level_count` (§3.2, §9.5) | behaviour identical for every image that exists today — nothing the engine currently builds can reach the new throw |
841
+ | 5 | **Landed.** `writeTexture` delegates to `texture_copy` (§6.3, §9.6). A BC7 texture now goes end to end with no GPU. | test tier only |
842
+ | 6 | **Landed.** Device features, `gpu_texture_compression_support`, `TextureRole`, `texture_format_preference`; §6.2 and §6.4 guards (§3.5, §9.7) | none until a compressed texture exists |
843
+ | 7 | **Landed.** `TextureVariantSet` (the manifest) and `texture_variant_select`; **not** in `AssetManager` (§5.1, §9.9) | additive — an unbuilt format falls through to the fallback, and nothing consumes this until step 8 |
844
+ | 8 | **Landed.** KTX2 reader in [`src/ktx2/`](../../../format/texture/ktx2/ktx2_read.js) — container to `ShadeTexelData`, for files already in a device format; ZLIB supercompression read rather than refused (§9.10) | additive — nothing imports it yet, and nothing existing changed except the cursor it reuses |
845
+ | 9 | **Landed.** CPU Basis transcoder in [`src/basis/`](../../../format/texture/basis/transcode_basis.js) — ETC1S to BC7 and BC1, implemented from the specification; `ktx2_read` accepts BasisLZ; `KHR_texture_basisu` read at both glTF sites (§4.3, §9.11) | the expensive one, and sequenced last deliberately |
846
+ | 9b | **Landed.** UASTC to BC7 and BC5 — the other Basis encoding, and the one normal maps use; every UASTC mode carried into the BC7 mode the specification pairs it with, partitions included; the glTF loader picks a target from the material slot, so a normal map reaches BC5; Zstandard read from RFC 8878, which is what real UASTC is wrapped in (§9.12) | additive, and within a few tenths of a decibel of the reference transcoder on every fixture |
847
+ | 9c | **Landed.** [`playground/basis_textures`](../../playground/basis_textures/README.md) — tier 8, and the first GPU to see any of this. Sponza at **4.00x** less texture memory, 69/69 textures accepted. Found the `GenerateMipMaps` refusal and the cumulative-total misreading (§9.13) | fixes one real defect in `load_gltf`; the playground itself is additive |
848
+ | 9d | **Landed.** `rgba8unorm` from both transcoders, and the glTF loader falls to it when the device can sample no compressed target — closing §9.13's refusal. ETC1S's uncompressed output is byte-exact against the reference (§9.15) | removes a refusal; costs 4x the memory, and only where there was no texture before |
849
+ | 9e | **Landed.** ETC1S to ETC2, byte-exact against the reference; EAC alpha fitted. `ktx2_read` takes an ordered target list so alpha can decide. **ASTC written and removed** — the layout was derived but the interpolation could not be verified and no ASTC device was reachable (§9.16) | additive; the target-list change also fixes a live BC1 hazard |
850
+ | 9f | **Landed.** ETC1S to ASTC 4x4, from the specification: exact interpolation, the CEM 8 endpoint ordering rule, placement search and endpoint refinement. **48.81 dB against the reference's 48.28** (§9.17), and §9.16's removal is retracted | additive; ASTC leads every role's preference where the device has it |
851
+ | 9g | **Landed.** The ordered target list falls through on *encoding* as well as alpha — an ASTC-first device went from 24 failures on Sponza to none. **UASTC to ASTC measured and declined**: 34.9 dB on real normal maps against an exact floor (§9.18) | bug fix; the decline leaves UASTC on the floor where it already was |
852
+ | 9h | **Landed.** UASTC to ASTC by repack — partition seeds searched, trit/quint packings inverted from the spec's decode, block modes assembled from the layout. Byte-identical to the reference on 2320 of 2352 blocks and provably equivalent on the rest. **Mobile reaches desktop parity: 173.1 -> 83.8 MiB** (§9.19) | additive; closes the last gap for third-party UASTC content |
853
+ | 10 | GPU transcoder (§4.4) — Basis payload to BC on the device | an optimization of step 9, not a prerequisite |
854
+ | 11 | **Landed, in part.** GPU block encoder (§4.5) — any sampleable texture to a named block format, on the device: the framework, the padded layout, the per-level copies, the format registry and the test harness, carried by `bc4-r-unorm`, then `bc5-rg-unorm`, `bc1-rgba-unorm`, `bc7-rgba-unorm` in mode 6, the two signed formats, `bc6h-rgb-ufloat` in mode 11 and `astc-4x4-unorm` in the one-partition block of step 9f; the engine's first real `copyBufferToTexture`. Found `texture_write_level` refusing every BC chain's tail on hardware since step 3, and fixed it (§9.20) | additive; `astc-6x6` and `astc-8x8` are refused by name until each lands |
855
+
856
+ Steps 1–6 introduce **no new capability and no new branch** — they are the refactor that makes
857
+ everything after them a data change rather than a second code path. If the project stops after step
858
+ 6, the engine is strictly better than it is now (correct memory accounting, one format table,
859
+ declared alpha, level-at-a-time upload, mips from data) and nothing has been added that must be
860
+ maintained for a feature that did not land.
861
+
862
+ ### 9.1 What step 0 actually shipped
863
+
864
+ Two class fields read `GPUTextureUsage` at module load, not one:
865
+ [`TextureDescriptor.js:61`](../../descriptor/texture/TextureDescriptor.js:61) and
866
+ [`TextureResourceDescriptor.js:56`](../../device/graph/TextureResourceDescriptor.js:56). Both now
867
+ import the shim. Everything else in the tree that touches the namespace — 139 sites — reads it inside
868
+ a function body or a default parameter, which is call-time and only reachable from a node tool that
869
+ gets that far; those stay on the global for now and convert with the JSDoc sweep.
870
+
871
+ [`TextureFormat`](../../descriptor/texture/format/TextureFormat.js) is generated from the key set of
872
+ `FORMATS`, so the two cannot start out disagreeing. 101 formats, and the key transform is mechanical
873
+ for every one of them — no exceptions to hand-tune, which is what made the reversibility test worth
874
+ writing.
875
+
876
+ The node-side test was checked against its own absence: with the shim import removed from
877
+ `TextureDescriptor`, it fails with `ReferenceError: GPUTextureUsage is not defined`, which is the
878
+ regression it exists to catch. A test that passes with and without the fix would have been the §11
879
+ failure mode applied to the test suite.
880
+
881
+ ### 9.3 What step 3 actually shipped
882
+
883
+ **`texture_write_level(queue, texture, level, bytes)`** — and nothing else. §3.4 sketched it taking
884
+ `format` and `size` too; both come off the texture, which is the authority on all three because it is
885
+ the thing being written into, and passing them would be one more pair of things that can disagree.
886
+
887
+ **No level range, and no optional parameter.** §11.4 asked for one so the uploader could honour
888
+ §3.2's "generated mips replace supplied ones". Two named functions answer that better than one
889
+ function with an optional argument: `texture_write_to_gpu` writes the whole image,
890
+ `texture_write_level` writes one level, and step 4 calls whichever the flag calls for. The decision
891
+ belongs where the flag is, which is `TextureManager`, not threaded through the uploader as a
892
+ parameter it would have to interpret.
893
+
894
+ The row stride the raw path used to compute for itself is gone — it comes from the format now, which
895
+ also fixes the padded case honestly: after RGB→RGBA padding the stride is the texture's four
896
+ channels because the *texture's format* says four, rather than because the padding code remembered to
897
+ recompute it.
898
+
899
+ **One test fixture changed, no assertions.** `texture_write_to_gpu.spec.js`'s stand-in `GPUTexture`
900
+ had no `mipLevelCount` or `dimension`; every real one has both, and the primitive reads them. The
901
+ fake was wrong, not the test.
902
+
903
+ ### 9.20 The GPU block encoder, and what the first real `copyBufferToTexture` found
904
+
905
+ §4.5 said runtime compression was "the same shape as §4.4 with a different shader" and that
906
+ everything in §4.4's numbered list applied unchanged. It is, and it did — with one item of that list
907
+ wrong in a way only a device could show, and one older step wrong the same way.
908
+
909
+ **What landed.** [`texture_encode`](encode/texture_encode.js): a sampleable texture in, a
910
+ block-compressed texture out, the target named by the caller and nothing else known — not the role,
911
+ not the preference list, not the material. One compute dispatch over every level of the chain, one
912
+ `copyBufferToTexture` per level, the block buffer laid out with 256-byte rows by
913
+ [`texture_encode_layout`](encode/texture_encode_layout.js) before anything runs. The shader carries no
914
+ immediates and no uniforms: a flat level table in a storage buffer is the whole of its parameters,
915
+ which is what lets one dispatch cover a chain and what lets it run on a browser build with no
916
+ immediate data. Fifteen targets are listed; `bc4-r-unorm` is implemented and the other fourteen are
917
+ refused by name, each as "planned, not implemented" rather than approximated by a neighbour.
918
+
919
+ **The encoder** is a fixed budget: extremes, two rounds of least squares, then a five-by-five search
920
+ of integer endpoint pairs at spacings of eight, two and one, in both of BC4's palette modes with the
921
+ six-value line seeded over the interior texels alone. Measured in the emulator against an exhaustive
922
+ search over every endpoint pair, it is within 0.5 dB on every fixture; the least-squares answer alone
923
+ was 3 dB short on the mask fixture, because the optimum sits in a basin no rounding of the
924
+ least-squares line reaches. The reading behind those choices, source by source, is in
925
+ [`encode/ENCODE_NOTES_2026_09_02.md`](encode/ENCODE_NOTES_2026_09_02.md).
926
+
927
+ **§4.4's item 2 was wrong.** "Origin and extent must be block multiples — trivially satisfied for a
928
+ whole-level copy, since the physical extent of a mip is its size rounded up to whole blocks." The
929
+ first half is right and the conclusion did not follow from it: a whole-level copy at the level's
930
+ *logical* size is not a block multiple for the 2×2 and 1×1 tail, and Dawn refuses it —
931
+ `copySize.width (2) is not a multiple of compressed texture format block width (4)`. The copy is
932
+ sized in whole blocks against the physical mip size, which
933
+ [`gpu_texture_format_physical_size`](../../descriptor/texture/format/gpu_texture_format_info.js) now
934
+ states in one place. The software device had implemented Vulkan's allowance instead — a partial
935
+ extent is fine where it reaches the edge — and so could not have caught it; it now implements
936
+ WebGPU's rule, refuses a compressed texture whose base size is not whole blocks, requires the
937
+ compression feature a block format needs, and validates a copy's row stride, offset and view formats.
938
+
939
+ **Which means step 3 had been failing on hardware all along.** `texture_write_level` wrote every
940
+ level at its logical extent, so `writeTexture` of a BC chain's last two levels was refused on every
941
+ device — silently, since a validation error does not throw, and §9.13's "69 of 69 accepted" counted
942
+ exceptions. Its spec asserted `[1, 1, 1]` for the 1×1 mip of a BC7 texture and passed. Fixed: the
943
+ extent is the physical size, the spec asserts `[4, 4, 1]`, and the stricter software device would
944
+ fail the old code.
945
+
946
+ **What the device said about the picture.** 2048² with twelve levels, 349,527 blocks, in 8.6 ms
947
+ wall from record to done; every level's blocks read back from the texture identical to the buffer;
948
+ the device choosing the same block as the f64 emulator on 83% of blocks and a block of equal error on
949
+ almost all the rest. And the hardware's BC4 decoder, read at full precision over every endpoint pair,
950
+ is not the specification's arithmetic: a palette entry is `A + (B − A) · c / 257` over 16-bit
951
+ endpoints with `c = 36, 72, 113, 144, 185, 221` for the eight-value mode and `48, 96, 161, 209` for
952
+ the six — within a level of the CPU decoder on 85% of entries and up to seven off on the widest
953
+ six-value spans, which on real content is 0.6 dB between what the specification decoder predicts and
954
+ what the GPU shows. The encoder keeps fitting against the specification's palette: a vendor's weights
955
+ would be wrong on the next vendor.
956
+
957
+ **Then BC5 and BC1.** BC5 is the BC4 chunk twice, red then green, and the kernel spec holds it to
958
+ exactly that: each level's blocks must equal the BC4 encoder over the red channel followed by the
959
+ BC4 encoder over the green, block for block. BC1 is the RGB line fit BC7 builds on — principal axis
960
+ by power iteration from the box diagonal, least squares with the thirds shared across channels,
961
+ 5:6:5 quantisation and coordinate descent against the decoder's truncated palette, with a flat
962
+ block's stored pair searched per channel for the one-third mix nearest its colour. Measured in the
963
+ emulator: 40.3 dB on a correlated colour fixture and 30.6 on three unrelated channels, against 25.3
964
+ and 24.3 for the 2007 inset bounding box; BC5 38.0 dB on a rough heightfield's normal map. On the
965
+ device: BC1 41.5 dB on the correlated source with the hardware decode within 0.02 dB of the CPU
966
+ decoder's, BC5 45.2 dB, both texture-equals-buffer on every level, and the device choosing the
967
+ emulator's BC1 block on 16,380 of 16,384. One thing the emulator taught: it aliases a vector on
968
+ assignment, so writing one component of a copy changed the original and broke both BC1 searches
969
+ silently; the chunk now builds the altered vector through `select`, which means the same on both.
970
+
971
+ **Then BC7, mode 6.** The same fit in four dimensions with sixteen points on the line and
972
+ eight-bit ends: the parity bit each end carries is chosen by rounding to all four combinations and
973
+ scoring each against the decoder's own palette, and the anchor is honoured by writing a block the
974
+ other way up when texel 0 lands in the upper half. The pack is held byte for byte against
975
+ `bc7_write_block`, the writer the transcoders use. 44.9 dB on correlated colour in the emulator,
976
+ 3.6 dB over BC1 on the channels they share; and on the device the cleanest result of the day — the
977
+ hardware decode identical to the CPU decoder on every sample of every level, because BC7's
978
+ interpolation is fully specified where BC4's is not. 46.0 dB at 512², 8.2 ms for a 2048² chain.
979
+ Mode 6 alone; the partitioned modes are where the rest of BC7's quality is, and a mode search is a
980
+ separate chunk for a later day.
981
+
982
+ **Then the signed formats**, which the brief called a path and not a tag, and which turned out to
983
+ be a domain: the BC4 fit takes its bounds as arguments and the signed entry point calls it over
984
+ `[-127, 127]` with the six-value mode's hard entries at the extremes, the pack taking the two's
985
+ complement. Two shaders, no new fitting code. On the device, a normal map as signed bytes encodes
986
+ to `bc5-rg-snorm` at exactly the unsigned figure less the 6.02 dB a halved peak costs, which is
987
+ what "the same fit" has to come out as.
988
+
989
+ **Then BC6H, mode 11.** The same line fitted in the half-float bit domain, because that is where
990
+ the format's decoder interpolates: an endpoint is a half's bit pattern quantised to ten bits, and
991
+ the palette is linear in bit patterns — a log-like scale, so the error the fit minimises is a
992
+ relative one. 45.1 dB over half bits and 0.078 stops RMS log error on an HDR fixture spanning
993
+ fourteen stops with a sun; on the device the hardware decode identical to the CPU decoder on every
994
+ sample of every level, which the format requires of a decoder and this was the first chance to see.
995
+ Mode 11 alone: a block holding the sun and its surround wants the two-region modes.
996
+
997
+ **Then ASTC 4×4**, in the one corner of the format §9.17 derived and §9.19 repacked into: block
998
+ mode `0x53`, one partition, colour endpoint mode 8, eight-bit ends and three-bit weights — the BC1
999
+ line under ASTC's own interpolation and its endpoint-order rule, both of which cost a level if
1000
+ ignored and both of which those steps already had in JavaScript. 43.3 dB on correlated colour in
1001
+ the emulator, between BC1 and BC7 mode 6 as eight levels over eight-bit ends should be. Not seen on
1002
+ hardware: nothing in reach samples ASTC, and the section says so rather than implying otherwise.
1003
+ The 6×6 and 8×8 footprints wait on block modes and partition seeds the tree does not hold.
1004
+
1005
+ **Testing, and the discipline §9.11 set.** The block layout is written twice, in JavaScript under
1006
+ `format/texture/bc/` — which the UASTC→BC5 and ETC1S→BC1 transcoders now pack through — and in
1007
+ WGSL, and the emulator specs require the same bytes from both five hundred times over. The kernel's per-level
1008
+ output must equal the encoder run directly over each level's image, byte for byte. Quality floors are
1009
+ in the spec, from measurement: 51.5, 42 and 54 dB on the three fixtures, and the fit must strictly
1010
+ beat the bounding line it starts from, which is the guard against the refinement's absence. On the
1011
+ software device the copies execute over host-written bytes, so the padded layout landing as tight
1012
+ rows in the right mip is asserted without a shader. Full suite: 15,583 passed.
1013
+
1014
+ ### 9.19 UASTC to ASTC, repacked — mobile reaches parity
1015
+
1016
+ §9.18 declined a *re-encode* at 34.9 dB and costed the repack. This is the repack, and it lands where
1017
+ the format intended: **mobile now costs exactly what desktop costs**.
1018
+
1019
+ | Sponza | before | after |
1020
+ |---|---|---|
1021
+ | desktop (BC) | 83.8 MiB | 83.8 MiB |
1022
+ | mobile (ASTC + ETC2, no BC) | 173.1 MiB | **83.8 MiB** |
1023
+
1024
+ The 24 UASTC normal maps that were falling to the uncompressed floor are now `astc-4x4-unorm`, and
1025
+ the scene is 66 ASTC plus 3 ETC2 for the textures with alpha.
1026
+
1027
+ **Nothing is fitted and nothing is lost.** UASTC's fields *are* ASTC's fields — endpoints already
1028
+ quantized at a range ASTC names, weights already ASTC weights, patterns taken from ASTC's own hash.
1029
+ The work is arithmetic on bit positions: ASTC interleaves its integer sequences where UASTC keeps
1030
+ them in two plain runs, and puts its weights at the top of the block in reverse.
1031
+
1032
+ **Verified by byte equality with the reference transcoder, which a re-encode could never be.**
1033
+
1034
+ | fixture | identical | equivalent | different |
1035
+ |---|---|---|---|
1036
+ | `uastc_gradient_64x64` | 256 | 0 | 0 |
1037
+ | `uastc_partitions_32x24` | 48 | 0 | 0 |
1038
+ | `uastc_subsets_128x128` | 1016 | 8 | 0 |
1039
+ | `uastc_zoo_128x128` | 1000 | 24 | 0 |
1040
+
1041
+ "Equivalent" is not a hedge. ASTC's integer sequence encoding is *redundant* — 256 packed trit groups
1042
+ carry 243 tuples and 128 quint groups carry 125 — so two correct encoders can differ bit for bit. The
1043
+ 32 such blocks are required to decode to identical endpoint values, and do; the spec reads both
1044
+ sequences back rather than accepting the difference.
1045
+
1046
+ **Three things were derived rather than copied**, and each is checked by its own failure mode:
1047
+
1048
+ - **The partition seeds.** ASTC computes patterns from a ten-bit seed through a hash rather than
1049
+ storing them, so UASTC's pattern indices have to be matched to seeds. The reference has that table;
1050
+ this searches all 1024 seeds for one whose grouping agrees texel for texel. All 60 patterns across
1051
+ the three tables resolve — which a wrong hash could not do — and dropping the small-block coordinate
1052
+ doubling fails five tests.
1053
+ - **The trit and quint packings.** The specification gives the decode only, saying outright that
1054
+ encoding is the implementation's business. So the decode is written and inverted by enumeration:
1055
+ 256 groups yield exactly 243 distinct trit tuples and 128 yield exactly 125 quint tuples, which is
1056
+ the check that the decode is right.
1057
+ - **The block modes.** Assembled from the layout — `D` at bit 10, `H` at 9, height at 8:7, width at
1058
+ 6:5, `R0` at 4, `R2` and `R1` at 1:0 — rather than tabulated. Swapping `R1` and `R2` fails four
1059
+ tests.
1060
+
1061
+ **Two rules with no flag, both caught by the byte comparison.** Endpoint modes 8 and 12 swap their
1062
+ pair and blue-contract when the second endpoint's channel sum falls below the first's, and UASTC
1063
+ orders its endpoints for BC7's rules instead — so a subset that would trip it has its pair exchanged
1064
+ and its weights inverted, which is the same block the other way up. And a void-extent block's bits 10
1065
+ and 11 are reserved and set; leaving them clear produced solid blocks that differed from the
1066
+ reference in nothing else.
1067
+
1068
+ **One thing that cannot be tested and is kept anyway.** A seed found by search need only *group* the
1069
+ texels as UASTC does; which group it calls subset 0 is the hash's business, and an encoder would in
1070
+ general have to reorder its endpoint pairs. No UASTC pattern actually needs it — all 60 come back as
1071
+ the identity, which follows from UASTC having taken its patterns from ASTC. Rather than carry
1072
+ reordering code that never runs and therefore is never right, the identity is asserted and a
1073
+ violation refused. Removing that assertion fails nothing, and the file says so.
1074
+
1075
+ **Still not written:** ETC2 from UASTC. An ETC2-capable device without ASTC would take the floor for
1076
+ UASTC content — a population the availability figures suggest is close to empty, since the two
1077
+ features report 29% and 30% with identical per-browser splits.
1078
+
1079
+ #### What the adversarial pass over §9.19 found
1080
+
1081
+ **Five of nineteen modes were never decoded by any fixture** — 5, 10, 15, 16 and 18. Everything the
1082
+ repack claims is per-mode, so those were untested bit layouts that happened to compile. Mode 18 was
1083
+ the one that mattered: the only user of the 32-level weight range *and* the 32-level endpoint range,
1084
+ and the tightest budget of any mode at 127 bits of 128.
1085
+
1086
+ Three fixtures were built to reach them, by giving `basisu` content shaped for the modes rather than
1087
+ for the eye: a luminance ramp with noise (15), a high-precision colour and alpha field (5, 10), and a
1088
+ single long smooth ramp at encoder level 4 (18). Coverage is now 18 of 19, and misreading mode 18's
1089
+ weight range now fails a test where it used to pass. **Mode 16 — two-subset luminance — remains
1090
+ unreached**: pure greyscale with a hard split inside every block, at every encoder level, produced
1091
+ single-subset modes every time. That is asserted as a known gap rather than left implicit, so a
1092
+ future fixture that happens to contain it will fail and shorten the list.
1093
+
1094
+ **A bit-budget scare that turned out to be sound.** Counting header, endpoints and weights suggested
1095
+ five modes overrun 128 bits — and four of those are in the fixtures and match the reference byte for
1096
+ byte. The size arithmetic was wrong, not the encoder: ASTC charges a partial trailing group only the
1097
+ bits it needs, where the naive count charges a whole one. The writer pads such a group with zeros and
1098
+ formally advances its cursor past where the weights begin, which raised the real question of whether
1099
+ a padded group's high bits could ever land in the weight field. Swept exhaustively over every mode
1100
+ and every value of every trailing group: they cannot, because zero digits give zero high bits. The
1101
+ unused size helper that started the scare is gone, with a note where it stood.
1102
+
1103
+ **Two pieces of dead logic removed.** The seed-table choice carried a `pat_bits === 4` branch and a
1104
+ repeated `subsets === 3` test, neither of which can be true where they were tested — no two-subset
1105
+ mode has four pattern bits. And the literal `7` selecting mode 7's borrowed pattern table is now the
1106
+ named constant the decoder already had.
1107
+
1108
+ Checked and found sound: `partition` is a full sixteen-entry array of zeros for single-subset blocks,
1109
+ so the per-subset weight inversion does reach them.
1110
+
1111
+
1112
+ ### 9.18 The list did not fall through, and UASTC to ASTC does not pay
1113
+
1114
+ Two findings from taking every Sponza texture through an ASTC-first preference, which is what a
1115
+ modern phone would ask for.
1116
+
1117
+ **A bug §9.16's list resolution introduced.** Handing `ktx2_read` an ordered list was meant to stop a
1118
+ target being committed to and then refused. It only ever filtered on *alpha*, and returned the head
1119
+ of the list unchanged for UASTC — so a list headed by `astc-4x4-unorm` threw on all 24 of Sponza's
1120
+ normal maps, because ASTC is a format ETC1S reaches and UASTC does not. The comment even said so:
1121
+ "UASTC carries its alpha in the block ... so its first choice always stands". It does not stand when
1122
+ the first choice is a format that encoding cannot write at all.
1123
+
1124
+ The resolution now filters on both, against the writing transcoder's own format list. The ASTC-first
1125
+ run goes from 24 failures to none: **42 ASTC, 3 ETC2 for the alpha textures, 24 falling to the
1126
+ uncompressed floor**.
1127
+
1128
+ **Why those 24 stay on the floor: UASTC to ASTC was written, measured, and not shipped.** Not for the
1129
+ reasons §9.16 gave — this time the measurement is sound and it says the approach is wrong.
1130
+
1131
+ Decoding a UASTC block to texels and fitting one ASTC partition to them gives, over Sponza's 24
1132
+ normal maps, **34.9 dB mean with a worst channel error of 140**. That is not a fitting failure. The
1133
+ adversarial block is legible: a 4x4 where red and blue vary along x while green varies along y — two
1134
+ independent gradients, which a single pair of endpoints and one weight plane cannot represent at any
1135
+ precision. Normal maps are made of exactly that, which is why `texture_format_preference` leads with
1136
+ `bc5-rg-unorm` for the role and why UASTC itself spends dual-plane and multi-subset modes on 46% of
1137
+ the blocks in the zoo fixture.
1138
+
1139
+ So the trade on offer was a quarter of the memory for a visibly wrong normal, against an uncompressed
1140
+ floor that is exact. Taken on those terms it is a regression, and the floor wins.
1141
+
1142
+ **What the real path costs.** UASTC's modes were laid out to map onto ASTC's, endpoints and weights
1143
+ already in ASTC's quantized space — a repack that loses nothing. Reaching it needs three things this
1144
+ tree does not have: the full block-mode table rather than the two single-plane 4x4 rows in
1145
+ `astc_block.js`, integer sequence encoding for endpoints and weights at trit and quint ranges, and
1146
+ ASTC's partition-seed function. Dual plane alone would fix the block above, and does not fit at eight
1147
+ bits an endpoint: 64 weight bits and two selector bits leave 45, so it needs the endpoint
1148
+ quantization too. That is the next piece of work, and it is a real one.
1149
+
1150
+ **What did land:** `astc_block.js`, holding the block mode derivation, the weight unquantization, the
1151
+ mode 8 endpoint ordering rule and the writer, shared by whatever encodes into it.
1152
+
1153
+ ### 9.17 ASTC, finished — and what §9.16 got wrong
1154
+
1155
+ §9.16 pulled ASTC on the grounds that its decode could not be verified. **That conclusion was
1156
+ wrong, and wrong in a way worth recording.**
1157
+
1158
+ The measurement it rested on was this: decoding `basisu`'s own ASTC blocks and comparing against the
1159
+ ETC1S texels gave 748 exact, 1938 off by one, and a tail to five. That was read as *this decoder is
1160
+ inaccurate*. It is not what the number says. Those are the reference encoder's blocks and its
1161
+ endpoint choices, so the gap is **`basisu`'s ASTC encoding loss against the ETC1S source** — the
1162
+ lossy step everyone expects, attributed to the wrong side. The right control was never run: compare
1163
+ the two decode formulas against *each other* on the same blocks. They disagree on 160 samples of
1164
+ 2928, all by one level. That is what a wrong interpolation model looks like, and five is not it.
1165
+
1166
+ With the specification in hand the remaining pieces were small.
1167
+
1168
+ **The interpolation is not the direct form.** ASTC widens each 8-bit endpoint to sixteen bits by
1169
+ replication, interpolates there with a `+32` round, and takes the top eight bits —
1170
+ `((c << 8 | c) * (64 - w) + ... + 32) / 64 >> 8`. Against `c0 + (c1 - c0) * w / 64` that is one texel
1171
+ in twenty, always by a level. Both the encoder's scoring and the test decoder now use the real one;
1172
+ fitting against one and decoding with the other would have put that disagreement into the output for
1173
+ nothing.
1174
+
1175
+ **Colour endpoint mode 8 has a rule about which way round the endpoints go.** With the second
1176
+ endpoint's channel sum below the first's, a decoder *swaps them and blue-contracts both* —
1177
+ `r = (r + b) >> 1`, `g = (g + b) >> 1`. That is a different picture rather than an inverted one, and
1178
+ no bit in the block records that it happened. §9.16 suspected this from the evidence that `basisu`
1179
+ never emits such a block in 61 tries, and was right to. `order_endpoints` now guarantees it, swapping
1180
+ the pair and inverting the weight levels, which leaves the decoded block identical where it does
1181
+ nothing.
1182
+
1183
+ **The block mode was already right.** `0x42` is the low-precision row `D H 0 B A R0 0 0 R2 R1` with
1184
+ `A = B = 2` for a 4x4 grid and `R = 4` for four weight levels on one plane — derived empirically in
1185
+ §9.16, confirmed against the layout table here.
1186
+
1187
+ **Quality, measured against the reference on both axes.** Over the 61 blocks `basisu` also writes
1188
+ single-plane it reaches 48.28 dB with a worst channel error of 4. This reaches **48.81 dB over all
1189
+ 64**, with a worst of 5. Better on the whole and marginally worse at the peak, and the reason is
1190
+ visible in one block: block 12, where both encoders pick identical weights and the endpoints differ
1191
+ by two in one channel. This pair has the lower summed squared error and the reference's has the lower
1192
+ maximum. Least squares is what the fit optimises and what PSNR rewards, so the trade is deliberate.
1193
+ `etc1s_antidiag_64x64` comes back exact.
1194
+
1195
+ Two things earn that: the placement search from §9.16 — spreading a block's *used* selectors over all
1196
+ four weight levels rather than mapping them one to one, worth nine decibels — and a bounded
1197
+ refinement of the winning placement's endpoints, since the rounded least-squares pair is usually but
1198
+ not always the best integer pair once `interpolate`'s own rounding is applied.
1199
+
1200
+ **Still not written:** ETC2 and ASTC from UASTC. A UASTC normal map on a device with neither BC nor
1201
+ ASTC still falls to the uncompressed floor.
1202
+
1203
+ **The lesson, which is §9.11's again.** A number that looks like evidence of a bug in your own code
1204
+ is worth attributing before acting on it. The control that would have settled it — the two formulas
1205
+ against each other — costs nothing and was not run, and the cost of not running it was deleting a
1206
+ working encoder.
1207
+
1208
+ ### 9.16 ETC2 landed; ASTC was written, could not be verified, and was removed
1209
+
1210
+ Asked for both. One shipped.
1211
+
1212
+ **ETC2 is a repack, and is checked byte for byte.** ETC1S is a constrained ETC1 and ETC1 is a subset
1213
+ of ETC2, so a Basis block is ETC1's differential mode with both subblocks given the same colour:
1214
+ deltas zero, both table codewords the block's intensity. Nothing is fitted and nothing is lost. All
1215
+ 64 blocks of the noise fixture come out identical to `basisu -unpack`'s own ETC1 output, and the
1216
+ RGBA8 spelling matches its ETC2_RGBA byte for byte too. Eight bits a texel against the uncompressed
1217
+ floor's thirty-two, at no cost in quality at all.
1218
+
1219
+ Two things that were not free:
1220
+
1221
+ - **The selector remap.** `INTENSITY_MODIFIER` is ordered darkest to brightest and ETC1 indexes the
1222
+ same four modifiers as `+a, +b, -a, -b`, so a selector written through unchanged mirrors every
1223
+ block through its own mid-grey. `[3, 2, 0, 1]`.
1224
+ - **EAC's alpha block**, the only encoded part. A payload with no alpha slice is written exactly
1225
+ opaque through table row 13, the one row containing a zero modifier — an opaque texture has to come
1226
+ back 255 and not 254, and `basisu` uses the same trick, which is why those blocks match bit for bit.
1227
+ A real alpha slice is fitted by searching all sixteen rows against sixteen multipliers, landing at
1228
+ 54.4 dB with a worst error of 1 against the payload's own alpha.
1229
+
1230
+ **The bug the opaque fixture hid.** EAC numbers its texels down each column like ETC1, and the first
1231
+ version wrote them in raster order. A constant block takes the exact path and never touches the
1232
+ fitter, so every opaque fixture agreed with the reference perfectly while every non-constant block was
1233
+ transposed — worth ten levels. The mutation pass caught it a second time after the fix, because the
1234
+ only ETC2 RGBA test at that point still used an opaque fixture; a test over the alpha fixture is what
1235
+ now holds it.
1236
+
1237
+ **ASTC: written, measured, and taken back out.** The block layout was derived empirically from the
1238
+ reference transcoder rather than from the specification's mode tables — block mode `0x42`, one
1239
+ partition, colour endpoint mode 8, six eight-bit endpoints at bit 17, thirty-two weight bits at the
1240
+ top of the block with the field's bit order mirrored. That much is real: decoding `basisu`'s own
1241
+ blocks with it reproduces the ETC1S texels. An encoder on top of it reached 47.6 dB with a
1242
+ placement search over which weight levels a block's used selectors take.
1243
+
1244
+ It was removed anyway, for two reasons that together make it unverifiable here:
1245
+
1246
+ 1. **The interpolation model is wrong by up to five levels.** Reading the reference's blocks back
1247
+ gives 748 texels exact, 1938 off by one, and a tail to five. Whatever hardware does with the
1248
+ endpoints — expansions and shifts at some width — is not the weighted average this computes, and
1249
+ the encoder was fitting against the same approximation, so the error on a real device is unknown
1250
+ rather than bounded.
1251
+ 2. **CEM 8 appears to carry an endpoint-swap rule this does not implement.** Across 61 reference
1252
+ blocks, `basisu` never once emits endpoints whose first sum exceeds the second — exactly what a
1253
+ swap-and-blue-contract rule would punish. This encoder could emit such a block, and the result
1254
+ would be wrong colours rather than slightly wrong ones.
1255
+
1256
+ No ASTC hardware was reachable to test on: the device here reports fourteen compressed formats, all
1257
+ BC. Shipping a colour path to every iOS device that could not be checked, on a format where the
1258
+ failure mode is "wrong colours, silently", is worse than not shipping it. What would unblock it is
1259
+ the ASTC specification's Table C.2.8, its endpoint unquantization, and the CEM 8 rule — or an ASTC
1260
+ decoder from real tooling to test against, which is the same standard every other transcoder here was
1261
+ held to.
1262
+
1263
+ **A hazard ASTC surfaced, which was kept.** ASTC cannot hold an ETC1S payload's alpha — the colour
1264
+ and alpha slices have their own selectors, and the dual-plane block mode that would separate them
1265
+ cannot also hold eight endpoints and thirty-two weight bits in 128 bits. Being first in every colour
1266
+ role's preference, it would have been committed to and then refused, after the point where another
1267
+ format could still have been chosen. The same hazard was already live for BC1.
1268
+
1269
+ The fix outlives ASTC: `ktx2_read` now accepts an **ordered list** of acceptable targets and picks
1270
+ from it, because the container's descriptor is the only thing that knows whether the file carries
1271
+ alpha, and the caller is the only thing that knows what the device can sample. The loader hands over
1272
+ its whole preference order ending in the uncompressed floor. `transcode_basis_carries_alpha` is what
1273
+ the reader asks.
1274
+
1275
+ **Still not written:** ASTC, and both ETC2 and ASTC from UASTC. A UASTC normal map on a device with
1276
+ neither BC nor a verified ASTC path falls to the uncompressed floor, which costs memory rather than
1277
+ correctness.
1278
+
1279
+ ### 9.15 The uncompressed target, and the end of the "no BC" refusal
1280
+
1281
+ §9.13 recorded a gap: a device advertising no compressed format got a named refusal rather than a
1282
+ picture. That is now closed. `transcode_basis` and `transcode_uastc` both write `rgba8unorm`, and the
1283
+ glTF loader falls to it when the device can sample none of the compressed targets.
1284
+
1285
+ **Why it had to exist.** Every compressed format in WebGPU is an optional feature —
1286
+ `texture-compression-bc`, `-etc2` and `-astc` alike — so a device exposing none of them is legal, and
1287
+ so is one exposing only the family this transcoder does not write. `rgba8unorm` is mandatory. Four
1288
+ bytes a texel against BC7's one is a poor outcome and no texture at all is a worse one.
1289
+
1290
+ Measured on the same scene, forcing the support set empty: 69 textures at **335.3 MiB**, which is
1291
+ exactly what the JPEG path costs, split `rgba8unorm-srgb` for the 25 albedo maps and `rgba8unorm` for
1292
+ the other 44 — the same sRGB split §9.14 fixed, arrived at through a different branch. It also loads
1293
+ in 11.4 s against the compressed path's ~19 s, because the BC re-encode is the expensive half and
1294
+ this skips it.
1295
+
1296
+ **The ETC1S path is byte-exact and the specs say so.** Every other target re-encodes the decoded
1297
+ texels and is therefore compared against a PSNR bound; `rgba8unorm` *is* the decode and nothing more,
1298
+ so it must equal the reference transcoder's own texels exactly. It does, across four fixtures and
1299
+ seven mip levels. That makes it the strictest check in the file, and it is only possible because
1300
+ there is no lossy step to hide behind. UASTC is bounded instead, against the reference's BC7, since
1301
+ the reference publishes no uncompressed output to compare with — what that bound really tests is
1302
+ arrangement rather than fidelity, because a row-order or crop error moves every texel at once.
1303
+
1304
+ Against the source JPEGs the whole chain lands at 31 to 49 dB, which is the ETC1S encode's own loss.
1305
+
1306
+ **One thing the mutation pass found, worth writing down.** Removing either bounds check in the two
1307
+ writers changes no output, and the tests were right not to fail. The index is row-major, so a write
1308
+ past the right edge of row `y` lands at the start of row `y + 1`, which is written afterwards and
1309
+ overwrites it; the only spill nothing corrects runs past the end of the buffer, where a typed array
1310
+ drops it silently. The guards are therefore unobservable — and they stay, because they are
1311
+ unobservable only while the iteration order holds, which is a coincidence and not a contract. Both
1312
+ files now say so, so that nobody removes them on the evidence of a green suite.
1313
+
1314
+ **Still not implemented:** ETC2 and ASTC. An ETC1S block already *is* an ETC1 block and UASTC was
1315
+ designed to transcode to ASTC nearly for free, so both remain much cheaper than the BC7 path already
1316
+ written, and both would beat four bytes a texel on the mobile devices that expose them. The
1317
+ difference is that failing to write them is now a memory cost rather than a refusal.
1318
+
1319
+ ### 9.14 The albedo maps were never decoded as sRGB
1320
+
1321
+ Reported by eye — "they feel colder than the JPEGs" — and true. All 25 of Sponza's albedo textures
1322
+ were created as `bc7-rgba-unorm` while the same model's JPEGs reached `rgba8unorm-srgb`, so the GPU
1323
+ skipped the sRGB-to-linear decode and the shader read sRGB-encoded values as though they were linear.
1324
+
1325
+ Measured over one 1024x1024 albedo map, comparing what the shader saw against what it should have:
1326
+
1327
+ | | correct | as sampled | change |
1328
+ |---|---|---|---|
1329
+ | mean saturation | 0.2712 | 0.1488 | **-45.1%** |
1330
+ | mean luminance | 0.1316 | 0.3575 | **+171.7%** |
1331
+ | warmth (R-B) relative to luminance | 0.242 | 0.116 | **-52.4%** |
1332
+
1333
+ Both halves of the report fall out of that. The decode is a power curve, so it *stretches* the gap
1334
+ between channels; skipping it compresses them, which is the lost saturation. And warm surfaces have
1335
+ the widest R-to-B gap, so they lose the most — absolute R-B rises with everything else, but as a
1336
+ fraction of brightness it halves, which is what reads as cold.
1337
+
1338
+ **The cause was a pinned decision, not an oversight.** `texture_format_from_shade_image` returned a
1339
+ texel payload's format verbatim, and a test asserted it: *"a texel payload states its own format,
1340
+ whatever the colour space says"*, reasoning that "there is nothing to infer from, so nothing to be
1341
+ overridden by". That conflates two things. Channel count and data type are inference, and a payload
1342
+ does state them. sRGB-ness is not inference at all — `bc7-rgba-unorm` and `bc7-rgba-unorm-srgb` are
1343
+ the same bytes, so a transcoder targeting either does identical work and a payload *cannot* express
1344
+ the choice. Naming one leaves the question open rather than answering it.
1345
+
1346
+ The test immediately above it already states the correct rule, for the other branch: a format decided
1347
+ before `color_space` is assigned "would leave every glTF albedo map on `rgba8unorm` and silently drop
1348
+ its sRGB decode". The texel branch did exactly that. Both branches now read `color_space`, and a
1349
+ payload with no sRGB counterpart under an sRGB image is refused by name rather than quietly returning
1350
+ the linear format — the silent wrong answer being the whole failure mode here.
1351
+
1352
+ `gpu_texture_format_srgb` is new and derives its pairs from the format table by suffix rather than
1353
+ listing them, so a format added to the table brings its own pairing.
1354
+
1355
+ **Two notes for the next person.** VRAM is unchanged: sRGB-ness is a decode, not a size, so nothing
1356
+ about §9.13's 4.00x moves. And the playground reported the *payload's* format, which is why its own
1357
+ table showed `bc7-rgba-unorm` for albedo and looked right while the render did not; it now reports
1358
+ what `texture_format_from_shade_image` returns, so the two paths' albedo rows can be compared.
1359
+
1360
+ **Separately checked and not a problem:** whether ETC1S itself desaturates. Across the opaque JPEGs
1361
+ it is within about 3%, rising to 9% only on near-pure primaries where quantization lifts a
1362
+ near-zero channel by roughly 2/255. That is ordinary lossy behaviour and not what was visible.
1363
+
1364
+ ### 9.13 What tier 8 found — the first GPU to see any of this
1365
+
1366
+ Run 2026-09-02 on an NVIDIA Lovelace device, through
1367
+ [`playground/basis_textures`](../../playground/basis_textures/README.md): Sponza twice, once as JPEG
1368
+ and once re-encoded by `basisu` to KTX2 with UASTC normals and ETC1S everything else.
1369
+
1370
+ | | textures | GPU memory | formats |
1371
+ |---|---|---|---|
1372
+ | JPEG | 69 | 335.3 MiB | 69 x `rgba8` |
1373
+ | Basis | 69 | 83.8 MiB | 45 x `bc7-rgba-unorm`, 24 x `bc5-rg-unorm` |
1374
+
1375
+ **4.00x**, both sides counted through `TextureDescriptor.memory_footprint` and both including their
1376
+ full mip chain. All 69 textures were accepted by the device. The 24 BC5 textures are exactly the 24
1377
+ normal maps, which is §3b's role-driven choice arriving where it was aimed.
1378
+
1379
+ Two defects, and both are §9.11's lesson a third time — a rule that held everywhere it had been
1380
+ checked, and had never been checked here.
1381
+
1382
+ 1. **Mip *generation* was requested for formats that cannot generate mips.** `ShadeTexture` sets
1383
+ `GenerateMipMaps` by default, which is right for a decoded JPEG and impossible for a transcoded
1384
+ payload: the generator renders between levels and no BC format can be a render attachment.
1385
+ `texture_descriptor_from_texture_shade` refuses that combination by name (§9.7 made it refuse
1386
+ rather than reinterpret, which is why this surfaced as a legible message instead of a black
1387
+ texture) — but nothing cleared the flag, so **every compressed texture in the scene was refused at
1388
+ upload**. `load_gltf` now clears it when the image's format cannot generate them, and the chain
1389
+ the container already carries is used. Every device-free test had built its textures directly and
1390
+ never went through the glTF loader's default flags.
1391
+ 2. **`TextureManager.gpu_memory_usage` is a running total, and removing a scene evicts nothing.**
1392
+ Reading it after each load reported the sum of every load so far, which inflated whichever variant
1393
+ was measured second — the one being argued for. It read 5.00x before this was noticed. The
1394
+ playground measures the difference across its own uploads instead. Worth recording as a property
1395
+ of the manager rather than of the playground: there is no eviction API, and a long-running editor
1396
+ that swaps scenes will accumulate.
1397
+
1398
+ **Then a third, which the numbers could not have found — every texture was upside down.** The
1399
+ converter passed `-y_flip` to `basisu`. glTF puts UV (0,0) at the top-left, and a KTX2 with no
1400
+ `KTXorientation` key means exactly that, so `KHR_texture_basisu` requires a top-down payload;
1401
+ `basisu` writes no such key either way, so the flag bakes a vertical mirror into the pixels and
1402
+ records nothing a reader could detect or undo. Confirmed by encoding a top-white/bottom-black image
1403
+ both ways and transcoding each: with the flag the top row decodes black, without it white. Then on
1404
+ the re-converted model itself, comparing each texture's top-band luminance against its bottom-band in
1405
+ the browser: all six asymmetric enough to judge agree in sign with the JPEG they came from, one to 0.3
1406
+ out of 143. The bug was in the dev tool and not in the engine — the container reader and both
1407
+ transcoders preserve orientation, which the fixture comparisons against reference-decoded output
1408
+ already pin, and nothing on the upload path flips either (`createImageBitmap` is left at the default
1409
+ `from-image`, `copyExternalImageToTexture` sets no `flipY`), so the JPEG and KTX2 paths agree.
1410
+
1411
+ Worth separating from the two above: those were found by *running* it, and this one only by
1412
+ *looking* at it. The VRAM table was correct in every particular while the scene was upside down,
1413
+ which is the caveat below arriving in practice rather than in principle.
1414
+
1415
+ **Still not seen — and that turned out to matter.** The renderer needs `maxImmediateSize` >= 24 and
1416
+ assumes the 64-byte floor; the Chrome build available here reports 0, so the frame loop dies in
1417
+ `GPUTypedTable/Upload elements` for every playground in the directory. The measurements above are
1418
+ taken through `TextureManager.obtain` — the same call the renderer makes, and the same moment the
1419
+ device accepts a format or does not — so what is proven is that the device takes these textures.
1420
+ That the sampled result *looks* like the picture is established only on the CPU side, against the
1421
+ reference decoder, and the `-y_flip` defect is what that gap costs: an asset bug that every number
1422
+ on the page was blind to, and that a single glance caught.
1423
+
1424
+ **The gap tier 8 made concrete.** There is no uncompressed transcode target, so a device advertising
1425
+ no BC at all gets a named refusal. No real WebGPU device is in that position, but the two cheap
1426
+ fillers should be recorded: an ETC1S block already *is* an ETC1 block, and UASTC was designed to
1427
+ transcode to ASTC nearly for free. Both are far cheaper than the BC7 path already written.
1428
+
1429
+ ### 9.12 What step 9b actually shipped — UASTC, and the one thing it still cannot open
1430
+
1431
+ **UASTC is the encoding normal maps use, and that is why it stopped being optional.** ETC1S puts a
1432
+ block's four colours on a line through the grey axis, which is a good model of a photograph and a bad
1433
+ model of a direction — a normal map encoded as ETC1S bands visibly. `gltf-transform uastc` and
1434
+ `toktx --uastc` exist for that, and any pipeline that cares about normals emits it.
1435
+
1436
+ Nineteen modes, 128-bit blocks, varying in subset count, endpoint precision, weight precision and
1437
+ whether one channel is interpolated separately, with the endpoints trit- or quint-coded in most modes
1438
+ and the weights packed a bit short at each subset's anchor texel. Implemented from the UASTC LDR 4x4
1439
+ specification.
1440
+
1441
+ **The tables were transcribed mechanically, not typed.** Sixty partition patterns, sixty anchor
1442
+ tables, eleven dequantisation tables and nineteen mode descriptors were extracted from the
1443
+ specification's own listings into
1444
+ [`uastc_tables.js`](../../../format/texture/basis/uastc/uastc_tables.js). The only thing computed is each mode's
1445
+ field offsets, and those are checked against the offsets the specification prints for every mode.
1446
+
1447
+ #### What it transcodes to, and the one that matters
1448
+
1449
+ **BC5, which is the point.** §11.1 established that a tangent-space normal's Z is reconstructed in
1450
+ the shader, so `bc5-rg-unorm` spends its whole budget on the two channels that carry something —
1451
+ and `texture_format_preference` has led with it for the normal role since step 3b with nothing able
1452
+ to produce one. Two independent scalar channels means no line through colour space to place, so the
1453
+ encode is near-optimal: worst channel error of **1** on the gradient fixture.
1454
+
1455
+ **BC7, by carrying the block across rather than re-encoding it.** A UASTC block already *is* two
1456
+ endpoints and a weight per texel, which is what BC7 stores, so the transcode is a change of
1457
+ representation. Decoding to RGBA and fitting a fresh line instead — which is what this did first —
1458
+ costs **eighteen decibels**, and the measurement is worth keeping because the wrong version looked
1459
+ entirely reasonable. Dual plane blocks go to BC7 mode 5, whose second index set is exactly what their
1460
+ separately-interpolated channel needs; everything else single-subset goes to mode 6, whose four-bit
1461
+ weight table is ASTC's. Where those two cover every block the output matches the reference
1462
+ transcoder's own figures: **55.68 dB with a worst channel of 1**, against its 55.68 and 1.
1463
+
1464
+ #### The role finally reaches the format choice
1465
+
1466
+ §9.11 recorded that the texture's role did not enter the target choice and that this would start
1467
+ mattering "the moment ETC1S→BC5 lands". It landed as UASTC→BC5 instead, and the rest followed:
1468
+ [`gltf_image_roles`](../loader/gltf/gltf_image_roles.js) reads the materials to learn which image is
1469
+ the normal map, and the loader orders its candidates by `texture_format_preference(role, ...)`.
1470
+
1471
+ **Ordering, not filtering.** A role's preference lists what is *good*, not what is acceptable — BC1
1472
+ is in no role's list — so a device that can sample only BC1 still gets a texture. The role moves a
1473
+ format to the front and never removes one.
1474
+
1475
+ #### Partitioned blocks, and what closing that gap turned up
1476
+
1477
+ The first version of this flattened every two- and three-subset block onto one BC7 line, because BC7's
1478
+ partitioned modes were not written. That cost **more than twenty decibels** on partitioned content —
1479
+ the subsets fixture scored 25.8 dB then and 52.0 now — and it is closed: every UASTC mode goes to the
1480
+ BC7 mode the specification pairs it with, including mode 7's oddity of being two subsets in UASTC and
1481
+ three in BC7, written with two of BC7's endpoint pairs alike.
1482
+
1483
+ One writer serves all six modes, because the specification says the fields are in the same order for
1484
+ every mode and only their widths change. Against the reference transcoder, colour only: identical on
1485
+ two fixtures, +0.14 dB on a third, and 0.21 and 0.44 dB behind on the other two.
1486
+
1487
+ **Two defects surfaced while closing it, and both were about alpha nobody authored.** Mode 1 was
1488
+ mapped to BC7 mode 6 rather than mode 3 — and the specification's choice is not arbitrary: mode 3 is
1489
+ the only mode holding eight-bit endpoints *without* an alpha channel, and mode 6's alpha parity bit is
1490
+ shared with colour, so it must be traded against it. That trade was the second defect: choosing the
1491
+ parity by total error over all four channels let a constant alpha outvote colour, and on an RGB source
1492
+ that alpha is 255 by construction rather than by authorship. Forcing it kept alpha exact and cost
1493
+ **1.66 dB of colour**; excluding it from the vote is right, because §9.2 already settled that a
1494
+ texture's alpha mode is declared rather than inferred from its texels. The reference makes the same
1495
+ trade — its own output carries 181,026 texels at alpha 254 on the same opaque file.
1496
+
1497
+ #### The mutation harness was lying, which is the more useful finding
1498
+
1499
+ The checks ran through `sed`, and `sed` reports nothing when it matches nothing — so a mutation whose
1500
+ pattern had the wrong indentation silently tested the *unmodified* code and was recorded as caught.
1501
+ Three of them had. The harness now fails loudly on a no-op and every mutation claimed for this work
1502
+ was re-run through it.
1503
+
1504
+ Two real holes appeared once they ran for real, and both are the §9.11 lesson again — a guard checked
1505
+ against its own absence says nothing about code no fixture reaches:
1506
+
1507
+ - **BC7's anchor tables were untested.** The anchor is texel 15 for the first seventeen partitions and
1508
+ something else after that, and every fixture happened to land in the first seventeen — so the table
1509
+ could have been replaced by the constant 15. `uastc_partitions_32x24` reaches anchors 2 and 8: one
1510
+ block per partition pattern, each subset ramping in its own direction, because two flat colours are
1511
+ collinear and an encoder answers them with a single subset.
1512
+ - **The three-subset permutation was untested by any bound that could see it.** Only seven blocks use
1513
+ a non-identity one, which moves an average by under a decibel while moving the worst texel by a
1514
+ hundred. The transcode tests now carry a per-channel bound beside the PSNR one — the same division
1515
+ §9.11 settled for ETC1S, arrived at a second time by getting it wrong.
1516
+
1517
+ #### Zstandard, which turned out not to be optional
1518
+
1519
+ `basisu -uastc -ktx2` compresses with Zstandard *by default*, and so do `gltf-transform` and
1520
+ `toktx` — so almost all UASTC in the wild sits behind it, and a UASTC transcoder without a Zstandard
1521
+ decompressor reads only files written with `-ktx2_no_zstandard`, which is not what anyone writes by
1522
+ accident. It was going to be step 9c and became part of this one, because the feature was not real
1523
+ without it.
1524
+
1525
+ **Implemented from RFC 8878**, in [`core/binary/zstd/`](../../../core/binary/zstd/zstd_decompress.js)
1526
+ beside `lz4/` and `meshopt/`, on the same argument that put those there. §9.10 read ZLIB rather than
1527
+ refusing it because `pako` was already present; this is the same conclusion reached the other way,
1528
+ by writing the decompressor rather than taking one.
1529
+
1530
+ It is Lempel-Ziv with two entropy stages over it, both read *backwards* — Finite State Entropy
1531
+ carries a state between symbols, so decoding runs opposite to encoding. Two details cost a debugging
1532
+ session each and are commented where they bit:
1533
+
1534
+ - **The normalised-count field width shrinks with a running threshold** rather than being recomputed
1535
+ from the probability that remains. The two readings look equivalent and are not: the halving lags
1536
+ behind the remainder, so a decoder that recomputes reads most distributions correctly and a few
1537
+ wrongly.
1538
+ - **The interleaved two-state weight decode emits, then advances**, and stops when an advance runs
1539
+ past the start of the stream — not when the *other* state could not advance. The wrong version
1540
+ produced weight sets right for most of their length, which surfaced as two literals swapped.
1541
+
1542
+ No dictionaries, and the content checksum is skipped rather than verified: the containers carrying
1543
+ these streams check their own payload sizes, and XXH64 to re-check a length is not worth a file.
1544
+
1545
+ Tested against `node:zlib`'s Zstandard — the reference library, so the oracle is somebody else's
1546
+ implementation and not this one's own output. Three thousand fuzz cases over six input shapes and
1547
+ every compression level, thirteen committed cases, and five mutations all caught. A
1548
+ Zstandard-wrapped UASTC file now transcodes byte-identically to the same file written uncompressed.
1549
+
1550
+ #### Testing
1551
+
1552
+ **17 of the 19 modes are exercised and verified** — every one an encoder could be made to emit.
1553
+ Modes 10 and 18 are not selected by `basisu` at any quality level or content tried, so they are
1554
+ implemented from the specification and unproven, which is stated here rather than left to be assumed.
1555
+ Getting the coverage took building content to provoke modes rather than to look like anything: solid
1556
+ blocks, sharply partitioned blocks, blocks with alpha independent of colour.
1557
+
1558
+ The ground truth is the reference transcoder's BC7 output, which the specification itself calls lossy
1559
+ by 0.75 to 1.5 dB — so it is a bound rather than an equality, and at a worst channel error of **6**
1560
+ across every fixture it is a decisive one: misreading a mode, a partition, an endpoint range or an
1561
+ anchor moves a block by tens or hundreds. UASTC→ASTC is the lossless transcode and would give an
1562
+ exact check; nothing here can decode an ASTC block to make use of it.
1563
+
1564
+ **And the test decoder now shares its tables with the writer**, which means it cannot disagree with
1565
+ it — a transcription error in a partition, anchor or mode table would be invisible to every
1566
+ comparison between the two. What breaks that symmetry is decoding blocks this repository did not
1567
+ write: the reference's output for these fixtures spans BC7 modes 1, 2, 3, 5, 6 and 7, and reproducing
1568
+ the texels it published for them is exact. That check runs before any other in the file, and it is
1569
+ what makes sharing the tables safe rather than circular.
1570
+
1571
+ Twelve mutations across the decoder and both transcoders, all caught — after the harness was fixed to
1572
+ report the ones that had silently applied to nothing.
1573
+
1574
+ ### 9.11 What step 9 actually shipped, and the two places §4.3 was optimistic
1575
+
1576
+ [`transcode_basis`](../../../format/texture/basis/transcode_basis.js) is the public surface, with §4.3's contract
1577
+ intact: bytes in, texels out, no device and no queue. `src/basis/` sits beside `src/ktx2/` and
1578
+ `src/avif/` for the same reason those do — a worker, a node tool and the offline encoder will each
1579
+ want it, and none should have to import a graphics API to get it.
1580
+
1581
+ **ETC1S only.** Both Basis encodings were in scope and one landed. UASTC is a different bitstream —
1582
+ mode-switched 128-bit blocks with BISE-coded weights, sharing nothing with ETC1S but the file it
1583
+ arrives in — and it is refused by name rather than half-implemented. ETC1S is the common case in the
1584
+ wild and the whole of what `basisu` and `gltf-transform` emit at their default settings, so the
1585
+ capability is useful without it; step 9b is the rest.
1586
+
1587
+ `ktx2_read` accepts BasisLZ, and `KHR_texture_basisu` is read at both glTF sites.
1588
+
1589
+ #### Where §4.3 was optimistic
1590
+
1591
+ **1. `payload` is not a byte array.** The signature reads `transcode_basis(payload, target_format)`,
1592
+ which suggests one buffer. An ETC1S texture is not stored that way: its endpoint and selector
1593
+ codebooks and its four Huffman tables live in KTX2's *supercompression global data*, its slices live
1594
+ in the mip levels, and which bytes are which is a question about the container. So `payload` is a
1595
+ `BasisEtc1sPayload` — the sections, already located — and finding them is
1596
+ [`ktx2_read_basis_lz_global_data`](../../../format/texture/ktx2/ktx2_read_basis_lz_global_data.js), which is KTX2
1597
+ Appendix C and therefore container code. A `.basis` reader would build the same payload from a
1598
+ different layout. Nothing about "no device, no queue, no engine" changes.
1599
+
1600
+ **2. "Block-to-block transforms with no cross-block dependencies" is true of the structure and badly
1601
+ understates the work.** The transform really is per block with no neighbours involved. But the
1602
+ obvious mapping — take the ETC1S palette's darkest and brightest colours as the destination's two
1603
+ endpoints — lands **8 to 10 dB below the reference transcoder**, and a texture that far down is
1604
+ visibly banded on saturated content. Three things close it, none of them evident from the format
1605
+ descriptions:
1606
+
1607
+ - **Clamping breaks the collinearity the transform depends on.** An ETC1S block is a base colour plus
1608
+ one modifier applied to all three channels, so its four colours are collinear *before* clamping.
1609
+ A base near white with a large modifier gives red `{67, 203, 255, 255}` beside green
1610
+ `{0, 0, 57, 193}` — still ordered, no longer on a line, because red saturated and green did not.
1611
+ Endpoints have to be **fitted** (least squares, alternating with the weight assignment), not read
1612
+ off the ends.
1613
+ - **The fit has to know which selectors the block actually uses.** That is a property of its
1614
+ *selector* codebook entry, not of the endpoint entry, so the tables are keyed on the **pairing** of
1615
+ the two. A block spanning only the darkest two colours is a line those two define exactly, and
1616
+ fitting it against colours it never samples throws that away.
1617
+ - **For BC1, which colour goes at which of the four positions has to be enumerated.** The best pair
1618
+ frequently lies **outside** the colours it encodes: three evenly spaced colours have no endpoints
1619
+ putting them at weights 0, something and 64, but a pair extended past both ends puts them at 64,
1620
+ 43 and 21 almost exactly. There are at most fifteen such assignments, so they are tried rather than
1621
+ searched for. BC1's pair is then searched in 5:6:5 space rather than rounded into it, because five-
1622
+ and six-bit endpoints are coarse enough that the best representable pair is not always the rounding
1623
+ of the best real-valued one.
1624
+
1625
+ All of it is per (endpoint entry, selector span) pairing — a few hundred to a few thousand — rather
1626
+ than per block, of which a texture has tens of thousands.
1627
+
1628
+ **Which of those is worth what, measured, because the first write-up of this got it wrong.** On the
1629
+ noise fixture, against 48.0 dB for BC7 and 44.1 for BC1:
1630
+
1631
+ | removed | BC7 | BC1 |
1632
+ |---|---|---|
1633
+ | selector-span conditioning | 38.8 dB, worst channel 30 | 38.1 dB, worst channel 36 |
1634
+ | BC1's assignment enumeration | — | 37.6 dB, worst channel 30 |
1635
+ | the least-squares solve | 48.0 dB, worst channel 2 | — |
1636
+ | BC1's 5:6:5 search | — | 43.9 dB |
1637
+
1638
+ So **span conditioning is the whole of it for BC7, and span conditioning plus the assignment
1639
+ enumeration are the whole of it for BC1.** The least-squares solve on top contributes nothing
1640
+ measurable on that fixture and about a decibel on the harder mip levels of the gradient and
1641
+ non-power-of-two files; the 5:6:5 search contributes 0.2 to 1.3 dB. The commit that landed this
1642
+ credited the 5:6:5 search with 9 dB, which was the assignment enumeration's — the two were written
1643
+ at the same time and never ablated apart until afterwards.
1644
+
1645
+ The result matches the reference transcoder: BC7 at 48.0 / 52.1 / 49.1 dB against its
1646
+ 46.6 / 51.7 / 51.8, BC1 at 44.1 / 49.7 against 44.0 / 49.8.
1647
+
1648
+ **And it is not fast.** A 1024×1024 texture takes **127 ms** to reach BC7 and **389 ms** to reach
1649
+ BC1, on the main thread, of which the codebooks and the slice decode are 22 ms together and the rest
1650
+ is building the templates — 18,620 pairings for that file, of which 71% are actually referenced.
1651
+ §4.2 said transcoding "pays CPU time on load"; this is what that costs, and a model with a dozen
1652
+ textures pays it a dozen times. The obvious places to go next are running it in the worker pool
1653
+ `avif/threaded_image_encoder.js` already has, building templates lazily for the pairings a slice
1654
+ actually names, and §4.4's GPU transcoder — which is step 10 and was always the answer to this.
1655
+
1656
+ #### The judgement calls
1657
+
1658
+ **BC7 needs two modes, and the plan assumed one transform.** Mode 6 gives every texel one four-bit
1659
+ index shared by all four channels; mode 5 gives colour and alpha their own indices, two bits each. An
1660
+ ETC1S file's alpha lives in a **second slice with its own selectors**, unrelated to the colour
1661
+ slice's, so mode 6's single index set cannot express an alpha payload at all. Opaque payloads take
1662
+ mode 6 for the finer index; alpha payloads take mode 5. Mode 6 additionally fixes its parity bit at
1663
+ one rather than fitting it, because that bit is shared between an endpoint's colour channels and its
1664
+ alpha, and an opaque texture decoding to alpha 254 is a bug nothing notices until something blends.
1665
+
1666
+ **`ktx2_read` gained a parameter, and it is not defaulted.** An ETC1S payload has no format of its
1667
+ own — what it should become depends on the device and on the material slot — so `transcode_target` is
1668
+ the caller's to state. Absent, an ETC1S file is refused naming `texture_format_preference` as where
1669
+ one comes from. Every other file ignores it, so no existing caller changed.
1670
+
1671
+ **BC1 refuses an alpha payload rather than dropping it.** BC1 has no alpha channel in the mode this
1672
+ writes. A payload carries alpha slices because something needed them, and a texture arriving opaque
1673
+ surfaces far away as a cutout that does not cut out.
1674
+
1675
+ **Both glTF sites now share one function rather than agreeing by inspection.**
1676
+ `gltf_texture_image_index` is the preference order, written once:
1677
+ `KHR_texture_basisu`, then `EXT_texture_webp`, then `source`. §9.10's neighbouring incident — the two
1678
+ disagreeing, and not one model in the level loading — is not fixed by being careful, it is fixed by
1679
+ there being one decision.
1680
+
1681
+ #### What this did not build, and why
1682
+
1683
+ **Device support does not reach the asset layer.** §3.5 divides capability (the engine's) from
1684
+ selection (the asset layer's), and the asset layer here is the glTF loader — which has an
1685
+ `assetManager` and a `scope` and **no device**. `load_gltf` gains a `texture_support` option and
1686
+ nothing passes it, because opening a channel from the renderer to every asset loader is a change of
1687
+ its own shape. Absent one, the loader targets BC7: an assumption about the hardware meep aims at,
1688
+ stated as one, and a caller that knows better passes its device's support and gets a named refusal
1689
+ instead of a texture the device cannot hold. `gpu_texture_compression_support` still has no
1690
+ non-test caller.
1691
+
1692
+ **The texture's role does not enter the choice.** Every format this transcoder writes is a
1693
+ four-channel BC colour format, so a normal map and an albedo map get the same answer and asking
1694
+ `texture_format_preference` for the role would be ceremony. It stops being ceremony the moment
1695
+ ETC1S→BC5 lands — BC5 is what §11.1 already established normals want, and an ETC1S file with an alpha
1696
+ slice carries exactly the two channels it needs.
1697
+
1698
+ #### Testing, and what §9.10's gap became
1699
+
1700
+ **Step 8's fixtures were synthesised and this one's are not.** §9.10 named that as its gap — "first
1701
+ contact with a real file is the test that has not been run" — and these are files from
1702
+ `basisu 1.16.4`, so for the ETC1S path the byte offsets are now known rather than believed. That
1703
+ includes the level index and the global data layout the KTX2 reader had only ever read from its own
1704
+ writer.
1705
+
1706
+ Three things make the specs load-bearing rather than self-confirming:
1707
+
1708
+ 1. **The ground truth is the encoder's own decode.** `basisu` unpacks each file to ETC1, which *is*
1709
+ the ETC1S texels; every level of every fixture comes out of this decoder byte-identical to it.
1710
+ That covers the global data, four Huffman tables, both codebooks, the endpoint predictor and the
1711
+ selector history in one assertion.
1712
+ 2. **The block decoder the transcode tests judge with is validated before it is trusted.** A decoder
1713
+ written beside an encoder can share the encoder's misreading and agree with it perfectly, so it is
1714
+ first pointed at blocks the *reference* transcoder produced and checked against the texels the
1715
+ reference published for them.
1716
+ 3. **Every guard was checked against its own absence.** Fourteen mutations, all caught — among them
1717
+ dropping the move-to-front promotion, misreading the upper-left predictor as the upper, floating
1718
+ the parity bit, skipping the anchor flip, starting the endpoint codebook from black instead of
1719
+ `(16, 16, 16)`, dropping the selector DPCM, shifting a five-bit channel instead of replicating it,
1720
+ dropping the endpoint index wraparound, reading alpha presence as always absent, and the three
1721
+ glTF ones. One found a real hole: **no fixture reached the upper-left endpoint predictor**, so
1722
+ that branch could have been deleted silently. `etc1s_antidiag_64x64` exists to exercise it, and
1723
+ diagonal structure is what made an encoder want it.
1724
+
1725
+ **What is still untested.** Alpha has no exact reference — `basisu` never emits the alpha slice in a
1726
+ form that is not already re-encoded — so that one path is bounded rather than exact.
1727
+
1728
+ **Tier 8 has now been run** — see §9.13.
1729
+
1730
+ #### What an adversarial pass over the above then found
1731
+
1732
+ Worth recording because two of the four were defects that the tests as written could not have caught,
1733
+ and both were of the same kind: a rule read once and then assumed.
1734
+
1735
+ 1. **`KHR_texture_basisu` images were identified by MIME type, and the extension does not require
1736
+ one.** `image/ktx2` is mandatory only inside a GLB, where the image comes from a bufferView; an
1737
+ image referenced by URI is merely *permitted* to declare it. So an ordinary `.gltf` carrying
1738
+ `{"uri": "wood.ktx2"}` went to `createImageBitmap` — which is precisely the failure
1739
+ `gltf_used_image_indices` exists to prevent, reintroduced by the change that cited it. The
1740
+ extension's own reference is now the authority and the MIME type a supplementary signal.
1741
+ 2. **A corrupt slice produced silent wrong texels rather than a refusal.** The three reuse predictors
1742
+ each name a neighbour that does not exist at an edge; the specification asserts an encoder never
1743
+ does this, and the decoder trusted that. Reading past the start of the previous row's array yields
1744
+ `undefined`, which compares false against every bound and stores as endpoint zero. Now refused,
1745
+ naming the predictor and the position.
1746
+ 3. **Two more branches had no fixture reaching them**, exactly as the upper-left predictor had none:
1747
+ the **grayscale endpoint codebook**, which sends one channel and means three — deletable with
1748
+ every test still passing — and **non-power-of-two** dimensions, where every mip tail is square in
1749
+ the fixtures that existed. Both work; neither was proven. `gray_64x64` and `npot_100x60` are here
1750
+ now. The lesson is that "verify a guard against its own absence" catches broken code and says
1751
+ nothing about code no test reaches, and only counting which branches a fixture set actually
1752
+ executes finds those.
1753
+ 4. **The quality attribution in the commit message was wrong**, as the table above records. Two
1754
+ optimisations written in the same sitting were credited by measuring their sum.
1755
+
1756
+ The non-power-of-two fixture is also the one place a bound had to be *loosened* rather than a bug
1757
+ fixed: its first form used a pattern with wrapping discontinuities, on which the transcode's worst
1758
+ channel error is 59 — and the reference transcoder's is 69, because no two-endpoint format holds four
1759
+ colours that far off a line. That is a fact about the format pair, so the fixture was re-made with
1760
+ smooth content, leaving the geometry under test and the thin per-channel bound intact.
1761
+
1762
+ ### 9.10 What step 8 actually shipped
1763
+
1764
+ [`ktx2_read`](../../../format/texture/ktx2/ktx2_read.js) is the whole public surface: `bytes -> ShadeTexelData`, one
1765
+ free function, no device and no queue. It lives at `src/ktx2/`, beside `src/avif/` rather than inside
1766
+ the renderer, which is §2's "living with its container" taken literally — the same function is what a
1767
+ worker, a node tool and the offline encoder will each want, and none of them should have to import a
1768
+ graphics API to get it.
1769
+
1770
+ **It reads what first-party tooling emits and refuses everything else by name**, one refusal per
1771
+ feature, each naming the scheme or the format: BasisLZ, Zstandard, an unknown scheme number, ETC1S,
1772
+ UASTC, an unmapped `vkFormat`, array textures, cube maps, 1D textures, 3D block-compressed payloads,
1773
+ `levelCount == 0` on a block format, a flipped `KTXorientation`, a non-identity `KTXswizzle`.
1774
+
1775
+ #### The reuse sweep, and what it moved
1776
+
1777
+ §9.4's lesson was applied before writing rather than discovered afterwards. Three things came out of
1778
+ it, and two of them are changes to existing code:
1779
+
1780
+ - **`ByteCursor` moved to [`core/binary/`](../../../core/binary/ByteCursor.js) and takes its byte
1781
+ order at `init`.** It was exactly the cursor this needed — bounds-checked, zero-copy, a named error
1782
+ per truncated field — and it was big-endian and living in `format/image/avif/isobmff/`. The alternatives
1783
+ were both worse: `BinaryBuffer` is a growable stream whose reads throw `RangeError` from a
1784
+ `DataView` rather than naming the field that ran out, and a second cursor in `ktx2/` would have
1785
+ been the fourth copy of bounds-checked reading in this tree. **Endianness is a property of the
1786
+ container, so it is stated once at `init`** rather than at each of the eighty-odd accessor calls —
1787
+ which is also why no AVIF accessor call site changed, only the ten places that construct a
1788
+ cursor, two of them outside tests. (The commit message says "four". It is wrong; this is the
1789
+ count.)
1790
+ [D10](../../../format/image/avif/DECISIONS.md) is amended to record this; the decision it made — read
1791
+ with a cursor, write with `BinaryBuffer` — is untouched.
1792
+ - **`gpu_texture_format_is_block_compressed` exists, and not where §2 put it.** §2 listed it under
1793
+ `renderer/texture/format/`. That was wrong twice over: it is a fact about the format table, so it
1794
+ belongs in the table's own module beside `gpu_texture_format_row_bytes` — which is what §6.1
1795
+ already decided for derived helpers — and a container reader cannot import from
1796
+ `shade/renderer/`. The three sites that had it written out inline (`SoftwareGPUTexture`,
1797
+ `gpu_texture_format_can_generate_mipmaps`, `download_texture_data`) now call it, so the reader's
1798
+ two uses are the fourth and fifth callers of one predicate rather than copies five and six.
1799
+ - **Nothing else was written from scratch that already existed.** Level byte lengths are
1800
+ `ShadeTexelData`'s, which checks them against the format table; the chain-length rule is
1801
+ `ShadeTexelData`'s; the identifier comparison is `isArrayEqualStrict`; the key/value strings are
1802
+ `utf8_decode`. The reader computes no extent and no row stride at all — it reads `byteLength` from
1803
+ the level index and lets the payload type disagree with it if the file is wrong.
1804
+
1805
+ **It is not behind [`Codec`](../../../engine/asset/codec/Codec.js).** That interface is
1806
+ `async decode(Uint8Array) -> T` and lives under `engine/asset/loaders/image/`, so subclassing it
1807
+ would put engine code in a container reader's import graph and make a synchronous byte-shuffle a
1808
+ promise for no reason. The image-codec review is still right that every image format should reach the
1809
+ engine through one door — but the door is an adapter, written when the compressed-texture *loader*
1810
+ lands and has somewhere to put the `ShadeImage` it builds. Nothing consumes this reader yet, exactly
1811
+ as nothing consumed step 7 until now.
1812
+
1813
+ #### Where the plan was overturned, and the judgement calls
1814
+
1815
+ **ZLIB is read, not refused.** §11.6 says "transport compression is the delivery layer's business",
1816
+ and it is right — about what meep *writes*. It says nothing about what meep must be able to *read*,
1817
+ and a KTX2 file with `supercompressionScheme` 3 holds device-format blocks behind an RFC 1950 stream
1818
+ that `pako` — already a dependency, already inflating every PNG — undoes in one call. Refusing it
1819
+ would have meant refusing a file the engine can use completely, over a wrapper it can already remove.
1820
+ The declared `uncompressedByteLength` is checked against what the stream actually inflates to, and
1821
+ then `ShadeTexelData` checks that against the format table, so a mislabelled file fails twice rather
1822
+ than uploading garbage. Zstandard stays refused because there is no decompressor here at all, and
1823
+ BasisLZ stays refused because it is not a wrapper — it is an encoding with its own codebooks, and
1824
+ undoing it is a transcoder.
1825
+
1826
+ > **Both of those refusals are gone now.** BasisLZ became step 9's transcoder, and step 9b wrote the
1827
+ > Zstandard decompressor this section said did not exist — see §9.12 for why that stopped being
1828
+ > optional. The reasoning above is left as it was written: it was right about the *kind* of thing
1829
+ > each is, and being right about that is what made both of them tractable when the time came.
1830
+
1831
+ **`pako.inflate` directly rather than
1832
+ [`png_inflate`](../../../format/image/png/png_inflate.js).** What that function adds
1833
+ over the bare call is PNG's two peculiarities: a stream split across `IDAT` chunks, and a fallback
1834
+ that abandons the adler32 check because real encoders get it wrong. A KTX2 level is one stream, and a
1835
+ texture payload whose checksum fails should fail. Reusing it would have meant a KTX2 reader importing
1836
+ from the PNG loader for the sake of a wrapper whose whole content it wants to opt out of.
1837
+
1838
+ **`levelCount == 0` is refused for a block format and read as one level otherwise.** It means
1839
+ "generate the mip chain at load", which is an *instruction*, and §3.1's payload carries data. The
1840
+ engine's instruction is `ShadeTextureFlags.GenerateMipMaps`, honoured or thrown by §3.2, and it is
1841
+ the caller's to state — so the reader returns the level the file physically contains and does not
1842
+ pretend to carry the request. For a block-compressed payload the request cannot be honoured by
1843
+ anything at any layer, so it is refused here, naming the format, rather than left to fail later with
1844
+ a message about a flag the file never mentioned.
1845
+
1846
+ **Both BC1 spellings map to `bc1-rgba-unorm`.** `VK_FORMAT_BC1_RGB_*` and `VK_FORMAT_BC1_RGBA_*` are
1847
+ the same sixty-four bits; the difference is whether a three-colour-mode block's fourth index reads as
1848
+ opaque black or transparent, and WebGPU exposes only the RGBA name. Mapped rather than refused,
1849
+ because an encoder targeting the RGB spelling has no alpha to store and so no reason to emit that
1850
+ index — refusing would reject a large share of real BC1 content over a difference the content does
1851
+ not contain. Written down in the table rather than left as a coincidence.
1852
+
1853
+ **Two refusals the plan never mentioned**, both because the failure is a *silent misread* rather than
1854
+ an error: `KTXorientation` other than right-down loads the texture upside down, and `KTXswizzle`
1855
+ other than `rgba` loads it in the wrong channels. Neither looks like a bug in the reader. Reading the
1856
+ key/value section costs forty lines and turns both into a named refusal quoting the value. The third
1857
+ axis of a 3D orientation is checked strictly — `rdi` and not `rdo` — which may be stricter than real
1858
+ files need; the refusal names the value, so a file that is genuinely fine is a one-line change rather
1859
+ than a mystery.
1860
+
1861
+ **The `vkFormat` table names its gaps.** 97 numbers map to 95 formats — 54 rows onto 52 distinct
1862
+ block-compressed ones, the two BC1 spellings collapsing, and 43 onto the uncompressed ones.
1863
+ Twenty-three more are *named* with a reason they cannot be mapped — three-channel formats,
1864
+ single-channel sRGB, `A2R10G10B10` and `A8B8G8R8_PACK32` byte order, depth and stencil — so a file
1865
+ carrying one is told what is wrong with it rather than handed a number. Anything outside both tables
1866
+ is refused by number, which is the honest answer for a format nobody here has heard of.
1867
+
1868
+ A spec asserts that **every colour format in `gpu_texture_format_info` is reachable from some
1869
+ `vkFormat`**, which is the tripwire for a format landing in the engine's table and not in this one.
1870
+ That assertion started out scoped to block-compressed formats, on the stated theory that several
1871
+ uncompressed formats had no Vulkan counterpart; measuring it during review showed the theory false —
1872
+ all 43 are mapped — so the tripwire is now as wide as the mapping, and depth and stencil are the only
1873
+ exclusions.
1874
+
1875
+ #### Testing, and the gap in it
1876
+
1877
+ Forty specs over the reader and its `vkFormat` table, and six more over the cursor's new parameter —
1878
+ the same bytes reading differently in each order, and `init` refusing to guess. The two that could
1879
+ have passed against a broken reader were checked against their own absence:
1880
+
1881
+ 1. **Level ordering.** The level index is base-first and the image data is conventionally stored
1882
+ smallest-first, so a reader that walked the file would return a plausible payload with the pyramid
1883
+ upside down — right byte counts, right level count, every texture blurred to its 1×1 mip. The
1884
+ fixture builder lays the data out either way; with `cursor.seek(byte_offset)` removed the
1885
+ smallest-first test fails and the base-first ones still pass, which is exactly the asymmetry that
1886
+ makes it worth having.
1887
+ 2. **Truncation.** Removing the per-level bounds check fails both the truncated-file test and the
1888
+ level-index-past-the-end test.
1889
+
1890
+ **The fixtures are synthesised, and that is the gap.** `ktx2_build` writes the container from the
1891
+ specification, so the reader and the fixture share one reading of it: a misreading would be
1892
+ reproduced identically on both sides and pass. What synthesis buys is every malformed variant as a
1893
+ one-line override, which a committed binary cannot give, and it is why the whole refusal surface is
1894
+ covered at all. The missing check is a file from `toktx` — an independent authority on the layout —
1895
+ and until one is read the byte offsets in this reader are believed rather than known. First contact
1896
+ with a real file is the test that has not been run.
1897
+
1898
+ #### What an adversarial pass found afterwards
1899
+
1900
+ Three things, all in the parts that were argued for hardest.
1901
+
1902
+ **`pako` throws a bare string, and the ZLIB path let it escape.** `pako.inflate` on a corrupt stream
1903
+ throws `"incorrect header check"` — a `String`, not an `Error`, with no `.message` and nothing naming
1904
+ the file or the level. So the one capability added beyond what this step was asked for was the one
1905
+ path that did not refuse by name, and the JSDoc's `@throws {Error}` was false for it. Now wrapped,
1906
+ and the message names the level. Worth recording *why* it was missed: the comment justifying
1907
+ `pako.inflate` over `png_inflate` listed two differences and was satisfied with them — multi-chunk
1908
+ streams and lenient adler32 — when there was a third, and the third was the one that mattered. An
1909
+ argument for not reusing something is only as good as its enumeration of what the thing does.
1910
+
1911
+ **`ShadeTexelData` refuses a legitimate 3D file with a full mip chain**, and this reader is the first
1912
+ code able to reach it. Its per-level byte lengths are 3D-correct — `level_byte_length` goes through
1913
+ `texture_mip_extent(…, "3d", …)`, which halves depth — but its chain-length rule is
1914
+ `compute_max_mip_count(width, height)`, which does not see depth at all. A 4×4×16 volume has a
1915
+ five-level chain and the constructor insists on three; `level_byte_length` will meanwhile happily
1916
+ size the two levels the constructor rejects. The type disagrees with itself.
1917
+
1918
+ **And it is not only there.** `texture_descriptor_from_texture_shade.js:50` computes the *generate*
1919
+ path's `mipLevelCount` the same way, as do `TextureResourceDescriptor` and `GPULightCollection` — so
1920
+ a 3D texture asking for generated mips gets a short chain today, with no container reader involved.
1921
+ That makes it §9.4's finding again, one axis over: the depth rule was given a home in
1922
+ `texture_mip_extent` and the *count* rule never was. Deliberately not fixed in that commit — it is a
1923
+ change to step 2's contract and to call sites that have nothing to do with KTX2, and burying it in a
1924
+ container reader's commit is how a shared invariant gets changed without anyone reviewing it. The
1925
+ reader refuses rather than misreads in the meantime, which is the behaviour that matters.
1926
+
1927
+ **Fixed since, on its own.** `texture_mip_level_count(width, height, depth_or_layers, dimension)` is
1928
+ now the counterpart §9.4 was missing, beside the extent in the same file, and
1929
+ `compute_max_mip_count` — which took no depth and so could not be told — is gone rather than
1930
+ deprecated, because a depth-blind mip count left reachable is a thing someone reaches for. All six
1931
+ callers went through it, and the sweep is the reason to have done it as its own change rather than
1932
+ only patching the two sites that were wrong:
1933
+
1934
+ - `ShadeTexelData` and `texture_descriptor_from_texture_shade` were the live bugs — a volume's chain
1935
+ and a volume's *generated* chain, both truncated to what width and height implied.
1936
+ - `TextureResourceDescriptor.from` had the dimension in its own argument list and was not passing it,
1937
+ which mattered: `graph_import_texture` re-derives `mipLevelCount` from resolution for anything with
1938
+ mips, so a 3D import would have had its chain recomputed short. It now also reads the padded
1939
+ `r.resolution` rather than the raw argument, so a two-axis caller reads as depth 1 and not
1940
+ `undefined`.
1941
+ - `GPULightCollection` (equirectangular environment), `HierarchicalZBuffer` (screen-space depth
1942
+ pyramid) and `GPULightMap` are 2D and were never wrong. The first now asks its descriptor for its
1943
+ dimension instead of the call site assuming one; the other two say `"2d"` at the call, which is the
1944
+ whole of what they know.
1945
+
1946
+ A spec pins the part that matters more than any single call site: for a range of shapes, the level
1947
+ the count ends on is the one where the halving axes reach 1, and the level before it is not. The
1948
+ count and the extent agree because they are derived from one rule, rather than because both call
1949
+ sites were written by someone who remembered.
1950
+
1951
+ Also corrected: an exported constant with no importers (`BC1_RGB_NOTE`, now prose beside the rows it
1952
+ explains), a `supercompression_scheme_name` whose only caller could never reach anything but its
1953
+ fallback — producing "supercompressionScheme 7 is an unknown scheme, which this reader does not
1954
+ know" — and an unused `truncate_to` option in the fixture builder. Three tests were added for
1955
+ promises nothing checked: an `ArrayBuffer` and an offset view as input, and a multi-level ZLIB file.
1956
+ The new ZLIB guard was checked against its own absence like the other two.
1957
+
1958
+ ### 9.9 What step 7 actually shipped, and where §5.1 was wrong
1959
+
1960
+ §5.1 said `AssetManager.assignAlias` "is the mechanism that already exists for exactly this
1961
+ indirection" and proposed making alias resolution format-aware. **That was wrong, for the reason
1962
+ `ShadeImage.format` was wrong:** it would push `GPUTextureFormat`, `TextureRole` and a device
1963
+ capability set into the generic asset system, which loads sounds and fonts and JSON and has no
1964
+ business knowing what a texel block is.
1965
+
1966
+ The indirection belongs in the texture layer, and once it is there `AssetManager` needs no change at
1967
+ all. [`TextureVariantSet`](TextureVariantSet.js) is §11.5's manifest — which encodings exist and
1968
+ where, known before the first request. [`texture_variant_select`](format/texture_variant_select.js)
1969
+ walks the preference list against it and returns a path, which the existing `AssetManager.promise`
1970
+ loads like anything else.
1971
+
1972
+ **It returns a path and nothing else.** Not the format it chose: a reader determines a payload's
1973
+ format from the payload's own header, which is the only source that cannot be wrong, and handing back
1974
+ a format alongside would invite someone to trust the manifest over the bytes. That also means the
1975
+ manifest is deliberately dumb about content — a manifest that lies produces a failed read rather than
1976
+ a texture full of garbage.
1977
+
1978
+ **It always answers.** There is no "nothing suitable" case for a caller to handle: a device with no
1979
+ compression, or one whose preferred formats were never built, gets the fallback — the same path a
1980
+ tool or a test asks for. A texture with no universally readable encoding is a broken manifest, not a
1981
+ runtime condition.
1982
+
1983
+ **Nothing consumes this yet**, which is the plan's shape rather than an oversight: step 8's KTX2
1984
+ reader is the first caller, and this is the contract it is written against.
1985
+
1986
+ ### 9.8 What step 3b actually shipped
1987
+
1988
+ [`chunk_decode_tangent_normal`](../../shader/chunk/geometry/normal/chunk_decode_tangent_normal.js)
1989
+ reconstructs Z from XY instead of reading the blue channel, at all five sites. §11.1's resolution,
1990
+ unchanged by contact with the code: one decode serving both channel counts, so nothing branches on
1991
+ the format and no pipeline is compiled twice.
1992
+
1993
+ Verified three ways, because the failure mode is a shader that renders wrong rather than one that
1994
+ fails to build:
1995
+
1996
+ 1. **Against the blue channel it ignores.** The chunk spec runs in the WGSL emulator and asserts the
1997
+ reconstruction agrees with the stored Z to five decimals over a set of normals, and that a
1998
+ two-channel texel decodes identically to a three-channel one. That is what makes this safe to land
1999
+ before any compressed texture exists — every RGB normal map in the engine should look exactly as
2000
+ it did.
2001
+ 2. **Against a missing registration.**
2002
+ [`normal_decode_call_sites.spec.js`](../../shader/chunk/geometry/normal/normal_decode_call_sites.spec.js)
2003
+ compiles each site and asserts the function is declared exactly once and called. Checked against
2004
+ its own absence: removing one registration fails it with an `undeclared-call` diagnostic. Three of
2005
+ the four sites had **no WGSL compile coverage at all** before this — only the VT shader did.
2006
+ 3. **Against the `max(0, …)` guard.** Filtering and lossy compression push samples off the unit
2007
+ circle; without it `sqrt` of a negative gives `NaN` rather than a normal in the tangent plane.
2008
+
2009
+ `texture_format_preference` now leads with `bc5-rg-unorm` for normals, and §9.7's tripwire — the test
2010
+ asserting two-channel formats were *absent* — has fired and been replaced by its opposite. That is the
2011
+ payoff: two channels of budget where four were being spent, on the one map type where a colour codec
2012
+ bands visibly.
2013
+
2014
+ ### 9.7 What step 6 actually shipped
2015
+
2016
+ The three features are requested when offered and never required, beside `TextureFormatsTier1`.
2017
+ [`gpu_texture_compression_support`](format/gpu_texture_compression_support.js) turns "which features
2018
+ did we get" into "which formats may we ask for", deriving the families from the format table's own
2019
+ names rather than a second list — 14 BC, 10 ETC2/EAC, 28 ASTC.
2020
+
2021
+ [`texture_format_preference`](format/texture_format_preference.js) is the engine's whole part in
2022
+ choosing an encoding, over a new [`TextureRole`](../TextureRole.js). It answers what would be good and
2023
+ stops: no fetching, no knowledge of what was built, no fallback — all of which are the asset layer's,
2024
+ in step 7.
2025
+
2026
+ **It deliberately offers no two-channel format for normals.** `bc5-rg-unorm` is the right answer and
2027
+ handing it out now would render black, because §11.1's Z reconstruction has not landed: BC5 samples as
2028
+ `(x, y, 0, 1)` and the five normal reads are still `.rgb * 2.0 - 1.0`. There is a test asserting the
2029
+ absence, which is a tripwire meant to fail — and be deleted — in the change that fixes the shader.
2030
+ Until then the list offers three-channel formats, which are worse for normals and correct.
2031
+
2032
+ **§6.2's guard found the real invariant.** `!is_srgb` was standing in for "the formats are
2033
+ copy-compatible", true only because every material texture was `rgba8unorm`. It is now
2034
+ `source.format === target.format`, which covers every format at once.
2035
+
2036
+ **§6.4's alpha rewrite regressed a case, and the test caught it.** Answering purely from the format
2037
+ called every three-channel image possibly-transparent: the upload pads RGB to RGBA because WebGPU has
2038
+ no three-channel format, so the *texture* has alpha while the *content* has none. Loose pixels are now
2039
+ answered by their channel count first, and only a texel payload — whose encoder already chose a
2040
+ format that says how many channels there are — is answered by the format alone. `bc1-rgba-unorm`
2041
+ correctly reports alpha (§11.6).
2042
+
2043
+ ### 9.6 What step 5 actually shipped
2044
+
2045
+ §6.3 proposed restating the queue's row loop in blocks. §9.4's lesson said to look at the mock first,
2046
+ and looking found that no restating was needed: `writeTexture` is `copyBufferToTexture` with the bytes
2047
+ supplied inline, and [`texture_copy.js`](../../device/mock/texture_copy.js) already had both halves —
2048
+ `texture_copy_refusal` and `texture_copy_execute`, block-aware since they were written, and already
2049
+ used by `SoftwareGPUCommandEncoder` for the buffer version.
2050
+
2051
+ So `writeTexture` now wraps its `data` and `dataLayout` as a buffer endpoint and calls the same two
2052
+ functions. Three things came with that beyond block support: multi-layer writes (the
2053
+ `depth === 1` assertion is gone), `rowsPerImage` honoured rather than ignored, and stricter
2054
+ validation — `texture_endpoint_refusal` turned out to be a superset of the queue's private
2055
+ `texture_write_refusal`, adding a `copyable` check, a mip-range check and a bounds check, with the
2056
+ same wording for the three they shared.
2057
+
2058
+ [`compressed_texture_upload.spec.js`](compressed_texture_upload.spec.js) is §10.4: a BC7 payload
2059
+ through `TextureManager`, the descriptor builder and the uploader, asserted byte-for-byte at every
2060
+ level, with no GPU. Checked against its own absence — against the pre-step-5 queue it fails with
2061
+ `bytes_per_texel: 'bc7-rgba-unorm' is block-compressed`.
2062
+
2063
+ Its "no validation error" assertion is itself guarded: the mock reports refusals through an error
2064
+ scope rather than by throwing, so a test that watched the wrong channel would pass against a texture
2065
+ nothing had been written to. A second test writes to a texture without `COPY_DST` and asserts the
2066
+ error does land, which is what makes the first one mean something.
2067
+
2068
+ ### 9.5 What step 4 actually shipped
2069
+
2070
+ The flag is now an instruction. `generate` is read once, the format is checked against
2071
+ [`gpu_texture_format_can_generate_mipmaps`](format/gpu_texture_format_can_generate_mipmaps.js), and a
2072
+ request that cannot be honoured throws with a message naming the texture, the format and what to do
2073
+ instead. Otherwise `mipLevelCount` is `image.level_count` — no inference, no comparison.
2074
+
2075
+ **The predicate covers three classes and says so.** Block-compressed and depth/stencil come out of
2076
+ `gpu_texture_format_info`; `snorm` and `rgb9e5ufloat` are a listed set. It is deliberately *not* a
2077
+ general renderability table — `rg11b10ufloat` is renderable only behind a feature and this does not
2078
+ check for it — because a partial table presented as complete is worse than a predicate whose scope is
2079
+ written down. Everything outside those classes answers `true`, which is correct for every format the
2080
+ engine currently produces.
2081
+
2082
+ **`texture_write_to_gpu` did get a `level_count` after all** — required, not optional, so every caller
2083
+ states it. Step 3 argued two named functions were enough; that was wrong for one case, which is a
2084
+ `ShadeTexelData` carrying a chain *and* the generate flag. `texture_write_level` cannot serve it,
2085
+ because an `ImageBitmap` still has to go through `copyExternalImageToTexture` and `TextureManager`
2086
+ should not be re-deriving which source kind it holds. A required argument keeps the decision at the
2087
+ caller without reintroducing an optional one.
2088
+
2089
+ **Nothing existing can reach the new throw.** `infer_format_from_parameters` only ever produces
2090
+ `unorm`, `unorm-srgb`, `uint` and `float` endings — never `snorm`, never a packed format — so no
2091
+ image the engine builds today has a format the generator would refuse.
2092
+
2093
+ ### 9.4 One mip-arithmetic rule, four copies
2094
+
2095
+ `SoftwareGPUTexture` has had `mip_width` / `mip_height` / `row_bytes` / `row_count` / `layer_bytes`
2096
+ all along — the oldest and most complete statement of per-level layout in the tree, and one this plan
2097
+ walked straight past. Steps 1, 2 and 3 each added another copy of the same arithmetic
2098
+ (`memory_footprint`'s loop, `ShadeTexelData.level_byte_length`, `texture_write_level`), which is
2099
+ precisely the outcome §0 exists to prevent, committed three times in a row.
2100
+
2101
+ Each rule now has one home:
2102
+
2103
+ - [`texture_mip_size(size, level)`](../../util/texture_mip_extent.js) — halve, floor at one.
2104
+ - [`texture_mip_extent(width, height, depth_or_layers, dimension, level)`](../../util/texture_mip_extent.js)
2105
+ — that, plus **only a 3D texture's depth halves**. This is the one worth naming: written by hand it
2106
+ reads as an obvious `>> level` on all three components, and that mistake is what §6.1 found already
2107
+ shipped in two places.
2108
+ - [`texture_mip_level_count(width, height, depth_or_layers, dimension)`](../../util/texture_mip_extent.js)
2109
+ — how long the chain the other two describe actually is: as long as the longest axis that halves,
2110
+ which is the *same* distinction one derivative up. Added after step 8, once §9.10 found that the
2111
+ extent rule having a home and the count rule not having one is a gap and not a tidiness complaint —
2112
+ `ShadeTexelData` had ended up sizing levels it then refused. It lives in the same file as the extent
2113
+ so the two cannot drift.
2114
+ - [`gpu_texture_format_level_bytes`](../../descriptor/texture/format/gpu_texture_format_info.js) —
2115
+ `row_bytes × row_count × layers`, which was written out in four.
2116
+
2117
+ `SoftwareGPUTexture` keeps its API — `texture_copy` and `SoftwareGPUQueue` both use it — and delegates
2118
+ to these. Nothing in `src/shade/` computes a mip dimension inline any more; the only surviving
2119
+ `>> level` is inside `texture_mip_size`, and the only `log2` is inside `texture_mip_level_count`.
2120
+
2121
+ The general lesson for the rest of the plan: **the mock is not a lesser implementation.** It was
2122
+ written against the format table from the start and had the block-aware answer to three of these
2123
+ questions before the renderer did.
2124
+
2125
+ ### 9.2 What step 2 actually shipped
2126
+
2127
+ `alpha_mode` **derives lazily, and cannot be captured at construction.** §11.3 said the default comes
2128
+ from `color_space` "at the point that constructs the image", which would have been wrong for the same
2129
+ reason §11.2 was: nine places assign `color_space` *after* the image exists, and two of them assign
2130
+ `ColorSpace.None` — `shade_load_data_image` and `build_lut_fog_parameters_jendersie_deon`. A value
2131
+ captured at construction would premultiply exactly the data textures the rule exists to protect. It
2132
+ is a getter over a private override: unset, it derives; set, it wins and keeps winning.
2133
+
2134
+ **`AlphaMode.Unspecified` is a member of the enum**, not a `undefined` sitting in a field typed as
2135
+ one. "Nothing has said yet" is a state the field genuinely has, so it gets a name; assigning it is
2136
+ also how a caller goes back to deriving after an override. The getter resolves and never returns it,
2137
+ which is its own test.
2138
+
2139
+ **The format lives in `format/texture_format_from_shade_image.js`**, not on `ShadeImage`. After the
2140
+ move the image imports neither `isImageBitmap` nor the inference — it is back to describing data and
2141
+ nothing else.
2142
+
2143
+ `infer_format_from_parameters` moved to
2144
+ [`format/infer_format_from_parameters.js`](format/infer_format_from_parameters.js), and its spec with
2145
+ it. `ShadeImage` needed it, and a data model importing from the descriptor *builder* is a dependency
2146
+ pointing the wrong way. The spec's assertions are byte-identical; only its import paths changed.
2147
+
2148
+ `texture_write_to_gpu` now asks `image.alpha_mode` instead of re-deriving `color_space !== None`
2149
+ inline. Provably the same decision today — which is what the untouched regression test shows — and
2150
+ the point is that it is now a decision an image can be *told*.
2151
+
2152
+ Steps 8 and 9 are separable on purpose: **a KTX2 file that already holds BC7 needs no transcoder**,
2153
+ and that is what first-party tooling produces. The transcoder is only for ETC1S/UASTC payloads, which
2154
+ is third-party content, which is why it can wait behind everything else.
2155
+
2156
+ ---
2157
+
2158
+ ## 10. Testing
2159
+
2160
+ Per the tiers this repo uses:
2161
+
2162
+ 1. **The enum does not drift.** `Object.values(TextureFormat)` and `gpu_texture_format_names()` are
2163
+ the same set — the one check that keeps §2.1's enum a view of the format table rather than a
2164
+ second copy of it. Cheap, and it fails the moment a new format lands in one and not the other.
2165
+ 2. **Node-side import.** A spec that imports `TextureDescriptor` with the vitest globals *removed*
2166
+ proves the §2.1 shim actually replaced the load-time global read, rather than the stub quietly
2167
+ continuing to cover for it. Without this the fix is untested by construction.
2168
+ 3. **Pure functions.** `gpu_texture_format_row_bytes` against hand-computed BC7/BC5/ASTC sizes
2169
+ including non-block-multiple widths — a 5-texel-wide BC7 row is two blocks, the case the current
2170
+ uploader's arithmetic gets wrong. `ShadeTexelData` rejecting a partial chain, and a payload whose
2171
+ byte length disagrees with its declared format and size. `texture_format_preference` returning
2172
+ only formats in the support set, and returning empty rather than guessing.
2173
+ 4. **Software device.** Build a `ShadeImage` over a synthetic BC7 payload, run it through
2174
+ `TextureManager`, and assert the mock texture's per-level bytes are exactly the input — end to end
2175
+ through `texture_descriptor_from_texture_shade` and `texture_write_level`, with no GPU. This is
2176
+ the test that proves the two paths really are one path.
2177
+ 5. **Streaming.** The same, but delivering levels out of order, coarsest first: each arrival writes
2178
+ only its own level, and `lodMinClamp` tracks the finest resident one.
2179
+ 6. **Descriptor, and the §3.2 instruction.** Over a compressed image with the flag clear:
2180
+ `RENDER_ATTACHMENT` is not set and `mipLevelCount` equals the supplied level count. With the flag
2181
+ set: throws, **for one supplied level and for a full chain alike** — the case that would regress
2182
+ silently if anyone reintroduced a level-count comparison. Over an uncompressed image with a full
2183
+ chain and the flag set: the generator is scheduled and only level 0 is uploaded.
2184
+ 7. **Alignment, for §7's benefit.** `VTPhysicalCache` constructed with a compressed layer asserts on a
2185
+ `slot_size` that is not a block multiple — the one check that keeps the tile path honest, worth
2186
+ writing before the tile path exists.
2187
+ 8. **Playground.** One scene, loaded with support forced to bc / astc / none, compared visually and by
2188
+ `TextureManager.gpu_memory_usage` — which, after §6.1, is the number that shows the feature
2189
+ working: the same scene at roughly a quarter of the VRAM.
2190
+
2191
+ ---
2192
+
2193
+ ## 11. Adversarial pass
2194
+
2195
+ Written against revision 4, before any code. Four findings change the design; two are corrections of
2196
+ fact. Each was checked against the tree rather than against the argument that produced it.
2197
+
2198
+ ### 11.1 "No shader changes" is false, and §5.2 is what makes it false
2199
+
2200
+ **§0 claims no shader changes. §5.2 recommends `bc5-rg-unorm` for normal maps. These cannot both
2201
+ stand.**
2202
+
2203
+ BC5 has two channels. A sample returns `(x, y, 0, 1)`. Every normal-map read in the tree is
2204
+ `.rgb * 2.0 - 1.0`, at five sites:
2205
+
2206
+ | Site | |
2207
+ |---|---|
2208
+ | [`fragment_gbuffer.js:325`](../material/standard/fragment_gbuffer.js:325) | the main deferred path |
2209
+ | [`fragment_gbuffer_vt.js:129`](../material/standard/fragment_gbuffer_vt.js:129) | virtual textures |
2210
+ | [`chunk_sample_material_data.js:205`](../path_tracer/shader/chunk_sample_material_data.js:205) | path tracer |
2211
+ | [`chunk_forward_shade_standard_fragment_ibl.js:40`](../rasterize/native/oit/chunk_forward_shade_standard_fragment_ibl.js:40) | forward / OIT |
2212
+ | [`chunk_forward_shade_standard_fragment_brick4.js:37`](../rasterize/native/oit/chunk_forward_shade_standard_fragment_brick4.js:37) | forward / OIT |
2213
+
2214
+ Against a BC5 texture each of those computes `z = 0 * 2 - 1 = -1`: every normal points directly into
2215
+ the surface. Not subtly wrong — no lighting at all.
2216
+
2217
+ **Resolution: always reconstruct Z, never read it.** A tangent-space normal has `z > 0` by
2218
+ construction, so `z = sqrt(max(0, 1 - x² - y²))` is exact for a unit normal and is *equally correct
2219
+ for an RGB source*, whose stored blue channel is redundant. One chunk, applied at all five sites,
2220
+ serving BC5 and RGB identically — no variant, no branch on format, nothing that would need
2221
+ [per-format codegen](../../wgsl).
2222
+
2223
+ So the honest statement is not "no shader changes" but:
2224
+
2225
+ > **No shader *variants*.** One decode change, shared by compressed and uncompressed alike, and after
2226
+ > it the sampling path does not know which it has.
2227
+
2228
+ Which is the invariant of §0 restated, rather than a hole in it — but it is a change to five shaders
2229
+ and it belongs in §9, not in a sentence claiming it does not exist. Two caveats worth stating: this
2230
+ assumes tangent-space normals (what glTF's `normalTexture` is, by specification), and the VT normal
2231
+ default `vec4(0.5, 0.5, 1.0, 1.0)` still reconstructs to `z = 1` unchanged.
2232
+
2233
+ The alternative — BC7 for normals, keeping `.rgb` — costs no shader work and real quality: BC7 spends
2234
+ its budget on three channels where BC5 spends the same 8 bpp on two. Recommendation stands with BC5,
2235
+ now that its actual price is on the table rather than hidden behind a false claim.
2236
+
2237
+ ### 11.2 `ShadeImage.format` cannot be memoised — the inputs are mutated after construction
2238
+
2239
+ §3.1 says format is `infer_format_from_parameters(...)` "and memoised". It cannot be.
2240
+ [`gltf_create_material.js:86`](../loader/gltf/gltf_create_material.js:86) and
2241
+ [`:54`](../loader/gltf/gltf_create_material.js:54) set `texture.image.color_space = ColorSpace.SRGB`
2242
+ **after** [`load_gltf.js:404`](../loader/gltf/load_gltf.js:404) has already built the `ShadeImage`.
2243
+ A format cached at construction is `rgba8unorm` forever, and every albedo and emissive texture in
2244
+ every glTF silently loses its sRGB decode.
2245
+
2246
+ `format` is therefore a plain getter, recomputed. It is a table lookup and a string concat on a path
2247
+ that runs once per texture, not per frame.
2248
+
2249
+ §3.1 was also inaccurate about *what* it computes:
2250
+ [`inferFormatFromImage`](texture_descriptor_from_texture_shade.js:99) has two arms, and the
2251
+ `ImageBitmap` one does **not** call `infer_format_from_parameters` — it returns `rgba16float`,
2252
+ `rgba8unorm-srgb` or `rgba8unorm` directly. Both arms move to `ShadeImage.format` unchanged.
2253
+
2254
+ **A pre-existing bug this surfaces, worth its own fix.** Images are deduplicated
2255
+ ([`load_gltf.js:403`](../loader/gltf/load_gltf.js:403) indexes by glTF image index), so one
2256
+ `ShadeImage` can back two textures. `gltf_create_material` then assigns `color_space` per *material
2257
+ slot*. An image used as both a base-colour and an ORM map gets whichever material was visited last —
2258
+ today that flips a decode, and under this plan it flips a format. Not caused by this plan, made
2259
+ load-bearing by it.
2260
+
2261
+ ### 11.3 The `alpha_mode` default silently premultiplies every data texture
2262
+
2263
+ §3.3 defaults `alpha_mode` to `Straight` and premultiplies `Straight` + uncompressed. Today's rule is
2264
+ `premultiplyAlpha = image.color_space !== ColorSpace.None`
2265
+ ([texture_write_to_gpu.js:217](texture_write_to_gpu.js:217)) — so **non-colour data is never
2266
+ premultiplied**, and under §3.3 as written it would be. Height fields, masks, ORM maps, LUTs: all
2267
+ scaled by their alpha channel on upload.
2268
+
2269
+ There is a regression test named for exactly this — `"color_space None skips premultiplication
2270
+ entirely"` ([texture_write_to_gpu.spec.js:84](texture_write_to_gpu.spec.js:84)) — which §9 step 2
2271
+ claims will pass unmodified. It would not.
2272
+
2273
+ **Resolution: three states, not two.** Premultiplication is not a property of the alpha channel; it
2274
+ is a property of whether the channel *means* opacity.
2275
+
2276
+ ```js
2277
+ export const AlphaMode = {
2278
+ Opaque: 0, // there is no meaningful alpha — data. Never premultiply. (ColorSpace.None today)
2279
+ Straight: 1, // colour with straight alpha. Premultiply if uncompressed; throw if block.
2280
+ Premultiplied: 2, // already done, by the encoder or by copyExternalImageToTexture
2281
+ };
2282
+ ```
2283
+
2284
+ The default is derived from `color_space` at the point that constructs the image — which preserves
2285
+ today's behaviour exactly, keeps the regression test green, and gives a compressed payload somewhere
2286
+ truthful to declare itself. Note this is a *default*, not an inference at use time; §3.2's rule
2287
+ against data-dependent behaviour concerns instructions being reinterpreted, and there is no
2288
+ instruction here to reinterpret.
2289
+
2290
+ ### 11.4 `texture_write_to_gpu` cannot honour §3.2, because it cannot see the flag
2291
+
2292
+ §3.2 says that when generation is requested the supplied levels above 0 are not uploaded. Nothing
2293
+ implements that: the signature is `texture_write_to_gpu(image, texture, queue)`
2294
+ ([:189](texture_write_to_gpu.js:189)), with two non-spec callers
2295
+ ([`TextureManager.js:147`](TextureManager.js:147),
2296
+ [`GPULightCollection.js:201`](../light/GPULightCollection.js:201)), and `ShadeTextureFlags` lives on
2297
+ the `ShadeTexture`, which it never receives.
2298
+
2299
+ **Resolution:** the uploader takes an explicit level range — `texture_write_to_gpu(image, texture,
2300
+ queue, { levels })` — defaulting to all of them. `TextureManager` passes `[0, 1)` when it is about to
2301
+ schedule the generator. This is the same parameter mip streaming needs (§3.4) in order to write one
2302
+ arriving level, so it is one addition serving both, not a special case for generation.
2303
+
2304
+ ### 11.5 Per-format resolution needs a manifest, which §5.1 does not have
2305
+
2306
+ §5.1's loop asks `resolve(logical_name, format)` and says a description "may not exist". It does not
2307
+ say how the client knows. If existence is discovered by requesting, every texture on a device whose
2308
+ preferred format was not built costs a 404 round-trip — possibly several, walking the preference
2309
+ list — before the fallback. On a streaming engine that is the worst place to put a stall.
2310
+
2311
+ The plan needs a **per-texture set of available formats, known before the first request**: a field in
2312
+ whatever index already names the texture, or in the alias list that
2313
+ [`loadAssetAliasList`](../../../engine/asset/loadAssetAliasList.js) reads. Cheap, but it is a
2314
+ prerequisite of §5.1 rather than a detail of it, and §9 step 7 currently reads as though the
2315
+ resolution rule were the whole job.
2316
+
2317
+ ### 11.6 Corrections of fact
2318
+
2319
+ - **BC1 has alpha.** §6.4 lists `bc1` among formats with "no usable alpha". WebGPU exposes it only as
2320
+ `bc1-rgba-unorm`, which decodes 1-bit punch-through alpha. `shade_texture_might_have_alpha` must
2321
+ answer **true** for it. `bc4`, `bc5` and `etc2-rgb8unorm` in that list are correct;
2322
+ `etc2-rgb8a1unorm` would be another true.
2323
+ - **The 256-alignment arithmetic in §4.4 is stated too strongly.** `ceil(w/4) × 16 ≡ 0 (mod 256)`
2324
+ requires `ceil(w/4)` to be a multiple of 16, which `w = 61…64` also satisfies — not only multiples
2325
+ of 64. For a mip chain, where widths are powers of two, the practical rule is `w ≥ 64`. The
2326
+ conclusion (pad the rows) is unchanged; the stated condition was wrong.
2327
+ - **`gpu_texture_format_info` has three consumers, not two** — `SoftwareGPUDevice`,
2328
+ `SoftwareGPUTexture`, `frame_graph_extract_topology`. Said twice, wrong twice.
2329
+ - **The LZ4 reference in §1 is vestigial.** It survives from revision 1's container, which revision 2
2330
+ deleted in favour of per-format resources (§5). Nothing in the plan now wraps block payloads in
2331
+ anything — transport compression is the delivery layer's business, and a BC7 payload compresses
2332
+ ~10–15 % anyway. Drop the bullet rather than leave a dependency the plan does not use.
2333
+
2334
+ ### 11.7 What survived
2335
+
2336
+ The load-bearing claims were checked and hold: `gpu_texture_format_info` is block-correct and is the
2337
+ right single table; `VTPhysicalCache` really is per-layer format-parameterised, so §7 stays small;
2338
+ `lodMinClamp` exists; `gpu_texture_format_srg_to_linear` and `gpu_texture_format_to_color_space`
2339
+ really are already compressed-aware; `@webgpu/types` really is absent (`"webgpu"` in `package.json`
2340
+ is a keyword, not a dependency); `GPUTextureFormat` really is never read as a runtime value; and
2341
+ `TextureDescriptor.js:61` really does read `GPUTextureUsage` at module load.
2342
+
2343
+ §0's central move — format on the image rather than inferred at upload — is untouched by all six
2344
+ findings. Three of them (11.2, 11.3, 11.4) are cases where the *transition* was underspecified rather
2345
+ than the destination being wrong, which is the failure mode a plan of this shape should expect.