@woosh/meep-engine 3.58.0 → 3.60.0

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 (818) hide show
  1. package/editor/particles/effect/ParticleEffectDocument.d.ts +162 -43
  2. package/editor/particles/effect/ParticleEffectDocument.d.ts.map +1 -1
  3. package/editor/particles/effect/ParticleEffectDocument.js +450 -43
  4. package/editor/particles/effect/ParticleReferenceSimulation.d.ts +12 -3
  5. package/editor/particles/effect/ParticleReferenceSimulation.d.ts.map +1 -1
  6. package/editor/particles/effect/ParticleReferenceSimulation.js +78 -23
  7. package/editor/particles/effect/make_starter_particle_effect.js +17 -17
  8. package/editor/view/particles/effect/ParticleEmitterInspectorView.d.ts +3 -3
  9. package/editor/view/particles/effect/ParticleEmitterInspectorView.d.ts.map +1 -1
  10. package/editor/view/particles/effect/ParticleEmitterInspectorView.js +224 -67
  11. package/editor/view/particles/effect/ParticleGraphEditorView.d.ts.map +1 -1
  12. package/editor/view/particles/effect/ParticleGraphEditorView.js +44 -11
  13. package/editor/view/particles/effect/ParticleNodeParametersView.d.ts +2 -1
  14. package/editor/view/particles/effect/ParticleNodeParametersView.d.ts.map +1 -1
  15. package/editor/view/particles/effect/ParticleNodeParametersView.js +46 -16
  16. package/editor/view/particles/effect/ParticlePreviewView.js +2 -2
  17. package/editor/view/particles/effect/particle-editor.css +77 -68
  18. package/package.json +1 -1
  19. package/src/engine/Engine.d.ts.map +1 -1
  20. package/src/engine/Engine.js +2 -2
  21. package/src/engine/ecs/EntityManager.d.ts +29 -5
  22. package/src/engine/ecs/EntityManager.d.ts.map +1 -1
  23. package/src/engine/ecs/EntityManager.js +37 -1
  24. package/src/engine/ecs/storage/populateEngineSerializationRegistry.d.ts.map +1 -1
  25. package/src/engine/ecs/storage/populateEngineSerializationRegistry.js +4 -0
  26. package/src/engine/graphics/ecs/particles/ParticleEffect.d.ts +74 -17
  27. package/src/engine/graphics/ecs/particles/ParticleEffect.d.ts.map +1 -1
  28. package/src/engine/graphics/ecs/particles/ParticleEffect.js +156 -25
  29. package/src/engine/graphics/ecs/particles/ParticleEffectSerializationAdapter.d.ts +13 -3
  30. package/src/engine/graphics/ecs/particles/ParticleEffectSerializationAdapter.d.ts.map +1 -1
  31. package/src/engine/graphics/ecs/particles/ParticleEffectSerializationAdapter.js +59 -13
  32. package/src/engine/graphics/ecs/particles/ParticleEffectSerializationUpgrader_1_2.d.ts +3 -3
  33. package/src/engine/graphics/ecs/particles/ParticleEffectSerializationUpgrader_1_2.js +9 -9
  34. package/src/engine/graphics/ecs/particles/ParticleEffectSerializationUpgrader_2_3.d.ts +15 -0
  35. package/src/engine/graphics/ecs/particles/ParticleEffectSerializationUpgrader_2_3.d.ts.map +1 -0
  36. package/src/engine/graphics/ecs/particles/ParticleEffectSerializationUpgrader_2_3.js +78 -0
  37. package/src/engine/graphics/ecs/trail3d/Trail3D.d.ts +2 -0
  38. package/src/engine/graphics/ecs/trail3d/Trail3D.d.ts.map +1 -1
  39. package/src/engine/graphics/ecs/trail3d/Trail3D.js +2 -0
  40. package/src/engine/graphics/ecs/trail3d/seed_trail_stroke.js +1 -1
  41. package/src/engine/graphics/ecs/trail3d/seed_trail_tube.js +1 -1
  42. package/src/engine/graphics/trail/tube/TubeX.d.ts +5 -3
  43. package/src/engine/graphics/trail/tube/TubeX.d.ts.map +1 -1
  44. package/src/engine/graphics/trail/tube/TubeX.js +5 -3
  45. package/src/engine/graphics/trail/tube/tube_attributes_spec.d.ts +3 -2
  46. package/src/engine/graphics/trail/tube/tube_attributes_spec.d.ts.map +1 -1
  47. package/src/engine/graphics/trail/tube/tube_attributes_spec.js +9 -8
  48. package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts +65 -4
  49. package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts.map +1 -1
  50. package/src/engine/graphics3/GPUParticleEmitterSystem.js +1351 -904
  51. package/src/engine/graphics3/trail/trail_tube_write_geometry.d.ts +1 -1
  52. package/src/engine/graphics3/trail/trail_tube_write_geometry.js +13 -13
  53. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  54. package/src/generation/foliage/FOLIAGE_DESIGN.md +26 -5
  55. package/src/generation/foliage/LOD_REWORK_BRIEF_2026_10_27.md +35 -0
  56. package/src/generation/foliage/SELF_ORGANIZING_FRAMEWORK_BRIEF_2026_10_26.md +2 -1
  57. package/src/generation/foliage/TWO_FRONT_ENDS_BRIEF_2026_10_28.md +552 -0
  58. package/src/generation/foliage/analysis/tree_crown_opacity.d.ts.map +1 -1
  59. package/src/generation/foliage/analysis/tree_crown_opacity.js +2 -1
  60. package/src/generation/foliage/analysis/tree_render_cost.d.ts +5 -3
  61. package/src/generation/foliage/analysis/tree_render_cost.d.ts.map +1 -1
  62. package/src/generation/foliage/analysis/tree_render_cost.js +8 -7
  63. package/src/generation/foliage/asset/TreeAsset.d.ts +17 -0
  64. package/src/generation/foliage/asset/TreeAsset.d.ts.map +1 -1
  65. package/src/generation/foliage/asset/TreeAsset.js +20 -0
  66. package/src/generation/foliage/asset/TreeSpeciesCache.d.ts +69 -0
  67. package/src/generation/foliage/asset/TreeSpeciesCache.d.ts.map +1 -0
  68. package/src/generation/foliage/asset/TreeSpeciesCache.js +147 -0
  69. package/src/generation/foliage/asset/generate_tree.d.ts +10 -0
  70. package/src/generation/foliage/asset/generate_tree.d.ts.map +1 -1
  71. package/src/generation/foliage/asset/generate_tree.js +57 -29
  72. package/src/generation/foliage/asset/place_tree.d.ts +2 -1
  73. package/src/generation/foliage/asset/place_tree.d.ts.map +1 -1
  74. package/src/generation/foliage/asset/place_tree.js +2 -1
  75. package/src/generation/foliage/asset/tree_lod_plan.d.ts +4 -2
  76. package/src/generation/foliage/asset/tree_lod_plan.d.ts.map +1 -1
  77. package/src/generation/foliage/asset/tree_lod_plan.js +12 -14
  78. package/src/generation/foliage/asset/tree_shoot_instances.d.ts +119 -0
  79. package/src/generation/foliage/asset/tree_shoot_instances.d.ts.map +1 -0
  80. package/src/generation/foliage/asset/tree_shoot_instances.js +410 -0
  81. package/src/generation/foliage/asset/tree_shoot_prototypes.d.ts +155 -0
  82. package/src/generation/foliage/asset/tree_shoot_prototypes.d.ts.map +1 -0
  83. package/src/generation/foliage/asset/tree_shoot_prototypes.js +251 -0
  84. package/src/generation/foliage/config/TreeGrowth.d.ts +10 -0
  85. package/src/generation/foliage/config/TreeGrowth.d.ts.map +1 -0
  86. package/src/generation/foliage/config/TreeGrowth.js +18 -0
  87. package/src/generation/foliage/config/TreeLODConfig.d.ts +6 -0
  88. package/src/generation/foliage/config/TreeLODConfig.d.ts.map +1 -1
  89. package/src/generation/foliage/config/TreeLODConfig.js +7 -0
  90. package/src/generation/foliage/index.d.ts +3 -0
  91. package/src/generation/foliage/index.js +3 -0
  92. package/src/generation/foliage/material/foliage_tile.d.ts +6 -6
  93. package/src/generation/foliage/material/foliage_tile.js +3 -3
  94. package/src/generation/foliage/material/tree_cluster_atlas.d.ts +44 -0
  95. package/src/generation/foliage/material/tree_cluster_atlas.d.ts.map +1 -0
  96. package/src/generation/foliage/material/tree_cluster_atlas.js +37 -0
  97. package/src/generation/foliage/mesh/foliage_collect_attachments.js +2 -2
  98. package/src/generation/foliage/mesh/foliage_scale_spread.d.ts +14 -0
  99. package/src/generation/foliage/mesh/foliage_scale_spread.d.ts.map +1 -0
  100. package/src/generation/foliage/mesh/foliage_scale_spread.js +15 -0
  101. package/src/generation/foliage/mesh/tree_cluster_representatives.d.ts +8 -7
  102. package/src/generation/foliage/mesh/tree_cluster_representatives.d.ts.map +1 -1
  103. package/src/generation/foliage/mesh/tree_cluster_representatives.js +85 -40
  104. package/src/generation/foliage/mesh/tree_clusters.d.ts +26 -5
  105. package/src/generation/foliage/mesh/tree_clusters.d.ts.map +1 -1
  106. package/src/generation/foliage/mesh/tree_clusters.js +52 -39
  107. package/src/generation/foliage/mesh/tree_mesh_build_bark.d.ts.map +1 -1
  108. package/src/generation/foliage/mesh/tree_mesh_build_bark.js +32 -54
  109. package/src/generation/foliage/presets/make_tree_species_preset.d.ts +6 -1
  110. package/src/generation/foliage/presets/make_tree_species_preset.d.ts.map +1 -1
  111. package/src/generation/foliage/presets/make_tree_species_preset.js +157 -3
  112. package/src/generation/foliage/skeleton/TreeSkeleton.d.ts +19 -0
  113. package/src/generation/foliage/skeleton/TreeSkeleton.d.ts.map +1 -1
  114. package/src/generation/foliage/skeleton/TreeSkeleton.js +41 -0
  115. package/src/generation/foliage/skeleton/tree_skeleton_compute_joint_mechanics.d.ts +5 -0
  116. package/src/generation/foliage/skeleton/tree_skeleton_compute_joint_mechanics.d.ts.map +1 -1
  117. package/src/generation/foliage/skeleton/tree_skeleton_compute_joint_mechanics.js +8 -1
  118. package/src/generation/foliage/skeleton/tree_skeleton_copy_branches.d.ts +70 -0
  119. package/src/generation/foliage/skeleton/tree_skeleton_copy_branches.d.ts.map +1 -0
  120. package/src/generation/foliage/skeleton/tree_skeleton_copy_branches.js +171 -0
  121. package/src/generation/foliage/skeleton/tree_skeleton_extend_branch.d.ts +17 -0
  122. package/src/generation/foliage/skeleton/tree_skeleton_extend_branch.d.ts.map +1 -0
  123. package/src/generation/foliage/skeleton/tree_skeleton_extend_branch.js +79 -0
  124. package/src/generation/foliage/skeleton/tree_skeleton_fit_crown_frame.d.ts +12 -0
  125. package/src/generation/foliage/skeleton/tree_skeleton_fit_crown_frame.d.ts.map +1 -0
  126. package/src/generation/foliage/skeleton/tree_skeleton_fit_crown_frame.js +56 -0
  127. package/src/generation/foliage/skeleton/tree_skeleton_floor_girth.d.ts +11 -0
  128. package/src/generation/foliage/skeleton/tree_skeleton_floor_girth.d.ts.map +1 -0
  129. package/src/generation/foliage/skeleton/tree_skeleton_floor_girth.js +38 -0
  130. package/src/generation/foliage/skeleton/tree_skeleton_grow.d.ts +4 -11
  131. package/src/generation/foliage/skeleton/tree_skeleton_grow.d.ts.map +1 -1
  132. package/src/generation/foliage/skeleton/tree_skeleton_grow.js +8 -2034
  133. package/src/generation/foliage/skeleton/tree_skeleton_grow_framework.d.ts +22 -0
  134. package/src/generation/foliage/skeleton/tree_skeleton_grow_framework.d.ts.map +1 -0
  135. package/src/generation/foliage/skeleton/tree_skeleton_grow_framework.js +2050 -0
  136. package/src/generation/foliage/skeleton/tree_skeleton_grow_shoots.d.ts +10 -0
  137. package/src/generation/foliage/skeleton/tree_skeleton_grow_shoots.d.ts.map +1 -1
  138. package/src/generation/foliage/skeleton/tree_skeleton_grow_shoots.js +27 -56
  139. package/src/shade/descriptor/pipeline/render/DepthStencilState.d.ts +4 -0
  140. package/src/shade/descriptor/pipeline/render/DepthStencilState.d.ts.map +1 -1
  141. package/src/shade/descriptor/pipeline/render/DepthStencilState.js +170 -144
  142. package/src/shade/descriptor/pipeline/render/MultisampleState.d.ts +4 -0
  143. package/src/shade/descriptor/pipeline/render/MultisampleState.d.ts.map +1 -1
  144. package/src/shade/descriptor/pipeline/render/MultisampleState.js +78 -65
  145. package/src/shade/descriptor/pipeline/render/RenderPipelineDescriptor.d.ts.map +1 -1
  146. package/src/shade/descriptor/pipeline/render/RenderPipelineDescriptor.js +176 -172
  147. package/src/shade/descriptor/pipeline/render/StencilFaceState.d.ts +4 -0
  148. package/src/shade/descriptor/pipeline/render/StencilFaceState.d.ts.map +1 -1
  149. package/src/shade/descriptor/pipeline/render/StencilFaceState.js +107 -88
  150. package/src/shade/playground/particle_ecs/README.md +9 -8
  151. package/src/shade/playground/particle_ecs/bonfire_editor.js +3 -3
  152. package/src/shade/playground/particle_ecs/bonfire_effects.d.ts +6 -6
  153. package/src/shade/playground/particle_ecs/bonfire_effects.js +6 -6
  154. package/src/shade/playground/particle_editor/README.md +14 -11
  155. package/src/shade/playground/particle_system/particle_prototype.js +18 -18
  156. package/src/shade/playground/particle_system/particle_scene.js +13 -13
  157. package/src/shade/playground/probe_clipmap/README.md +42 -0
  158. package/src/shade/playground/probe_clipmap/index.html +190 -0
  159. package/src/shade/playground/probe_clipmap/main.d.ts +8 -0
  160. package/src/shade/playground/probe_clipmap/main.d.ts.map +1 -0
  161. package/src/shade/playground/probe_clipmap/main.js +365 -0
  162. package/src/shade/renderer/Renderer.d.ts +3 -1
  163. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  164. package/src/shade/renderer/Renderer.js +97 -43
  165. package/src/shade/renderer/extension/ViewTextures.d.ts +2 -1
  166. package/src/shade/renderer/extension/ViewTextures.d.ts.map +1 -1
  167. package/src/shade/renderer/extension/ViewTextures.js +2 -1
  168. package/src/shade/renderer/fluid/GPUFluidCascade.d.ts +3 -3
  169. package/src/shade/renderer/fluid/GPUFluidCascadeStack.d.ts +1 -1
  170. package/src/shade/renderer/fluid/forcing/GPUFluidForcing.d.ts +6 -6
  171. package/src/shade/renderer/fluid/pressure/GPUFluidPressureSolve.d.ts +1 -1
  172. package/src/shade/renderer/fluid/query/GPUFluidQueries.d.ts +1 -1
  173. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.d.ts +63 -1
  174. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.d.ts.map +1 -1
  175. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.js +228 -16
  176. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts +27 -4
  177. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts.map +1 -1
  178. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.js +526 -23
  179. package/src/shade/renderer/geometry/sdf/cards/MeshCardCaptureMode.d.ts +11 -0
  180. package/src/shade/renderer/geometry/sdf/cards/MeshCardCaptureMode.d.ts.map +1 -0
  181. package/src/shade/renderer/geometry/sdf/cards/MeshCardCaptureMode.js +21 -0
  182. package/src/shade/renderer/geometry/sdf/cards/mesh_cards_raster_pass_descriptor.d.ts +65 -0
  183. package/src/shade/renderer/geometry/sdf/cards/mesh_cards_raster_pass_descriptor.d.ts.map +1 -0
  184. package/src/shade/renderer/geometry/sdf/cards/mesh_cards_raster_pass_descriptor.js +376 -0
  185. package/src/shade/renderer/geometry/sdf/cards/shader_mesh_cards_capture.d.ts +7 -3
  186. package/src/shade/renderer/geometry/sdf/cards/shader_mesh_cards_capture.d.ts.map +1 -1
  187. package/src/shade/renderer/geometry/sdf/cards/shader_mesh_cards_capture.js +70 -6
  188. package/src/shade/renderer/geometry/sdf/cards/shader_mesh_cards_raster_args.d.ts +20 -0
  189. package/src/shade/renderer/geometry/sdf/cards/shader_mesh_cards_raster_args.d.ts.map +1 -0
  190. package/src/shade/renderer/geometry/sdf/cards/shader_mesh_cards_raster_args.js +80 -0
  191. package/src/shade/renderer/geometry/sdf/cards/shader_mesh_cards_raster_scatter.d.ts +18 -0
  192. package/src/shade/renderer/geometry/sdf/cards/shader_mesh_cards_raster_scatter.d.ts.map +1 -0
  193. package/src/shade/renderer/geometry/sdf/cards/shader_mesh_cards_raster_scatter.js +113 -0
  194. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.d.ts +34 -12
  195. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.d.ts.map +1 -1
  196. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.js +56 -15
  197. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.d.ts +2 -2
  198. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.d.ts.map +1 -1
  199. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.js +13 -26
  200. package/src/shade/renderer/geometry/virtual/runtime/VGeoViewCut.d.ts +25 -2
  201. package/src/shade/renderer/geometry/virtual/runtime/VGeoViewCut.d.ts.map +1 -1
  202. package/src/shade/renderer/geometry/virtual/runtime/VGeoViewCut.js +54 -32
  203. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_VERDICT.d.ts +14 -0
  204. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_VERDICT.d.ts.map +1 -0
  205. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_VERDICT.js +37 -0
  206. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_arena.d.ts.map +1 -1
  207. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_arena.js +16 -0
  208. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_claim.d.ts.map +1 -1
  209. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_claim.js +2 -6
  210. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_descent.d.ts +131 -0
  211. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_descent.d.ts.map +1 -0
  212. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_descent.js +467 -0
  213. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_instance.d.ts +24 -0
  214. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_instance.d.ts.map +1 -0
  215. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_instance.js +111 -0
  216. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_rule.d.ts +42 -0
  217. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_rule.d.ts.map +1 -0
  218. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_rule.js +404 -0
  219. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_stat.d.ts +16 -0
  220. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_stat.d.ts.map +1 -0
  221. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_stat.js +23 -0
  222. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_want.d.ts +17 -0
  223. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_want.d.ts.map +1 -0
  224. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_want.js +65 -0
  225. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_collect.d.ts.map +1 -1
  226. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_collect.js +14 -70
  227. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.d.ts.map +1 -1
  228. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.js +13 -773
  229. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_cut_descent.d.ts +55 -0
  230. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_cut_descent.d.ts.map +1 -0
  231. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_cut_descent.js +248 -0
  232. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_instances_to_meshlets.d.ts.map +1 -1
  233. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_instances_to_meshlets.js +15 -174
  234. package/src/shade/renderer/geometry/virtual/sdf/VGEO_SDF.d.ts +47 -0
  235. package/src/shade/renderer/geometry/virtual/sdf/VGEO_SDF.d.ts.map +1 -0
  236. package/src/shade/renderer/geometry/virtual/sdf/VGEO_SDF.js +80 -0
  237. package/src/shade/renderer/geometry/virtual/sdf/VGeoSDFProxies.d.ts +91 -0
  238. package/src/shade/renderer/geometry/virtual/sdf/VGeoSDFProxies.d.ts.map +1 -0
  239. package/src/shade/renderer/geometry/virtual/sdf/VGeoSDFProxies.js +902 -0
  240. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_collect.d.ts +16 -0
  241. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_collect.d.ts.map +1 -0
  242. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_collect.js +67 -0
  243. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_decode.d.ts +17 -0
  244. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_decode.d.ts.map +1 -0
  245. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_decode.js +145 -0
  246. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_traverse.d.ts +12 -0
  247. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_traverse.d.ts.map +1 -0
  248. package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_traverse.js +105 -0
  249. package/src/shade/renderer/global_illumination/brick4/gpu/draw/chunk_brick4_probe_meta_pick2.d.ts.map +1 -1
  250. package/src/shade/renderer/global_illumination/brick4/gpu/draw/chunk_brick4_probe_meta_pick2.js +8 -24
  251. package/src/shade/renderer/global_illumination/brick4/gpu/draw/chunk_brick4_sample_forward_indirect.d.ts.map +1 -1
  252. package/src/shade/renderer/global_illumination/brick4/gpu/draw/chunk_brick4_sample_forward_indirect.js +1 -25
  253. package/src/shade/renderer/global_illumination/lpv/deprecated/chunk_lpv_get_visibility_mask.d.ts.map +1 -1
  254. package/src/shade/renderer/global_illumination/lpv/deprecated/chunk_lpv_get_visibility_mask.js +5 -23
  255. package/src/shade/renderer/global_illumination/probe/octahedral/shader_lpv_build_gbuffer.d.ts.map +1 -1
  256. package/src/shade/renderer/global_illumination/probe/octahedral/shader_lpv_build_gbuffer.js +2 -1
  257. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.d.ts +20 -5
  258. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.d.ts.map +1 -1
  259. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.js +111 -11
  260. package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_CLIPMAP_PROPOSAL_2026_10_27.md +1596 -52
  261. package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_RAYS_VARIABLE_AND_SORTED_SCOPE_2026_10_30.md +410 -0
  262. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts +3 -2
  263. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts.map +1 -1
  264. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.js +4 -3
  265. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSettings.d.ts +41 -17
  266. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSettings.d.ts.map +1 -1
  267. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSettings.js +57 -33
  268. package/src/shade/renderer/global_illumination/probe_clipmap/brick/BRICK_CLIPMAP.d.ts +18 -3
  269. package/src/shade/renderer/global_illumination/probe_clipmap/brick/BRICK_CLIPMAP.d.ts.map +1 -1
  270. package/src/shade/renderer/global_illumination/probe_clipmap/brick/BRICK_CLIPMAP.js +21 -4
  271. package/src/shade/renderer/global_illumination/probe_clipmap/brick/GPUBrickClipmap.d.ts +9 -2
  272. package/src/shade/renderer/global_illumination/probe_clipmap/brick/GPUBrickClipmap.d.ts.map +1 -1
  273. package/src/shade/renderer/global_illumination/probe_clipmap/brick/GPUBrickClipmap.js +8 -1
  274. package/src/shade/renderer/global_illumination/probe_clipmap/brick/chunk_brick_clipmap.d.ts +12 -0
  275. package/src/shade/renderer/global_illumination/probe_clipmap/brick/chunk_brick_clipmap.d.ts.map +1 -1
  276. package/src/shade/renderer/global_illumination/probe_clipmap/brick/chunk_brick_clipmap.js +28 -0
  277. package/src/shade/renderer/global_illumination/probe_clipmap/brick/chunk_brick_clipmap_trace.d.ts +1 -29
  278. package/src/shade/renderer/global_illumination/probe_clipmap/brick/chunk_brick_clipmap_trace.d.ts.map +1 -1
  279. package/src/shade/renderer/global_illumination/probe_clipmap/brick/chunk_brick_clipmap_trace.js +1 -74
  280. package/src/shade/renderer/global_illumination/probe_clipmap/brick/{mesh/chunk_mesh_brick_object.d.ts → chunk_brick_object.d.ts} +8 -8
  281. package/src/shade/renderer/global_illumination/probe_clipmap/brick/chunk_brick_object.d.ts.map +1 -0
  282. package/src/shade/renderer/global_illumination/probe_clipmap/brick/chunk_brick_object.js +42 -0
  283. package/src/shade/renderer/global_illumination/probe_clipmap/brick/chunk_brick_provider.d.ts +45 -0
  284. package/src/shade/renderer/global_illumination/probe_clipmap/brick/chunk_brick_provider.d.ts.map +1 -0
  285. package/src/shade/renderer/global_illumination/probe_clipmap/brick/chunk_brick_provider.js +93 -0
  286. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/MESH_BRICK.d.ts +161 -0
  287. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/MESH_BRICK.d.ts.map +1 -0
  288. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/MESH_BRICK.js +219 -0
  289. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/MESH_BRICK_INSTANCE_STRUCT.d.ts +1 -15
  290. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/MESH_BRICK_INSTANCE_STRUCT.d.ts.map +1 -1
  291. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/MESH_BRICK_INSTANCE_STRUCT.js +1 -17
  292. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/MeshSDFBrickProvider.d.ts +40 -12
  293. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/MeshSDFBrickProvider.d.ts.map +1 -1
  294. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/MeshSDFBrickProvider.js +373 -31
  295. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_instance.d.ts +3 -4
  296. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_instance.d.ts.map +1 -1
  297. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_instance.js +6 -8
  298. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_texel.d.ts +15 -8
  299. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_texel.d.ts.map +1 -1
  300. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_texel.js +28 -14
  301. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_walk.d.ts +23 -0
  302. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_walk.d.ts.map +1 -0
  303. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_walk.js +66 -0
  304. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/shader_mesh_brick_batch.d.ts +29 -0
  305. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/shader_mesh_brick_batch.d.ts.map +1 -0
  306. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/shader_mesh_brick_batch.js +210 -0
  307. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/shader_mesh_brick_command.d.ts +29 -0
  308. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/shader_mesh_brick_command.d.ts.map +1 -0
  309. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/shader_mesh_brick_command.js +86 -0
  310. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/shader_mesh_brick_gather.d.ts +41 -0
  311. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/shader_mesh_brick_gather.d.ts.map +1 -0
  312. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/shader_mesh_brick_gather.js +439 -0
  313. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/shader_mesh_brick_resolve.d.ts +23 -0
  314. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/shader_mesh_brick_resolve.d.ts.map +1 -0
  315. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/shader_mesh_brick_resolve.js +135 -0
  316. package/src/shade/renderer/global_illumination/probe_clipmap/brick/shader_brick_clipmap_free.d.ts +3 -1
  317. package/src/shade/renderer/global_illumination/probe_clipmap/brick/shader_brick_clipmap_free.d.ts.map +1 -1
  318. package/src/shade/renderer/global_illumination/probe_clipmap/brick/shader_brick_clipmap_free.js +12 -3
  319. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts +32 -15
  320. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts.map +1 -1
  321. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.js +380 -57
  322. package/src/shade/renderer/global_illumination/probe_clipmap/probe/PROBE_CLIPMAP.d.ts +156 -8
  323. package/src/shade/renderer/global_illumination/probe_clipmap/probe/PROBE_CLIPMAP.d.ts.map +1 -1
  324. package/src/shade/renderer/global_illumination/probe_clipmap/probe/PROBE_CLIPMAP.js +189 -13
  325. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap.d.ts +193 -9
  326. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap.d.ts.map +1 -1
  327. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap.js +458 -32
  328. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_cage.d.ts +19 -0
  329. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_cage.d.ts.map +1 -0
  330. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_cage.js +40 -0
  331. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_pick.d.ts +72 -0
  332. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_pick.d.ts.map +1 -0
  333. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_pick.js +343 -0
  334. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_sample_radiance.d.ts +29 -0
  335. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_sample_radiance.d.ts.map +1 -0
  336. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_sample_radiance.js +189 -0
  337. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_want.d.ts +26 -0
  338. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_want.d.ts.map +1 -0
  339. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_want.js +132 -0
  340. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_accumulate.d.ts +40 -10
  341. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_accumulate.d.ts.map +1 -1
  342. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_accumulate.js +244 -32
  343. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_admit.d.ts +8 -1
  344. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_admit.d.ts.map +1 -1
  345. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_admit.js +29 -12
  346. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_diffuse.d.ts +12 -0
  347. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_diffuse.d.ts.map +1 -0
  348. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_diffuse.js +101 -0
  349. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_due.d.ts +15 -6
  350. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_due.d.ts.map +1 -1
  351. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_due.js +21 -22
  352. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_guide.d.ts +36 -0
  353. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_guide.d.ts.map +1 -0
  354. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_guide.js +257 -0
  355. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_indirect_lighting.d.ts +15 -0
  356. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_indirect_lighting.d.ts.map +1 -0
  357. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_indirect_lighting.js +175 -0
  358. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_light.d.ts +37 -0
  359. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_light.d.ts.map +1 -0
  360. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_light.js +316 -0
  361. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_order.d.ts +23 -0
  362. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_order.d.ts.map +1 -0
  363. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_order.js +110 -0
  364. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_project.d.ts +24 -2
  365. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_project.d.ts.map +1 -1
  366. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_project.js +90 -10
  367. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_ray_layout.d.ts +45 -0
  368. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_ray_layout.d.ts.map +1 -0
  369. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_ray_layout.js +205 -0
  370. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_relocate.js +1 -1
  371. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_retire.d.ts +15 -2
  372. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_retire.d.ts.map +1 -1
  373. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_retire.js +54 -9
  374. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_schedule.d.ts +19 -4
  375. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_schedule.d.ts.map +1 -1
  376. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_schedule.js +60 -6
  377. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.d.ts +6 -9
  378. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.d.ts.map +1 -1
  379. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.js +26 -37
  380. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_trace.d.ts +21 -14
  381. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_trace.d.ts.map +1 -1
  382. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_trace.js +101 -38
  383. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_trace_command.d.ts +6 -2
  384. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_trace_command.d.ts.map +1 -1
  385. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_trace_command.js +26 -10
  386. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_want.d.ts +5 -9
  387. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_want.d.ts.map +1 -1
  388. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_want.js +9 -110
  389. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_want_hits.d.ts +28 -0
  390. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_want_hits.d.ts.map +1 -0
  391. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_want_hits.js +110 -0
  392. package/src/shade/renderer/gpu_primitive/sort/SORT_DIGIT_SETTINGS_STRUCT.d.ts +10 -0
  393. package/src/shade/renderer/gpu_primitive/sort/SORT_DIGIT_SETTINGS_STRUCT.d.ts.map +1 -0
  394. package/src/shade/renderer/gpu_primitive/sort/SORT_DIGIT_SETTINGS_STRUCT.js +21 -0
  395. package/src/shade/renderer/gpu_primitive/sort/SORT_PASS_SETTINGS_STRUCT.d.ts +5 -2
  396. package/src/shade/renderer/gpu_primitive/sort/SORT_PASS_SETTINGS_STRUCT.d.ts.map +1 -1
  397. package/src/shade/renderer/gpu_primitive/sort/SORT_PASS_SETTINGS_STRUCT.js +31 -23
  398. package/src/shade/renderer/gpu_primitive/sort/SORT_SIZE_STRUCT.d.ts +10 -0
  399. package/src/shade/renderer/gpu_primitive/sort/SORT_SIZE_STRUCT.d.ts.map +1 -0
  400. package/src/shade/renderer/gpu_primitive/sort/SORT_SIZE_STRUCT.js +22 -0
  401. package/src/shade/renderer/gpu_primitive/sort/chunk_sort_constants.d.ts +9 -0
  402. package/src/shade/renderer/gpu_primitive/sort/chunk_sort_constants.d.ts.map +1 -0
  403. package/src/shade/renderer/gpu_primitive/sort/chunk_sort_constants.js +18 -0
  404. package/src/shade/renderer/gpu_primitive/sort/chunk_sort_digit.d.ts +8 -3
  405. package/src/shade/renderer/gpu_primitive/sort/chunk_sort_digit.d.ts.map +1 -1
  406. package/src/shade/renderer/gpu_primitive/sort/chunk_sort_digit.js +41 -32
  407. package/src/shade/renderer/gpu_primitive/sort/chunk_sort_histogram_tile.d.ts +31 -0
  408. package/src/shade/renderer/gpu_primitive/sort/chunk_sort_histogram_tile.d.ts.map +1 -0
  409. package/src/shade/renderer/gpu_primitive/sort/chunk_sort_histogram_tile.js +71 -0
  410. package/src/shade/renderer/gpu_primitive/sort/chunk_sort_scatter_tile.d.ts +47 -0
  411. package/src/shade/renderer/gpu_primitive/sort/chunk_sort_scatter_tile.d.ts.map +1 -0
  412. package/src/shade/renderer/gpu_primitive/sort/chunk_sort_scatter_tile.js +210 -0
  413. package/src/shade/renderer/gpu_primitive/sort/defs.d.ts +5 -4
  414. package/src/shade/renderer/gpu_primitive/sort/defs.d.ts.map +1 -1
  415. package/src/shade/renderer/gpu_primitive/sort/defs.js +5 -4
  416. package/src/shade/renderer/gpu_primitive/sort/graph_radix_sort.d.ts +56 -15
  417. package/src/shade/renderer/gpu_primitive/sort/graph_radix_sort.d.ts.map +1 -1
  418. package/src/shade/renderer/gpu_primitive/sort/graph_radix_sort.js +410 -61
  419. package/src/shade/renderer/gpu_primitive/sort/shader_radix_sort_histogram.d.ts +4 -1
  420. package/src/shade/renderer/gpu_primitive/sort/shader_radix_sort_histogram.d.ts.map +1 -1
  421. package/src/shade/renderer/gpu_primitive/sort/shader_radix_sort_histogram.js +16 -42
  422. package/src/shade/renderer/gpu_primitive/sort/shader_radix_sort_histogram_indirect.d.ts +15 -0
  423. package/src/shade/renderer/gpu_primitive/sort/shader_radix_sort_histogram_indirect.d.ts.map +1 -0
  424. package/src/shade/renderer/gpu_primitive/sort/shader_radix_sort_histogram_indirect.js +60 -0
  425. package/src/shade/renderer/gpu_primitive/sort/shader_radix_sort_prepare.d.ts +25 -0
  426. package/src/shade/renderer/gpu_primitive/sort/shader_radix_sort_prepare.d.ts.map +1 -0
  427. package/src/shade/renderer/gpu_primitive/sort/shader_radix_sort_prepare.js +89 -0
  428. package/src/shade/renderer/gpu_primitive/sort/shader_radix_sort_scatter.d.ts +3 -8
  429. package/src/shade/renderer/gpu_primitive/sort/shader_radix_sort_scatter.d.ts.map +1 -1
  430. package/src/shade/renderer/gpu_primitive/sort/shader_radix_sort_scatter.js +28 -160
  431. package/src/shade/renderer/gpu_primitive/sort/shader_radix_sort_scatter_indirect.d.ts +12 -0
  432. package/src/shade/renderer/gpu_primitive/sort/shader_radix_sort_scatter_indirect.d.ts.map +1 -0
  433. package/src/shade/renderer/gpu_primitive/sort/shader_radix_sort_scatter_indirect.js +74 -0
  434. package/src/shade/renderer/gpu_primitive/sort/verify_radix_sort.d.ts +3 -1
  435. package/src/shade/renderer/gpu_primitive/sort/verify_radix_sort.d.ts.map +1 -1
  436. package/src/shade/renderer/gpu_primitive/sort/verify_radix_sort.js +387 -188
  437. package/src/shade/renderer/light/world_cluster/chunk_world_light_cluster_lookup.d.ts +13 -0
  438. package/src/shade/renderer/light/world_cluster/chunk_world_light_cluster_lookup.d.ts.map +1 -1
  439. package/src/shade/renderer/light/world_cluster/chunk_world_light_cluster_lookup.js +40 -0
  440. package/src/shade/renderer/light/world_cluster/graph_build_world_light_clusters.d.ts +11 -6
  441. package/src/shade/renderer/light/world_cluster/graph_build_world_light_clusters.d.ts.map +1 -1
  442. package/src/shade/renderer/light/world_cluster/graph_build_world_light_clusters.js +15 -8
  443. package/src/shade/renderer/light/world_cluster/shader_world_cluster_assign_lights.d.ts +9 -2
  444. package/src/shade/renderer/light/world_cluster/shader_world_cluster_assign_lights.d.ts.map +1 -1
  445. package/src/shade/renderer/light/world_cluster/shader_world_cluster_assign_lights.js +25 -11
  446. package/src/shade/renderer/lightmap/bake/shader_lightmap_viz_to_irradiance_flat.js +1 -1
  447. package/src/shade/renderer/lightmap/bake/shader_lightmap_viz_to_sh.js +1 -1
  448. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  449. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts.map +1 -1
  450. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.js +90 -72
  451. package/src/shade/renderer/material/resident/RESIDENT_MATERIAL_TEXTURE_LAYOUT.d.ts +5 -0
  452. package/src/shade/renderer/material/resident/RESIDENT_MATERIAL_TEXTURE_LAYOUT.d.ts.map +1 -1
  453. package/src/shade/renderer/material/resident/RESIDENT_MATERIAL_TEXTURE_LAYOUT.js +9 -0
  454. package/src/shade/renderer/particles/DESIGN.md +151 -42
  455. package/src/shade/renderer/particles/EMITTER_GLOBAL_STATE_PLAN_2026_10_29.md +283 -0
  456. package/src/shade/renderer/particles/GPUParticleSystem.d.ts +10 -2
  457. package/src/shade/renderer/particles/GPUParticleSystem.d.ts.map +1 -1
  458. package/src/shade/renderer/particles/GPUParticleSystem.js +46 -8
  459. package/src/shade/renderer/particles/ParticleConstants.d.ts +23 -6
  460. package/src/shade/renderer/particles/ParticleConstants.d.ts.map +1 -1
  461. package/src/shade/renderer/particles/ParticleConstants.js +25 -6
  462. package/src/shade/renderer/particles/bounds/chunk_particle_bounds.js +5 -5
  463. package/src/shade/renderer/particles/bounds/shader_particle_bounds.js +187 -187
  464. package/src/shade/renderer/particles/data/PARTICLE_DATABASE_SPEC.d.ts +20 -9
  465. package/src/shade/renderer/particles/data/PARTICLE_DATABASE_SPEC.d.ts.map +1 -1
  466. package/src/shade/renderer/particles/data/PARTICLE_DATABASE_SPEC.js +36 -10
  467. package/src/shade/renderer/particles/data/PARTICLE_EMITTER_STRUCT.d.ts +8 -7
  468. package/src/shade/renderer/particles/data/PARTICLE_EMITTER_STRUCT.d.ts.map +1 -1
  469. package/src/shade/renderer/particles/data/PARTICLE_EMITTER_STRUCT.js +25 -16
  470. package/src/shade/renderer/particles/data/PARTICLE_FIELD_BINDING.d.ts +51 -0
  471. package/src/shade/renderer/particles/data/PARTICLE_FIELD_BINDING.d.ts.map +1 -0
  472. package/src/shade/renderer/particles/data/PARTICLE_FIELD_BINDING.js +56 -0
  473. package/src/shade/renderer/particles/data/particle_emitter_record.d.ts.map +1 -1
  474. package/src/shade/renderer/particles/data/particle_emitter_record.js +2 -0
  475. package/src/shade/renderer/particles/graph/ParticleGroupBuilder.d.ts +1 -1
  476. package/src/shade/renderer/particles/graph/ParticleGroupBuilder.js +1 -1
  477. package/src/shade/renderer/particles/graph/ParticleNodeDescription.d.ts +22 -11
  478. package/src/shade/renderer/particles/graph/ParticleNodeDescription.d.ts.map +1 -1
  479. package/src/shade/renderer/particles/graph/ParticleNodeDescription.js +14 -11
  480. package/src/shade/renderer/particles/graph/ParticleNodeRegistry.d.ts +4 -4
  481. package/src/shade/renderer/particles/graph/ParticleNodeRegistry.d.ts.map +1 -1
  482. package/src/shade/renderer/particles/graph/ParticleNodeRegistry.js +59 -24
  483. package/src/shade/renderer/particles/graph/ParticleValueType.d.ts +3 -3
  484. package/src/shade/renderer/particles/graph/ParticleValueType.js +287 -287
  485. package/src/shade/renderer/particles/graph/compile_particle_graph.d.ts +13 -6
  486. package/src/shade/renderer/particles/graph/compile_particle_graph.d.ts.map +1 -1
  487. package/src/shade/renderer/particles/graph/compile_particle_graph.js +46 -27
  488. package/src/shade/renderer/particles/graph/infer_particle_graph_types.d.ts +8 -2
  489. package/src/shade/renderer/particles/graph/infer_particle_graph_types.d.ts.map +1 -1
  490. package/src/shade/renderer/particles/graph/infer_particle_graph_types.js +240 -234
  491. package/src/shade/renderer/particles/graph/make_particle_group.d.ts +1 -1
  492. package/src/shade/renderer/particles/graph/make_particle_group.js +1 -1
  493. package/src/shade/renderer/particles/graph/particle_graph_authoring.d.ts +3 -2
  494. package/src/shade/renderer/particles/graph/particle_graph_authoring.d.ts.map +1 -1
  495. package/src/shade/renderer/particles/graph/particle_graph_authoring.js +3 -2
  496. package/src/shade/renderer/particles/graph/particle_graph_distributions.js +170 -170
  497. package/src/shade/renderer/particles/graph/particle_graph_json.d.ts +25 -16
  498. package/src/shade/renderer/particles/graph/particle_graph_json.d.ts.map +1 -1
  499. package/src/shade/renderer/particles/graph/particle_graph_json.js +16 -2
  500. package/src/shade/renderer/particles/graph/particle_node_parameters.d.ts +29 -15
  501. package/src/shade/renderer/particles/graph/particle_node_parameters.d.ts.map +1 -1
  502. package/src/shade/renderer/particles/graph/particle_node_parameters.js +54 -22
  503. package/src/shade/renderer/particles/graph/particle_node_presentation.d.ts.map +1 -1
  504. package/src/shade/renderer/particles/graph/particle_node_presentation.js +18 -8
  505. package/src/shade/renderer/particles/graph/validate_particle_graph.d.ts +9 -2
  506. package/src/shade/renderer/particles/graph/validate_particle_graph.d.ts.map +1 -1
  507. package/src/shade/renderer/particles/graph/validate_particle_graph.js +70 -32
  508. package/src/shade/renderer/particles/graph_particles.d.ts +9 -4
  509. package/src/shade/renderer/particles/graph_particles.d.ts.map +1 -1
  510. package/src/shade/renderer/particles/graph_particles.js +27 -4
  511. package/src/shade/renderer/particles/isa/InstructionStream.d.ts +7 -6
  512. package/src/shade/renderer/particles/isa/InstructionStream.d.ts.map +1 -1
  513. package/src/shade/renderer/particles/isa/InstructionStream.js +17 -16
  514. package/src/shade/renderer/particles/isa/ParticleAssembler.d.ts +17 -0
  515. package/src/shade/renderer/particles/isa/ParticleAssembler.d.ts.map +1 -1
  516. package/src/shade/renderer/particles/isa/ParticleAssembler.js +35 -0
  517. package/src/shade/renderer/particles/isa/ParticleProgram.d.ts +26 -6
  518. package/src/shade/renderer/particles/isa/ParticleProgram.d.ts.map +1 -1
  519. package/src/shade/renderer/particles/isa/ParticleProgram.js +61 -9
  520. package/src/shade/renderer/particles/isa/ParticleVMISA.d.ts +55 -4
  521. package/src/shade/renderer/particles/isa/ParticleVMISA.d.ts.map +1 -1
  522. package/src/shade/renderer/particles/isa/ParticleVMISA.js +79 -10
  523. package/src/shade/renderer/particles/isa/particle_assembly.d.ts +20 -6
  524. package/src/shade/renderer/particles/isa/particle_assembly.d.ts.map +1 -1
  525. package/src/shade/renderer/particles/isa/particle_assembly.js +136 -25
  526. package/src/shade/renderer/particles/isa/particle_link_global_fields.d.ts +20 -0
  527. package/src/shade/renderer/particles/isa/particle_link_global_fields.d.ts.map +1 -0
  528. package/src/shade/renderer/particles/isa/particle_link_global_fields.js +46 -0
  529. package/src/shade/renderer/particles/isa/validate_particle_program.js +8 -8
  530. package/src/shade/renderer/particles/layout/ParticleLayout.d.ts +62 -28
  531. package/src/shade/renderer/particles/layout/ParticleLayout.d.ts.map +1 -1
  532. package/src/shade/renderer/particles/layout/ParticleLayout.js +102 -62
  533. package/src/shade/renderer/particles/optimizer/dag/optimize_particle_program_vector.d.ts.map +1 -1
  534. package/src/shade/renderer/particles/optimizer/dag/optimize_particle_program_vector.js +2 -0
  535. package/src/shade/renderer/particles/optimizer/dag/particle_vector_groups.js +1 -1
  536. package/src/shade/renderer/particles/optimizer/dag/particle_vector_lift.js +4 -3
  537. package/src/shade/renderer/particles/optimizer/dag/particle_vector_lower.js +2 -2
  538. package/src/shade/renderer/particles/optimizer/dag/particle_vector_pack.d.ts +3 -3
  539. package/src/shade/renderer/particles/optimizer/dag/particle_vector_pack.js +11 -11
  540. package/src/shade/renderer/particles/optimizer/optimize_particle_program.d.ts +4 -4
  541. package/src/shade/renderer/particles/optimizer/optimize_particle_program.d.ts.map +1 -1
  542. package/src/shade/renderer/particles/optimizer/optimize_particle_program.js +5 -4
  543. package/src/shade/renderer/particles/optimizer/particle_integer_bounds.d.ts +1 -1
  544. package/src/shade/renderer/particles/optimizer/particle_integer_bounds.js +1 -1
  545. package/src/shade/renderer/particles/optimizer/particle_program_equivalence.d.ts +4 -4
  546. package/src/shade/renderer/particles/optimizer/particle_program_equivalence.d.ts.map +1 -1
  547. package/src/shade/renderer/particles/optimizer/particle_program_equivalence.js +13 -11
  548. package/src/shade/renderer/particles/optimizer/particle_program_fuzz.js +7 -7
  549. package/src/shade/renderer/particles/optimizer/particle_vm_semantics.d.ts +3 -2
  550. package/src/shade/renderer/particles/optimizer/particle_vm_semantics.d.ts.map +1 -1
  551. package/src/shade/renderer/particles/optimizer/particle_vm_semantics.js +5 -4
  552. package/src/shade/renderer/particles/optimizer/pass_allocate_registers.d.ts.map +1 -1
  553. package/src/shade/renderer/particles/optimizer/pass_dedup_constants.d.ts.map +1 -1
  554. package/src/shade/renderer/particles/optimizer/pass_eliminate_dead_code.d.ts.map +1 -1
  555. package/src/shade/renderer/particles/optimizer/pass_eliminate_dead_code.js +51 -51
  556. package/src/shade/renderer/particles/optimizer/pass_fold_constants.d.ts.map +1 -1
  557. package/src/shade/renderer/particles/optimizer/pass_fold_constants.js +5 -5
  558. package/src/shade/renderer/particles/optimizer/pass_inline_constants.d.ts.map +1 -1
  559. package/src/shade/renderer/particles/optimizer/pass_narrow_widths.d.ts.map +1 -1
  560. package/src/shade/renderer/particles/optimizer/pass_pack_registers.d.ts.map +1 -1
  561. package/src/shade/renderer/particles/optimizer/pass_peephole.d.ts.map +1 -1
  562. package/src/shade/renderer/particles/optimizer/pass_propagate_copies.d.ts.map +1 -1
  563. package/src/shade/renderer/particles/optimizer/pass_schedule.d.ts.map +1 -1
  564. package/src/shade/renderer/particles/optimizer/pass_schedule.js +2 -2
  565. package/src/shade/renderer/particles/optimizer/pass_share_subexpressions.d.ts.map +1 -1
  566. package/src/shade/renderer/particles/optimizer/pass_share_subexpressions.js +5 -4
  567. package/src/shade/renderer/particles/runtime/EmitterRegistry.d.ts +93 -22
  568. package/src/shade/renderer/particles/runtime/EmitterRegistry.d.ts.map +1 -1
  569. package/src/shade/renderer/particles/runtime/EmitterRegistry.js +336 -29
  570. package/src/shade/renderer/particles/runtime/GPUParticleEmitterContext.d.ts +43 -3
  571. package/src/shade/renderer/particles/runtime/GPUParticleEmitterContext.d.ts.map +1 -1
  572. package/src/shade/renderer/particles/runtime/GPUParticleEmitterContext.js +43 -3
  573. package/src/shade/renderer/particles/runtime/ParticleEmitter.d.ts +56 -26
  574. package/src/shade/renderer/particles/runtime/ParticleEmitter.d.ts.map +1 -1
  575. package/src/shade/renderer/particles/runtime/ParticleEmitter.js +89 -33
  576. package/src/shade/renderer/particles/runtime/ParticleGlobalFields.d.ts +85 -0
  577. package/src/shade/renderer/particles/runtime/ParticleGlobalFields.d.ts.map +1 -0
  578. package/src/shade/renderer/particles/runtime/ParticleGlobalFields.js +221 -0
  579. package/src/shade/renderer/particles/runtime/ProgramHeap.d.ts +143 -18
  580. package/src/shade/renderer/particles/runtime/ProgramHeap.d.ts.map +1 -1
  581. package/src/shade/renderer/particles/runtime/ProgramHeap.js +378 -28
  582. package/src/shade/renderer/particles/runtime/create_particle_effect.d.ts +19 -7
  583. package/src/shade/renderer/particles/runtime/create_particle_effect.d.ts.map +1 -1
  584. package/src/shade/renderer/particles/runtime/create_particle_effect.js +22 -9
  585. package/src/shade/renderer/particles/shaders/chunk_particle_billboard_vertex.js +8 -8
  586. package/src/shade/renderer/particles/shaders/chunk_particle_render_math.js +1 -1
  587. package/src/shade/renderer/particles/shaders/chunk_particle_setup_context.d.ts +3 -0
  588. package/src/shade/renderer/particles/shaders/chunk_particle_setup_context.d.ts.map +1 -1
  589. package/src/shade/renderer/particles/shaders/chunk_particle_setup_context.js +4 -0
  590. package/src/shade/renderer/particles/shaders/shader_particle_field_bind.d.ts +30 -0
  591. package/src/shade/renderer/particles/shaders/shader_particle_field_bind.d.ts.map +1 -0
  592. package/src/shade/renderer/particles/shaders/shader_particle_field_bind.js +121 -0
  593. package/src/shade/renderer/particles/vm/PARTICLE_VM_CONTEXT_STRUCT.d.ts.map +1 -1
  594. package/src/shade/renderer/particles/vm/PARTICLE_VM_CONTEXT_STRUCT.js +3 -1
  595. package/src/shade/renderer/particles/vm/ParticleVMReference.d.ts +12 -4
  596. package/src/shade/renderer/particles/vm/ParticleVMReference.d.ts.map +1 -1
  597. package/src/shade/renderer/particles/vm/ParticleVMReference.js +48 -11
  598. package/src/shade/renderer/particles/vm/chunk_particle_vm.d.ts.map +1 -1
  599. package/src/shade/renderer/particles/vm/chunk_particle_vm.js +36 -14
  600. package/src/shade/renderer/path_tracer/shader/INDIRECT_TEXTURE_FILTER.d.ts +13 -0
  601. package/src/shade/renderer/path_tracer/shader/INDIRECT_TEXTURE_FILTER.d.ts.map +1 -0
  602. package/src/shade/renderer/path_tracer/shader/INDIRECT_TEXTURE_FILTER.js +14 -0
  603. package/src/shade/renderer/path_tracer/shader/IndirectTextureFilter.d.ts +10 -0
  604. package/src/shade/renderer/path_tracer/shader/IndirectTextureFilter.d.ts.map +1 -0
  605. package/src/shade/renderer/path_tracer/shader/IndirectTextureFilter.js +15 -0
  606. package/src/shade/renderer/path_tracer/shader/chunk_indirect_sample_tangent_normal.d.ts +4 -3
  607. package/src/shade/renderer/path_tracer/shader/chunk_indirect_sample_tangent_normal.d.ts.map +1 -1
  608. package/src/shade/renderer/path_tracer/shader/chunk_indirect_sample_tangent_normal.js +6 -5
  609. package/src/shade/renderer/path_tracer/shader/chunk_indirect_sample_texture.d.ts +17 -2
  610. package/src/shade/renderer/path_tracer/shader/chunk_indirect_sample_texture.d.ts.map +1 -1
  611. package/src/shade/renderer/path_tracer/shader/chunk_indirect_sample_texture.js +25 -57
  612. package/src/shade/renderer/path_tracer/shader/chunk_indirect_sample_texture_exact.d.ts +13 -0
  613. package/src/shade/renderer/path_tracer/shader/chunk_indirect_sample_texture_exact.d.ts.map +1 -0
  614. package/src/shade/renderer/path_tracer/shader/chunk_indirect_sample_texture_exact.js +70 -0
  615. package/src/shade/renderer/path_tracer/shader/chunk_indirect_sample_texture_stochastic.d.ts +10 -0
  616. package/src/shade/renderer/path_tracer/shader/chunk_indirect_sample_texture_stochastic.d.ts.map +1 -0
  617. package/src/shade/renderer/path_tracer/shader/chunk_indirect_sample_texture_stochastic.js +59 -0
  618. package/src/shade/renderer/path_tracer/shader/chunk_indirect_texture_load.d.ts +12 -0
  619. package/src/shade/renderer/path_tracer/shader/chunk_indirect_texture_load.d.ts.map +1 -0
  620. package/src/shade/renderer/path_tracer/shader/chunk_indirect_texture_load.js +49 -0
  621. package/src/shade/renderer/path_tracer/shader/chunk_ray_hit_to_opacity.d.ts.map +1 -1
  622. package/src/shade/renderer/path_tracer/shader/chunk_ray_hit_to_opacity.js +1 -0
  623. package/src/shade/renderer/path_tracer/shader/chunk_render_trace_path.d.ts.map +1 -1
  624. package/src/shade/renderer/path_tracer/shader/chunk_render_trace_path.js +1 -0
  625. package/src/shade/renderer/path_tracer/shader/chunk_sample_material.d.ts.map +1 -1
  626. package/src/shade/renderer/path_tracer/shader/chunk_sample_material.js +2 -1
  627. package/src/shade/renderer/path_tracer/shader/chunk_sample_material_data.d.ts +7 -0
  628. package/src/shade/renderer/path_tracer/shader/chunk_sample_material_data.d.ts.map +1 -1
  629. package/src/shade/renderer/path_tracer/shader/chunk_sample_material_data.js +15 -6
  630. package/src/shade/renderer/postprocess/PostProcess.d.ts.map +1 -1
  631. package/src/shade/renderer/postprocess/PostProcess.js +2 -1
  632. package/src/shade/renderer/postprocess/gtao/GTAO.d.ts +31 -19
  633. package/src/shade/renderer/postprocess/gtao/GTAO.d.ts.map +1 -1
  634. package/src/shade/renderer/postprocess/gtao/GTAO.js +44 -30
  635. package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.d.ts.map +1 -1
  636. package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.js +16 -2
  637. package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
  638. package/src/shade/renderer/postprocess/nss/NSS.d.ts.map +1 -1
  639. package/src/shade/renderer/postprocess/nss/NSS.js +4 -3
  640. package/src/shade/renderer/postprocess/ssr/SSR.d.ts +7 -1
  641. package/src/shade/renderer/postprocess/ssr/SSR.d.ts.map +1 -1
  642. package/src/shade/renderer/postprocess/ssr/SSR.js +16 -0
  643. package/src/shade/renderer/postprocess/ssr/resolve/chunk_ssr_add_hit_irradiance.d.ts +20 -2
  644. package/src/shade/renderer/postprocess/ssr/resolve/chunk_ssr_add_hit_irradiance.d.ts.map +1 -1
  645. package/src/shade/renderer/postprocess/ssr/resolve/chunk_ssr_add_hit_irradiance.js +53 -3
  646. package/src/shade/renderer/postprocess/ssr/resolve/chunk_ssr_sample_radiance.d.ts +10 -2
  647. package/src/shade/renderer/postprocess/ssr/resolve/chunk_ssr_sample_radiance.d.ts.map +1 -1
  648. package/src/shade/renderer/postprocess/ssr/resolve/chunk_ssr_sample_radiance.js +31 -3
  649. package/src/shade/renderer/postprocess/ssr/resolve/ssr_resolve_shader.d.ts.map +1 -1
  650. package/src/shade/renderer/postprocess/ssr/resolve/ssr_resolve_shader.js +17 -11
  651. package/src/shade/renderer/postprocess/taa/TAA.d.ts +4 -2
  652. package/src/shade/renderer/postprocess/taa/TAA.d.ts.map +1 -1
  653. package/src/shade/renderer/postprocess/taa/TAA.js +6 -1
  654. package/src/shade/renderer/postprocess/taa/chunk_fsr2_compute_depth_clip.d.ts.map +1 -1
  655. package/src/shade/renderer/postprocess/taa/chunk_fsr2_compute_depth_clip.js +10 -5
  656. package/src/shade/renderer/postprocess/taa/shader_taa.d.ts.map +1 -1
  657. package/src/shade/renderer/postprocess/taa/shader_taa.js +15 -12
  658. package/src/shade/renderer/postprocess/velocity/graph_add_per_object_velocity.d.ts.map +1 -1
  659. package/src/shade/renderer/postprocess/velocity/graph_add_per_object_velocity.js +26 -17
  660. package/src/shade/renderer/rasterize/forward/chunk_forward_sample_indirect.d.ts +4 -3
  661. package/src/shade/renderer/rasterize/forward/chunk_forward_sample_indirect.d.ts.map +1 -1
  662. package/src/shade/renderer/rasterize/forward/chunk_forward_sample_indirect.js +6 -4
  663. package/src/shade/renderer/rasterize/forward/chunk_forward_sample_indirect_probe_clipmap.d.ts +12 -0
  664. package/src/shade/renderer/rasterize/forward/chunk_forward_sample_indirect_probe_clipmap.d.ts.map +1 -0
  665. package/src/shade/renderer/rasterize/forward/chunk_forward_sample_indirect_probe_clipmap.js +57 -0
  666. package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.d.ts +4 -2
  667. package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.d.ts.map +1 -1
  668. package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.js +4 -2
  669. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.d.ts +8 -2
  670. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.d.ts.map +1 -1
  671. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.js +28 -15
  672. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_draw.d.ts +2 -1
  673. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_draw.d.ts.map +1 -1
  674. package/src/shade/renderer/rasterize/native/avboit/shader_avboit_draw.js +19 -3
  675. package/src/shade/renderer/rasterize/native/graph_rasterize_meshes_alpha_tested.d.ts.map +1 -1
  676. package/src/shade/renderer/rasterize/native/graph_rasterize_meshes_alpha_tested.js +4 -2
  677. package/src/shade/renderer/rasterize/native/oit/graph_rasterize_meshes_transparent_oit.d.ts +3 -1
  678. package/src/shade/renderer/rasterize/native/oit/graph_rasterize_meshes_transparent_oit.d.ts.map +1 -1
  679. package/src/shade/renderer/rasterize/native/oit/graph_rasterize_meshes_transparent_oit.js +27 -14
  680. package/src/shade/renderer/rasterize/native/oit/shader_oit_resolve_moments.d.ts +2 -1
  681. package/src/shade/renderer/rasterize/native/oit/shader_oit_resolve_moments.d.ts.map +1 -1
  682. package/src/shade/renderer/rasterize/native/oit/shader_oit_resolve_moments.js +19 -3
  683. package/src/shade/renderer/scene/GPUSceneContext.d.ts +7 -1
  684. package/src/shade/renderer/scene/GPUSceneContext.d.ts.map +1 -1
  685. package/src/shade/renderer/scene/GPUSceneContext.js +26 -1
  686. package/src/shade/renderer/shader/chunk/bvh/chunk_bvh_query_aabb_cooperative.d.ts +7 -7
  687. package/src/shade/renderer/shader/chunk/bvh/chunk_bvh_query_aabb_cooperative.d.ts.map +1 -1
  688. package/src/shade/renderer/shader/chunk/bvh/chunk_bvh_query_aabb_cooperative.js +262 -294
  689. package/src/shade/renderer/shader/chunk/filter/MULTISCALE_MEAN_STRUCT.d.ts +9 -0
  690. package/src/shade/renderer/shader/chunk/filter/MULTISCALE_MEAN_STRUCT.d.ts.map +1 -0
  691. package/src/shade/renderer/shader/chunk/filter/MULTISCALE_MEAN_STRUCT.js +21 -0
  692. package/src/shade/renderer/shader/chunk/filter/chunk_multiscale_mean.d.ts +43 -0
  693. package/src/shade/renderer/shader/chunk/filter/chunk_multiscale_mean.d.ts.map +1 -0
  694. package/src/shade/renderer/shader/chunk/filter/chunk_multiscale_mean.js +87 -0
  695. package/src/shade/renderer/shader/chunk/geometry/meshlet/chunk_read_meshlet_triangle_bounds.d.ts +2 -8
  696. package/src/shade/renderer/shader/chunk/geometry/meshlet/chunk_read_meshlet_triangle_bounds.d.ts.map +1 -1
  697. package/src/shade/renderer/shader/chunk/geometry/meshlet/chunk_read_meshlet_triangle_bounds.js +25 -52
  698. package/src/shade/renderer/shader/chunk/geometry/meshlet/chunk_read_meshlet_triangle_positions.d.ts +15 -0
  699. package/src/shade/renderer/shader/chunk/geometry/meshlet/chunk_read_meshlet_triangle_positions.d.ts.map +1 -0
  700. package/src/shade/renderer/shader/chunk/geometry/meshlet/chunk_read_meshlet_triangle_positions.js +52 -0
  701. package/src/shade/renderer/shader/chunk/geometry/triangle/TRIANGLE_POSITIONS_STRUCT.d.ts +7 -0
  702. package/src/shade/renderer/shader/chunk/geometry/triangle/TRIANGLE_POSITIONS_STRUCT.d.ts.map +1 -0
  703. package/src/shade/renderer/shader/chunk/geometry/triangle/TRIANGLE_POSITIONS_STRUCT.js +12 -0
  704. package/src/shade/renderer/shader/chunk/grid/chunk_index_to_grid2d_reciprocal.d.ts +22 -0
  705. package/src/shade/renderer/shader/chunk/grid/chunk_index_to_grid2d_reciprocal.d.ts.map +1 -0
  706. package/src/shade/renderer/shader/chunk/grid/chunk_index_to_grid2d_reciprocal.js +37 -0
  707. package/src/shade/renderer/shader/chunk/light/lpv/depth/chunk_lpv_mask_weights_by_visibility.d.ts.map +1 -1
  708. package/src/shade/renderer/shader/chunk/light/lpv/depth/chunk_lpv_mask_weights_by_visibility.js +26 -60
  709. package/src/shade/renderer/shader/chunk/light/probe/chunk_probe_visibility_weight.d.ts +23 -0
  710. package/src/shade/renderer/shader/chunk/light/probe/chunk_probe_visibility_weight.d.ts.map +1 -0
  711. package/src/shade/renderer/shader/chunk/light/probe/chunk_probe_visibility_weight.js +54 -0
  712. package/src/shade/renderer/shader/chunk/math/chunk_f16_round_stochastic.d.ts +25 -0
  713. package/src/shade/renderer/shader/chunk/math/chunk_f16_round_stochastic.d.ts.map +1 -0
  714. package/src/shade/renderer/shader/chunk/math/chunk_f16_round_stochastic.js +43 -0
  715. package/src/shade/renderer/shader/chunk/noise/r/chunk_r2_canonical.d.ts +7 -1
  716. package/src/shade/renderer/shader/chunk/noise/r/chunk_r2_canonical.d.ts.map +1 -1
  717. package/src/shade/renderer/shader/chunk/noise/r/chunk_r2_canonical.js +1 -1
  718. package/src/shade/renderer/shader/chunk/random/chunk_jitter_from_vectors.d.ts +14 -0
  719. package/src/shade/renderer/shader/chunk/random/chunk_jitter_from_vectors.d.ts.map +1 -0
  720. package/src/shade/renderer/shader/chunk/random/chunk_jitter_from_vectors.js +37 -0
  721. package/src/shade/renderer/shader/chunk/random/reservoir/chunk_pick2_wrs_mat2x4.d.ts +27 -0
  722. package/src/shade/renderer/shader/chunk/random/reservoir/chunk_pick2_wrs_mat2x4.d.ts.map +1 -0
  723. package/src/shade/renderer/shader/chunk/random/reservoir/chunk_pick2_wrs_mat2x4.js +61 -0
  724. package/src/shade/renderer/shader/chunk/spherical_harmonics/l1i/chunk_resolve_irradiance_sh_to_specular.d.ts +21 -0
  725. package/src/shade/renderer/shader/chunk/spherical_harmonics/l1i/chunk_resolve_irradiance_sh_to_specular.d.ts.map +1 -0
  726. package/src/shade/renderer/shader/chunk/spherical_harmonics/l1i/chunk_resolve_irradiance_sh_to_specular.js +41 -0
  727. package/src/shade/renderer/shader/chunk/subgroup/chunk_subgroup_reserve_workgroup.d.ts +21 -0
  728. package/src/shade/renderer/shader/chunk/subgroup/chunk_subgroup_reserve_workgroup.d.ts.map +1 -0
  729. package/src/shade/renderer/shader/chunk/subgroup/chunk_subgroup_reserve_workgroup.js +42 -0
  730. package/src/shade/renderer/texture/util/texture_fill_rectangle.d.ts.map +1 -1
  731. package/src/shade/renderer/texture/util/texture_fill_rectangle.js +282 -267
  732. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts +8 -4
  733. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts.map +1 -1
  734. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.js +36 -36
  735. package/src/shade/renderer/volumetrics/irradiance_clipmap/GPUIrradianceClipmap.d.ts +6 -1
  736. package/src/shade/renderer/volumetrics/irradiance_clipmap/GPUIrradianceClipmap.d.ts.map +1 -1
  737. package/src/shade/renderer/volumetrics/irradiance_clipmap/GPUIrradianceClipmap.js +498 -484
  738. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_gi_sample.d.ts +15 -2
  739. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_gi_sample.d.ts.map +1 -1
  740. package/src/shade/renderer/volumetrics/irradiance_clipmap/chunk_irradiance_clipmap_gi_sample.js +57 -3
  741. package/src/shade/renderer/volumetrics/irradiance_clipmap/shader_irradiance_clipmap_build.d.ts.map +1 -1
  742. package/src/shade/renderer/volumetrics/irradiance_clipmap/shader_irradiance_clipmap_build.js +197 -191
  743. package/src/shade/renderer/volumetrics/taa/shader_volumetrics_taa.js +4 -4
  744. package/src/shade/renderer/water/GPUWaterRenderer.d.ts +2 -1
  745. package/src/shade/renderer/water/GPUWaterRenderer.d.ts.map +1 -1
  746. package/src/shade/renderer/water/GPUWaterRenderer.js +5 -1
  747. package/src/shade/renderer/water/chunk_water_gi.d.ts +13 -3
  748. package/src/shade/renderer/water/chunk_water_gi.d.ts.map +1 -1
  749. package/src/shade/renderer/water/chunk_water_gi.js +47 -5
  750. package/src/shade/renderer/water/query/GPUWaterQueries.d.ts +1 -1
  751. package/src/shade/renderer/water/shader_water.d.ts +3 -2
  752. package/src/shade/renderer/water/shader_water.d.ts.map +1 -1
  753. package/src/shade/renderer/water/shader_water.js +9 -3
  754. package/src/shade/renderer/wind/GPUSceneWind.d.ts +1 -1
  755. package/src/shade/wgsl/emulator/CPUBitmapData.d.ts +19 -0
  756. package/src/shade/wgsl/emulator/CPUBitmapData.d.ts.map +1 -1
  757. package/src/shade/wgsl/emulator/CPUBitmapData.js +27 -0
  758. package/src/shade/wgsl/emulator/WGSLLanguageCore.d.ts.map +1 -1
  759. package/src/shade/wgsl/emulator/WGSLLanguageCore.js +39 -18
  760. package/src/view/elements/progress/segmented/GhostTrail.d.ts +56 -0
  761. package/src/view/elements/progress/segmented/GhostTrail.d.ts.map +1 -0
  762. package/src/view/elements/progress/segmented/GhostTrail.js +98 -0
  763. package/src/view/elements/progress/segmented/RESOURCE_BAR_SEGMENTS.d.ts +3 -1
  764. package/src/view/elements/progress/segmented/RESOURCE_BAR_SEGMENTS.d.ts.map +1 -1
  765. package/src/view/elements/progress/segmented/RESOURCE_BAR_SEGMENTS.js +3 -1
  766. package/src/view/elements/progress/segmented/SEGMENT_TIERS_DEFAULT.d.ts +11 -0
  767. package/src/view/elements/progress/segmented/SEGMENT_TIERS_DEFAULT.d.ts.map +1 -0
  768. package/src/view/elements/progress/segmented/SEGMENT_TIERS_DEFAULT.js +15 -0
  769. package/src/view/elements/progress/segmented/SegmentDefinition.d.ts +3 -0
  770. package/src/view/elements/progress/segmented/SegmentDefinition.d.ts.map +1 -1
  771. package/src/view/elements/progress/segmented/SegmentDefinition.js +3 -0
  772. package/src/view/elements/progress/segmented/SegmentTier.d.ts +38 -0
  773. package/src/view/elements/progress/segmented/SegmentTier.d.ts.map +1 -0
  774. package/src/view/elements/progress/segmented/SegmentTier.js +50 -0
  775. package/src/view/elements/progress/segmented/SegmentedResourceBarView.d.ts +155 -10
  776. package/src/view/elements/progress/segmented/SegmentedResourceBarView.d.ts.map +1 -1
  777. package/src/view/elements/progress/segmented/SegmentedResourceBarView.js +427 -101
  778. package/src/view/elements/progress/segmented/playground/index.html +78 -0
  779. package/src/view/elements/progress/segmented/playground/main.d.ts +2 -0
  780. package/src/view/elements/progress/segmented/playground/main.d.ts.map +1 -0
  781. package/src/view/elements/progress/segmented/playground/main.js +173 -0
  782. package/src/view/elements/progress/segmented/segment_cell_layout.d.ts +48 -0
  783. package/src/view/elements/progress/segmented/segment_cell_layout.d.ts.map +1 -0
  784. package/src/view/elements/progress/segmented/segment_cell_layout.js +75 -0
  785. package/src/view/elements/progress/segmented/segment_notch_place.d.ts +18 -0
  786. package/src/view/elements/progress/segmented/segment_notch_place.d.ts.map +1 -0
  787. package/src/view/elements/progress/segmented/segment_notch_place.js +28 -0
  788. package/src/view/elements/progress/segmented/segment_tick_positions.d.ts +32 -0
  789. package/src/view/elements/progress/segmented/segment_tick_positions.d.ts.map +1 -0
  790. package/src/view/elements/progress/segmented/segment_tick_positions.js +103 -0
  791. package/src/view/elements/progress/segmented/segmented-resource-bar.css +142 -0
  792. package/src/shade/renderer/geometry/sdf/SDF_BUILD_SETTINGS_STRUCT.d.ts +0 -3
  793. package/src/shade/renderer/geometry/sdf/SDF_BUILD_SETTINGS_STRUCT.d.ts.map +0 -1
  794. package/src/shade/renderer/geometry/sdf/SFD_BUILD_WORKGROUP_SIZE.d.ts +0 -2
  795. package/src/shade/renderer/geometry/sdf/SFD_BUILD_WORKGROUP_SIZE.d.ts.map +0 -1
  796. package/src/shade/renderer/geometry/sdf/chunk_blas_query_sdf.d.ts +0 -8
  797. package/src/shade/renderer/geometry/sdf/chunk_blas_query_sdf.d.ts.map +0 -1
  798. package/src/shade/renderer/geometry/sdf/chunk_blas_query_volume_sign.d.ts +0 -3
  799. package/src/shade/renderer/geometry/sdf/chunk_blas_query_volume_sign.d.ts.map +0 -1
  800. package/src/shade/renderer/geometry/sdf/shader_build_sdf.d.ts +0 -3
  801. package/src/shade/renderer/geometry/sdf/shader_build_sdf.d.ts.map +0 -1
  802. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_object.d.ts.map +0 -1
  803. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_object.js +0 -42
  804. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/shader_mesh_brick_provider.d.ts +0 -35
  805. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/shader_mesh_brick_provider.d.ts.map +0 -1
  806. package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/shader_mesh_brick_provider.js +0 -247
  807. package/src/shade/renderer/particles/shaders/shader_particle_init_free_list.d.ts +0 -31
  808. package/src/shade/renderer/particles/shaders/shader_particle_init_free_list.d.ts.map +0 -1
  809. package/src/shade/renderer/shadow/vsm/VSMMoverTracker.d.ts +0 -42
  810. package/src/shade/renderer/shadow/vsm/VSMMoverTracker.d.ts.map +0 -1
  811. package/src/shade/renderer/shadow/vsm/VSM_MOVER_LAYOUT.d.ts +0 -54
  812. package/src/shade/renderer/shadow/vsm/VSM_MOVER_LAYOUT.d.ts.map +0 -1
  813. package/src/shade/renderer/shadow/vsm/shader/chunk_vsm_append_mover.d.ts +0 -14
  814. package/src/shade/renderer/shadow/vsm/shader/chunk_vsm_append_mover.d.ts.map +0 -1
  815. package/src/shade/renderer/shadow/vsm/shader/shader_vsm_movers.d.ts +0 -30
  816. package/src/shade/renderer/shadow/vsm/shader/shader_vsm_movers.d.ts.map +0 -1
  817. package/src/shade/renderer/shadow/vsm/shader/shader_vsm_skinned_movers.d.ts +0 -23
  818. package/src/shade/renderer/shadow/vsm/shader/shader_vsm_skinned_movers.d.ts.map +0 -1
