@woosh/meep-engine 3.60.4 → 3.60.5

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 (379) hide show
  1. package/CHANGELOG.md +2602 -0
  2. package/CONTRIBUTING.md +7 -0
  3. package/MIGRATION_2_TO_3.md +136 -0
  4. package/SECURITY.md +4 -0
  5. package/editor/EditorCameraNavigation.d.ts +2 -1
  6. package/editor/EditorCameraNavigation.d.ts.map +1 -1
  7. package/editor/EditorCameraNavigation.js +51 -57
  8. package/package.json +6 -2
  9. package/src/engine/EngineHarness.d.ts +3 -9
  10. package/src/engine/EngineHarness.d.ts.map +1 -1
  11. package/src/engine/EngineHarness.js +6 -8
  12. package/src/engine/graphics/camera/makeOrbitalCameraController.d.ts +9 -6
  13. package/src/engine/graphics/camera/makeOrbitalCameraController.d.ts.map +1 -1
  14. package/src/engine/graphics/camera/makeOrbitalCameraController.js +44 -71
  15. package/src/engine/graphics/camera/orbital/MomentumAxis.d.ts +64 -0
  16. package/src/engine/graphics/camera/orbital/MomentumAxis.d.ts.map +1 -0
  17. package/src/engine/graphics/camera/orbital/MomentumAxis.js +187 -0
  18. package/src/engine/graphics/camera/orbital/OrbitalCameraRig.d.ts +86 -0
  19. package/src/engine/graphics/camera/orbital/OrbitalCameraRig.d.ts.map +1 -0
  20. package/src/engine/graphics/camera/orbital/OrbitalCameraRig.js +319 -0
  21. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.d.ts +34 -10
  22. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.d.ts.map +1 -1
  23. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.js +116 -30
  24. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts +1 -7
  25. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts.map +1 -1
  26. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.js +1 -37
  27. package/src/engine/graphics/ecs/water/Water.d.ts +8 -0
  28. package/src/engine/graphics/ecs/water/Water.d.ts.map +1 -1
  29. package/src/engine/graphics/ecs/water/Water.js +8 -0
  30. package/src/engine/graphics3/WaterSystem.d.ts.map +1 -1
  31. package/src/engine/graphics3/WaterSystem.js +731 -734
  32. package/src/engine/graphics3/decal/DecalTileShader.d.ts +61 -0
  33. package/src/engine/graphics3/decal/DecalTileShader.d.ts.map +1 -0
  34. package/src/engine/graphics3/decal/DecalTileShader.js +138 -0
  35. package/src/engine/graphics3/decal/GPUDecalRenderer.d.ts +10 -4
  36. package/src/engine/graphics3/decal/GPUDecalRenderer.d.ts.map +1 -1
  37. package/src/engine/graphics3/decal/GPUDecalRenderer.js +149 -44
  38. package/src/engine/graphics3/decal/chunk_decal_tile_vertex.d.ts +25 -0
  39. package/src/engine/graphics3/decal/chunk_decal_tile_vertex.d.ts.map +1 -0
  40. package/src/engine/graphics3/decal/chunk_decal_tile_vertex.js +55 -0
  41. package/src/engine/graphics3/decal/graph_gather_decal_tiles.d.ts +22 -0
  42. package/src/engine/graphics3/decal/graph_gather_decal_tiles.d.ts.map +1 -0
  43. package/src/engine/graphics3/decal/graph_gather_decal_tiles.js +70 -0
  44. package/src/engine/graphics3/decal/shader_decal_composite.d.ts +7 -5
  45. package/src/engine/graphics3/decal/shader_decal_composite.d.ts.map +1 -1
  46. package/src/engine/graphics3/decal/shader_decal_composite.js +48 -44
  47. package/src/engine/graphics3/decal/shader_decal_tile_copy.d.ts +12 -0
  48. package/src/engine/graphics3/decal/shader_decal_tile_copy.d.ts.map +1 -0
  49. package/src/engine/graphics3/decal/shader_decal_tile_copy.js +78 -0
  50. package/src/engine/graphics3/decal/shader_gather_decal_tiles.d.ts +11 -0
  51. package/src/engine/graphics3/decal/shader_gather_decal_tiles.d.ts.map +1 -0
  52. package/src/engine/graphics3/decal/shader_gather_decal_tiles.js +80 -0
  53. package/src/engine/input/devices/mouse/MouseEventButtons.d.ts +13 -0
  54. package/src/engine/input/devices/mouse/MouseEventButtons.d.ts.map +1 -0
  55. package/src/engine/input/devices/mouse/MouseEventButtons.js +23 -0
  56. package/src/engine/input/ecs/controllers/KeyboardCameraController.d.ts +4 -0
  57. package/src/engine/input/ecs/controllers/KeyboardCameraController.d.ts.map +1 -1
  58. package/src/engine/input/ecs/controllers/KeyboardCameraController.js +9 -45
  59. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  60. package/src/shade/playground/water_refraction/README.md +5 -6
  61. package/src/shade/playground/water_refraction/index.html +1 -1
  62. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  63. package/src/shade/renderer/Renderer.js +7 -5
  64. package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts +40 -80
  65. package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts.map +1 -1
  66. package/src/shade/renderer/camera/orbital/OrbitalCameraController.js +197 -147
  67. package/src/shade/renderer/fluid/GPUFluidCascade.d.ts +3 -3
  68. package/src/shade/renderer/fluid/GPUFluidCascadeStack.d.ts +1 -1
  69. package/src/shade/renderer/fluid/forcing/GPUFluidForcing.d.ts +6 -6
  70. package/src/shade/renderer/fluid/pressure/GPUFluidPressureSolve.d.ts +1 -1
  71. package/src/shade/renderer/fluid/query/GPUFluidQueries.d.ts +1 -1
  72. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.d.ts +2 -2
  73. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.js +2 -2
  74. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts +2 -1
  75. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts.map +1 -1
  76. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.js +18 -1
  77. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts +31 -0
  78. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts.map +1 -0
  79. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.js +343 -0
  80. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts +17 -0
  81. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts.map +1 -0
  82. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.js +29 -0
  83. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.d.ts.map +1 -1
  84. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.js +7 -13
  85. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.d.ts.map +1 -1
  86. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.js +1 -8
  87. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts +5 -5
  88. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts.map +1 -1
  89. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.js +10 -10
  90. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts +1 -4
  91. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts.map +1 -1
  92. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.js +1 -10
  93. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts +5 -15
  94. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts.map +1 -1
  95. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.js +6 -34
  96. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.d.ts.map +1 -1
  97. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.js +1 -7
  98. package/src/shade/renderer/hiz/HZBFootprint.d.ts +14 -0
  99. package/src/shade/renderer/hiz/HZBFootprint.d.ts.map +1 -0
  100. package/src/shade/renderer/hiz/HZBFootprint.js +16 -0
  101. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts +10 -0
  102. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts.map +1 -0
  103. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.js +11 -0
  104. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts +10 -0
  105. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts.map +1 -0
  106. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.js +11 -0
  107. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts +0 -7
  108. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts.map +1 -1
  109. package/src/shade/renderer/hiz/HierarchicalZBuffer.js +405 -350
  110. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts +12 -0
  111. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts.map +1 -0
  112. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.js +27 -0
  113. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts +12 -0
  114. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts.map +1 -0
  115. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.js +27 -0
  116. package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts +14 -0
  117. package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts.map +1 -0
  118. package/src/shade/renderer/hiz/hzb_footprint_texels.js +34 -0
  119. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts +0 -5
  120. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts.map +1 -1
  121. package/src/shade/renderer/hiz/shader_hzb_reduce_any.js +37 -57
  122. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts +11 -0
  123. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts.map +1 -0
  124. package/src/shade/renderer/hiz/{shader_hzb_reduce_mip0_clip.js → shader_hzb_reduce_mip0.js} +120 -111
  125. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  126. package/src/shade/renderer/material/GPUMaterialManager.d.ts +7 -0
  127. package/src/shade/renderer/material/GPUMaterialManager.d.ts.map +1 -1
  128. package/src/shade/renderer/material/GPUMaterialManager.js +11 -0
  129. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts +12 -12
  130. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts.map +1 -1
  131. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.js +17 -118
  132. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.d.ts.map +1 -1
  133. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.js +7 -2
  134. package/src/shade/renderer/water/GPUWaterRenderer.d.ts +0 -6
  135. package/src/shade/renderer/water/GPUWaterRenderer.d.ts.map +1 -1
  136. package/src/shade/renderer/water/GPUWaterRenderer.js +12 -17
  137. package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts +35 -0
  138. package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts.map +1 -0
  139. package/src/shade/renderer/water/medium/graph_water_meshlets.js +89 -0
  140. package/src/shade/renderer/water/query/GPUWaterQueries.d.ts +1 -1
  141. package/src/shade/renderer/water/shader_water.d.ts.map +1 -1
  142. package/src/shade/renderer/water/shader_water.js +22 -13
  143. package/src/shade/renderer/water/shader_water_avboit.d.ts.map +1 -1
  144. package/src/shade/renderer/water/shader_water_avboit.js +7 -7
  145. package/src/shade/renderer/water/surface/graph_water_tessellation.d.ts +1 -1
  146. package/src/shade/renderer/water/surface/graph_water_tessellation.js +1 -1
  147. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +9 -1
  148. package/editor/REWORK_PLAN_2026_08.md +0 -692
  149. package/src/CODEC_LAYOUT_VERIFICATION.md +0 -419
  150. package/src/CORRECTNESS_HUNT_2026_07_24.md +0 -142
  151. package/src/DUPLICATION_AUDIT_2026_08_07/00_core_reuse_surface.md +0 -738
  152. package/src/DUPLICATION_AUDIT_2026_08_07/A_core_math_geom.md +0 -79
  153. package/src/DUPLICATION_AUDIT_2026_08_07/B_core_data.md +0 -93
  154. package/src/DUPLICATION_AUDIT_2026_08_07/C_gfx_render.md +0 -80
  155. package/src/DUPLICATION_AUDIT_2026_08_07/D_gfx_features.md +0 -164
  156. package/src/DUPLICATION_AUDIT_2026_08_07/E_physics.md +0 -69
  157. package/src/DUPLICATION_AUDIT_2026_08_07/F_sound.md +0 -110
  158. package/src/DUPLICATION_AUDIT_2026_08_07/G_net_save.md +0 -129
  159. package/src/DUPLICATION_AUDIT_2026_08_07/H_engine_misc.md +0 -72
  160. package/src/DUPLICATION_AUDIT_2026_08_07/I_generation_view.md +0 -74
  161. package/src/DUPLICATION_AUDIT_2026_08_07/K_crosscutting.md +0 -169
  162. package/src/DUPLICATION_AUDIT_2026_08_07.md +0 -254
  163. package/src/DUPLICATION_AUDIT_REMEDIATION.md +0 -250
  164. package/src/DUPLICATION_REVIEW_2026_10_24.md +0 -1072
  165. package/src/REVIEW_2026_08_06/adv-core.md +0 -511
  166. package/src/REVIEW_2026_08_06/adv-gfx.md +0 -480
  167. package/src/REVIEW_2026_08_06/adv-view.md +0 -446
  168. package/src/REVIEW_2026_08_06/api-core.md +0 -296
  169. package/src/REVIEW_2026_08_06/api-engine.md +0 -195
  170. package/src/REVIEW_2026_08_06/bvh-phys-sound-net.md +0 -507
  171. package/src/REVIEW_2026_08_06/core-binary.md +0 -348
  172. package/src/REVIEW_2026_08_06/core-collection.md +0 -376
  173. package/src/REVIEW_2026_08_06/core-math.md +0 -221
  174. package/src/REVIEW_2026_08_06/core-process.md +0 -266
  175. package/src/REVIEW_2026_08_06/core-science.md +0 -223
  176. package/src/REVIEW_2026_08_06/engine-ai-nav.md +0 -279
  177. package/src/REVIEW_2026_08_06/engine-animation.md +0 -167
  178. package/src/REVIEW_2026_08_06/engine-asset.md +0 -166
  179. package/src/REVIEW_2026_08_06/engine-ecs.md +0 -920
  180. package/src/REVIEW_2026_08_06/engine-input.md +0 -197
  181. package/src/REVIEW_2026_08_06/engine-misc.md +0 -248
  182. package/src/REVIEW_2026_08_06/engine-terrain.md +0 -52
  183. package/src/REVIEW_2026_08_06/generation.md +0 -176
  184. package/src/REVIEW_2026_08_06/geom-2d.md +0 -434
  185. package/src/REVIEW_2026_08_06/geom-3d.md +0 -117
  186. package/src/REVIEW_2026_08_06/gfx-gi.md +0 -143
  187. package/src/REVIEW_2026_08_06/gfx-render.md +0 -740
  188. package/src/REVIEW_2026_08_06/gfx-texture.md +0 -690
  189. package/src/REVIEW_2026_08_06/prior-criticals.md +0 -49
  190. package/src/REVIEW_2026_08_06/prior-highs.md +0 -138
  191. package/src/REVIEW_2026_08_06/test-gaps.md +0 -364
  192. package/src/REVIEW_2026_08_06/view.md +0 -667
  193. package/src/REVIEW_2026_08_06.md +0 -610
  194. package/src/core/binary/lz4/LZ4_HIGH_COMPRESSION_PLAN_2026_09_14.md +0 -542
  195. package/src/core/bvh2/bvh3/NOTES.md +0 -2
  196. package/src/core/bvh8/NOTES.md +0 -63
  197. package/src/core/cache/wtinylfu/benchmark/CacheWTinylfu.bench.md +0 -243
  198. package/src/core/color/COLOR_FIX_PLAN_2026_08_28.md +0 -417
  199. package/src/core/color/COLOR_REVIEW_2026_08_28.md +0 -779
  200. package/src/core/geom/2d/polygon/TRIANGULATION_DESIGN.md +0 -433
  201. package/src/core/geom/3d/atlas/ATLAS_PORT_PLAN.md +0 -486
  202. package/src/core/geom/3d/atlas/BFF_PLAN.md +0 -174
  203. package/src/core/geom/3d/atlas/REVIEW_02.md +0 -392
  204. package/src/core/geom/3d/gjk/GJK_REVIEW_NOTES.md +0 -146
  205. package/src/core/geom/3d/gjk/NOTES.md +0 -8
  206. package/src/core/geom/3d/topology/TOPO_MESH_RETIREMENT_PLAN.md +0 -135
  207. package/src/core/geom/GEOMETRY_REVIEW_002_API_ISSUES.md +0 -381
  208. package/src/core/geom/GEOMETRY_REVIEW_002_NAMING.md +0 -684
  209. package/src/core/geom/MANIFOLD_COMPARISON_2026_08_01.md +0 -554
  210. package/src/core/model/reactive/evaluation/PREDICATE_APPLICABILITY_PROPOSAL.md +0 -446
  211. package/src/engine/REVIEW_2026_07_03.md +0 -211
  212. package/src/engine/asset/loaders/image/IMAGE_CODEC_REVIEW_2026_08_28.md +0 -815
  213. package/src/engine/control/first-person/DESIGN.md +0 -658
  214. package/src/engine/control/first-person/DESIGN_COLLISION.md +0 -382
  215. package/src/engine/control/first-person/DESIGN_EXTENSIONS.md +0 -563
  216. package/src/engine/control/first-person/TODO.md +0 -160
  217. package/src/engine/ecs/ASYNC_SYSTEMS_PROPOSAL.md +0 -531
  218. package/src/engine/ecs/HIERARCHY_REVIEW.md +0 -717
  219. package/src/engine/ecs/async/WORKER_SYSTEM_PLAN.md +0 -262
  220. package/src/engine/ecs/ik/PLAN.md +0 -179
  221. package/src/engine/ecs/storage/binary/SERIALIZATION_COVERAGE.md +0 -272
  222. package/src/engine/ecs/storage/binary/object-v2/DESIGN.md +0 -199
  223. package/src/engine/ecs/transform/TRANSFORM64_MIGRATION_PLAN_2026_09_04.md +0 -521
  224. package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +0 -463
  225. package/src/engine/graphics/ecs/camera/pp/NOTES.md +0 -48
  226. package/src/engine/graphics/render/frame_graph/CORRECTNESS_FIXES_2026_09_04.md +0 -659
  227. package/src/engine/graphics/render/frame_graph/FRAME_GRAPH_RECOMMENDATIONS.md +0 -235
  228. package/src/engine/graphics/sh3/path_tracer/REVIEW_001_ACTION_PLAN.md +0 -490
  229. package/src/engine/graphics/texture/sampler/compare/FLIP_IMPLEMENTATION_PLAN.md +0 -466
  230. package/src/engine/input/GAMEPAD_API_PROPOSAL.md +0 -717
  231. package/src/engine/input/ecs/ism/ISM_DESIGN.md +0 -1523
  232. package/src/engine/input/ecs/ism/ISM_IMPLEMENTATION_PLAN.md +0 -627
  233. package/src/engine/navigation/mesh/OVERLAPPING_SURFACES_2026_08_25.md +0 -661
  234. package/src/engine/navigation/mesh/PATHFINDING_PLAN.md +0 -229
  235. package/src/engine/network/CONGESTION_CONTROL.md +0 -87
  236. package/src/engine/network/MESSAGE_TRANSPORT.md +0 -364
  237. package/src/engine/network/SMOOTHNESS_DESIGN.md +0 -400
  238. package/src/engine/physics/cloth/MEASUREMENTS.md +0 -5304
  239. package/src/engine/physics/cloth/PLAN.md +0 -3056
  240. package/src/engine/physics/fluid/GPU_FLUID_CASCADES_PROPOSAL_2026_10_16.md +0 -1382
  241. package/src/engine/physics/fluid/REVIEW_02_PLAN.md +0 -243
  242. package/src/engine/physics/fluid/solver/NOTES.md +0 -46
  243. package/src/engine/sound/SOUND_EMITTER_RETIREMENT_PLAN.md +0 -536
  244. package/src/engine/sound/simulation/IMPLEMENTATION_PLAN.md +0 -432
  245. package/src/engine/sound/simulation/PROBE_PLACEMENT.md +0 -250
  246. package/src/engine/sound/simulation/PROBE_TRANSFER_RESEARCH.md +0 -322
  247. package/src/engine/sound/simulation/REFLECTION_IR_PLAN.md +0 -261
  248. package/src/engine/sound/simulation/VISIBILITY_GRAPH_PLAN.md +0 -121
  249. package/src/engine/sound/sopra/IMPLEMENTATION_PLAN.md +0 -999
  250. package/src/format/image/avif/DECISIONS.md +0 -545
  251. package/src/format/image/jpeg/NOTES.md +0 -3
  252. package/src/format/scene/gltf/MESHOPT_COMPRESSION_PLAN.md +0 -814
  253. package/src/generation/COORDINATES.md +0 -54
  254. package/src/generation/REVIEW_01_ACTION_PLAN.md +0 -628
  255. package/src/generation/foliage/ANNUAL_SHOOTS_BRIEF_2026_10_25.md +0 -220
  256. package/src/generation/foliage/CROWN_FILLING_BRIEF_2026_10_26.md +0 -172
  257. package/src/generation/foliage/CROWN_FULLNESS_BRIEF_2026_10_26.md +0 -86
  258. package/src/generation/foliage/FOLIAGE_DESIGN.md +0 -547
  259. package/src/generation/foliage/FOLIAGE_PLAN.md +0 -198
  260. package/src/generation/foliage/LEAF_ARCHITECTURE_BRIEF_2026_10_26.md +0 -158
  261. package/src/generation/foliage/LOD_REWORK_BRIEF_2026_10_27.md +0 -521
  262. package/src/generation/foliage/REAL_TIME_TREES_BRIEF_2026_10_27.md +0 -86
  263. package/src/generation/foliage/SELF_ORGANIZING_FRAMEWORK_BRIEF_2026_10_26.md +0 -347
  264. package/src/generation/foliage/SHOOT_SYSTEMS_ALL_SPECIES_BRIEF_2026_10_26.md +0 -248
  265. package/src/generation/foliage/SPEEDTREE_COMPARISON_2026_10_24.md +0 -694
  266. package/src/generation/foliage/TWO_FRONT_ENDS_BRIEF_2026_10_28.md +0 -552
  267. package/src/shade/RENDERER_CONTRACT.md +0 -697
  268. package/src/shade/SHADER_F16_PROPOSAL_2026_10_24.md +0 -263
  269. package/src/shade/device/IMMEDIATE_DATA_PLAN_2026_08_28.md +0 -569
  270. package/src/shade/device/PASS_ENCODER_PROPOSAL_2026_08_31.md +0 -755
  271. package/src/shade/device/mock/EXTENSION_PROPOSAL_2026_08_22.md +0 -458
  272. package/src/shade/device/mock/IMPLEMENTATION_PLAN.md +0 -255
  273. package/src/shade/device/mock/REVIEW_2026_08_22.md +0 -527
  274. package/src/shade/device/timing/GPU_PROFILER_PROPOSAL_2026_08_28.md +0 -894
  275. package/src/shade/device/timing/profile/SGPT_FORMAT.md +0 -345
  276. package/src/shade/renderer/NOTES.md +0 -78
  277. package/src/shade/renderer/PLAN.md +0 -8
  278. package/src/shade/renderer/animation/ADDITIVE_BLENDING_PLAN.md +0 -145
  279. package/src/shade/renderer/animation/SKINNING_FRAME_PLAN.md +0 -357
  280. package/src/shade/renderer/atmosphere/NOTES.md +0 -6
  281. package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts +0 -76
  282. package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts.map +0 -1
  283. package/src/shade/renderer/camera/orbital/MomentumAxis.js +0 -164
  284. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts +0 -45
  285. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts.map +0 -1
  286. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.js +0 -66
  287. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts +0 -98
  288. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts.map +0 -1
  289. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.js +0 -184
  290. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts +0 -175
  291. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts.map +0 -1
  292. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.js +0 -411
  293. package/src/shade/renderer/camera/orbital/README.md +0 -81
  294. package/src/shade/renderer/camera/orbital/index.d.ts +0 -6
  295. package/src/shade/renderer/camera/orbital/index.d.ts.map +0 -1
  296. package/src/shade/renderer/camera/orbital/index.js +0 -9
  297. package/src/shade/renderer/extension/RENDER_EXTENSION_DESIGN.md +0 -964
  298. package/src/shade/renderer/fluid/GPU_FLUID_OCCUPANCY_AND_SOTA_REVIEW_2026_10_26.md +0 -361
  299. package/src/shade/renderer/geometry/bvh/BLAS_REFIT_BATCH_PLAN.md +0 -619
  300. package/src/shade/renderer/geometry/meshlet/COMPACTION_PLAN_2026_09_16.md +0 -674
  301. package/src/shade/renderer/geometry/sdf/NOTES.md +0 -6
  302. package/src/shade/renderer/geometry/virtual/ATTRIBUTE_PLAN_2026_09_18.md +0 -540
  303. package/src/shade/renderer/geometry/virtual/MICRON_COMPARISON_2026_08_20.md +0 -305
  304. package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +0 -913
  305. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +0 -787
  306. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +0 -481
  307. package/src/shade/renderer/geometry/virtual/VK_LOD_CLUSTERS_COMPARISON_2026_08_20.md +0 -301
  308. package/src/shade/renderer/geometry/virtual/WEDGE_PLAN_2026_09_18.md +0 -448
  309. package/src/shade/renderer/geometry/virtual/runtime/CULL_PLAN_2026_09_16.md +0 -385
  310. package/src/shade/renderer/geometry/virtual/runtime/CUT_PLAN_2026_09_14.md +0 -656
  311. package/src/shade/renderer/geometry/virtual/runtime/REVIEW_LEDGER.md +0 -308
  312. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN.md +0 -657
  313. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN_2026_09_14.md +0 -551
  314. package/src/shade/renderer/geometry/virtual/runtime/SHADOW_PLAN_2026_09_28.md +0 -529
  315. package/src/shade/renderer/global_illumination/lpv/NOTES.md +0 -3
  316. package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_CLIPMAP_PROPOSAL_2026_10_27.md +0 -4306
  317. package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_RAYS_VARIABLE_AND_SORTED_SCOPE_2026_10_30.md +0 -410
  318. package/src/shade/renderer/global_illumination/sharc/NOTES.md +0 -8
  319. package/src/shade/renderer/gpu_primitive/bvh/GPU_BVH_BUILD_PLAN.md +0 -1369
  320. package/src/shade/renderer/gpu_primitive/bvh/PHASE1_HANDOVER.md +0 -202
  321. package/src/shade/renderer/gpu_primitive/indirect/NOTES.md +0 -94
  322. package/src/shade/renderer/hiz/NOTES.md +0 -3
  323. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts +0 -8
  324. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts.map +0 -1
  325. package/src/shade/renderer/light/cluster/NOTES.md +0 -11
  326. package/src/shade/renderer/light/cluster/PROPOSAL_ALGORITHM_2026_10_16.md +0 -524
  327. package/src/shade/renderer/light/cluster/PROPOSAL_HIERARCHICAL_2026_10_16.md +0 -1101
  328. package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts +0 -24
  329. package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts.map +0 -1
  330. package/src/shade/renderer/material/resident/shader_resident_texture_means.js +0 -120
  331. package/src/shade/renderer/particles/DESIGN.md +0 -1011
  332. package/src/shade/renderer/particles/EMITTER_GLOBAL_STATE_PLAN_2026_10_29.md +0 -280
  333. package/src/shade/renderer/particles/ORIENTATION_PLAN_2026_10_15.md +0 -453
  334. package/src/shade/renderer/path_tracer/PATH_TRACED_MEDIA_PLAN_2026_10_02.md +0 -520
  335. package/src/shade/renderer/postprocess/bloom/NOTES.md +0 -6
  336. package/src/shade/renderer/postprocess/fsr/NOTES.md +0 -4
  337. package/src/shade/renderer/postprocess/gtao/NOTES.md +0 -6
  338. package/src/shade/renderer/postprocess/nss/NOTES.md +0 -332
  339. package/src/shade/renderer/postprocess/perspective/NOTES.md +0 -3
  340. package/src/shade/renderer/postprocess/sscs/NOTES.md +0 -5
  341. package/src/shade/renderer/postprocess/ssr/NOTES.md +0 -14
  342. package/src/shade/renderer/postprocess/ssr/REPROJECTION_PROPOSAL.md +0 -286
  343. package/src/shade/renderer/postprocess/ssr/prefilter/NOTES.md +0 -39
  344. package/src/shade/renderer/postprocess/taa/NOTES.md +0 -11
  345. package/src/shade/renderer/postprocess/upscale-dither/NOTES.md +0 -78
  346. package/src/shade/renderer/postprocess/vrs/NOTES.md +0 -4
  347. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_LEDGER.md +0 -1218
  348. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_PLAN_2026_09_03.md +0 -600
  349. package/src/shade/renderer/restir/reservoir/NOTES.md +0 -6
  350. package/src/shade/renderer/shader/chunk/atmosphere/NOTES.md +0 -6
  351. package/src/shade/renderer/shader/chunk/brdf/FRESNEL_BRIEF_2026_10_16.md +0 -391
  352. package/src/shade/renderer/shader/chunk/color/tonemap/ACES_TONE_MAPPING_BRIEF_2026_10_17.md +0 -153
  353. package/src/shade/renderer/shader/chunk/color/tonemap/HDR_TONE_MAPPING_FORMS_BRIEF_2026_10_16.md +0 -322
  354. package/src/shade/renderer/shader/chunk/color/tonemap/SDR_TONE_MAPPING_BRIEF_2026_10_16.md +0 -247
  355. package/src/shade/renderer/shader/chunk/fsr/NOTES.md +0 -3
  356. package/src/shade/renderer/shader/chunk/fsr/spd/NOTES.md +0 -4
  357. package/src/shade/renderer/shader/chunk/geometry/ray/NOTES.md +0 -4
  358. package/src/shade/renderer/shader/chunk/material/MULTISCATTER.md +0 -159
  359. package/src/shade/renderer/shader/chunk/mboit/NOTES.md +0 -38
  360. package/src/shade/renderer/shader/chunk/spherical_harmonics/l1i/NOTES.md +0 -3
  361. package/src/shade/renderer/shader/chunk/spherical_harmonics/sampling/NOTES.md +0 -8
  362. package/src/shade/renderer/shader/compiler/SHADER_SWITCH_PROPOSAL_2026_10_16.md +0 -927
  363. package/src/shade/renderer/shadow/contact/FINDINGS.md +0 -413
  364. package/src/shade/renderer/shadow/map/csm/NOTES.md +0 -5
  365. package/src/shade/renderer/shadow/ray/NOTES.md +0 -31
  366. package/src/shade/renderer/shadow/sdf/NOTES.md +0 -9
  367. package/src/shade/renderer/shadow/sdf/brixelizer/NOTES.md +0 -5
  368. package/src/shade/renderer/shadow/sdf/cascade/NOTES.md +0 -9
  369. package/src/shade/renderer/shadow/vsm/NOTES.md +0 -12
  370. package/src/shade/renderer/shadow/vsm/PLAN.md +0 -442
  371. package/src/shade/renderer/shadow/vsm/REVIEW_LEDGER.md +0 -43
  372. package/src/shade/renderer/texture/COMPRESSED_TEXTURE_PLAN_2026_09_01.md +0 -2444
  373. package/src/shade/renderer/texture/mipmaps/NOTES.md +0 -3
  374. package/src/shade/renderer/volumetrics/NOTES.md +0 -1329
  375. package/src/shade/renderer/volumetrics/VOLUMETRIC_NOISE_PLAN_2026_10_15.md +0 -336
  376. package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts +0 -23
  377. package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts.map +0 -1
  378. package/src/shade/renderer/water/medium/GPUWaterBodies.js +0 -75
  379. package/src/shade/wgsl/NOTES.md +0 -4
