@woosh/meep-engine 3.60.4 → 3.60.6

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 (419) hide show
  1. package/CHANGELOG.md +2621 -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 +11 -0
  63. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  64. package/src/shade/renderer/Renderer.js +50 -6
  65. package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts +40 -80
  66. package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts.map +1 -1
  67. package/src/shade/renderer/camera/orbital/OrbitalCameraController.js +197 -147
  68. package/src/shade/renderer/fluid/GPUFluidCascade.d.ts +3 -3
  69. package/src/shade/renderer/fluid/GPUFluidCascadeStack.d.ts +1 -1
  70. package/src/shade/renderer/fluid/forcing/GPUFluidForcing.d.ts +6 -6
  71. package/src/shade/renderer/fluid/pressure/GPUFluidPressureSolve.d.ts +1 -1
  72. package/src/shade/renderer/fluid/query/GPUFluidQueries.d.ts +1 -1
  73. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.d.ts +28 -45
  74. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.d.ts.map +1 -1
  75. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.js +111 -119
  76. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts +22 -3
  77. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts.map +1 -1
  78. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.js +84 -3
  79. package/src/shade/renderer/geometry/virtual/proxy/vgeo_proxy_triangle_limit.d.ts +19 -0
  80. package/src/shade/renderer/geometry/virtual/proxy/vgeo_proxy_triangle_limit.d.ts.map +1 -0
  81. package/src/shade/renderer/geometry/virtual/proxy/vgeo_proxy_triangle_limit.js +25 -0
  82. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts +31 -0
  83. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts.map +1 -0
  84. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.js +343 -0
  85. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.d.ts +15 -0
  86. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.d.ts.map +1 -1
  87. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.js +38 -0
  88. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts +17 -0
  89. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts.map +1 -0
  90. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.js +29 -0
  91. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.d.ts.map +1 -1
  92. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.js +7 -13
  93. package/src/shade/renderer/geometry/virtual/sdf/VGeoSDFProxies.d.ts +28 -59
  94. package/src/shade/renderer/geometry/virtual/sdf/VGeoSDFProxies.d.ts.map +1 -1
  95. package/src/shade/renderer/geometry/virtual/sdf/VGeoSDFProxies.js +84 -789
  96. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.d.ts.map +1 -1
  97. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.js +1 -8
  98. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts +7 -6
  99. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts.map +1 -1
  100. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.js +12 -11
  101. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_instance.d.ts +2 -2
  102. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_instance.js +2 -2
  103. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts +1 -4
  104. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts.map +1 -1
  105. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.js +1 -10
  106. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts +5 -15
  107. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts.map +1 -1
  108. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.js +6 -34
  109. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.d.ts.map +1 -1
  110. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.js +1 -7
  111. package/src/shade/renderer/hiz/HZBFootprint.d.ts +14 -0
  112. package/src/shade/renderer/hiz/HZBFootprint.d.ts.map +1 -0
  113. package/src/shade/renderer/hiz/HZBFootprint.js +16 -0
  114. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts +10 -0
  115. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts.map +1 -0
  116. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.js +11 -0
  117. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts +10 -0
  118. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts.map +1 -0
  119. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.js +11 -0
  120. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts +0 -7
  121. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts.map +1 -1
  122. package/src/shade/renderer/hiz/HierarchicalZBuffer.js +405 -350
  123. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts +12 -0
  124. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts.map +1 -0
  125. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.js +27 -0
  126. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts +12 -0
  127. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts.map +1 -0
  128. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.js +27 -0
  129. package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts +14 -0
  130. package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts.map +1 -0
  131. package/src/shade/renderer/hiz/hzb_footprint_texels.js +34 -0
  132. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts +0 -5
  133. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts.map +1 -1
  134. package/src/shade/renderer/hiz/shader_hzb_reduce_any.js +37 -57
  135. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts +11 -0
  136. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts.map +1 -0
  137. package/src/shade/renderer/hiz/{shader_hzb_reduce_mip0_clip.js → shader_hzb_reduce_mip0.js} +120 -111
  138. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  139. package/src/shade/renderer/material/GPUMaterialManager.d.ts +11 -1
  140. package/src/shade/renderer/material/GPUMaterialManager.d.ts.map +1 -1
  141. package/src/shade/renderer/material/GPUMaterialManager.js +16 -8
  142. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts +12 -12
  143. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts.map +1 -1
  144. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.js +17 -118
  145. package/src/shade/renderer/postprocess/gtao/GTAO.d.ts +7 -3
  146. package/src/shade/renderer/postprocess/gtao/GTAO.d.ts.map +1 -1
  147. package/src/shade/renderer/postprocess/gtao/GTAO.js +26 -13
  148. package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.d.ts +14 -0
  149. package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.d.ts.map +1 -1
  150. package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.js +196 -68
  151. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao.d.ts.map +1 -1
  152. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao.js +42 -28
  153. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao_with_irradiance.d.ts.map +1 -1
  154. package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao_with_irradiance.js +429 -414
  155. package/src/shade/renderer/postprocess/gtao/shader_gtao_spatial_denoise.d.ts +16 -0
  156. package/src/shade/renderer/postprocess/gtao/shader_gtao_spatial_denoise.d.ts.map +1 -0
  157. package/src/shade/renderer/postprocess/gtao/shader_gtao_spatial_denoise.js +156 -0
  158. package/src/shade/renderer/scene/GPUSceneContext.d.ts +2 -2
  159. package/src/shade/renderer/scene/GPUSceneContext.js +5 -5
  160. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.d.ts.map +1 -1
  161. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.js +7 -2
  162. package/src/shade/renderer/water/GPUWaterRenderer.d.ts +0 -6
  163. package/src/shade/renderer/water/GPUWaterRenderer.d.ts.map +1 -1
  164. package/src/shade/renderer/water/GPUWaterRenderer.js +12 -17
  165. package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts +35 -0
  166. package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts.map +1 -0
  167. package/src/shade/renderer/water/medium/graph_water_meshlets.js +89 -0
  168. package/src/shade/renderer/water/query/GPUWaterQueries.d.ts +1 -1
  169. package/src/shade/renderer/water/shader_water.d.ts.map +1 -1
  170. package/src/shade/renderer/water/shader_water.js +22 -13
  171. package/src/shade/renderer/water/shader_water_avboit.d.ts.map +1 -1
  172. package/src/shade/renderer/water/shader_water_avboit.js +7 -7
  173. package/src/shade/renderer/water/surface/graph_water_tessellation.d.ts +1 -1
  174. package/src/shade/renderer/water/surface/graph_water_tessellation.js +1 -1
  175. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +9 -1
  176. package/editor/REWORK_PLAN_2026_08.md +0 -692
  177. package/src/CODEC_LAYOUT_VERIFICATION.md +0 -419
  178. package/src/CORRECTNESS_HUNT_2026_07_24.md +0 -142
  179. package/src/DUPLICATION_AUDIT_2026_08_07/00_core_reuse_surface.md +0 -738
  180. package/src/DUPLICATION_AUDIT_2026_08_07/A_core_math_geom.md +0 -79
  181. package/src/DUPLICATION_AUDIT_2026_08_07/B_core_data.md +0 -93
  182. package/src/DUPLICATION_AUDIT_2026_08_07/C_gfx_render.md +0 -80
  183. package/src/DUPLICATION_AUDIT_2026_08_07/D_gfx_features.md +0 -164
  184. package/src/DUPLICATION_AUDIT_2026_08_07/E_physics.md +0 -69
  185. package/src/DUPLICATION_AUDIT_2026_08_07/F_sound.md +0 -110
  186. package/src/DUPLICATION_AUDIT_2026_08_07/G_net_save.md +0 -129
  187. package/src/DUPLICATION_AUDIT_2026_08_07/H_engine_misc.md +0 -72
  188. package/src/DUPLICATION_AUDIT_2026_08_07/I_generation_view.md +0 -74
  189. package/src/DUPLICATION_AUDIT_2026_08_07/K_crosscutting.md +0 -169
  190. package/src/DUPLICATION_AUDIT_2026_08_07.md +0 -254
  191. package/src/DUPLICATION_AUDIT_REMEDIATION.md +0 -250
  192. package/src/DUPLICATION_REVIEW_2026_10_24.md +0 -1072
  193. package/src/REVIEW_2026_08_06/adv-core.md +0 -511
  194. package/src/REVIEW_2026_08_06/adv-gfx.md +0 -480
  195. package/src/REVIEW_2026_08_06/adv-view.md +0 -446
  196. package/src/REVIEW_2026_08_06/api-core.md +0 -296
  197. package/src/REVIEW_2026_08_06/api-engine.md +0 -195
  198. package/src/REVIEW_2026_08_06/bvh-phys-sound-net.md +0 -507
  199. package/src/REVIEW_2026_08_06/core-binary.md +0 -348
  200. package/src/REVIEW_2026_08_06/core-collection.md +0 -376
  201. package/src/REVIEW_2026_08_06/core-math.md +0 -221
  202. package/src/REVIEW_2026_08_06/core-process.md +0 -266
  203. package/src/REVIEW_2026_08_06/core-science.md +0 -223
  204. package/src/REVIEW_2026_08_06/engine-ai-nav.md +0 -279
  205. package/src/REVIEW_2026_08_06/engine-animation.md +0 -167
  206. package/src/REVIEW_2026_08_06/engine-asset.md +0 -166
  207. package/src/REVIEW_2026_08_06/engine-ecs.md +0 -920
  208. package/src/REVIEW_2026_08_06/engine-input.md +0 -197
  209. package/src/REVIEW_2026_08_06/engine-misc.md +0 -248
  210. package/src/REVIEW_2026_08_06/engine-terrain.md +0 -52
  211. package/src/REVIEW_2026_08_06/generation.md +0 -176
  212. package/src/REVIEW_2026_08_06/geom-2d.md +0 -434
  213. package/src/REVIEW_2026_08_06/geom-3d.md +0 -117
  214. package/src/REVIEW_2026_08_06/gfx-gi.md +0 -143
  215. package/src/REVIEW_2026_08_06/gfx-render.md +0 -740
  216. package/src/REVIEW_2026_08_06/gfx-texture.md +0 -690
  217. package/src/REVIEW_2026_08_06/prior-criticals.md +0 -49
  218. package/src/REVIEW_2026_08_06/prior-highs.md +0 -138
  219. package/src/REVIEW_2026_08_06/test-gaps.md +0 -364
  220. package/src/REVIEW_2026_08_06/view.md +0 -667
  221. package/src/REVIEW_2026_08_06.md +0 -610
  222. package/src/core/binary/lz4/LZ4_HIGH_COMPRESSION_PLAN_2026_09_14.md +0 -542
  223. package/src/core/bvh2/bvh3/NOTES.md +0 -2
  224. package/src/core/bvh8/NOTES.md +0 -63
  225. package/src/core/cache/wtinylfu/benchmark/CacheWTinylfu.bench.md +0 -243
  226. package/src/core/color/COLOR_FIX_PLAN_2026_08_28.md +0 -417
  227. package/src/core/color/COLOR_REVIEW_2026_08_28.md +0 -779
  228. package/src/core/geom/2d/polygon/TRIANGULATION_DESIGN.md +0 -433
  229. package/src/core/geom/3d/atlas/ATLAS_PORT_PLAN.md +0 -486
  230. package/src/core/geom/3d/atlas/BFF_PLAN.md +0 -174
  231. package/src/core/geom/3d/atlas/REVIEW_02.md +0 -392
  232. package/src/core/geom/3d/gjk/GJK_REVIEW_NOTES.md +0 -146
  233. package/src/core/geom/3d/gjk/NOTES.md +0 -8
  234. package/src/core/geom/3d/topology/TOPO_MESH_RETIREMENT_PLAN.md +0 -135
  235. package/src/core/geom/GEOMETRY_REVIEW_002_API_ISSUES.md +0 -381
  236. package/src/core/geom/GEOMETRY_REVIEW_002_NAMING.md +0 -684
  237. package/src/core/geom/MANIFOLD_COMPARISON_2026_08_01.md +0 -554
  238. package/src/core/model/reactive/evaluation/PREDICATE_APPLICABILITY_PROPOSAL.md +0 -446
  239. package/src/engine/REVIEW_2026_07_03.md +0 -211
  240. package/src/engine/asset/loaders/image/IMAGE_CODEC_REVIEW_2026_08_28.md +0 -815
  241. package/src/engine/control/first-person/DESIGN.md +0 -658
  242. package/src/engine/control/first-person/DESIGN_COLLISION.md +0 -382
  243. package/src/engine/control/first-person/DESIGN_EXTENSIONS.md +0 -563
  244. package/src/engine/control/first-person/TODO.md +0 -160
  245. package/src/engine/ecs/ASYNC_SYSTEMS_PROPOSAL.md +0 -531
  246. package/src/engine/ecs/HIERARCHY_REVIEW.md +0 -717
  247. package/src/engine/ecs/async/WORKER_SYSTEM_PLAN.md +0 -262
  248. package/src/engine/ecs/ik/PLAN.md +0 -179
  249. package/src/engine/ecs/storage/binary/SERIALIZATION_COVERAGE.md +0 -272
  250. package/src/engine/ecs/storage/binary/object-v2/DESIGN.md +0 -199
  251. package/src/engine/ecs/transform/TRANSFORM64_MIGRATION_PLAN_2026_09_04.md +0 -521
  252. package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +0 -463
  253. package/src/engine/graphics/ecs/camera/pp/NOTES.md +0 -48
  254. package/src/engine/graphics/render/frame_graph/CORRECTNESS_FIXES_2026_09_04.md +0 -659
  255. package/src/engine/graphics/render/frame_graph/FRAME_GRAPH_RECOMMENDATIONS.md +0 -235
  256. package/src/engine/graphics/sh3/path_tracer/REVIEW_001_ACTION_PLAN.md +0 -490
  257. package/src/engine/graphics/texture/sampler/compare/FLIP_IMPLEMENTATION_PLAN.md +0 -466
  258. package/src/engine/input/GAMEPAD_API_PROPOSAL.md +0 -717
  259. package/src/engine/input/ecs/ism/ISM_DESIGN.md +0 -1523
  260. package/src/engine/input/ecs/ism/ISM_IMPLEMENTATION_PLAN.md +0 -627
  261. package/src/engine/navigation/mesh/OVERLAPPING_SURFACES_2026_08_25.md +0 -661
  262. package/src/engine/navigation/mesh/PATHFINDING_PLAN.md +0 -229
  263. package/src/engine/network/CONGESTION_CONTROL.md +0 -87
  264. package/src/engine/network/MESSAGE_TRANSPORT.md +0 -364
  265. package/src/engine/network/SMOOTHNESS_DESIGN.md +0 -400
  266. package/src/engine/physics/cloth/MEASUREMENTS.md +0 -5304
  267. package/src/engine/physics/cloth/PLAN.md +0 -3056
  268. package/src/engine/physics/fluid/GPU_FLUID_CASCADES_PROPOSAL_2026_10_16.md +0 -1382
  269. package/src/engine/physics/fluid/REVIEW_02_PLAN.md +0 -243
  270. package/src/engine/physics/fluid/solver/NOTES.md +0 -46
  271. package/src/engine/sound/SOUND_EMITTER_RETIREMENT_PLAN.md +0 -536
  272. package/src/engine/sound/simulation/IMPLEMENTATION_PLAN.md +0 -432
  273. package/src/engine/sound/simulation/PROBE_PLACEMENT.md +0 -250
  274. package/src/engine/sound/simulation/PROBE_TRANSFER_RESEARCH.md +0 -322
  275. package/src/engine/sound/simulation/REFLECTION_IR_PLAN.md +0 -261
  276. package/src/engine/sound/simulation/VISIBILITY_GRAPH_PLAN.md +0 -121
  277. package/src/engine/sound/sopra/IMPLEMENTATION_PLAN.md +0 -999
  278. package/src/format/image/avif/DECISIONS.md +0 -545
  279. package/src/format/image/jpeg/NOTES.md +0 -3
  280. package/src/format/scene/gltf/MESHOPT_COMPRESSION_PLAN.md +0 -814
  281. package/src/generation/COORDINATES.md +0 -54
  282. package/src/generation/REVIEW_01_ACTION_PLAN.md +0 -628
  283. package/src/generation/foliage/ANNUAL_SHOOTS_BRIEF_2026_10_25.md +0 -220
  284. package/src/generation/foliage/CROWN_FILLING_BRIEF_2026_10_26.md +0 -172
  285. package/src/generation/foliage/CROWN_FULLNESS_BRIEF_2026_10_26.md +0 -86
  286. package/src/generation/foliage/FOLIAGE_DESIGN.md +0 -547
  287. package/src/generation/foliage/FOLIAGE_PLAN.md +0 -198
  288. package/src/generation/foliage/LEAF_ARCHITECTURE_BRIEF_2026_10_26.md +0 -158
  289. package/src/generation/foliage/LOD_REWORK_BRIEF_2026_10_27.md +0 -521
  290. package/src/generation/foliage/REAL_TIME_TREES_BRIEF_2026_10_27.md +0 -86
  291. package/src/generation/foliage/SELF_ORGANIZING_FRAMEWORK_BRIEF_2026_10_26.md +0 -347
  292. package/src/generation/foliage/SHOOT_SYSTEMS_ALL_SPECIES_BRIEF_2026_10_26.md +0 -248
  293. package/src/generation/foliage/SPEEDTREE_COMPARISON_2026_10_24.md +0 -694
  294. package/src/generation/foliage/TWO_FRONT_ENDS_BRIEF_2026_10_28.md +0 -552
  295. package/src/shade/RENDERER_CONTRACT.md +0 -697
  296. package/src/shade/SHADER_F16_PROPOSAL_2026_10_24.md +0 -263
  297. package/src/shade/device/IMMEDIATE_DATA_PLAN_2026_08_28.md +0 -569
  298. package/src/shade/device/PASS_ENCODER_PROPOSAL_2026_08_31.md +0 -755
  299. package/src/shade/device/mock/EXTENSION_PROPOSAL_2026_08_22.md +0 -458
  300. package/src/shade/device/mock/IMPLEMENTATION_PLAN.md +0 -255
  301. package/src/shade/device/mock/REVIEW_2026_08_22.md +0 -527
  302. package/src/shade/device/timing/GPU_PROFILER_PROPOSAL_2026_08_28.md +0 -894
  303. package/src/shade/device/timing/profile/SGPT_FORMAT.md +0 -345
  304. package/src/shade/renderer/NOTES.md +0 -78
  305. package/src/shade/renderer/PLAN.md +0 -8
  306. package/src/shade/renderer/animation/ADDITIVE_BLENDING_PLAN.md +0 -145
  307. package/src/shade/renderer/animation/SKINNING_FRAME_PLAN.md +0 -357
  308. package/src/shade/renderer/atmosphere/NOTES.md +0 -6
  309. package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts +0 -76
  310. package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts.map +0 -1
  311. package/src/shade/renderer/camera/orbital/MomentumAxis.js +0 -164
  312. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts +0 -45
  313. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts.map +0 -1
  314. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.js +0 -66
  315. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts +0 -98
  316. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts.map +0 -1
  317. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.js +0 -184
  318. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts +0 -175
  319. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts.map +0 -1
  320. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.js +0 -411
  321. package/src/shade/renderer/camera/orbital/README.md +0 -81
  322. package/src/shade/renderer/camera/orbital/index.d.ts +0 -6
  323. package/src/shade/renderer/camera/orbital/index.d.ts.map +0 -1
  324. package/src/shade/renderer/camera/orbital/index.js +0 -9
  325. package/src/shade/renderer/extension/RENDER_EXTENSION_DESIGN.md +0 -964
  326. package/src/shade/renderer/fluid/GPU_FLUID_OCCUPANCY_AND_SOTA_REVIEW_2026_10_26.md +0 -361
  327. package/src/shade/renderer/geometry/bvh/BLAS_REFIT_BATCH_PLAN.md +0 -619
  328. package/src/shade/renderer/geometry/meshlet/COMPACTION_PLAN_2026_09_16.md +0 -674
  329. package/src/shade/renderer/geometry/sdf/NOTES.md +0 -6
  330. package/src/shade/renderer/geometry/virtual/ATTRIBUTE_PLAN_2026_09_18.md +0 -540
  331. package/src/shade/renderer/geometry/virtual/MICRON_COMPARISON_2026_08_20.md +0 -305
  332. package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +0 -913
  333. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +0 -787
  334. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +0 -481
  335. package/src/shade/renderer/geometry/virtual/VK_LOD_CLUSTERS_COMPARISON_2026_08_20.md +0 -301
  336. package/src/shade/renderer/geometry/virtual/WEDGE_PLAN_2026_09_18.md +0 -448
  337. package/src/shade/renderer/geometry/virtual/runtime/CULL_PLAN_2026_09_16.md +0 -385
  338. package/src/shade/renderer/geometry/virtual/runtime/CUT_PLAN_2026_09_14.md +0 -656
  339. package/src/shade/renderer/geometry/virtual/runtime/REVIEW_LEDGER.md +0 -308
  340. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN.md +0 -657
  341. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN_2026_09_14.md +0 -551
  342. package/src/shade/renderer/geometry/virtual/runtime/SHADOW_PLAN_2026_09_28.md +0 -529
  343. package/src/shade/renderer/geometry/virtual/sdf/VGEO_SDF.d.ts +0 -47
  344. package/src/shade/renderer/geometry/virtual/sdf/VGEO_SDF.d.ts.map +0 -1
  345. package/src/shade/renderer/geometry/virtual/sdf/VGEO_SDF.js +0 -80
  346. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_collect.d.ts +0 -16
  347. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_collect.d.ts.map +0 -1
  348. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_collect.js +0 -67
  349. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_decode.d.ts +0 -17
  350. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_decode.d.ts.map +0 -1
  351. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_decode.js +0 -145
  352. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_traverse.d.ts +0 -12
  353. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_traverse.d.ts.map +0 -1
  354. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_traverse.js +0 -105
  355. package/src/shade/renderer/global_illumination/lpv/NOTES.md +0 -3
  356. package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_CLIPMAP_PROPOSAL_2026_10_27.md +0 -4306
  357. package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_RAYS_VARIABLE_AND_SORTED_SCOPE_2026_10_30.md +0 -410
  358. package/src/shade/renderer/global_illumination/sharc/NOTES.md +0 -8
  359. package/src/shade/renderer/gpu_primitive/bvh/GPU_BVH_BUILD_PLAN.md +0 -1369
  360. package/src/shade/renderer/gpu_primitive/bvh/PHASE1_HANDOVER.md +0 -202
  361. package/src/shade/renderer/gpu_primitive/indirect/NOTES.md +0 -94
  362. package/src/shade/renderer/hiz/NOTES.md +0 -3
  363. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts +0 -8
  364. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts.map +0 -1
  365. package/src/shade/renderer/light/cluster/NOTES.md +0 -11
  366. package/src/shade/renderer/light/cluster/PROPOSAL_ALGORITHM_2026_10_16.md +0 -524
  367. package/src/shade/renderer/light/cluster/PROPOSAL_HIERARCHICAL_2026_10_16.md +0 -1101
  368. package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts +0 -24
  369. package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts.map +0 -1
  370. package/src/shade/renderer/material/resident/shader_resident_texture_means.js +0 -120
  371. package/src/shade/renderer/particles/DESIGN.md +0 -1011
  372. package/src/shade/renderer/particles/EMITTER_GLOBAL_STATE_PLAN_2026_10_29.md +0 -280
  373. package/src/shade/renderer/particles/ORIENTATION_PLAN_2026_10_15.md +0 -453
  374. package/src/shade/renderer/path_tracer/PATH_TRACED_MEDIA_PLAN_2026_10_02.md +0 -520
  375. package/src/shade/renderer/postprocess/bloom/NOTES.md +0 -6
  376. package/src/shade/renderer/postprocess/fsr/NOTES.md +0 -4
  377. package/src/shade/renderer/postprocess/gtao/NOTES.md +0 -6
  378. package/src/shade/renderer/postprocess/nss/NOTES.md +0 -332
  379. package/src/shade/renderer/postprocess/perspective/NOTES.md +0 -3
  380. package/src/shade/renderer/postprocess/sscs/NOTES.md +0 -5
  381. package/src/shade/renderer/postprocess/ssr/NOTES.md +0 -14
  382. package/src/shade/renderer/postprocess/ssr/REPROJECTION_PROPOSAL.md +0 -286
  383. package/src/shade/renderer/postprocess/ssr/prefilter/NOTES.md +0 -39
  384. package/src/shade/renderer/postprocess/taa/NOTES.md +0 -11
  385. package/src/shade/renderer/postprocess/upscale-dither/NOTES.md +0 -78
  386. package/src/shade/renderer/postprocess/vrs/NOTES.md +0 -4
  387. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_LEDGER.md +0 -1218
  388. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_PLAN_2026_09_03.md +0 -600
  389. package/src/shade/renderer/restir/reservoir/NOTES.md +0 -6
  390. package/src/shade/renderer/shader/chunk/atmosphere/NOTES.md +0 -6
  391. package/src/shade/renderer/shader/chunk/brdf/FRESNEL_BRIEF_2026_10_16.md +0 -391
  392. package/src/shade/renderer/shader/chunk/color/tonemap/ACES_TONE_MAPPING_BRIEF_2026_10_17.md +0 -153
  393. package/src/shade/renderer/shader/chunk/color/tonemap/HDR_TONE_MAPPING_FORMS_BRIEF_2026_10_16.md +0 -322
  394. package/src/shade/renderer/shader/chunk/color/tonemap/SDR_TONE_MAPPING_BRIEF_2026_10_16.md +0 -247
  395. package/src/shade/renderer/shader/chunk/fsr/NOTES.md +0 -3
  396. package/src/shade/renderer/shader/chunk/fsr/spd/NOTES.md +0 -4
  397. package/src/shade/renderer/shader/chunk/geometry/ray/NOTES.md +0 -4
  398. package/src/shade/renderer/shader/chunk/material/MULTISCATTER.md +0 -159
  399. package/src/shade/renderer/shader/chunk/mboit/NOTES.md +0 -38
  400. package/src/shade/renderer/shader/chunk/spherical_harmonics/l1i/NOTES.md +0 -3
  401. package/src/shade/renderer/shader/chunk/spherical_harmonics/sampling/NOTES.md +0 -8
  402. package/src/shade/renderer/shader/compiler/SHADER_SWITCH_PROPOSAL_2026_10_16.md +0 -927
  403. package/src/shade/renderer/shadow/contact/FINDINGS.md +0 -413
  404. package/src/shade/renderer/shadow/map/csm/NOTES.md +0 -5
  405. package/src/shade/renderer/shadow/ray/NOTES.md +0 -31
  406. package/src/shade/renderer/shadow/sdf/NOTES.md +0 -9
  407. package/src/shade/renderer/shadow/sdf/brixelizer/NOTES.md +0 -5
  408. package/src/shade/renderer/shadow/sdf/cascade/NOTES.md +0 -9
  409. package/src/shade/renderer/shadow/vsm/NOTES.md +0 -12
  410. package/src/shade/renderer/shadow/vsm/PLAN.md +0 -442
  411. package/src/shade/renderer/shadow/vsm/REVIEW_LEDGER.md +0 -43
  412. package/src/shade/renderer/texture/COMPRESSED_TEXTURE_PLAN_2026_09_01.md +0 -2444
  413. package/src/shade/renderer/texture/mipmaps/NOTES.md +0 -3
  414. package/src/shade/renderer/volumetrics/NOTES.md +0 -1329
  415. package/src/shade/renderer/volumetrics/VOLUMETRIC_NOISE_PLAN_2026_10_15.md +0 -336
  416. package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts +0 -23
  417. package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts.map +0 -1
  418. package/src/shade/renderer/water/medium/GPUWaterBodies.js +0 -75
  419. package/src/shade/wgsl/NOTES.md +0 -4