@@ -22,7 +22,8 @@ Alex reviewed revision (a). Items 1–8 are his; item 9 follows from them.
22
22
  performance (§6.1).
23
23
  4. **Brick providers.** A pass that writes requested bricks plugs in; outputs merge per brick, so the
24
24
  representation stays sparse and only bricks that scroll in or change are written. Virtual geometry
25
- is served by one (§7.6, §7.8).
25
+ has caches of its own, baked from a proxy cut from each geometry and composed as a mesh's are (§7.8,
26
+ S8).
26
27
  5. **Skinned meshes** use their unskinned representation, with hooks for a dense animated patch later
27
28
  (§6.3, §7.7, §17).
28
29
  6. **Pixels pick 2 of the 8 cage probes** by Brick4's weighted reservoir sampling, after a
@@ -79,8 +80,9 @@ What was asked for:
79
80
  3. **The probe clipmap is what shading samples (§8)**, as in (a).
80
81
  - A sparse cache, wanted from the depth buffer. Octahedral radiance `rgba16float`, depth moments
81
82
  `rg16float`, SH2 in the engine's YCoCg L1 (`METRO_L1_IRRADIANCE_STRUCT`).
82
- - A fixed ray budget. Hits are lit by emissive, the sun through an SDF soft shadow, local lights and
83
- last frame's probes. Their diffuse albedo and emissive come from the cards.
83
+ - A fixed ray budget. Hits are lit by emissive, the sun and one picked local light through shadow
84
+ rays traced in the bricks, and last frame's probes. Their diffuse albedo and emissive come from the
85
+ cards.
84
86
  4. **Pixels pick two probes (§9).**