@@ -1,814 +0,0 @@
1
- # `EXT_meshopt_compression` / `KHR_meshopt_compression` — implementation plan
2
-
3
- Status: plan. Written 2026-08-20.
4
-
5
- Scope: decode side of both extensions.
6
-
7
- - `EXT_meshopt_compression` — ratified, Vendor.
8
- <https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Vendor/EXT_meshopt_compression>
9
- - `KHR_meshopt_compression` — ratified, Khronos, merged 2026-01-23 (PR #2517).
10
- <https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_meshopt_compression>
11
-
12
- Reference encoder/decoder: <https://github.com/zeux/meshoptimizer>. Consulted for questions the
13
- specification text leaves ambiguous; **not vendored and not transcribed** — no new dependency, and
14
- the bitstream below is implemented from the two specifications.
15
-
16
- ## 1. What these extensions are, and why that shapes the split
17
-
18
- Neither is a geometry extension. Both are *bufferView* extensions: a bufferView declares that its
19
- bytes are produced by running a decoder over a byte range of some other buffer. Nothing about the
20
- decode depends on what the bufferView is later read as — vertex attributes, index data, morph
21
- deltas, animation keyframes and `EXT_mesh_gpu_instancing` transforms all go through the same three
22
- modes and (now) five filters.
23
-
24
- That is the seam. Everything except roughly two dozen lines is a **byte-array in, byte-array out**
25
- codec with no knowledge of glTF at all:
26
-
27
- ```
28
- src/core/binary/meshopt/ pure codec — bytes in, bytes out.
29
- ^ No glTF, no geometry, no engine.
30
- | called by
31
- src/format/scene/gltf/ext/meshopt/ one bufferView's worth of meshopt: read the
32
- ^ extension object, check it, return the bytes.
33
- | called through GltfBufferViewExtension by
34
- src/format/scene/gltf/ext/ the generic bufferView-extension layer — which
35
- ^ extension claims a view, and what becomes of what
36
- | it produced. Names no codec.
37
- | called by
38
- src/shade/renderer/loader/gltf/tiny-gltf.js one call, after buffers resolve. Names no
39
- extension at all.
40
- ```
41
-
42
- `core/binary` is the codec's right home: it is where meep already keeps byte-level codecs
43
- (`base64/`, `base58/`, `half_to_float_uint16`, `BinaryBuffer`'s LEB128 `readUintVar`). The codec
44
- imports nothing outside `core/`, so it is usable from a worker, a tool, or the USD loader without
45
- dragging Shade in.
46
-
47
- ### 1.1 KHR is EXT plus three things
48
-
49
- The two extensions share a JSON shape, a `bufferView`/`buffer` model, fallback buffers, and modes
50
- 1 and 2 byte for byte. KHR adds:
51
-
52
- 1. **Attribute codec version 1** (header `0xa1`). A genuinely different bitstream for mode 0: a
53
- per-block control header, four control modes, an extra 1-bit sentinel encoding, and three
54
- *channel* modes that widen the delta from 8-bit to 16-bit or to a rotated 32-bit XOR.
55
- Version 0 (`0xa0`) remains legal under KHR and is bit-identical to EXT.
56
- 2. **Filter 4, `COLOR`** — YCoCg, `byteStride` 4 or 8.
57
- 3. One extra validity rule (`ATTRIBUTES` requires `byteStride >= 4`) and the constraint that a
58
- bufferView must not carry both extensions.
59
-
60
- So there is one codec that decodes everything, and a glTF layer that enforces the per-name subset:
61
- a `0xa1` stream or a `COLOR` filter under the `EXT_` name is an invalid file and is rejected.
62
-
63
- ## 2. Modules
64
-
65
- ### 2.1 `src/core/binary/meshopt/` — the codec
66
-
67
- | File | Export |
68
- |:--|:--|
69
- | `MeshoptCompressionMode.js` | `@enum` — `Attributes: 0`, `Triangles: 1`, `Indices: 2` |
70
- | `MeshoptCompressionFilter.js` | `@enum` — `None: 0`, `Octahedral: 1`, `Quaternion: 2`, `Exponential: 3`, `Color: 4` |
71
- | `MESHOPT_GROUP_SIZE.js` | the group size, and the 24-byte bound a group read stays inside |
72
- | `meshopt_decode_byte_group.js` | one group of 16 byte-deltas at a given sentinel width |
73
- | `meshopt_decode_data_block.js` | one "data block": header bits + its groups, for one byte position |
74
- | `meshopt_decode_vertex_buffer.js` | mode 0, versions 0 and 1 |
75
- | `meshopt_decode_index_buffer.js` | mode 1 |
76
- | `meshopt_decode_index_sequence.js` | mode 2 |
77
- | `meshopt_decode_filter_octahedral.js` | filter 1, in place |
78
- | `meshopt_decode_filter_quaternion.js` | filter 2, in place |
79
- | `meshopt_decode_filter_exponential.js` | filter 3, in place |
80
- | `meshopt_decode_filter_color.js` | filter 4, in place |
81
- | `meshopt_decode.js` | dispatch: mode, then filter |
82
- | `meshopt_read_uint_var.js` | bounded LEB128 over a `Uint8Array` |
83
- | `MESHOPT_VERTEX_BLOCK.js` | the attribute stream's block shape, shared by both sides |
84
- | `meshopt_encode_byte_group.js` | one group, at a width it prices out of the four on offer |
85
- | `meshopt_encode_data_block.js` | one data block: a width per group, then the header and the groups |
86
- | `meshopt_encode_vertex_buffer.js` | mode 0, versions 0 and 1 |
87
- | `meshopt_encode_index_sequence.js` | mode 2, choosing a baseline per index |
88
- | `meshopt_write_uint_var.js` | LEB128 into a `Uint8Array` at an offset |
89
-
90
- Signatures:
91
-
92
- ```js
93
- meshopt_decode_vertex_buffer(destination, count, byte_stride, source)
94
- meshopt_decode_index_buffer(destination, index_count, index_size, source)
95
- meshopt_decode_index_sequence(destination, index_count, index_size, source)
96
- meshopt_decode_filter_*(destination, count, byte_stride)
97
- meshopt_decode(destination, count, byte_stride, mode, filter, source)
98
- ```
99
-
100
- `destination` and `source` are `Uint8Array` throughout — the codec never assumes an aligned
101
- `ArrayBuffer` offset, because a compressed source range in a glTF is only byte-aligned. Index modes
102
- and filters build their typed views internally from `index_size` / `byte_stride`.
103
-
104
- Two additions outside the package, both genuinely generic and both matching meep's existing naming:
105
-
106
- - `src/core/math/round_half_away_from_zero.js` — `Math.round` breaks ties toward `+Infinity`
107
- (`Math.round(-2.5) === -2`). Four of the five filters specify `round()` on a signed symmetric
108
- quantity and round half away from zero. Sits beside the existing `round_up.js`.
109
- - nothing else: `msb_32` (`core/binary`) is already `findMSB` including its `-1` for zero, `exp2`
110
- and `copysign` (`core/math`) already exist, and `decode_octahedron_to_unit`
111
- (`core/geom/3d/normal/octahedron`) is reused by filter 1 — see §4.1.
112
-
113
- ### 2.2 `src/format/scene/gltf/ext/` — bufferView extensions, generically
114
-
115
- A bufferView extension says "these bytes are not in the buffer I point at; run this to get them".
116
- Meshopt is two of them; a future codec would be a third. The parser should know that shape and
117
- nothing else, so the layer is split in two — see §9 for why this is not how it was first written.
118
-
119
- | File | What it is |
120
- |:--|:--|
121
- | `GltfBufferViewExtension.js` | the contract one extension implements: a `name`, `decode()`, and whether it declares placeholder buffers |
122
- | `GltfBufferViewExtensionSet.js` | which extensions a parser honours, and `claim(view)` — the *only* thing that decides whether anything happens |
123
- | `gltf_apply_buffer_view_extensions.js` | the walk: claim, delegate, append the buffer, replace the descriptor. Identical for every extension |
124
- | `gltf_buffer_view_extensions.js` | `GLTF_BUFFER_VIEW_EXTENSIONS`, the set the parser uses by default |
125
- | `meshopt/MeshoptBufferViewExtension.js` | one class, two instances: `EXT_MESHOPT_COMPRESSION`, `KHR_MESHOPT_COMPRESSION` |
126
-
127
- The walker:
128
-
129
- 1. returns a **new** `bufferViews` array — claimed entries are replaced by fresh descriptors,
130
- unclaimed entries pass through **by reference**,
131
- 2. **appends** each decoded buffer to `buffers`, pointing the new descriptor at it with
132
- `byteOffset: 0`,
133
- 3. keeps `byteStride`, `target` and everything else from the parent, so accessor reads are unchanged,
134
- 4. refuses a decoder that answered with the wrong number of bytes, whichever decoder it was.
135
-
136
- Returning a new array rather than mutating the old descriptors is load-bearing, not taste — §6.3.
137
-
138
- The difference between the two meshopt instances is two fields and no branches: which filter names
139
- the extension defines, and the newest codec version each *mode* may carry. Per mode because both
140
- index codecs' version 1 is legal under either extension; it is only the attribute codec that the
141
- vendor extension pins at 0.
142
-
143
- ### 2.3 `tiny-gltf.js` — the call site
144
-
145
- Three edits, all in `loadFromJson`:
146
-
147
- - **`~L422-448`, buffer resolution.** Today a buffer with no `uri` outside the GLB fast path is fed
148
- to `resolveUri(undefined, baseUrl)` and fetched as `<baseUrl>undefined`. That is exactly the shape
149
- of a meshopt *fallback* buffer (`{ "byteLength": N }`, no URI), so today a `gltfpack`-produced
150
- `.gltf` cannot load at all. A buffer that is marked `fallback` or has no `uri` resolves to a
151
- zero-length `ArrayBuffer` instead of a request (§6.8). Also: the GLB branch currently loads *only*
152
- the binary chunk and ignores `buffers[1..n]`, which is where both extensions put the placeholder —
153
- the branch becomes "buffer 0 is the chunk, the rest resolve normally".
154
- - **`~L524-534`, an image sourced from a bufferView.** Throw if that bufferView is compressed. The
155
- combination is legal in the abstract and pointless in practice (the payload is already a PNG or a
156
- JPEG); the alternative to throwing is handing `createImageBitmap` the fallback buffer's bytes,
157
- which is a silent wrong answer.
158
- - **`~L557`, after `await Promise.all(pendingBuffers)`.** One call to
159
- `gltf_apply_buffer_view_extensions` before `json.buffers` and `json.bufferViews` are published.
160
-
161
- The parser names no extension: it asks `this.bufferViewExtensions` — a public field, defaulted to the
162
- standard set and replaceable — and the set is the one place that knows what is implemented.
163
-
164
- **Nothing in `load_gltf.js` changes.** It reads `gltf.bufferViews` / `gltf.buffers` inside
165
- `process()`, which runs after `loadFromJson` has returned, so it sees decoded views and never learns
166
- either extension exists. That is the point of doing this at the bufferView layer.
167
-
168
- ## 3. The bitstream, as implemented
169
-
170
- Written from the two specifications' Appendix A/B. Notes marked ⚠ are places where a plausible
171
- reading is wrong.
172
-
173
- ### 3.1 Mode 0 — attributes
174
-
175
- ```
176
- header 1 byte: 0xa0 (version 0) or 0xa1 (version 1, KHR only)
177
- blocks one or more attribute blocks
178
- tail padding zero bytes
179
- tail block baseline element (byte_stride bytes)
180
- channel modes (byte_stride / 4 bytes, version 1 only)
181
- ```
182
-
183
- ⚠ **The padding comes BEFORE the tail block; the tail block is flush against the end of the
184
- stream.** So the baseline element starts at `byteLength - tail_size`, where
185
-
186
- ```
187
- tail_size = byte_stride + (version === 1 ? byte_stride / 4 : 0)
188
- tail_size_pad = max(tail_size, version === 1 ? 24 : 32)
189
- ```
190
-
191
- and the decoder must have consumed exactly `byteLength - tail_size_pad` bytes when the blocks are
192
- done. EXT's wording — "a baseline element stored verbatim, padded to 32 bytes" — reads equally well
193
- as element-then-padding, which for any stride below 32 puts the baseline in the wrong place and
194
- decodes garbage. KHR's rewording ("Tail padding, which pads the size of the *subsequent* tail
195
- block…") settles it, and the reference decoder agrees: it takes the baseline from
196
- `data_end - tail_size`, with `tail_size` **unpadded**, while validating the remainder against
197
- `tail_size_pad`. Two different quantities that coincide only when `byte_stride >= 32`.
198
-
199
- Block loop, identical in both versions:
200
-
201
- ```
202
- max_block_elements = min(floor(8192 / byte_stride) & ~15, 256)
203
- block_elements = min(remaining, max_block_elements)
204
- group_count = ceil(block_elements / 16)
205
- ```
206
-
207
- ⚠ `8192 / byte_stride` is integer division — `floor` before the mask. With `byte_stride <= 256` the
208
- result is never below 32, so the loop always advances.
209
-
210
- A block is, in order:
211
-
212
- - a **control header** of `byte_stride / 4` bytes — **version 1 only**,
213
- - `byte_stride` **data blocks**, one per byte position `k` of the element.
214
-
215
- The control header packs a 2-bit *control mode* per byte position, low bits first:
216
- `(c0 << 0) | (c1 << 2) | (c2 << 4) | (c3 << 6)`, one byte per aligned group of four byte positions.
217
- The control mode selects what the data block for that byte position looks like:
218
-
219
- | control mode | data block |
220
- |:--|:--|
221
- | 0 | header bits, group encodings `{zero, 1-bit, 2-bit, 4-bit}` |
222
- | 1 | header bits, group encodings `{1-bit, 2-bit, 4-bit, raw}` |
223
- | 2 | nothing at all — every delta byte is 0, no header bits, no bytes consumed |
224
- | 3 | literal: `group_count * 16` delta bytes, no header bits |
225
-
226
- Version 0 has no control header and behaves as a fixed table
227
- `{zero, 2-bit, 4-bit, raw}` for every byte position.
228
-
229
- Where header bits are present they are `ceil(group_count / 4)` bytes, 2 bits per group, packed
230
- **low bits first**: `(g0 << 0) | (g1 << 2) | (g2 << 4) | (g3 << 6)`. Group encodings:
231
-
232
- | encoding | packed bytes | sentinel | total |
233
- |:--|:--|:--|:--|
234
- | zero | 0 | — | 0 |
235
- | 1-bit | 2 | `1` | 2..18 |
236
- | 2-bit | 4 | `3` | 4..20 |
237
- | 4-bit | 8 | `15` | 8..24 |
238
- | raw | 16 | — | 16 |
239
-
240
- ⚠ Bit order inside a packed byte is **not uniform**. 2-bit and 4-bit run high bits first —
241
- `(d3 << 0) | (d2 << 2) | (d1 << 4) | (d0 << 6)` and `(d1 << 0) | (d0 << 4)` — while **1-bit runs low
242
- bits first**, `(d0 << 0) | (d1 << 1) | … | (d7 << 7)`. The spec says so explicitly (it is there to
243
- let SIMD implementations reuse a lookup table); it is not a transcription slip.
244
-
245
- A delta equal to the sentinel means the real value is outside the bit range and is taken as a whole
246
- byte from the run that follows the packed bytes, in order of appearance.
247
-
248
- The delta bytes so recovered are then turned into values by the **channel mode**. Channels are
249
- aligned groups of four byte positions; version 1 stores one mode byte per channel in the tail, and
250
- version 0 behaves as though every channel were mode 0.
251
-
252
- | channel mode | reconstruction |
253
- |:--|:--|
254
- | 0 | per byte: `v = (unzigzag8(d) + prev) & 0xff` |
255
- | 1 | per aligned 2 bytes, little-endian: `v = (unzigzag16(d) + prev) & 0xffff` |
256
- | 2 | per aligned 4 bytes, little-endian: `v = rotl32(d, r) ^ prev` |
257
-
258
- `r` is the **high** 4 bits of the channel mode byte and applies only to mode 2. ⚠ The decoder
259
- rotates **left by `r`** — the spec's `rotate(v, r) = (v << r) | (v >> ((32 - r) & 31))` is the
260
- decode direction, despite the surrounding sentence describing how the encoder produced the delta.
261
- A mode byte whose low nibble is 3 or above, or whose high nibble is non-zero for mode 0 or 1, makes
262
- the stream invalid.
263
-
264
- Groups always decode sixteen elements; anything past `block_elements` is discarded and does not
265
- advance the delta chain. Each block ends by taking its **last decoded element** as the next block's
266
- baseline.
267
-
268
- The specification's own worked example is the acceptance test for the group decoder (§5.1).
269
-
270
- ### 3.2 Mode 1 — triangles
271
-
272
- ```
273
- header 1 byte, (h & 0xf0) == 0xe0; version = h & 0x0f, 0 or 1 (both specs pin 0xe1)
274
- codes index_count / 3 bytes, one per triangle
275
- data variable, immediately after the codes
276
- codeaux the last 16 bytes
277
- ```
278
-
279
- State: `next` (high-watermark, 0), `last` (0), a 16-entry edge FIFO and a 16-entry vertex FIFO, both
280
- filled with `0xFFFFFFFF`. `data` must end exactly at the start of `codeaux` — that equality is the
281
- stream's own checksum.
282
-
283
- `fec_max` is 13 for version 1 and 15 for version 0; it is what makes codes `0xXd`/`0xXe` mean
284
- `last - 1` / `last + 1` rather than two more FIFO slots. Reading it from the header rather than
285
- hard-coding 13 is why both versions decode correctly.
286
-
287
- ⚠ The two branches index the vertex FIFO differently: the `code < 0xf0` path reads
288
- `(offset - 1 - fec) & 15`, the `0xf0..0xff` paths read `(offset - feb) & 15`. This follows from the
289
- spec's own wording ("index Y" versus "index Z-1") and is not a slip.
290
-
291
- ⚠ A conditional push writes the value at `offset` and then does not advance `offset`. Skipping the
292
- write as well is *not* equivalent: every later FIFO read at distance `d` would see what belongs at
293
- `d - 1`. Two tests reach back past a vertex that was not pushed, because until something does the
294
- difference is invisible.
295
-
296
- ⚠ The explicit indices in the `0xfe`/`0xff` branch are read in the order a, b, c, and `last` chains
297
- through them, so that order is load-bearing. Their position relative to the `next++` allocations is
298
- *not*: an index is either fresh or explicit, never both, so the two passes do not interfere. (The
299
- plan asserted otherwise; see §8.)
300
-
301
- ### 3.3 Mode 2 — indices
302
-
303
- ```
304
- header 1 byte, (h & 0xf0) == 0xd0 (both specs pin 0xd1)
305
- deltas one LEB128 varint per index
306
- tail 4 reserved bytes
307
- ```
308
-
309
- `v & 1` selects one of two running baselines; `v >> 1` is a zigzag delta applied to it. Two
310
- baselines rather than one is what lets an encoder splice two monotonic runs (strips, line lists)
311
- cheaply. Again `data` must land exactly on the 4-byte tail.
312
-
313
- ### 3.4 Filters
314
-
315
- All five are in place over the already-decoded bufferView, `count` elements of `byte_stride` bytes.
316
-
317
- - **Octahedral** — `byte_stride` 4 → `Int8Array`, 8 → `Int16Array`. Components 0 and 1 are the
318
- octahedral pair, component 2 encodes 1.0 at the element's own precision `K`, component 3 passes
319
- through untouched. Output scaled by `max` = 127 or 32767. Reuses meep's own
320
- `decode_octahedron_to_unit` — see §4.1.
321
- - **Quaternion** — `byte_stride` 8, `Int16Array`. `one = input[3] | 3`,
322
- `max_component = input[3] & 3`; outputs land at `(max_component + 1..3) & 3` for x/y/z and
323
- `(max_component + 0) & 3` for w.
324
- ⚠ `input[3]` is read for *both* purposes before any output is written — one of the four writes
325
- lands on it.
326
- ⚠ The radicand `1 - x² - y² - z²` is clamped at 0. meep already learned this in
327
- `quat3_decode_from_uint32`; a NaN quaternion out of a loader poisons every transform downstream.
328
- - **Exponential** — `byte_stride` divisible by 4. Each 32-bit word is `e = word >> 24` (signed),
329
- `m = (word << 8) >> 8` (signed); the result `2^e · m` is written back as a float32. `e` is
330
- spec-bounded to [-100, 100] and `|m| < 2^23`, so the product is exact in float32 and computing it
331
- in JS doubles and storing through a `Float32Array` is bit-identical to the reference's
332
- build-the-exponent-and-multiply.
333
- - **Color** (KHR only) — `byte_stride` 4 → `Uint8Array`, 8 → `Uint16Array`. YCoCg:
334
-
335
- ```
336
- as = (1 << (msb_32(input[3]) + 1)) - 1
337
- r = input[0] + int(input[1]) - int(input[2])
338
- g = input[0] + int(input[2])
339
- b = input[0] - int(input[1]) - int(input[2])
340
- a = input[3] & (as >> 1); a = (a << 1) | (a & 1)
341
- output = round(component * (UINTN_MAX / as))
342
- ```
343
-
344
- ⚠ Components 0 and 3 are **unsigned**, components 1 and 2 are **signed**, in the same element.
345
- `msb_32` already returns `-1` for zero, so a malformed `input[3] === 0` yields `as = 0`, an
346
- infinite scale, and finite garbage bytes once stored through a `Uint8Array` — defined, ugly, and
347
- not worth a guard (§6.9).
348
-
349
- ## 4. What is reused, and what deliberately is not
350
-
351
- ### 4.1 Reused
352
-
353
- - **`decode_octahedron_to_unit`** (`core/geom/3d/normal/octahedron`) implements filter 1's core.
354
- meep's fold is the same arithmetic as the spec's `copysign` form (`x + t·sign(x)` either way) and
355
- it renormalises, but it is written **Y-up**: meep's `(x, y, z)` is the spec's `(x, z, y)`. The
356
- swizzle is the whole of the adaptation, and a spec checks the composition against the
357
- specification's formula directly rather than against meep's.
358
- - **`msb_32`** (`core/binary`) is `findMSB` for filter 4, `-1`-on-zero included.
359
- - **`exp2`** and **`copysign`** (`core/math`).
360
- - **`gltf_test_fixtures.js`** (`engine/asset/loaders`) already builds a one-triangle glTF and GLB by
361
- hand; the end-to-end fixture extends it rather than introducing a second fixture vocabulary.
362
-
363
- ### 4.2 Deliberately not
364
-
365
- - **`BinaryBuffer.readUintVar`** is the same LEB128 scheme and reusing it would honour "use meep's
366
- existing code". It is still wrong here: it reads through a `DataView` bound to a whole
367
- `ArrayBuffer` where the compressed range is a slice, it is unbounded on malformed input where the
368
- reference stops at five bytes, and `value |= (b & 0x7f) << shift` turns negative once `shift`
369
- reaches 28 — which is inside the range mode 1 and mode 2 indices legitimately use. The deviation
370
- and its three reasons are documented where `meshopt_read_uint_var` is defined, so the next reader
371
- does not "fix" the duplication.
372
- - **`quat3_decode_from_uint32`** is a different packing — 10-bit, fixed range, dropped component
373
- inserted *at* index `max` — where filter 2 is 16-bit, variable precision and *cyclic*. Documented
374
- as deliberately not reused rather than left to look overlooked.
375
- - **`Vector3` / `Quaternion` / `vec3` helpers.** The filters run over hundreds of thousands of
376
- elements through typed-array views; routing each through an object allocation would be the one
377
- place in this feature where reuse costs more than it is worth. Scalars, module-static scratch.
378
-
379
- ## 5. Tests
380
-
381
- Every module gets a `.spec.js` beside it. House rules apply: black-box, no mocks, and no rejection
382
- tests against `assert.*` (stripped in production) — everything that must reject bad *file* input
383
- does so with a real `throw`, and those are tested.
384
-
385
- ### 5.1 The independent oracle
386
-
387
- The single most valuable test is free: **Appendix A of both specifications contains a worked
388
- example** of the 4-bit sentinel encoding —
389
-
390
- ```
391
- 0x17 0x5f 0xf0 0xbc 0x77 0xa9 0x21 0x00 0x34 0xb5
392
- -> -1 -4 -3 26 -91 0 -6 6 -4 -4 5 -5 1 -1 0 0
393
- ```
394
-
395
- — which pins the packed-nibble order, the sentinel value, the sentinel run's position and its
396
- ordering, and the zigzag, all at once, from a document that is not this implementation. It is the
397
- first test written, and the group decoder is developed against it. (Checked by hand while writing
398
- this plan: the nibbles are `1 7 5 S S 0 11 12 7 7 10 9 2 1 0 0`, the two sentinels take `0x34` and
399
- `0xb5`, and de-zigzagging reproduces the stated line exactly.)
400
-
401
- ### 5.2 Hand-derived vectors
402
-
403
- The remaining streams are small enough to author by hand from the spec, with the derivation in the
404
- test as a comment. Worked out in advance so this plan can be checked before any code exists:
405
-
406
- - **mode 0 v0, all-zero groups.** `byte_stride` 4, `count` 3 → one block, one group, four data
407
- blocks each a single `0x00` header byte: `[0xa0, 0x00, 0x00, 0x00, 0x00]` then 28 zero bytes then
408
- the 4 baseline bytes. All three elements equal the baseline. This is the test that fails if the
409
- tail is read from the wrong end (§3.1 ⚠) — under the element-then-padding reading the baseline
410
- comes out `0,0,0,0`.
411
- - **mode 0 v1, control modes.** The same shape with header `0xa1`, one control byte, and one tail
412
- channel-mode byte: control mode 2 (nothing stored) on every byte position; then control mode 3
413
- (literal) with hand-written delta bytes; then control mode 0 with a 1-bit sentinel group, which
414
- is the only place the low-bits-first packing is exercised.
415
- - **mode 0 v1, channel modes 1 and 2.** `byte_stride` 4, one channel: a 16-bit delta pair that
416
- wraps modulo 65536, and a rotated XOR whose `r` is non-zero — the two places where a
417
- left-versus-right rotation or a byte-order slip is invisible at `r = 0`.
418
- - **mode 1, one triangle.** `[0xe1, 0xf0]` + 16 `codeaux` bytes with `codeaux[0] = 0x00` →
419
- `feb = fec = 0` → `a, b, c = next++, next++, next++` → `[0, 1, 2]`. 18 bytes, `1 + 6/3 + 16`;
420
- `data` lands exactly on `codeaux`.
421
- - **mode 1, two triangles sharing an edge.** Append code `0x00`: after triangle one the edge FIFO
422
- holds `(1,0), (2,1), (0,2)` at offset 3, so `fe = 0` reads `(0, 2)` and `fec = 0` takes
423
- `c = next = 3` → `[0, 1, 2, 0, 2, 3]`. This is the test that fails if the edge-FIFO push order is
424
- wrong.
425
- - **mode 1, the `0xXf` and `0xfe`/`0xff` paths** — explicit indices through `decodeIndex`, and the
426
- `codeaux == 0` restart that resets `next`.
427
- - **mode 2.** `[0xd1, 0x14, 0x08, 0, 0, 0, 0]` → `0x14 = 20`: baseline 0, zigzag 10, delta +5 → `5`;
428
- `0x08 = 8`: baseline 0, zigzag 4, delta +2 → `7`. Output `[5, 7]`. Plus a vector that alternates
429
- the baseline bit, which is the whole reason mode 2 exists.
430
- - **filters.** Values computed from each spec's own pseudo-code in the test, not from the
431
- implementation: unit ±X/±Y/±Z normals through octahedral at both strides, identity and
432
- each-of-four-`max_component` quaternions, exponential words with negative exponents, negative
433
- mantissas and `m = 0`, and for colour a grey, a saturated primary and a partial alpha at both
434
- strides and at a `K` below the component width.
435
- - **rejection.** Truncated streams, wrong header bytes, a `0xa1` stream offered under the `EXT_`
436
- name, a `COLOR` filter under the `EXT_` name, an invalid channel mode byte, and a mode 1 stream
437
- whose `data` does not land on `codeaux`.
438
-
439
- ### 5.3 Round trip
440
-
441
- A **test-only** mode 0 encoder in the spec file (raw-byte groups only, the simplest legal encoding)
442
- gives a randomised round trip over strides 4/8/12/16/64 and counts that straddle the 16-element
443
- group and 256-element block boundaries. It is deliberately *secondary*: it shares the layout
444
- assumptions of the decoder, so a shared misreading passes. Its job is exercising the block and group
445
- loop bounds, not proving the format. §5.1 and §5.2 prove the format.
446
-
447
- ### 5.4 End to end
448
-
449
- `gltf_test_fixtures.js` gains `build_meshopt_triangle_gltf_json(extension_name, buffer_uri, options)`
450
- and a matching `_glb`: the one-triangle model with its four bufferViews compressed (positions, normals and
451
- uvs in mode 0, indices in mode 1) by the fixture's own minimal encoder, a `fallback: true`
452
- placeholder buffer with no URI, and `extensionsUsed` / `extensionsRequired` set. A spec then loads
453
- it through the real `AssetManager` and `GLTFSceneBundleAssetLoader` — as
454
- `GLTFSceneBundleAssetLoader.spec.js` already does — under **both** extension names, and asserts the
455
- resulting bundle's geometry equals the uncompressed fixture's. That covers the fallback-buffer path,
456
- the bufferView rewrite, and the fact that `load_gltf.js` needs no changes.
457
-
458
- That file, though, is one the *vendor* extension could have described just as well — the Khronos name
459
- on it and nothing else. `options` makes it a file only KHR defines: attribute streams at codec
460
- version 1 carrying a channel mode each (a rotated exclusive-or over the positions, byte deltas over
461
- the normals, 16-bit deltas over the texture coordinates), and a `COLOR`-filtered `COLOR_0` the mesh
462
- reader maps to `StandardAttributes.Color`. Each of the two is served on its own as well as together,
463
- so that a test watching the vendor extension refuse one is refusing *that* one — asserting against a
464
- file carrying both passes on either constraint, and then removing one of them from the vendor
465
- extension goes unnoticed.
466
-
467
- ### 5.5 What none of this covers
468
-
469
- No test here decodes a stream produced by `gltfpack`. Every byte under test was authored from a
470
- specification by me or by a fixture encoder I also wrote; a misreading shared between them survives.
471
- §5.1 is the only genuinely external oracle, and it covers one group encoding of one mode.
472
-
473
- **Recommendation to the sponsor: drop one `gltfpack -noq -cc` output (and one `-cc -vv` for codec
474
- version 1) into the repo, and add a golden test that decodes it and checks every `POSITION`
475
- accessor's values against that accessor's declared `min`/`max`, and every index against the vertex
476
- count.** Those bounds are written by the encoder, are not derivable from the compressed bytes, and
477
- would catch a systematic misreading immediately. Left out of this plan because it needs files only
478
- the sponsor can supply.
479
-
480
- ## 6. Adversarial review of this plan
481
-
482
- Each item is a way this plan is wrong or insufficient, and what it changes.
483
-
484
- ### 6.1 The tail was read from the wrong end
485
-
486
- The first draft of this plan had the baseline element at the **start** of a `max(stride, 32)`-byte
487
- tail, from EXT's "a baseline element stored verbatim, padded to 32 bytes", and cited a reading of
488
- the reference decoder that appeared to confirm it. Both were wrong, and for any `byteStride` below
489
- 32 — which is every attribute meshopt is actually used on — it decodes an entire model to garbage
490
- while every unit test written from the same misreading passes.
491
-
492
- What caught it was reading KHR, which reworded the same clause to "Tail padding, which pads the size
493
- of the *subsequent* tail block…" and added a second item to the tail block that forces the question.
494
- The corroborating detail is that the reference keeps two quantities, `tail_size` and
495
- `tail_size_pad`, uses the **unpadded** one to locate the baseline and the **padded** one to validate
496
- the remainder — a distinction that has no meaning at all under the wrong reading.
497
-
498
- **Changes:** §3.1 states both quantities and where each is used; §5.2's first vector is chosen
499
- specifically to discriminate them; and the general lesson is written into §6.11.
500
-
501
- ### 6.2 `Uint8Array` out-of-bounds reads return `undefined`, and `undefined & 0xff` is `0`
502
-
503
- The worst failure mode in the design. JavaScript does not fault on a typed-array read past the end —
504
- it yields `undefined`, and every bitwise operator coerces that to `0`. A truncated or malformed
505
- stream therefore decodes to *plausible-looking zeros* rather than an error, and the model loads
506
- wrong instead of failing. The reference is bounds-checked at every group and every index read for
507
- exactly this reason.
508
-
509
- **Change:** explicit bounds checks are not optional, and are not `assert` (stripped in production —
510
- this input is a file, not a caller). Real `throw` at the header, at each byte group against the
511
- 24-byte decode limit, at each literal and control-header read, at each index varint, and at the
512
- final data-lands-on-the-tail equality in all three modes. Reject-a-truncated-stream cases added to
513
- §5.2.
514
-
515
- ### 6.3 Mutating the bufferView descriptors races the image loader
516
-
517
- The first draft rewrote `json.bufferViews[i]` in place. `loadFromJson` builds the image promises
518
- *before* buffers resolve, and each one closes over its `bufferView` object and reads `.byteOffset` /
519
- `.byteLength` inside a `.then()` that runs *after* the rewrite. Mutating in place changes what an
520
- already-scheduled image read sees, from a different code path, with no visible link between the two.
521
-
522
- **Change:** §2.2 returns a new array of new descriptors and leaves the originals untouched. The
523
- image path keeps reading exactly what it reads today, whatever order the promises settle in. This is
524
- the reason for a design choice that otherwise looks like needless allocation, and it is recorded
525
- where the code lives.
526
-
527
- ### 6.4 `Math.round` is not the spec's `round`
528
-
529
- `Math.round(-2.5)` is `-2`. The filters round signed symmetric quantities and the reference rounds
530
- half away from zero. Half a quantisation step on normals is invisible in a screenshot and visible in
531
- a diff against any other decoder — exactly the kind of drift nobody attributes correctly six months
532
- later.
533
-
534
- **Change:** `round_half_away_from_zero` in `core/math`, used by every filter, with the asymmetry
535
- named in its JSDoc.
536
-
537
- ### 6.5 A green suite here does not mean a `gltfpack` file loads
538
-
539
- Two things sit between this feature and a real compressed asset, and neither is in scope:
540
-
541
- - **`KHR_mesh_quantization`.** `gltfpack`'s default output quantises as well as compresses, and meep
542
- does not implement that extension. Worse, quantised attributes are padded — a `VEC3`/`SHORT`
543
- position gets `byteStride` 8, not 6 — and `load_gltf.js` rejects any bufferView whose `byteStride`
544
- differs from `componentSize × componentCount` with `'Interleaved attributes are not supported'`.
545
- So a default `gltfpack` file will fail *after* decompressing correctly.
546
- - **Interleaved attributes**, same rejection, for the same reason.
547
-
548
- `gltfpack -noq -cc` produces meshopt-compressed, non-quantised, non-interleaved output, and that is
549
- the configuration this feature makes work. Stated here rather than discovered later; picking up
550
- `KHR_mesh_quantization` is separate work and is not silently folded into this one.
551
-
552
- ### 6.6 Pure JS is roughly an order of magnitude off the reference
553
-
554
- The reference reaches ~1 GB/s with WebAssembly SIMD. This will not, and the extension exists partly
555
- *because* decode is meant to be nearly free. Decompression is synchronous inside `loadFromJson`, so
556
- it lands on whichever thread is parsing — the main thread for a foreground load.
557
-
558
- Accepted, not designed around: the alternative is a WASM dependency, which the brief excludes.
559
- Mitigated by the shape the reference already uses — a module-static 8 KiB delta scratch (one
560
- attribute block's worth: `byte_stride × max_block_elements <= 8192` by construction) and a 256-byte
561
- group buffer, so the strided writes stay in cache and there is no per-block allocation. No
562
- performance claim is made without a measurement, and none has been taken.
563
-
564
- ### 6.7 The extension's `count` is not the accessor's `count`
565
-
566
- `count` is the number of *stride-sized elements in the bufferView*; for index modes that is the
567
- index count, not the triangle count. Reading it as either is a silent 3× error that only shows on
568
- non-triangle-aligned data.
569
-
570
- **Change:** each spec's own invariants become the validation list, checked before decode and thrown
571
- on. Parent `byteLength === byteStride × count`; parent `byteStride`, when present, equals the
572
- extension's; `ATTRIBUTES` → stride divisible by 4, `>= 4` (KHR) and `<= 256`; `TRIANGLES` → count
573
- divisible by 3; `TRIANGLES`/`INDICES` → stride 2 or 4 and filter absent or `NONE`; `OCTAHEDRAL` →
574
- stride 4 or 8; `QUATERNION` → stride 8; `EXPONENTIAL` → stride divisible by 4; `COLOR` → stride 4
575
- or 8 **and** the KHR name. Plus the source range lying inside the source buffer, which both specs
576
- leave implicit, and the KHR rule that a bufferView must not carry both extension names.
577
-
578
- ### 6.8 The fallback buffer is allocated at its declared `byteLength`
579
-
580
- Both specs require a placeholder large enough to hold every uncompressed view referencing it — a
581
- megabyte-scale allocation for a real model, of a buffer that by construction nothing reads once the
582
- views are rewritten.
583
-
584
- **Change:** it resolves to a **zero-length** buffer. A well-formed file never touches it; a
585
- malformed one — a view referencing the fallback without carrying the extension — fails loudly on an
586
- out-of-range typed-array construction, which is the correct outcome and is preferable to quietly
587
- handing out a megabyte of zeros that renders as a degenerate mesh.
588
-
589
- ### 6.9 Version and name policy has three different answers, and uniformity would be a bug
590
-
591
- The temptation is one rule for all three headers. They are not the same:
592
-
593
- - Mode 0 version 1 is a *different bitstream*, so decoding `0xa1` as `0xa0` is a silent wrong
594
- answer. It is supported, and it is rejected under the `EXT_` name, which does not define it.
595
- - Mode 1 and mode 2 versions 0 and 1 differ only in `fec_max`, which is read from the header, so
596
- both decode correctly and both are accepted under either name.
597
- - Filter `COLOR` is accepted only under the `KHR_` name.
598
-
599
- And two decode-time hazards are left alone on purpose, because the specs declare them unspecified
600
- and JavaScript's typed arrays already contain the damage: a colour element whose fourth component is
601
- zero (infinite scale, finite garbage bytes on store) and a quaternion whose retained components
602
- exceed unit length (already clamped at the radicand). Neither can produce a `NaN` that escapes into
603
- the scene, and neither gets a guard.
604
-
605
- ### 6.10 Nothing checks `extensionsRequired`
606
-
607
- The loader ignores `extensionsRequired` entirely, so a file requiring an extension meep does not
608
- implement loads and renders wrong. Pre-existing and out of scope — noted so it is not mistaken for
609
- something this change introduced or fixed.
610
-
611
- ### 6.11 The residual risk this plan cannot retire
612
-
613
- Three details are taken from questions put to the reference implementation rather than from
614
- specification text, and one is taken from an inference:
615
-
616
- - literal control mode 3 consumes the **group-aligned** element count per byte position, not the
617
- block's unaligned count (inferred: all four control modes must agree on the scratch stride);
618
- - channel mode 2 rotates **left** by `r` on decode;
619
- - the version 1 tail-padding minimum is 24 bytes, not 32.
620
-
621
- Each is a silent-garbage failure if wrong, and none is discriminated by any test in §5 — a fixture
622
- encoder written from the same understanding agrees with the decoder. They are the specific reason
623
- §5.5's ask matters: one real `gltfpack -cc -vv` file settles all three at once. Until there is one,
624
- **version 1 support is implemented and tested but not evidenced**, and this paragraph is the honest
625
- statement of that.
626
-
627
- ## 7. Commit sequence
628
-
629
- Each commit is independently green (`npm run test` from `H:/git/moh`).
630
-
631
- 1. `feat(math): rounding that breaks its ties away from zero`
632
- 2. `feat(binary): meshopt's attribute codec, against the bytes the spec itself decodes`
633
- — mode 0 version 0, the group and channel decoders, the varint, the two enums, specs.
634
- 3. `feat(binary): the attribute codec's second version, and the channels it decodes through`
635
- — mode 0 version 1: control header, control modes, 1-bit groups, channel modes, specs.
636
- 4. `feat(binary): meshopt's two index codecs, and the tail that checks them`
637
- — modes 1 and 2, specs.
638
- 5. `feat(binary): four meshopt filters, one of them meep's own octahedron`
639
- — filters 1/2/3/4, specs.
640
- 6. `feat(binary): one entry point per compressed buffer view`
641
- — `meshopt_decode` dispatch, spec.
642
- 7. `feat(gltf): a compressed buffer view becomes an ordinary one`
643
- — the binding module and its validation, under both extension names, spec.
644
- 8. `fix(gltf): a buffer with no URI is a placeholder, not a request`
645
- — the tiny-gltf buffer loop, GLB buffers past the chunk.
646
- 9. `feat(gltf): the loader decodes compressed views before an accessor reads one`
647
- — the `loadFromJson` call, the compressed-image-view throw, the end-to-end fixture and spec.
648
-
649
- ## 8. What execution changed
650
-
651
- Written after the work landed. The plan above is corrected in place where it was factually wrong
652
- about the format; everything else is recorded here rather than edited away, because a plan that
653
- quietly matches its outcome is not evidence of anything.
654
-
655
- ### 8.1 The plan was wrong about the `0xfe`/`0xff` ordering
656
-
657
- §3.2 claimed that computing and explicitly reading each of the three indices in turn "diverges on
658
- the first triangle that mixes them". It does not. An index in that branch is either fresh — and
659
- takes a turn at `next` — or explicit, and takes a varint; never both. The two passes are disjoint
660
- per index, so interleaving them is equivalent.
661
-
662
- Found by mutation: rewriting the branch to interleave them left all fifty-four tests green, and the
663
- question "which test should have caught this?" has the answer "none, because there is nothing to
664
- catch". What *is* order-dependent is the sequence of the explicit reads, because `last` chains
665
- through them; a test with a codeaux of `0xff`, where all three indices are explicit, now pins it.
666
-
667
- §3.2 also conflated two things about the conditional FIFO push. The *write* at a non-advancing
668
- offset is unobservable (no well-formed stream reads distance 16). The *non-advance* is very much
669
- observable, at any distance a later code reaches. Two tests now do.
670
-
671
- ### 8.2 A specification erratum, in both documents
672
-
673
- Of the three worked examples both extensions print for the varint encoding,
674
-
675
- ```
676
- 0x7f => 0x7f
677
- 0x81 0x04 => 0x201
678
- 0xff 0xa0 0x05 => 0x1fd005
679
- ```
680
-
681
- the third is wrong. Those bytes are the LEB128 encoding of `0x1fd005` written back to front; read as
682
- LEB128 they are `0x1507f`, and `0x1fd005` encodes as `0x85 0xa0 0x7f`. The example only works if the
683
- groups are read most significant first, which contradicts the example above it, the encoding the
684
- document names, and the reference implementation.
685
-
686
- `meshopt_read_uint_var.spec.js` pins both readings of both byte strings so that nobody later "fixes"
687
- the reader to match the printed line.
688
-
689
- ### 8.3 Two things the plan claimed mattered, which cannot be observed
690
-
691
- Both were found by mutating the implementation and watching the suite stay green, and both are now
692
- written down where they live rather than left to look like tested behaviour.
693
-
694
- - **§6.4, the rounding mode.** `round_half_away_from_zero` cannot be told from `Math.round` through
695
- any of the four filters. A renormalised octahedral component is a ratio with an irrational
696
- denominator and never lands halfway between two integers — checked exhaustively over the whole
697
- 8-bit input space, 8388477 component values, no ties — and the quaternion filter's scale is
698
- irrational for the same reason. The colour filter's scale is rational, but its inputs are
699
- non-negative for any valid element, and a positive tie rounds the same way either way. The helper
700
- is kept because it is what the specifications say; the claim that a test proves it is withdrawn,
701
- and a standing sweep now asserts the no-ties property that makes it moot.
702
- - **§3.4, the quaternion radicand's clamp.** The output goes through an `Int16Array`, and storing a
703
- `NaN` there yields the same zero the clamp produces. Kept so the function is total on its own
704
- rather than by a coincidence thirty lines downstream — which is exactly the kind of thing a
705
- refactor removes — but nothing tests it, and the spec now says so.
706
-
707
- ### 8.4 Structure
708
-
709
- - Planned commits 2 and 3 (attribute codec versions 0 and 1) landed as one. The block loop, the
710
- group decoder and the channel reconstruction are shared; splitting them would have meant writing
711
- the module twice.
712
- - The test-only encoder §5.3 put inline in a spec file became `__meshopt_test_streams.js`, because
713
- the end-to-end glTF fixtures need the same streams. `**/__**.js` is already how meep's coverage
714
- configuration spells "scaffolding" — but the package's `files` list is not the coverage
715
- configuration, and it drops `*.spec.js` and `__fixtures__/` and nothing else, so the file shipped
716
- to every consumer of the engine.
717
-
718
- Since replaced by a real encoder (`meshopt_encode_vertex_buffer.js` and
719
- `meshopt_encode_index_sequence.js`), which belongs in the package on its own merits and leaves the
720
- specs with nothing test-only to share. The encoder decides what the decoder was only ever told:
721
- a width per group, a control mode per byte position, a mode per channel — each priced against the
722
- alternatives rather than fixed, where the test-only encoder stored every group raw.
723
-
724
- What it does not cover: mode 1, the triangle codec, whose encoder is a different algorithm
725
- (vertex and edge FIFOs, and a rotation per triangle) rather than the inverse of a decode loop;
726
- and the four filters, which are lossy and so are a quantisation choice rather than a codec one.
727
- Neither had a consumer here, and neither is what the test-only encoder was doing. A triangle list
728
- can go through mode 2 in the meantime, which is legal and decodes, just larger. There is no
729
- `meshopt_encode.js` to match `meshopt_decode.js` for the same reason: a dispatcher over three
730
- modes and five filters, two thirds of which throw, is worse than the two functions that exist.
731
- - Three test vectors in the first draft were wrong and the implementation was right: a quaternion
732
- component stored at full scale is `1/sqrt(2)` and not 1; a quaternion whose four components are
733
- equal has its dropped component absorb all three of the others' rounding, so it is up to three and
734
- a half quantisation steps out rather than half a step; and a colour alpha of `0x80` is the marker
735
- bit over an empty field, which is zero and not one. The bounds are now derived in the test from
736
- the quantisation step rather than picked.
737
-
738
- ### 8.5 The record
739
-
740
- Thirty-two mutations were applied to the finished implementation to check the suite says so. Thirty
741
- failed at least one test. The two that did not are §8.3, and are documented rather than papered over.
742
-
743
- `npm run test` from `H:/git/moh` covers all of it; the meshopt package and its glTF binding are 96
744
- tests across 12 files, plus 7 end-to-end through the real asset framework.
745
-
746
- ### 8.6 What is still owed
747
-
748
- §5.5 and §6.11 stand unchanged, and are the honest limit of this work:
749
-
750
- **No test here decodes a stream `gltfpack` produced.** Every byte under test was authored from a
751
- specification by me, or by a fixture encoder I also wrote. Three details — literal control mode 3's
752
- element count, channel mode 2's rotation direction, and version 1's 24-byte tail minimum — come from
753
- questions put to the reference rather than from specification text, and no test discriminates any of
754
- them.
755
-
756
- One `gltfpack -noq -cc` output and one `-cc -vv` output, checked in with a golden test that
757
- validates every `POSITION` accessor against its own declared `min`/`max` and every index against the
758
- vertex count, would settle all three at once. Those bounds are written by the encoder and are not
759
- derivable from the compressed bytes.
760
-
761
- Until then: version 0 is corroborated by the specification's own worked example, and **version 1 is
762
- implemented and tested but not evidenced.**
763
-
764
- ## 9. The seam this was first built on the wrong side of
765
-
766
- The first version put the whole glTF half in one module, `gltf_meshopt_compression.js`, exporting
767
- `gltf_meshopt_decode_buffer_views(buffer_views, buffers)`. The parser imported that by name and
768
- called it on every model it loaded.
769
-
770
- It worked, and every test above passed against it. It was still the wrong shape, for reasons worth
771
- writing down because none of them is a bug:
772
-
773
- - **The name described the caller's hope, not the function.** A thing called
774
- `gltf_meshopt_decode_buffer_views` does not decode bufferViews. It walks all of them, works out
775
- which are anybody's, decodes those, and rewrites the array. Three jobs under one codec's name.
776
- - **Detection was welded to decoding.** The parser assumed meshopt by importing it, and the check
777
- that it was meshopt happened *inside*, four frames down. Reading the call site told you nothing
778
- about whether a plain glTF paid anything — which is exactly the question it should answer.
779
- - **The second extension proved the point and the third would have made it.** `KHR_` was bolted on
780
- as `if (name === MESHOPT_COMPRESSION_EXT && ...)` inside the decoder. A Draco or a future codec
781
- would have been another module the parser imported by name, and another branch.
782
-
783
- What replaced it is in §2.2. The parser asks a set what claims a view; the set answers with an
784
- extension or with nothing; the walk is identical for all of them. `tiny-gltf.js` no longer contains
785
- the string "meshopt" anywhere, comments included.
786
-
787
- Two smaller things went the same way:
788
-
789
- - `meshopt_decode`'s mode and filter dispatch were a seven-branch `if / else if` chain. The three
790
- modes already share a signature and the filters already share theirs, so both are now arrays
791
- indexed by the enum, and "none" is a function that does nothing rather than a branch that asks
792
- whether to.
793
- - The extension validator was fourteen `if (...) throw` in a row, several of them comparing mode and
794
- filter names. The specifications state those constraints as a table, so it is a table: what each
795
- mode and each filter admits of a stride and a count, with the rule's own wording carried alongside
796
- for the message. Eight guards, none of them naming a mode.
797
-
798
- Eight mutations were applied to the new seams to check the suite says so — the walker decoding
799
- unclaimed views, the walker dropping its length backstop, the set taking the first claimant, the set
800
- forgetting placeholder buffers, meshopt admitting any filter it can name, one version ceiling for
801
- every mode, and each dispatch table shuffled. All eight fail at least one test.
802
-
803
- ## Addendum, 2026-08-20: a document's resolved buffers are `Uint8Array`
804
-
805
- Written into the plan as `ArrayBuffer`, and changed when the virtual-geometry builder started
806
- reading containers through the same layer. A buffer is a *range* of bytes: a `.glb`'s binary chunk
807
- is a range of the file it arrived in, and a buffer read off a filesystem starts wherever the runtime
808
- put it. Carrying the offset with the bytes is what lets the chunk be handed on without copying a
809
- model-sized allocation, and `Uint8Array.subarray` is what a decoder's source range wants anyway —
810
- `MeshoptBufferViewExtension` reads one with `subarray` rather than a three-argument constructor that
811
- can only be laid over an aligned offset.
812
-
813
- `decode()` answers with a `Uint8Array` for the same reason, and the length check over it is
814
- unchanged.