@@ -1,815 +0,0 @@
1
- # Image codecs — duplication and shared-construct review
2
-
3
- Review, 2026-08-28. Alex Goldring / Company Named Limited.
4
-
5
- Scope: `src/format/image/avif/`, `src/format/image/png/`, `src/format/image/jpeg/`. Everything below
6
- is an itemised proposal; nothing has been changed.
7
-
8
- > **2026-09-02.** The three trees were relocated under `src/format/image/` (see
9
- > `src/CODEC_LAYOUT_VERIFICATION.md`); the links below were rewritten to follow them, and the paths in
10
- > prose were not. C1 has since landed in three parts, all in `engine/asset/`: `codec/DecodedImage.js`
11
- > is the one result struct; `loaders/image/codec/{JpegCodec,AvifCodec}.js` put both decoders behind
12
- > the `Codec` door, last in `ImageRGBADataLoader`'s chain so a browser keeps its native decoder and
13
- > node gains both; `guessAssetType` maps `.avif` to `"image"`. One inversion from the proposal:
14
- > `AvifCodec` wraps `avif_to_sampler2d` rather than the other way round, because the adapter is the
15
- > one with the HDR path and the reusable decoder context, and an 8-bit asset codec wants neither.
16
- >
17
- > Not on this review's list, and gone the same day: `shade/renderer/loader/usd/usd_decode_image.js` carried a
18
- > *third* PNG decoder (8-bit RGB/RGBA, fixed-Huffman only, its own inflate). It now calls `PNGReader` and
19
- > widens the result to RGBA8 — one more `itemSize → RGBA` loop for C2 to fold, and one fewer decoder.
20
-
21
- Placed here rather than under `src/format/image/avif/` because the majority of the *cross-tree* items land
22
- on the loader side, and `src/format/image/avif/DECISIONS.md` is a decision log rather than a review.
23
-
24
- ---
25
-
26
- ## 0. Verdict
27
-
28
- Three decoders, written at three different times against three different sets of conventions, doing
29
- substantially the same job.
30
-
31
- **The AVIF tree is the disciplined one.** It already has the abstractions the other two lack —
32
- a bounds-checked cursor, a container walker, a parameterised colour transform, generated tables, a
33
- decision log. Its own duplication is residue rather than structure: about **fifteen verbatim
34
- copy-paste sites**, almost all of them the encode/decode mirror that
35
- [D30](../../../../format/image/avif/DECISIONS.md) already set out to eliminate and did not quite finish.
36
- Cheap to close, and closing it is the difference between a mirror that stays honest and one that
37
- drifts.
38
-
39
- **The cross-tree duplication is the expensive one, and it is not symmetrical.** PNG and JPEG each
40
- reimplement, badly and privately, things AVIF already has generalised — channel expansion to RGBA,
41
- YCbCr conversion, chroma upsampling, chunk walking, bounds checking. The single most valuable move
42
- in this document is not a merge: it is **registering AVIF and JPEG behind the `Codec` interface that
43
- already exists** ([Codec.js](../../codec/Codec.js)) so that all three formats reach the engine through one
44
- door with one result shape. Today PNG goes through a worker, JPEG goes nowhere at all, and AVIF
45
- bypasses the asset-loader system entirely via `avif_to_sampler2d`.
46
-
47
- **One number worth stating up front:** `JpegImage.getData` hard-codes the coefficients `1.402`,
48
- `-0.3441363`, `-0.71413636`, `1.772`. Those are *exactly* what
49
- `colour_transform_init(t, MC_BT_601, ·, 1, 8)` computes
50
- ([ColourTransform.js:98](../../../../format/image/avif/color/ColourTransform.js#L98)). The JPEG decoder is
51
- a hand-inlined special case of a general transform that already exists two directories away.
52
-
53
- To be clear about what that is and is not worth: JFIF is full-range BT.601, so those constants are
54
- *correct* and JPEG has no need for the other matrices. The win is not new format coverage. It is
55
- that the derivation `cr_to_r = 2(1 − kr)`, `cb_to_g = −2·kb(1 − kb)/kg`, … is computed once in
56
- `ColourTransform` and hand-transcribed twice more — as BT.601 literals in `JpegImage`, as BT.709
57
- literals in `core/color/YCbCr/` — and that adopting the row-based transform is what makes C4
58
- possible.
59
-
60
- **What not to do:** do not merge `ByteCursor` into `BinaryBuffer`, and do not try to fold
61
- `encode_tile` into `decode_tile`. D10 and D30 already settled both, with reasons that still hold.
62
- Section 6 says so explicitly so nobody re-litigates them from this document.
63
-
64
- Priority ordering is in section 7.
65
-
66
- ---
67
-
68
- ## Status
69
-
70
- Eleven items were selected and have landed. **File and line references below are as of the review
71
- date and pre-date those changes** — the proposals for the remaining items still hold, but their
72
- anchors have moved.
73
-
74
- | Item | Commit | Departure from the proposal |
75
- | --- | --- | --- |
76
- | D1, D4 | `chore(image): the unreachable halves…` | none |
77
- | P4 | `refactor(png): the reader keeps one cursor…` | also removed `this.bytes`, assigned and never read |
78
- | J2, J4 | `refactor(jpeg): frame components become a Map…` | none |
79
- | C7 | `refactor(binary): crc32 moves to core…` | none |
80
- | C8 | `refactor(binary): one UTF-8 codec…` | three functions, not two: splitting `utf8_encoded_length` off is what lets `BoxWriter` grow once and encode straight into the buffer |
81
- | A10 | `refactor(avif): the small numeric helpers…` | six inline `clz32` sites, not seven — see the correction in A10 |
82
- | P2 | `refactor(png): one inflate path…` | no deprecation shim; the export changes name, arity and return type |
83
- | P3 | `fix(png): bit depths of 1, 2 and 4 decode correctly…` | the suspected bug was real, and worse than described — see P3 |
84
- | C4 | `refactor(jpeg): one row sampler…` | **`upsample_chroma_row` is not reusable here** — see the correction in C4 |
85
-
86
- Not taken this round: C1, C2, C3, C5, C6, A1–A9, A11, A12, D2, D3, D5, D6, P1, P5, P6, J1, J3.
87
- D5 and D6 came along with P4 and J2 respectively.
88
-
89
- ---
90
-
91
- ## 1. Cross-package items
92
-
93
- These are the ones where a construct exists in one tree and is re-invented in another.
94
-
95
- ### C1 — One `Codec` door for all three formats *(highest value)*
96
-
97
- **Today.** Three formats, three unrelated entry conventions, three result shapes:
98
-
99
- | Format | Entry point | Result shape |
100
- | --- | --- | --- |
101
- | PNG | worker → [ImageDecoderWorker.js:3](ImageDecoderWorker.js#L3) → `PNGReader` | `{data, width, height, itemSize, bitDepth}` |
102
- | JPEG | [jpeg_decode.js:9](jpeg/jpeg_decode.js#L9) — **not reachable from any loader** | `{data, width, height, exifBuffer, comments?}` — no `itemSize`, no `bitDepth`, always 3-channel |
103
- | AVIF | [avif_to_sampler2d.js:26](../../../graphics/texture/sampler/avif_to_sampler2d.js#L26) — bypasses the loader entirely, called direct from `deserialize_scene` | `{data, width, height}` — no `itemSize`, no `bitDepth`, always RGBA |
104
-
105
- `ImageDecoderWorker.decode` has a `switch (type)` with exactly one arm and a `default: throw new
106
- Error('Unsupported type')`. `ThreadedImageDecoder.test` sniffs `PNG_HEADER_BYTES` and returns `false`
107
- for everything else, so a JPEG or AVIF asset silently falls through to `NativeImageDecoder` — the
108
- browser canvas path — which is why the native JPEG decoder has never needed to work.
109
-
110
- **Proposal.**
111
-
112
- 1. Define one result struct, `DecodedImage {data, width, height, itemSize, bitDepth, ...}`, next to
113
- `Codec.js`. Fill it in from all three decoders. `exifBuffer` and `comments` become optional
114
- fields rather than a shape difference.
115
- 2. Add `PngCodec`, `JpegCodec`, `AvifCodec`, each a `Codec` subclass whose `test(data)` is a magic
116
- sniff and whose `decode(data)` returns a `DecodedImage`. The sniffs already exist in three places
117
- in three styles — `PNG_HEADER_BYTES` as an exported array, `0xFFD8` inline at
118
- [JpegImage.js:139](jpeg/JpegImage.js#L139), and `ftyp`/`meta` presence at
119
- [parse_avif_file.js:51](../../../../format/image/avif/heif/parse_avif_file.js#L51). Give each format a
120
- `*_HEADER_BYTES` (or a `test` predicate) beside its decoder and have the codec use it.
121
- 3. `CodecWithFallback` ([CodecWithFallback.js:11](../../codec/CodecWithFallback.js#L11)) already does
122
- ordered try-with-sniff correctly. Compose `[Threaded(PNG), Jpeg, Avif, Native]` and the format
123
- switch disappears from `ImageRGBADataLoader` and from `ImageDecoderWorker`.
124
- 4. `avif_to_sampler2d` stays — it is the HDR-aware adapter and the exact inverse of
125
- `sampler2d_to_avif` (D32) — but it becomes a thin wrapper over `AvifCodec` rather than a second
126
- independent path into the decoder.
127
-
128
- **Payoff.** JPEG becomes reachable. AVIF becomes loadable as an asset. The worker learns a second
129
- format for free. One place decides what "a decoded image" is.
130
-
131
- **Risk.** Low. `CodecWithFallback` and `Codec` already exist and are spec-covered; this is
132
- population, not new machinery.
133
-
134
- ---
135
-
136
- ### C2 — Channel expansion to RGBA is written four times
137
-
138
- Same job — take interleaved samples with `itemSize` channels and produce RGBA — in four places:
139
-
140
- - [PNG.js:303](png/PNG.js#L303) `getRGBA8Array` + `getRGBA8Array_fromRGB` +
141
- `getRGBA8Array_generic` + `getPixel` — **all four dead**, see D1 below
142
- - [PNG.js:325](png/PNG.js#L325) `getUint8Data_case3`, palette expansion with the `tRNS` alpha rule
143
- - [JpegImage.js:561](jpeg/JpegImage.js#L561) `copyToImageData`, one loop per component count with
144
- `formatAsRGBA` deciding whether to emit the fourth channel
145
- - [sampler2d_to_avif.js:68](../../../graphics/texture/sampler/sampler2d_to_avif.js#L68) `to_rgba`, the
146
- cleanest of the four and the only one that handles half-float input
147
-
148
- And the engine already has a fifth: [sampler2d_to_uint8_RGBA.js](../../../graphics/texture/sampler/sampler2d_to_uint8_RGBA.js),
149
- wrapped by `sampler2d_ensure_uint8_RGBA`.
150
-
151
- **Proposal.** One free function in `core/` or beside `Codec.js`:
152
-
153
- ```js
154
- expand_to_rgba(source, item_size, count, destination, opaque_value)
155
- ```
156
-
157
- with the three input families (`Uint8`, `Uint16` half-float, `Float32`) selected by the caller, not
158
- sniffed. `to_rgba`'s structure is the right one to generalise from: it is the only one that already
159
- handles `item_size < 3` by zero-filling and `item_size < 4` by writing opaque.
160
-
161
- Palette expansion (`getUint8Data_case3`) stays PNG-specific — a palette lookup is not a channel
162
- expansion — but it should call the shared function for the final write rather than open-coding the
163
- `destination_address + 0..3` stores.
164
-
165
- **Payoff.** Removes ~120 lines of dead code plus ~150 lines of live duplication, and makes the
166
- "what fills alpha when the source has none" rule a single decision instead of four (today: `255`,
167
- `255`, `255`, and `1` for float — all correct, but nothing says so in one place).
168
-
169
- ---
170
-
171
- ### C3 — YCbCr → RGB exists three times, with three different capability levels
172
-
173
- | Where | Matrix | Range | Depth | Parameterised? |
174
- | --- | --- | --- | --- | --- |
175
- | [ycbcr_row_to_rgb.js:24](../../../../format/image/avif/color/ycbcr_row_to_rgb.js#L24) | any `MC_*`, plus identity and YCgCo | full or limited | 8/10/12 | yes, via `ColourTransform` |
176
- | [JpegImage.js:491](jpeg/JpegImage.js#L491) | BT.601 only, hard-coded | full only | 8 only | no |
177
- | [YCbCr_to_rgb_uint24.js](../../../../core/color/YCbCr/YCbCr_to_rgb_uint24.js) | BT.709 only, hard-coded | full only | 8 only | no |
178
-
179
- The JPEG constants are not "close to" BT.601 — they *are* `colour_transform_init` with
180
- `MC_BT_601`, `color_range = 1`, `bit_depth = 8`. `cr_to_r = 2(1 − 0.299) = 1.402`,
181
- `cb_to_b = 2(1 − 0.114) = 1.772`, `cb_to_g = −2·0.114·0.886/0.587 = −0.3441363`,
182
- `cr_to_g = −2·0.299·0.701/0.587 = −0.71413636`. Digit for digit.
183
-
184
- The same holds for YCgCo. `rgb_row_to_ycbcr`'s `CONVERSION_YCGCO` branch
185
- ([rgb_row_to_ycbcr.js:40](../../../../format/image/avif/color/rgb_row_to_ycbcr.js#L40)) computes
186
- `0.25r + 0.5g + 0.25b`, `−0.25r + 0.5g − 0.25b`, `0.5r − 0.5b` — which is
187
- [rgb_to_ycocg.js](../../../../core/color/YCoCg/rgb_to_ycocg.js) verbatim, with Cg and Co swapped in
188
- the output order. Its inverse likewise reproduces `ycocg_to_rgb`.
189
-
190
- **Proposal.** Promote `ColourTransform` + `colour_transform_init` + the two row functions out of
191
- `src/format/image/avif/color/` into `src/core/color/`. They have no AV1 dependency beyond the `MC_*` /
192
- `CP_*` symbol constants, which should move with them (they are CICP codes from ITU-T H.273, not AV1
193
- inventions — the AVIF tree just happens to be where they were first needed).
194
-
195
- Then:
196
-
197
- - `JpegImage.getData` builds a `ColourTransform` once per image from `adobe.transformCode` /
198
- component count and calls `ycbcr_row_to_rgb` per row. The arithmetic is unchanged — JFIF is
199
- full-range BT.601 and that is exactly what `MC_BT_601` with `color_range = 1` produces — so this
200
- is deduplication, not a behaviour change. What it buys is the row-based shape that C4 needs, and
201
- one fewer transcription of the coefficients.
202
- - `YCbCr_to_rgb_uint24` / `rgb_to_YCbCr_uint24` keep their packed-uint24 signature but derive their
203
- coefficients from a module-level `ColourTransform` initialised with `MC_BT_709`, so the constants
204
- stop being transcribed.
205
- - The `CONVERSION_YCGCO` branches delegate to `rgb_to_ycocg` / `ycocg_to_rgb`.
206
-
207
- **Risk.** Medium. This moves live colour code. `color.spec.js` and the AVIF fixture round-trips cover
208
- the AVIF side; the JPEG side is a bit-exactness question — the current code computes in `number` with
209
- those literals, the shared path computes the same coefficients from `kr`/`kb`, and the two can differ
210
- in the last ulp before rounding. Extend `jpeg_decode.spec.js` with a per-pixel comparison against the
211
- current output (tolerance 0, then 1) before it lands. The JPEG path is currently unreachable (C1), so
212
- the exposure while that is settled is nil.
213
-
214
- ---
215
-
216
- ### C4 — Chroma upsampling is written twice, one of them by accident
217
-
218
- [upsample_chroma_row.js:36](../../../../format/image/avif/color/upsample_chroma_row.js#L36) has nearest and
219
- bilinear modes, edge clamping, and honours `subsampling_x` / `subsampling_y` independently.
220
-
221
- `JpegImage.getData` does the same thing implicitly, in every one of its four `switch` arms: the
222
- expression `0 | (· * componentN.scale· · ·)` appears **27 times** in that one function (17 on the
223
- column axis, 10 on the row axis). That is
224
- nearest-neighbour upsampling combined with output rescaling, inlined per component per arm. It is
225
- also why JPEG 4:2:0 chroma is visibly blockier than AVIF 4:2:0 chroma at the same subsampling.
226
-
227
- **Proposal.** Have the JPEG component loop upsample each component to a full-width row, then run
228
- one interleave loop instead of four near-identical ones. `getData`'s four cases collapse to: build
229
- N full-width rows, then one component-count-dependent combine.
230
-
231
- **Correction, from doing it.** The row sampler could *not* be `upsample_chroma_row`.
232
- `upsample_chroma_row` expresses subsampling as a shift — `sub_x` and `sub_y` are 0 or 1 — so it can
233
- say "half" and not "a third", while JPEG's `h` and `v` sampling factors run from one to four and
234
- `getData` also rescales to an arbitrary requested output size. Generalising it to rational scaling
235
- would have changed the AVIF decoder's per-row path for no AVIF benefit. The sampler lives in
236
- `jpeg/jpeg_component_row.js` instead, with that reasoning on it, and stays nearest-neighbour: the
237
- four arms were nearest-neighbour, and interpolating would change every decoded picture.
238
-
239
- So the shared-with-AVIF half of this item is withdrawn. The deduplication half stands and was worth
240
- it on its own.
241
-
242
- **Payoff, realised.** `getData` drops from ~140 lines to ~55 across three functions, the twenty-seven
243
- sampling expressions become one, and the colour-transform decision — spelt out twice with a
244
- different default each time — becomes one method. Verified byte-for-byte against the previous
245
- implementation over 300+ combinations of sampling ratio, output size, component count, Adobe marker
246
- and option state.
247
-
248
- ---
249
-
250
- ### C5 — A container walker for length-prefixed records
251
-
252
- The AVIF tree has this construct twice, correctly, in two variants:
253
-
254
- - [for_each_box.js:24](../../../../format/image/avif/isobmff/for_each_box.js#L24) — reusable header
255
- instance, visitor returns `false` to stop, cursor repositioned to the next record regardless of how
256
- much the visitor consumed
257
- - [for_each_obu.js:25](../../../../format/image/avif/av1/obu/for_each_obu.js#L25) — same contract, two
258
- reusable readers
259
-
260
- PNG has the same construct, open-coded and split across two methods:
261
- [PNGReader.decodeChunk:151](png/PNGReader.js#L151) reads `length / type / payload / crc` and then
262
- `switch`es on type in the same function, and [PNGReader.parse:654](png/PNGReader.js#L654) is the
263
- `for(;;)` that drives it and detects `IEND`. JPEG has it as a 200-line `switch` inside
264
- `JpegImage.parse` with `offset` mutated by nested closures.
265
-
266
- **Proposal.** Two separable pieces, and the second matters more than the first:
267
-
268
- 1. `for_each_png_chunk(cursor, visit)` in the PNG folder, mirroring `for_each_box`'s contract
269
- exactly — reusable `PngChunkHeader {type, offset, payload_offset, payload_size, crc}`, visitor
270
- returns `false` to stop. `decodeChunk` becomes a visitor with a `switch` and nothing else; the
271
- CRC check moves into the walker where it belongs.
272
- 2. **Separate parsing from dispatch in JPEG.** `JpegImage.parse` currently reads the segment,
273
- interprets it, and mutates decoder state in one 250-line function with four closures
274
- (`readUint16`, `readDataBlock`, `prepareComponents`) capturing `offset`. Split it into
275
- `for_each_jpeg_segment(data, visit)` plus one handler per marker family. The malformed-APP1
276
- recovery ([JpegImage.js:368](jpeg/JpegImage.js#L368)) and the eaten-`0xFF` recovery belong to the
277
- walker, not to the `default:` arm of a dispatch switch.
278
-
279
- I am **not** proposing one generic TLV walker across all three formats. The record headers differ
280
- too much (ISOBMFF has three size encodings and a `uuid` case; PNG has a trailing CRC; JPEG has
281
- stuffed bytes and standalone markers). What should be shared is the *contract* — reusable header
282
- struct, visitor returns `false`, walker guarantees forward progress — so that the three read alike.
283
-
284
- ---
285
-
286
- ### C6 — Bounds checking exists on one side of the fence only
287
-
288
- `ByteCursor.require(count, what)` ([ByteCursor.js:96](../../../../core/binary/ByteCursor.js#L96))
289
- validates every read and names the field in the error. `BitReader` does the same for every
290
- descriptor. There is a `robustness.spec.js` that feeds the decoder truncated and hostile files.
291
-
292
- PNG reads through two unchecked private helpers
293
- ([PNGReader.js:28](png/PNGReader.js#L28), [PNGReader.js:42](png/PNGReader.js#L42)):
294
-
295
- ```js
296
- function readUInt32(buffer, offset) {
297
- return (buffer[offset] << 24) | (buffer[offset+1] << 16) | (buffer[offset+2] << 8) | buffer[offset+3];
298
- }
299
- ```
300
-
301
- Past the end this yields `NaN`-free garbage — `undefined << 24` is `0` — so a truncated `IHDR`
302
- produces a plausible width and height rather than an error. JPEG reads `data[offset++]` throughout
303
- with no checks at all.
304
-
305
- Both of those helpers are also **already redundant with the `BinaryBuffer` the same constructor
306
- built**: `PNGReader` creates `this.buffer` with `endianness = BigEndian`
307
- ([PNGReader.js:83](png/PNGReader.js#L83)) and then reads chunk payloads with the unchecked free
308
- functions anyway. `buffer.readUint32()` / `readUint8()` were there the whole time.
309
-
310
- **Proposal.** Delete `readUInt32` and `readUInt8` from `PNGReader.js`; read chunk payloads through a
311
- `BinaryBuffer` positioned on the chunk (or through the `for_each_png_chunk` header from C5). Add a
312
- `robustness.spec.js` to the PNG and JPEG folders modelled on the AVIF one — truncated header,
313
- truncated chunk, chunk length exceeding the file, garbage after `IEND`.
314
-
315
- **Payoff.** Removes two functions and closes the class of failure where a malformed image decodes to
316
- a plausible-looking wrong picture instead of throwing.
317
-
318
- ---
319
-
320
- ### C7 — `crc32` is a general utility living in the PNG folder
321
-
322
- [crc32.js](png/crc32.js) is a complete, spec-cited, tested CRC-32/ISO-3309 implementation with an
323
- `(buffer, offset, length)` signature. It sits under `png/` and is imported by exactly one file.
324
-
325
- Meanwhile [unpack_usdz.js:66](../../../../format/scene/usd/unpack_usdz.js#L66) reads ZIP
326
- local file headers and *skips* the CRC field with a comment, because verifying it would mean writing
327
- this function again.
328
-
329
- **Proposal.** Move to `src/core/binary/hash/crc32.js`, beside `xxh3_64` and `xxh3_128`. Re-export
330
- from `png/crc32.js` for one release if any external consumer matters, otherwise just move it and fix
331
- the two imports.
332
-
333
- Also worth noting while it moves: `PNGReader.crc_enabled` defaults to `false`
334
- ([PNGReader.js:91](png/PNGReader.js#L91)) and, when enabled, `console.warn`s rather than throwing.
335
- That is a defensible choice for a game engine — but it is an undocumented one, and the AVIF tree
336
- records choices like this in `DECISIONS.md`. Give the PNG folder the same treatment.
337
-
338
- ---
339
-
340
- ### C8 — NUL-terminated UTF-8, encoded and decoded privately in two places
341
-
342
- [ByteCursor.js:307](../../../../core/binary/ByteCursor.js#L307) has a private `utf8_decode`
343
- with the stated reason that `TextDecoder` is not universally available.
344
- [BoxWriter.js:205](../../../../format/image/avif/isobmff/BoxWriter.js#L205) has the matching encoder inline
345
- in `utf8_string`. `BinaryBuffer` has `readUTF8String` / `writeUTF8String` but they are
346
- **length-prefixed** (meep's own serialisation format) and therefore genuinely unusable here — this
347
- is a real gap, not a missed reuse.
348
-
349
- **Proposal.** One pair of free functions in `core/binary/`:
350
-
351
- ```js
352
- utf8_decode_range(bytes, start, end) // → string
353
- utf8_encode_into(text, buffer) // → bytes written
354
- ```
355
-
356
- `ByteCursor.utf8_string` and `BoxWriter.utf8_string` become three-line wrappers that add the
357
- NUL handling. Optionally give `BinaryBuffer` `readUTF8StringNullTerminated` /
358
- `writeUTF8StringNullTerminated` built on the same pair, which would let a future ISOBMFF-adjacent
359
- reader use it directly.
360
-
361
- This does **not** conflict with D10: it shares the character codec, not the cursor.
362
-
363
- ---
364
-
365
- ## 2. Inside `src/format/image/avif/` — finishing D30
366
-
367
- D30 says derived state is shared and traversal is mirrored. That is the right rule. These are the
368
- places where derived state is still written twice, and they are the residue D30 was aiming at.
369
-
370
- ### A1 — Tile geometry derivation, 18 identical lines
371
-
372
- [parse_frame_header.js:250–267](../../../../format/image/avif/av1/obu/parse_frame_header.js#L250) and
373
- [write_frame_header.js:162–179](../../../../format/image/avif/av1/obu/write_frame_header.js#L162) open with
374
- the same eighteen lines: `sb_shift`, `sb_size`, `sb_cols`, `sb_rows`, `max_tile_width_sb`,
375
- `max_tile_area_sb`, `min_log2_tile_cols`, `max_log2_tile_cols`, `max_log2_tile_rows`,
376
- `min_log2_tiles`. Character for character, `let` versus `const` on `max_tile_area_sb` aside.
377
-
378
- **Proposal.** `derive_tile_limits(sequence, header, out)` in a shared module (`av1/obu/tile_limits.js`
379
- or extending the existing `av1/util/`), filling a small struct or an `Int32Array`. Both call it. This
380
- is the highest-consequence one in this section: these ten values decide how many `f(1)` bits the
381
- uniform-spacing loop reads and writes, so a divergence here is a bitstream desync, not a wrong pixel.
382
-
383
- ### A2 — CDF adaptation, 24 identical lines in reader and writer
384
-
385
- [SymbolReader.js:178–202](../../../../format/image/avif/av1/entropy/SymbolReader.js#L178) and
386
- [SymbolWriter.js:216–240](../../../../format/image/avif/av1/entropy/SymbolWriter.js#L216) are the same
387
- block verbatim: the `rate` computation, the `tmp` sweep, the count saturation at 32.
388
-
389
- **Proposal.** `cdf_adapt(cdf, offset, n, symbol)` beside `symbol_cost.js`. Both call it. The
390
- adaptation rate is normative and shared by construction; there is no reason for two copies.
391
-
392
- While there: the interval boundary
393
- `((range_high * (f >> EC_PROB_SHIFT)) >> (7 - EC_PROB_SHIFT)) + EC_MIN_PROB * (n - symbol - 1)`
394
- appears once in the reader and twice in the writer
395
- ([SymbolWriter.js:200](../../../../format/image/avif/av1/entropy/SymbolWriter.js#L200) and
396
- [:206](../../../../format/image/avif/av1/entropy/SymbolWriter.js#L206)). A `symbol_boundary(range_high,
397
- cdf, offset, n, symbol)` helper covers all three; it is on the per-symbol path, so measure before
398
- committing, but it is a pure function of five numbers and should inline.
399
-
400
- ### A3 — `clear_above_context` / `clear_left_context`, byte-identical
401
-
402
- [decode_tile.js:115](../../../../format/image/avif/av1/decode/decode_tile.js#L115) and
403
- [:125](../../../../format/image/avif/av1/decode/decode_tile.js#L125); the same two functions verbatim at
404
- [encode_tile.js:277](../../../../format/image/avif/av1/encode/encode_tile.js#L277) and
405
- [:287](../../../../format/image/avif/av1/encode/encode_tile.js#L287).
406
-
407
- **Proposal.** Move both onto `Av1FrameContext` as methods, or export them from
408
- `av1/entropy/coefficient_context.js` — which is already the shared home for exactly this kind of
409
- context state. Twelve lines, zero risk.
410
-
411
- ### A4 — `dc_delta` / `ac_delta`, byte-identical
412
-
413
- [reconstruct.js:222](../../../../format/image/avif/av1/decode/reconstruct.js#L222) and
414
- [:235](../../../../format/image/avif/av1/decode/reconstruct.js#L235); identical copies at
415
- [quantise.js:193](../../../../format/image/avif/av1/encode/quantise.js#L193) and
416
- [:206](../../../../format/image/avif/av1/encode/quantise.js#L206). `reconstruct.js` already exports
417
- `dc_quant`, `ac_quant` and `get_qindex` and `quantise.js` already imports from it — these two just
418
- never made the trip.
419
-
420
- **Proposal.** Export them from `reconstruct.js` alongside their siblings. Delete the copies.
421
-
422
- ### A5 — `BINARY_PARTITION_CDF` construction
423
-
424
- [decode_tile.js:273](../../../../format/image/avif/av1/decode/decode_tile.js#L273) and
425
- [encode_tile.js:838](../../../../format/image/avif/av1/encode/encode_tile.js#L838) each declare a
426
- three-element scratch and each fill it with `(1<<15) - psum, 1<<15, 0`, each save and restore
427
- `disable_cdf_update` around the call. The decoder explains in a comment why the adaptation is
428
- skipped (the spec rebuilds this CDF from scratch every time, so any update is discarded); the
429
- encoder does the same thing with no explanation, which is the half of the pair that will get
430
- "simplified" by someone who does not know.
431
-
432
- **Proposal.** `build_binary_partition_cdf(psum, out)` in
433
- [partition_cdf.js](../../../../format/image/avif/av1/entropy/partition_cdf.js), which is already the shared
434
- module for partition CDF selection. The scratch array stays per-module (correctly — it prevents
435
- reentrancy coupling), only the fill and the save/restore discipline move.
436
-
437
- ### A6 — `plane_residual_size`, one expression in seven places
438
-
439
- `SUBSAMPLED_SIZE[(size * 2 + sub_x) * 2 + sub_y]` appears at:
440
-
441
- - [decode_tile.js:572](../../../../format/image/avif/av1/decode/decode_tile.js#L572) (as a named function)
442
- - [encode_tile.js:525](../../../../format/image/avif/av1/encode/encode_tile.js#L525),
443
- [:1728](../../../../format/image/avif/av1/encode/encode_tile.js#L1728),
444
- [:2088](../../../../format/image/avif/av1/encode/encode_tile.js#L2088),
445
- [:2179](../../../../format/image/avif/av1/encode/encode_tile.js#L2179) (four times, inline)
446
- - [coefficient_context.js:50](../../../../format/image/avif/av1/entropy/coefficient_context.js#L50)
447
- - [loop_filter.js:110](../../../../format/image/avif/av1/filter/loop_filter.js#L110)
448
-
449
- **Proposal.** Export `plane_residual_size(mi_size, sub_x, sub_y)` from
450
- [derived_block_tables.js](../../../../format/image/avif/av1/tables/derived_block_tables.js) — which already
451
- exists for precisely this purpose ("Deriving beats transcribing: the two can never drift apart") —
452
- and use it in all seven sites.
453
-
454
- **And flag while you are there:** the decoder's `get_tx_size`
455
- ([decode_tile.js:963](../../../../format/image/avif/av1/decode/decode_tile.js#L963)) clamps a chroma
456
- transform of 64 down to 32/16x32/32x16; the encoder's `transform_size`
457
- ([encode_tile.js:2167](../../../../format/image/avif/av1/encode/encode_tile.js#L2167)) does not, and adds a
458
- `lossless → TX_4X4` case the decoder handles elsewhere. They may both be correct given the encoder's
459
- narrow syntax (D30: blocks 8x8 to 32x32, so 64 is unreachable) — but that is an invariant holding by
460
- accident across two files, which is exactly the shape of bug D30 exists to prevent. Either share the
461
- function with the encoder's extra cases guarded, or leave a comment on both stating the invariant.
462
-
463
- ### A7 — Losslessness derived twice, and the two do not agree
464
-
465
- [parse_frame_header.js:527](../../../../format/image/avif/av1/obu/parse_frame_header.js#L527)
466
- `compute_losslessness` walks all `MAX_SEGMENTS`, checks all five quantiser deltas, fills
467
- `lossless_array` and `seg_qm_level`, and derives `all_lossless` from `frame_width ===
468
- upscaled_width`.
469
-
470
- [build_headers.js:236](../../../../format/image/avif/av1/encode/build_headers.js#L236) does:
471
-
472
- ```js
473
- const lossless = frame.base_q_idx === 0;
474
- frame.coded_lossless = lossless ? 1 : 0;
475
- frame.all_lossless = frame.coded_lossless;
476
- frame.lossless_array.fill(lossless ? 1 : 0);
477
- ```
478
-
479
- Correct *today*, because the encoder emits no segmentation and no non-zero deltas (D30). It stops
480
- being correct the moment either is added — and `seg_qm_level` is never populated on the encode side
481
- at all, which matters if `using_qmatrix` is ever turned on.
482
-
483
- **Proposal.** Export `compute_losslessness` and have `build_headers` call it after setting
484
- `base_q_idx` and the deltas. The encoder's assumptions then become inputs to the shared derivation
485
- rather than a parallel implementation of it.
486
-
487
- ### A8 — Shared constants declared twice
488
-
489
- | Constant | Sites |
490
- | --- | --- |
491
- | `ALPHA_AUX_TYPE` | [decode_avif.js:15](../../../../format/image/avif/api/decode_avif.js#L15), [encode_avif.js:41](../../../../format/image/avif/api/encode_avif.js#L41) |
492
- | `QM_TOTAL_SIZE` | [av1_symbols.js:261](../../../../format/image/avif/av1/tables/av1_symbols.js#L261) (exported) and [reconstruct.js:33](../../../../format/image/avif/av1/decode/reconstruct.js#L33) (local copy of the same `3344`) |
493
- | `HLG_A/B/C` | [transfer_to_linear.js:50](../../../../format/image/avif/color/transfer_to_linear.js#L50), [linear_to_transfer.js:18](../../../../format/image/avif/color/linear_to_transfer.js#L18) |
494
-
495
- **Proposal.** `ALPHA_AUX_TYPE` → a shared `heif/aux_types.js` (there will be others: depth, gain
496
- map). `QM_TOTAL_SIZE` → delete the local copy, import the exported one. `HLG_A/B/C` → a small
497
- `color/hlg_constants.js`, or better, a `core/color/HLG/` module beside the existing `core/color/PQ/`
498
- — the two curves are peers and only one of them has a home today.
499
-
500
- ### A9 — `MAX_TX_DEPTH` is two different exported symbols
501
-
502
- [av1_symbols.js:176](../../../../format/image/avif/av1/tables/av1_symbols.js#L176) exports
503
- `MAX_TX_DEPTH = 2` (a scalar bound).
504
- [block_tables.js:12](../../../../format/image/avif/av1/tables/block_tables.js#L12) exports
505
- `MAX_TX_DEPTH = new Uint8Array([...])` (a 22-entry table).
506
-
507
- `decode_tile.js` imports the table under the alias `MAX_TX_DEPTH_TABLE`; `encode_tile.js` imports it
508
- unaliased. Nothing currently imports both into one module, so nothing is broken — but the next file
509
- that needs both gets a shadowing bug that will look like a table lookup returning a number.
510
-
511
- **Proposal.** Rename the generated table to `MAX_TX_DEPTH_BY_BLOCK` in the generator's naming rule
512
- (D22 — the generator is out of tree, so this is a rename in `block_tables.js` plus a note in
513
- `DECISIONS.md` recording the deviation from the spec's name), or rename the scalar to
514
- `MAX_TX_DEPTH_LIMIT`. Either way, not both.
515
-
516
- ### A10 — `clamp_unit`, `clamp_index`, and friends against `core/math`
517
-
518
- - `clamp_unit` is duplicated at
519
- [convert_to_rgba.js:217](../../../../format/image/avif/api/convert_to_rgba.js#L217) and
520
- [ycbcr_row_to_rgb.js:83](../../../../format/image/avif/color/ycbcr_row_to_rgb.js#L83) — and is
521
- byte-for-byte [core/math/clamp01.js](../../../../core/math/clamp01.js), which `loop_restoration.js`
522
- already knows how to import from (it uses `core/math/clamp.js`).
523
- - `clamp_index` is duplicated at
524
- [choose_restoration.js:622](../../../../format/image/avif/av1/encode/choose_restoration.js#L622) and
525
- [upsample_chroma_row.js:94](../../../../format/image/avif/color/upsample_chroma_row.js#L94) — same body,
526
- different parameter names.
527
- - [floor_log2.js:15](../../../../format/image/avif/av1/util/floor_log2.js#L15) is `31 - Math.clz32(x)`,
528
- which is [core/binary/msb_32.js](../../../../core/binary/msb_32.js) exactly. And it is
529
- *re-inlined* six more times in `SymbolReader`/`SymbolWriter` rather than called. (Six, not seven:
530
- `SymbolWriter.#normalize`'s `32 - Math.clz32(range)` is a bit *length*, libaom's `OD_ILOG_NZ`,
531
- not a floor log. It computes the same shift as the reader's `15 - floor_log2(x)` and is
532
- deliberately left alone — rewriting it would break the correspondence with the source it was
533
- transliterated from.)
534
- - [convert_to_rgba.js:199](../../../../format/image/avif/api/convert_to_rgba.js#L199) `quantise` and
535
- [encode_avif.js:415](../../../../format/image/avif/api/encode_avif.js#L415) `clamp_sample` are the same
536
- "round then clamp to `0..maximum`", differing only in whether the scale-up happens inside.
537
-
538
- **Proposal.** Import `clamp01` for `clamp_unit`. Extract one `clamp_index(index, extent)` beside
539
- `clamp01`. Make `floor_log2` an alias — `export const floor_log2 = msb_32;` with the AV1 spec
540
- docstring retained, since the spec-named alias is genuinely useful — and replace the six inline
541
- `31 - Math.clz32(...)` sites with calls. Unify `quantise` / `clamp_sample` into one function.
542
-
543
- **Note on the entropy coder.** `SymbolReader` says of itself that it is the hottest code in the
544
- decoder, and it bypasses `BitReader` for that reason, so replacing an inline expression with a call
545
- there needs evidence rather than taste. Three runs of the AVIF suite before and after: 13.77s,
546
- 15.19s, 12.74s against 13.03s, 12.91s, 12.77s. No regression, so the change stands; had there been
547
- one, the inline forms should have kept a comment naming them instead.
548
-
549
- The landed shape put `clamp_sample(value, maximum)` — round then clamp — in `format/image/avif/color/`,
550
- with `convert_to_rgba` keeping `quantise` as a one-line normalised wrapper over it, so the inner
551
- loop reads as it did.
552
-
553
- The `w`/`m` derivation for `ns(n)` is a fourth instance of the same theme: it appears at
554
- [BitReader.js:216](../../../../format/image/avif/bitstream/BitReader.js#L216),
555
- `BitWriter.ns`, [SymbolReader.js:286](../../../../format/image/avif/av1/entropy/SymbolReader.js#L286) and
556
- [SymbolWriter.js:283](../../../../format/image/avif/av1/entropy/SymbolWriter.js#L283). The two symbol-coder
557
- copies use the inline `clz32` form rather than calling `floor_log2`, which is how they drifted apart
558
- in style in the first place.
559
-
560
- ### A11 — Name collisions across unrelated modules
561
-
562
- Not duplication, but the same trap as A9 and worth one pass:
563
-
564
- - `filter_edge` — [loop_filter.js](../../../../format/image/avif/av1/filter/loop_filter.js) (a deblocking
565
- edge) and [predict_intra.js](../../../../format/image/avif/av1/predict/predict_intra.js) (an intra edge
566
- smoothing kernel). Unrelated jobs, identical name.
567
- - `restore_block` — [loop_restoration.js](../../../../format/image/avif/av1/filter/loop_restoration.js)
568
- (Wiener/SGR restoration) and
569
- [Av1BlockSnapshot.js](../../../../format/image/avif/av1/encode/Av1BlockSnapshot.js) (undo an encoder
570
- trial). Opposite meanings of "restore".
571
- - `MAX_SIDE` — 64 in `Av1BlockSnapshot.js`, a different 64 in `forward_transform_2d.js`.
572
-
573
- **Proposal.** Rename to the job: `filter_deblock_edge` / `filter_intra_edge`,
574
- `restore_lr_block` / `snapshot_restore_block`, `SNAPSHOT_MAX_SIDE` / `TRANSFORM_MAX_SIDE`. All six are
575
- file-local, so each rename is contained to one file.
576
-
577
- ### A12 — Table shape exports are almost entirely unused
578
-
579
- `block_tables.js` exports a `*_SHAPE` array next to every table (`SUBSAMPLED_SIZE_SHAPE`,
580
- `MAX_TX_DEPTH_SHAPE`, …). Exactly one is consumed outside the tables folder:
581
- `PARTITION_SUBSIZE_SHAPE` at
582
- [encode_tile.js:142](../../../../format/image/avif/av1/encode/encode_tile.js#L142), to derive a stride.
583
-
584
- **Proposal.** Either keep them (they are generated, they cost nothing at runtime, and they document
585
- the flattening) — in which case say so in `DECISIONS.md` so nobody prunes them — or have the
586
- generator emit `*_STRIDE` scalars for the multi-dimensional tables only, which is the thing callers
587
- actually want. I lean towards keeping and documenting: D24 validates the generated tables
588
- structurally, and the shapes are what that validation reads.
589
-
590
- ---
591
-
592
- ## 3. Inside the PNG loader
593
-
594
- ### P1 — `PNGReader` is prototype-style in an ES-class codebase
595
-
596
- [PNGReader.js:51](png/PNGReader.js#L51) is a `function` constructor with fourteen
597
- `PNGReader.prototype.x = function` assignments. Everything around it — `PNG`, `Codec`,
598
- `ColourTransform`, `ByteCursor`, `JpegImage` — is an ES class. The file also opens with
599
- `"use strict"` and a `/*global Uint8Array:true ArrayBuffer:true */` comment, both vestigial.
600
-
601
- **Proposal.** Straight mechanical conversion to `class PNGReader`. No behaviour change, no signature
602
- change. Worth doing as its own commit before any of the other PNG items so their diffs stay readable.
603
-
604
- ### P2 — Two inflate paths in one package
605
-
606
- [PNGReader.decodePixels:356](png/PNGReader.js#L356) constructs a `zlib.Inflate`, pushes chunks,
607
- and on error falls back to `inflateRaw` on the concatenated bytes with the 2-byte header stripped —
608
- about 30 lines with a well-argued comment about real-world encoders and libpng leniency.
609
-
610
- [inflate.js:8](png/inflate.js#L8) is a separate, simpler wrapper used only by the two text-chunk
611
- decoders, with no leniency and a different error shape (`throw new Error(inflator.err)` — a numeric
612
- code, not a message).
613
-
614
- **Proposal.** One `png_inflate(chunks, {lenient})` in `inflate.js` covering both. The multi-chunk
615
- concatenation is the general case; single-chunk is `[chunk]`. The lenient fallback stays opt-in and
616
- keeps its comment, which is the most valuable thing in that function.
617
-
618
- ### P3 — Unfilter functions have five different signatures
619
-
620
- | Filter | Signature |
621
- | --- | --- |
622
- | `None` | `(data, addr, out, out_offset, length, bit_depth)` |
623
- | `Sub` | `(data, addr, out, bpp, out_offset, length)` |
624
- | `Up` | `(data, addr, out, out_offset, prev_offset, length)` |
625
- | `Average` | `(data, addr, out, bpp, out_offset, prev_offset, length)` |
626
- | `Paeth` | `(data, addr, out, bpp, out_offset, prev_offset, length)` |
627
-
628
- Which forces [PNGReader.unFilter:576](png/PNGReader.js#L576) to be a 60-line `switch` that
629
- reassembles a different argument list per case.
630
-
631
- **Proposal.** One uniform signature — `(data, scanline_address, output, bytes_per_pixel,
632
- output_offset, output_offset_previous, length, bit_depth)` — with each filter ignoring what it does
633
- not need, and a frozen dispatch table `PNG_UNFILTER = [none, sub, up, average, paeth]`. `unFilter`
634
- becomes a bounds check plus `PNG_UNFILTER[filter_type](...)`.
635
-
636
- **The signature mess is hiding a bug — check this before refactoring.** `png_filter_unFilterNone` is
637
- doing two jobs: unfiltering *and* unpacking sub-byte samples (bit depths 1/2/4). No other filter
638
- unpacks. Two consequences follow from reading `interlaceNone`
639
- ([PNGReader.js:420](png/PNGReader.js#L420)) against it:
640
-
641
- - It passes `length = color_bytes_per_row`, a **byte** count, and `unFilterNone` treats that as a
642
- **sample** count when `bit_depth < 8`. A 16-pixel-wide 1-bit greyscale row has
643
- `color_bytes_per_row = 2`, so two samples get unpacked instead of sixteen — and `pixels` is
644
- allocated at `color_bytes_per_row * height`, so there is nowhere to put the other fourteen anyway.
645
- - Filters 1–4 write packed bytes into an output that `PNG.getUint8Data` then reads as unpacked
646
- samples, so any sub-byte-depth image using them is wrong on a second axis.
647
-
648
- `PNGReader.spec.js` tests bit depths 8 and 16 only — **no test anywhere uses depth 1, 2 or 4**, which
649
- is why this has survived. Write that test first; the refactor must not preserve the behaviour.
650
- Separating "unfilter" (byte-wise, all five filters) from "unpack samples" (a second pass, depth-aware)
651
- fixes both axes at once and is the right shape regardless of whether you agree with the diagnosis.
652
-
653
- **Confirmed, and there was a third axis.** The tests were written first and failed exactly as
654
- predicted: a 16-wide 1-bit row produced two samples, and a Sub-filtered 4-bit row came back as raw
655
- packed bytes. What the diagnosis missed is `interlaceAdam7`, which was worse — its bytes-per-pixel
656
- was `colors * bitDepth / 8`, a *fraction* below eight bits, and every length and buffer size derived
657
- from it. Seven new decoder tests cover it now, two of them rebuilding an 8x8 image from all seven
658
- Adam7 passes at 1 and 4 bits.
659
-
660
- ### P4 — `PNGReader` carries two cursors and uses one
661
-
662
- The constructor sets `this.i = 0` ([PNGReader.js:56](png/PNGReader.js#L56)) *and* builds a
663
- `BinaryBuffer` with its own `position`. `this.i` is written once, read once (in `decodeHeader`'s
664
- `if (this.i !== 0) throw`) and never advanced — a dead cursor guarding a condition that can never be
665
- false.
666
-
667
- **Proposal.** Delete `this.i` and the guard. `this.header` (an 8-byte `Uint8Array` field) can be a
668
- local in `decodeHeader`.
669
-
670
- ### P5 — `crc_enabled` defaults off with a `console.warn` on failure
671
-
672
- Covered in C7. Restating here because it is a PNG-side decision that deserves a written rationale in
673
- the same place the other PNG decisions will live.
674
-
675
- ### P6 — Text chunks are decoded and thrown away
676
-
677
- `decodeChunk` calls `png_chunk_decode_zTXt(chunk)` and `png_chunk_decode_iTXt(chunk)`
678
- ([PNGReader.js:212](png/PNGReader.js#L212)) and **discards both return values**. `png.text` is only
679
- ever written by `decodetEXt` / `decodeiEXt`, which the dispatch switch never reaches (D2 below).
680
-
681
- So the PNG decoder pays for inflating every compressed text chunk and keeps none of it.
682
-
683
- **Proposal.** Either wire the results into `png.text` (three lines, and it makes the `text` field
684
- mean something), or stop calling them from `decodeChunk` and keep the two decoders as a library the
685
- metadata path can use when there is one. Pick one; the current state is the cost of the first with
686
- the benefit of neither.
687
-
688
- ---
689
-
690
- ## 4. Inside the JPEG loader
691
-
692
- ### J1 — `JpegImage.parse` is one function doing six jobs
693
-
694
- 250 lines, four nested closures capturing a mutable `offset`, a `switch` with sixteen cases, two
695
- error-recovery paths, and the component-preparation pass all in one scope. There is also a
696
- duplicated `case 0xFFEC:` (APP12 appears twice, [JpegImage.js:161](jpeg/JpegImage.js#L161) and
697
- [:162](jpeg/JpegImage.js#L162)) — harmless, but a symptom.
698
-
699
- **Proposal.** Covered structurally by C5.2. Concretely: `for_each_jpeg_segment` walker, then
700
- `parse_app_segment`, `parse_dqt`, `parse_sof`, `parse_dht`, `parse_sos`, `prepare_components` as
701
- free functions taking explicit state. `prepareComponents` in particular is a pure function of a
702
- `JpegFrame` and belongs in its own file next to `JpegFrame.js`.
703
-
704
- ### J2 — `for...in` over `frame.components` with `hasOwnProperty` guards
705
-
706
- `frame.components` is a plain object keyed by numeric component id, iterated with `for (componentId
707
- in frame.components)` plus `hasOwnProperty` checks
708
- ([JpegImage.js:74](jpeg/JpegImage.js#L74)), and later mutated with `delete cp[j].quantizationIdx`
709
- ([JpegImage.js:390](jpeg/JpegImage.js#L390)). `frame.componentsOrder` already carries the ids in
710
- order.
711
-
712
- **Proposal.** `components` becomes a `Map<number, JpegFrameComponent>`, or the array indexed by
713
- `componentsOrder` that the code already maintains alongside it. The `delete` goes away — a
714
- `quantizationIdx` that has been resolved into `quantizationTable` can simply stay, or be set to
715
- `-1`. Deleting a property to mark state is a deopt and a lie about the struct's shape, and
716
- `JpegFrameComponent` is a declared class with declared fields
717
- ([JpegFrameComponent.js](jpeg/JpegFrameComponent.js)) — the delete contradicts it.
718
-
719
- ### J3 — `getData` and `copyToImageData` both switch on component count
720
-
721
- [JpegImage.js:421](jpeg/JpegImage.js#L421) `getData` has arms for 1, 2, 3, 4 components.
722
- [JpegImage.js:561](jpeg/JpegImage.js#L561) `copyToImageData` has arms for 1, 3, 4 — no 2, so a
723
- two-component PDF-style JPEG parses and then throws at the last step. The CMYK arm inverts twice
724
- (`255 - C` inside `getData`, `255 - clampTo8bit(C * (1 - K/255) + K)` in `copyToImageData`), which is
725
- correct but only legible if you read both functions together.
726
-
727
- **Proposal.** After C3 and C4 land, `getData` produces normalised RGB (or RGBA) rows directly and
728
- `copyToImageData` becomes the channel-expansion call from C2. The component-count switch survives in
729
- exactly one place: choosing the colour transform.
730
-
731
- ### J4 — `clampTo8bit` against `core/math`
732
-
733
- [JpegImage.js:39](jpeg/JpegImage.js#L39) is `clamp(a, 0, 255)` with a nested ternary.
734
- `core/math/clamp.js` exists and `core/binary/float_to_uint8.js` exists. Use one.
735
-
736
- ---
737
-
738
- ## 5. Dead code
739
-
740
- Removing these first makes every other item in this document a smaller diff.
741
-
742
- | # | What | Lines | Evidence |
743
- | --- | --- | --- | --- |
744
- | D1 | [PNG.js:188](png/PNG.js#L188) `getPixel`, [:257](png/PNG.js#L257) `getRGBA8Array_fromRGB`, [:282](png/PNG.js#L282) `getRGBA8Array_generic`, [:303](png/PNG.js#L303) `getRGBA8Array` | ~120 | No importer, no spec. Only `getUint8Data` is called. |
745
- | D2 | [PNGReader.js:260](png/PNGReader.js#L260) `decodetEXt`, [:273](png/PNGReader.js#L273) `decodeiEXt` | ~35 | `decodeChunk`'s switch has no `tEXt` or `iTXt`→`decodeiEXt` arm. Also **both are wrong if revived**: `decodetEXt` reads the value with `readASCIICharacters(keyword.length - 1)` (the *keyword's* length), and `decodeiEXt` calls `readUTF8String()`, which reads a 4-byte length prefix PNG does not have. |
746
- | D3 | [prototypePNG.js](png/prototypePNG.js) | 37 | A hard-coded-path browser demo with top-level side effects, sitting in `src/`. No importer. |
747
- | D4 | [idct8x8_fixed.js](jpeg/idct8x8_fixed.js) | 213 | No importer. `buildComponentData` uses `idct8x8_float`. If it is kept as a documented alternative, say so in a header comment and give it a spec — otherwise the MBOIT-chunk precedent (guarded + spec'd + a note saying why) is the bar. |
748
- | D5 | [PNGReader.js:56](png/PNGReader.js#L56) `this.i` | 3 | P4. |
749
- | D6 | [JpegImage.js:162](jpeg/JpegImage.js#L162) duplicate `case 0xFFEC` | 1 | J1. |
750
-
751
- Total: roughly 410 lines, none of it reachable.
752
-
753
- ---
754
-
755
- ## 6. What *not* to unify
756
-
757
- Recording these so the document cannot be read as arguing for them.
758
-
759
- - **`ByteCursor` into `BinaryBuffer`.** D10 settled this: growable stream for writing, zero-copy
760
- `DataView` cursor for reading. The reasoning still holds, and `ByteCursor.sub_cursor` has no sane
761
- `BinaryBuffer` equivalent. C8 shares the UTF-8 codec, not the cursor. What *is* worth doing is
762
- making the naming rhyme — `ByteCursor.u32()` versus `BoxWriter.u32()` versus
763
- `BinaryBuffer.readUint32BE()` are three spellings of one operation, and `BoxWriter` already chose
764
- the cursor's spelling for its own methods, so the cursor pair is self-consistent and only
765
- `BinaryBuffer` reads differently. Leave it.
766
- - **`encode_tile` into `decode_tile`.** D30 settled this. The traversal is mirrored deliberately;
767
- inverting the decoder's control flow to serve both would be worse. Every item in section 2 shares
768
- *derived state*, which is precisely what D30 asks for, and none of them touches traversal.
769
- - **`BitReader` / `BitWriter` into one class.** They are a clean symmetric pair with no shared
770
- implementation to extract — A10's `ns` width derivation is the only overlap and that is four
771
- lines.
772
- - **A single generic TLV walker for boxes, chunks and markers.** See C5. Share the contract, not the
773
- implementation.
774
- - **The generated tables.** D4 and D22 settled provenance; A9 and A12 are naming and documentation
775
- items, not proposals to hand-edit generated output.
776
-
777
- ---
778
-
779
- ## 7. Sequencing
780
-
781
- Ordered by value per unit of risk. Each row is independently landable.
782
-
783
- | Order | Items | Effort | Risk | Why here |
784
- | --- | --- | --- | --- | --- |
785
- | 1 | **D1–D6** (dead code) | ~1h | none | Shrinks every later diff. |
786
- | 2 | **A3, A4, A5, A8, A10** (verbatim AVIF duplicates) | ~2h | none | Pure deletion + import. Fixture round-trips cover it. |
787
- | 3 | **A1, A2** (tile limits, CDF adapt) | ~2h | low | Highest-consequence duplicates: divergence here is a bitstream desync. Do them while the memory of section 2 is fresh. |
788
- | 4 | **P1, P4, C6** (PNG class conversion, dead cursor, checked reads) | ~3h | low | Mechanical, and C6 needs P1's shape to be clean. |
789
- | 5 | **C7, C8** (crc32 and UTF-8 to `core`) | ~2h | low | Unblocks USDZ CRC verification as a side effect. |
790
- | 6 | **C1** (`Codec` registration) | ~1d | low | The one that changes what the engine can load. Everything above makes it a smaller change. |
791
- | 7 | **A6, A7, A9** (plane_residual_size, losslessness, MAX_TX_DEPTH) | ~half day | low–med | A7 needs a decision about whether the encoder's assumptions become inputs or stay assumptions. |
792
- | 8 | **P2, P3, J1, J2** (PNG inflate, filter table, JPEG parse split) | ~1d | med | P3 wants the sub-byte-depth test from section P3 written **first**. |
793
- | 9 | **C3, C4, C2** (colour transform, upsampling, channel expansion to `core`) | ~2d | med | The largest win and the largest blast radius. Needs the JPEG fixtures extended first; do it last, when JPEG is reachable (C1) and therefore testable end to end. |
794
- | 10 | **A11, A12, P5, P6** (naming, docs, text chunks) | ~half day | none | Housekeeping; can slot in anywhere. |
795
-
796
- Items 1–5 are about a day and a half and are all deletions or moves. If only one thing from this
797
- document gets done, do row 1 and row 3.
798
-
799
- ---
800
-
801
- ## 8. One thing this review did not do
802
-
803
- I read `src/format/image/avif/DECISIONS.md` before writing, and several proposals here are shaped by it —
804
- sections 2 and 6 in particular. **PNG and JPEG have no equivalent.** There is a `jpeg/NOTES.md` with
805
- two URLs in it and nothing else.
806
-
807
- That asymmetry is itself a finding. The AVIF tree's `crc_enabled`-style choices are all written
808
- down; the PNG tree's are not, which is why C6 (unchecked reads), P5 (CRC off by default) and P6
809
- (text decoded and discarded) each read as "possible oversight" rather than "decision". Some of them
810
- almost certainly *are* decisions — a game engine that warns on a bad CRC instead of refusing the
811
- texture is making a defensible call — but nothing in the tree says so, so the next person to touch it
812
- has to guess.
813
-
814
- Whatever subset of this document gets acted on, a `src/engine/asset/loaders/image/DECISIONS.md`
815
- recording the ones that get **rejected** is worth as much as the refactors that get accepted.