85
87
  - A visibility-only pass weighs the eight cage probes: trilinear × wrap back face × Chebyshev ×
86
88
  crush.
@@ -281,8 +283,11 @@ clipmap toolkit (§16).
281
283
  - it interpolates uv, vertex colour, normal and tangent from a triangle and barycentrics;
282
284
  - it samples the material: albedo × colour factor × vertex colour, ORM, emissive × factor, opacity.
283
285
  - It reads `GPUResidentMaterialContext` (on `GraphicsContext`): the material table
284
- (`INDIRECT_MATERIAL_STRUCT`) and a 2-D array of 64² texture slots, one level each, sampled by
285
- `indirect_sample_texture` with each sampler's wrap mode and a stochastic bilinear tap.
286
+ (`INDIRECT_MATERIAL_STRUCT`) and a 2-D array of 64² texture slots, each with its mip chain down to
287
+ one texel since 2026-10-30, sampled by `indirect_sample_texture` at an explicit level with each
288
+ sampler's wrap mode: one stochastic tap of the trilinear filter by default, which at level 0 is the
289
+ stochastic bilinear tap it always was, or the filter itself where the shader's
290
+ `INDIRECT_TEXTURE_FILTER` is `Exact`.
286
291
  - Its taps and dither draw on the path tracer's random state. Alpha is stochastic: a hit is opaque
287
292
  with probability equal to its opacity (`chunk_ray_shaded_query_occluded`, through
288
293
  `sample_material_alpha`).
@@ -296,7 +301,7 @@ clipmap toolkit (§16).
296
301
 
297
302
  **What rays could not see.** vgeo rows are in the TLAS with an empty BLAS, and DynamicMesh has no row.
298
303
  Under (b):
299
- - virtual geometry is written by its provider (§7.8);
304
+ - virtual geometry is baked from a proxy cut from it, and composed as a mesh (§7.8, S8);
300
305
  - DynamicMesh stays out by construction: it changes every frame, and a cache of it would be a cache of
301
306
  nothing (§17).
302
307
 
@@ -420,11 +425,11 @@ the SDF [49:08].
420
425
  | octahedral radiance + border; distance with mean/mean² | same, `rgba16float` and `rg16float` with the engine's skirt chunks (§8.2) | hardware bilinear for every consumer tap |
421
426
  | irradiance as octahedral 12² (frustum) or SH (world cache) | SH2 YCoCg L1 for every probe | the request; 32 B a probe |
422
427
  | fixed ray budget; per-probe requests scaled to it | budget in rays a frame; period follows the live count (§8.5) | same intent, deterministic schedule |
423
- | importance-sampled directions + every-5th-frame uniform | one jittered ray per texel (stratified), always | no starvation, no second schedule; revisit only on a measured noise problem (§17) |
424
- | moving-average estimator, 5 u32 per texel | exponential blend over a response time in seconds (§8.8) | DDGI's form; one history; a physical knob |
428
+ | importance-sampled directions + every-5th-frame uniform | the same: every 5th update uniform, the rest a CDF walk over the probe's history from a rotating corner, each texel held under 64 rays (§8.6, as built 2026-10-29) | Alex found the noise of one jittered ray a texel visible (D11) |
429
+ | moving-average estimator, 5 u32 per texel | the same estimator, its short mean over a response time in seconds; 6 words a texel with the atlas's (§8.8) | built 2026-10-29, once Alex judged the noise visible (D11); the knob stays physical |
425
430
  | SDF shadow ray per hit for the sun | same (§8.6) | shadow maps answer "lit" off screen (§1) |
426
431
  | light cache for point lights | the world light clusters, keyed by this clipmap (§8.7) | reuse |
427
- | world-cache probes at hits | not in v1 (§17) | a second probe kind; added only if off-screen bounce measures short |
432
+ | world-cache probes at hits | hits want the probes they read, one step from the screen, the one probe kind (§8.3) | the off-screen bounce measured short at S5a (D31) |
428
433
  | stochastic 1-of-8 + reservoirs + denoise | 2 of 8 by Brick4's WRS, resolved by TAA (§9.1, §9.4) | the review; two probe reads, no denoiser |
429
434
  | prefiltered radiance levels for specular | the radiance tap blended with GGX-convolved SH2 by roughness (§9.3) | the review: WebGPU's mips; LPV's blend |
430
435
  | ambient probes and froxel GI | the existing irradiance clipmap samples this mode's probes (§9.5) | fog has no GI in any mode today |
@@ -692,7 +697,7 @@ Embark Studios' research renderer.
692
697
  | Mesh SDF | sparse bricks in size classes, with levels | `GPUGeometrySDFManager`'s dense tiles | the review; memory follows the surface |
693
698
  | Material | cards holding diffuse albedo and emissive captured per (geometry, material), recaptured when the material's version moves | the voxel lattice, (a); cards holding uvs with the material evaluated live | the review: no material work at a hit (§15) |
694
699
  | Probe residency | sparse cache from the screen | dense lattice | 10× less memory and work at the same lattice (§11) |
695
- | Rays | one jittered ray per texel | importance sampling | no starvation, no extra schedule, no CDF |
700
+ | Rays | Enshrouded's: guided by the probe's radiance, every 5th update uniform (§8.6) | one jittered ray per texel, (b) as first built | its noise measured visible (D11) |
696
701
  | Temporal | exponential over a response time | moving-average estimator | one history, 8 B a texel |
697
702
  | Pixel sampling | 2 of 8 by WRS | all eight, (a); 1 of 8 + reservoirs + denoise (Enshrouded) | the review; two probe reads, no denoiser |
698
703
  | Specular | radiance tap ⊕ GGX-convolved SH2 | prefiltered levels | the review; LPV's blend |
@@ -795,10 +800,10 @@ Verdicts:
795
800
  | Pick 2 of 8 | `sample_discrete_wrs_mat2x4`; the selection inside `brick4_probe_meta_pick2` | **REUSE**; **EXTRACT** `pick2_wrs_mat2x4`, Brick4 keeps its address mapping |
796
801
  | Noise | `chunk_stbn_sample_vec2`; `jitter_from_vectors` (private to Brick4's forward chunk) | **REUSE**; **EXTRACT** the hash |
797
802
  | Directional lights | `directional_lights_iteration_mask`, the light database records | **REUSE** |
798
- | Local lights at a world point | `world_light_cluster_metadata_by_texel`, `irradiance_clipmap_get_point_light_info_by_index` / `_spot_…`, `volumetrics_point_light_shadow` / `_spot_…` | **GENERALISE** the cluster keying from `IRRADIANCE_CLIPMAP_STRUCT` to a `CLIPMAP_LEVEL_ARRAY`; the light and shadow chunks **REUSE** |
803
+ | Local lights at a world point | `world_light_cluster_metadata_by_texel`, `irradiance_clipmap_get_point_light_info_by_index` / `_spot_…` | **GENERALISE** the cluster keying from `IRRADIANCE_CLIPMAP_STRUCT` to a `CLIPMAP_LEVEL_ARRAY`; the light chunks **REUSE**. Their shadows were `volumetrics_{point,spot}_light_shadow` until 2026-10-30, and are shadow rays in the bricks since (§8.7) |
799
804
  | Sky for misses; final fallback | `sample_environment_color`, `get_ibl_irradiance` | **REUSE** |
800
805
  | Specular split at a pixel | `compute_indirect_specular`, `tSplitSum`, inside the deferred resolve | **REUSE** |
801
- | SDF soft-shadow penumbra | inside `shader_scene_sdf_soft_shadows` | **EXTRACT** the estimator as a pure chunk; the dense-volume shader keeps calling it. Not done (S3e): its classic estimate cannot meet §12's oracle, and the brick clipmap's is Quílez's two-sided one (§7.9) |
806
+ | SDF soft-shadow penumbra | inside `shader_scene_sdf_soft_shadows` | **EXTRACT** the estimator as a pure chunk; the dense-volume shader keeps calling it. Not done (S3e): its classic estimate cannot meet §12's oracle, and the brick clipmap's was Quílez's two-sided one (§7.9), itself replaced by shadow rays on 2026-10-30 (§8.7) |
802
807
  | Appends | `chunk_collection_u32_append` | **REUSE** |
803
808
  | Parallax toward a probe's hit | LPV's iterative correction in `lpv_sample_light` | **GENERALISE** to take a depth-moment reader (§9.3) |
804
809
 
@@ -843,7 +848,7 @@ brick: scroll ─► movers ─► take ─► clear ─► providers (mesh, v
843
848
  │ brick map, SDF atlas, object cells, occupancy
844
849
  probe: retire ─► want (depth) ─► admit ─► schedule ─┐
845
850
  ▼
846
- trace (+ sun soft shadow) ─► shade ─► accumulate ─► publish
851
+ trace (+ sun shadow rays) ─► shade ─► light (+ local shadow ray) ─► accumulate ─► publish
847
852
  (rays = trace list × rays_per_probe ≤ ray_budget)
848
853
  │ indirection, states, records (SH2),
849
854
  │ radiance + depth atlases
@@ -877,8 +882,8 @@ How the bit travels:
877
882
  - A row outside the SDF world is drawn by the camera and invisible to every GI ray. It occludes
878
883
  nothing, emits nothing into GI and bounces nothing. That is the point of the bit: performance.
879
884
 
880
- **A geometry is cached** when at least one row in the SDF world draws it and it is not virtual.
881
- - Virtual geometry has its own provider (§7.8).
885
+ **A geometry is cached** when at least one row in the SDF world draws it.
886
+ - A virtual geometry's cache is baked from a proxy cut from it, and adopted into it (§7.8, S8).
882
887
  - DynamicMesh has no row, so it is absent by construction.
883
888
 
884
889
  **What is cached, and keyed by what**:
@@ -1129,11 +1134,38 @@ the row's pair.
1129
1134
  is empty.
1130
1135
  3. At the opaque hit, `sample_material_data` runs with the material's resident row and an identity
1131
1136
  instance transform (the card is in local space).
1132
- - Its random state is seeded from (pair, texel), which fixes its stochastic bilinear tap and its
1133
- dither.
1137
+ - Since 2026-10-30 both reads take the material's textures at the texel's footprint, trilinearly
1138
+ (`mesh_card_texture_level`; the capture is `INDIRECT_TEXTURE_FILTER` `Exact`): the card's texel
1139
+ steps carried along δ onto the triangle and through its uv per position, the level
1140
+ log2(texels × 64), the last level for a triangle edge-on.
1141
+ - Its random state is seeded from (pair, texel), which fixes its alpha draws and its dither.
1134
1142
  - A recapture of an unchanged material therefore writes the same bytes.
1135
1143
  4. The texel stores √(albedo × (1 − metalness)), depth, and emissive / s_e.
1136
1144
 
1145
+ **Capture by raster** (`MeshCardCaptureMode.Raster`, the default since 2026-10-30), UE's way: the
1146
+ cards are drawn rather than traced, through the material's own template.
1147
+ - The trace above took one level-0 tap of the material's 64² resident copy a texel and kept it, a
1148
+ point sample of a texture a card texel covers many texels of. On Sponza's outer wall the brick
1149
+ texture's dark band repeats every 12.8 card texels; it landed on a row of sample points once and
1150
+ between rows at its other repeats, a hard seam across the wall. It now reads the copy's mips at
1151
+ the footprint (step 3), and over that wall its cards differ from the raster's by 0.77 codes on
1152
+ average, against 10.5 before.
1153
+ - A pair is taken whole. Its cards are drawn by one indirect draw, an instance a meshlet of each card
1154
+ the layout has (a compute pass writes the count), each card into a cell of the pair's region of a
1155
+ scratch: orthographic along δ, the lattice point at the texel's centre, the slab the depth range,
1156
+ so the hardware clips to it as the trace's t ≤ depth does. Culling is off: a card sees either side.
1157
+ - The fragment stage is the template's `material_surface` and `material_opacity`, with the material's
1158
+ one bind group, the G-buffer pass's: its textures, their mips and hardware filtering, at the card
1159
+ texel's footprint, unbiased as UE's card capture leaves `MaterialTextureMipBias` at 0. Coverage is
1160
+ drawn as the trace draws it, from a seed fixed per (pair, card, texel); the depth test keeps the
1161
+ nearest opaque draw. A template without a surface function is traced.
1162
+ - A scatter then moves each page's texels from the pair's cells into its atlas tiles, in the trace's
1163
+ encoding.
1164
+ - Measured on Sponza: the cards' colours agree with the trace's to 3 codes on average over the wall,
1165
+ floor, ceiling and banners, without the seam or the trace's speckle. Recapturing all 154,138 pages
1166
+ at the largest budget, interleaved in one process, cost the trace 9.1–12.7 ms and the raster 0.6 ms
1167
+ on the RTX 4090, and 335 ms against 33 ms on the AMD iGPU.
1168
+
1137
1169
  **Recapture.**
1138
1170
  - `GPUResidentMaterialContext` signals when it repacks a material whose version moved (§4.1). The GI
1139
1171
  keeps the reverse map from a material to its pairs and queues each for recapture: O(changes), E5.
@@ -1435,12 +1467,23 @@ This is the plugin system.
1435
1467
  cannot name a row writes 0, and its hits take the material of no row: zero albedo, counted.
1436
1468
  6. **Invalidation.** When a provider's data for a region changes (streaming, an edit), it appends that
1437
1469
  world box to the invalidation list, and next frame's movers queue the cells.
1470
+ 7. **Retry** (S9). A provider that could not finish a cell this frame sets the cell's retry word in the
1471
+ staging (`BRICK_STAGING_RETRY_WORD`). Free decides the cell as any other, keeps its key pending and
1472
+ puts it back on the ring, so the cell is built again from the start. What was merged meanwhile is
1473
+ published for those frames, and can over-estimate where the unfinished work would have read lower;
1474
+ the frame that finishes the cell keeps contract 4.
1438
1475
 
1439
1476
  A provider sees every taken key. One with nothing in a cell writes nothing and pays only its own
1440
1477
  early-out.
1441
1478
 
1442
1479
  ### 7.7 The mesh provider
1443
1480
 
1481
+ **Superseded in part by S9 (2026-10-29).** Alex, after S8's holes: "Go with (b): batches of 64 spread over
1482
+ workgroups by count, with an atomic min/max merge." The cap of 64 instances below bound from cascade 2 on Zorah, and
1483
+ the instances it kept were the lowest rows, in load order: the ground under the greenhouse's ring lost to the kitbash
1484
+ numbered before it. The accept, the per-instance level and the texel stand; the cap, and one workgroup doing all of a
1485
+ cell, are gone. What is built now is "As built (S9)", at the end of this section.
1486
+
1444
1487
  `MeshSDFBrickProvider` runs one 64-lane workgroup per taken key.
1445
1488
 
1446
1489
  **Gather.** A TLAS cooperative query with box = cell ⊕ R_c. The leaf accept takes a row when all of
@@ -1510,11 +1553,83 @@ count (E3), and no triangle is read.
1510
1553
  - With EMPTY signed (§6.3) the building's air reads inside its open hull (geometry 102), so the
1511
1554
  courtyard is solid in the world bricks too; put to Alex.
1512
1555
 
1556
+ **As built (S9, 2026-10-29)**: every instance whose box comes within the band of a taken cell is sampled there,
1557
+ however many. Three passes, each sized by a count on the device, and a one-thread command
1558
+ (`shader_mesh_brick_command`) before each but the walks from the root:
1559
+ - **Walk** (`shader_mesh_brick_gather`), a workgroup a walk: the cooperative query's level-at-a-time expansion and
1560
+ the S3 accept, nothing dropped.
1561
+ - A level keeps 512 nodes (`MESH_BRICK_FRONTIER`); the children past them leave in task records of 64 nodes, each
1562
+ walked by the next round of the same kernel. On Zorah's TLAS (12,608 leaves, 27 levels) the widest level of a
1563
+ cell's walk is 758 nodes at cascade 2 and 2,410 at cascade 5, so every walk ends in its third round.
1564
+ - Rounds start at two and grow by one, from a read-back count, when the last round could not finish a walk, up to
1565
+ six: enough for any LBVH of 2^20 leaves.
1566
+ - Accepted rows leave in batch records of 64, and the rows left when the walk ends in one more.
1567
+ - An iteration of a level costs the workgroup a barrier and a `workgroupUniformLoad`: lane 0 hands every lane
1568
+ where the next iteration's children start and, on the level's last, how many nodes the next level holds.
1569
+ - **Batch** (`shader_mesh_brick_batch`), a workgroup a record: its rows sorted, its 64 instances in workgroup memory,
1570
+ 512 texels × 64 as S3c samples them, merged into the cell's accumulators by atomic maxima of 255 less the byte and
1571
+ of the object key.
1572
+ - **Resolve** (`shader_mesh_brick_resolve`), a workgroup a cell the walks listed: its accumulators into the staging
1573
+ by contracts 2 and 3. A walk lists a cell the first time it flags it, for a batch made or a walk left unfinished;
1574
+ most taken cells have no instance within their band and cost the walk from the root alone.
1575
+ - Contract 7: a record past its capacity, a level past the budget, or nodes past the last round's frontier leave the
1576
+ cell unfinished. It is retried, and the capacity grows to the power of two at or past the most asked for, or the
1577
+ rounds by one. Of the records a walk asks for at once, those below the capacity are kept, so a command never
1578
+ dispatches a record that went unwritten.
1579
+ - The texel compares in the staged byte, the lowest row on a tie, so the object no longer depends on the order of
1580
+ the instances; the batch sorts its rows, so a cell of one batch reads the same whatever order its walk found them.
1581
+ - **Device, Zorah** (S8's load, filled; from the aerial view, the GI released and all 196,608 cells of the windows
1582
+ built again, 60 frames; S8's provider against this one in one process, old/new/new/old), with the frontier at 1,024
1583
+ nodes as S9 landed it; the 512 it has now follows below:
1584
+
1585
+ | | iGPU, S8 → S9 | 4090, S8 → S9 |
1586
+ |---|---|---|
1587
+ | brick view's hole pixels | 10,323–10,379 → 41 | 10,071–10,078 → 36 |
1588
+ | the provider over the rebuild | 76.9–78.0 → 132.6–132.7 ms | 7.2–7.6 → 16.0–18.1 ms |
1589
+ | walk / batches / resolve | 71.4 / 56.4 / 4.7 ms | 5.5–7.6 / 8.5–9.7 / 0.5–1.1 ms |
1590
+ | a frame of it, p50 / p90 / worst | 1.15 / 2.0 / 6.4 → 1.08 / 3.6–4.2 / 17.7 ms | 0.09–0.11 / 0.17 / 0.9–1.4 → 0.20 / 0.65–0.73 / 1.3–1.4 ms |
1591
+ | at rest | 1.4–1.5 → 8.5–11.2 µs | 3.1 → 16.4 µs |
1592
+ | rebuild against rebuild, bricks that differ | 276 → 0 | 254 → 363 |
1593
+
1594
+ - The 36–41 pixels left are the dome's lattice, thinner than cascade 2's voxel: S8's open item, not the cap. The
1595
+ ground cells under the ring, S8's open item, were the cap.
1596
+ - Nothing retried, no record past a capacity, and every walk ended in the second round: at most 1,431 batches and
1597
+ 220 tasks a frame (iGPU), within the initial capacities; at most 805 cells a frame listed for the resolve.
1598
+ - Where the cost goes, from one-off arms in the same process with a part of the work deleted:
1599
+ - The batches sample every instance: 257,216 rows a rebuild, where S8's provider dropped 37,780 instances past 64
1600
+ a cell, 93,700 candidates past 128 and 43,500 frontier nodes, so sampled at most 125,736. The batch pass with
1601
+ nothing sampled costs 0.3–0.5 ms, and its sort 1.7 ms on the iGPU (within the noise on the 4090).
1602
+ - The walk's floor, a workgroup a taken cell with its memory cleared and its task read, is 25.8 ms on the iGPU and
1603
+ 0.67 ms on the 4090; S8's walk alone, 48.3 and 2.3–2.4 ms. On the 4090 this walk cut to what S8's walked (256 nodes
1604
+ a level, the rest dropped) costs 3.3 ms against its 4.4 ms: 1.1 ms is walking every node, and the 0.9–1.0 ms
1605
+ left over S8's walk is this walk's own bookkeeping, not split further.
1606
+ - Alex asked for a hand-off once a window of four iterations instead of each, the spill and the ring sized for the
1607
+ window (11.3 KiB of workgroup memory against 9.1). Measured paired against this walk, it was no faster on the 4090
1608
+ (the walk's median 4.36 against 4.29 ms, six trials each) and 27% slower on the iGPU (90.3 against 71.3 ms, four
1609
+ each); a window of two was 4% slower there. Not taken: on the iGPU the walk slows with its workgroup memory.
1610
+ - So the frontier went the other way, halved to 512 nodes (Alex, on this measurement): the walk's workgroup memory
1611
+ from 9.1 to 5.1 KiB. Paired against 1,024 (four trials each on the iGPU, six on the 4090, after a warm-up whose
1612
+ rounds grew to three, retrying 8 and 2 cells; none after): on the iGPU the walk 71.1 → 43.0 ms, the provider
1613
+ over the rebuild 132.0 → 104.1 ms, its median frame 1.08–1.27 → 0.65 ms; on the 4090 the rebuild even (17.9 ms
1614
+ by the mean). At rest the third round's command and walk add 2.0 µs on the iGPU and 4.1 µs on the 4090 (9.8 and
1615
+ 20.5 µs); batches 7,077 → 7,331, the rows of a cell split over more walks; holes alike. Against 1,024, 30 of the
1616
+ iGPU's 2,684 bricks differ, the texel's box test being order-dependent for a one-sided field, as before.
1617
+ - At rest, seven dispatches (three one-thread commands, and four passes dispatched with no workgroup) against S8's
1618
+ one.
1619
+ - Tier 2: free's retry (red on S8's free), the resolve's contracts, list and tail, the command, the walk's spill
1620
+ partition, the texel's order-free row (red on S8's chunk). Tier 3: the provider's dispatches and their commands, its
1621
+ buffers and their growth. The walk and the batches run on the device only. 25 of 25 mutants were killed.
1622
+
1513
1623
  **TODO hook (skinned).** The leaf accept, where a clone resolves to its source's header, is where a
