@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,964 +0,0 @@
1
- # Render extensions — design
2
-
3
- Replaces `Renderer.add_pass` / `remove_pass`, `FramePhase`, `FramePassContext` and
4
- `FramePassRegistry` (MEEP 3, phase 2, item S3).
5
-
6
- Status: **design, not built.** Nothing in §3 through §11 is committed code.
7
-
8
- **Revised 2026-08-24** against the frame-graph work that has landed since the first draft:
9
- `FrameGraphState`, `validate()`, recording scopes, record-time producers, and the `exportToJson`
10
- fix. Those came out of this document's §4 and §13 and are tracked in
11
- `engine/graphics/render/frame_graph/FRAME_GRAPH_RECOMMENDATIONS.md`. §1.2, §3.4, §4, §5 and §13
12
- describe what the graph does now, not what it did — the net effect is that the design leans on the
13
- graph in three more places and asks it for nothing.
14
-
15
- The same revision **withdrew the cull diagnostic** and the engine change it needed, on sponsor
16
- review: culling is what a frame graph is for, and a check that calls it a failure when an extension
17
- is the recorder invents a distinction that does not exist and outlaws the speculative producer.
18
- §5.1 is the argument; the bar it sets — a check may only fire on something that cannot be true —
19
- also took out a provenance check in §3.4.
20
-
21
- **Revised 2026-09-16: `after` / `before` are gone, replaced by `relative_order`.** They were
22
- built as specified, shipped, and then did the thing §1.4 rejected integer offsets to avoid — from
23
- the other direction. A constraint over classes is an import: `DebugDrawSystem`, which draws
24
- developer gizmos and knows nothing about anything, had to import `FogOfWarExtension` in order to
25
- say "on top". That is a generic extension acquiring a dependency on a game-specific one, at module
26
- scope, to express a fact about itself. `relative_order` is a number that defaults to 0 and names
27
- nothing: lower records first, negative pulls forward, positive pushes back. It is **less precise**
28
- and that is the trade — it cannot say "immediately after that one", and there is no cycle to detect
29
- because there is no graph. §1.4, §3.1, §3.6, §5, §7, §8 and §11 are updated; §3.6's ambiguity check
30
- was never built and is unaffected.
31
-
32
- **Revised 2026-09-24: trails left the `AfterTransparency` chain.** §9's first known limit — an
33
- extension cannot take part in OIT — still holds for extensions, and it is why trails moved out: a
34
- trail drawn at `AfterTransparency` covered every transparent surface whatever its depth. They are now
35
- a *transparency channel* (`Renderer.add_transparency_channel`, `GraphicsEngine.add_transparency_channel`),
36
- drawn inside the AVBOIT pipeline beside the particles; see the AVBOIT ledger, entries 22 and 23. The
37
- chain in §8's table is five deep, and the fog still covers trails, by construction: it runs after the
38
- transparency resolve they are part of. So does the rest of the chain, which is the one visible
39
- reordering — path display and the highlight, which trails used to draw over, now draw over trails.
40
-
41
- ---
42
-
43
- ## 0. Summary
44
-
45
- An extension is a **named object with declarations** — a phase, where in that phase it sits, the
46
- optional frame products it needs — and one method that records into the frame's graph. Everything it exchanges
47
- with the frame goes through **typed per-frame records** holding frame-graph handles: it reads a
48
- handle out of a record and writes a replacement back. There is no return value and no privileged
49
- resource.
50
-
51
- Three properties follow, and they are the point:
52
-
53
- 1. **Every resource the frame carries is exchangeable**, not just the colour. The G-buffer stops
54
- being a documented special case, depth stops being read-only, and an extension can publish a
55
- record of its own that another extension reads.
56
- 2. **Order is declared**, not emergent from the order in which ECS systems happened to reach their
57
- first `await`.
58
- 3. **The frame can be described without being rendered, and read back attributed once it has been.**
59
- The set of extensions, their order per phase, and the products they force on are data, so
60
- `describe()` prints the pipeline; and the registry records each extension inside its own graph
61
- scope, so a DOT export of the frame groups by who asked for what instead of by whatever the
62
- `graph_*` helpers named things. That pair is what "the frame graph is a first-class construct"
63
- has to mean at this seam.
64
-
65
- Underneath all three, one correction that is not an abstraction: **the upscale is a phase boundary
66
- and the surface says so.** Three phases are before it and two are after, the before case is the
67
- common one, and the size of what an extension is drawing into is derived from the live handle rather
68
- than handed over as a field the renderer fills in once for the whole frame — which is what the
69
- current mechanism does, and gets wrong at two of its five phases (§1.3b).
70
-
71
- The shape is Unity 6 URP's — `ContextContainer` / `ContextItem`, `ScriptableRenderPass`,
72
- `ConfigureInput` — with URP's two worst traits removed (§2).
73
-
74
- ---
75
-
76
- ## 1. What is wrong with `add_pass`
77
-
78
- ### 1.1 One resource is modifiable
79
-
80
- `FramePassRegistry.run` threads exactly one value:
81
-
82
- ```js
83
- const produced = passes[i](context);
84
- if (produced === undefined) { continue; }
85
- context.color = produced;
86
- ```
87
-
88
- A pass's only sanctioned output is a colour handle. Everything else on `FramePassContext` — depth,
89
- previous depth, the four G-buffer targets, the two visibility targets — is an input. The one
90
- exception, the G-buffer being in/out at `AfterGBuffer`, is implemented by letting the pass reach into
91
- `context.gbuffer` and assign, and costs three paragraphs of prose in `FramePassContext` to explain
92
- why replacing beats writing in place.
93
-
94
- What that closes off, concretely: writing depth (terrain and water both rasterize geometry that
95
- should own depth), publishing a resource an extension computed for other extensions to read (fog of
96
- war's visibility grid is exactly this and currently lives outside the graph), and reading anything
97
- the frame builds but the struct does not name — velocity, the Hi-Z pyramid, the TAA history, the
98
- shadow atlas, the light cluster tables.
99
-
100
- The struct is closed by construction: adding a field is an edit to Shade. That is the wrong cost for
101
- "my game needs one more thing from the frame".
102
-
103
- ### 1.2 The graph is an escape hatch, not the mechanism
104
-
105
- `context.graph` hands the pass the `FrameGraph` and steps back. The registry never learns what the
106
- pass read or wrote, so:
107
-
108
- - **It cannot order passes.** It orders by registration order.
109
- - **It cannot attribute anything.** `exportToDot` shows the nodes an extension created with whatever
110
- names its helpers gave them, unrelated to the extension. The graph has since gained recording
111
- scopes, which are exactly the missing mechanism — but they are opt-in and `add_pass` opens none,
112
- so a frame's DOT export is still one flat wall of passes.
113
- - **It cannot validate.** Reading `context.color` at `AfterGBuffer` yields `-1` and fails somewhere
114
- inside a binding builder. Retaining a handle across frames is undetectable. Publishing a stale
115
- handle is now caught, but by the graph at `compile()` and against the *reader* — the pass that
116
- consumed it — so the report names the built-in pass that broke, never the extension that handed
117
- it the bad handle.
118
- - **`AfterGBuffer` has no return channel, so the culling contract cannot be met there.** The rule
119
- itself is the graph's, and it is right everywhere: work nothing consumes is elided. But the only
120
- way this mechanism lets a pass thread its result into the frame is by returning a colour handle,
121
- and at `AfterGBuffer` there is no colour — so a pass whose G-buffer work *should* be consumed
122
- reaches for `make_side_effect()` instead, which does not say "my output is used", it says "run me
123
- anyway". Three paragraphs of `FramePhase` document the workaround. The record model removes the
124
- reason for it (§3.3) and leaves the culling rule alone (§5.1).
125
-
126
- And the injection points themselves are call sites: a phase is wherever `#run_frame_passes` was
127
- typed inside an 800-line method. There are five because five were typed.
128
-
129
- ### 1.3 What this has already produced
130
-
131
- Four findings from reading the current code, each independent of the design that replaces it.
132
-
133
- **(a) Order is an accident, and the code says so.** `TerrainSystem.startup`:
134
-
135
- > `// before anything is awaited, so that pass order within the phase is the profile's system order —`
136
- > `// the decal composite has to see the textured ground, not precede it`
137
-
138
- An ordering requirement between two extensions is being met by placing a registration call above an
139
- `await`. Adding an `await` to a system's startup silently reorders the frame.
140
-
141
- The same mechanism decides the six-deep chain at `AfterTransparency`, and **it has already produced a
142
- shipped bug**. In profile order that chain is fog of war, particles, path display, highlight, debug
143
- draw, trails — so the fog composites *first* and everything drawn after it is unfogged. Confirmed
144
- with the sponsor: that is not intended. Fog of war conceals, so it belongs over everything it
145
- conceals; particles drawing on top of it defeat what it is for.
146
-
147
- Nobody chose that order. Nothing recorded it. Nothing failed when it turned out wrong. That is the
148
- argument for §3.6 in one paragraph.
149
-
150
- **(b) The one size the context states is wrong at two phases.** `Renderer` fills
151
- `pass_context.resolution` from `resolution_internal.asArray()`, once, for all five phases. But TAA
152
- and NSS upscale to `output_resolution` before `BeforePresent`, and the tonemap writes the canvas at
153
- output resolution — so at `BeforePresent` and `Overlay` the colour is *not* the size the context
154
- reports, whenever TAA is on. With TAA off it is, because the upscale is missing (there is a `TODO`
155
- in `Renderer` saying so). The size of the frame's colour at `BeforePresent` is therefore not
156
- derivable from the context at all; it depends on a renderer feature flag the extension cannot see.
157
-
158
- `probeShadePassInjection` — the gate for the current mechanism — does
159
- `output_resolution: context.resolution` at `BeforePresent`. It passes because the probe runs at 100%
160
- scale. The game ships with dynamic resolution scaling enabled against a 30 fps floor, and at any
161
- scale below 100% that pass produces a cropped, wrongly-scaled frame while the gate still measures a
162
- luminance change and reports green.
163
-
164
- The single field is the tell: the surface was built as though a frame had one resolution. Three of
165
- the five phases are before the upscale and two are after it, and nothing in the surface says which
166
- is which — `FramePhase`'s own text for `BeforePresent` says "still HDR" and never mentions that the
167
- colour has been upscaled by then. §3.5 makes the boundary part of what a phase *is*.
168
-
169
- **(c) `-1` is a sentinel with no guard.** `color` is `-1` at `AfterGBuffer`. Nothing stops a pass
170
- reading it. The house rule is that the common case is the only case and preconditions are asserted,
171
- not encoded as magic values.
172
-
173
- **(d) The G-buffer special case exists only because colour owns the return channel.** Given a
174
- uniform way to publish any handle, "the G-buffer is in/out at one phase" stops being a rule and
175
- becomes an instance of the general one.
176
-
177
- ---
178
-
179
- ## 2. What is borrowed from URP, and what is not
180
-
181
- Borrowed:
182
-
183
- | URP 6 | Here | Why |
184
- | --- | --- | --- |
185
- | `ContextContainer` + `ContextItem` | `FrameContext.get(RecordType)` + frame records | The open, typed resource vocabulary. This is the fix for §1.1, and the reason it is keyed by type rather than by string is that the house rule forbids string-keyed registries. |
186
- | `UniversalResourceData` | `SceneColor`, `GBufferTextures`, `ViewTextures`, `PresentTarget` | Built-in resources are ordinary classes with named fields, carried in the same container as anyone else's. One mechanism, not two. |
187
- | `ScriptableRenderPass` | `RenderExtension` | A declaring object, not a closure. Declarations are what makes ordering, validation and description possible. |
188
- | `ConfigureInput(ScriptableRenderPassInput.Motion)` | `RenderExtension.requires` + `FrameProduct` | An extension states what the frame must produce and the renderer turns it on. This is the answer to "velocity exists, is built, and is unreachable". |
189
- | Renderer feature list, ordered | The registry's resolved order, printable | The pipeline is inspectable before it runs. |
190
-
191
- Not borrowed, deliberately:
192
-
193
- - **`RenderPassEvent` + integer offsets.** URP orders same-event passes by an `int` you add to the
194
- enum. It is the same defect as registration order with an extra step: unreadable, unstable under
195
- insertion, and it makes every ordering question a numerology question. Replaced by explicit
196
- `after` / `before` constraints over extension classes (§3.6).
197
-
198
- > *Reversed, 2026-09-16.* Constraints over classes were built and are gone; §3.6 is now an offset
199
- > within the phase. What this entry got right is that an offset is imprecise. What it missed is
200
- > that it is imprecise *and names nothing*, where a constraint is precise and is an import — and
201
- > an import is how a debug overlay ends up depending on the fog of war. The enum-offset objection
202
- > also does not carry over: URP's `int` is an offset into a 17-value event enum, where a phase
203
- > here is one of six coarse points and the offset orders only what shares it.
204
- - **`SetGlobalTexture` / global shader state.** An implicit channel between passes that the graph
205
- cannot see. Records are the channel.
206
- - **`AddUnsafePass`.** URP needs a legacy `CommandBuffer` escape hatch. There is no legacy to carry
207
- here, and adding an unchecked door on day one guarantees it becomes the door.
208
- - **Two-level feature/pass split.** URP splits `ScriptableRendererFeature` from
209
- `ScriptableRenderPass` because a feature is a `ScriptableObject` living on an asset. That reason
210
- does not exist here, so there is one class.
211
-
212
- ---
213
-
214
- ## 3. The model
215
-
216
- ### 3.1 `RenderExtension`
217
-
218
- ```js
219
- export class RenderExtension {
220
- /**
221
- * Stable identity. Used in error decoration and `describe()`, and the tie-break within one
222
- * `relative_order`.
223
- * @type {string}
224
- */
225
- name = "";
226
-
227
- /** @type {FramePhase} */
228
- phase = FramePhase.AfterTransparency;
229
-
230
- /**
231
- * Where in the phase this records. Lower first; any finite number, negative to pull forward.
232
- * It names no other extension, which is the point.
233
- * @type {number}
234
- */
235
- relative_order = 0;
236
-
237
- /**
238
- * Optional frame products this extension reads. A bitmask of {@link FrameProduct}.
239
- * Reading a product nobody required is an assert failure, not a silent `-1`.
240
- * @type {number}
241
- */
242
- requires = 0;
243
-
244
- /**
245
- * Record this extension's work into the frame. Called once per frame, when the phase comes
246
- * round. Returns nothing: everything this produces reaches the rest of the frame by
247
- * publication (§3.4).
248
- *
249
- * @param {FrameContext} frame
250
- */
251
- record(frame) {}
252
- }
253
- ```
254
-
255
- `record` returning `void` is load-bearing. It is what makes colour stop being privileged.
256
-
257
- An extension that has nothing to draw this frame returns without recording; that is the only "off"
258
- switch and there is no `enabled` flag beside it.
259
-
260
- ### 3.2 `FrameContext`
261
-
262
- ```js
263
- export class FrameContext {
264
- /** @type {FrameGraph} the frame's graph — record into this with the `graph_*` helpers */
265
- graph;
266
- /** @type {ViewRenderContext} */
267
- view;
268
- /** @type {FramePhase} */
269
- phase;
270
-
271
- /** @template T @param {new()=>T} type @returns {T} throws if not present at this phase */
272
- get(type) {}
273
-
274
- /** @param {Function} type @returns {boolean} */
275
- has(type) {}
276
-
277
- /** @template T @param {new()=>T} type @returns {T} adds a record this extension owns */
278
- create(type) {}
279
-
280
- /**
281
- * Pixel size of the target this phase carries, derived from the live handle rather than filled
282
- * in by the renderer.
283
- * @returns {number[]}
284
- */
285
- get resolution() {}
286
-
287
- /** @param {number} handle @returns {ResourceDescriptor} size, format and usage of a handle */
288
- describe(handle) {}
289
- }
290
- ```
291
-
292
- Three accessors rather than one `get_or_create`, because "absent" has to stay distinguishable from
293
- "empty" — that is what lets `get(SceneColor)` at `AfterGBuffer` throw with a sentence instead of
294
- handing back `-1`.
295
-
296
- **`resolution` is derived, and that is the whole of the fix for defect (b).** It reads the graph
297
- through the handle this phase's target actually is, so it is right before the upscale, right after
298
- it, right when TAA is off and the upscale does not happen at all, and right at every dynamic
299
- resolution scale. The common case — "how big is the thing I am drawing into" — stays one property
300
- access and cannot be wrong.
301
-
302
- `describe(handle)` is the general form, and it is needed whenever two things in one pass are
303
- different sizes: an extension at `BeforePresent` reading the G-buffer is working across the upscale
304
- boundary and has to ask for both.
305
-
306
- > Both lean on descriptors being trustworthy, which was checked: transient and imported resources
307
- > both carry a real `TextureResourceDescriptor` holding absolute pixels.
308
- > `TextureResourceDescriptor.resolution_domain` says otherwise by default and is vestigial — nothing
309
- > branches on it, only `hash()` folds it in. It stays; it is now documented as vestigial on the field
310
- > itself so the next reader does not take it for a contract.
311
-
312
- ### 3.3 Frame records
313
-
314
- A record is a plain class holding frame-graph handles. The renderer publishes the built-in ones as
315
- the frame reaches the phase where they are meaningful.
316
-
317
- ```js
318
- /** present from AfterGBuffer */
319
- class GBufferTextures { albedo; normal; pbr; emissive; }
320
-
321
- /** present from AfterGBuffer */
322
- class ViewTextures {
323
- depth; depth_previous;
324
- visibility_mesh; visibility_triangle;
325
- velocity; // requires FrameProduct.Velocity
326
- }
327
-
328
- /** present from AfterLighting through BeforePresent */
329
- class SceneColor {
330
- color;
331
- copy; // requires FrameProduct.ColorCopy
332
- }
333
-
334
- /** present at Overlay, and only there */
335
- class PresentTarget { canvas; }
336
- ```
337
-
338
- Presence by phase:
339
-
340
- | phase | records | resolution |
341
- | --- | --- | --- |
342
- | `FrameStart` | — (the view only) | internal |
343
- | `AfterGBuffer` | `GBufferTextures` (writable), `ViewTextures` | internal |
344
- | `AfterLighting` | `GBufferTextures` (frozen), `ViewTextures`, `SceneColor` | internal |
345
- | `AfterTransparency` | as above | internal |
346
- | `BeforePresent` | as above — but `SceneColor` and `GBufferTextures` are now different sizes | output |
347
- | `Overlay` | `PresentTarget` | output |
348
-
349
- Two of those rows are decisions worth naming.
350
-
351
- **`SceneColor` is absent at `Overlay`, and `PresentTarget` is not `SceneColor`.** The current
352
- mechanism hands the tonemapped colour to `Overlay` passes and then documents, at length, that they
353
- must not read it — a texture cannot be a render attachment and a sampled source in the same pass.
354
- That was found by measurement after the fact. Handing back a differently-named record with one field
355
- says the same thing structurally: there is no `color` at `Overlay` to be tempted by, and an
356
- extension that wants the finished image as *input* discovers at `get` time that it belongs at
357
- `BeforePresent`.
358
-
359
- **`GBufferTextures` is present but frozen after `AfterGBuffer`.** Outline and highlight effects
360
- legitimately read the G-buffer and the visibility buffer late. Publishing into it late is
361
- meaningless — shading has run — so the record is frozen and the assert says why (§5). At
362
- `BeforePresent` it is also on the far side of the upscale from the colour (§3.5), so the two are
363
- different sizes; `describe()` is how a pass learns the ratio, and the record's own doc comment says
364
- to expect it.
365
-
366
- Extensions add their own records with `create`:
367
-
368
- ```js
369
- export class FogOfWarVisibility { grid; } // a graph buffer handle
370
-
371
- // producer, at FrameStart
372
- frame.create(FogOfWarVisibility).grid = this.#update_grid(frame.graph);
373
-
374
- // consumer, at AfterTransparency
375
- if (frame.has(FogOfWarVisibility)) { ... }
376
- ```
377
-
378
- Which is the second half of the fix for §1.1: the vocabulary is open, and extending it costs no
379
- edit to Shade.
380
-
381
- ### 3.4 Publication
382
-
383
- A field on a record is an accessor. Writing it is publication:
384
-
385
- ```js
386
- const scene = frame.get(SceneColor);
387
-
388
- const [fogged] = graph_image_pass({
389
- graph: frame.graph,
390
- shader: this.#shader,
391
- output_resolution: frame.describe(scene.color).resolution,
392
- inputs: { input_color: scene.color, input_depth: frame.get(ViewTextures).depth }
393
- });
394
-
395
- scene.color = fogged; // the rest of the frame carries this
396
- ```
397
-
398
- The setter asserts three things, all free in production:
399
-
400
- 1. The record is not frozen (§3.3), and the frame index matches — a handle captured last frame
401
- cannot be published this frame.
402
- 2. `graph.is_valid_resource(handle)` — the handle is the current version of its resource.
403
- 3. The current phase is one where the record is writable.
404
-
405
- Check 2 exists for **attribution**, not detection. The graph catches a superseded handle on its
406
- own: whoever reads it next trips `validate()`'s superseded-read check at `compile()` (§4). But that
407
- report names the *reader* — some built-in pass downstream — and says nothing about which extension
408
- put the bad handle into the record. Checking at the setter fails at the line that caused it, with
409
- the extension's name on it. It forbids nothing: one `ResourceEntry` owns one concrete resource, so
410
- an older node is not a snapshot of older contents and publishing one is wrong in every case.
411
-
412
- **A provenance check was considered here and rejected** — "the handle's producer is a pass recorded
413
- inside this extension's scope", which the record-time producer (§4) made possible. It would have
414
- forbidden a legitimate thing: `import_resource` leaves a node with no producer, so an extension that
415
- imports a texture it owns and publishes it as the frame's colour — a debug view, video playback —
416
- would have failed a check aimed at typos. What it uniquely caught, publishing a handle you read
417
- rather than wrote, surfaces immediately downstream anyway.
418
-
419
- Reading a field asserts frame identity and, for product-gated fields, that the *recording extension*
420
- declared the product (§3.7).
421
-
422
- **Publication is not the dependency edge.** The graph already has the edge: `FramePassBuilder.write`
423
- renames a written resource into a versioned clone and records the read, so "this pass consumed the
424
- colour as it stood and produced a new one" is already a graph fact. Publication is the *naming*
425
- step — which handle currently means "the scene colour". Keeping those two ideas separate is what
426
- lets the record model be this small.
427
-
428
- ### 3.5 Phases
429
-
430
- `FrameStart`, `AfterGBuffer`, `AfterLighting`, `AfterTransparency`, `BeforePresent`, `Overlay`.
431
-
432
- **The upscale is a phase boundary, and saying so is part of what a phase means.** `AfterGBuffer`,
433
- `AfterLighting` and `AfterTransparency` are before it and run at internal resolution; TAA and NSS
434
- upscale to output resolution, so `BeforePresent` and `Overlay` are after it and run at output
435
- resolution.
436
-
437
- **Before the upscale is the common case, and the design leans on it.** An extension drawing into the
438
- scene belongs at `AfterTransparency`: its work is then temporally resolved, upscaled and sharpened
439
- along with everything else, and it is the cheapest place to draw because it draws at internal
440
- resolution. `BeforePresent` is the deliberate other choice — full output resolution, still HDR,
441
- nothing after it but the tonemap — and it is a trade, not an upgrade: work there gets no temporal
442
- history and costs full-resolution pixels. An extension that does not have a reason to be after the
443
- upscale should not be.
444
-
445
- One wrinkle, and it belongs to the renderer rather than to this design: with
446
- `feature_taa_enabled === false` the upscale does not happen at all — there is a `TODO` in `Renderer`
447
- saying exactly that — so in that configuration `BeforePresent` is at internal resolution. A derived
448
- `frame.resolution` (§3.2) is right in both configurations. A documented constant would be wrong in
449
- one, which is how the current mechanism ended up wrong in two.
450
-
451
- Five phases are the existing ones, unchanged in meaning. **`FrameStart` is the one addition**: a point after
452
- the view is updated and before anything is rasterized. It exists because per-frame GPU compute that
453
- later phases depend on currently has nowhere to go — fog of war's grid update, a custom cull, a
454
- procedural buffer build — so it is either bolted onto a draw pass at a later phase, which puts it in
455
- the wrong place in the graph and mis-attributes it in a capture, or done outside the graph entirely,
456
- which is precisely the aliasing-and-lifetime blindness §1.2 is about.
457
-
458
- Considered and rejected: `BeforeGBuffer` (nothing distinguishes it from `FrameStart` for anything an
459
- extension can do), `AfterShadows` (shadow rendering is per-light and interior to
460
- `GPULightCollection`; a phase there would be a lie about what is stable), per-post-effect phases
461
- (URP has seventeen events and their number is a symptom, not a feature).
462
-
463
- ### 3.6 Ordering
464
-
465
- Within a phase, order is `relative_order` ascending. It defaults to 0, takes any finite number, and
466
- says nothing about any other extension: an extension states where it belongs from what it *does* — a
467
- fog conceals, so it is late; a debug overlay must not be concealed, so it is later still.
468
-
469
- Ties are broken by `name`, ascending. Stable and reproducible; deliberately *not* registration
470
- order, so that adding an `await` to a system's startup cannot change the picture.
471
-
472
- **This is coarse on purpose.** It cannot express "immediately after that one", and an effect that
473
- genuinely needs one particular extension's result has a real dependency rather than a positional
474
- one — it should read a record that extension published, which the graph then orders and keeps alive
475
- (§4). Because there are no edges there is no cycle: an order always resolves, and `add` refuses
476
- only a `relative_order` that is not a finite number.
477
-
478
- Name-order is stable, not correct. So the resolver also reports **ambiguity**: two extensions on one
479
- phase that publish the same field of the same record at the same `relative_order` — so that only
480
- their names separate them — is an assert failure naming both and the field. Publication goes through our setters, so the frame knows
481
- who published what — no extra syntax on the author's side. This is the check that would have made
482
- somebody write down what the six-deep `AfterTransparency` chain is supposed to be.
483
-
484
- The ambiguity check is per-frame and assert-only. It costs nothing in production and fires on the
485
- first frame in development.
486
-
487
- ### 3.7 Products
488
-
489
- `FrameProduct` is a bitmask of things the frame *can* build but does not always:
490
-
491
- ```js
492
- export const FrameProduct = {
493
- /** per-object + camera velocity, built even when TAA and motion blur are off */
494
- Velocity: 1,
495
- /** a readable snapshot of the scene colour as it stood when the phase began */
496
- ColorCopy: 2,
497
- };
498
- ```
499
-
500
- The union over registered extensions is computed once per registration change, and the renderer
501
- consults it the way it consults its own feature flags. `ColorCopy` is produced immediately before
502
- the passes of each phase that requires it.
503
-
504
- Two members, because two have a nameable consumer: `Velocity` for any effect an extension wants to
505
- temporally reproject itself (currently built for TAA, unreachable by anyone else), and `ColorCopy`
506
- for refraction — a pass that rasterizes geometry *into* the live colour with a `load` op and must
507
- also sample what is behind it, which is refractive water and distortion particles. A full-screen
508
- pass that reads the colour and writes a new one does not need it and must not ask for it.
509
-
510
- Considered and deferred with reasons in §9: `HiZ`, `PreviousColor`, `SceneDepthPyramid`.
511
-
512
- This is the part of the design with the weakest present-day demand and it ships last (§10).
513
-
514
- ---
515
-
516
- ## 4. What the frame graph does, and what it does not
517
-
518
- Stated plainly, because the design would be dishonest without it.
519
-
520
- `FrameGraph.execute` iterates `__pass_nodes` **in insertion order**. It does not topologically sort,
521
- and there is no plan for it to. What the graph gives is culling (`ref_count` propagation from
522
- unreferenced resources), transient resource lifetime and aliasing, versioned renaming of written
523
- resources, and export.
524
-
525
- So: **the graph does not order extensions; §3.6 does.** Every claim in this document about ordering
526
- is a claim about the resolver, not about the graph.
527
-
528
- **What the graph does instead of sorting is check.** The first draft of this document listed two
529
- implementation constraints the design had to route around. Both are now fixed, and a third thing
530
- landed that the design had assumed away, so the division of labour is different from what §3.6 was
531
- written against. The four that matter here — the full list and its evidence are in
532
- `engine/graphics/render/frame_graph/FRAME_GRAPH_RECOMMENDATIONS.md`:
533
-
534
- - **`validate()` proves the insertion order is a valid topological order**, and `compile()` asserts
535
- on it. It reports a *read before write* (a pass reading a node a later pass produces) and a
536
- *superseded read* (a pass reading a version an earlier pass has already replaced — one
537
- `ResourceEntry` owns one concrete resource, so an older node is not a snapshot). This is the
538
- honest answer to insertion-order execution: not to sort, but to refuse a recording whose order is
539
- wrong. It is asserted, so it costs nothing shipped and fails the frame in development.
540
- - **The lifecycle is one-shot and stated.** `FrameGraphState`: `Recording` → `Compiled` →
541
- `Executed`, monotonic, asserted at every mutating entry point. `compile()` was not idempotent and
542
- `execute()` could not tell whether `compile()` had run — a graph executed uncompiled culls itself
543
- entirely and says nothing. The design's old "the extension layer must never compile the graph
544
- itself" caveat is deleted: the graph says so now.
545
- - **`ResourceNode.producer` is assigned at record time**, by `FramePassBuilder`, rather than in
546
- `compile()`. So "this handle was produced by the pass that is publishing it" is answerable at
547
- publication (§3.4).
548
- - **Recording scopes exist.** `push_scope(name)` / `pop_scope()` stamp every pass and resource node
549
- created inside them, nesting through a parent chain. This supersedes §13's request for a
550
- `pass_count` getter and is what §1.2's attribution complaint was asking for.
551
-
552
- One finding from that work belongs here rather than there, because it is the strongest available
553
- argument for the whole model: `validate()`, on its first run against the live 61-pass `Shading`
554
- graph, found that **Shade's own rasterization was not threading its colour targets.** Depth was
555
- threaded through the two-phase opaque pass and colour was not, so every rasterization pass read
556
- `out_color/0` at version 0 while depth advanced. It was harmless only because those passes write an
557
- imported resource and therefore always execute, and because insertion order happened to give the
558
- right picture — the colour dependency between them was simply not a graph fact. That is what
559
- "working around the graph rather than through it" costs, in the engine's own code, found the day
560
- someone checked.
561
-
562
- What remains true, and what §3.6 rests on: the graph checks that a recording order is *consistent*,
563
- never that it is the one anybody wanted. Two extensions each reading the colour and writing a new
564
- one are valid in either order. Data-order violations are the graph's; visual-order mistakes are
565
- §3.6's, and nothing else will catch them.
566
-
567
- ---
568
-
569
- ## 5. Validation and diagnostics
570
-
571
- All of it under `assert`, which is compiled out of production builds — so the price of every check
572
- here is zero in a shipped game and the checks may be as thorough as they like.
573
-
574
- | check | when | what it catches |
575
- | --- | --- | --- |
576
- | record present at phase | `get` | reading `SceneColor` at `AfterGBuffer` — replaces `-1` |
577
- | record frozen | publish | publishing a G-buffer handle after shading |
578
- | frame identity | read + publish | a handle retained across frames |
579
- | `is_valid_resource` | publish | publishing a superseded handle, attributed to the extension |
580
- | product declared by the recording extension | read | reading `velocity` because *someone else* required it |
581
- | `relative_order` finite | registration | an order that no comparator can answer, so the phase would sort by luck |
582
- | publication ambiguity | end of phase | two extensions writing one field at the same `relative_order` |
583
-
584
- Beneath all of them the graph runs its own `validate()` inside `compile()` (§4), which catches the
585
- data-order failures this layer cannot see.
586
-
587
- Every one of these fires on a **statement that cannot be true** — a superseded handle names one
588
- physical resource holding newer contents, `NaN` is not a position, an undeclared collision has no
589
- answer. None of them fires on an extension that is behaving correctly. That is the bar; §5.1 is
590
- what happens when a check is written without holding to it.
591
-
592
- ### 5.1 Culling is not an error, and no diagnostic here treats it as one
593
-
594
- An earlier revision of this document asked the graph to report per-scope culling so that "an
595
- extension whose entire recorded work was culled" could be an assert. **That was wrong and it is
596
- removed**, along with the engine change it needed. Three reasons, in increasing order of weight.
597
-
598
- Culling is what a frame graph is *for*. Frostbite's paper is about eliminating work nobody
599
- consumes; that is why the pattern spread. A mechanism cannot adopt the construct and then treat its
600
- central behaviour as a failure.
601
-
602
- The distinction it drew does not exist. "Culled built-in pass: optimisation. Culled extension pass:
603
- bug." Nothing about the recorder changes what culling means, and encoding the split would have made
604
- extensions second-class in the one place the design claims they are not.
605
-
606
- And it forbids the pattern this design exists to enable. §3.3's own worked example is an extension
607
- publishing `FogOfWarVisibility` at `FrameStart` for a consumer at `AfterTransparency`. Take the
608
- consumer out — disable fog — and the producer's work is dead, the graph elides it, and the frame is
609
- correct and cheaper. Under the assert that is a hard error. A speculative producer whose consumer
610
- may or may not be present is the whole point of an open record vocabulary, and the check would have
611
- outlawed it on day one.
612
-
613
- **What replaces it: nothing, because nothing is missing.** `exportToJson` already reports
614
- `culled` per pass and `exportToDot` already greys culled passes out; with the registry's scopes
615
- (§5.2) a culled extension is a grey cluster with its own name on it. An author asking "why did my
616
- pass not run" dumps the graph and sees it. That is a tool, not a tribunal.
617
-
618
- The culling contract itself is stated once, on `RenderExtension.record`, in the same terms it holds
619
- for every other pass in the graph: work whose output nothing consumes is elided; thread your result
620
- into a record and it is consumed; if you want work that runs regardless, `make_side_effect()` says
621
- so, exactly as it does for a built-in pass.
622
-
623
- ### 5.2 Recording scopes
624
-
625
- **The registry opens a recording scope per extension**, named after it, around the `record()` call:
626
-
627
- ```js
628
- graph.push_scope(extension.name);
629
-
630
- try {
631
- extension.record(frame);
632
- } finally {
633
- graph.pop_scope();
634
- }
635
- ```
636
-
637
- The `finally` is not error handling — nothing is caught and nothing is swallowed. It is there
638
- because `compile()` asserts the scope stack came back empty, so an extension that throws while
639
- recording would otherwise fail on an unbalanced scope and bury the real error behind it. The throw
640
- propagates, decorated with the extension's name the way `FrameGraph` already decorates a failing
641
- pass with its own.
642
-
643
- **Scopes are for reading the frame back, not for policing it.** `exportToDot` puts each extension's
644
- passes in its own labelled cluster, nested as the scopes nest, and a graph that opens no scope
645
- exports byte-identically to before. That is the whole of §1.2's attribution complaint, answered by
646
- an API that already exists and that nothing currently calls — and it is what makes §5.1's answer to
647
- culling a real answer rather than a shrug: the grey box in the dump has the extension's name on it.
648
-
649
- No check depends on scopes. The one that did is gone (§5.1), and the provenance check that would
650
- have used `is_within` was rejected on its own merits (§3.4).
651
-
652
- `RenderExtensionRegistry.describe()` returns the resolved pipeline as text: per phase, the extensions
653
- in resolved order, each with the products it required and the records it publishes into. Printable
654
- without a device, which is what makes it testable without one.
655
-
656
- ---
657
-
658
- ## 6. Lifecycle
659
-
660
- Registration is device-free and survives device loss — the current mechanism's one clearly right
661
- decision, kept. `GraphicsEngine` holds the extension list; a renderer replaced by a restart is
662
- re-populated from it. A system registers once at startup without knowing whether a device exists.
663
-
664
- ```js
665
- graphics.add_extension(extension); // returns the extension
666
- graphics.remove_extension(extension); // returns whether it was registered
667
- ```
668
-
669
- Identity is the object, so the phase no longer has to be passed to removal. Registering or removing
670
- during `record` is an assert failure.
671
-
672
- `GraphicsEngine` gains no surface it did not have: this replaces `add_pass` / `remove_pass`
673
- one-for-one, so D14's "the facade does not grow" holds. `pass_count(phase)` becomes
674
- `extension_count(phase)`, which the start → stop → start leak gate needs.
675
-
676
- **Views.** There is one injection site today: the primary view inside `render_to_target`. Shadow
677
- views, probe bakes and picking do not run extensions and this design does not change that. When a
678
- second view kind gains injection points, the filter belongs on the registry and the extension gains
679
- a declaration for it; building that now would be inventing a contract for a caller that does not
680
- exist.
681
-
682
- ---
683
-
684
- ## 7. The whole API
685
-
686
- That is the entire surface.
687
-
688
- ```js
689
- // declaration
690
- class RenderExtension { name; phase; relative_order; requires; record(frame); }
691
- const FramePhase = { FrameStart, AfterGBuffer, AfterLighting, AfterTransparency, BeforePresent, Overlay };
692
- const FrameProduct = { Velocity, ColorCopy };
693
-
694
- // recording
695
- class FrameContext { graph; view; phase; resolution; get(type); has(type); create(type); describe(handle); }
696
-
697
- // built-in records
698
- class GBufferTextures { albedo; normal; pbr; emissive; }
699
- class ViewTextures { depth; depth_previous; visibility_mesh; visibility_triangle; velocity; }
700
- class SceneColor { color; copy; }
701
- class PresentTarget { canvas; }
702
-
703
- // registration
704
- class RenderExtensionRegistry { add(e); remove(e); count(phase); describe(); run(frame); }
705
- // on GraphicsEngine / Renderer
706
- add_extension(e); remove_extension(e); extension_count(phase);
707
- ```
708
-
709
- ---
710
-
711
- ## 8. Migration
712
-
713
- Nine live systems and one probe. Every one of them is the same mechanical change:
714
- a closure becomes a class, `context.color` becomes `frame.get(SceneColor).color`, and the return
715
- value becomes a publication.
716
-
717
- | consumer | phase | change beyond the mechanical |
718
- | --- | --- | --- |
719
- | `TerrainSystem` | `AfterGBuffer` | drops the registration-before-`await` comment; nothing else |
720
- | `DecalSystem` | `AfterGBuffer` | `relative_order = 1`, which is what the comment in `TerrainSystem` was hand-enforcing; stops assigning through `context.gbuffer` |
721
- | `WaterSystem` | `AfterLighting` | publication instead of return; a candidate first consumer of `FrameProduct.ColorCopy` if refraction is wanted |
722
- | `FogOfWarSystem` | `AfterTransparency` | the grid update becomes a `FrameStart` extension publishing `FogOfWarVisibility`, and the composite reads it; `relative_order = 1` — **this is where §1.3a's shipped bug is fixed** |
723
- | ~~`ParticleEmitterSystem`~~ | ~~`AfterTransparency`~~ | *removed 2026-09-28 with the CPU particle system; the GPU particles draw inside the transparency pipeline* |
724
- | `PathDisplaySystem` | `AfterTransparency` | — |
725
- | `HighlightSystem` | `AfterTransparency` | reads `ViewTextures.visibility_*`, unchanged in meaning |
726
- | `DebugDrawSystem` | `AfterTransparency` | `relative_order = 2` |
727
- | `Trail3DSystem` | ~~`AfterTransparency`~~ | *since 2026-09-24 a transparency channel, not an extension — see the revision note at the top* |
728
- | `probeShadePassInjection` | all | the gate is rewritten and gains the two checks it is currently blind to: a G-buffer replacement, and an order flip |
729
-
730
- **The `AfterTransparency` chain is settled, not open.** Fog of war is after the content
731
- extensions, because it conceals and therefore belongs over everything it is meant to conceal —
732
- particles, trails, path display and the selection highlight all sit at the default order and are all
733
- subject to it. Debug draw is the single deliberate exception at `relative_order = 2`: developer
734
- geometry that the fog can hide is a debugging tool that stops working exactly where debugging is
735
- needed.
736
-
737
- That is six extensions and two numbers. Written down once, in the two files that care, instead of
738
- being a property of where an `await` sits — and a seventh content extension is concealed by being
739
- added, without either of those two files changing.
740
-
741
- **The bug does not have to wait for the migration.** Today's ordering is registration order, so
742
- moving `FogOfWarSystem` below `Trail3DSystem` in `graphics_profile.js` fixes the picture now — at
743
- the cost of it being another undeclared ordering fact, which is what this design exists to end.
744
-
745
- ---
746
-
747
- ## 9. Non-goals and known limits
748
-
749
- **Shade's own frame is not rewritten into a list of extensions.** URP's real strength is that
750
- built-in and injected passes are the same type. Getting there means restructuring
751
- `Renderer.render_to_target`, and renderer internals are hands-off. The consequence, stated so it is
752
- not discovered later: injection points remain hand-placed call sites, so the phase set is only as
753
- good as the places the renderer publishes records, and an extension cannot insert itself between two
754
- built-in post passes.
755
-
756
- **Extensions cannot participate in OIT.** Work at `AfterTransparency` composites over the resolved
757
- transparency; it cannot interleave with sorted transparents. True today, unchanged, and the honest
758
- reason the phase model has a ceiling. *2026-09-24:* the ceiling is on extensions, not on callers —
759
- content that has to interleave registers a transparency channel (`AVBOITSideChannel`) instead, which
760
- is asked for its contribution inside the AVBOIT pipeline; trails do.
761
-
762
- **Extensions cannot add to a built-in pass** — no extra material variants in the G-buffer raster, no
763
- extra lights. A new pass is the only unit.
764
-
765
- **No `FrameProduct` for `HiZ` / `PreviousColor` / depth pyramid yet.** Each is built by the renderer
766
- and each would be a one-line addition, and none has a consumer today. Adding them speculatively puts
767
- three untested branches in the renderer's product wiring.
768
-
769
- **No view kinds.** §6.
770
-
771
- **Ordering ambiguity is caught at frame time, not registration time**, because publication is
772
- observed rather than declared. Declaring publications up front would catch it earlier at the cost of
773
- a second declaration that can disagree with the code; observation cannot disagree with the code.
774
-
775
- ---
776
-
777
- ## 10. Delivery
778
-
779
- Three steps, each independently green.
780
-
781
- - **A — the model.** Records, `FrameContext`, publication with all four checks, registry with
782
- ordering and scoped recording. Port the nine systems and the viewer. Delete
783
- `FramePassContext`, `FramePassRegistry` and `add_pass` / `remove_pass`. `FrameStart` and
784
- `PresentTarget` land here, because they are what the fog-of-war split and the `Overlay` rule need.
785
- - **B — diagnostics.** Ambiguity check and `describe()`. Nothing here waits on the engine; scoped
786
- recording lands in A, because it needs nothing that is not already there.
787
- - **C — products.** `FrameProduct.Velocity` and `ColorCopy`, and the renderer wiring that honours
788
- them.
789
-
790
- A is the whole of the user-visible design. B is what makes it hold. C is the part with the weakest
791
- demand and it is separable on purpose.
792
-
793
- ---
794
-
795
- ## 11. Tests and gates
796
-
797
- Device-free (`vitest`, from the moh root), black-box, real objects:
798
-
799
- - resolved order for a set of extensions with a `relative_order`, positive and negative; ties
800
- resolve by name and the result does not change when registration order does; an order that is not
801
- a finite number is refused and leaves the registry as it was.
802
- - `get` of an absent record throws; `create` twice throws; `has` agrees with both.
803
- - publication of a superseded handle fails; publication into a frozen record fails; a handle from a
804
- previous frame fails. `FrameGraph` and a `SoftwareGPUDevice` are enough for all of these — no real
805
- device, per the standing rule.
806
- - **an extension whose output nothing consumes is culled, and that is not an error** — the frame
807
- renders, no assert fires, and the pass reports `culled` in the JSON export. Pinned as a test
808
- because it is a property somebody will otherwise "fix" (§5.1).
809
- - an extension's passes land inside its own DOT cluster, and a graph recorded with no extensions
810
- exports exactly as it did before. The scope machinery is already specced on the graph; what this
811
- adds is that the registry opens and closes one per extension, including when `record()` throws.
812
- - `describe()` output is stable and contains every registered extension.
813
- - registry survives a simulated renderer replacement with the same extensions in the same order.
814
-
815
- On a device, extending `probeShadePassInjection`:
816
-
817
- - a colour-writing extension at each colour-carrying phase, in and out, restoring to within the
818
- existing 25% bar.
819
- - **a G-buffer-replacing extension at `AfterGBuffer`** — writes a flat albedo, and the lit frame
820
- changes. The current gate has no such check, which is why the in/out G-buffer has never been
821
- exercised outside the decal system.
822
- - **an order flip** — two extensions publishing the colour, with their `relative_order` swapped
823
- between runs, producing different frames. This is the property the current mechanism cannot state
824
- at all.
825
- - **the upscale boundary, at 60% dynamic resolution scale**: an extension at `AfterTransparency` and
826
- one at `BeforePresent`, each sizing its output from `frame.resolution`, both producing a full,
827
- uncropped frame. They must resolve to *different* sizes. The `BeforePresent` half of this fails on
828
- the current mechanism (§1.3b), and the same check run with `feature_taa_enabled = false` must
829
- still pass, which is what pins `resolution` as derived rather than declared.
830
- - **fog of war over particles** — the `AfterTransparency` chain resolved from declarations, with a
831
- particle emitter inside the concealed region staying concealed. This is §1.3a's bug, as a gate.
832
-
833
- ---
834
-
835
- ## 12. Adversarial review
836
-
837
- The design above is the post-review version. What follows is what was attacked and what changed.
838
-
839
- **"A type-keyed container is a registry, and the house rule forbids registries."** The rule forbids
840
- *string*-keyed factories chosen to save boilerplate. A class is a type; the container is a typed
841
- map. The alternative — a struct with fixed fields — is defect §1.1 restated. *Survived, with the
842
- concession that every built-in record is an ordinary class with named fields, so the container is
843
- the only indirection.*
844
-
845
- **"You claim the graph orders passes. It executes in insertion order."** Correct, and the first
846
- draft leaned on the graph for ordering. **Changed:** §4 now states the execution model outright, and
847
- every ordering claim is attributed to the resolver. The design would have been wrong without this.
848
-
849
- > *Since.* The graph answered this from its own side rather than by being routed around: `validate()`
850
- > now proves that the recorded insertion order is a valid topological order and `compile()` asserts
851
- > on it. The design's position is unchanged — §3.6 still owns ordering — but the failure mode it was
852
- > protecting against narrowed sharply. A recording order that is *inconsistent* is now the graph's
853
- > to reject; §3.6 is left owning the orders that are consistent and still wrong, which is exactly
854
- > the fog-over-particles case (§1.3a) and is the only kind of ordering bug the game actually had.
855
-
856
- **"A constraint over classes is an import, and you have just given the debug overlay a dependency on
857
- the fog of war."** Correct, and not caught here: it was caught by the shipped code, where
858
- `DebugDrawSystem` imports `FogOfWarExtension` at module scope for no reason but to say "on top".
859
- **Changed, 2026-09-16:** §3.6 is `relative_order`, a number that names nothing. The cost is real
860
- and is stated rather than argued away — an offset cannot express "immediately after that one", and
861
- §1.4 rejected offsets for being imprecise. The answer to the case that needs precision is that it is
862
- not a positional requirement at all: it is a data dependency, and a published record states it
863
- exactly and is enforced by the graph.
864
-
865
- **"`scene.color = handle` is untracked mutation — the same side channel you criticised
866
- `context.gbuffer` for being."** Half right. The criticism of `context.gbuffer` was that it was the
867
- *only* extra mutable thing, at *one* phase, invisible in the pass signature. **Changed:** publication
868
- is now uniform across every record and validated at the setter — freeze state, frame identity, and
869
- `is_valid_resource` against the graph. The last of those catches a class of silent wrong-picture bug
870
- that neither the current mechanism nor URP catches at all.
871
-
872
- **"You deleted `resolution` and told every extension to call `describe()`. That is a worse API for
873
- the case everyone has."** Fair, and the first draft did exactly that in reaction to defect (b).
874
- **Changed:** `frame.resolution` came back as a derived getter over the phase's live target handle,
875
- so the common case is one property access *and* correct on both sides of the upscale, with
876
- `describe()` left for the genuinely two-sized case. The lesson from (b) is that the number must not
877
- be a field somebody fills in, not that the number should be hard to get.
878
-
879
- **"Ambiguity detection needs the frame to know who is recording — hidden global state."** One field,
880
- set by the loop that calls `record`. Re-entrant recording is an assert failure. *Survived.*
881
-
882
- **"Alphabetical tie-breaking is arbitrary. Why is arbitrary-by-name better than
883
- arbitrary-by-registration?"** Because it does not change when a system's `startup` gains an `await`.
884
- Reproducibility is the property, not correctness. **Changed:** the ambiguity check was added in the
885
- same pass, so an arbitrary order between two extensions that actually conflict is an error rather
886
- than a silently-chosen order.
887
-
888
- **"`FrameProduct` is a closed enum, so §1.1 is only half fixed."** Concede. The frame's own optional
889
- products are Shade's features and enumerating them is correct; the *open* half is the record
890
- vocabulary, which needs no engine edit. **Changed:** §3.7 says this, the enum was cut to two members
891
- with named consumers, and it was moved to the last delivery step.
892
-
893
- **"An `enabled` flag and an early return from `record` are two ways to do one thing."** Agreed.
894
- **Changed:** the flag was cut.
895
-
896
- **"`get_or_create` is friendlier than three methods."** It is, and it destroys the property that
897
- made `-1` go away: absent has to stay distinguishable from empty. *Rejected.*
898
-
899
- **"Six phases plus a blackboard plus a topological sort, to replace two methods, for nine
900
- consumers."** The two methods currently need four paragraphs of documentation about culling, carry a
901
- wrong resolution at two of five phases, and enforce a documented ordering requirement through the
902
- placement of an `await`. The sort is thirty lines over at most a dozen nodes. *Survived, and it is
903
- the reason §10 splits diagnostics out: if the sort and the records are the value, B and C can be
904
- judged separately.*
905
-
906
- **"`SceneColor` absent at `Overlay` means an extension that wants the tonemapped image just gets a
907
- throw."** Yes, with a message naming `BeforePresent`. That is strictly better than the current
908
- outcome, which is a validation error from inside a binding builder, and it is better than the
909
- documentation-only fix that shipped, which was discovered by measurement after the fact. *Survived.*
910
-
911
- **"The migration makes somebody decide the `AfterTransparency` order, and they may get it wrong."**
912
- It was decided while this document was under review, and the order in the shipped game turned out to
913
- be wrong (§1.3a). That is the argument for declaring it, not a counter to it — a mechanism where the
914
- answer is written down is a mechanism where being wrong is a diff. **Changed:** §8 states the
915
- resolved chain instead of leaving it open.
916
-
917
- **"`describe()` is a feature nobody asked for."** It is the only thing that makes "the frame graph is
918
- a first-class construct" mean something testable at this seam — a pipeline you can print without a
919
- device is a pipeline you can spec without a device. *Survived, in delivery step B.*
920
-
921
- **"Culling is what a frame graph is for. You made it an error for one class of user."** Sponsor, on
922
- the revision that asked the graph to report per-scope culling so a fully-culled extension could
923
- assert. **The objection is correct and the check is gone (§5.1).** Three ways it was wrong, and the
924
- review that produced the rest of this document did not catch any of them:
925
-
926
- - It adopted the construct and then treated its central behaviour as a failure.
927
- - The line it drew — culled built-in pass is optimisation, culled extension pass is a bug — has
928
- nothing behind it. Who recorded a pass does not change what culling means.
929
- - Fatally, it outlawed a pattern this document's own §3.3 puts forward as the reason the record
930
- vocabulary is open: an extension publishing `FogOfWarVisibility` for a consumer that may not be
931
- registered. Disable the consumer and the producer is correctly elided — and the assert would have
932
- called that a hard error. A speculative producer is the point, not an accident.
933
-
934
- Withdrawn with it: `FRAME_GRAPH_RECOMMENDATIONS.md` item 5, which was the last open engine change,
935
- and the provenance check in §3.4, which fell to the same standard once it was applied consistently
936
- — it forbade publishing an imported texture in order to catch a typo. What the argument leaves
937
- standing is a bar the remaining checks are now stated against: **a check may only fire on something
938
- that cannot be true**, never on an extension behaving correctly.
939
-
940
- ---
941
-
942
- ## 13. Engine changes required
943
-
944
- Small, and enumerated so the blast radius is visible. **`FrameGraph` needs nothing.** The graph-side
945
- items this document originally asked for have either landed or been withdrawn — the last of them,
946
- the culling report, was withdrawn on the reasoning in §5.1. Everything below is renderer and facade
947
- work.
948
-
949
- 1. `Renderer` publishes records at six points instead of filling one context at five. The record
950
- objects are built where `#frame_pass_context` is built today.
951
- 2. `Renderer` gains a `FrameStart` call site, and (delivery C) honours the product union when
952
- deciding whether to build velocity and the colour copy.
953
- 3. `GraphicsEngine` swaps `add_pass`/`remove_pass`/`pass_count` for
954
- `add_extension`/`remove_extension`/`extension_count`. No net surface change.
955
- 4. `src/shade/renderer/pass/` becomes `src/shade/renderer/extension/`. The folder is MEEP 3's, not
956
- Shade's, and "pass" collides with `FramePassNode` — the graph's passes and an extension are
957
- different things and should not share a word.
958
-
959
- Nothing in Shade's frame structure, no pass reordering, no change to how built-in passes record.
960
-
961
- **Already done, and nothing further is needed from them** — `FrameGraphState` and its asserts,
962
- `validate()` with both order checks, `push_scope` / `pop_scope` and DOT clustering,
963
- `ResourceNode.producer` at record time, and the `exportToJson` fix. `getDescriptor`,
964
- `is_valid_resource` and `onExecuted` were already there and the design uses them as they stand.