1514
1624
  dense animated patch takes over later.
1515
1625
 
1516
1626
  ### 7.8 The virtual geometry provider
1517
1627
 
1628
+ **Superseded by S8 (2026-10-29).** Alex, after S7e: "Move vgeo to per-geometry signed SDF caches (baked once from a
1629
+ cut, composed per instance) instead of fixing the per-cell sign in place." The per-cell provider below (S7b–S7e) is
1630
+ deleted; S7a's cut refactor and S7e's measurements stand. What is built now is "As built (S8)", at the end of this
1631
+ section.
1632
+
1518
1633
  S7, its own stage. vgeo rows are mesh rows with a virtual geometry: their BLAS is empty and they have
1519
1634
  no cache. The provider, per taken key:
1520
1635
  1. **Gather**: the mesh provider's TLAS query, accepting vgeo rows in the SDF world only.
@@ -1535,6 +1650,343 @@ no cache. The provider, per taken key:
1535
1650
  Triangle work here happens per requested cell (scroll-ins and changes), never per frame. It is bounded
1536
1651
  by the cut error: at error ½·h_c, a cell's triangles number on the order of its surface in voxels.
1537
1652
 
1653
+ **What the vgeo runtime gave, read before building (2026-10-29)**. The plan and its choices are the shared
1654
+ note `s7-vgeo-provider-plan-2026-10-29.md`; Alex took them with "make sure we don't introduce more
1655
+ coupling. If you need to refactor how wants work - please do so. Keep performance in mind. [...] DRY and
1656
+ KISS".
1657
+ - vgeo rows are ordinary `meshes` rows in the scene's TLAS with an empty BLAS;
1658
+ `geometries[row.geometry].vg_row` names the geometry's root page and page lookup.
1659
+ - The cut's rule was fused into the camera's traverse kernel with its drawing, claims and subgroup
1660
+ scheduling, so no other kernel could call it.
1661
+ - Wants reach the residency only through the frame's feedback slots, whose reading wrote a view's fields.
1662
+ - No event says a page arrived: counters and lookups only.
1663
+ - A cut of its own a lane a pair would be a per-thread loop of hundreds of steps. The camera cut's wavefront
1664
+ is the engine's answer to that (one item a lane a step, a uniform step bound, spills between dispatches),
1665
+ so the box cut runs it rather than a walk of its own.
1666
+
1667
+ **As built (S7a, 2026-10-29)**: the vgeo runtime, refactored so that a box cut runs the camera cut's rule and
1668
+ descent, the camera's cut unchanged.
1669
+ - `chunk_vgeo_cut_rule`: the cut's items and expansions, and the rule as two verdicts, `vg_cut_node` and
1670
+ `vg_cut_cluster` (`VGEO_CUT_VERDICT`). The importing kernel defines the two culls,
1671
+ `vg_cut_sphere_outside` and `vg_cut_box_outside`.
1672
+ - `chunk_vgeo_cut_descent`: what the descent does with a verdict (counts, wants, claims, the walk up) and the
1673
+ wavefront's scheduling, with one more hook, `vg_cut_take`, for what taking a cluster means. The camera's
1674
+ traverse is that chunk with its frustum culls and `vg_draw`.
1675
+ - `graph_vgeo_cut_descent`: the seam's seed, wavefront and tail, for any cut's instance records and traverse
1676
+ kernel. The seam keeps its collect.
1677
+ - `vg_want` and `vg_stat` are chunks of their own, so a kernel that only counts binds no claim buffer.
1678
+ - A feedback slot is any `VGeoFeedbackConsumer`'s: the loop hands each consumer its slot's counters through
1679
+ `read_feedback`, and whether it shared the slot. `VGeoViewCut` implements it. The runtime fills a slot's
1680
+ own params (`fill_slot_params`) for any cut.
1681
+ - Measured on Zorah (the textured UE rebuild: 1,012 containers, 12,597 instances) from its manifest's camera
1682
+ at 1280×720. Blocks of frames alternated the refactored traverse with the one before S7a, for every cut of
1683
+ the frame, once after the rule moved and again after the descent did.
1684
+ - The frame view's counters are the same between the arms on both GPUs: 32,618 clusters drawn, 163,353
1685
+ items, 10,822 pages, 144,079 clusters tested once settled. The 4090's second run held 118 clamped
1686
+ clusters in both arms.
1687
+ - The traverse's 80 dispatches a frame cost the same: p50 0.96 and 1.00 of before on the RTX 4090
1688
+ (1.93 against 1.92 ms the second time), 0.99 and 0.98 on the AMD iGPU (4.19 against 4.27 ms).
1689
+ - Tier 2: the cut agreement spec runs the traverse on the emulator unchanged. `VGeoFeedbackLoop.spec.js`
1690
+ adds a consumer that is not a view (handed its counters, told when it shared its slot) and a slot's params
1691
+ against the feedback's wire layout, which no spec held before. `graph_vgeo_expand.spec.js` counts the
1692
+ wavefronts a cut records and encodes a cut's graph on the software device, which no spec did either.
1693
+ 20 of 20 mutants were killed.
1694
+
1695
+ **As built (S7b, 2026-10-29)**: the gather and the box cut, which leave each taken cell a list of the clusters in
1696
+ its cut. Not registered yet: S7c's distance pass is the lists' reader.
1697
+ - `VGeoBrickProvider`, a `BrickProvider` with a `VGeoViewCut` of its own. In a frame the scene's vgeo runtime has
1698
+ open, it records the gather, the claim widths scanned by `graph_prefix_scan_csdldf`, and `graph_vgeo_cut_descent`
1699
+ with `shader_vgeo_brick_traverse`, then publishes the cut's feedback: its wants reach the residency through its
1700
+ own slot, ranked with the camera's, and its counters come back to its cut. With no runtime, or none open, it
1701
+ records nothing.
1702
+ - `shader_vgeo_brick_gather`: a 64-lane workgroup a taken cell, dispatched by take, running the mesh provider's
1703
+ cooperative TLAS query over the cell ⊕ R.
1704
+ - `vgeo_brick_accept` takes a row in the SDF world whose geometry is virtual and whose box is at least ½ voxel
1705
+ across.
1706
+ - `vgeo_brick_pair` writes a cut's instance record for each. k is σ_max/(½·h_c), the distance weight is 0 and
1707
+ there is no eye, so `vg_project` is the world error over ½·h_c against τ = 1. The cell ⊕ R box, taken into the
1708
+ instance's object space as the bounds of its eight corners, goes in the record's frustum words, and the
1709
+ taken cell in word 30.
1710
+ - A flattened instance makes no pair. Pairs past `budget × 8` records are dropped and counted, as is what the
1711
+ query drops.
1712
+ - `shader_vgeo_brick_traverse`: `chunk_vgeo_cut_descent`, culled by the box's six faces, a sphere as its cube.
1713
+ The take puts a cluster onto its cell's list as its pair and its meshlet row, up to 64. A list counts past what
1714
+ it holds, and the clusters past it are counted dropped (`MESHLETS_DROPPED`).
1715
+ - Shared rather than copied: `chunk_vgeo_cut_instance` (what any collect writes of an instance record, now under
1716
+ the camera's collect too) and `chunk_brick_provider` (a taken cell's key, window, voxel and band, and the
1717
+ resolution test, now under the mesh provider too).
1718
+ - Tier 2, on the emulator:
1719
+ - `vgeo_brick_traverse.spec.js` runs the seed, prepare, the box traverse and the remainder over a built torus
1720
+ of 26 pages. Each list is the CPU oracle's cut at the pair's error with the pair's box as its planes
1721
+ (`VGeoCutView.plane_count`, new: a camera tests five).
1722
+ - Six fine cells around the ring and over and under the tube take 8 to 10 clusters each, and the whole ring
1723
+ at a coarse error 7 over 3 pages.
1724
+ - With a page withheld, the cell takes the oracle's stand-in, counts the clamp, and wants the page through
1725
+ the cut's slot.
1726
+ - Ten instances in one cell take 70 clusters: the list holds 64, counts 70, and 6 are counted dropped.
1727
+ - `vgeo_brick_pair.spec.js` covers the accept and the record under a skew turn and a non-uniform scale:
1728
+ k, w, the eye in a buffer of stale NaNs, the box, the cell, and the cut's own fields. It also covers the
1729
+ drops.
1730
+ - Tier 3: `VGeoBrickProvider.spec.js` on the software device.
1731
+ - Nothing is recorded without an open runtime.
1732
+ - The gather runs through take, then the cut's wavefronts, between clear and free.
1733
+ - τ is 1 with the camera's at 3; the cut has a slot of its own; its tail publishes the frame's feedback;
1734
+ destroy gives back what it made.
1735
+ - 37 of 38 mutants were killed. The survivor never culls by a sphere, which changes nothing on the torus: its
1736
+ pages hold a node each. A page's sphere lies outside the box only if the cluster refining into it does, so
1737
+ the sphere decides at a page's inner nodes alone.
1738
+ - The gather is a barrier kernel the emulator cannot run, as the mesh provider's is. Its glue is checked on the
1739
+ device in S7c, once the lists have a reader.
1740
+
1741
+ **As built (S7c, 2026-10-29)**: the distances, and the provider registered: the GI runs it after the mesh
1742
+ provider, before the plugins.
1743
+ - `shader_vgeo_brick_distance` runs a 256-lane workgroup for each taken cell that the box cut put a cluster in.
1744
+ The traverse lists a cell at its first take (`vgeo_brick_filled`), and `graph_build_indirect_dispatch_command`
1745
+ turns the count into the dispatch. The plan's two passes are kept as the cut and this one: the triangles are
1746
+ culled at the start of this pass, so the frame carries each cell's 516-byte cluster list rather than a triangle
1747
+ list (about 42 MB at 4,096 cells).
1748
+ - Triangles: a lane a triangle, two of the list's clusters at a time. A triangle whose box misses its pair's
1749
+ (the cell ⊕ R, in the instance's space) lies past the band from every texel and is skipped. The rest go into
1750
+ workgroup memory, up to 256 (§7.8's choice), past it dropped and counted. They are held in the cell's own
1751
+ space, the translation taken less the cell's origin first, and wound out of the surface their instance
1752
+ draws: `triangle_winding_is_reversed` swaps b and c.
1753
+ - Texels, two a lane, `vgeo_brick_texel`: the nearest triangle within the band, signed by its side. Of
1754
+ triangles as near, the one whose normal lies most along the texel's offset decides; "as near" is a tolerance
1755
+ derived from the closest points' rounding in the cell's space, 2.0·10⁻³ of the squared distance. A material
1756
+ drawn on both sides signs positive, as UE's two-sided fields do. Past the band a texel is unsigned, so
1757
+ positive.
1758
+ - The merge: the bytes meet in workgroup memory. A lane a staged word takes each byte's least (contracts 1 and
1759
+ 2). A lane an object cell writes the vgeo row where its nearest texel is nearer than the staged minimum from
1760
+ before the merge (contract 3).
1761
+ - Shared rather than copied:
1762
+ - `chunk_brick_object`: the encoding and the object bids, out of the mesh provider, renamed `brick_*`;
1763
+ - `chunk_read_meshlet_triangle_positions`, which `read_meshlet_triangle_bounds` is now built on;
1764
+ - `chunk_brick_provider_cell`, split out of `chunk_brick_provider`;
1765
+ - `chunk_vgeo_brick_box`, the traverse's too.
1766
+
1767
+ A cell's list carries its key. `graph_vgeo_cut_descent` returns the records at the seed's version, their claim
1768
+ bases in them.
1769
+ - Tier 2, `vgeo_brick_distance.spec.js`:
1770
+ - the cull, past each of the six faces;
1771
+ - the placement: a turn, a scale, an offset origin, reversed, the reach, sided;
1772
+ - the texel over an octahedron against a brute force of the closest points: the distance to 5 places, the
1773
+ sign, the row's triangle as near as the nearest, and none past the band;
1774
+ - two faces meeting at 15°, whose planes disagree in front of their edge: the face the texel stands over
1775
+ decides, in either order;
1776
+ - a two-sided surface, positive on both sides.
1777
+
1778
+ `vgeo_brick_traverse.spec.js` holds the filled list: each cell that took a cluster, once. Tier 3: the
1779
+ distance pass between the cut and free; the GI's order with the scene's vgeo open. 30 of 30 mutants were
1780
+ killed.
1781
+ - Device, on both GPUs: two tori built in the page (128×48, 40 pages), one of them mirrored. The mode was entered
1782
+ again until no fill clamped, which was the second fill (S7d is the re-queue), and every brick texel was held to
1783
+ the analytic distance.
1784
+ - The mean error is 1.9–2.4 bytes and p99 is at most 8 bytes (a quarter voxel), against the half-voxel bound
1785
+ of 17. No cell the surface crosses is without a brick.
1786
+ - 65 texels at cascade 0 (of 180,751 within the band) have the wrong sign. The vgeo builder's simplification
1787
+ flipped 2 of the torus's triangles, at its inner top (cos −0.99 and −0.66 against the analytic normal), and
1788
+ the nearest triangle's side follows them.
1789
+ - The 256-triangle cap: 538 triangles were dropped, in cascade-1 cells whose 3.75 m box takes in a quarter of a
1790
+ torus. 159 texels there are over the bound on the 4090, 178 on the iGPU, since atomic order picks the
1791
+ triangles dropped. On a coarser torus (64×24) 275 are dropped and cascade 1 is clean, while three 15 m
1792
+ cascade-3 cells over both tori drop instead.
1793
+ - Inside the tube past the band (1.05 m against cascade 0's 0.5 m band), 24,314 texels are unsigned, so
1794
+ positive, as §7.8 writes them.
1795
+ - A fill frame, p50 / p90 / max:
1796
+
1797
+ | | gather | cut | distances |
1798
+ |---|---|---|---|
1799
+ | 4090 | 10 / 12 / 12 µs | 39 / 138 / 881 µs | 58 / 91 / 399 µs |
1800
+ | iGPU | 605 / 619 / 728 µs | 20 / 63 / 248 µs | 1.3 µs / 1.04 / 6.49 ms |
1801
+
1802
+ The gather is a workgroup a taken cell, whose launch is the iGPU's 605 µs; the mesh provider's 1.07 ms here
1803
+ is the same floor. With no vgeo in any cell, the cut's empty wavefronts cost 22 µs (4090) and 20 µs (iGPU).
1804
+ - Dispatching the distances by the taken cells instead cost 1.73 ms on the iGPU with no vgeo in any cell, all
1805
+ of it launch. By the filled count it costs 1.3 µs.
1806
+ - Sponza's census is the same on the tree before S7b and after, so both refactors left the mesh provider as it
1807
+ was: 7,171 bricks in every census (progressive, again, fresh), every cascade's counts, and the instances
1808
+ dropped past 64 (402, 460, 58).
1809
+
1810
+ **As built (S7d, 2026-10-29)**: a cell built with a stand-in waits, and is built again once what it waited on
1811
+ arrives (contract 6).
1812
+ - The box traverse records a cell's first stand-in in its list header (`VGEO_BRICK_CELL_OFFSET`): the page it
1813
+ stood in for, as its lookup word, and the group in that page. The first claimant writes them, decided by an add
1814
+ on the header's count of stand-ins, so no compare-and-swap is needed. A clamp (the page not resident) and a held
1815
+ group (resident, not selectable) wait alike, until the page is resident and the group selectable.
1816
+ - The distance pass puts each cell with a stand-in on a waiting list. The list holds budget × 4 cells; those past
1817
+ it are dropped and counted.
1818
+ - `shader_vgeo_brick_wait` heads the provider's passes each frame, a thread a cell the last frame left waiting.
1819
+ A cell whose page is resident and whose group is selectable is queued with `brick_clipmap_enqueue`, so exactly
1820
+ the cells that stood something in are built again, once what they waited on is there. The rest wait on, onto
1821
+ the list this frame's distances add to; the two lists trade places each frame.
1822
+ - Contract 6's seam on the device: the provider context hands a provider the clipmap's queue, as `state`, `ring`
1823
+ and the new `pending` bits. A pass that queues cells writes `state` and hands its version on.
1824
+ - `vg_group_row` and `vg_group_selectable` move into `chunk_vgeo_cut_arena`, so a pass that asks whether a group
1825
+ is drawable binds only the arena.
1826
+ - Tier 2, `vgeo_brick_wait.spec.js`, over the runtime's tables with a page withheld:
1827
+ - a cell is queued, while one waits on the missing page and one on a group that page holds back;
1828
+ - installing the page queues those two and no other;
1829
+ - a key already pending is not queued twice;
1830
+ - a list whose count runs past its capacity is read only as far as it holds.
1831
+
1832
+ `vgeo_brick_traverse.spec.js` checks that a clamp records its page and group, and that nothing is recorded when
1833
+ all is resident; the torus's lookup now starts past word 0. Tier 3: the wait runs before the gather, indirect;
1834
+ the distances bind the list the wait keeps onto; the lists trade places; destroy gives them back. 16 of 17
1835
+ mutants were killed. The survivor drops the gather's reset of a cell's stand-ins, inside a barrier kernel that
1836
+ only the device runs.
1837
+ - Device, on both GPUs: the tori of S7c in one fill, never left.
1838
+ - The 4090 had 187 cells waiting at frame 10 and none at frame 20, by when all 15 pages were resident; nothing
1839
+ clamped after frame 20. On the iGPU none waited at frame 10, and nothing clamped after it.
1840
+ - The bricks are those S7c built by entering the mode again until no fill clamped. At cascade 0 the mean error
1841
+ is 1.865 bytes on the 4090 and 1.862 on the iGPU, p99 6, with the same 66 and 64 texels of the wrong sign.
1842
+ Cascades 2 to 5 have the same counts and errors; cascade 1 differs only in the texels the 256-triangle cap
1843
+ leaves over the bound, by atomic order. No cell the surface crosses is without a brick.
1844
+ - The wait costs 4.1 µs a frame at p50 on the 4090 (11 µs at worst) and 1.8 µs on the iGPU (6.2 µs).
1845
+ - The iGPU's distances reached 13.1 ms in one frame, against a p90 of 1.4 µs. Zorah (S7e) is where that is
1846
+ measured on a real scene.
1847
+
1848
+ **As built (S7e, 2026-10-29)**: Zorah lit by its own geometry, measured; and the wait keeps its cells' wants.
1849
+ - Zorah, the textured UE rebuild (1,012 containers, 12,597 instances, 332,613 pages), one grey material, the
1850
+ manifest's sun, 1280×720. Zorah has no other geometry, so all the probes see of it is the vgeo provider's bricks.
1851
+ - At ground level, in an arcade, IBL leaves the frame black (mean luma 6.3 of 255; Zorah's IBL sky is black).
1852
+ ProbeClipmap lights it from the sunlit floor: 19 to 37 over five runs, the probes 240 frames after the fill.
1853
+ - Of the probes' rays, 70% hit Zorah's bricks at the manifest camera, 76% at ground level and 48% from the air.
1854
+ - Timings, p50 of a settled frame.
1855
+ - The AMD iGPU: IBL 39.5 ms, ProbeClipmap 62.8 ms (+23.3 ms). That is the trace's 11.0 ms (8 ms on Sponza: Zorah's
1856
+ bricks are denser), the fused indirect pass's 9.3 ms (as S6c2 measured it), light 2.1 ms, and every other pass
1857
+ under 0.6 ms. The provider at rest costs ~20 µs: wait 3.5, gather 1.6, cut 11.1, distances 1.4.
1858
+ - The 4090, sharing its GPU with another process (its medians stand, its maxima do not): 6.9 → 8.3 ms. The trace is
1859
+ 1.2 ms and the fused pass 81 µs.
1860
+ - The fill on the iGPU (196,608 cells at 4,096 a frame, and the cells queued again): 62 ms a frame at p50, 69 ms
1861
+ at p99 and 132 ms at worst. The distances take 56.6 ms in their worst frame, the cut 6.6 ms and the gather 3.6 ms.
1862
+ - **§7.8's bound does not hold on Zorah: its caps bind as budgets.** Over one fill on the iGPU (the 4090 is within
1863
+ 10%):
1864
+ - 46% of the clusters the box cut took are past a cell's 64 (1.44M of 3.13M): 39% in cascade 0, rising to 79% in
1865
+ cascade 5;
1866
+ - 18–19.5M triangles are past a cell's 256: 9.8M in cascade 0, 6.4M in 1, 2.0M in 2;
1867
+ - 80–150k pairs are past the records (103k in cascade 1), and the TLAS query drops 182–224k candidates and 48k
1868
+ nodes.
1869
+
1870
+ A cell ⊕ R box in kitbashed content holds many overlapping instances and layered surfaces. The cut's error bounds
1871
+ how big a triangle is, not how many surfaces cross a box.
1872
+ - What the image measures, and what it does not come from.
1873
+ - Holes: from the air the ground plane's bricks are missing beside the greenhouse, and cells of the ground, the
1874
+ greenhouse and the tower read inside out.
1875
+ - Of the probes, 17–48% are dead and 8–16% of rays meet back faces. Sponza on the mesh caches has 1.6–4% dead and
1876
+ 2% back-face rays.
1877
+ - With the provider's cut 4× and 16× coarser, the drops fall about threefold (triangles 19.3M → 6.2M → 5.6M,
1878
+ clusters past a list 46% → 26% → 29%). The back faces, the dead probes and the holes stay. **The caps are not
1879
+ what the image suffers from.**
1880
+ - Each texel the least over its instances of that instance's own signed distance, in place of the nearest
1881
+ triangle of all of them, is no better: back faces 8–18% and dead probes 18–54%. The nearest triangle's side is
1882
+ the open question on content made of single-sided, interpenetrating pieces.
1883
+ - The two tori of S7c, as vgeo and as plain meshes: of the probes deep inside a tube, vgeo leaves 4 alive and 36
1884
+ dead, while the meshes' signed fields relocate 26 out alive and leave 14 dead. Past the band a vgeo interior is
1885
+ unsigned (§7.8), so its probes die where a mesh's move out.
1886
+ - **The wait keeps its cells' wants.** On Zorah, 826 cells (iGPU) and 888 (4090) waited for ever, every one on a
1887
+ page not resident.
1888
+ - A want lasts the reading it is in: past its 64 fetches a reading, `VGeoResidency.apply` drops what it defers.
1889
+ The camera's cut wants its pages again every frame; the box cut wants only while it cuts.
1890
+ - Now the traverse records what a cell's first stand-in's want is made of, in the cell's header
1891
+ (`VGEO_BRICK_WAIT_OFFSET`): the page, the group, the row the stand-in was found in and that page's own index, the
1892
+ child-table entry, and the error. The distances copy it onto the waiting list whole.
1893
+ - Each frame, the wait wants an absent page again through the provider's slot, while that row still holds that page
1894
+ (`vg_lookup[lookup + source_page] == source`). Once the row holds another page, the cell is queued again instead.
1895
+ The wait writes the frame's feedback ahead of the gather and hands its version on.
1896
+ - A group held back carries no want, as in the cut.
1897
+ - On Zorah no cell is left waiting on an absent page (827 → 0). 7 to 47 wait at rest, every one on a resident page
1898
+ whose group the activation rule holds back. That is the runtime's own gap, which the 4090's camera cut shows
1899
+ too: 1,539 clusters held for good, where S7a saw 118.
1900
+ - Tier 2 (`vgeo_brick_wait.spec.js`, over the runtime's tables with a page withheld): a cell waiting on an absent page
1901
+ wants it again each frame, through its source, at its error; one whose source row holds another page is queued
1902
+ again and wants nothing; a held group wants nothing; the cells kept go onto the list whole. The traverse spec holds a
1903
+ clamp's want words to what the residency needs: the source row holds the page named, whose child table names the
1904
+ page withheld at the entry recorded, and the error is above τ. Tier 3: the wait binds the frame's feedback and the
1905
+ want stamps. 17 of 18 mutants were killed. The survivor shortens the distances' copy onto the list, inside a barrier
1906
+ kernel that only the device runs.
1907
+ - Open, measured, cause not established: on both GPUs, the lit frame at the manifest camera has dark blotches on a
1908
+ lattice over a column whose bricks are clean.
1909
+
1910
+ **As built (S8, 2026-10-29)**: a virtual geometry's cache is baked from a proxy cut from it, adopted into it, and
1911
+ composed by the mesh provider; the per-cell provider is deleted. The plan and its one fork are the shared note
1912
+ `s8-vgeo-geometry-caches-plan-2026-10-29.md`; Alex took (a), a readback of the cut's triangles into the host's own
1913
+ geometry path.
1914
+ - **The caches** (`GPUGeometrySDFManager`).
1915
+ - `onRequested` is sent with a record each time its cache is queued: at its first holder, at a new voxel size, and
1916
+ when its holders' side changes. `is_requested` says a cache waits queued, held or not, as the bake bakes it until
1917
+ a trim; `holds_two_sided` says the side a bake of it takes.
1918
+ - **Sources** (`add_source`, a `MeshSDFSource`) run first in each `graph_bake`. A source makes the caches of
1919
+ geometries with no tree of their own, which wait queued as a geometry whose tree is not resident yet does.
1920
+ - `adopt(target, proxy)`: the resident cache baked for the proxy becomes the target's, nothing copied. The target's
1921
+ header takes the proxy's layout, entries and side, and the proxy's reads None, as dropped; `onBaked` is sent with
1922
+ the target. The cache has no cards, the target having no tree to capture them with, so a vgeo hit keeps the
1923
+ material's mean (§17). Held for another side than the proxy was baked for, it is queued again at the next bake.
1924
+ - A bake is recorded only for work there is: a queue of geometries no bake can admit yet, and placements dropped or
1925
+ adopted away, record none.
1926
+ - **The source** (`VGeoSDFProxies`, made with the runtime by `GPUSceneContext.enable_virtual_geometry`).
1927
+ - A job a geometry asked for, 32 at once. Each frame a job's geometry is cut whole by the camera cut's own descent:
1928
+ `shader_vgeo_sdf_collect` writes its record (`chunk_vgeo_cut_instance`), the claim widths are scanned, and
1929
+ `graph_vgeo_cut_descent` runs `shader_vgeo_sdf_traverse`, which culls nothing and puts every cluster in the cut onto
1930
+ a list under its job, counting stand-ins and the clusters past the list. It cuts under a slot of its own at τ = 1,
1931
+ k = 1/(½·v₀) in the geometry's own units, v₀ its cache's level-0 voxel. A clamp wants its page there; since a want
1932
+ lasts one reading, the job is cut again each frame until its cut stands nothing in.
1933
+ - `shader_vgeo_sdf_decode`, a workgroup a listed cluster, counts every job's vertices and triangles. For a job
1934
+ whose last cut stood nothing in, it writes the clusters out in room those counts made: positions as stored,
1935
+ triangles as indices into the job's own vertices.
1936
+ - One readback a frame: the jobs' counters, the cut's own (a subtree left undescended voids the frame for every
1937
+ job), and the meshes. A write-out whose counts disagree with its room's is cut again.
1938
+ - The proxy: one a frame on the host, `meshlet_geometry_build_from_geometry` (2.2–4.1 µs a triangle, measured),
1939
+ added to the geometry manager, its cache held with the side the geometry's holders draw. When it turns resident,
1940
+ the next update adopts it into the geometry and removes the proxy. A proxy whose mesh another's has is that one's
1941
+ record: only one adopts, and the other geometry is extracted again.
1942
+ - A job whose geometry is no longer queued is let go; a new voxel size restarts every job.
1943
+ - **The holds** (`ProbeClipmapSceneHolds`): a vgeo row holds its cache as a mesh row does. Its pair's card set waits for
1944
+ good, since an adopted cache has no card layout.
1945
+ - **Deleted**: `brick/vgeo/` (the gather, the box cut, the distances, the wait and their specs), its registration in
1946
+ the GI, and the provider context's `budget` and `pending`, which only it used.
1947
+ - **Device, Zorah** (as S7e loads it: 1,012 geometries, 12,597 instances, one grey material, the manifest's sun,
1948
+ 1280×720), entered once with the default settings.
1949
+ - The fill: every one of the 1,012 caches resident after 3,160 frames on the 4090 (31 s) and 3,200 on the AMD iGPU
1950
+ (247 s). 1,013 proxies were built, one twice (a proxy came out alike to another's), from 423,584 triangles: step
1951
+ 0's "a few hundred thousand". 8 geometries were coarsened (step 0's 8). The bake evaluated 52,911 bricks, the
1952
+ atlas grew to 65,536 slots, and the caches hold 35.3 MB. Their cuts took 34–42k clusters over ~1,180 readings and
1953
+ stood one in 6–9k times while their pages came; no wavefront ran out, nothing was dropped, and the cluster list
1954
+ never grew past 4,096.
1955
+ - The fill's pace, 0.32 geometries a frame, is mostly the bake's budget of 23 bricks a frame, at 52 bricks a
1956
+ geometry: it averaged 16.7 a frame, idle in 27% of the frames while proxies waited on their pages, their write-out
1957
+ or their build. Zorah's proxies cost less a frame than Sponza's meshes: the evaluate is 652 µs at p50 and 9.0 ms at worst on
1958
+ the 4090, 3.7 ms at p50, 17.7 ms at p90 and 48.9 ms at worst on the iGPU, inside what the budget costs on Sponza
1959
+ (§6.4: 26 ms at the median and 79 ms at worst on the iGPU). The extraction's own passes, in the 1,734 frames that
1960
+ cut: the cut 48 µs at p50 and 499 µs at worst on the 4090 (36 and 337 µs on the iGPU), the decode 4 µs at p50.
1961
+ - The host: a frame that builds a proxy is 4.4 ms at p50 against 3.0 ms without on the 4090's host, 20.7 ms at p99,
1962
+ and 190 ms once, for the largest geometry's proxy (the iGPU run's host: 4.8 / 3.2 / 20.4 / 150 ms).
1963
+ - The frame while filling, p50 / p90: 3.82 / 5.56 ms on the 4090 and 66.0 / 80.5 ms on the iGPU (158.9 ms at worst).
1964
+ At rest nothing of the extraction runs: 3.28 ms on the 4090 and 63.0 ms on the iGPU, S7e's 62.8 ms.
1965
+ - The probes and their rays, after the fill, both GPUs alike:
1966
+
1967
+ | view | dead probes, S7e → S8 | back-face rays, S7e → S8 | lit luma, S7e → S8 |
1968
+ |---|---|---|---|
1969
+ | manifest | 23% → 3.2% | 12% → 4.7–5.0% | 89 → 91 |
1970
+ | aerial | 18% → 2.0% | 9% → 4.5–5.1% | 74 → 76 |
1971
+ | ground | 49% → 6.8–7.0% | 17% → 5.5–6.6% | 21 → 79–83 |
1972
+
1973
+ Sponza on the mesh caches has 1.6–4% dead and 2% back-face rays. The arcade at ground level, which S7e lit at 19 to
1974
+ 37, is lit at 79 to 83 from the sunlit floor. From the air the bricks read as Zorah, the greenhouse's dome and the
1975
+ tower whole, where S7e's read inside out (`s8f_igpu_aerial_bricks.png` against `s7e2_aerial_bricks.png`).
1976
+ - Sponza's census is unchanged (4090): 7,171 bricks, every cascade's counts, 104 geometries baked, resident at frame
1977
+ 355.
1978
+ - Open, measured, cause not established: from the air, cells of the ground under the greenhouse's ring are still
1979
+ black (the ray passed through); the column at the manifest camera still has its dark blotches on a lattice; its
1980
+ flutes, narrower than a voxel, read as back faces in the brick view. A proxy's id is not given back
1981
+ (`GPUGeometryManager` hands ids out once), so the fill takes Zorah's ids from 1,012 to 2,024.
1982
+ - Tier 2 (`vgeo_sdf_extract.spec.js`, over a built torus in the runtime's tables and in a meshlet pool): a job's list is
1983
+ the CPU oracle's cut at its error with no cull, and its counts are the listed clusters'; written out, its mesh is
1984
+ every triangle of those clusters as the meshlet pool's own reader reads them back; a room too small takes no cluster
1985
+ past it; a missing page is a stand-in, wanted through the slot; past the list's capacity the count runs on. Tier 3:
1986
+ the caches' adopt, sources, requests and waits (`GPUGeometrySDFManager.spec.js`, a bake done on the device standing
1987
+ in its log); the proxies' host half end to end (`VGeoSDFProxies.spec.js`, the device's readbacks standing in); a vgeo
1988
+ row's hold. 43 of 43 mutants were killed.
1989
+
1538
1990
  ### 7.9 Tracing
1539
1991
 
1540
1992
  `brick_clipmap_trace(origin, direction, t_min, t_max)` is a pure chunk over seam readers, so the
@@ -1650,6 +2102,10 @@ left the estimator to the measurement. It is Quílez's two-sided estimate.
1650
2102
  - A linear map in place of `¼(1 + x)²(2 − x)` survives within the oracle's tolerance. On the CPU its
1651
2103
  mean error is 35% more, and its worst no more.
1652
2104
  - Never stopping in the umbra is equivalent: the least only falls.
2105
+ - **Replaced 2026-10-30** by a shadow ray toward a point of the sun's disk, and the chunk deleted (§8.7, as
2106
+ built): the estimate reads the field's clearance about one ray, so two occluders from either side of
2107
+ the cone read as one. Under a slit narrower than the disk it saw 0.675 of the sun where the disk is
2108
+ 0.327 open.
1653
2109
 
1654
2110
  **As built (S3f)**: `ProbeClipmapDebugView.BrickClipmap` traces each pixel's camera ray through the
1655
2111
  world's bricks (`debug/graph_probe_clipmap_debug_brick_clipmap.js`).
@@ -1718,7 +2174,7 @@ Measured on Sponza with its caches resident, at the defaults: 6 cascades of 32³
1718
2174
  | States | storage buffer, one `u32` a slot: NEW / ALIVE / DEAD and the update count. Its own array, so the pick reads 4 B and not a record (§9.1) |
1719
2175
  | Records | storage buffer, 64 B a slot: cascade and world cell (3 × 21-bit signed, two words), wanted frame, traced time (s), relocation offset (3 × f16), back-face fraction, SH2 as `METRO_L1_IRRADIANCE_STRUCT` (32 B) |
1720
2176
  | Radiance | 2-D `rgba16float` atlas; a slot's tile is `(n_r + 2)²`: the octahedral map plus a one-texel skirt. One level only (§9.3). Slots are placed by `index_to_grid2d`, as in `lpv_atlas_patch_texel_coord` |
1721
- | Depth | 2-D `rg16float` atlas, tile `(n_d + 2)²`. Moments of `min(t, √3·s_c)/s_c`: normalised by spacing, so every cascade has the same precision, and clamped at the cage diagonal, past which a hit cannot occlude anything the cage shades |
2177
+ | Depth | 2-D `rg16float` atlas, tile `(n_d + 2)²`. Moments of `min(t, (1 + 0.45)·√3·s_c)/s_c`: normalised by spacing, so every cascade has the same precision, and clamped at the farthest a cage tests a relocated probe (D9), past which a hit cannot occlude anything the cage shades |
1722
2178
  | Wanted bits | `N_p³·C_p` bits, cleared each frame |
1723
2179
  | Lists | active (double-buffered), wanted, trace (≤ U, §8.5) |
1724
2180
  | Free list | the extracted slot stack (§4.1) |
@@ -1734,6 +2190,19 @@ hardware taps.
1734
2190
  The price: a texture cannot be read and written in one dispatch without `texture-formats-tier2`, so
1735
2191
  the update is two passes (§8.8). That is LPV's own answer for its depth atlas.
1736
2192
 
2193
+ **As built (2026-10-30), the record packed**, 48 B a slot, `PROBE_RECORD_STRUCT.pack()`:
2194
+ - the cell, `vec3<i32>`;
2195
+ - one flags word: the back-face share as unorm16 in the low half; ten bits of flags above it, the screen's
2196
+ mark the first; the cascade in the top six bits;
2197
+ - the wanted and the traced frame, `u32`, of the frames the clipmap has recorded: exact where f32 seconds
2198
+ lose their step as the clock runs on, and an update happens in a frame anyway. The blend's Δt is the
2199
+ frames since the update and τ is `response_frames` (default 30, half a second at 60 fps), so the probes
2200
+ take no clock from the caller. Eviction's age buckets count frames: 0, 1, 2–3, 4–7, … Retire evicts by the bucket a probe was
2201
+ counted in, its age a frame before. With integer ages a probe one frame younger than an asked bucket
2202
+ aged into it between schedule and retire, and was taken uncounted;
2203
+ - the SH, `vec3<u32>` of half floats: Y's xy, Y's zw, CoCg, held to ±65,504. Half the Metro L1 struct's
2204
+ 32 B.
2205
+
1737
2206
  ### 8.3 Lifetime
1738
2207
 
1739
2208
  Per frame, in this order:
@@ -1741,7 +2210,9 @@ Per frame, in this order:
1741
2210
  1. **retire** (thread per active probe). A probe is freed when either holds:
1742
2211
  - its world cell is no longer in its cascade's window (`clipmap_contains` against the current
1743
2212
  corner);
1744
- - it was last wanted more than `retention_time` ago (default 1 s).
2213
+ - it was last wanted more than `retention_time` ago (default 1 s). Since 2026-10-30 instead: the last
2214
+ frame's schedule evicted it, by the pressure on the slots, least recently wanted first (as built,
2215
+ below the measured churn).
1745
2216
 
1746
2217
  Freeing it means:
1747
2218
  - its slot is pushed;
@@ -1758,10 +2229,20 @@ Per frame, in this order:
1758
2229
  - At a cascade's outer edge a probe cell projects to `H / (N_p·tan(fov/2))` pixels: 58 px at
1759
2230
  1080p and 60°, 19 px at 120°. A tile size of 8 therefore samples every cell several times a
1760
2231
  frame.
1761
- 3. **admit** (thread per wanted cell).
2232
+ 3. **want hits** (thread per ray of the last frame's updates; S5a, D31 reversed).
2233
+ - A front-face hit that bounces anything wants the cages its pick reads, as a pixel does, since the
2234
+ next bounce reads those probes (§8.7). So a probe a hit needs is admitted where nothing on screen
2235
+ wanted one, and kept while hits read it.
2236
+ - Only a probe a pixel wanted has hits that want: the chain stops after one step, as Enshrouded's
2237
+ world-cache probes do. Unstopped, every surface the windows hold would come to want a probe, the
2238
+ dense lattice D31 turned down.
2239
+ - It reads the last frame's rays before this frame's trace writes over them.
2240
+ 4. **admit** (thread per wanted cell).
1762
2241
  - An EMPTY entry pops a slot (a failure is counted), writes a NEW state, a record and the
1763
2242
  indirection entry, and appends to the active list.
1764
2243
  - An existing probe refreshes its wanted frame.
2244
+ - Either way the record is marked the screen's when a pixel wanted the point, and the hits'
2245
+ otherwise: what the next frame's hits read to stop the chain.
1765
2246
 
1766
2247
  Consumers weight a NEW probe at zero until its first update.
1767
2248
 
@@ -1820,6 +2301,133 @@ records the frame's lifetime as one graph pass: retire, want, admit's command, a
1820
2301
  - Tier 3, `GPUProbeClipmap.spec.js`: the first frame's fills and the passes' order, sizes and commands;
1821
2302
  the cage windows; the clock; the capacity; growth; destroy.
1822
2303
 
2304
+ **As built (S5a, 2026-10-28), the hits' wants**: `probe/shader_probe_clipmap_want_hits.js`, between want
2305
+ and admit's command, through the launch the last frame's rays had, `PROBE_DISPATCH_RAYS`.
2306
+ - **Want's cages are one chunk**, `probe/chunk_probe_clipmap_want.js`: the pick's cages, the claims and
2307
+ the appends. Want and the hits' wants both call it, so a hit wants what its pick reads.
2308
+ - **A wanted list entry's top bit says a pixel wanted the point**, `PROBE_WANTED_SCREEN`. Want runs
2309
+ first, so a point both want is listed once, as the screen's. Admit copies the bit into the record's
2310
+ `screen` word, which sits in the padding the record had: still 64 B.
2311
+ - **The row says which rays want**: shade leaves a front-face hit's albedo there and zero in every other
2312
+ ray's, so a ray that takes no bounce wants nothing.
2313
+ - **The trace list is cleared after the hits' wants**, which read the last frame's list, origins and
2314
+ rays, told that frame's U and R by the host.
2315
+ - Tier 2, `probe_clipmap_lifetime.spec.js`:
2316
+ - a pixel's points are listed as the screen's, and so are their probes;
2317
+ - a front-face hit of a screen probe wants its pick's cage where its ray ends, and admit gives the
2318
+ points NEW probes, the hits';
2319
+ - a probe only hits wanted has hits that want nothing;
2320
+ - misses, unresolved rays, back faces, black hits, and updates past the list's count or bound want
2321
+ nothing;
2322
+ - a point a pixel and a hit both want is listed once, as the screen's;
2323
+ - a probe the screen wanted and then only hits did becomes the hits', and the screen's again; a slot a
2324
+ screen probe gave back is the hits' when a hit's point takes it;
2325
+ - the cage viewed back along the ray, and the next cascade's in the skirt.
2326
+ - Tier 3, `GPUProbeClipmap.spec.js`: the pass's place, its command, and the last frame's U and R.
2327
+ - Mutants: 24 of 24 killed.
2328
+
2329
+ **Measured (2026-10-30), the churn in a still scene.** Sponza at 1280×720, the clock at 60 Hz, the camera
2330
+ and the light still, 3,600 frames settled. Then 600 frames, each read back: every slot's state and record,
2331
+ the wanted list, the trace list and the period. A slot's probe is followed by its point and its update
2332
+ count. Modelled as §8.5 has it, due agreed with the trace list on every probe past its first update, those
2333
+ a full list turned away aside: 2.7 million checks on each GPU, none apart.
2334
+ - **Hits' probes come and go, about four a frame.**
2335
+ - The RTX 4090 admitted 3.9 a frame and retired 3.8–3.9 (three runs); the AMD iGPU 4.1 and 4.0.
2336
+ - The screen admitted none.
2337
+ - Every probe retired had gone unwanted for 1.017 s, the retention time and a frame.
2338
+ - **They are the probes up to three updates old**, §8.6's 11%. Of the 4090's 501 (11.4% of the alive),
2339
+ 487 were admitted within the 600 frames; of the iGPU's 532 (12.2%), 509. The few others are hits'
2340
+ probes wanted on about one frame in twenty, whose want seldom falls on their turn.
2341
+ - **The same points come back.** Of the 896 points admitted in the 10 s, 650 were admitted again, three
2342
+ times at the median.
2343
+ - **Each is wanted by one far ray.** A twin of the hits' wants kept, for each point, its shortest wanting
2344
+ hit and the rays that wanted it.
2345
+ - A point admitted was wanted by one ray (p50 and p90), 10.8 m from its probe at the median (p10
2346
+ 5.3 m). Of those rays 60% were a uniform update's.
2347
+ - A point kept was wanted by 6 rays a frame at the median, at 3.2 m.
2348
+ - **Why far.** A radiance texel's ray stands for the texel's cone, t·√(4π)/n_r across where it lands:
2349
+ 0.22·t at n_r = 16, which past t = 4.5 m is wider than a cage of the finest cascade. That cascade holds
2350
+ 95% of the admissions. Such a cage is landed in by chance, once every few seconds (a probe both
2351
+ admitted and retired within the 600 frames lived 1.3 s at the median), and the retention frees its
2352
+ probes in between. The next landing finds no probe, so its ray takes no bounce from them.
2353
+ - **What it costs**: the admitted probes' first updates and their next three cast 2,560–2,870 of the
2354
+ frame's 52,600–53,700 rays on both GPUs, 4.8–5.5%, and are 1.3% of the updates. On the AMD iGPU the
2355
+ passes a ray (trace, shade, light, the hits' wants) take 5.4 ms at the median and those a probe 2.0 ms,
2356
+ so by those shares about 0.3 ms a frame; not timed against a build without it.
2357
+ - **What moves it**, on the 4090, in admissions a frame against 3.9:
2358
+ - a retention of 3 s: 0.7. Those up to three updates old fall to 5.9%, and 3.5% more probes are live;
2359
+ - every request held at R, 256: 3.0. Those are the rays before the variable requests (§8.6), 2.3 times
2360
+ as many, so the requests are not the cause;
2361
+ - half the ray budget, T from 6 to 10: 5.1, and 15.9% up to three updates old. A hit renews a probe
2362
+ at most once a hitter's period, while the retention is in seconds, so the churn grows with the live
2363
+ count;
2364
+ - the hits' wants and the light pass's pick at the finest cascade whose spacing covers the texel's
2365
+ footprint, a twin of both: 2.4, with 11% fewer probes live and 4% fewer rays. At twice the footprint:
2366
+ 1.9, with 30% fewer probes and 9% fewer rays. Both gather their admissions where the cascade changes
2367
+ (t of 4.4 m and 2.2 m at the median). The cascade then follows each ray's distance, not the point, so
2368
+ a point's wants split between two cascades.
2369
+ - The frame's mean luma: 72.3 as built (72.4 on the iGPU), 71.5 at 3 s, 71.9 and 71.8 by footprint.
2370
+ As built read 71.0–72.0 between runs (§8.6).
2371
+ - **Hits' probes are retraced less often than every T frames.**
2372
+ - The 3,077 hits' probes alive through the 600 frames were wanted on 67% of frames at the median, 11%
2373
+ at p10.
2374
+ - They were updated 6.8 times a second at the median and 0.8 at p10; the screen's, at least 6.6.
2375
+ - At p90, 1.2 s had passed since their last update; at p99, 6 s.
2376
+ - A hit wants a probe in the frame after its hitter's update, so its point's bit is set on fewer of
2377
+ the probe's turns than a pixel's is.
2378
+ - The passes did what this section and §8.5 said. The churn was the retention's, on points that hits land
2379
+ in once every few seconds; a fixed retention was the departure from Enshrouded, whose cache evicts by
2380
+ pressure (Alex, 2026-10-30), built below.
2381
+
2382
+ **As built (2026-10-30), eviction by pressure, not by a retention time.** Enshrouded's cache stays full and
2383
+ evicts every frame, more the fuller it is, so a probe keeps its history as long as there is room.
2384
+ - **Retire** frees a probe whose point left its window, or one the last frame's schedule asked for.
2385
+ `retention_time` is gone, from the settings and the passes: a hard break.
2386
+ - **How many**: as many slots as the free ones fall short of the frame's wanted points and 5% of the
2387
+ capacity (`PROBE_FREE_SLACK`, Alex's). The wanted points are the most a frame can admit, a camera cut's
2388
+ included, and the slack is room for a burst of them the frame it comes.
2389
+ - **Which, the least recently wanted first**: retire counts each probe it keeps and could evict into one of
2390
+ 32 binades of the seconds since it was last wanted (`probe_clipmap_age_bucket`, the float's exponent, 1 s
2391
+ beginning the middle one), `PROBE_COUNTER_AGES` on. Schedule walks them from the oldest, taking a bucket
2392
+ whole while the shortfall takes it and the rest of the shortfall from the next, and writes that to
2393
+ `probe_eviction`; retire's threads take that bucket's while a counter, `PROBE_COUNTER_EVICTED`, is under
2394
+ it. Past the youngest bucket anything was counted in, schedule asks for nothing. No sort, no pass of its
2395
+ own, nothing read back.
2396
+ - **In use, never evicted**: a probe a want waits on, NEW or wanted since its last update (§8.5), and one
2397
+ wanted the last frame (retire takes that frame's time). Where those fill the slots, an admission starves
2398
+ and the capacity grows, as before.
2399
+ - **Device, Sponza, the 4090**, the still view, 600 frames, against the base before §8.5's as-built note:
2400
+ - admitted 0.007 probes a frame against 3.9, retired none against 3.8;
2401
+ - the young probes' rays 19 a frame against 2,600; the frame's 54,800 against 53,800 (with §8.5's change
2402
+ alone 58,500, the churn's probes taking their second updates);
2403
+ - 5,400 probes live against 4,500;
2404
+ - the probes up to three updates old still 11.3% of the alive: probes kept that hits want once in a few
2405
+ seconds, updated when they are.
2406
+ - **A walk**, the courtyard's length and back over 1,200 frames, the 4090:
2407
+ - at the default capacity, 16,384, at least 6,849 slots stayed free: nothing was evicted;
2408
+ - at N = 16, 4,096 slots, 488 frames evicted, no admission starved and the capacity never grew. The
2409
+ frame's wanted points were half the slots there, so a frame short of room freed every probe out of use
2410
+ older than about two frames.
2411
+ - **The sun doubled**, frames to 50%, 90% and 95% of the change in the mean band 0 of the probes wanted:
2412
+ 37–42, 98–106 and 117–133, against the base's 38–41, 85–99 and 117–135, two runs each.
2413
+ - **The AMD iGPU**, the still view, separate processes and not paired, against the base:
2414
+ - admitted 0.007 a frame against 4.0, the young probes' rays 24 against 2,840, the frame's 54,700
2415
+ against 53,700;
2416
+ - a want waited 6 frames for its update at the median, the screen's 13 and the hits' 20 at p99;
2417
+ - the trace list held U, 512, nearly every frame against 477 a frame: the period counts every probe a
2418
+ want waits on (§8.5), so the update fills its budget, where the hits' probes it skipped left it
2419
+ short. The probe passes summed 7.91 ms at the median against 7.55, of which trace, shade and light
2420
+ 5.25 against 5.02;
2421
+ - retire 26.6 µs against 21.3, admit 13.6 against 8.3, schedule 3.5 against 0.8, the due passes 9.4
2422
+ against 15.4.
2423
+ - Tier 2, `probe_clipmap_lifetime.spec.js`: a probe nobody wants keeps its slot while there is room; past
2424
+ the room the probes wanted longest ago go first, as many as the room lacks, a bucket in part where it
2425
+ lacks part of one; one that leaves its window as its bucket is partly evicted takes none of it; a NEW
2426
+ probe and one wanted the frame before are never evicted, and an admission the room still lacks starves;
2427
+ a shortfall the evictable probes cannot cover asks for those counted, none younger. The first three
2428
+ were red against the retention time; the rest guard the protections. Mutants: 13 of 13 killed.
2429
+ - Tier 3, `GPUProbeClipmap.spec.js`: retire's times, schedule's slack, the eviction zero as made.
2430
+
1823
2431
  ### 8.4 Relocation and classification
1824
2432
 
1825
2433
  **Relocation**, at the start of every update:
@@ -1892,6 +2500,31 @@ assumes.
1892
2500
  - Mutants: 19 of 21 killed. The two survivors are equivalent on the fields the specs build: the early
1893
2501
  exit where the field is flat, and the gradient's flatness threshold on fields exactly flat.
1894
2502
 
2503
+ **As built (2026-10-30), due waits on a want, not on this frame's bit.** The hits' wants (§8.3) set a
2504
+ point's bit the frame after their probe's update, so a probe only hits keep had its bit on few of its
2505
+ turns: measured on Sponza (§8.3), the hits' probes were updated 0.8 times a second at p10 against the
2506
+ screen's 6.6 at the least, and 6 s had passed since the last update at p99, 21 s at most.
2507
+ - **Waiting**: a probe is NEW, or a pixel or a hit wanted it since its last update, its wanted time past
2508
+ its traced time. The update comes after admit in its frame, so a want in the frame of an update is
2509
+ served by it. Due lists the NEW in its first pass, and a waiting ALIVE or DEAD probe on its turn.
2510
+ - **The period counts the waiting**, `T = max(1, ⌈P_w / U⌉)` with P_w the probes waiting, not the
2511
+ points wanted this frame. Retire counts the waiting probes it keeps and admit those the frame's wants
2512
+ add, one atomic a subgroup each (`subgroup_atomic_add_u32`), into `PROBE_COUNTER_WAITING`, the
2513
+ counters' fourth word; schedule takes the period from it and empties it.
2514
+ - **Device, Sponza, the 4090**, the same still view and 600 frames, against master:
2515
+ - the hits' probes alive through them, 2,984: 4.7 updates a second at p10 and 8.2 at the median,
2516
+ against 0.8 and 6.8; 0.42 s since the last update at p99 and 1 s at most, against 7.2 s and 21 s;
2517
+ - the screen's: 8.3 a second at the median against 10.8, the period 7 frames against 5–6;
2518
+ - the frame's rays 58,500 against 53,800, the young probes' 5,300 of them against 2,600: the churn's
2519
+ probes now take their next updates when a hit wants them again (§8.3);
2520
+ - the trace list held more than U on 454 of the 600 frames, 524 a frame on average: due lists every
2521
+ NEW probe besides a period's share of the waiting, so its count sits at U, and the last ones past it
2522
+ wait their next turn.
2523
+ - Tier 2: `probe_clipmap_schedule.spec.js` (the period over the waiting; a probe wanted between its turns
2524
+ due on its turn; one whose wants its update served, the same frame's included, not due; a NEW one at
2525
+ once), `probe_clipmap_lifetime.spec.js` (retire and admit counting each waiting probe once, through the
2526
+ period). Each was red against the shaders before. Mutants: 8 of 8 killed.
2527
+
1895
2528
  ### 8.6 Rays
1896
2529
 
1897
2530
  **trace**: one thread per ray, `trace_count × rays_per_probe` rays.
@@ -1911,13 +2544,28 @@ assumes.
1911
2544
  visibilities}.
1912
2545
 
1913
2546
  **As built (S4c, 2026-10-28)**, `probe/shader_probe_clipmap_trace.js`:
1914
- - **The ray record** is 28 B (`PROBE_RAY_STRUCT`) since S4d: `t`; the direction and the hit's
1915
- normal,
2547
+ - **The ray record** is 24 B (`PROBE_RAY_STRUCT`; 28 B with the light in three half floats until
2548
+ 2026-10-30, now one RGB9E5 word): `t`; the direction and the hit's normal,
1916
2549
  each octahedral in two unorm16; the `meshes` row of the hit's surface, from `brick_clipmap_object`;
1917
- the flags (kind, back face, the hit's cascade); and the light, three half floats. Trace writes the
1918
- direct irradiance there and shade writes the radiance over it. The hit's position is the probe's
1919
- origin plus `t` along the direction, which shading recomputes. (S4c's record held the two suns'
2550
+ the flags (kind, back face, the hit's cascade); and the light. Trace writes the
2551
+ direct irradiance there and shade writes the radiance over it. The hit's position is the ray's
2552
+ start plus `t` along the direction, which shading recomputes; the start was the probe's origin until
2553
+ the ray origins below. (S4c's record held the two suns'
1920
2554
  visibility in place of the light; S4d moved E_dir into the trace, below.)
2555
+ - **The light as RGB9E5 (2026-10-30, Alex: the half floats were a poor encoding).** The engine's
2556
+ `rgbe9995` chunks: one word, the three channels sharing an exponent. The encoder clamps to its
2557
+ largest value, 65,408, which replaced the half-float holds in shade and the light pass and still
2558
+ keeps the half-float atlases finite. A channel far below the brightest loses precision: beside
2559
+ 65,408, 2 and 3 round to 0.
2560
+ - Measured paired in one process. The five passes that read or write the record (trace, shade,
2561
+ light, accumulate, the hits' wants) ran as twins built from master's WGSL, swapped in at
2562
+ `setPipeline`, the two encodings each twice. There were 120 frames an arm, the clock at 60 Hz:
2563
+ - 4090: Sponza +14 µs a frame across the five, all in the trace (+3%; its repeats differ by 3–10
2564
+ µs); Emerald Square's avenue the same.
2565
+ - iGPU: Sponza −124 µs (−2%; shade and light −4% each); Emerald the same.
2566
+ - The image, settled 1,200 frames, master and the tree in separate processes: on Sponza they differ
2567
+ by 1.6–1.7 codes a channel on average, and two runs of the tree by 1.8. The probes' noise is not
2568
+ the same from process to process. On Emerald's avenue they differ by 0.01.
1921
2569
  - **The texel mapping** is `chunk_probe_clipmap_ray_texel`, both ways (a ray's texel, a texel's rays),
1922
2570
  so accumulation finds each texel's rays without a scatter. The bit-reversed subset repeats every
1923
2571
  n_r²/R updates, which keeps its product in range.
@@ -1935,7 +2583,7 @@ assumes.
1935
2583
  zero there. The light keeps its place among the shadowed, so which lights are shadowed does not turn
1936
2584
  on the surface's orientation.
1937
2585
  - **E_dir is the trace's** since S4d: Σ colour·max(0, n·l)·visibility over every directional light,
1938
- the first two that cast shadows by their soft shadow, at a front-face hit. The trace reads the lights
2586
+ the first two that cast shadows by their soft shadow (a shadow ray since 2026-10-30), at a front-face hit. The trace reads the lights
1939
2587
  already, and shade would have needed the light table as an eleventh storage buffer (below).
1940
2588
  - **On the device**, Sponza at 1280×720 with the caches baked, 131,072 rays a frame (U = 512 × R = 256):
1941
2589
  96% hit, 3.4% miss, 0.2% unresolved, 2.5% back faces; the mean hit at 2.6 m; 13% of the sun seen at
@@ -1965,6 +2613,236 @@ assumes.
1965
2613
  starts at the relocated probe; the rays of a probe past U are not traced.
1966
2614
  - Mutants: 18 of 18 killed.
1967
2615
 
2616
+ **As built (ray guiding, 2026-10-29): Enshrouded's importance-sampled rays** (D11 reversed in its rays'
2617
+ half). Alex found the noise of one jittered ray a texel visible and asked for Enshrouded's scheme,
2618
+ rotating corner and all.
2619
+ - **Which updates are guided**: every `PROBE_UNIFORM_PERIOD`-th update (5, the talk's) is uniform, as
2620
+ above. So are a probe's first n_r²/R updates, one when R ≥ n_r², which together reach every texel:
2621
+ Enshrouded gives a new probe its most rays for the same reason, and the guide would weigh a texel no
2622
+ ray has reached as dark. Every other update is guided, except a DEAD probe's, which are all uniform:
2623
+ readers weigh it at nothing, and only uniform rays reclassify it. Guided, its rare updates kept it dead
2624
+ for 40 periods, and Sponza's dead probes rose by a quarter.
2625
+ - **The guide** (`probe/shader_probe_clipmap_guide.js`), three passes a thread a row of each listed
2626
+ probe's radiance map before the trace, per-invocation as projection is:
2627
+ - rows: the luminance of the row's history texels;
2628
+ - weights: each texel's share of the probe's luminance in fixed point (`PROBE_GUIDE_SCALE`), held to
2629
+ `(PROBE_TEXEL_RAYS − 2 − R/n_r²)/R`;
2630
+ - starts: the first ray of each walk position, `⌈R·Σ_{q<p}(w_q + e)/W − φ⌉`, with `e` the even share
2631
+ of what the weights leave of the scale, and `φ` a hash of slot and update count. The walk is the
2632
+ map's rows from the corner `updates mod 4` names.
2633
+ - Integer weights make the walk's sums exact, so trace and accumulation read the same rays for each
2634
+ texel. The hold leaves every texel under 64 rays, the gather budget, and what it takes off is spread
2635
+ over every texel. A dark history spreads its rays evenly, which classification can trust.
2636
+ - Kept: `probe_clipmap_guide_words` = 2·n_r² + 2·n_r + 2 words a listed probe (1.1 MiB at U = 512).
2637
+ - **The trace** bisects the first rays for its walk position, at most log2(n_r²) steps. A texel's later
2638
+ rays rotate its STBN jitter by R2 (`probe_clipmap_guided_ray_jitter`). The light pass, shade and the
2639
+ hits' wants are unchanged: any per-ray noise serves the light pass's pick.
2640
+ - **What it does to the rays**: on Sponza the back faces fall from 3–4% of the rays to 2.5–3%. The
2641
+ hits shift toward lit surfaces, whose sun shadow rays are the trace's cost.
2642
+ - **Measured, Sponza 1280×720.** Every comparison is the tree at period 1 (every update uniform, the
2643
+ tree without guiding) against period 5, in separate runs on each GPU; values are given as AMD iGPU /
2644
+ RTX 4090.
2645
+ - **Screen, S4g's stability method.** TAA and AO were off; SSR on, so that the deferred diffuse and its
2646
+ twins draw. The runs took 240 frames after the bake and 240 to settle, with the probes' clock fixed
2647
+ at 60 Hz. Each number is the mean relative luma change of a pixel against the previous frame at the
2648
+ same surface point, and **none moves**:
2649
+
2650
+ | p50, period 1 → 5 | still | orbit |
2651
+ |---|---|---|
2652
+ | eight corners blended (the probes alone) | 0.55 → 0.56% / 0.54 → 0.55% | 3.83 → 4.01% / 3.95 → 4.09% |
2653
+ | the shipped pick | 14.1 → 14.4% / 14.3 → 14.2% | 19.0 → 19.7% / 18.7 → 20.1% |
2654
+ | the frame, SSR off (fused pass) | 10.7 → 10.3% / 10.3 → 10.2% | |
2655
+
2656
+ Per frame, the pick's draw of two of eight is most of the diffuse's change (14% against 0.55% with
2657
+ all eight blended), as S6's estimator session also found.
2658
+ - **Probes**, the estimator session's probe-level method (`msme.js`, `probe_reads`):
2659
+
2660
+ | period 1 → 5 | AMD iGPU | RTX 4090 |
2661
+ |---|---|---|
2662
+ | texels, an update apart, p50 | 1.05 → 1.37% | 0.92–1.03 → 1.27–1.29% |
2663
+ | SH band 0, an update apart, p50 | 0.27 → 0.40% | 0.26–0.28 → 0.36–0.38% |
2664
+ | atlas texels, 24 frames apart, Σ abs(ΔL) / Σ L | 8.7 → 7.5% | 8.7 → 7.2% |
2665
+ | a doubled sun, 50% / 90% taken | | 44 / 160 → 34 / 117 frames |
2666
+ | its halving, 50% / 90% | | 55 / 183 → 44 / 140 frames |
2667
+
2668
+ Guiding makes the probes follow a light change about a quarter sooner. Their noise between one
2669
+ update and the next rises a third, while over four updates it falls by a sixth. The uniform update
2670
+ every fifth, which gives a texel one ray where the guided ones gave it several, is the likely source
2671
+ of the update-to-update jitter; this is not isolated.
2672
+ - **Before the multi-scale mean** (master at d1ca833d6, the exponential blend) the probes' noise fell
2673
+ without the rise: the atlas 24.9 → 18.0% / 24.6 → 17.9%, the eight corners' p90 1.30 → 0.92% /
2674
+ 1.24 → 0.87%. The screen did not move then either. The estimator took most of what guiding
2675
+ took off.
2676
+ - Dead probes fall by 9–17%. An alive probe is re-tested only on its uniform updates, a dead one on
2677
+ every update, so a probe near the quarter spends more of its time alive. A probe inside a solid is
2678
+ dead from its first update, which is uniform.
2679
+ - **Paired cost**: one page, periods 1 and 5 alternating in blocks of 16 frames (ABBA, the first 4 of
2680
+ each dropped), the camera still, p50 in each arm, on master with the multi-scale mean:
2681
+
2682
+ | | AMD iGPU | RTX 4090 |
2683
+ |---|---:|---:|
2684
+ | trace | 6.79 → 7.58 ms | 335 → 470 µs |
2685
+ | accumulate | 429 → 676 µs | 11 → 21 µs |
2686
+ | the guide's three passes (both arms) | 496 µs | 28 µs |
2687
+ | shade and light | +76 µs | ±1 µs |
2688
+
2689
+ In all, **+1.6 ms a frame on the iGPU and +0.17 ms on the 4090**. The trace's rise is the sun's soft
2690
+ shadow at the lit hits that guided rays favour.
2691
+ - Tier 2: `probe_clipmap_guide.spec.js` (a dark history's even spread, shares by luminance, the hold,
2692
+ the corner, U), `probe_clipmap_trace.spec.js` (each walk position's run in its texel, a texel's rays
2693
+ apart, a uniform update and a DEAD probe ignoring the guide), `probe_clipmap_accumulate.spec.js`
2694
+ (a texel's run averaged, the period by rarity on uniform updates for radiance and depth, a depth texel
2695
+ as a mean of means), `probe_clipmap_project.spec.js` (a guided update keeps the kind; a dark guide's
2696
+ update and a DEAD probe's classify).
2697
+ - Mutants: 16 of 16 killed, among them the trace, accumulation and projection as they were.
2698
+
2699
+ **The adversarial review (2026-10-29) found two defects in the first cut, both fixed and pinned:**
2700
+ - **With R < n_r², a new probe took 5× the updates to see its sphere.** Its first update reached R texels
2701
+ and cleared the rest, and the guide weighed the cleared ones dark. At n_r = 16 and R = 64, the whole
2702
+ sphere was seen at update 15, not 3.
2703
+ - Now a probe's first n_r²/R updates are uniform (above).
2704
+ - Measured on the 4090, Sponza orbiting at R = 64, two runs each: the alive probes' mean irradiance
2705
+ (E/π) was 0.0137 every update uniform, 0.0079 before the fix, and 0.0121 after it.
2706
+ - The 12% that remains is not traced. The likely source is the guide itself at R < n_r²: a texel
2707
+ whose sample read dark takes its next ray from the uniform updates, which reach it once in
2708
+ 5·n_r²/R updates. At the default R = n_r², still, the guided irradiance matches the uniform within
2709
+ 0.5%.
2710
+ - **A guided depth texel took only the radiance texels that took rays**, so where it mixes lit and dark
2711
+ directions (a back face, a shadowed wall) its depth leaned to the lit ones'.
2712
+ - Now a guided update takes a depth texel only when every radiance texel inside it took rays. Its
2713
+ period counts the least reached of them.
2714
+ - Measured on Sponza at the defaults, still: the median depth moment was 1.590 every update uniform,
2715
+ 1.599 before the fix, and 1.584 after it. The means agree within 0.3%, so on Sponza the bias was
2716
+ small.
2717
+
2718
+ **As built (ray origins, 2026-10-29): each ray starts at a point of its own**, uniform in a ball of
2719
+ `PROBE_RAY_ORIGIN_JITTER` = 1/16 of the spacing around the probe, as Brick4's bake jitters its probes
2720
+ (Alex's request).
2721
+ - The point is `chunk_probe_clipmap_ray_origin`'s, a hash of the probe's slot and the ray's packed
2722
+ direction. Shade, light and want hits put the hit back together from the same start, so the record
2723
+ carries nothing new. The trace's jitter moves the direction every update, and the start with it.
2724
+ - A sixteenth is half a brick voxel at the default sizes, well inside the relocation clearance (¼), so
2725
+ wherever relocation reached the clearance every ray starts outside the solid.
2726
+ - Measured on Sponza, the camera still, two runs a radius on the 4090 and one on the iGPU (harness:
2727
+ session 481e40f0's scratchpad, `device/oj.js`):
2728
+
2729
+ | radius, of the spacing | texel change an update apart, p50 | dead probes | depth σ, p50 |
2730
+ |---|---:|---:|---:|
2731
+ | 0 | 1.11–1.19% | 3.2% | 0.046 |
2732
+ | 1/16 | 1.00–1.01% | 3.5% | 0.051 |
2733
+ | 1/8 | 1.17–1.26% | 4.5% | 0.073 |
2734
+ | 1/4 | 1.40–1.67% | 5.9% | 0.122 |
2735
+
2736
+ - Alex compared the converged eight-corner diffuse at ¼ against 0. The jittered image is markedly
2737
+ smoother, and sharp-contrast artifacts are gone. The whole-image |Δ|/L read under its own noise floor,
2738
+ so the change is local, and that metric misses it. He chose 1/16.
2739
+ - Brick4's own radius, 5e-5 m, changes nothing here: an SDF march has no float ties for it to break.
2740
+ - Rebuilding hits from the probe instead of the ray's start is measurably wrong from ⅛ up: E/π +3.5–4%
2741
+ and 10–13% more probes, from hits wanting the wrong cells. Matching costs at most one timer step a
2742
+ pass on either GPU.
2743
+ - Cost, measured at ¼ (paired by blocks of frames in one page): the trace +3.5% on the 4090 (442 →
2744
+ 458 µs), nothing on the iGPU.
2745
+ - Tier 2: `probe_clipmap_trace.spec.js` (the starts within the ball and spread over it; a wall's `t`
2746
+ from each ray's own start), `probe_clipmap_light.spec.js` (a probe of one ray placed by its start; the
2747
+ pick's hits from it), `probe_clipmap_lifetime.spec.js` (a hit across a lattice plane from its probe
2748
+ wants the cage its start puts it in).
2749
+ - Mutants: 5 of 6 killed. The survivor is shade rebuilding its hit from the probe: its spec has no card
2750
+ resident, so no hit position reaches its output.
2751
+
2752
+ **As built (a probe's own count of rays, 2026-10-30): the frame's rays are laid out by listed probe**, the
2753
+ first stage of `PROBE_RAYS_VARIABLE_AND_SORTED_SCOPE_2026_10_30.md`. Every probe still casts R; what
2754
+ changes is that nothing assumes it.
2755
+ - **The layout.** After the trace command, the ray counts pass writes each listed probe's count of rays
2756
+ (`probe_ray_counts`), and `graph_prefix_scan_csdldf`'s shaders scan them into `probe_ray_sums`. The k-th
2757
+ probe's rays are `[sums[k − 1], sums[k])`, and the last sum is the frame's count, which a one-thread pass
2758
+ turns into the rays' command.
2759
+ - **Each pass finds its probe.** Trace bisects the sums (`lower_bound_branchless`, as the mesh expansion
2760
+ does) and writes the probe's place in the list into the ray's flags, above the cascade, which is now
2761
+ three bits. Shade and the hits' wants read it there. The light pass, accumulation and the guide also
2762
+ read the probe's own count from the sums.
2763
+ - **Past the last ray.** The threads of the launch's last workgroup past the frame's last ray write a
2764
+ record of no ray (kind 3, `PROBE_RAY_NONE`). Every pass after trace stops there, so none needs the
2765
+ frame's count. The rays buffer is rounded up to whole workgroups for them.
2766
+ - **R still means the uniform update.** A probe's first n_r²/R updates are uniform, and the rate's
2767
+ uniform term (§8.8) keeps the settings' R. A guided update's walk, its gather and its expected rays
2768
+ take the probe's own count.
2769
+ - **Storage buffers:** shade stays at 9, the light pass and accumulation go to 9, and trace goes to 8.
2770
+ - **Device, Sponza, 4090,** clock at 60 Hz, 900 frames settled, two runs of each build alternating:
2771
+ master and the layout agree within master's own run-to-run spread (frame luma 71.8/71.8 against
2772
+ 71.0/72.0, alive probes 4,393–4,463 against 4,400–4,403, SH E/π mean 0.0131–0.0134 against 0.0133).
2773
+ The passes' times are unchanged. The ray counts cost 4 µs and the rays' command 2 µs.
2774
+ - Tier 2: `probe_clipmap_trace.spec.js` (a probe casting its own count after the probes before it, each
2775
+ ray naming it; the records of no ray), `probe_clipmap_accumulate.spec.js` (two rays a texel averaged
2776
+ where R says one), `probe_clipmap_guide.spec.js` (the walk sharing out the update's own count),
2777
+ `probe_clipmap_schedule.spec.js` (the ray counts and the rays' command). Each was red against the
2778
+ shaders before it.
2779
+
2780
+ **As built (variable rays, 2026-10-30): each probe asks for its next update's rays**, the second stage of
2781
+ `PROBE_RAYS_VARIABLE_AND_SORTED_SCOPE_2026_10_30.md`: Enshrouded's requests, on the period kept (F1).
2782
+ - **Counts and requests.** The ray counts pass writes two parts for each listed probe. The first is what
2783
+ the budget does not scale: a uniform update's R (a NEW probe's among them), and a DEAD probe's fewest,
2784
+ n_r²/4 (`PROBE_RAYS_PER_TEXEL_MIN`). The second is what it does scale: a guided update's request, at
2785
+ least the fewest. Both are scanned, and the ray layout pass gives the requests what the budget leaves
2786
+ past the counts: all of it when they fit, in proportion when they do not (F2). The k-th probe's rays
2787
+ end at its counts' sum plus its requests' sum so scaled, rounded down, so the frame never passes the
2788
+ budget and needs no third scan.
2789
+ - **The request** is the slot's own record, `probe_requests` (16 bytes a slot, not packed into `screen`
2790
+ as F6 had it: the statistics below need three words). Projection writes it after every update:
2791
+ - after a probe's first update, the most, 4·n_r² (`PROBE_RAYS_PER_TEXEL_MAX`), Enshrouded's new probe;
2792
+ - when its light has moved, the most again;
2793
+ - otherwise half the last request, down to the fewest.
2794
+ - **Moved.** Accumulation leaves each blended texel's fall from its mean, luminance, in its own
2795
+ deviation and held to ±3 (`PROBE_CHANGE_SIGNIFICANCE`), in a fourth word of the maps. Projection sums
2796
+ them over the root of their count: the update's change score. Moved is the score past 3 of the probe's
2797
+ own running deviation (at least 1) from its own running mean, both kept over the estimator's variance
2798
+ window, 2τ, a running mean while young. A score past their reach counts at it, so a change in the light
2799
+ does not widen the probe's spread at once, while a lasting shift of its own is taken in within a few
2800
+ windows. A first update clears the slot's statistics, which were the last owner's.
2801
+ - **DEAD probes** are due every period at the fewest rays (F4), not every eighth at R;
2802
+ `PROBE_DEAD_PERIODS` is gone.
2803
+ - **Not built: F3**, weighing a sample by its rays in the estimator. It waits on a measurement.
2804
+ - **Device, Sponza, 4090,** clock at 60 Hz, the camera and the light still:
2805
+ - false alarms, the probes past 64 updates asking for the most, 3,600 frames settled: 4.2% when the
2806
+ change score summed the raw falls against zero, 3.2% with each texel's fall bounded, 1.1–1.3% as
2807
+ built, against the normal tail's 0.27%;
2808
+ - the probes up to three updates old are 11% of the alive ones (477 of 4,387) and 69–76% of them ask
2809
+ for the most, as new probes do. That churn is the lifetime's admissions and retirements, measured in
2810
+ §8.3, not the request's;
2811
+ - the frame's rays, the last frame's layout of each run: 48,704–65,856, against master's U·R;
2812
+ - trace p50 404–600 µs against master's 726–790 µs, separate processes and not paired;
2813
+ - the noise between reads six frames apart: texels 1.25–1.49% against master's 1.23%, band 0
2814
+ 0.38–0.46% against 0.35%;
2815
+ - the sun doubled, the alive probes' mean band 0 reaches 50%, 90% and 95% of the change in 40, 156
2816
+ and 246 frames, against master's 42, 177 and 257;
2817
+ - the post-load dark spell is unchanged: measured with the first build of the request, the probes
2818
+ were resident at 17.6 s and converged at about 32–35 s, master's timeline.
2819
+ - **Storage buffers:** the ray counts pass binds 5, the ray layout 3, projection's probe pass 6.
2820
+ - Tier 2: `probe_clipmap_project.spec.js` (the most after a first update; moved past three deviations
2821
+ and the halving to the fewest; a steady offset sips once held; a jump asks the most while it lasts; a
2822
+ lasting shift is taken in; a new probe forgets the slot's last owner), `probe_clipmap_accumulate.spec.js`
2823
+ (the fall in its own deviation, bounded), `probe_clipmap_schedule.spec.js` (the counts by kind, the
2824
+ budget's proportion, DEAD every period). Each was red against the shaders before it.
2825
+
2826
+ **As built (the probes' order, 2026-10-30): the trace list is sorted by its probes' places, and the rays
2827
+ are not sorted**, the end of `PROBE_RAYS_VARIABLE_AND_SORTED_SCOPE_2026_10_30.md` §2, where every
2828
+ measurement is.
2829
+ - **The order pass** (`shader_probe_clipmap_order.js`) runs after the update's command: a workgroup a run
2830
+ of 1,024 listed probes (`PROBE_ORDER_RUN`, through `PROBE_DISPATCH_ORDER` over the live count) sorts
2831
+ its run in place by the cascade above the Morton code of the probe's lattice point in the cascade's
2832
+ window, with the bitonic workgroup sort (`chunk_sort_bitonic_workgroup`). Every pass after it reads a
2833
+ probe's place from the sorted list, so neighbouring probes' rays run in neighbouring waves. Past one
2834
+ run, the list is ordered within each run.
2835
+ - **Why not the rays.** A wave already holds one probe's rays walking its texels in rows. Every ray sort
2836
+ measured costs more than it buys: global radix keys, local sorts by direction (8×8 or 4×4 bins, chunks
2837
+ of 2,048 to 512), within each probe, and position bits above the bin. On the iGPU at 131k rays the
2838
+ local sorts lose 4–22%, a hash-keyed control 29–57% of the trace. The 4090 is level throughout.
2839
+ - **Device:** iGPU 1–4% off the rays' stretch at 131k rays in most batches, the 4090 level; the pass
2840
+ costs 11.8 µs on the 4090. Storage buffers: 3.
2841
+ - Tier 1: the validity spec. Tier 2: `probe_clipmap_schedule.spec.js` (a workgroup a run, up to U). Tier
2842
+ 3: `GPUProbeClipmap.spec.js` (the pass after the update's command, through its command). The sort
2843
+ itself runs workgroup barriers the emulator cannot, so the device checks it: each run ordered and a
2844
+ permutation of the list.
2845
+
1968
2846
  ### 8.7 Hit shading
1969
2847
 
1970
2848
  **shade**: one thread per ray.
@@ -1984,14 +2862,17 @@ The three irradiance terms of a HIT:
1984
2862
  - **E_loc** comes from the world light clusters at the hit, generalised to this mode's brick levels
1985
2863
  so that every hit position has a cell.
1986
2864
  - At most 32 lights, `IRRADIANCE_CLIPMAP_LIGHT_BUDGET`'s value. Past that, lights are dropped.
1987
- - Each light goes through `irradiance_clipmap_get_{point,spot}_light_info_by_index`, with shadow
1988
- from `volumetrics_{point,spot}_light_shadow`.
1989
- - A light whose shadow map is cleared lights hits unshadowed, which is a stated limit.
2865
+ - Each light goes through `irradiance_clipmap_get_{point,spot}_light_info_by_index`. One of them is
2866
+ picked by WRS on its luminance there and divided by the chance of the pick; its shadow is a ray
2867
+ traced in the bricks (as built 2026-10-30, below). Until then each light was summed, its shadow from
2868
+ `volumetrics_{point,spot}_light_shadow`, and a light whose shadow map was cleared lit hits
2869
+ unshadowed.
1990
2870
  - **E_ind** is this mode's own diffuse sampling (§9.2) at the hit, with last frame's SH: the next
1991
2871
  bounce.
1992
2872
  - Its pick takes the ray's own STBN sample as noise; the probe blend averages it.
1993
2873
  - Where no probe exists, the bounce is **zero**, not the environment. A hit behind the camera in a
1994
2874
  cave must not bring daylight in.
2875
+ - The hit wants its cage (§8.3), so its probes stand there a frame or two later.
1995
2876
  - LPV's 0.97 factor is dropped: albedo below 1 already bounds the series.
1996
2877
 
1997
2878
  **Hits are diffuse only**, as Lumen's surface cache lights diffusely (D7).
@@ -2001,7 +2882,7 @@ The three irradiance terms of a HIT:
2001
2882
 
2002
2883
  **As built (S4d, 2026-10-28)**, `probe/shader_probe_clipmap_shade.js`, a thread a ray after trace
2003
2884
  through the same command:
2004
- - **Its radiance goes over the ray's light**, three half floats: no buffer of its own. With one, shade
2885
+ - **Its radiance goes over the ray's light** (RGB9E5 since 2026-10-30): no buffer of its own. With one, shade
2005
2886
  would bind eleven storage buffers, past the ten a stage has (§12): the rays, the origins, the trace
2006
2887
  list, the scene database, the texture means, the SDF headers and indirection, the card sets, the card
2007
2888
  rows, and the radiance. In place it binds nine.
@@ -2041,16 +2922,158 @@ through the same command:
2041
2922
  cannot see on the texture build's own encoder; the device check reads the means back against the
2042
2923
  atlas.
2043
2924
 
2925
+ **As built (S5a, 2026-10-28)**, E_ind: `probe/shader_probe_clipmap_light.js`, a pass of its own after
2926
+ shade, through shade's own launch.
2927
+ - **Why its own pass.** Shade binds nine of a stage's ten storage buffers, and the pick takes two more.
2928
+ The light pass binds five.
2929
+ - **The albedo.** Shade leaves a front-face hit's diffuse albedo in the ray's `row`, `pack4x8unorm`, and
2930
+ zero in every other ray's. Nothing after shade reads `row`.
2931
+ - **The pick** is made at the hit, along its normal, with the view back along the ray.
2932
+ - Its noise is the ray's own jitter, `probe_clipmap_ray_jitter`, extracted from the trace into
2933
+ `chunk_probe_clipmap.js`; the trace aims its ray with it.
2934
+ - It reads last frame's SH, since the projection writes the records later in the frame.
2935
+ - Where nothing is found the bounce is zero, as above.
2936
+ - **Tier 2.**
2937
+ - `probe_clipmap_light.spec.js` checks:
2938
+ - a hit takes its albedo by its probes' SH;
2939
+ - misses, unresolved rays, back faces and black hits keep their light, and nothing is found nowhere;
2940
+ - the pick is made at the hit, viewed back along the ray, with the ray's own jitter, across two
2941
+ probes, against the chunks themselves;
2942
+ - the half-float hold;
2943
+ - a fresh jitter each update and round.
2944
+ - Shade's stash is checked in `probe_clipmap_shade.spec.js`.
2945
+ - Mutants: 18 of 18 killed, six by tests added for them.
2946
+ - **Measured, the multi-bounce furnace**: a closed room of ρ = 0.5 glowing L_e, no light, the clock held
2947
+ at 1/60 s, 900 frames. On the RTX 4090:
2948
+ - With the rule above, E reaches **0.836 of πL_e/(1 − ρ)** at p50, p01 0.718.
2949
+ - 12% of the front-face hits find no probe. Every one lies on a wall the camera does not see, behind it
2950
+ or beside it, since probes stand only where pixels want them, and the rule makes their bounce zero.
2951
+ - A twin that falls back to the tracing probe's own SH where nothing is found reaches 0.994 (p01
2952
+ 0.988), but only because a uniform room makes every probe's irradiance the right one.
2953
+ - **Alex, 2026-10-28: the hits want their probes (D31 reversed in part), and nothing stands in.** "If we
2954
+ get no indirect bounce, it is better to lose energy than to invent energy." A hit whose cage has no
2955
+ probe wants it (§8.3), and its bounce is zero until that probe's first update, a frame or two later.
2956
+ - **Measured with the hits' wants**, same room and clock:
2957
+ - E reaches **0.994** at p50 (p01 0.988) on the RTX 4090 and on the AMD iGPU, against 0.836 without.
2958
+ No front-face hit finds nothing, against 12%. At ρ = 0.8 it reaches 0.992 after 1,800 frames.
2959
+ - The room holds 418 probes, against 228: the walls the camera does not see.
2960
+ - **On Sponza in the sun**, the camera still at 1280×720, the defaults (512 probes a frame), with the
2961
+ hits' wants and without:
2962
+
2963
+ | | nave view | toward a side wall |
2964
+ |---|---|---|
2965
+ | irradiance before AO, mean of the frame, 4090 | 0.108 vs 0.110 | **0.054 vs 0.050** |
2966
+ | the same, iGPU | 0.112 vs 0.110 | **0.055 vs 0.050** |
2967
+ | points wanted a frame | 2,600–3,200 vs 640 | 2,000–2,100 vs 160 |
2968
+ | live probes | 4,600 vs 770 | 3,500 vs 180 |
2969
+ | period T | 6–7 vs 2 frames | 4–5 vs 1 |
2970
+ | the hits' wants pass | 25 µs 4090, 0.44 ms iGPU | 17 µs, 0.42 ms |
2971
+ | trace, iGPU | 6.6 vs 4.9 ms | 7.6 vs 2.2 ms |
2972
+
2973
+ - Toward the wall, where most of the building is off screen, the hits bring 7–9% more light. In the
2974
+ nave view the frame's mean moves less than its own frame-to-frame swing.
2975
+ - **Stability is unchanged** by the longer period: S4g's measure, the change of the diffuse from frame
2976
+ to frame at reprojected points, p50 on the 4090, still 16.3% against 16.3%, orbiting 22.3% against
2977
+ 25.1%. A probe the hits keep around the camera is there when the orbit brings its surface on screen.
2978
+ - **The cost is the budget**: with the hits' probes wanted, each frame's updates reach U where the
2979
+ screen alone left them under it, so the trace costs what the ray budget allows (§8.5). On the iGPU
2980
+ that is most of the GI's growth.
2981
+
2982
+ **As built (S5b, 2026-10-28)**, E_loc, in the light pass beside E_ind:
2983
+ - **The world light clusters take any clipmap's windows.** `graph_build_world_light_clusters` binds a
2984
+ uniform that starts with a `CLIPMAP_LEVEL_ARRAY`, and is told the cascade count and a border of the
2985
+ clipmap's own cells. The irradiance clipmap passes its uniform and its tiles' skirt, as it always had
2986
+ them; the probe clipmap the bricks' levels and no border, so every hit has a cell.
2987
+ - **A cell by position**, `chunk_world_light_cluster_lookup_at`: the cell of a cascade holding a world
2988
+ point, the nearest past the cells.
2989
+ - **At a front-face hit**: the cell of the hit's brick cascade, at most `IRRADIANCE_CLIPMAP_LIGHT_BUDGET`
2990
+ point lights and as many spots, each the irradiance clipmap's at the footprint of a voxel of that
2991
+ cascade, its cosine at the normal, its map's nearest texel from a voxel off the surface. The light pass
2992
+ binds 8 storage buffers. The renderer hands the GI the frame's shadow map atlas.
2993
+ - **Measured on the device**, a closed room of albedo 0.5, nothing glowing, one point light inside: one
2994
+ frame through a twin that adds E_loc alone, every front-face hit facing the light against I·cosθ/d²
2995
+ under the range window.
2996
+ - 0.9958 at p50 on both GPUs, and 1.0039 above p99: the albedo the hits read, 127/255 from the cards
2997
+ and 128/255 from the material mean, against the 0.5 the check assumed. E_loc is exact.
2998
+ - 0.27% of those hits read shadowed, all within a few centimetres of the floor's far corner 7.6 m from
2999
+ the light, at a grazing angle, their points 0.12 m into the slab: acne a voxel's lift does not clear
3000
+ there. None do without the light's shadow.
3001
+ - **Its cost**, paired on Sponza (the light pass with E_loc and a twin without it, alternating frames),
3002
+ p50:
3003
+
3004
+ | | 4090 | iGPU |
3005
+ |---|---:|---:|
3006
+ | no local light | 28.7 against 27.6 µs | 1.70 against 1.64 ms |
3007
+ | 8 shadowed point lights along the nave | 34.8 against 27.6 µs | 2.32 against 1.58 ms |
3008
+
3009
+ - Tier 2: `probe_clipmap_light.spec.js`, against the light chunks themselves; `world_light_clusters.spec.js`
3010
+ for assign with one light, which the emulator's lane-serial run serves exactly, since lane 0 loads the
3011
+ batch before any lane reads it, and for the lookup.
3012
+ - Mutants: 27 of 29 killed. The two left: the cascade bound in assign, which the launch sized by the
3013
+ cascade count makes redundant; the cull box's far corner, which the host spec has no light to cull
3014
+ against, and which the device check shows holding its light.
3015
+
3016
+ **As built (2026-10-30), shadow rays at hits.** Alex: a shadow map is made for the main view, so where rays
3017
+ reach it is wrong in most places, and the sun's cone soft shadow is made in another domain than the rays',
3018
+ so it is bound to leak light and shadow. Both are traced now, through the bricks the hit's own ray met:
3019
+ - **The sun**, in the trace pass: for each of the first `PROBE_SHADOWED_SUNS` lights that cast shadows, one
3020
+ ray from a voxel off the hit along its normal toward a point of the light's disk, drawn as the path
3021
+ tracer draws it (`cone_sample_direction`), from `pcg3d` of the ray's slot and packed direction and the
3022
+ light's place among the shadowed. The sun is seen whole if the ray misses, not at all if it hits or
3023
+ spends its steps. The probe's blend of its rays integrates the penumbra.
3024
+ - **The local lights**, in the light pass: one of the cell's point and spot lights, picked by weighted
3025
+ reservoir sampling over their luminance at the hit, with one number stretched over the candidates as
3026
+ `sample_discrete_wrs` does. The pick lights the hit by its irradiance over the chance of its pick, w/W,
3027
+ so the probe's blend averages the picks to the cell's sum. Its shadow is one ray from a voxel off the
3028
+ surface toward a point of the light's sphere (`sphere_sample_volume`, as the path tracer), stopping short
3029
+ of it by the light's near clip or a voxel, whichever is more: the field's half-voxel expand at that end,
3030
+ as the lift is at the other. A light that casts no shadow traces none. The pick and the point are
3031
+ `pcg4d` of the ray's slot and direction, apart from the origin's draw.
3032
+ - What a ray cannot answer is no light, not all of it: a march that spends its steps shadows.
3033
+ - The light pass binds the brick textures in place of the shadow atlas, the same 8 storage buffers. The
3034
+ GI no longer reads the shadow maps, and the renderer no longer hands it their atlas.
3035
+ - `brick_clipmap_soft_shadow` is deleted with its specs; nothing else called it.
3036
+ - **Cost**, paired on Sponza at 1280×720 (the sun and the playground's 8 shadowed point lights along the
3037
+ nave; HEAD's passes and these in blocks of 4 frames, ABBA, each block's first frame dropped), p50:
3038
+
3039
+ | | 4090 | iGPU |
3040
+ |---|---:|---:|
3041
+ | trace (the sun) | 364.5 against 415.7 µs | 9.01 against 9.25 ms |
3042
+ | light (local lights) | 43.0 against 28.7 µs | 3.38 against 2.43 ms |
3043
+ | shade, unchanged, the clock's check | ×1.000 | ×0.995 |
3044
+
3045
+ A hit ray stops at its first occluder, where the cone estimate marched on into the umbra, so the sun
3046
+ costs less. One traced ray in the light pass costs more than reading every light's map. On the iGPU the
3047
+ two together are 0.72 ms a frame more.
3048
+ - Tier 2:
3049
+ - `probe_clipmap_trace.spec.js`: under a slit 0.5 m wide in a roof, a sun of 0.25 rad, 128 probes'
3050
+ rays each see the whole sun or none, and on average 0.258 of it against the 0.327 of the disk the
3051
+ slit lets through, computed on the CPU with the walls standing out half a voxel (1,024 rays read
3052
+ 0.295: the bricks round the slit's corners). The cone estimate read 0.675 there.
3053
+ - `probe_clipmap_light.spec.js`, over bricks built from boxes: a slab between a point light and the
3054
+ hit shadows it, the floor under the hit and a slab past the light do not, and a light that casts no
3055
+ shadow is lit through the slab; a slab shadows a spot; a light 3 cm under a ceiling lights 16 hits
3056
+ from under it to 4 m out; over 256 rays at one hit, each ray is one of four lights over the chance of
3057
+ its pick, each light picked within four sigma of its share, and the mean is their sum.
3058
+ - Red against HEAD's passes, its light pass given a cleared atlas, as a light never drawn has: the
3059
+ slab between the point light and the hit lit it, so did the slab over the spot, and every ray was the
3060
+ four lights' sum.
3061
+ - Mutants: 11 of 11 killed: no division by the pick's chance; the first light always; the number not
3062
+ stretched; the ray to the light's point, or short of it by its near clip only; from the surface, not
3063
+ a voxel off it; a light's no-shadow flag ignored; a hit counted as light, for the sun and the local
3064
+ light; the sun's disk centre alone; one draw for every probe.
3065
+
2044
3066
  ### 8.8 Update
2045
3067
 
2046
3068
  **accumulate**: one workgroup per traced probe.
2047
3069
  - **Blend weight**: α = 1 on the first update. After that, α = max(1/(n+1), 1 − e^(−Δt/τ)), with Δt
2048
3070
  the seconds since the probe's previous update and τ = `response_time` (default 0.5 s).
2049
3071
  - DDGI's 0.97 a frame at 60 fps is τ = 0.55 s.
3072
+ - As built (2026-10-30): Δt is the frames since the update and τ = `response_frames` (default 30), §8.2.
2050
3073
  - **Radiance**: each texel hit this update takes the mean of its rays, blended with its history.
2051
3074
  The history is read from the atlas through a sampled binding.
2052
- - **Depth**: each depth texel takes the moments of the rays landing in it (a miss counts as √3),
2053
- blended the same way.
3075
+ - **Depth**: each depth texel takes the moments of the rays landing in it (a miss counts as the
3076
+ depth limit, D9), blended the same way.
2054
3077
  - Texels without a ray keep their history; so does an unresolved ray's texel.
2055
3078
  - **The transient holds the probe's whole maps**, new texels and kept ones: n_r² × 8 B + n_d² × 4 B
2056
3079
  per traced probe, 1.2 MiB for U = 512. `publish` cannot read the atlas it writes, and it needs
@@ -2082,10 +3105,61 @@ a per-invocation function instead. Each is tier 2 whole, and no thread's loop ru
2082
3105
  between a texel's rays (n_r²/R below a ray a texel). Below a ray a texel, a texel's first ray is
2083
3106
  then taken whole whenever it comes, which the probe's count would get wrong. Depth blends by the
2084
3107
  probe's α.
3108
+ - **The radiance is a multi-scale mean since 2026-10-29** (D11; `shader/chunk/filter/chunk_multiscale_mean.js`):
3109
+ the texel's sample feeds Enshrouded's estimator. Its state past the mean is a slot's n_r² ×
3110
+ 16 B in `probe_estimators` (short mean, deviation, blend reduction, inconsistency, as halves
3111
+ rounded stochastically): 4 KiB a slot at n_r = 16.
3112
+ - The short mean blends by the α above, its variance by the same with 2τ. The mean takes at least
3113
+ 1/(a+1) until the count's cap, so a young texel is its running mean; the estimator's blend
3114
+ reduction starts at nothing and would otherwise hold a new probe on its first ray.
3115
+ - **The blend reduction follows by the reference's 0.1 a sample** (fixed 2026-10-29), not by the short
3116
+ blend as first built. The catch-up takes the last sample's reduction. Once samples are a response
3117
+ time apart, the short blend is 1 − 1/e, the reduction tracked each sample, and the long mean fell back
3118
+ fastest from the bright ones. Measured on the emulator over samples of mean 1 and deviation 1, at
3119
+ short blends of 0.55, 0.63, 0.78 and 0.9:
3120
+ - the texel settled 7, 18, 28 and 39% low;
3121
+ - at 0.1 a sample, 2, 3, 7 and 15%, with the same step response.
3122
+
3123
+ A texel the guide passes over has a short blend of 0.63 at 60 Hz with a period of 6 frames. At
3124
+ 15 fps, every texel's short blend is past 0.55. At the short blend of 0.18 that the table below ran
3125
+ at, the fix moves nothing (error 13.9 → 14.0%, update to update 0.69 → 0.70%), and that table was
3126
+ not measured again.
3127
+ - The firefly clip, `0.1 + short + 8·deviation`, waits until 1/(a+1) is no longer the variance's
3128
+ blend. Its 0.1 is the reference's, in radiance units; on Sponza under a sun of 7.2 a texel's
3129
+ luma is under 0.26 at p99, so it clips only far outliers there.
3130
+ - Measured on Sponza, the camera still, 4090 (`scratchpad/device/msme.js`, 2026-10-29):
3131
+
3132
+ | τ | texel change an update | band 0 change | 50% / 90% of a doubled sun | 50% / 90% of its halving |
3133
+ |---|---|---|---|---|
3134
+ | 0.5 s, before | 10.8% | 1.54% | 29 / 105 frames | 29 / 102 |
3135
+ | 0.5 s | 1.0% | 0.27% | 55 / 389 | 54 / 212 |
3136
+ | 0.25 s, before | 20.5% | 3.0% | 17 / 79 | 17 / 77 |
3137
+ | 0.25 s | 5.0% | 1.24% | 23 / 234 | 24 / 99 |
3138
+ | 0.125 s | 26% | 4.9% | 19 / >390 | 6 / 37 |
3139
+
3140
+ The screen's diffuse barely moved: a frame against the last at the same point, 4.72% → 4.66% of
3141
+ all eight corners and 16.1% → 15.8% of the pick, still; 9.4% → 9.0% orbiting. The SH averages
3142
+ a probe's texels, so what changes a still frame's diffuse is not the probes' radiance.
3143
+ - Cost, the old pass beside the new in the same frames, which goes first alternating: 4090 15–16
3144
+ against 13 µs; AMD iGPU 860–1,264 against 337–523 µs, 2.0–2.5×. A twin with the estimator's
3145
+ arithmetic replaced by a plain blend costs what the shipped pass does: the time is the state's
3146
+ load and store, not the estimator. Run order moves the iGPU's figures by 2×, so the split
3147
+ between loading and packing the state is not settled.
2085
3148
  - **A new probe's history is its slot's last owner's**: on its first update a texel no ray reached is
2086
- cleared, radiance to nothing and depth to a miss's moments (√3, 3).
2087
- - Depth samples, in spacings: a miss √3, a back face 0.2·t, a hit t, each held to √3. A depth texel
3149
+ cleared, radiance to nothing and depth to a miss's moments (L, L²), L the depth limit (D9).
3150
+ - Depth samples, in spacings: a miss L, a back face 0.2·t, a hit t, each held to L. A depth texel
2088
3151
  gathers R/n_d² rays, so the GI refuses R > 64·n_d².
3152
+ - **Since ray guiding (2026-10-29, §8.6)**: on a guided update a texel's rays are its run in the guide.
3153
+ Its mean needs no weight for the guide's choice, since its rays spread evenly over it.
3154
+ - p is one over the share of updates that reach the texel, averaged over the uniform period:
3155
+ (min(R/n_r², 1) + (P − 1)·min(R·pdf, 1))/P. A texel the guide passes over then blends what the
3156
+ uniform updates bring as fully as its rarity asks, and the response stays τ.
3157
+ - A guided depth texel is the mean of its radiance texels' own means, taken only when every one of
3158
+ them took rays. A plain mean would lean to the brighter ones' depths, and so would a mean over
3159
+ only the lit ones. Its period counts a uniform update as reaching it, and a guided one as often
3160
+ as its least reached radiance texel.
3161
+ - Classification takes only uniform rays: a guided update keeps the kind and the back-face share,
3162
+ since guided rays shun back faces (§8.4).
2089
3163
  - **the projection** (`probe/shader_probe_clipmap_project.js`): a thread a row of a probe's radiance
2090
3164
  map sums its texels as `convert_to_irradiance_sh(L, ω)·dω/(2π)` and its rays' counts; then a thread a
2091
3165
  probe sums its rows into the record's SH, its back-face share, its time, and its state: dead past a
@@ -2117,6 +3191,30 @@ a per-invocation function instead. Each is tier 2 whole, and no thread's loop ru
2117
3191
  texel's solid angle against one integrated from the decode, and the three launches the update's
2118
3192
  command adds.
2119
3193
 
3194
+ **Measured (S4g, 2026-10-28): the stores round toward zero, so accumulate rounds them itself.**
3195
+ - Both GPUs gave the half toward zero for all 131,072 values tried, on each of three paths:
3196
+ `pack2x16float`, `textureStore` into `rgba16float`, and a render target's write. The stall above is
3197
+ therefore a whole ULP/α, and one-sided: below the target.
3198
+ - α follows the frame rate, since the GI's clock is the caller's (`performance.now()` in `Renderer`).
3199
+ - On §12's closed room glowing L, after 900 frames to settle, E/πL was:
3200
+
3201
+ | | 60 Hz | 240 Hz |
3202
+ |---|---:|---:|
3203
+ | rounded by the store, toward zero | 0.985 | 0.941 |
3204
+ | rounded to nearest before the store | 0.992 | 0.970 |
3205
+ | rounded stochastically (built) | 0.999 | 0.999 |
3206
+
3207
+ The first row is the 1.5% and 5.9% that ULP/α predicts.
3208
+ - **Accumulate now rounds every stored component stochastically onto a half itself**, for radiance and
3209
+ for depth: `shader/chunk/math/chunk_f16_round_stochastic.js`, with noise from `pcg3d(slot, texel,
3210
+ updates)`.
3211
+ - The value it hands the store is a half already, so the device's own rounding never applies.
3212
+ - The blend moves in expectation however small its step.
3213
+ - UE 5.4's TSR stores its history through `QuantizeForFloatRenderTarget` for the same reason.
3214
+ - The emulator's `pack2x16float` rounds to nearest even, so tier 2 sees half the devices' stall, and on
3215
+ both sides. `probe_clipmap_accumulate.spec.js` pins convergence from either side at α = 1/64; it was
3216
+ red before the change.
3217
+
2120
3218
  ## 9. Sampling
2121
3219
 
2122
3220
  ### 9.1 The pick: `probe_clipmap_pick(position, normal, view_direction, ξ) -> pick`
@@ -2152,6 +3250,45 @@ Per pixel and cascade the visibility pass makes 8 indirection loads, 8 state wor
2152
3250
  depth taps. Then the pick reads two probes: two SH records and, for specular, two radiance taps. (a)
2153
3251
  read eight 64 B records and resolved eight SH.
2154
3252
 
3253
+ **As built (S4f, 2026-10-28)**, `probe/chunk_probe_clipmap_pick.js`:
3254
+ - **The cage is want's.** `probe_clipmap_cage` and `probe_clipmap_biased` live in
3255
+ `probe/chunk_probe_clipmap_cage.js`, and want calls them too, so a pixel reads the cage it wanted.
3256
+ - **A corner's weight**, `probe_clipmap_corner_weight`:
3257
+ - No slot, or a state that is not ALIVE, weighs 0.
3258
+ - The probe sits at its lattice point plus the relocation offset in its state's second word.
3259
+ - The tap is toward the biased point, at its distance in the cascade's spacings, which is how
3260
+ publish writes depth (§8.8). The wrap back-face term is taken from the point itself.
3261
+ - **The weight is LPV's, extracted**: `probe_visibility_weight` and `probe_visibility_chebyshev` in
3262
+ `shader/chunk/light/probe/chunk_probe_visibility_weight.js`. `chunk_lpv_mask_weights_by_visibility`
3263
+ and `chunk_lpv_get_visibility_mask` call it, unchanged.
3264
+ - **The pick is Brick4's, extracted**: `pick2_wrs_mat2x4` in
3265
+ `shader/chunk/random/reservoir/chunk_pick2_wrs_mat2x4.js`, which `chunk_brick4_probe_meta_pick2`
3266
+ now calls.
3267
+ - It reads and clears the drawn weights through masks, because indexing the matrix by a value puts
3268
+ it in local memory. That cost 3% of the diffuse on the iGPU: 7.29 → 7.06 ms, A/B in one frame
3269
+ with the order alternating.
3270
+ - Extracting it found a defect in the draw it is built on: `sample_discrete_wrs_mat2x4` and
3271
+ `_vec4` drew NaN after leading zero weights, 0/0 in the first step. It is fixed and pinned in
3272
+ `5a3c8e968`.
3273
+ - **Reads** per cascade: 8 indirection loads, 8 state words and 8 depth taps. The pair's two slots
3274
+ are loaded again by index rather than kept, so no array is indexed by a value.
3275
+ - **A depth tile's place takes no integer division** since S4g: `index_to_grid2d_reciprocal`, through
3276
+ the reciprocal the sampling uniform carries. RDNA2 emulates integer division. The iGPU's diffuse went
3277
+ from 7.08 to 6.65 ms, paired, with the image the same to the bit on both GPUs.
3278
+ - **The clipmap's shape is a uniform**, `PROBE_SAMPLING_STRUCT`: C_p, N_p, n_d, the depth atlas's
3279
+ tiles a row and its texel size. `GPUProbeClipmap.write_sampling_bindings` puts it and the six
3280
+ resources into a reader's bindings.
3281
+ - **Tier 2**, `probe_clipmap_pick.spec.js`:
3282
+ - picks as often as the weights, and dead, new and missing corners passed over;
3283
+ - the depth read toward the point, at its direction's own texel, in the cascade's spacings;
3284
+ - a relocated probe weighed from where it moved;
3285
+ - the fallback cascade, the skirt's share, nothing found;
3286
+ - two weighing corners blended in every draw;
3287
+ - the SH mixed by the weights.
3288
+
3289
+ `chunk_probe_visibility_weight.spec.js` and `chunk_pick2_wrs_mat2x4.spec.js` cover the extracted
3290
+ pieces, and LPV's and Brick4's specs pass unchanged.
3291
+
2155
3292
  ### 9.2 Diffuse: `probe_clipmap_sample_irradiance(position, normal, view_direction, ξ) -> E`
2156
3293
 
2157
3294
  - The two picked records' Metro L1 structs are mixed by t, then resolved once with
@@ -2159,6 +3296,12 @@ read eight 64 B records and resolved eight SH.
2159
3296
  - The resolve returns E/π by its convention. The pass multiplies by π, because the resolve pass takes
2160
3297
  irradiance E.
2161
3298
 
3299
+ **As built (S4f)**: there is no `probe_clipmap_sample_irradiance`. `probe_clipmap_sample_sh` returns the
3300
+ mixed Metro L1 struct and whether anything was found, and the caller resolves at the normal it
3301
+ chooses. The deferred pass uses two normals where this section has one n:
3302
+ - it picks with the surface's own normal, as want chose its cage;
3303
+ - it resolves at the bent normal, as IBL's pass does.
3304
+
2162
3305
  ### 9.3 Specular: `probe_clipmap_sample_radiance(position, normal, view_direction, roughness, ξ) -> L`
2163
3306
 
2164
3307
  The two probes of the same pick are used. Per Brick4's fused pass, the pair's blend is re-weighted for
@@ -2186,6 +3329,34 @@ per probe would be manual strips, with their own skirts and their own downsample
2186
3329
  per storage binding. Two SH bands hold the rough end of the lobe well, and a single tap holds the
2187
3330
  smooth end.
2188
3331
 
3332
+ **As built (S6a, S6b, 2026-10-28)**:
3333
+ - **The GGX resolve**, `resolve_irradiance_sh_to_specular` in `shader/chunk/spherical_harmonics/l1i/`
3334
+ (S6a). Its spec holds both invariants above. 7 of 7 mutants were killed.
3335
+ - **The sample**, `probe_clipmap_sample_radiance` in `probe/chunk_probe_clipmap_sample_radiance.js` (S6b).
3336
+ It returns the radiance and whether anything was found, as the SH sample does.
3337
+ - *The pick* is the diffuse's, `probe_clipmap_pick`.
3338
+ - *The re-weight* takes each picked probe's whole weight again with the lobe in place of the normal,
3339
+ at the visibility it was picked at. That visibility is recomputed, one depth tap toward the biased
3340
+ point. The crush below 0.2 keeps a ratio of the wrap terms alone from standing in for it.
3341
+ - *The parallax* takes the far root of the reflected ray against the sphere, so a point inside the
3342
+ sphere always meets it. Where the ray misses the sphere, the tap is along the lobe itself.
3343
+ - *The radiance tap* reads the radiance atlas through the depth's sampler, bilinear and clamped. Its
3344
+ tiles are placed as the depth's are, and the sampling uniform now carries the radiance atlas's shape
3345
+ as well (`radiance_resolution`, its tiles a row, its texel and the row's reciprocal).
3346
+ - *The blend* is `roughness_to_mip_ratio`, between the renormalised taps and the SH. The SH is mixed
3347
+ by the same shares and resolved for the lobe.
3348
+ - *Reads a picked probe*: its record, its state word, two depth taps (visibility and parallax) and one
3349
+ radiance tap.
3350
+ - *The probes' sampling group* takes the radiance atlas, so every reader binds it in every mode. The
3351
+ GI's placeholders gained a texel of radiance.
3352
+ - *The deferred diffuse pass* listed seven of those declarations by hand. A frame graph pass must declare every
3353
+ input it is given, so it threw under SSR on once the mode had probes. It declares the group whole since the
3354
+ fix that followed S6c1, pinned by its spec running it in a frame graph on the GI's bindings.
3355
+ - *Tier 2*: `probe/probe_clipmap_sample_radiance.spec.js` uses tiles whose interior texels hold their
3356
+ own uv, so a tap shows what it read. It covers the parallax tap at a depth that varies with
3357
+ direction, the GGX-resolved SH at the rough end, the mip ratio's blend between, the re-weighted
3358
+ shares with a relocated probe, and nothing found. 16 of 16 mutants were killed.
3359
+
2189
3360
  ### 9.4 Noise and its resolve
2190
3361
 
2191
3362
  - Image passes take ξ from STBN at (pixel, frame), `chunk_stbn_sample_vec2`, as Brick4 does.
@@ -2203,7 +3374,7 @@ smooth end.
2203
3374
  | `forward_sample_indirect(p, v, n, roughness, &radiance, &irradiance)` | one pick (hash noise); irradiance from §9.2, radiance from §9.3 (environment until S6) |
2204
3375
  | `water_gi_irradiance(p, n)` / `water_gi_specular(p, n, v, roughness)` | §9.2 with v = n, since the irradiance call has no view direction / §9.3 (environment until S6); hash noise |
2205
3376
  | `ssr_sample_radiance(p, n, v, roughness, coord)` | §9.3 with STBN at coord (environment until S6) |
2206
- | `ssr_add_hit_irradiance(&result, confidence, p, hit_vs, hit_n, coord)` | LPV's body with §9.2: albedo·E/π at the hit when confidence > 0.05 |
3377
+ | `ssr_add_hit_irradiance(&result, confidence, p, hit_vs, hit_n, coord)` | albedo·E/π at the hit when confidence > 0.05, E the deferred pass's own at the hit's texel (as built; LPV's body with §9.2 measured 41% of the resolve) |
2207
3378
  | `irradiance_clipmap_gi_sample(p) -> array<vec3f,4>` | `probe_clipmap_sample_sh(p)` × `INDIRECT_LIGHT_INTENSITY`: the pick without the back-face term (there is no normal), returned as RGB SH2 |
2208
3379
 
2209
3380
  Each alternative declares `PROBE_CLIPMAP_RESOURCES` with `requireResource`:
@@ -2216,6 +3387,152 @@ Each alternative declares `PROBE_CLIPMAP_RESOURCES` with `requireResource`:
2216
3387
 
2217
3388
  Union bindings were measured free in stage 2 of the switch proposal (p50 1.000).
2218
3389
 
3390
+ **As built (S5c, 2026-10-28)**:
3391
+ - **The probes are bound in every mode.** A reader binds every mode's resources, and the GI is released
3392
+ out of its mode. So `GPUProbeClipmapGI.write_sampling_bindings` hands out its probes' bindings once it
3393
+ has probes. Before that, or out of the mode, it hands out a clipmap of no cascade: the sampling
3394
+ uniform counts none, so the pick selects no cascade and reads nothing past the levels. These are six
3395
+ objects of a few bytes, made on the first ask and destroyed with the GI. The seven declarations are
3396
+ `PROBE_CLIPMAP_SAMPLING_GROUP`, in `probe/chunk_probe_clipmap_pick.js`; the sampler comes from its
3397
+ declaration's descriptor.
3398
+ - **SSR's hit**, `chunk_ssr_add_hit_irradiance_probe_clipmap`: albedo × the deferred pass's indirect
3399
+ diffuse at the hit's texel / π, where the hit's confidence passes 0.05. That output is E with AO in,
3400
+ so a reflection shows the indirect diffuse of the surface it hits, with its AO, its pick and its IBL
3401
+ fallback.
3402
+ - The renderer records the diffuse pass before SSR, which it reads nothing of, and hands it to SSR as
3403
+ `input_indirect_diffuse` under this mode. The resolve's `tIndirectDiffuse` is black for the others.
3404
+ - Tier 2: `chunk_ssr_add_hit_irradiance.spec.js` reads the hit's texel of both inputs, and nothing at
3405
+ confidence 0.05. `SSR.spec.js` binds the diffuse under this mode alone. 7 of 7 mutants were killed.
3406
+ - **Why not the pick at the hit** (§9.5's first form, LPV's body; built in `9208dd7cf`, replaced by Alex's
3407
+ call the same day). Measured on Sponza at 1280×720, sun only, SSR on, the probes settled. The arms
3408
+ alternated frame by frame over one set of probes, and each frame's resolve was also drawn by every
3409
+ arm on the same inputs.
3410
+ - 0.81% of the pixels have a confident hit.
3411
+ - The pick's cost is its work at scattered hits, where a wave pays the whole pick for a few lanes. Its
3412
+ code present but never run is about a sixth of it.
3413
+
3414
+ | SSR resolve, p50 | RTX 4090 | AMD iGPU |
3415
+ |---|---:|---:|
3416
+ | no bounce (IBL's variant) | 39.9 µs | 2.06 ms |
3417
+ | the pick's code present, never run | 42.0 µs | 2.19 ms |
3418
+ | the pick at the hit | 54.3 µs | 2.91 ms |
3419
+ | **as built**: the deferred pass's texel, against no bounce in the same run | × 1.019 | × 1.034 (2.03 → 2.10 ms) |
3420
+
3421
+ The SSR trace, which no arm touches, reads 1.00 between arms on both GPUs.
3422
+ - The pick lit 0.64% of the pixels and added 2.4% of the reflections' light.
3423
+ - The texel lights 0.70%: where no probe weighs anything, the diffuse pass's IBL fallback lights the
3424
+ hit too. It adds 1.9% of the reflections' light, with the hit's AO now applied.
3425
+ - Neither darkens a pixel, and both leave the metadata the same to the bit.
3426
+ - **Forward**, `rasterize/forward/chunk_forward_sample_indirect_probe_clipmap.js`: one pick by the
3427
+ shading normal and the view, jittered by `jitter_from_vectors(p, n)`. That hash is extracted from
3428
+ Brick4's forward sampling into `shader/chunk/random/`.
3429
+ - E = π × the SH resolved at the normal, or the environment's where nothing is found.
3430
+ - The specular is the environment's until S6.
3431
+ - The AVBOIT draw and MBOIT's resolve add the probe declarations to their group 2, which both stages
3432
+ see, and set those entries to the fragment stage alone. The probes' two storage buffers would
3433
+ otherwise take the vertex stage to 10, the floor.
3434
+ - **Water**, `chunk_water_gi_probe_clipmap` in `water/chunk_water_gi.js`: the forward pick with v = n,
3435
+ since the irradiance call has no view, and the environment's specular until S6. The optics'
3436
+ brick4/view group takes the probes, and `water_pipeline` keeps them fragment-only.
3437
+ - **Bindings**: the owners write `write_sampling_bindings` into their hand-built bindings. The renderer
3438
+ hands the GI to the transparency orchestrators, and AVBOIT's hands it on to the water's side channel
3439
+ (`avboit_prepare`'s `probes`).
3440
+ - Tier 2 is `chunk_forward_sample_indirect_probe_clipmap.spec.js` and `water/chunk_water_gi.spec.js`
3441
+ against the pick and the environment map, the fragment-only test in
3442
+ `indirect_lighting_consumers.spec.js`, and the AVBOIT orchestrator's water optics in every mode.
3443
+ - 18 of 19 mutants were killed. MBOIT's resolve without the probes survives because no spec draws
3444
+ MBOIT; the device check below draws it.
3445
+ - **Measured** on Sponza at 1280×720, sun only, with five half-transparent boxes down the nave. Each
3446
+ pass alternated frame by frame with IBL's variant over one set of probes.
3447
+
3448
+ | p50, IBL → the probes | RTX 4090 | AMD iGPU |
3449
+ |---|---:|---:|
3450
+ | AVBOIT draw | 13.3 → 28.7 µs | 132 → 312 µs |
3451
+ | MBOIT's resolve | 11.3 → 26.6 µs | 129 → 317 µs |
3452
+
3453
+ - Per transparent fragment, that is about what the deferred pick costs a pixel.
3454
+ - Averaged over 24 frames, 2.8% of the pixels, the boxes, move by 14 of 255 between the arms. The noise
3455
+ floor, an arm against itself, is 0.1–0.25%. The boxes are lit brighter by the probes than by the
3456
+ sky (luma 97 against 88), the same on both GPUs and in both techniques.
3457
+ - The water optics compile under every mode on both GPUs.
3458
+ - **The irradiance clipmap**, `chunk_irradiance_clipmap_gi_sample_probe_clipmap`: the pick at the voxel,
3459
+ with no normal and no view, jittered by `jitter_from_vectors(p, 0)` and the view's frame. Where nothing
3460
+ is found, the environment SH.
3461
+ - The probes' Metro L1 is handed over as the radiance SH2 that `volumetrics_irradiance_sh_add_radiance`
3462
+ turns back into it: [DC, y, z, x] = [YCoCg→RGB(2π·(Y.w, 0.28209·CoCg)), 2π·Y.y, 2π·Y.z, 2π·Y.x], the
3463
+ band-1 terms grey.
3464
+ - The build binds the probes as a group of its own, which brings it to 9 of the floor's 10 storage
3465
+ buffers, and binds the view.
3466
+ - The cascades are a cache, so a voxel's pick holds until its cascade is rebuilt: cascade 0 and the
3467
+ ruler's each frame, the others as their windows move.
3468
+ - Tier 2: `chunk_irradiance_clipmap_gi_sample.spec.js` accumulates the handed-over SH2 as the build
3469
+ does and gets the pick back, with the frame's noise and the environment where nothing is found.
3470
+ `graph_build_froxel_gi.spec.js` finds the GI's probes and the view bound to the build. 11 of 11
3471
+ mutants were killed.
3472
+ - **Measured** on Sponza at 1280×720, sun only. A sink kept the volume, which only lit particles read.
3473
+ The build's p50 went from 7.2 to 15.4 µs on the 4090 and from 127 to 430 µs on the iGPU, for two
3474
+ cascades of 34³ voxels a frame.
3475
+ - The volume's band 0 moves by 0.0016 between the arms, 6× the probe arm's own change from one run to
3476
+ the next (0.00026). The IBL arm is steady to the bit. The sun is most of the volume here, so its mean
3477
+ moves by 0.5%.
3478
+
3479
+ **As built (S6c2, 2026-10-28)**: SSR's fallback, `chunk_ssr_sample_radiance_probe_clipmap`, is §9.3 at the pixel:
3480
+ the probes' specular picked by its normal and view, with STBN at the pixel and the resolve's frame index, as
3481
+ Brick4's is. Where no probe weighs anything, it is the environment's.
3482
+ - The resolve binds the probes' sampling group again, through SSR's `probes`, in every mode.
3483
+ - The radiance chunk now declares what it reads itself. `switched_shaders_declare_bindings.spec.js` audits it
3484
+ since SSR's resolve links it.
3485
+ - Tier 2: `chunk_ssr_sample_radiance.spec.js` runs the fallback against `probe_clipmap_sample_radiance` with the
3486
+ STBN texel at the pixel and frame, and against the environment where nothing is found. `SSR.spec.js` binds the
3487
+ probes in every mode; `indirect_lighting_consumers.spec.js` finds the probes' specular in this mode's variant
3488
+ alone. 7 of 7 mutants were killed.
3489
+ - **Measured** on Sponza at 1280×720, sun only, SSR on, the probes settled. One of three arms a frame, in turn,
3490
+ over one set of probes; 131 frames an arm, p50:
3491
+
3492
+ | arm | deferred pass | SSR resolve | together, RTX 4090 | together, AMD iGPU |
3493
+ |---|---|---|---:|---:|
3494
+ | **as built**: the fallback picks again | 87 µs / 6.64 ms | 140 µs / 10.39 ms | 227 µs | 17.03 ms |
3495
+ | S5c: IBL's fallback | 87 µs / 6.64 ms | 61 µs / 2.09 ms | 148 µs | 8.74 ms |
3496
+ | the deferred pass's one pick writes the specular too, and the fallback reads its texel | 114 µs / 9.02 ms | 61 µs / 1.93 ms | 175 µs | 10.95 ms |
3497
+
3498
+ - The SSR trace, which no arm touches, reads 0.98–1.01 between arms.
3499
+ - The fallback runs at every foreground pixel, so its pick is the whole pick again: 4.96× the resolve on the
3500
+ iGPU. The texel arm's light is the as-built arm's in the mean; 1% of the pixels differ, by the pick's noise.
3501
+ - The probes' fallback lifts the reflections' mean luma 28× over the harness's dim sky. No pixel goes darker.
3502
+ - **Alex kept the pick** (2026-10-28), as this section designs it, with these measurements before him.
3503
+
3504
+ **As built (S6d, 2026-10-28)**: the forward passes' and the water's specular from the probes, §9.3.
3505
+ - **Forward**, `chunk_forward_sample_indirect_probe_clipmap`: one pick for both lobes, as the fused deferred pass
3506
+ takes it (S6c1). The irradiance is the pick's SH resolved at the normal, as before. The specular is
3507
+ `probe_clipmap_radiance_of` the same pick, at the fragment's roughness. Where no probe weighs anything, both are
3508
+ the environment's.
3509
+ - **Water**, `water_gi_specular`: `probe_clipmap_sample_radiance` by the surface's normal and view, its noise the
3510
+ irradiance's hash, or the environment's where nothing is found. The optics ask for the irradiance at other points
3511
+ and by other normals (the in-scatter's column, a receiver's downwelling light), so each call picks for itself.
3512
+ - **Bindings** are unchanged. Each of the three passes has declared the probes' group whole since S6b, the radiance
3513
+ atlas with it, fragment-only.
3514
+ - **Tier 2**: `chunk_forward_sample_indirect_probe_clipmap.spec.js` and `water/chunk_water_gi.spec.js` give each
3515
+ probe a colour of its own over its radiance tile. They check the specular against `probe_clipmap_sample_radiance`
3516
+ with the call's own noise, view and roughness, and against the environment where nothing is found.
3517
+ `indirect_lighting_consumers.spec.js` finds the probes' radiance in this mode's forward and water variants alone.
3518
+ 16 of 16 mutants were killed.
3519
+ - **Measured** on Sponza at 1280×720, sun only, with S5c's five half-transparent boxes down the nave and S5c's
3520
+ harness. Each pass alternated frame by frame with IBL's variant over one set of probes. IBL's arm reads within
3521
+ 1 µs between S5c's run and this one.
3522
+
3523
+ | p50, IBL → the probes | S5c: the irradiance | S6d: both lobes |
3524
+ |---|---:|---:|
3525
+ | AVBOIT draw, RTX 4090 | 13.3 → 28.7 µs | 12.3 → 31.7 µs |
3526
+ | MBOIT's resolve, RTX 4090 | 11.3 → 26.6 µs | 11.3 → 30.7 µs |
3527
+ | AVBOIT draw, AMD iGPU | 132 → 312 µs | 131 → 379 µs |
3528
+ | MBOIT's resolve, AMD iGPU | 129 → 317 µs | 130 → 383 µs |
3529
+
3530
+ - The specular adds 26–27% (4090) and 35–38% (iGPU) to what the probes' light costs each pass over IBL's.
3531
+ - Averaged over 24 frames, 3.6–3.8% of the pixels move by more than 4 of 255 between the arms, against 2.8–3.0% in
3532
+ S5c: the boxes' reflections now take the probes' light too. An arm against itself moves 0.2–0.7% of them
3533
+ (S5c 0.1–0.25%), since the specular carries the pick's noise; TAA is off in this harness.
3534
+ - The water optics compile under every mode on both GPUs.
3535
+
2219
3536
  ### 9.6 Deferred
2220
3537
 
2221
3538
  A new case in `#graph_indirect_lighting`, shaped like Brick4's:
@@ -2227,6 +3544,72 @@ A new case in `#graph_indirect_lighting`, shaped like Brick4's:
2227
3544
 
2228
3545
  Until S6, specular is `shader_specular_IBL`, as LPV mode does today.
2229
3546
 
3547
+ **As built (S4f, 2026-10-28)**: `probe/shader_probe_clipmap_diffuse.js` serves SSR on and off alike.
3548
+ The fused `shader_probe_clipmap_indirect_lighting` waits for S6, since until then there is no probe
3549
+ specular to fuse with.
3550
+ - `Renderer` swaps the pass in for `shader_diffuse_IBL` once `GPUProbeClipmapGI.has_probes`, that
3551
+ is, once the mode has run a frame.
3552
+ - A pixel with no probe found takes `get_ibl_irradiance(bent normal)` inside the pass.
3553
+ - AO multiplies the result, as in the IBL pass.
3554
+ - **Tier 2**: `shader_probe_clipmap_diffuse.spec.js` runs the pass whole on the emulator:
3555
+ - π × the SH resolved at the bent normal × occlusion;
3556
+ - IBL's output where nothing is found;
3557
+ - the pick by the surface normal and the view, against `probe_clipmap_sample_sh` itself.
3558
+ - **Mutants**: 32 of 32 killed (the pick 16, the visibility weight 3, the pick of two 2, the cage 1,
3559
+ the host's uniform 2, the pass 8). Four were killed by tests added for them:
3560
+ - a relocated probe weighed from where it moved;
3561
+ - the pair's second slot and the blend's direction, both by two corners blended in every draw;
3562
+ - the depth read at its direction's own texel.
3563
+ - **Measured** on Sponza at 1280×720, a still camera, the caches baked. The frame's mean luma goes
3564
+ from 2.6 under IBL to 66 under the probes (8-bit), the same on both GPUs.
3565
+
3566
+ | p50 | AMD iGPU | RTX 4090 |
3567
+ |---|---:|---:|
3568
+ | the diffuse | 7.06 ms | 58 µs |
3569
+
3570
+ The iGPU's time, split by twins drawn in the same frame:
3571
+
3572
+ | part | iGPU |
3573
+ |---|---:|
3574
+ | depth taps and Chebyshev | 2.37 ms |
3575
+ | the draw of two | 1.30 ms |
3576
+ | SH reads and the resolve | 0.18 ms |
3577
+ | the skirt | 0.04 ms |
3578
+ | the pass without a pick | 0.42 ms |
3579
+ | the rest of the corner weights: indirection loads, state words, the trilinear and back-face terms | ~3.0 ms |
3580
+
3581
+ **As built (S6c1, 2026-10-28)**: `probe/shader_probe_clipmap_indirect_lighting.js`, the fused pass, with SSR off.
3582
+ - `Renderer` draws it in place of the diffuse, specular and resolve passes once `has_probes`, as Brick4's fused
3583
+ pass replaces its three. With SSR on, the diffuse pass and SSR stay.
3584
+ - **One pick a pixel**, by its own normal and the view, with the frame's STBN, for both lobes.
3585
+ `probe_clipmap_sh_of(pick)` and `probe_clipmap_radiance_of(pick, …)` take a pick already made;
3586
+ `probe_clipmap_sample_sh` and `probe_clipmap_sample_radiance` make one and call them.
3587
+ - **The diffuse** is the diffuse pass's: the pick's SH resolved at the bent normal, times π, times AO. Its
3588
+ shares are not re-weighted for the bent normal, as Brick4's are. The diffuse pass takes them as picked (S4f),
3589
+ so the fused pass lights the diffuse as SSR on does.
3590
+ - **The specular** is `probe_clipmap_radiance_of` at the surface's roughness, as the IBL pass samples its own.
3591
+ - A pixel no probe weighs anything for takes the environment's pair, as the IBL passes give it. The pair is
3592
+ resolved as `shader_resolve_indirect_lighting` resolves it and added to the scene's colour.
3593
+ - **Tier 2**: `shader_probe_clipmap_indirect_lighting.spec.js` runs the pass on the emulator against the
3594
+ diffuse pass, `probe_clipmap_sample_radiance` and the resolve pass on the same pixel. Where nothing is found,
3595
+ it runs against the IBL pair. The pixel's bent normal is placed so that its visibility cone ends across the
3596
+ specular lobe, and the specular occlusion shows. 14 of 14 mutants were killed.
3597
+ - **Measured** on Sponza at 1280×720, sun only, SSR off, the probes settled. The fused pass's node drew one of
3598
+ three arms a frame, in turn, over one set of probes. 138 frames an arm; p50 of the indirect light's passes:
3599
+
3600
+ | arm | RTX 4090 | AMD iGPU |
3601
+ |---|---:|---:|
3602
+ | **as built**: the fused pass | 265 µs | 9.66 ms |
3603
+ | before: the probes' diffuse, IBL's specular, the resolve | 270 µs (208 + 31 + 33) | 8.17 ms (6.65 + 0.62 + 0.91) |
3604
+ | the same light unfused: the probes' diffuse, a pass of their specular, the resolve | 480 µs (208 + 237 + 36) | 16.18 ms (6.65 + 8.62 + 0.92) |
3605
+
3606
+ - The probe trace, which no arm touches, reads 0.97–1.00 between arms.
3607
+ - One pick for both lobes costs 0.55× (4090) and 0.60× (iGPU) of the two picks unfused. Against the passes it
3608
+ replaces, the probes' specular costs nothing measurable on the 4090 and 1.5 ms on the iGPU.
3609
+ - The fused pass's light matches the unfused passes' to 0.6% of a pixel's luma, their f16 intermediates. The
3610
+ probes' specular lifts the indirect light's mean luma 36% over the sky's, which is dim in this harness. Both
3611
+ GPUs agree.
3612
+
2230
3613
  ### 9.7 Every file the change touches
2231
3614
 
2232
3615
  The new mode value:
@@ -2266,7 +3649,7 @@ the shadow knobs' pattern.
2266
3649
  - **The mesh cache fields** (`mesh_sdf_voxel_size`, `mesh_sdf_max_bricks`): a change rebakes every
2267
3650
  cached geometry.
2268
3651
  - **Budgets and times** (`brick_build_budget`, `mesh_sdf_bake_budget`, `card_capture_budget`,
2269
- `rays_per_probe`, `ray_budget`, `response_time`, `retention_time`): uniforms and transient sizes, so
3652
+ `rays_per_probe`, `ray_budget`, `response_frames`): uniforms and transient sizes, so
2270
3653
  they take effect the next frame without a rebuild.
2271
3654
 
2272
3655
  | Setting | Default | Range | Changes |
@@ -2286,8 +3669,9 @@ the shadow knobs' pattern.
2286
3669
  | `depth_resolution` | 8 | 4–32, power of two | octahedral depth texels per side |
2287
3670
  | `rays_per_probe` | 256 | a power of two in [n_r²/4, 4·n_r²] | rays per probe update; below n_r² an update covers a spread subset of texels, above it each texel averages several |
2288
3671
  | `ray_budget` | 131,072 | ≥ `rays_per_probe` | rays per frame (a time knob) |
2289
- | `response_time` | 0.5 s | > 0 | temporal time constant τ |
2290
- | `retention_time` | 1 s | > 0 | how long an unwanted probe keeps its slot |
3672
+ | `response_frames` | 30 | > 0 | the radiance's short mean's time constant τ in frames (§8.8), and the depth's blend's |
3673
+
3674
+ `retention_time` (1 s) was removed on 2026-10-30: probes are evicted by the pressure on the slots (§8.3).
2291
3675
 
2292
3676
  The defaults:
2293
3677
  - They sit between Enshrouded's low and mid tiers (100k and 250k rays; radiance 8 and 16).
@@ -2300,7 +3684,8 @@ The defaults:
2300
3684
  - the size-class step of two bricks;
2301
3685
  - the hit threshold and minimum step, ½ voxel;
2302
3686
  - 64 steps a ray;
2303
- - 64 instances per cell in the mesh provider, 256 triangles per cell in the vgeo provider;
3687
+ - the mesh provider's 64 rows a batch and 512 nodes a level of a walk, past which the work goes on in more
3688
+ batches and rounds (S9);
2304
3689
  - 32 cards per geometry, and 4 alpha continuations per capture texel;
2305
3690
  - the 2³-texel object cell;
2306
3691
  - 2 probes per pick;
@@ -2381,6 +3766,8 @@ specs. A real compile on a device catches what the validator misses.
2381
3766
  - The soft-shadow penumbra matches the analytic disk light at a sphere's edge.
2382
3767
  - As built (S3e): against the disk past the surface the tracer hits, half a voxel out, within 0.1;
2383
3768
  half at the edge within 0.1.
3769
+ - Since 2026-10-30 the hits' shadows are rays: under a slit narrower than the sun's disk, each ray
3770
+ sees all of the sun or none, and their mean the part of the disk the slit lets through (§8.7).
2384
3771
  - `mesh_card_surface`: a hit on a card's surface returns that texel's diffuse albedo and emissive,
2385
3772
  and a hit hidden from every card returns the material mean.
2386
3773
  - **Probes**:
@@ -2395,7 +3782,7 @@ specs. A real compile on a device catches what the validator misses.
2395
3782
  - The cascade blend is continuous across the skirt.
2396
3783
  - `resolve_irradiance_sh_to_specular` at g₁ = 2/3 equals `resolve_irradiance_sh_to_diffuse`, and
2397
3784
  returns L under constant radiance L at every α.
2398
- - **Extracted pieces** (probe weight, pick, hash, soft-shadow estimator, free-list pop/push): their
3785
+ - **Extracted pieces** (probe weight, pick, hash, free-list pop/push): their
2399
3786
  old callers' specs stay green. LPV's and Brick4's sampling are unchanged to the bit.
2400
3787
 
2401
3788
  **Tier 3, SoftwareGPUDevice**: host orchestration, black-box on what reaches the device.
@@ -2430,6 +3817,138 @@ specs. A real compile on a device catches what the validator misses.
2430
3817
  allocation failures, period T.
2431
3818
  - **The storage-write rounding question of §8.8.**
2432
3819
 
3820
+ **As built, S4's done-when (S4g, 2026-10-28)**: one bounce, on both GPUs. The harness is the session's
3821
+ scratchpad `device/s4g.js`.
3822
+ - **How the runs were held.** The GI's clock is the caller's `performance.now()`, so α followed each run's
3823
+ frame pacing, and a furnace read differently from run to run. Every run below holds the GI's clock at
3824
+ 1/60 s a frame by wrapping `GPUProbeClipmapGI.graph_update`. The furnaces then let it settle 900
3825
+ frames, 30 τ.
3826
+ - **White furnace, one bounce.**
3827
+ - *A closed room* of 8 × 4 × 8 m, every face glowing L, no light. Before AO, E/πL is 0.9991 at p50 and
3828
+ 0.994 at p01, on both GPUs and at 60 or 240 Hz. Every ray hits, every texel holds L, and the
3829
+ records' E/π is 0.4998 against 0.5. The 1.5% and 5.9% short before were the store's rounding (§8.8).
3830
+ - GTAO's occlusion multiplies the probes' diffuse, as it does IBL's. The room's corners then read p10
3831
+ 0.886 and p01 0.557: the probes see those walls glowing, and AO calls them dark. A furnace therefore
3832
+ reads E before AO.
3833
+ - *An open sky* of radiance L over a dark floor of 200 m, no light. L1 holds a lit hemisphere's
3834
+ irradiance exactly at its pole, so πL is the floor's exact answer. E/πL is 0.997 on the iGPU and
3835
+ 0.980 on the RTX 4090. **Open, not root-caused**: the shortfall is in the 4090's probes, not the
3836
+ resolve, since the surface normal gives 0.983 against the bent normal's 0.981.
3837
+ - Texel by texel, every probe holds L from 10° above the horizon up on both GPUs. At 2–10° the
3838
+ 4090's texels hold 0.98 and 0.96 of L, against the iGPU's 0.996 and 0.993, on rays that resolve
3839
+ alike.
3840
+ - **D4's reversal condition is met here.** 64 steps leave 63% of the rays within 2° above the
3841
+ horizon unresolved, 41% at 2–5°, 28% at 5–10° and 10% at 10–20°. Over Sponza it is 0.24–0.34% of
3842
+ all rays.
3843
+ - **Stability**, on Sponza with the sun: each pixel against the same surface point a frame before,
3844
+ reprojected through the previous frame's view-projection. The two GPUs agree to the per-mille.
3845
+ - Raw, the indirect diffuse before TAA:
3846
+
3847
+ | p50 | still | orbit | dolly |
3848
+ |---|---:|---:|---:|
3849
+ | the eight corners blended (a twin) | 5.0% | 10.1% | 9.2% |
3850
+ | the pick of two (shipped) | 16.9% | 26.4% | 17.6% |
3851
+ | pixels matched | 99.6% | 95.8% | 98.3% |
3852
+
3853
+ - With TAA, the final image:
3854
+ - Still: the pick 0.95% against the eight's 0.65%.
3855
+ - Orbiting, the medians are alike, 14.3% against 13.1%, which is texture detail resampled.
3856
+ **The tails are not: p90 50% against 21%, mean 22.9% against 13.5%.** TAA does not resolve the
3857
+ pick's noise under motion in a tenth of the frames, which §9.4 expected it to.
3858
+ - The eight corners' cost, paired in one frame with the order alternating: iGPU 5.93 ms against the
3859
+ pick's 6.64 ms, **0.72 ms less**; RTX 4090 65.5 µs against 58.4 µs.
3860
+ - **Alex kept the pick of two** for the diffuse (2026-10-28), with these measurements before him.
3861
+ - **Counters**, on Sponza:
3862
+ - live probes: 769 still, 1,341 orbiting; dead: 13 and 54;
3863
+ - the period 2, and nothing starved;
3864
+ - rays: 3–5% miss, 0.24–0.34% unresolved, 2% back faces;
3865
+ - all 103 geometries' fields coarsened; the bake starved 16 frames and skipped 90.
3866
+ - **Timings**, Sponza at 1280 × 720, p50:
3867
+
3868
+ | | AMD iGPU (orbit) | RTX 4090 (still) |
3869
+ |---|---:|---:|
3870
+ | want | 116 µs | 7 µs |
3871
+ | relocate | 20 µs | 25 µs |
3872
+ | trace, with the sun's shadow | 5.16 ms | 310 µs |
3873
+ | shade | 1.04 ms | 26 µs |
3874
+ | accumulate, the projection's rows and probes, publish | 0.75 ms | 33 µs |
3875
+ | the diffuse | 6.65 ms (7.06 before S4g's tile address) | 58 µs |
3876
+ | the rest, each | under 8 µs | under 8 µs |
3877
+
3878
+ **As built, S5's done-when (S5d, 2026-10-28)**. The harness is the session's scratchpad `device/s5d2.js`.
3879
+ - **The multi-bounce furnace** reads 0.994 once a hit wants its probes (S5a, §8.7).
3880
+ - **Cornell box against the path tracer.**
3881
+ - *The scene*: a box 4 m inside, open toward the camera, under a black sky, lit by a point light 0.4 m
3882
+ below the ceiling. The walls have the classic reflectances (white 0.73, red (0.63, 0.065, 0.05),
3883
+ green (0.14, 0.45, 0.091)), with a short and a tall white box inside.
3884
+ - *The images*: 480×360, tone mapping NONE at a fixed exposure, the sRGB transfer undone on the CPU.
3885
+ Each image's mean is taken per surface, the surface found by casting each pixel's ray on the CPU.
3886
+ The probes' image is the mean of 64 frames after 900 frames of settling.
3887
+ - *The reference is the accumulating path tracer through twins of its shader.* The engine's tracer
3888
+ stops at three bounces (`BOUNCE_LIMIT = 3u`). In this box that leaves its total at 84–94% of the
3889
+ converged one, and its indirect at 72–77%. So the reference traces 24 bounces at 512 samples a
3890
+ pixel, and a twin at one bounce gives the tracer's own direct light.
3891
+
3892
+ | surface | total, probes / traced | indirect, probes / traced | direct, raster / traced |
3893
+ |---|---:|---:|---:|
3894
+ | floor | 0.88 | 0.89 | 0.88 |
3895
+ | ceiling | 0.90 | 0.94 | 0.89 |
3896
+ | back | 1.03 | 1.07 | 0.98 |
3897
+ | red wall | 1.03 | 1.15 (r 1.41, g 0.78, b 0.73) | 0.87 |
3898
+ | green wall | 0.99 | 1.04 (r 0.82, g 1.09, b 0.76) | 0.92 |
3899
+ | the boxes | 1.14 | 1.55 | 0.64 |
3900
+
3901
+ The iGPU's probe image agrees with the 4090's within 0.5% on every surface.
3902
+ - **The direct light differs before the GI does.** The rasterizer's point light is 2–13% under the
3903
+ tracer's on the walls and 36% under on the boxes. That is the direct path's, not the probes'; it was
3904
+ measured here and not looked into.
3905
+ - **The indirect alone**, each image less its own direct, is within −11% to +7% on the floor, ceiling,
3906
+ back and green wall, +15% on the red wall, and +55% on the boxes.
3907
+ - **The colour bleed is too saturated** on the coloured walls: the red wall's indirect is redder than
3908
+ traced, the green wall's greener. The probes keep chroma in the constant band alone (Metro L1), so a
3909
+ probe beside a coloured wall lends its colour to every direction.
3910
+ - **None of these gaps is a follow-up** (Alex, 2026-10-28). The tracer at three bounces is the engine's
3911
+ reference, and a probe GI cannot match a path tracer: angular resolution, depth bias and the probes'
3912
+ sampling offsets bound it. S5's done-when is the comparison, not parity with it.
3913
+ - **Timings**, the probe clipmap's passes summed at their p50, Cornell at 480×360: **0.22 ms** on the 4090
3914
+ and **5.6 ms** on the iGPU. The iGPU's trace is 2.8 ms of it, the light 0.62 ms, the diffuse 0.80 ms,
3915
+ shade 0.34 ms and the hit wants 0.32 ms.
3916
+
3917
+ **As built, S6's done-when (S6e, 2026-10-28)**. The harness is the session's scratchpad `device/s6e.js`.
3918
+ - **The room** is S4g's, 8 × 4 × 8 m inside six slabs, with no light under a black sky. SSR is off, so the fused
3919
+ pass draws, and the probes settle for 900 frames. Captures drawn beside the fused pass, on its bindings, write
3920
+ the probes' specular at each pixel as the pass reads it: `probe_clipmap_sample_radiance` with the pixel's STBN
3921
+ noise. A twin of the sample without the parallax, which taps along the lobe, is drawn beside it every frame.
3922
+ - **Reflections in a closed room, as a furnace.** Every face glows L with albedo 0, so every probe texel holds L.
3923
+ The specular over L at roughness 0.05, 0.3, 0.6 and 1 is 0.9995 at p50 and at least 0.996 at p01, on both GPUs,
3924
+ with the parallax and without.
3925
+ - **The parallax A/B.** The walls and the ceiling each glow a colour of their own with albedo 0; the floor is black
3926
+ metal at roughness 0.05. Each floor pixel's specular, averaged over one cycle of the STBN (64 frames), is held
3927
+ against the exact mirror: the face the pixel's mirror ray meets, cast on the CPU.
3928
+ - *Right face* is the share of floor pixels whose nearest face colour is that face's.
3929
+ - *The error* is |L − L_face|₁ / |L_face|₁.
3930
+ - The table gives the range over two runs on each GPU; the two GPUs agree within half a percent.
3931
+
3932
+ | the mirror ray's face is | with the parallax (as built) | without |
3933
+ |---|---:|---:|
3934
+ | under 2 m away (17% of the floor) | 87.5–87.8% right, error 0.200–0.202 | 97.0%, 0.105–0.107 |
3935
+ | 2–4 m away (32%) | 97.2–97.3%, 0.146–0.148 | 95.5–95.9%, 0.141–0.144 |
3936
+ | 4–8 m away (51%) | 87.8–88.4%, 0.255–0.258 | 79.6–79.9%, 0.245–0.246 |
3937
+ | the whole floor | 90.7–91.0%; error p50 0.159–0.162, p90 0.384–0.390 | 87.6–87.9%; 0.140–0.143, 0.384–0.388 |
3938
+
3939
+ - The parallax puts more of the reflections from 2 m out on their face, and fewer of those under 2 m. The
3940
+ ceiling, 4 m and more above the floor, is the one face it does worse on as a whole: 85.7–87.0% right against
3941
+ 100%.
3942
+ - *Two other forms were measured and not taken.*
3943
+ - Leaving the parallax out where the depth reads its √3 limit gives 86.1–86.4% right and a p90 error of
3944
+ 0.44–0.45. Each probe decides alone, so a picked pair can tap along two inconsistent directions.
3945
+ - Meeting the sphere where the ray enters, for a point outside it, is the same from 2 m out and worse under
3946
+ it: 85.3% right, error 0.222.
3947
+ - The parallax costs the capture 133 against 126–128 µs on the 4090 and 4.86 against 4.59 ms on the iGPU, at
3948
+ 960 × 540 with every pixel the room's.
3949
+ - **The parallax stays as built.** It places more of the floor's reflections, and the far ones most; the near ones
3950
+ it misplaces about four times as often as a flat tap does.
3951
+
2433
3952
  ## 13. Stages
2434
3953
 
2435
3954
  Each stage lands suite-green and is measured before the next.
@@ -2444,7 +3963,8 @@ Each stage lands suite-green and is measured before the next.
2444
3963
  | S5 | next bounce (E_ind); local lights via generalised world clusters; the forward, water, SSR-hit and irradiance-clipmap alternatives | furnace (multi-bounce), Cornell vs path tracer |
2445
3964
  | S6 | specular: radiance ⊕ GGX-SH2, parallax, fused pass, SSR fallback, forward and water specular | parallax A/B, reflections in a closed room |
2446
3965
  | S7 | the virtual geometry provider: box cut, streaming invalidation | Zorah lit by its own geometry; timings |
2447
- | S8 | measured follow-ups only (§17) | — |
3966
+ | S8 | virtual geometry's caches per geometry (Alex, 2026-10-29): a proxy cut from each, baked by the mesh path, adopted; the per-cell provider deleted; then measured follow-ups only (§17) | Zorah's holes, back faces and dead probes against S7e's and Sponza's; the frames and cost of the bake; the frame at rest |
3967
+ | S9 | the mesh provider without its cap (Alex, 2026-10-29): walks that hand on what they cannot hold, batches of 64 spread over workgroups by count, an atomic merge, the resolve on the cells the walks listed; contract 7, retry | Zorah's holes against S8's; the provider's cost on both GPUs, paired with S8's |
2448
3968
 
2449
3969
  ## 14. Decisions taken here
2450
3970
 
@@ -2470,8 +3990,8 @@ Each says what would reverse it.
2470
3990
  - A hit does no material work.
2471
3991
  - *Reverse:* none of mine; Alex chose it over cards that hold uvs.
2472
3992
  - **D6. No triangle is read per frame.** Meshes are read at bakes (the SDF, per geometry) and at
2473
- captures (the cards, per pair and per material change). The vgeo provider reads its cut per
2474
- requested cell, never per frame. *Reverse:* none; it is the review's rule.
3993
+ captures (the cards, per pair and per material change). A virtual geometry's cut is read once, for
3994
+ its proxy's bake (S8), never per frame. *Reverse:* none; it is the review's rule.
2475
3995
  - **D7. Hits are diffuse only: `L = emissive + ρ_d·E/π`.**
2476
3996
  - That is what Lumen's payload holds, and Lumen's surface cache lights diffusely too.
2477
3997
  - (b) first had the full BRDF's directional albedo at the hit's NoV (`tSplitSum`), which needed
@@ -2481,22 +4001,44 @@ Each says what would reverse it.
2481
4001
  of the diffuse one. That needs the same storage and no hit-side change.
2482
4002
  - **D8. Radiance `rgba16float`, two-pass update.** Hardware bilinear for every consumer.
2483
4003
  *Reverse:* if the stall bias measures visible, f32 history in a buffer.
2484
- - **D9. Depth moments normalised by spacing, clamped at √3·s.**
4004
+ - **D9. Depth moments normalised by spacing, clamped at (1 + 0.45)·√3·s.**
4005
+ - Chebyshev's bound asks only how many samples lie past the point, which a clamp past every point the
4006
+ cage tests leaves as it was, and a larger clamp only loosens it. The clamp is one constant, so a
4007
+ reader de-normalises with the spacing alone.
4008
+ - **Corrected 2026-10-30**: it was √3·s, the cage diagonal, which holds only for a probe on its
4009
+ lattice point. Relocation (§8.4) moves a probe up to 0.45 of a spacing on each axis, so a biased
4010
+ point can be (1 + 0.45)·√3 ≈ 2.51 spacings from a corner probe. A direction that sees nothing read as
4011
+ occluded past √3: visibility ≤ 0.41 at 1.8 spacings, 0.043 at 2.0. Over a cell, a corner moved 0.45
4012
+ on every axis away from it lost up to 0.70% of the cage's trilinear weight; 0.25 on each, 0.013%.
4013
+ - RTXGI clamps at `1.5·|spacing|` (`ProbeBlendingCS.hlsl`), the same reach over DDGI 2021's
4014
+ half-spacing relocation bound; neither DDGI paper's text gives a value. RTXGI's own relocation is a
4015
+ sphere of 0.45, so its clamp has headroom.
2485
4016
  - **D10. SH2 projected from blended radiance, not blended separately.**
2486
- - **D11. Exponential blend over a response time in seconds; jittered stratified rays.**
4017
+ - **D11. Radiance by a multi-scale mean whose short mean follows a response time in seconds; depth
4018
+ by an exponential blend; jittered stratified rays.**
2487
4019
  - Lumen overwrites without jitter, but its screen probes filter downstream. Here consumers read
2488
4020
  the probes directly.
2489
4021
  - DDGI 2021's per-texel change boost is left out. A texel here sees one ray an update, so a change
2490
4022
  test on it is a noise test.
2491
- - *Reverse:* if noise or lag measures visible at the defaults, Enshrouded's moving-average
2492
- estimator, which is the variance-aware form of that boost.
4023
+ - **Reversed for the radiance, 2026-10-29**: Alex judged the noise visible, and Enshrouded's
4024
+ estimator (RT Gems ch. 25) was built (§8.8, as built). Measured on Sponza at τ = 0.5 s: a texel
4025
+ moves 1.0% an update against 10.8%, the SH's band 0 0.27% against 1.54%; a doubled sun is half
4026
+ taken in 55 frames against 29, and 90% in 389 against 105.
4027
+ - A variant testing the means' gap against the short mean's own deviation, as SSR's accumulation
4028
+ does, was modelled on the CPU and followed a step on a noisy texel more slowly still; not built.
4029
+ - **The rays' half reversed too (2026-10-29).** Alex found the probes' noise visible, and asked for
4030
+ Enshrouded's importance-sampled rays, rotating corner and all (§8.6, as built). The estimator's short
4031
+ mean now takes a texel's period from its share of the updates that reach it (§8.8).
2493
4032
  - **D12. Schedule: NEW first, then a slot-hashed phase with a period from the live count.**
2494
4033
  - **D13. Pixels pick 2 of 8 by WRS after visibility-only weights** (the review). Brick4's sampler and
2495
4034
  noise. *Reverse:* if the measured noise under TAA is visible, then a screen-space reservoir resolve,
2496
4035
  argued from that measurement.
2497
- - **D14. Sun shadows at hits by SDF soft-shadow rays.** Shadow maps read lit off screen.
2498
- - **D15. Local lights from the world clusters with the shadow-atlas lookups.** A light with a
2499
- cleared map is unshadowed at hits.
4036
+ - **D14. Sun shadows at hits by shadow rays traced in the bricks**, toward a point of the disk.
4037
+ Shadow maps read lit off screen; the cone soft shadow S3e built leaked through gaps narrower than
4038
+ the disk, and was replaced on 2026-10-30 (§8.7).
4039
+ - **D15. Local lights from the world clusters, one picked by WRS over the chance of its pick, its
4040
+ shadow a ray traced in the bricks** (2026-10-30, §8.7). Until then every light of the cell, each
4041
+ through its shadow map, a light with a cleared map unshadowed at hits.
2500
4042
  - **D16. Settings sub-object, rebuild on mismatch.**
2501
4043
  - **D17. Capacity learned from reported failures, never shrinks.**
2502
4044
  - **D18. The mode is named `ProbeClipmap`**, after what shading samples, as Brick4 and LPV are.
@@ -2555,6 +4097,10 @@ Each says what would reverse it.
2555
4097
  - The cost of sparse: no probes where nothing visible needs them, so off-screen bounces see only the
2556
4098
  probes that exist.
2557
4099
  - *Reverse:* if the off-screen bounce measures short, Enshrouded's world-cache probes (§17).
4100
+ - **Reversed in part at S5a (Alex, 2026-10-28)**: the multi-bounce furnace measured the off-screen
4101
+ bounce short (§8.7), so hits want the probes they read as well, one step from the screen, as the one
4102
+ probe kind (§8.3). "If we get no indirect bounce, it is better to lose energy than to invent energy":
4103
+ where no probe has data yet the bounce stays zero, and no other probe stands in.
2558
4104
  - **D32. The mover pass moves from VSM to scene level, with a skinned bit** (Alex, as recommended).
2559
4105
  - The kernels and the list stay the same. The scene context owns them and produces the list once a
2560
4106
  frame for any reader.
@@ -2621,8 +4167,6 @@ Each item is added only if a measurement asks for it, or, where marked, when Ale
2621
4167
  emissive. The measured fix is the BRDF's hemispherical albedo captured in place of the diffuse
2622
4168
  one.
2623
4169
  - **Caches persisted across sessions**, an offline bake format.
2624
- - World-cache probes at ray hits (Enshrouded's second kind), if the off-screen bounce measures short.
2625
- - Importance-sampled ray directions, if noise at the defaults measures visible.
2626
4170
  - Ray and history reprojection between probes, if long-range propagation into enclosed spaces
2627
4171
  measures short.
2628
4172
  - Foliage and translucency in the field, Enshrouded's open problem too.