@woosh/meep-engine 3.33.0 → 3.35.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 (503) hide show
  1. package/editor/Editor.d.ts.map +1 -1
  2. package/editor/Editor.js +0 -3
  3. package/editor/ecs/component/prototypeObjectEditor.js +1 -165
  4. package/editor/ecs/component/registerBasicTypeEditors.d.ts.map +1 -1
  5. package/editor/ecs/component/registerBasicTypeEditors.js +0 -5
  6. package/editor/ecs/component/registerEngineComponentEditors.d.ts.map +1 -1
  7. package/editor/ecs/component/registerEngineComponentEditors.js +0 -20
  8. package/editor/process/SymbolicDisplayProcess.d.ts.map +1 -1
  9. package/editor/process/SymbolicDisplayProcess.js +100 -106
  10. package/editor/prototypeEditorShell.d.ts.map +1 -1
  11. package/editor/prototypeEditorShell.js +190 -186
  12. package/editor/templates/entity_templates.d.ts.map +1 -1
  13. package/editor/templates/entity_templates.js +10 -27
  14. package/editor/view/makeEntityDecorators.js +2 -2
  15. package/package.json +1 -1
  16. package/src/{engine/graphics/particles/particular/engine → core/geom/3d/aabb}/MovingBoundingBox.d.ts +3 -3
  17. package/src/core/geom/3d/aabb/MovingBoundingBox.d.ts.map +1 -0
  18. package/src/{engine/graphics/particles/particular/engine → core/geom/3d/aabb}/MovingBoundingBox.js +5 -5
  19. package/src/core/math/lookup/RGBA_LUT_HEATMAP_IR.d.ts +3 -0
  20. package/src/core/math/lookup/RGBA_LUT_HEATMAP_IR.d.ts.map +1 -0
  21. package/src/{engine/graphics/particles/particular/engine/parameter/sample → core/math/lookup}/RGBA_LUT_HEATMAP_IR.js +1 -1
  22. package/src/core/math/lookup/RGB_LUT_INFERNO.d.ts +6 -0
  23. package/src/core/math/lookup/RGB_LUT_INFERNO.d.ts.map +1 -0
  24. package/src/{engine/graphics/particles/particular/engine/parameter/sample → core/math/lookup}/RGB_LUT_INFERNO.js +1 -1
  25. package/src/{engine/graphics/particles/particular/engine/parameter/sample → core/math/lookup}/RGB_LUT_TURBO.d.ts +1 -1
  26. package/src/core/math/lookup/RGB_LUT_TURBO.d.ts.map +1 -0
  27. package/src/{engine/graphics/particles/particular/engine/parameter/sample → core/math/lookup}/RGB_LUT_TURBO.js +1 -1
  28. package/src/{engine/graphics/particles/particular/engine/parameter/sample → core/math/lookup}/RGB_LUT_VIRIDIS.d.ts +1 -1
  29. package/src/core/math/lookup/RGB_LUT_VIRIDIS.d.ts.map +1 -0
  30. package/src/{engine/graphics/particles/particular/engine/parameter/sample → core/math/lookup}/RGB_LUT_VIRIDIS.js +1 -1
  31. package/src/engine/animation/AnimatedActions.d.ts.map +1 -1
  32. package/src/engine/animation/AnimatedActions.js +2 -4
  33. package/src/engine/animation/AnimationUtils.d.ts +1 -17
  34. package/src/engine/animation/AnimationUtils.d.ts.map +1 -1
  35. package/src/engine/animation/AnimationUtils.js +138 -246
  36. package/src/engine/ecs/EntityManager.d.ts +1 -1
  37. package/src/engine/ecs/EntityManager.d.ts.map +1 -1
  38. package/src/engine/ecs/EntityManager.js +2 -2
  39. package/src/engine/ecs/storage/binary/SERIALIZATION_COVERAGE.md +268 -275
  40. package/src/engine/ecs/storage/populateEngineSerializationRegistry.d.ts.map +1 -1
  41. package/src/engine/ecs/storage/populateEngineSerializationRegistry.js +0 -10
  42. package/src/engine/graphics/geometry/distribute_points_on_indexed_triangles.d.ts.map +1 -0
  43. package/src/engine/graphics/{particles/particular/engine/utils → geometry}/distribute_points_on_indexed_triangles.js +3 -3
  44. package/src/engine/graphics/{particles/particular/engine/utils/volume → texture/sampler}/lut_to_sampler.d.ts +1 -1
  45. package/src/engine/graphics/texture/sampler/lut_to_sampler.d.ts.map +1 -0
  46. package/src/engine/graphics/{particles/particular/engine/utils/volume → texture/sampler}/lut_to_sampler.js +1 -1
  47. package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts +0 -3
  48. package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts.map +1 -1
  49. package/src/engine/graphics3/GPUParticleEmitterSystem.js +0 -3
  50. package/src/engine/network/NetworkSession.d.ts +1 -1
  51. package/src/engine/network/NetworkSession.js +1 -1
  52. package/src/engine/physics/cloth/MEASUREMENTS.md +115 -0
  53. package/src/engine/physics/cloth/solver/cloth_contact_find.d.ts.map +1 -1
  54. package/src/engine/physics/cloth/solver/cloth_contact_find.js +2 -0
  55. package/src/engine/physics/cloth/solver/constraint/cloth_contact_row.d.ts +9 -0
  56. package/src/engine/physics/cloth/solver/constraint/cloth_contact_row.d.ts.map +1 -1
  57. package/src/engine/physics/cloth/solver/constraint/cloth_contact_row.js +30 -7
  58. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  59. package/src/engine/sound/ecs/audio/AudioEmitterSystem.d.ts.map +1 -1
  60. package/src/engine/sound/ecs/audio/AudioEmitterSystem.js +12 -2
  61. package/src/engine/sound/ecs/audio/LiveEmitterSet.d.ts +11 -2
  62. package/src/engine/sound/ecs/audio/LiveEmitterSet.d.ts.map +1 -1
  63. package/src/engine/sound/ecs/audio/LiveEmitterSet.js +14 -7
  64. package/src/engine/sound/ecs/audio/SpatialAudioIndex.d.ts +6 -15
  65. package/src/engine/sound/ecs/audio/SpatialAudioIndex.d.ts.map +1 -1
  66. package/src/engine/sound/ecs/audio/SpatialAudioIndex.js +161 -174
  67. package/src/shade/device/mock/SoftwareGPUBuffer.d.ts +5 -3
  68. package/src/shade/device/mock/SoftwareGPUBuffer.d.ts.map +1 -1
  69. package/src/shade/device/mock/SoftwareGPUBuffer.js +32 -3
  70. package/src/shade/device/timing/profile/GPUProfileSession.d.ts +25 -11
  71. package/src/shade/device/timing/profile/GPUProfileSession.d.ts.map +1 -1
  72. package/src/shade/device/timing/profile/GPUProfileSession.js +553 -474
  73. package/src/shade/playground/avboit/main.d.ts.map +1 -1
  74. package/src/shade/playground/avboit/main.js +888 -916
  75. package/src/shade/playground/caldera/main.js +73 -60
  76. package/src/shade/playground/particle_ecs/README.md +13 -7
  77. package/src/shade/playground/particle_ecs/bonfire_effects.d.ts +5 -5
  78. package/src/shade/playground/particle_ecs/bonfire_effects.js +5 -5
  79. package/src/shade/playground/particle_ecs/effects/smoke.json +1859 -1859
  80. package/src/shade/playground/particle_ecs/effects/soot.json +1605 -1605
  81. package/src/shade/playground/particle_system/README.md +1 -2
  82. package/src/shade/playground/profile_hotkey.d.ts.map +1 -1
  83. package/src/shade/playground/profile_hotkey.js +4 -40
  84. package/src/shade/playground/skinned_blas_refit/main.d.ts.map +1 -1
  85. package/src/shade/playground/skinned_blas_refit/main.js +4 -39
  86. package/src/shade/playground/vgeo_runtime/main.js +25 -77
  87. package/src/shade/playground/vgeo_scene/main.js +2231 -2217
  88. package/src/shade/playground/volumetrics_froxel/main.d.ts.map +1 -1
  89. package/src/shade/playground/volumetrics_froxel/main.js +4 -39
  90. package/src/shade/playground/volumetrics_froxel/measure.d.ts.map +1 -1
  91. package/src/shade/playground/volumetrics_froxel/measure.js +2 -4
  92. package/src/shade/renderer/GraphicsContext.d.ts +7 -0
  93. package/src/shade/renderer/GraphicsContext.d.ts.map +1 -1
  94. package/src/shade/renderer/GraphicsContext.js +11 -2
  95. package/src/shade/renderer/Renderer.d.ts +1 -1
  96. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  97. package/src/shade/renderer/Renderer.js +41 -1
  98. package/src/shade/renderer/animation/skinning/GPUMeshSkinningContext.d.ts +8 -0
  99. package/src/shade/renderer/animation/skinning/GPUMeshSkinningContext.d.ts.map +1 -1
  100. package/src/shade/renderer/animation/skinning/GPUMeshSkinningContext.js +21 -0
  101. package/src/shade/renderer/extension/RENDER_EXTENSION_DESIGN.md +1 -1
  102. package/src/shade/renderer/geometry/GPUGeometryManager.d.ts.map +1 -1
  103. package/src/shade/renderer/geometry/GPUGeometryManager.js +9 -10
  104. package/src/shade/renderer/geometry/bvh/GPUGeometryBVHManager.d.ts +5 -0
  105. package/src/shade/renderer/geometry/bvh/GPUGeometryBVHManager.d.ts.map +1 -1
  106. package/src/shade/renderer/geometry/bvh/GPUGeometryBVHManager.js +11 -0
  107. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryPool.d.ts +12 -0
  108. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryPool.d.ts.map +1 -1
  109. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryPool.js +25 -0
  110. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.d.ts +115 -101
  111. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.d.ts.map +1 -1
  112. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.js +282 -140
  113. package/src/shade/renderer/geometry/virtual/runtime/REVIEW_LEDGER.md +30 -0
  114. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN_2026_09_14.md +4 -1
  115. package/src/shade/renderer/geometry/virtual/runtime/SHADOW_PLAN_2026_09_28.md +536 -0
  116. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.d.ts +6 -2
  117. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.d.ts.map +1 -1
  118. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.js +68 -19
  119. package/src/shade/renderer/geometry/virtual/runtime/VGeoPageFetcher.d.ts +2 -1
  120. package/src/shade/renderer/geometry/virtual/runtime/VGeoPageFetcher.d.ts.map +1 -1
  121. package/src/shade/renderer/geometry/virtual/runtime/VGeoPageFetcher.js +17 -1
  122. package/src/shade/renderer/geometry/virtual/runtime/VGeoResidency.d.ts +4 -0
  123. package/src/shade/renderer/geometry/virtual/runtime/VGeoResidency.d.ts.map +1 -1
  124. package/src/shade/renderer/geometry/virtual/runtime/VGeoResidency.js +42 -12
  125. package/src/shade/renderer/geometry/virtual/runtime/VGeoViewCut.d.ts +93 -12
  126. package/src/shade/renderer/geometry/virtual/runtime/VGeoViewCut.d.ts.map +1 -1
  127. package/src/shade/renderer/geometry/virtual/runtime/VGeoViewCut.js +166 -20
  128. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_PARAMS_STRUCT.d.ts.map +1 -1
  129. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_PARAMS_STRUCT.js +30 -7
  130. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.d.ts.map +1 -1
  131. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.js +12 -5
  132. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_claim.d.ts.map +1 -1
  133. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_claim.js +2 -2
  134. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_collect.d.ts.map +1 -1
  135. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_collect.js +23 -13
  136. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_queue_remainder.d.ts.map +1 -1
  137. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_queue_remainder.js +1 -2
  138. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_seed.d.ts.map +1 -1
  139. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_seed.js +1 -2
  140. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.d.ts.map +1 -1
  141. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.js +10 -6
  142. package/src/shade/renderer/geometry/virtual/runtime/cut/vgeo_cut_feedback_layout.d.ts +45 -17
  143. package/src/shade/renderer/geometry/virtual/runtime/cut/vgeo_cut_feedback_layout.d.ts.map +1 -1
  144. package/src/shade/renderer/geometry/virtual/runtime/cut/vgeo_cut_feedback_layout.js +61 -19
  145. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_if_enabled.d.ts +8 -11
  146. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_if_enabled.d.ts.map +1 -1
  147. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_if_enabled.js +9 -16
  148. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_instances_to_meshlets.d.ts.map +1 -1
  149. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_instances_to_meshlets.js +25 -10
  150. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_FEEDBACK_OFFSET.js +10 -8
  151. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_INSTANCE_OFFSET.d.ts +1 -0
  152. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_INSTANCE_OFFSET.js +19 -7
  153. package/src/shade/renderer/geometry/virtual/runtime/make_vgeo_page_worker.d.ts +4 -2
  154. package/src/shade/renderer/geometry/virtual/runtime/make_vgeo_page_worker.d.ts.map +1 -1
  155. package/src/shade/renderer/geometry/virtual/runtime/make_vgeo_page_worker.js +4 -2
  156. package/src/shade/renderer/global_illumination/brick4/Brick4LightMap.d.ts +4 -0
  157. package/src/shade/renderer/global_illumination/brick4/Brick4LightMap.d.ts.map +1 -1
  158. package/src/shade/renderer/global_illumination/brick4/Brick4LightMap.js +7 -0
  159. package/src/shade/renderer/global_illumination/lpv/GPULightProbeVolume.d.ts.map +1 -1
  160. package/src/shade/renderer/global_illumination/lpv/GPULightProbeVolume.js +1 -0
  161. package/src/shade/renderer/light/GPULightCollection.d.ts +9 -1
  162. package/src/shade/renderer/light/GPULightCollection.d.ts.map +1 -1
  163. package/src/shade/renderer/light/GPULightCollection.js +41 -0
  164. package/src/shade/renderer/light/environment/shader_project_environment_sh.d.ts +37 -0
  165. package/src/shade/renderer/light/environment/shader_project_environment_sh.d.ts.map +1 -0
  166. package/src/shade/renderer/light/environment/shader_project_environment_sh.js +105 -0
  167. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  168. package/src/shade/renderer/material/GPUMaterialManager.d.ts.map +1 -1
  169. package/src/shade/renderer/material/GPUMaterialManager.js +2 -0
  170. package/src/shade/renderer/particles/DESIGN.md +69 -18
  171. package/src/shade/renderer/particles/GPUParticleSystem.d.ts +7 -1
  172. package/src/shade/renderer/particles/GPUParticleSystem.d.ts.map +1 -1
  173. package/src/shade/renderer/particles/GPUParticleSystem.js +1247 -1244
  174. package/src/shade/renderer/particles/ParticleConstants.d.ts +1 -3
  175. package/src/shade/renderer/particles/ParticleConstants.d.ts.map +1 -1
  176. package/src/shade/renderer/particles/ParticleConstants.js +1 -3
  177. package/src/shade/renderer/particles/graph_particles_avboit.d.ts +7 -1
  178. package/src/shade/renderer/particles/graph_particles_avboit.d.ts.map +1 -1
  179. package/src/shade/renderer/particles/graph_particles_avboit.js +9 -0
  180. package/src/shade/renderer/particles/shaders/chunk_particle_billboard_vertex.d.ts +3 -4
  181. package/src/shade/renderer/particles/shaders/chunk_particle_billboard_vertex.d.ts.map +1 -1
  182. package/src/shade/renderer/particles/shaders/chunk_particle_billboard_vertex.js +7 -6
  183. package/src/shade/renderer/particles/shaders/chunk_particle_lighting.d.ts +15 -9
  184. package/src/shade/renderer/particles/shaders/chunk_particle_lighting.d.ts.map +1 -1
  185. package/src/shade/renderer/particles/shaders/chunk_particle_lighting.js +29 -11
  186. package/src/shade/renderer/particles/shaders/chunk_particle_lighting_model.d.ts +24 -0
  187. package/src/shade/renderer/particles/shaders/chunk_particle_lighting_model.d.ts.map +1 -0
  188. package/src/shade/renderer/particles/shaders/chunk_particle_lighting_model.js +45 -0
  189. package/src/shade/renderer/particles/shaders/chunk_particle_render_math.d.ts +3 -0
  190. package/src/shade/renderer/particles/shaders/chunk_particle_render_math.d.ts.map +1 -1
  191. package/src/shade/renderer/particles/shaders/chunk_particle_render_math.js +38 -0
  192. package/src/shade/renderer/particles/shaders/shader_particle_avboit_draw.d.ts.map +1 -1
  193. package/src/shade/renderer/particles/shaders/shader_particle_avboit_draw.js +190 -185
  194. package/src/shade/renderer/rasterize/bucket/graph_meshlets_rasterize_by_bucket.d.ts +45 -0
  195. package/src/shade/renderer/rasterize/bucket/graph_meshlets_rasterize_by_bucket.d.ts.map +1 -0
  196. package/src/shade/renderer/rasterize/bucket/graph_meshlets_rasterize_by_bucket.js +99 -0
  197. package/src/shade/renderer/rasterize/bucket/rasterization_opaque_buckets.d.ts +4 -4
  198. package/src/shade/renderer/rasterize/bucket/rasterization_opaque_buckets.js +36 -36
  199. package/src/shade/renderer/rasterize/bucket/shader_gather_bucket.d.ts +15 -0
  200. package/src/shade/renderer/rasterize/bucket/shader_gather_bucket.d.ts.map +1 -0
  201. package/src/shade/renderer/rasterize/bucket/shader_gather_bucket.js +103 -0
  202. package/src/shade/renderer/rasterize/expand/GPUCollectionLimits.d.ts +3 -0
  203. package/src/shade/renderer/rasterize/expand/GPUCollectionLimits.d.ts.map +1 -1
  204. package/src/shade/renderer/rasterize/expand/GPUCollectionLimits.js +6 -1
  205. package/src/shade/renderer/rasterize/expand/bucket/meshlet/graph_extract_meshlets_from_bucket.d.ts +26 -0
  206. package/src/shade/renderer/rasterize/expand/bucket/meshlet/graph_extract_meshlets_from_bucket.d.ts.map +1 -0
  207. package/src/shade/renderer/rasterize/expand/bucket/meshlet/graph_extract_meshlets_from_bucket.js +91 -0
  208. package/src/shade/renderer/rasterize/fast/graph_gather_meshes_from_buckets.d.ts +22 -0
  209. package/src/shade/renderer/rasterize/fast/graph_gather_meshes_from_buckets.d.ts.map +1 -0
  210. package/src/shade/renderer/rasterize/fast/graph_gather_meshes_from_buckets.js +102 -0
  211. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.d.ts +2 -1
  212. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.d.ts.map +1 -1
  213. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.js +28 -24
  214. package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.d.ts +8 -0
  215. package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.d.ts.map +1 -1
  216. package/src/shade/renderer/rasterize/native/avboit/AVBOITSideChannel.js +5 -0
  217. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.d.ts +8 -1
  218. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.d.ts.map +1 -1
  219. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.js +9 -0
  220. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.d.ts.map +1 -1
  221. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.js +32 -28
  222. package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.d.ts.map +1 -1
  223. package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.js +9 -105
  224. package/src/shade/renderer/scene/GPUInstancesAccelerationStructure.d.ts +5 -0
  225. package/src/shade/renderer/scene/GPUInstancesAccelerationStructure.d.ts.map +1 -1
  226. package/src/shade/renderer/scene/GPUInstancesAccelerationStructure.js +11 -0
  227. package/src/shade/renderer/scene/GPUSceneContext.d.ts +11 -3
  228. package/src/shade/renderer/scene/GPUSceneContext.d.ts.map +1 -1
  229. package/src/shade/renderer/scene/GPUSceneContext.js +93 -21
  230. package/src/shade/renderer/scene/rows/GPUSceneRows.d.ts +4 -1
  231. package/src/shade/renderer/scene/rows/GPUSceneRows.d.ts.map +1 -1
  232. package/src/shade/renderer/scene/rows/GPUSceneRows.js +5 -23
  233. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.d.ts.map +1 -1
  234. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.js +63 -0
  235. package/src/shade/renderer/shadow/map/SHADOWMAP_VGEO_TAU_SCALE.d.ts +12 -0
  236. package/src/shade/renderer/shadow/map/SHADOWMAP_VGEO_TAU_SCALE.d.ts.map +1 -0
  237. package/src/shade/renderer/shadow/map/SHADOWMAP_VGEO_TAU_SCALE.js +11 -0
  238. package/src/shade/renderer/shadow/map/Shadowmap.d.ts +15 -0
  239. package/src/shade/renderer/shadow/map/Shadowmap.d.ts.map +1 -1
  240. package/src/shade/renderer/shadow/map/Shadowmap.js +17 -0
  241. package/src/shade/renderer/shadow/map/shadowmap_caster_detail_pending.d.ts +25 -0
  242. package/src/shade/renderer/shadow/map/shadowmap_caster_detail_pending.d.ts.map +1 -0
  243. package/src/shade/renderer/shadow/map/shadowmap_caster_detail_pending.js +22 -0
  244. package/src/shade/renderer/shadow/map/shadowmap_update_score.d.ts.map +1 -1
  245. package/src/shade/renderer/shadow/map/shadowmap_update_score.js +5 -2
  246. package/src/shade/renderer/texture/TextureManager.d.ts +2 -1
  247. package/src/shade/renderer/texture/TextureManager.d.ts.map +1 -1
  248. package/src/shade/renderer/texture/TextureManager.js +4 -1
  249. package/src/shade/renderer/texture/mipmaps/WebGPUMipmapGenerator.js +1 -1
  250. package/src/shade/renderer/util/Uint32BufferSamplingStats.d.ts +9 -1
  251. package/src/shade/renderer/util/Uint32BufferSamplingStats.d.ts.map +1 -1
  252. package/src/shade/renderer/util/Uint32BufferSamplingStats.js +55 -5
  253. package/src/shade/renderer/view/ViewManager.d.ts +7 -1
  254. package/src/shade/renderer/view/ViewManager.d.ts.map +1 -1
  255. package/src/shade/renderer/view/ViewManager.js +17 -1
  256. package/src/shade/renderer/volumetrics/GPUSceneVolumetrics.d.ts.map +1 -1
  257. package/src/shade/renderer/volumetrics/GPUSceneVolumetrics.js +1 -0
  258. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts +13 -3
  259. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.d.ts.map +1 -1
  260. package/src/shade/renderer/volumetrics/GPUViewVolumetrics.js +9 -1
  261. package/src/shade/renderer/volumetrics/chunk_volumetrics_irradiance_sh.d.ts +39 -0
  262. package/src/shade/renderer/volumetrics/chunk_volumetrics_irradiance_sh.d.ts.map +1 -0
  263. package/src/shade/renderer/volumetrics/chunk_volumetrics_irradiance_sh.js +107 -0
  264. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_froxel_centre_ws.d.ts +15 -0
  265. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_froxel_centre_ws.d.ts.map +1 -0
  266. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_froxel_centre_ws.js +44 -0
  267. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_gi.d.ts +19 -0
  268. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_gi.d.ts.map +1 -0
  269. package/src/shade/renderer/volumetrics/gi/chunk_volumetrics_gi.js +48 -0
  270. package/src/shade/renderer/volumetrics/gi/graph_build_froxel_gi.d.ts +29 -0
  271. package/src/shade/renderer/volumetrics/gi/graph_build_froxel_gi.d.ts.map +1 -0
  272. package/src/shade/renderer/volumetrics/gi/graph_build_froxel_gi.js +90 -0
  273. package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_brick4.d.ts +10 -0
  274. package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_brick4.d.ts.map +1 -0
  275. package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_brick4.js +55 -0
  276. package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_ibl.d.ts +10 -0
  277. package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_ibl.d.ts.map +1 -0
  278. package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_ibl.js +47 -0
  279. package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_lpv.d.ts +10 -0
  280. package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_lpv.d.ts.map +1 -0
  281. package/src/shade/renderer/volumetrics/gi/shader_volumetrics_gi_lpv.js +63 -0
  282. package/src/shade/renderer/volumetrics/gi/volumetrics_gi_resources.d.ts +17 -0
  283. package/src/shade/renderer/volumetrics/gi/volumetrics_gi_resources.d.ts.map +1 -0
  284. package/src/shade/renderer/volumetrics/gi/volumetrics_gi_resources.js +26 -0
  285. package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts +8 -1
  286. package/src/shade/renderer/volumetrics/graph_build_volumetrics.d.ts.map +1 -1
  287. package/src/shade/renderer/volumetrics/graph_build_volumetrics.js +50 -2
  288. package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.d.ts.map +1 -1
  289. package/src/shade/renderer/volumetrics/shader_volumetrics_build_lighting.js +488 -436
  290. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.d.ts +1 -1
  291. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.d.ts.map +1 -1
  292. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +254 -63
  293. package/editor/ecs/component/editors/ecs/ParameterLookupTableEditor.d.ts +0 -20
  294. package/editor/ecs/component/editors/ecs/ParameterLookupTableEditor.d.ts.map +0 -1
  295. package/editor/ecs/component/editors/ecs/ParameterLookupTableEditor.js +0 -35
  296. package/editor/ecs/component/editors/ecs/ParameterTrackEditor.d.ts +0 -12
  297. package/editor/ecs/component/editors/ecs/ParameterTrackEditor.d.ts.map +0 -1
  298. package/editor/ecs/component/editors/ecs/ParameterTrackEditor.js +0 -17
  299. package/editor/ecs/component/editors/ecs/ParameterTrackSetEditor.d.ts +0 -14
  300. package/editor/ecs/component/editors/ecs/ParameterTrackSetEditor.d.ts.map +0 -1
  301. package/editor/ecs/component/editors/ecs/ParameterTrackSetEditor.js +0 -18
  302. package/editor/ecs/component/editors/ecs/ParticleEmitterEditor.d.ts +0 -52
  303. package/editor/ecs/component/editors/ecs/ParticleEmitterEditor.d.ts.map +0 -1
  304. package/editor/ecs/component/editors/ecs/ParticleEmitterEditor.js +0 -58
  305. package/editor/ecs/component/editors/ecs/ParticleEmitterLayerEditor.d.ts +0 -47
  306. package/editor/ecs/component/editors/ecs/ParticleEmitterLayerEditor.d.ts.map +0 -1
  307. package/editor/ecs/component/editors/ecs/ParticleEmitterLayerEditor.js +0 -58
  308. package/editor/ecs/component/editors/ecs/SimulationStepDefinitionEditor.d.ts +0 -16
  309. package/editor/ecs/component/editors/ecs/SimulationStepDefinitionEditor.d.ts.map +0 -1
  310. package/editor/ecs/component/editors/ecs/SimulationStepDefinitionEditor.js +0 -21
  311. package/editor/particles/rebuildParticleEmitter.d.ts +0 -11
  312. package/editor/particles/rebuildParticleEmitter.d.ts.map +0 -1
  313. package/editor/particles/rebuildParticleEmitter.js +0 -31
  314. package/editor/process/ParticleEmitterRebuildProcess.d.ts +0 -66
  315. package/editor/process/ParticleEmitterRebuildProcess.d.ts.map +0 -1
  316. package/editor/process/ParticleEmitterRebuildProcess.js +0 -217
  317. package/editor/process/symbolic/ParticleEmitterSymbolicDisplay.d.ts +0 -18
  318. package/editor/process/symbolic/ParticleEmitterSymbolicDisplay.d.ts.map +0 -1
  319. package/editor/process/symbolic/ParticleEmitterSymbolicDisplay.js +0 -140
  320. package/src/engine/animation/removeEntityGenericEffect.d.ts +0 -18
  321. package/src/engine/animation/removeEntityGenericEffect.d.ts.map +0 -1
  322. package/src/engine/animation/removeEntityGenericEffect.js +0 -160
  323. package/src/engine/animation/removeEntityWithMeshParticlesEffect.d.ts +0 -16
  324. package/src/engine/animation/removeEntityWithMeshParticlesEffect.d.ts.map +0 -1
  325. package/src/engine/animation/removeEntityWithMeshParticlesEffect.js +0 -194
  326. package/src/engine/graphics/ecs/sprite/Sprite.d.ts +0 -15
  327. package/src/engine/graphics/ecs/sprite/Sprite.d.ts.map +0 -1
  328. package/src/engine/graphics/ecs/sprite/Sprite.js +0 -14
  329. package/src/engine/graphics/ecs/sprite/SpriteSystemPE.d.ts +0 -11
  330. package/src/engine/graphics/ecs/sprite/SpriteSystemPE.d.ts.map +0 -1
  331. package/src/engine/graphics/ecs/sprite/SpriteSystemPE.js +0 -144
  332. package/src/engine/graphics/particles/particular/engine/MovingBoundingBox.d.ts.map +0 -1
  333. package/src/engine/graphics/particles/particular/engine/ParticularEngine.d.ts +0 -61
  334. package/src/engine/graphics/particles/particular/engine/ParticularEngine.d.ts.map +0 -1
  335. package/src/engine/graphics/particles/particular/engine/ParticularEngine.js +0 -229
  336. package/src/engine/graphics/particles/particular/engine/emitter/EmissionFromType.d.ts +0 -6
  337. package/src/engine/graphics/particles/particular/engine/emitter/EmissionFromType.d.ts.map +0 -1
  338. package/src/engine/graphics/particles/particular/engine/emitter/EmissionFromType.js +0 -8
  339. package/src/engine/graphics/particles/particular/engine/emitter/EmissionShapeType.d.ts +0 -7
  340. package/src/engine/graphics/particles/particular/engine/emitter/EmissionShapeType.d.ts.map +0 -1
  341. package/src/engine/graphics/particles/particular/engine/emitter/EmissionShapeType.js +0 -9
  342. package/src/engine/graphics/particles/particular/engine/emitter/PARTICLE_ATTRIBUTES.d.ts +0 -13
  343. package/src/engine/graphics/particles/particular/engine/emitter/PARTICLE_ATTRIBUTES.d.ts.map +0 -1
  344. package/src/engine/graphics/particles/particular/engine/emitter/PARTICLE_ATTRIBUTES.js +0 -12
  345. package/src/engine/graphics/particles/particular/engine/emitter/PARTICULAR_PARTICLE_SPECIFICATION.d.ts +0 -3
  346. package/src/engine/graphics/particles/particular/engine/emitter/PARTICULAR_PARTICLE_SPECIFICATION.d.ts.map +0 -1
  347. package/src/engine/graphics/particles/particular/engine/emitter/PARTICULAR_PARTICLE_SPECIFICATION.js +0 -84
  348. package/src/engine/graphics/particles/particular/engine/emitter/ParticleEmitter.d.ts +0 -285
  349. package/src/engine/graphics/particles/particular/engine/emitter/ParticleEmitter.d.ts.map +0 -1
  350. package/src/engine/graphics/particles/particular/engine/emitter/ParticleEmitter.js +0 -1201
  351. package/src/engine/graphics/particles/particular/engine/emitter/ParticleEmitterFlag.d.ts +0 -19
  352. package/src/engine/graphics/particles/particular/engine/emitter/ParticleEmitterFlag.d.ts.map +0 -1
  353. package/src/engine/graphics/particles/particular/engine/emitter/ParticleEmitterFlag.js +0 -67
  354. package/src/engine/graphics/particles/particular/engine/emitter/ParticleLayer.d.ts +0 -197
  355. package/src/engine/graphics/particles/particular/engine/emitter/ParticleLayer.d.ts.map +0 -1
  356. package/src/engine/graphics/particles/particular/engine/emitter/ParticleLayer.js +0 -398
  357. package/src/engine/graphics/particles/particular/engine/emitter/ParticleParameters.d.ts +0 -5
  358. package/src/engine/graphics/particles/particular/engine/emitter/ParticleParameters.d.ts.map +0 -1
  359. package/src/engine/graphics/particles/particular/engine/emitter/ParticleParameters.js +0 -4
  360. package/src/engine/graphics/particles/particular/engine/emitter/ParticlePool.d.ts +0 -162
  361. package/src/engine/graphics/particles/particular/engine/emitter/ParticlePool.d.ts.map +0 -1
  362. package/src/engine/graphics/particles/particular/engine/emitter/ParticlePool.js +0 -472
  363. package/src/engine/graphics/particles/particular/engine/emitter/computeEmissionFunction.d.ts +0 -10
  364. package/src/engine/graphics/particles/particular/engine/emitter/computeEmissionFunction.d.ts.map +0 -1
  365. package/src/engine/graphics/particles/particular/engine/emitter/computeEmissionFunction.js +0 -37
  366. package/src/engine/graphics/particles/particular/engine/emitter/distance_to_camera.d.ts +0 -9
  367. package/src/engine/graphics/particles/particular/engine/emitter/distance_to_camera.d.ts.map +0 -1
  368. package/src/engine/graphics/particles/particular/engine/emitter/distance_to_camera.js +0 -25
  369. package/src/engine/graphics/particles/particular/engine/emitter/serde/ParameterLookupTableSerializationAdapter.d.ts +0 -19
  370. package/src/engine/graphics/particles/particular/engine/emitter/serde/ParameterLookupTableSerializationAdapter.d.ts.map +0 -1
  371. package/src/engine/graphics/particles/particular/engine/emitter/serde/ParameterLookupTableSerializationAdapter.js +0 -28
  372. package/src/engine/graphics/particles/particular/engine/emitter/serde/ParticleEmitterSerializationAdapter.d.ts +0 -19
  373. package/src/engine/graphics/particles/particular/engine/emitter/serde/ParticleEmitterSerializationAdapter.d.ts.map +0 -1
  374. package/src/engine/graphics/particles/particular/engine/emitter/serde/ParticleEmitterSerializationAdapter.js +0 -162
  375. package/src/engine/graphics/particles/particular/engine/emitter/serde/ParticleEmitterSerializationUpgrader_0_1.d.ts +0 -5
  376. package/src/engine/graphics/particles/particular/engine/emitter/serde/ParticleEmitterSerializationUpgrader_0_1.d.ts.map +0 -1
  377. package/src/engine/graphics/particles/particular/engine/emitter/serde/ParticleEmitterSerializationUpgrader_0_1.js +0 -103
  378. package/src/engine/graphics/particles/particular/engine/emitter/write_particle_patch_uv.d.ts +0 -8
  379. package/src/engine/graphics/particles/particular/engine/emitter/write_particle_patch_uv.d.ts.map +0 -1
  380. package/src/engine/graphics/particles/particular/engine/emitter/write_particle_patch_uv.js +0 -28
  381. package/src/engine/graphics/particles/particular/engine/parameter/ParameterLookupTable.d.ts +0 -2
  382. package/src/engine/graphics/particles/particular/engine/parameter/ParameterLookupTable.d.ts.map +0 -1
  383. package/src/engine/graphics/particles/particular/engine/parameter/ParameterLookupTable.js +0 -2
  384. package/src/engine/graphics/particles/particular/engine/parameter/ParameterLookupTableFlags.d.ts +0 -2
  385. package/src/engine/graphics/particles/particular/engine/parameter/ParameterLookupTableFlags.d.ts.map +0 -1
  386. package/src/engine/graphics/particles/particular/engine/parameter/ParameterLookupTableFlags.js +0 -2
  387. package/src/engine/graphics/particles/particular/engine/parameter/ParameterSet.d.ts +0 -60
  388. package/src/engine/graphics/particles/particular/engine/parameter/ParameterSet.d.ts.map +0 -1
  389. package/src/engine/graphics/particles/particular/engine/parameter/ParameterSet.js +0 -128
  390. package/src/engine/graphics/particles/particular/engine/parameter/ParameterTrack.d.ts +0 -47
  391. package/src/engine/graphics/particles/particular/engine/parameter/ParameterTrack.d.ts.map +0 -1
  392. package/src/engine/graphics/particles/particular/engine/parameter/ParameterTrack.js +0 -77
  393. package/src/engine/graphics/particles/particular/engine/parameter/ParameterTrackSet.d.ts +0 -45
  394. package/src/engine/graphics/particles/particular/engine/parameter/ParameterTrackSet.d.ts.map +0 -1
  395. package/src/engine/graphics/particles/particular/engine/parameter/ParameterTrackSet.js +0 -93
  396. package/src/engine/graphics/particles/particular/engine/parameter/ParticleParameter.d.ts +0 -95
  397. package/src/engine/graphics/particles/particular/engine/parameter/ParticleParameter.d.ts.map +0 -1
  398. package/src/engine/graphics/particles/particular/engine/parameter/ParticleParameter.js +0 -239
  399. package/src/engine/graphics/particles/particular/engine/parameter/sample/RGBA_LUT_HEATMAP_IR.d.ts +0 -3
  400. package/src/engine/graphics/particles/particular/engine/parameter/sample/RGBA_LUT_HEATMAP_IR.d.ts.map +0 -1
  401. package/src/engine/graphics/particles/particular/engine/parameter/sample/RGB_LUT_INFERNO.d.ts +0 -6
  402. package/src/engine/graphics/particles/particular/engine/parameter/sample/RGB_LUT_INFERNO.d.ts.map +0 -1
  403. package/src/engine/graphics/particles/particular/engine/parameter/sample/RGB_LUT_TURBO.d.ts.map +0 -1
  404. package/src/engine/graphics/particles/particular/engine/parameter/sample/RGB_LUT_VIRIDIS.d.ts.map +0 -1
  405. package/src/engine/graphics/particles/particular/engine/renderers/ParticleRenderer.d.ts +0 -13
  406. package/src/engine/graphics/particles/particular/engine/renderers/ParticleRenderer.d.ts.map +0 -1
  407. package/src/engine/graphics/particles/particular/engine/renderers/ParticleRenderer.js +0 -28
  408. package/src/engine/graphics/particles/particular/engine/renderers/billboard/BILLBOARD_PARTICLE_SPECIFICATION.d.ts +0 -9
  409. package/src/engine/graphics/particles/particular/engine/renderers/billboard/BILLBOARD_PARTICLE_SPECIFICATION.d.ts.map +0 -1
  410. package/src/engine/graphics/particles/particular/engine/renderers/billboard/BILLBOARD_PARTICLE_SPECIFICATION.js +0 -24
  411. package/src/engine/graphics/particles/particular/engine/renderers/billboard/SoftBillboardParticlePool.d.ts +0 -13
  412. package/src/engine/graphics/particles/particular/engine/renderers/billboard/SoftBillboardParticlePool.d.ts.map +0 -1
  413. package/src/engine/graphics/particles/particular/engine/renderers/billboard/SoftBillboardParticlePool.js +0 -27
  414. package/src/engine/graphics/particles/particular/engine/renderers/billboard/SoftBillboardParticleRenderer.d.ts +0 -33
  415. package/src/engine/graphics/particles/particular/engine/renderers/billboard/SoftBillboardParticleRenderer.d.ts.map +0 -1
  416. package/src/engine/graphics/particles/particular/engine/renderers/billboard/SoftBillboardParticleRenderer.js +0 -80
  417. package/src/engine/graphics/particles/particular/engine/shader/ShaderManager.d.ts +0 -49
  418. package/src/engine/graphics/particles/particular/engine/shader/ShaderManager.d.ts.map +0 -1
  419. package/src/engine/graphics/particles/particular/engine/shader/ShaderManager.js +0 -135
  420. package/src/engine/graphics/particles/particular/engine/shader/debugAtlas.d.ts +0 -7
  421. package/src/engine/graphics/particles/particular/engine/shader/debugAtlas.d.ts.map +0 -1
  422. package/src/engine/graphics/particles/particular/engine/shader/debugAtlas.js +0 -14
  423. package/src/engine/graphics/particles/particular/engine/simulator/AbstractSimulationStep.d.ts +0 -41
  424. package/src/engine/graphics/particles/particular/engine/simulator/AbstractSimulationStep.d.ts.map +0 -1
  425. package/src/engine/graphics/particles/particular/engine/simulator/AbstractSimulationStep.js +0 -59
  426. package/src/engine/graphics/particles/particular/engine/simulator/SimulationStepApplyForce.d.ts +0 -4
  427. package/src/engine/graphics/particles/particular/engine/simulator/SimulationStepApplyForce.d.ts.map +0 -1
  428. package/src/engine/graphics/particles/particular/engine/simulator/SimulationStepApplyForce.js +0 -56
  429. package/src/engine/graphics/particles/particular/engine/simulator/SimulationStepCurlNoiseAcceleration.d.ts +0 -4
  430. package/src/engine/graphics/particles/particular/engine/simulator/SimulationStepCurlNoiseAcceleration.d.ts.map +0 -1
  431. package/src/engine/graphics/particles/particular/engine/simulator/SimulationStepCurlNoiseAcceleration.js +0 -84
  432. package/src/engine/graphics/particles/particular/engine/simulator/SimulationStepCurlNoiseVelocity.d.ts +0 -4
  433. package/src/engine/graphics/particles/particular/engine/simulator/SimulationStepCurlNoiseVelocity.d.ts.map +0 -1
  434. package/src/engine/graphics/particles/particular/engine/simulator/SimulationStepCurlNoiseVelocity.js +0 -80
  435. package/src/engine/graphics/particles/particular/engine/simulator/SimulationStepDefinition.d.ts +0 -29
  436. package/src/engine/graphics/particles/particular/engine/simulator/SimulationStepDefinition.d.ts.map +0 -1
  437. package/src/engine/graphics/particles/particular/engine/simulator/SimulationStepDefinition.js +0 -43
  438. package/src/engine/graphics/particles/particular/engine/simulator/SimulationStepFixedPhysics.d.ts +0 -5
  439. package/src/engine/graphics/particles/particular/engine/simulator/SimulationStepFixedPhysics.d.ts.map +0 -1
  440. package/src/engine/graphics/particles/particular/engine/simulator/SimulationStepFixedPhysics.js +0 -104
  441. package/src/engine/graphics/particles/particular/engine/simulator/SimulationStepType.d.ts +0 -9
  442. package/src/engine/graphics/particles/particular/engine/simulator/SimulationStepType.d.ts.map +0 -1
  443. package/src/engine/graphics/particles/particular/engine/simulator/SimulationStepType.js +0 -11
  444. package/src/engine/graphics/particles/particular/engine/simulator/update_parameters.d.ts +0 -9
  445. package/src/engine/graphics/particles/particular/engine/simulator/update_parameters.d.ts.map +0 -1
  446. package/src/engine/graphics/particles/particular/engine/simulator/update_parameters.js +0 -64
  447. package/src/engine/graphics/particles/particular/engine/utils/distributeParticlesOnObject3D.d.ts +0 -9
  448. package/src/engine/graphics/particles/particular/engine/utils/distributeParticlesOnObject3D.d.ts.map +0 -1
  449. package/src/engine/graphics/particles/particular/engine/utils/distributeParticlesOnObject3D.js +0 -172
  450. package/src/engine/graphics/particles/particular/engine/utils/distribute_points_on_indexed_triangles.d.ts.map +0 -1
  451. package/src/engine/graphics/particles/particular/engine/utils/distrubuteParticlesOnMesh.d.ts +0 -20
  452. package/src/engine/graphics/particles/particular/engine/utils/distrubuteParticlesOnMesh.d.ts.map +0 -1
  453. package/src/engine/graphics/particles/particular/engine/utils/distrubuteParticlesOnMesh.js +0 -84
  454. package/src/engine/graphics/particles/particular/engine/utils/volume/AttributeValue.d.ts +0 -81
  455. package/src/engine/graphics/particles/particular/engine/utils/volume/AttributeValue.d.ts.map +0 -1
  456. package/src/engine/graphics/particles/particular/engine/utils/volume/AttributeValue.js +0 -200
  457. package/src/engine/graphics/particles/particular/engine/utils/volume/SamplingFunctionKind.d.ts +0 -6
  458. package/src/engine/graphics/particles/particular/engine/utils/volume/SamplingFunctionKind.d.ts.map +0 -1
  459. package/src/engine/graphics/particles/particular/engine/utils/volume/SamplingFunctionKind.js +0 -8
  460. package/src/engine/graphics/particles/particular/engine/utils/volume/lut_to_sampler.d.ts.map +0 -1
  461. package/src/engine/graphics/particles/particular/group/Operation.d.ts +0 -20
  462. package/src/engine/graphics/particles/particular/group/Operation.d.ts.map +0 -1
  463. package/src/engine/graphics/particles/particular/group/Operation.js +0 -20
  464. package/src/engine/graphics/particles/particular/group/OperationType.d.ts +0 -7
  465. package/src/engine/graphics/particles/particular/group/OperationType.d.ts.map +0 -1
  466. package/src/engine/graphics/particles/particular/group/OperationType.js +0 -9
  467. package/src/engine/graphics/particles/particular/group/ParticleAttribute.d.ts +0 -27
  468. package/src/engine/graphics/particles/particular/group/ParticleAttribute.d.ts.map +0 -1
  469. package/src/engine/graphics/particles/particular/group/ParticleAttribute.js +0 -27
  470. package/src/engine/graphics/particles/particular/group/ParticleAttributeType.d.ts +0 -8
  471. package/src/engine/graphics/particles/particular/group/ParticleAttributeType.d.ts.map +0 -1
  472. package/src/engine/graphics/particles/particular/group/ParticleAttributeType.js +0 -11
  473. package/src/engine/graphics/particles/particular/group/ParticleDataType.d.ts +0 -7
  474. package/src/engine/graphics/particles/particular/group/ParticleDataType.d.ts.map +0 -1
  475. package/src/engine/graphics/particles/particular/group/ParticleDataType.js +0 -10
  476. package/src/engine/graphics/particles/particular/group/ParticleSpecification.d.ts +0 -24
  477. package/src/engine/graphics/particles/particular/group/ParticleSpecification.d.ts.map +0 -1
  478. package/src/engine/graphics/particles/particular/group/ParticleSpecification.js +0 -48
  479. package/src/engine/graphics/particles/particular/group/itemSizeFromAttributeType.d.ts +0 -7
  480. package/src/engine/graphics/particles/particular/group/itemSizeFromAttributeType.d.ts.map +0 -1
  481. package/src/engine/graphics/particles/particular/group/itemSizeFromAttributeType.js +0 -20
  482. package/src/engine/graphics/particles/particular/group/optimizeCommandQueue.d.ts +0 -6
  483. package/src/engine/graphics/particles/particular/group/optimizeCommandQueue.d.ts.map +0 -1
  484. package/src/engine/graphics/particles/particular/group/optimizeCommandQueue.js +0 -162
  485. package/src/engine/graphics/particles/particular/group/typedArrayConstructorFromDataType.d.ts +0 -8
  486. package/src/engine/graphics/particles/particular/group/typedArrayConstructorFromDataType.d.ts.map +0 -1
  487. package/src/engine/graphics/particles/particular/group/typedArrayConstructorFromDataType.js +0 -19
  488. package/src/engine/graphics3/ParticleEmitterSystem.d.ts +0 -77
  489. package/src/engine/graphics3/ParticleEmitterSystem.d.ts.map +0 -1
  490. package/src/engine/graphics3/ParticleEmitterSystem.js +0 -307
  491. package/src/engine/graphics3/ParticleEmitterSystem3.d.ts +0 -10
  492. package/src/engine/graphics3/ParticleEmitterSystem3.d.ts.map +0 -1
  493. package/src/engine/graphics3/ParticleEmitterSystem3.js +0 -10
  494. package/src/engine/graphics3/particles/GPUParticularRenderer.d.ts +0 -61
  495. package/src/engine/graphics3/particles/GPUParticularRenderer.d.ts.map +0 -1
  496. package/src/engine/graphics3/particles/GPUParticularRenderer.js +0 -627
  497. package/src/engine/graphics3/particles/particle_gpu_records.d.ts +0 -85
  498. package/src/engine/graphics3/particles/particle_gpu_records.d.ts.map +0 -1
  499. package/src/engine/graphics3/particles/particle_gpu_records.js +0 -227
  500. package/src/shade/renderer/particles/shaders/shader_particle_render.d.ts +0 -35
  501. package/src/shade/renderer/particles/shaders/shader_particle_render.d.ts.map +0 -1
  502. package/src/shade/renderer/particles/shaders/shader_particle_render.js +0 -135
  503. /package/src/engine/graphics/{particles/particular/engine/utils → geometry}/distribute_points_on_indexed_triangles.d.ts +0 -0
@@ -1,1244 +1,1247 @@
1
- import { assert } from "../../../core/assert.js";
2
- import { ShadeGPUCommandContext } from "../../device/ShadeGPUCommandContext.js";
3
- import { BufferResourceDescriptor } from "../../device/graph/BufferResourceDescriptor.js";
4
- import { warn_limited } from "../../util/warn_limited.js";
5
- import { hash_mix2 } from "../../../core/math/hash/hash_mix2.js";
6
- import {
7
- PARTICLE_EMITTER_TICK_WORKGROUP_SIZE,
8
- PARTICLE_RECORD_WORD_COUNT,
9
- PARTICLE_SIMULATE_WORKGROUP_SIZE,
10
- PARTICLE_SPAWN_COMMAND_WORDS,
11
- PARTICLE_VM_FAST_REGISTER_SLOTS,
12
- PARTICLE_WARMUP_EMITTER_JITTER,
13
- PARTICLE_WARMUP_QUEUE_WORDS,
14
- } from "./ParticleConstants.js";
15
- import {
16
- PARTICLE_BOUNDS_WORDS,
17
- PARTICLE_COUNTER,
18
- PARTICLE_COUNTER_COUNT,
19
- particle_bounds_empty_words,
20
- particle_counters_word_count,
21
- } from "./data/PARTICLE_COUNTERS.js";
22
- import { graph_particles_simulate } from "./graph_particles.js";
23
- import {
24
- PARTICLE_INIT_FREE_LIST_WORKGROUP_SIZE,
25
- shader_particle_init_free_list,
26
- } from "./shaders/shader_particle_init_free_list.js";
27
- import {
28
- graph_particle_avboit_draw,
29
- graph_particle_avboit_occupancy,
30
- graph_particle_avboit_splat,
31
- } from "./graph_particles_avboit.js";
32
- import { EmitterRegistry } from "./runtime/EmitterRegistry.js";
33
- import { WARMUP_QUEUE } from "./warmup/shader_particle_warmup.js";
34
- import { warmup_schedule } from "./warmup/warmup_schedule.js";
35
- import { GPUTextureContext } from "../texture/GPUTextureContext.js";
36
- import { inspect_gpu_buffer } from "../buffer/inspect_gpu_buffer.js";
37
-
38
- const WORD = Uint32Array.BYTES_PER_ELEMENT;
39
-
40
- /*
41
- A wide usage mask costs bandwidth: the driver places a buffer by what it was told the buffer might
42
- have to do, and the flags below are what each one actually does. `pool` and the alive lists are the
43
- big ones and are touched by nothing but shaders. Adding a flag "just in case" moves 64 MiB onto a
44
- slower path for a copy nobody makes — see `BufferResourceDescriptor`.
45
- */
46
- /** Written only by shaders. The free list is one of these: `init_free_list` fills it on-device. */
47
- const STORAGE = GPUBufferUsage.STORAGE;
48
- /** Seeded or refilled from the host with `writeBuffer`. */
49
- const STORAGE_UPLOAD = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST;
50
- /** Uploaded, and copied forward into its replacement when the emitter set outgrows it. */
51
- const STORAGE_GROWABLE = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC;
52
- /**
53
- * An indirect command the GPU writes and the GPU consumes. COPY_SRC is for {@link
54
- * GPUParticleSystem#read_stats}, which reads the args back to show that the dispatch really is
55
- * sized on-device — twelve bytes.
56
- */
57
- const INDIRECT = GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_SRC;
58
-
59
- const DEFAULT_CAPACITY = 524288;
60
-
61
- /**
62
- * What {@link GPUParticleSystem#read_stats} decodes: the counts the GPU keeps between frames, named.
63
- *
64
- * @typedef {object} ParticleSystemStats
65
- * @property {number} alive live particles the frame ended with
66
- * @property {number} dead length of the free list
67
- * @property {number} spawn_budget free slots the frame never promised to a spawn — its headroom
68
- * @property {number} alive_in entries the frame's simulate consumed — the padded, program-coherent
69
- * list the frame before it built, so never fewer than that frame's `alive`
70
- * @property {number} sim_padded length of the padded, program-coherent simulate list built this
71
- * frame; the gap to `alive` is what keeping each wave on one VM program costs in lanes
72
- * @property {number} sim_workgroups workgroups the NEXT simulate will dispatch — read out of the
73
- * indirect args rather than derived, so it is what the GPU will actually launch
74
- */
75
-
76
- /**
77
- * The GPU particle system: a feature of a scene, in the mould of `ReSTIRDI` — construct once
78
- * against a {@link GPUSceneContext}, and contribute the frame's passes through
79
- * {@link execute}.
80
- *
81
- * ## What it owns
82
- *
83
- * Persistent GPU state, and only that: a buffer is reserved here if and only if its contents have
84
- * to survive to the next frame. That is the particle pool, the alive list, the dead
85
- * free-list, the counters, the simulation-order (coherence) list, simulate's indirect dispatch args
86
- * (written at the end of one frame, read by the next), the emitter database (the emitter table and
87
- * the GPU-owned state table, one buffer), the packed program buffer, and the two CPU-uploaded
88
- * queues — the spawn-command ring and the warm-up queue. Buffers that depend on the emitter set —
89
- * the coherence list on the number of distinct programs, the program buffer on the heap, the
90
- * database on its own pages, the counters on the emitter table's element capacity (they carry the
91
- * per-emitter bounds accumulator, see `bounds/`) — are grown inside {@link execute}, the way
92
- * ReSTIR's reservoirs follow the resolution.
93
- *
94
- * Everything else a frame touches is the frame graph's. The bucketing's bins, cursors and keys, the
95
- * draw args, the spawn counts and the VM's wide register file are produced and consumed inside one
96
- * graph, so the pass that first writes each one declares it and the graph allocates it just in time
97
- * and releases it after its last reader — into the pool the rest of the renderer allocates from.
98
- * Reserving them here instead would be megabytes at the default capacity that no other feature
99
- * could ever borrow, and most of them are cleared by a reset pass every frame regardless.
100
- *
101
- * ## The alive list is borrowed, not reserved
102
- *
103
- * The alive list crosses the frame boundary and so cannot be the graph's — but it does not have to
104
- * be the feature's either. It is taken from the graphics context's buffer allocator and given back
105
- * every frame ({@link #swap_alive_list}), which is the difference between one list and two.
106
- *
107
- * Two was the obvious reading of the ping-pong: the frame builds a new list while the previous one
108
- * is still there to be read. But nothing reads the previous one. Emit and simulate append to the
109
- * new list from a counter the frame resets to zero, the draws walk that same new list, and the only
110
- * pass that ever looks at the one before it is the bucketing rebuild on a frame whose program
111
- * assignments changed — a live program edit, not a frame. So on an ordinary frame the previous list
112
- * goes back to the pool BEFORE the new one is asked for, the pool answers with the same memory, and
113
- * the feature's reserved footprint is one list rather than two. On the rare frame that does rebuild,
114
- * the old list is held one frame longer — until the graph recorded around it has been submitted —
115
- * and the feature briefly costs what it used to cost always.
116
- *
117
- * Every pass stays within the eight storage buffers a stage gets by default: the emitter record
118
- * and the GPU-owned state are two tables of one database buffer, and the VM's constants and code
119
- * are one packed buffer. The one exception is the wide-register fallback (below), whose emit and
120
- * simulate passes bind a ninth for the register file — inside the ten the engine already demands of
121
- * an adapter, but past the default.
122
- *
123
- * ## Two interpreters
124
- *
125
- * The VM's register file normally lives in shader registers, and is therefore small — see
126
- * {@link ./ParticleConstants.js PARTICLE_VM_FAST_REGISTER_SLOTS}. An effect whose compiled program
127
- * needs more than it holds does not fail: the whole system swaps emit and simulate for variants
128
- * that keep the file in a storage buffer, which is far slower and effectively unbounded. The choice
129
- * is per-frame, whole-system, and made here — the registry reports `max_register_count` over the
130
- * effects it holds and this is where that number meets the VM's; nothing about authoring a graph has
131
- * a register ceiling.
132
- *
133
- * ## Emitters are registered at rows
134
- *
135
- * {@link add_emitter} gives a {@link ParticleEmitter} a row in the emitter table pointed at a
136
- * `transforms` row — an entity's, one on, held through the scene context's rows by whoever owns
137
- * the entity — and it starts spawning on the next {@link execute}; under a joint's row it moves
138
- * with the joint. {@link remove_emitter} retires the row and frees its particles. Nothing here
139
- * walks the scene: an emitter is where its owner says it is, and per-frame upkeep is proportional
140
- * to what changed rather than to how many emitters there are.
141
- *
142
- * ## Spawning is the GPU's decision
143
- *
144
- * The CPU never counts particles or walks emitters per frame. The emitter tick pass integrates
145
- * every emitter's rate on the GPU, and the emit dispatch is sized by a prefix scan of what it
146
- * decided. The host's one way to ask for particles is {@link spawn}: a burst queued into a small
147
- * command list that a pass adds to the emitter's pending spawns, on the same path as everything
148
- * else. See `graph_particles.js` for the frame.
149
- *
150
- * ## Drawing
151
- *
152
- * {@link execute} is the whole per-frame call, once per frame, and it records only the simulation:
153
- * everything that moves particles, leaving the alive list and the indirect draw args as graph
154
- * handles. It draws nothing.
155
- *
156
- * The drawing is the transparency pipeline's. The system is an `AVBOITSideChannel`, and the AVBOIT
157
- * orchestrator asks it for its occupancy, its extinction and its draw at the three points where the
158
- * transparent meshes have contributed theirs ({@link avboit_occupancy}, {@link avboit_splat},
159
- * {@link avboit_draw}). Those read the handles `execute` produced and composite into a voxel volume
160
- * and a set of accumulators that are sums — so the particles need no depth ordering at all, and
161
- * none is ever built.
162
- *
163
- * ## Allocation
164
- *
165
- * The per-frame path allocates the frame graph's records and nothing else: emitter rows are
166
- * staged through one shared record, spawn commands go into a preallocated ring, and the
167
- * per-dispatch settings are reused objects. A command context is opened only on frames that have
168
- * something to upload.
169
- */
170
- export class GPUParticleSystem {
171
-
172
- /**
173
- * Engine GraphicsContext.
174
- * @type {object}
175
- */
176
- graphics;
177
-
178
- /**
179
- * The scene whose rows the emitters are placed at, and whose database their world matrices
180
- * come from.
181
- * @type {import("../scene/GPUSceneContext.js").GPUSceneContext}
182
- */
183
- scene_context;
184
-
185
- /**
186
- * Maximum number of simultaneously live particles.
187
- * @type {number}
188
- */
189
- capacity;
190
-
191
- /**
192
- * @type {EmitterRegistry}
193
- */
194
- registry;
195
-
196
- /**
197
- * Frames elapsed. The VM's frame builtin.
198
- * @type {number}
199
- */
200
- frame = 0;
201
-
202
- /**
203
- * Seconds since start, the VM's `TIME` builtin. Advanced by `dt` each {@link execute}.
204
- * @type {number}
205
- */
206
- time = 0;
207
-
208
- /**
209
- * Cap on how much missed time a culled emitter makes up for when it comes back into view.
210
- * @type {number}
211
- */
212
- max_catchup_seconds = 1;
213
-
214
- /**
215
- * Sprite atlas the billboards sample, and the sampler they sample it with. A 1x1 white texel by
216
- * default, so an emitter with no art draws as a plain quad rather than nothing.
217
- * @type {GPUTextureView}
218
- */
219
- atlas;
220
-
221
- /** @type {GPUSampler} */
222
- atlas_sampler;
223
-
224
- /**
225
- * The persistent GPU buffers, by the names the graph functions bind them under. Exposed for
226
- * inspection — {@link bufferSizes} measures them, {@link read_stats} reads the counts out of
227
- * them — never for the frame loop, which reads nothing back. Entries that grow with the
228
- * emitter set are replaced in place.
229
- *
230
- * `alive` and `alive_write` are one buffer under two names — what the feature holds, and what
231
- * the frame writes — and `alive_read` is the previous frame's list, present only on the frames
232
- * that read one. The list itself belongs to the allocator, not here; see {@link #swap_alive_list}.
233
- *
234
- * @type {object}
235
- */
236
- buffers = {};
237
-
238
- /**
239
- * Distinct programs the coherence scratch is currently sized for.
240
- * @type {number}
241
- */
242
- #sim_bucket_capacity = 0;
243
-
244
- /**
245
- * What the alive list is asked for under: one `u32` per pool slot, touched by nothing but
246
- * shaders, and deliberately not cleared — every entry a reader sees was appended this frame,
247
- * from a counter the frame's first pass resets to zero, so whatever the last borrower left in
248
- * it is unreadable by construction. One instance, reused: the pool reads a descriptor and does
249
- * not keep it.
250
- *
251
- * @type {BufferResourceDescriptor}
252
- */
253
- #alive_descriptor;
254
-
255
- /**
256
- * The alive list the last recorded frame built — the next frame's `alive_read`. Borrowed from
257
- * `graphics.buffer_allocator_main`, so it is released rather than destroyed.
258
- * @type {GPUBuffer|null}
259
- */
260
- #alive = null;
261
-
262
- /**
263
- * An alive list a recorded frame still reads: the bucketing rebuild's input on a frame whose
264
- * program assignments changed. Handed back on the next {@link execute}, by which point that
265
- * frame's graph has been executed and submitted.
266
- * @type {GPUBuffer|null}
267
- */
268
- #alive_retired = null;
269
-
270
- /**
271
- * The spawn-command ring: `[row, count]` pairs, filled by {@link spawn}, merged and uploaded on
272
- * the next {@link execute}.
273
- * @type {Uint32Array}
274
- */
275
- #spawn_commands;
276
-
277
- /**
278
- * The emitter each ring entry was issued for, so an entry whose emitter has since left the
279
- * scene — or whose row has gone to somebody else — is dropped at upload rather than delivered
280
- * to the row's next tenant.
281
- * @type {import("./runtime/ParticleEmitter.js").ParticleEmitter[]}
282
- */
283
- #spawn_command_emitters;
284
-
285
- /** @type {number} */
286
- #spawn_command_count = 0;
287
-
288
- /**
289
- * Scratch for merging the ring: pending count by row. Grown with the emitter table, never per
290
- * frame.
291
- * @type {Uint32Array}
292
- */
293
- #spawn_by_row = new Uint32Array(0);
294
-
295
- /**
296
- * Emitters registered since the last frame with a pre-warm to run — the input of the warm-up
297
- * pipeline (`warmup/`), consumed by the frame that records it. Filled where the emitter is
298
- * registered ({@link add_emitter}), which is the one moment the host knows an emitter is
299
- * new: the GPU decides everything else about spawning, but "this one has just arrived and asks
300
- * to arrive warm" is a host-side fact.
301
- *
302
- * @type {import("./runtime/ParticleEmitter.js").ParticleEmitter[]}
303
- */
304
- #warmup_queue = [];
305
-
306
- /**
307
- * The queue as the GPU reads it, `PARTICLE_WARMUP_QUEUE_WORDS` per entry — see
308
- * `warmup/shader_particle_warmup.js`. Grown to fit, never per frame.
309
- * @type {Uint32Array}
310
- */
311
- #warmup_words = new Uint32Array(0);
312
-
313
-
314
- /**
315
- * The 1x1 white texture {@link atlas} points at until a caller replaces it. Held so
316
- * {@link destroy} can free it; a caller that assigns its own atlas owns that one.
317
- * @type {GPUTextureContext|null}
318
- */
319
- #default_atlas = null;
320
-
321
-
322
- /**
323
- * Program assignments used to build the previous frame's coherence list.
324
- * @type {number}
325
- */
326
- #program_assignment_version = 0;
327
-
328
- /**
329
- * Per-frame scalars handed to {@link graph_particles_simulate}, reused rather than rebuilt.
330
- * @type {object}
331
- */
332
- #frame = {
333
- dt: 0, time: 0, index: 0, max_catchup: 1, spawn_command_count: 0,
334
- warmup_count: 0, warmup_capacity: 0, warmup_schedule: null,
335
- rebucket: false,
336
- };
337
-
338
- /** @type {{capacity: number, group_count: number}} */
339
- #emitters = { capacity: 0, group_count: 0 };
340
-
341
- /**
342
- * This frame's simulation, for the side channels that draw it later in the frame: the graph it
343
- * was recorded into, the scene database handle it read, and the handles it produced. Reused,
344
- * never reallocated.
345
- * @type {{graph: object|null, scene_database: number, emitter_group_count: number, handles: object|null}}
346
- */
347
- #simulated = { graph: null, scene_database: -1, emitter_group_count: 0, handles: null };
348
-
349
- /**
350
- * @param {object} graphics engine GraphicsContext (has `.device`)
351
- * @param {import("../scene/GPUSceneContext.js").GPUSceneContext} scene_context
352
- * @param {object} [options]
353
- * @param {number} [options.capacity] max simultaneous particles
354
- * @param {number} [options.spawn_command_capacity] most {@link spawn} calls one frame keeps
355
- */
356
- constructor(graphics, scene_context, { capacity = DEFAULT_CAPACITY, spawn_command_capacity = 256 } = {}) {
357
- assert.defined(graphics, "graphics");
358
- assert.defined(scene_context, "scene_context");
359
- assert.equal(scene_context.isGPUSceneContext, true, "scene_context.isGPUSceneContext !== true");
360
- assert.isPositiveInteger(capacity, "capacity");
361
- assert.isPositiveInteger(spawn_command_capacity, "spawn_command_capacity");
362
-
363
- this.graphics = graphics;
364
- this.scene_context = scene_context;
365
- this.capacity = capacity;
366
-
367
- this.registry = new EmitterRegistry(graphics.device);
368
-
369
- this.#spawn_commands = new Uint32Array(spawn_command_capacity * PARTICLE_SPAWN_COMMAND_WORDS);
370
- this.#spawn_command_emitters = new Array(spawn_command_capacity).fill(null);
371
-
372
- this.#create_buffers();
373
- this.#create_default_atlas();
374
- }
375
-
376
- /**
377
- * The buffer the emitter table lives in — what every emitter-reading pass binds.
378
- * @returns {GPUBuffer}
379
- */
380
- get emitter_database() {
381
- return this.registry.database.buffer;
382
- }
383
-
384
- /**
385
- * The live emitters, in no particular order.
386
- * @returns {import("./runtime/ParticleEmitter.js").ParticleEmitter[]}
387
- */
388
- get emitters() {
389
- return this.registry.emitters;
390
- }
391
-
392
- /**
393
- * Number of simulation-order buckets — one per program id the registry can hand out, which is
394
- * its peak number of distinct compiled programs rather than its current one.
395
- *
396
- * This is the coherence pass's only host-provided number, and it is static topology rather than
397
- * per-frame state: it changes when an effect the set has never held is registered, not as
398
- * particles are born and die. Everything the pass measures per frame stays on the GPU. Sizing it
399
- * from the peak rather than the live count is what keeps it from moving every time an effect is
400
- * unregistered — the bins of released ids are empty, and an empty bin rounds up to zero
401
- * workgroups and shifts nothing after it.
402
- *
403
- * @returns {number}
404
- */
405
- bucketCount() {
406
- return Math.max(1, this.registry.program_id_capacity);
407
- }
408
-
409
- /**
410
- * Ask for `count` particles from an emitter, on top of its rate, on the next frame. Goes
411
- * through the GPU path — the tick pass folds it into the emitter's accumulator, the budget and
412
- * the culling apply — so it is a request, not a guarantee.
413
- *
414
- * @param {import("./runtime/ParticleEmitter.js").ParticleEmitter} emitter a registered emitter
415
- * @param {number} count
416
- */
417
- spawn(emitter, count) {
418
- assert.defined(emitter, "emitter");
419
- assert.isNonNegativeInteger(count, "count");
420
-
421
- const context = this.registry.context(emitter);
422
-
423
- assert.defined(context, "emitter is not registered");
424
-
425
- const ring = this.#spawn_commands;
426
- const base = this.#spawn_command_count * PARTICLE_SPAWN_COMMAND_WORDS;
427
-
428
- if (base >= ring.length) {
429
- // the text is the key warn_limited counts on, so it carries no burst size: one message
430
- // per ring size, not one per number a caller happens to ask for
431
- warn_limited(`GPUParticleSystem: spawn command ring is full (${ring.length / PARTICLE_SPAWN_COMMAND_WORDS} per frame); dropping this frame's remaining bursts`);
432
- return;
433
- }
434
-
435
- ring[base] = context.row;
436
- ring[base + 1] = count;
437
- this.#spawn_command_emitters[this.#spawn_command_count] = emitter;
438
- this.#spawn_command_count++;
439
- }
440
-
441
- /**
442
- * The system's whole contribution to the frame, once per frame: reconcile the emitter set with
443
- * the scene, push what changed to the GPU, and record every pass that moves particles — through
444
- * to the indirect draw args.
445
- *
446
- * It draws nothing and touches no colour target. The handles it returns are what the AVBOIT
447
- * side channels ({@link avboit_occupancy}, {@link avboit_splat}, {@link avboit_draw}) consume
448
- * later in the same frame, off this same graph, once the transparent meshes have contributed
449
- * theirs.
450
- *
451
- * Call after the scene context has built for the frame (`GPUSceneContext#update`), so every
452
- * emitter's `transforms` row is uploaded, and after the hierarchy pass has run, so the rows'
453
- * `global` matrices are this frame's.
454
- *
455
- * **Once per frame, not once per view.** This advances the simulation by `dt` and hands out
456
- * handles into the graph it recorded into; a second view of the same scene records a second
457
- * graph and would step the simulation twice. The renderer has one view per `render` call today,
458
- * so this holds by construction; a second view of one scene needs the step and the handles
459
- * separated — record the passes for the first graph, import the same buffers into the rest.
460
- *
461
- * @param {object} params
462
- * @param {import("../../../engine/graphics/render/frame_graph/FrameGraph.js").FrameGraph} params.graph
463
- * @param {number} params.camera graph handle of the camera uniform buffer
464
- * @param {number} params.scene_database graph handle of the scene database — the one the
465
- * hierarchy pass returned, so this reads the matrices it composed
466
- * @param {number} params.dt seconds since the previous frame
467
- * @returns {import("./graph_particles.js").ParticleSimulationHandles}
468
- */
469
- execute({ graph, camera, scene_database, dt }) {
470
- assert.defined(graph, "graph");
471
- assert.isNumber(dt, "dt");
472
-
473
- this.#upload();
474
-
475
- const frame = this.#frame;
476
- frame.dt = dt;
477
- frame.time = this.time;
478
- frame.index = this.frame;
479
- frame.max_catchup = this.max_catchup_seconds;
480
- frame.spawn_command_count = this.#spawn_command_count;
481
- frame.rebucket = this.#program_assignment_version !== this.registry.program_assignment_version;
482
- this.#program_assignment_version = this.registry.program_assignment_version;
483
-
484
- const table = this.registry.table;
485
- const emitters = this.#emitters;
486
- emitters.capacity = table.element_capacity;
487
- emitters.group_count = table.dispatch_group_count(PARTICLE_EMITTER_TICK_WORKGROUP_SIZE);
488
-
489
- const buffers = this.buffers;
490
-
491
- this.#swap_alive_list(frame.rebucket);
492
-
493
- // The table's database owns and grows its own buffer; it is re-read every frame for that
494
- // reason.
495
- buffers.emitter_database = this.registry.database.buffer;
496
-
497
- const simulated = graph_particles_simulate({
498
- graph,
499
- camera,
500
- scene_database,
501
- gpu: buffers,
502
- emitters,
503
- capacity: this.capacity,
504
- bucket_count: this.bucketCount(),
505
- // Whose register file the VM runs on is decided here, once, for the whole system: the
506
- // registry reports what the hungriest registered effect needs and this compares it
507
- // against what the fast file holds. Re-read every frame because registering an emitter —
508
- // or unregistering the one hungry effect — can flip it either way.
509
- wide_registers: this.registry.max_register_count > PARTICLE_VM_FAST_REGISTER_SLOTS,
510
- frame,
511
- });
512
-
513
- // Kept for the side channels that draw later in this frame, and for nothing past it.
514
- const current = this.#simulated;
515
- current.graph = graph;
516
- current.scene_database = scene_database;
517
- current.emitter_group_count = emitters.group_count;
518
- current.handles = simulated;
519
-
520
- // Consumed: the counts were recorded into the graph, the words were uploaded.
521
- this.#spawn_command_count = 0;
522
- this.#warmup_queue.length = 0;
523
-
524
- this.frame++;
525
- this.time += dt;
526
-
527
- return simulated;
528
- }
529
-
530
- /**
531
- * What this frame's {@link execute} left, checked against the graph a side-channel call names:
532
- * the AVBOIT orchestrator records its passes into the same graph, later in the same frame.
533
- *
534
- * @param {import("../../../engine/graphics/render/frame_graph/FrameGraph.js").FrameGraph} graph
535
- * @returns {{graph: object, scene_database: number, emitter_group_count: number, handles: import("./graph_particles.js").ParticleSimulationHandles}}
536
- */
537
- #simulated_for(graph) {
538
- const current = this.#simulated;
539
-
540
- assert.equal(current.graph, graph, "the particles were not simulated into this graph this frame — call execute() before the transparency pipeline records");
541
-
542
- return current;
543
- }
544
-
545
- /**
546
- * `AVBOITSideChannel#avboit_occupancy`: the emitters' measured bounding boxes, as depth slices.
547
- *
548
- * @param {object} args see `AVBOITSideChannel`
549
- * @param {import("../../../engine/graphics/render/frame_graph/FrameGraph.js").FrameGraph} args.graph
550
- * @param {number} args.camera graph handle of the camera uniform buffer
551
- * @param {ArrayLike<number>} args.cluster_parameters the frame's depth curve
552
- * @param {number} args.occupancy graph handle of the occupancy bits
553
- * @returns {number} the occupancy handle after the write
554
- */
555
- avboit_occupancy({ graph, camera, cluster_parameters, occupancy }) {
556
- const current = this.#simulated_for(graph);
557
-
558
- return graph_particle_avboit_occupancy({
559
- graph,
560
- camera,
561
- cluster_parameters,
562
- occupancy,
563
- emitter_database: current.handles.emitter_database,
564
- emitter_group_count: current.emitter_group_count,
565
- });
566
- }
567
-
568
- /**
569
- * `AVBOITSideChannel#avboit_splat`: the live particles' extinction, into the volume.
570
- *
571
- * @param {object} args see `AVBOITSideChannel`
572
- * @param {import("../../../engine/graphics/render/frame_graph/FrameGraph.js").FrameGraph} args.graph
573
- * @param {number} args.camera graph handle of the camera uniform buffer
574
- * @param {number} args.warp graph handle of the warp LUT
575
- * @param {number} args.depth_texture graph handle of the scene depth
576
- * @param {{extinction: number, overflow: number, dummy: number}} args.volume the voxel volume
577
- * @returns {{extinction: number, overflow: number, dummy: number}} the volume after the write
578
- */
579
- avboit_splat({ graph, camera, warp, depth_texture, volume }) {
580
- const { handles } = this.#simulated_for(graph);
581
-
582
- return graph_particle_avboit_splat({
583
- graph,
584
- camera,
585
- warp,
586
- depth_texture,
587
- volume,
588
- atlas: this.atlas,
589
- atlas_sampler: this.atlas_sampler,
590
- pool: handles.pool,
591
- alive: handles.alive,
592
- emitter_database: handles.emitter_database,
593
- draw_args: handles.draw_args,
594
- });
595
- }
596
-
597
- /**
598
- * `AVBOITSideChannel#avboit_draw`: the live particles, into the accumulators.
599
- *
600
- * @param {object} args see `AVBOITSideChannel`
601
- * @param {import("../../../engine/graphics/render/frame_graph/FrameGraph.js").FrameGraph} args.graph
602
- * @param {import("../view/GPUViewContext.js").GPUViewContext} args.view_ctx
603
- * @param {number} args.camera graph handle of the camera uniform buffer
604
- * @param {number} args.depth_texture graph handle of the scene depth
605
- * @param {number} args.warp graph handle of the warp LUT
606
- * @param {number} args.integral graph handle of the integral texture
607
- * @param {number} args.volumetrics_lut graph handle
608
- * @param {number} args.volumetrics_metadata graph handle
609
- * @param {{color: number, norm: number, extinction: number, emission: number}} args.accumulators
610
- * @returns {{color: number, norm: number, extinction: number, emission: number}} after the draw
611
- */
612
- avboit_draw({ graph, view_ctx, camera, depth_texture, warp, integral, volumetrics_lut, volumetrics_metadata, accumulators }) {
613
- const { handles } = this.#simulated_for(graph);
614
-
615
- return graph_particle_avboit_draw({
616
- graph,
617
- view_ctx,
618
- camera,
619
- depth_texture,
620
- warp,
621
- integral,
622
- volumetrics_lut,
623
- volumetrics_metadata,
624
- accumulators,
625
- atlas: this.atlas,
626
- atlas_sampler: this.atlas_sampler,
627
- pool: handles.pool,
628
- alive: handles.alive,
629
- emitter_database: handles.emitter_database,
630
- draw_args: handles.draw_args,
631
- });
632
- }
633
-
634
- /**
635
- * Register an emitter at a `transforms` row, and if it asks, warm it up before its first frame.
636
- *
637
- * The row is an entity's, one on (`row_of_entity`), and whoever owns the entity holds it through
638
- * the scene context's rows for as long as the emitter is registered; this only records which
639
- * row the emitter's particles are born at. Nothing is uploaded until the next {@link execute}.
640
- *
641
- * @param {import("./runtime/ParticleEmitter.js").ParticleEmitter} emitter
642
- * @param {number} transform_row
643
- */
644
- add_emitter(emitter, transform_row) {
645
- assert.defined(emitter, 'emitter');
646
- assert.equal(emitter.isParticleEmitter, true, 'emitter.isParticleEmitter !== true');
647
- assert.isNonNegativeInteger(transform_row, 'transform_row');
648
-
649
- this.registry.add(emitter, transform_row);
650
-
651
- // Just arrived, and asked to arrive warm: the warm-up pipeline runs its simulation forward
652
- // this frame, before it is first drawn. A reused row is a new emitter as far as this is
653
- // concerned — it is the emitter that asks, not the row.
654
- if (emitter.prewarm > 0) {
655
- this.#warmup_queue.push(emitter);
656
- }
657
- }
658
-
659
- /**
660
- * Retire an emitter's row; its particles go within a frame, carrying the row's generation.
661
- *
662
- * @param {import("./runtime/ParticleEmitter.js").ParticleEmitter} emitter
663
- */
664
- remove_emitter(emitter) {
665
- assert.defined(emitter, 'emitter');
666
-
667
- this.registry.remove(emitter);
668
- }
669
-
670
- /**
671
- * Point a registered emitter at another row.
672
- *
673
- * @param {import("./runtime/ParticleEmitter.js").ParticleEmitter} emitter
674
- * @param {number} transform_row
675
- */
676
- set_transform_row(emitter, transform_row) {
677
- assert.isNonNegativeInteger(transform_row, 'transform_row');
678
-
679
- this.registry.set_transform_row(emitter, transform_row);
680
- }
681
-
682
- /**
683
- * Push everything the CPU changed to the GPU: dirty emitter rows, the state buffer's growth,
684
- * rebuilt program buffers, the spawn-command ring, and the coherence scratch's growth. A
685
- * command context is opened only if something needs one.
686
- */
687
- #upload() {
688
- const registry = this.registry;
689
- const device = this.graphics.device;
690
-
691
- registry.flush();
692
-
693
- // Both tables stage into the one database: the emitter rows the author changed, and the
694
- // state rows claimed or released with them.
695
- const rows_staged = registry.table.element_upload_buffer.position > 0
696
- || registry.state_table.element_upload_buffer.position > 0;
697
-
698
- if (rows_staged) {
699
- const cmd = ShadeGPUCommandContext.create(this.graphics, "particles/upload");
700
-
701
- registry.database.update(cmd);
702
-
703
- cmd.finish();
704
- }
705
-
706
- if (registry.programs_dirty) {
707
- this.buffers.program = replace_data_buffer(device, this.buffers.program, "particles/program", registry.build_program());
708
- }
709
-
710
- this.#merge_spawn_commands();
711
-
712
- if (this.#spawn_command_count > 0) {
713
- device.queue.writeBuffer(
714
- this.buffers.spawn_commands, 0,
715
- this.#spawn_commands.buffer, 0,
716
- this.#spawn_command_count * PARTICLE_SPAWN_COMMAND_WORDS * WORD,
717
- );
718
- }
719
-
720
- this.#upload_warmup_queue();
721
-
722
- this.#ensure_sim_buffers(this.bucketCount());
723
- this.#ensure_counters(registry.table.element_capacity);
724
- }
725
-
726
- /**
727
- * Turn the emitters queued for a warm-up into the words the warm-up tick reads, and the numbers
728
- * the frame graph sizes the loop by; upload the words. Nothing when nothing is queued, which is
729
- * nearly every frame.
730
- *
731
- * Two per-emitter numbers are hashed here rather than left at zero, both to keep emitters warmed
732
- * in one batch off each other's beat: the accumulator's starting fraction, so their first whole
733
- * particles fall on different ticks, and the clock each one's tick runs at. See
734
- * `warmup/shader_particle_warmup.js`.
735
- *
736
- * The transient pool is sized by what the batch can spawn in all — every emitter's rate over its
737
- * own pre-warm, plus one each for the rounding — and no larger than the main pool it is bound
738
- * for: what would not fit there is dropped at migration anyway.
739
- */
740
- #upload_warmup_queue() {
741
- const queue = this.#warmup_queue;
742
- const frame = this.#frame;
743
-
744
- // A queued emitter that has since left the scene has no row to warm; drop it here.
745
- let count = 0;
746
-
747
- for (let i = 0; i < queue.length; i++) {
748
- const emitter = queue[i];
749
-
750
- if (this.registry.context(emitter) !== undefined) {
751
- queue[count++] = emitter;
752
- }
753
- }
754
-
755
- queue.length = count;
756
-
757
- if (count === 0) {
758
- frame.warmup_count = 0;
759
- frame.warmup_capacity = 0;
760
- frame.warmup_schedule = null;
761
- return;
762
- }
763
-
764
- let words = this.#warmup_words;
765
- const needed = count * PARTICLE_WARMUP_QUEUE_WORDS;
766
-
767
- if (words.length < needed) {
768
- words = new Uint32Array(Math.max(needed, words.length * 2));
769
- this.#warmup_words = words;
770
- }
771
-
772
- const floats = new Float32Array(words.buffer, words.byteOffset, words.length);
773
-
774
- let max_prewarm = 0;
775
- let spawned = 0;
776
-
777
- for (let i = 0; i < count; i++) {
778
- const emitter = queue[i];
779
- const context = this.registry.context(emitter);
780
- const base = i * PARTICLE_WARMUP_QUEUE_WORDS;
781
-
782
- // Two unit fractions from one hash of the row and the emitter's seed: the same emitter
783
- // in the same row warms the same way twice, and two emitters do not.
784
- const hash = hash_mix2(context.row, context.generation) >>> 0;
785
- const phase = (hash >>> 8) / 16777216;
786
- const beat = ((hash & 0xff) / 255) - 0.5;
787
-
788
- words[base + WARMUP_QUEUE.ROW] = context.row;
789
- floats[base + WARMUP_QUEUE.PREWARM] = emitter.prewarm;
790
- floats[base + WARMUP_QUEUE.ACCUMULATOR] = phase;
791
- floats[base + WARMUP_QUEUE.CLOCK] = 1 + PARTICLE_WARMUP_EMITTER_JITTER * beat;
792
-
793
- max_prewarm = Math.max(max_prewarm, emitter.prewarm);
794
- spawned += Math.ceil(emitter.spawn_rate * emitter.prewarm) + 1;
795
- }
796
-
797
- const device = this.graphics.device;
798
- const bytes = needed * WORD;
799
-
800
- if (this.buffers.warmup_queue.size < bytes) {
801
- this.buffers.warmup_queue.destroy();
802
- this.buffers.warmup_queue = device.createBuffer({ label: "particles/warmup_queue", size: bytes, usage: STORAGE_UPLOAD });
803
- }
804
-
805
- device.queue.writeBuffer(this.buffers.warmup_queue, 0, words.buffer, words.byteOffset, bytes);
806
-
807
- frame.warmup_count = count;
808
- frame.warmup_capacity = Math.max(1, Math.min(this.capacity, spawned));
809
- frame.warmup_schedule = warmup_schedule(max_prewarm);
810
- }
811
-
812
- /**
813
- * Fold the ring down to one entry per row, dropping entries whose emitter no longer holds the
814
- * row it was issued against.
815
- *
816
- * The spawn-command pass adds to a state row with a plain read-modify-write, so two entries
817
- * naming one row would lose one; and a burst issued to an emitter that left the scene this
818
- * frame must not land on the row's next tenant. Both are settled here, in place, over a few
819
- * dozen entries at most.
820
- */
821
- #merge_spawn_commands() {
822
- const count = this.#spawn_command_count;
823
-
824
- if (count === 0) {
825
- return;
826
- }
827
-
828
- const ring = this.#spawn_commands;
829
- const emitters = this.#spawn_command_emitters;
830
-
831
- let rows = this.#spawn_by_row;
832
- const needed = Math.max(1, this.registry.table.element_capacity);
833
-
834
- if (rows.length < needed) {
835
- rows = new Uint32Array(needed);
836
- this.#spawn_by_row = rows;
837
- }
838
-
839
- for (let i = 0; i < count; i++) {
840
- const emitter = emitters[i];
841
- const row = ring[i * PARTICLE_SPAWN_COMMAND_WORDS];
842
- const context = this.registry.context(emitter);
843
-
844
- if (context !== undefined && context.row === row) {
845
- rows[row] += ring[i * PARTICLE_SPAWN_COMMAND_WORDS + 1];
846
- }
847
-
848
- emitters[i] = null;
849
- }
850
-
851
- let merged = 0;
852
-
853
- for (let i = 0; i < count; i++) {
854
- const row = ring[i * PARTICLE_SPAWN_COMMAND_WORDS];
855
- const pending = rows[row];
856
-
857
- if (pending === 0) {
858
- continue;
859
- }
860
-
861
- ring[merged * PARTICLE_SPAWN_COMMAND_WORDS] = row;
862
- ring[merged * PARTICLE_SPAWN_COMMAND_WORDS + 1] = pending;
863
- rows[row] = 0;
864
- merged++;
865
- }
866
-
867
- this.#spawn_command_count = merged;
868
- }
869
-
870
- /**
871
- * The counters buffer holds the per-emitter bounds accumulators after the counters themselves
872
- * (`data/PARTICLE_COUNTERS.js`), so it follows the emitter table's element capacity.
873
- *
874
- * Grown by copy, not by reallocation: the counters at the head of it are live GPU state — the
875
- * free-list length above all, which no CPU value can reconstruct — and the accumulators are this
876
- * frame's measurements of emitters that have not moved. A fresh buffer would zero the free list
877
- * and strand the whole pool.
878
- *
879
- * The rows past the old end are the only ones the copy does not answer for, and zero is the
880
- * wrong answer for them: a zero accumulator reads back as a degenerate box at the world origin
881
- * rather than as an absence, and the first box measured for such a row would be stretched to
882
- * include the origin. They get the empty interval written over them instead — the same two words
883
- * the publish pass resets a row to.
884
- *
885
- * @param {number} emitter_capacity element capacity of the emitter table
886
- */
887
- #ensure_counters(emitter_capacity) {
888
- const size = particle_counters_word_count(emitter_capacity) * WORD;
889
- const existing = this.buffers.counters;
890
-
891
- if (existing.size >= size) {
892
- return;
893
- }
894
-
895
- const device = this.graphics.device;
896
- const grown = device.createBuffer({ label: "particles/counters", size, usage: STORAGE_GROWABLE });
897
-
898
- const cmd = ShadeGPUCommandContext.create(this.graphics, "particles/counters grow");
899
-
900
- cmd.gpu_encoder.copyBufferToBuffer(existing, 0, grown, 0, existing.size);
901
-
902
- cmd.finish();
903
-
904
- // The new region is a whole number of accumulators: the counters are the head of the buffer
905
- // and every growth adds rows behind them.
906
- const added_rows = (size - existing.size) / (PARTICLE_BOUNDS_WORDS * WORD);
907
-
908
- device.queue.writeBuffer(grown, existing.size, particle_bounds_empty_words(added_rows));
909
-
910
- existing.destroy();
911
-
912
- this.buffers.counters = grown;
913
- }
914
-
915
- /**
916
- * The coherence list follows the peak number of program ids. It is the one thing the bucketing
917
- * leaves behind: the next simulate consumes its entries and padding with the previous frame's
918
- * saved count, so it cannot be a transient and it cannot be reallocated empty. Growth copies it
919
- * forward; the old count and dispatch stay valid because the entries keep their places.
920
- *
921
- * The bins, cursors, keys and dispatch args the bucketing needs alongside it live and die inside
922
- * one frame graph, so `coherence/` declares them on the passes that write them and the graph
923
- * owns them.
924
- *
925
- * @param {number} buckets
926
- */
927
- #ensure_sim_buffers(buckets) {
928
- if (buckets <= this.#sim_bucket_capacity) {
929
- return;
930
- }
931
-
932
- const device = this.graphics.device;
933
- const buffers = this.buffers;
934
-
935
- // Padding rounds each bucket up to a whole workgroup, so it can add at most `workgroup - 1`
936
- // entries per bucket; one whole workgroup per bucket is the bound.
937
- const previous = buffers.sim_list;
938
- buffers.sim_list = device.createBuffer({
939
- label: "particles/sim_list",
940
- size: (this.capacity + buckets * PARTICLE_SIMULATE_WORKGROUP_SIZE) * WORD,
941
- usage: STORAGE_GROWABLE,
942
- });
943
-
944
- if (previous !== undefined) {
945
- const cmd = ShadeGPUCommandContext.create(this.graphics, "particles/coherence grow");
946
- cmd.gpu_encoder.copyBufferToBuffer(previous, 0, buffers.sim_list, 0, previous.size);
947
- cmd.finish();
948
- previous.destroy();
949
- }
950
-
951
- this.#sim_bucket_capacity = buckets;
952
- }
953
-
954
- /**
955
- * Give this frame its alive list, and hand back the ones nothing reads any more.
956
- *
957
- * The list is the allocator's memory on loan rather than a pair of buffers reserved here — see
958
- * the class doc. Three things happen, in this order:
959
- *
960
- * 1. A list an earlier frame retired goes back. That frame's graph has been recorded, executed
961
- * and submitted since, so the bucketing pass that was reading it is done.
962
- * 2. The previous frame's list goes back too — BEFORE the new one is asked for, so that the
963
- * pool can answer with the same memory and the steady-state footprint is one list. Skipped
964
- * when this frame rebuilds the buckets, because that pass reads it; it is retired instead
965
- * and goes back on the next call, one step behind.
966
- * 3. The new list is taken. Uncleared: the frame appends to it from a counter its first pass
967
- * zeroes, so no reader can reach what the last borrower left.
968
- *
969
- * The pool may answer with a larger buffer than asked for, which changes nothing — every
970
- * reader is bounded by a count, never by the buffer's length.
971
- *
972
- * @param {boolean} rebucket whether this frame's bucketing rebuild reads the previous list
973
- */
974
- #swap_alive_list(rebucket) {
975
- const allocator = this.graphics.buffer_allocator_main;
976
- const buffers = this.buffers;
977
- const previous = this.#alive;
978
-
979
- // One is taken at construction and none is ever let go without another taking its place, so
980
- // there is always a previous list — including on the first frame, which can already be a
981
- // rebucket if a program was edited before it.
982
- assert.notNull(previous, "the previous alive list");
983
-
984
- if (this.#alive_retired !== null) {
985
- allocator.release(this.#alive_retired);
986
- this.#alive_retired = null;
987
- }
988
-
989
- if (rebucket) {
990
- this.#alive_retired = previous;
991
- } else {
992
- allocator.release(previous);
993
- }
994
-
995
- const alive = allocator.get(this.#alive_descriptor);
996
-
997
- this.#alive = alive;
998
-
999
- buffers.alive = alive;
1000
- buffers.alive_write = alive;
1001
- // Named only on the frames that read one: on every other frame `previous` has already gone
1002
- // back to the pool, and may be somebody else's by the time the graph runs.
1003
- buffers.alive_read = rebucket ? previous : undefined;
1004
- }
1005
-
1006
- /**
1007
- * Hand every alive list back to the allocator and stop naming them.
1008
- *
1009
- * {@link destroy} cannot simply destroy them with the rest: the memory is the pool's, and a
1010
- * pooled buffer destroyed behind the pool's back is a handle it will hand to the next caller.
1011
- */
1012
- #release_alive_lists() {
1013
- const allocator = this.graphics.buffer_allocator_main;
1014
-
1015
- if (this.#alive !== null) {
1016
- allocator.release(this.#alive);
1017
- this.#alive = null;
1018
- }
1019
-
1020
- if (this.#alive_retired !== null) {
1021
- allocator.release(this.#alive_retired);
1022
- this.#alive_retired = null;
1023
- }
1024
-
1025
- delete this.buffers.alive;
1026
- delete this.buffers.alive_write;
1027
- delete this.buffers.alive_read;
1028
- }
1029
-
1030
- /**
1031
- * Fill the free list with every slot, on the GPU — see `shaders/shader_particle_init_free_list.js`
1032
- * for why this is a dispatch and not a host write.
1033
- *
1034
- * The launch is clamped to what a dispatch dimension holds and the shader strides through the
1035
- * rest: at the default capacity this is a couple of thousand workgroups, but the capacity is
1036
- * the caller's to choose and a dispatch past `maxComputeWorkgroupsPerDimension` is refused.
1037
- */
1038
- #init_free_list() {
1039
- const device = this.graphics.device;
1040
-
1041
- const cmd = ShadeGPUCommandContext.create(this.graphics, "particles/free list init");
1042
-
1043
- shader_particle_init_free_list.dispatch({
1044
- encoder: cmd,
1045
- group_count_x: Math.min(
1046
- Math.ceil(this.capacity / PARTICLE_INIT_FREE_LIST_WORKGROUP_SIZE),
1047
- device.limits.maxComputeWorkgroupsPerDimension,
1048
- ),
1049
- bindings: { dead: this.buffers.dead, u_capacity: this.capacity },
1050
- });
1051
-
1052
- cmd.finish();
1053
- }
1054
-
1055
- /**
1056
- * The buffers whose size is fixed by the pool capacity, and the free list's initial contents.
1057
- */
1058
- #create_buffers() {
1059
- const device = this.graphics.device;
1060
- const capacity = this.capacity;
1061
- const buffers = this.buffers;
1062
-
1063
- buffers.pool = device.createBuffer({ label: "particles/pool", size: capacity * PARTICLE_RECORD_WORD_COUNT * WORD, usage: STORAGE });
1064
-
1065
- this.#alive_descriptor = BufferResourceDescriptor.from(capacity * WORD, STORAGE);
1066
- this.#alive = this.graphics.buffer_allocator_main.get(this.#alive_descriptor);
1067
- buffers.alive = this.#alive;
1068
-
1069
- buffers.dead = device.createBuffer({ label: "particles/dead", size: capacity * WORD, usage: STORAGE });
1070
- // Just the counters to begin with. The per-emitter bounds accumulator that shares this
1071
- // buffer follows the emitter table's capacity, and there is no table yet.
1072
- buffers.counters = device.createBuffer({ label: "particles/counters", size: PARTICLE_COUNTER_COUNT * WORD, usage: STORAGE_GROWABLE });
1073
-
1074
- // Simulate's workgroup count, written by the END of one frame's build-indirect and read by
1075
- // the NEXT frame's dispatch — the one indirect buffer that crosses the boundary, and so the
1076
- // one the graph cannot own. Zero-initialised (WebGPU spec), so the first frame's indirect
1077
- // dispatch is a no-op until the first build-indirect has run.
1078
- buffers.dispatch_args = device.createBuffer({ label: "particles/dispatch_args", size: 3 * WORD, usage: INDIRECT });
1079
-
1080
- buffers.spawn_commands = device.createBuffer({ label: "particles/spawn_commands", size: this.#spawn_commands.byteLength, usage: STORAGE_UPLOAD });
1081
-
1082
- // The warm-up queue: one entry to start with, grown on upload to what a frame registers.
1083
- buffers.warmup_queue = device.createBuffer({ label: "particles/warmup_queue", size: PARTICLE_WARMUP_QUEUE_WORDS * WORD, usage: STORAGE_UPLOAD });
1084
-
1085
- // The counters say every slot is free; the pass below makes it so. A handful of words is
1086
- // the whole of what the host ever writes into either — the list itself is the GPU's.
1087
- const counters = new Uint32Array(PARTICLE_COUNTER_COUNT);
1088
- counters[PARTICLE_COUNTER.DEAD] = capacity;
1089
-
1090
- device.queue.writeBuffer(buffers.counters, 0, counters);
1091
-
1092
- this.#init_free_list();
1093
- }
1094
-
1095
- /**
1096
- * A 1x1 white atlas and a linear sampler, so the system draws before anyone hands it art.
1097
- * @returns {void}
1098
- */
1099
- #create_default_atlas() {
1100
- const device = this.graphics.device;
1101
-
1102
- const texture = new GPUTextureContext(device);
1103
- texture.descriptor.label = "particles/default_atlas";
1104
- texture.descriptor.size = [1, 1];
1105
- texture.descriptor.format = "rgba8unorm";
1106
- texture.descriptor.usage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST;
1107
-
1108
- device.queue.writeTexture({ texture: texture.gpu_texture }, new Uint8Array([255, 255, 255, 255]), { bytesPerRow: 4 }, [1, 1]);
1109
- texture.incrementVersion();
1110
-
1111
- // kept, not just viewed: a texture nothing holds is a texture nothing can free
1112
- this.#default_atlas = texture;
1113
- this.atlas = texture.obtainView();
1114
- this.atlas_sampler = device.createSampler({
1115
- label: "particles/atlas_sampler",
1116
- magFilter: "linear",
1117
- minFilter: "linear",
1118
- addressModeU: "clamp-to-edge",
1119
- addressModeV: "clamp-to-edge",
1120
- });
1121
- }
1122
-
1123
- /**
1124
- * Sizes of the persistent buffers, in bytes, as they stand — one entry per distinct buffer, so
1125
- * this is the feature's footprint and not a list of the names it binds them under.
1126
- * `alive_write` is `alive` under the frame's name for it and would otherwise be counted twice;
1127
- * `alive_read` is a second list, and appears only on the frames that hold one. Exposed for tests.
1128
- *
1129
- * @returns {Object<string, number>}
1130
- */
1131
- bufferSizes() {
1132
- const sizes = {};
1133
- const counted = new Set();
1134
-
1135
- for (const [name, buffer] of Object.entries(this.buffers)) {
1136
- if (buffer === undefined || buffer === null || counted.has(buffer)) {
1137
- continue;
1138
- }
1139
-
1140
- counted.add(buffer);
1141
- sizes[name] = buffer.size;
1142
- }
1143
-
1144
- return sizes;
1145
- }
1146
-
1147
- /**
1148
- * Copy the counts the GPU keeps back to the host and decode them, for display and diagnostics.
1149
- *
1150
- * Read back to *look at*: nothing here may size a pass. Every number describes a frame that has
1151
- * already been submitted, so by the time one arrives it is at best one frame stale — and sizing
1152
- * a dispatch or a draw from a stale count is a correctness bug, not an inaccuracy, for the
1153
- * reasons `shader_particle_build_indirect` sets out. This is the whole of what the feature ever
1154
- * reads back, and the only reason {@link INDIRECT} carries `COPY_SRC` at all.
1155
- *
1156
- * The draw args are not here and cannot be: they never leave the frame graph, which allocates
1157
- * them at the build-indirect pass and releases them after the last draw that reads them. Their
1158
- * instance count is {@link ParticleSystemStats#alive}, which the counters carry anyway.
1159
- *
1160
- * Two copies, two submissions and a map round trip, so a caller polling this for an overlay
1161
- * wants to rate-limit itself and to hold off while a previous read is still outstanding.
1162
- *
1163
- * @returns {Promise<ParticleSystemStats>}
1164
- */
1165
- async read_stats() {
1166
- const device = this.graphics.device;
1167
- const buffers = this.buffers;
1168
-
1169
- const read = (buffer, words) => inspect_gpu_buffer({
1170
- device,
1171
- buffer,
1172
- buffer_size: words * WORD,
1173
- // The mapped range is only valid for the length of the callback, so the decode either
1174
- // happens inside it or takes a copy. A copy, so one `Promise.all` sees both buffers.
1175
- callback: bytes => new Uint32Array(bytes.slice(0)),
1176
- });
1177
-
1178
- const [counters, dispatch] = await Promise.all([
1179
- // The head of the buffer only: what follows the counters is the per-emitter bounds
1180
- // accumulator, which is boxes rather than counts and grows with the emitter table.
1181
- read(buffers.counters, PARTICLE_COUNTER_COUNT),
1182
- read(buffers.dispatch_args, 3),
1183
- ]);
1184
-
1185
- return {
1186
- alive: counters[PARTICLE_COUNTER.ALIVE],
1187
- dead: counters[PARTICLE_COUNTER.DEAD],
1188
- spawn_budget: counters[PARTICLE_COUNTER.SPAWN_BUDGET],
1189
- alive_in: counters[PARTICLE_COUNTER.ALIVE_IN],
1190
- sim_padded: counters[PARTICLE_COUNTER.SIM_PADDED],
1191
- sim_workgroups: dispatch[0],
1192
- };
1193
- }
1194
-
1195
- destroy() {
1196
- /*
1197
- The emitters go first, and they matter more than the buffers: they belong to their owners,
1198
- they outlive this system, and each is holding a row of the table about to be freed.
1199
- Retiring them here puts every emitter back where it was before this system saw it, so its
1200
- owner can register it with the next system.
1201
- */
1202
- for (const emitter of this.registry.emitters.slice()) {
1203
- this.registry.remove(emitter);
1204
- }
1205
-
1206
- // Before the loop below, and not in it: the alive lists are the allocator's and go back to
1207
- // it. Destroying one would leave the pool holding a handle it will hand to the next caller.
1208
- this.#release_alive_lists();
1209
-
1210
- for (const buffer of Object.values(this.buffers)) {
1211
- if (buffer !== undefined && buffer !== null) {
1212
- buffer.destroy();
1213
- }
1214
- }
1215
-
1216
- this.buffers = {};
1217
- this.registry.database.destroy();
1218
-
1219
- // the default atlas is this system's own; a page that replaced it owns what it put there
1220
- this.#default_atlas?.destroy();
1221
- this.#default_atlas = null;
1222
- }
1223
- }
1224
-
1225
- /**
1226
- * A storage buffer holding `data`, replacing `previous` (destroyed) if it exists. The program
1227
- * buffer is monolithic — a program is variable-length, so there is no table to page it into — and
1228
- * is rebuilt whole on the rare frame the program heap changes.
1229
- *
1230
- * @param {GPUDevice} device
1231
- * @param {GPUBuffer|undefined} previous
1232
- * @param {string} label
1233
- * @param {Uint32Array} data
1234
- * @returns {GPUBuffer}
1235
- */
1236
- function replace_data_buffer(device, previous, label, data) {
1237
- previous?.destroy();
1238
-
1239
- const buffer = device.createBuffer({ label, size: Math.max(WORD, data.byteLength), usage: STORAGE_UPLOAD });
1240
-
1241
- device.queue.writeBuffer(buffer, 0, data);
1242
-
1243
- return buffer;
1244
- }
1
+ import { assert } from "../../../core/assert.js";
2
+ import { ShadeGPUCommandContext } from "../../device/ShadeGPUCommandContext.js";
3
+ import { BufferResourceDescriptor } from "../../device/graph/BufferResourceDescriptor.js";
4
+ import { warn_limited } from "../../util/warn_limited.js";
5
+ import { hash_mix2 } from "../../../core/math/hash/hash_mix2.js";
6
+ import {
7
+ PARTICLE_EMITTER_TICK_WORKGROUP_SIZE,
8
+ PARTICLE_RECORD_WORD_COUNT,
9
+ PARTICLE_SIMULATE_WORKGROUP_SIZE,
10
+ PARTICLE_SPAWN_COMMAND_WORDS,
11
+ PARTICLE_VM_FAST_REGISTER_SLOTS,
12
+ PARTICLE_WARMUP_EMITTER_JITTER,
13
+ PARTICLE_WARMUP_QUEUE_WORDS,
14
+ } from "./ParticleConstants.js";
15
+ import {
16
+ PARTICLE_BOUNDS_WORDS,
17
+ PARTICLE_COUNTER,
18
+ PARTICLE_COUNTER_COUNT,
19
+ particle_bounds_empty_words,
20
+ particle_counters_word_count,
21
+ } from "./data/PARTICLE_COUNTERS.js";
22
+ import { graph_particles_simulate } from "./graph_particles.js";
23
+ import {
24
+ PARTICLE_INIT_FREE_LIST_WORKGROUP_SIZE,
25
+ shader_particle_init_free_list,
26
+ } from "./shaders/shader_particle_init_free_list.js";
27
+ import {
28
+ graph_particle_avboit_draw,
29
+ graph_particle_avboit_occupancy,
30
+ graph_particle_avboit_splat,
31
+ } from "./graph_particles_avboit.js";
32
+ import { EmitterRegistry } from "./runtime/EmitterRegistry.js";
33
+ import { WARMUP_QUEUE } from "./warmup/shader_particle_warmup.js";
34
+ import { warmup_schedule } from "./warmup/warmup_schedule.js";
35
+ import { GPUTextureContext } from "../texture/GPUTextureContext.js";
36
+ import { inspect_gpu_buffer } from "../buffer/inspect_gpu_buffer.js";
37
+
38
+ const WORD = Uint32Array.BYTES_PER_ELEMENT;
39
+
40
+ /*
41
+ A wide usage mask costs bandwidth: the driver places a buffer by what it was told the buffer might
42
+ have to do, and the flags below are what each one actually does. `pool` and the alive lists are the
43
+ big ones and are touched by nothing but shaders. Adding a flag "just in case" moves 64 MiB onto a
44
+ slower path for a copy nobody makes — see `BufferResourceDescriptor`.
45
+ */
46
+ /** Written only by shaders. The free list is one of these: `init_free_list` fills it on-device. */
47
+ const STORAGE = GPUBufferUsage.STORAGE;
48
+ /** Seeded or refilled from the host with `writeBuffer`. */
49
+ const STORAGE_UPLOAD = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST;
50
+ /** Uploaded, and copied forward into its replacement when the emitter set outgrows it. */
51
+ const STORAGE_GROWABLE = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC;
52
+ /**
53
+ * An indirect command the GPU writes and the GPU consumes. COPY_SRC is for {@link
54
+ * GPUParticleSystem#read_stats}, which reads the args back to show that the dispatch really is
55
+ * sized on-device — twelve bytes.
56
+ */
57
+ const INDIRECT = GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_SRC;
58
+
59
+ const DEFAULT_CAPACITY = 524288;
60
+
61
+ /**
62
+ * What {@link GPUParticleSystem#read_stats} decodes: the counts the GPU keeps between frames, named.
63
+ *
64
+ * @typedef {object} ParticleSystemStats
65
+ * @property {number} alive live particles the frame ended with
66
+ * @property {number} dead length of the free list
67
+ * @property {number} spawn_budget free slots the frame never promised to a spawn — its headroom
68
+ * @property {number} alive_in entries the frame's simulate consumed — the padded, program-coherent
69
+ * list the frame before it built, so never fewer than that frame's `alive`
70
+ * @property {number} sim_padded length of the padded, program-coherent simulate list built this
71
+ * frame; the gap to `alive` is what keeping each wave on one VM program costs in lanes
72
+ * @property {number} sim_workgroups workgroups the NEXT simulate will dispatch — read out of the
73
+ * indirect args rather than derived, so it is what the GPU will actually launch
74
+ */
75
+
76
+ /**
77
+ * The GPU particle system: a feature of a scene, in the mould of `ReSTIRDI` — construct once
78
+ * against a {@link GPUSceneContext}, and contribute the frame's passes through
79
+ * {@link execute}.
80
+ *
81
+ * ## What it owns
82
+ *
83
+ * Persistent GPU state, and only that: a buffer is reserved here if and only if its contents have
84
+ * to survive to the next frame. That is the particle pool, the alive list, the dead
85
+ * free-list, the counters, the simulation-order (coherence) list, simulate's indirect dispatch args
86
+ * (written at the end of one frame, read by the next), the emitter database (the emitter table and
87
+ * the GPU-owned state table, one buffer), the packed program buffer, and the two CPU-uploaded
88
+ * queues — the spawn-command ring and the warm-up queue. Buffers that depend on the emitter set —
89
+ * the coherence list on the number of distinct programs, the program buffer on the heap, the
90
+ * database on its own pages, the counters on the emitter table's element capacity (they carry the
91
+ * per-emitter bounds accumulator, see `bounds/`) — are grown inside {@link execute}, the way
92
+ * ReSTIR's reservoirs follow the resolution.
93
+ *
94
+ * Everything else a frame touches is the frame graph's. The bucketing's bins, cursors and keys, the
95
+ * draw args, the spawn counts and the VM's wide register file are produced and consumed inside one
96
+ * graph, so the pass that first writes each one declares it and the graph allocates it just in time
97
+ * and releases it after its last reader — into the pool the rest of the renderer allocates from.
98
+ * Reserving them here instead would be megabytes at the default capacity that no other feature
99
+ * could ever borrow, and most of them are cleared by a reset pass every frame regardless.
100
+ *
101
+ * ## The alive list is borrowed, not reserved
102
+ *
103
+ * The alive list crosses the frame boundary and so cannot be the graph's — but it does not have to
104
+ * be the feature's either. It is taken from the graphics context's buffer allocator and given back
105
+ * every frame ({@link #swap_alive_list}), which is the difference between one list and two.
106
+ *
107
+ * Two was the obvious reading of the ping-pong: the frame builds a new list while the previous one
108
+ * is still there to be read. But nothing reads the previous one. Emit and simulate append to the
109
+ * new list from a counter the frame resets to zero, the draws walk that same new list, and the only
110
+ * pass that ever looks at the one before it is the bucketing rebuild on a frame whose program
111
+ * assignments changed — a live program edit, not a frame. So on an ordinary frame the previous list
112
+ * goes back to the pool BEFORE the new one is asked for, the pool answers with the same memory, and
113
+ * the feature's reserved footprint is one list rather than two. On the rare frame that does rebuild,
114
+ * the old list is held one frame longer — until the graph recorded around it has been submitted —
115
+ * and the feature briefly costs what it used to cost always.
116
+ *
117
+ * Every pass stays within the eight storage buffers a stage gets by default: the emitter record
118
+ * and the GPU-owned state are two tables of one database buffer, and the VM's constants and code
119
+ * are one packed buffer. The one exception is the wide-register fallback (below), whose emit and
120
+ * simulate passes bind a ninth for the register file — inside the ten the engine already demands of
121
+ * an adapter, but past the default.
122
+ *
123
+ * ## Two interpreters
124
+ *
125
+ * The VM's register file normally lives in shader registers, and is therefore small — see
126
+ * {@link ./ParticleConstants.js PARTICLE_VM_FAST_REGISTER_SLOTS}. An effect whose compiled program
127
+ * needs more than it holds does not fail: the whole system swaps emit and simulate for variants
128
+ * that keep the file in a storage buffer, which is far slower and effectively unbounded. The choice
129
+ * is per-frame, whole-system, and made here — the registry reports `max_register_count` over the
130
+ * effects it holds and this is where that number meets the VM's; nothing about authoring a graph has
131
+ * a register ceiling.
132
+ *
133
+ * ## Emitters are registered at rows
134
+ *
135
+ * {@link add_emitter} gives a {@link ParticleEmitter} a row in the emitter table pointed at a
136
+ * `transforms` row — an entity's, one on, held through the scene context's rows by whoever owns
137
+ * the entity — and it starts spawning on the next {@link execute}; under a joint's row it moves
138
+ * with the joint. {@link remove_emitter} retires the row and frees its particles. Nothing here
139
+ * walks the scene: an emitter is where its owner says it is, and per-frame upkeep is proportional
140
+ * to what changed rather than to how many emitters there are.
141
+ *
142
+ * ## Spawning is the GPU's decision
143
+ *
144
+ * The CPU never counts particles or walks emitters per frame. The emitter tick pass integrates
145
+ * every emitter's rate on the GPU, and the emit dispatch is sized by a prefix scan of what it
146
+ * decided. The host's one way to ask for particles is {@link spawn}: a burst queued into a small
147
+ * command list that a pass adds to the emitter's pending spawns, on the same path as everything
148
+ * else. See `graph_particles.js` for the frame.
149
+ *
150
+ * ## Drawing
151
+ *
152
+ * {@link execute} is the whole per-frame call, once per frame, and it records only the simulation:
153
+ * everything that moves particles, leaving the alive list and the indirect draw args as graph
154
+ * handles. It draws nothing.
155
+ *
156
+ * The drawing is the transparency pipeline's. The system is an `AVBOITSideChannel`, and the AVBOIT
157
+ * orchestrator asks it for its occupancy, its extinction and its draw at the three points where the
158
+ * transparent meshes have contributed theirs ({@link avboit_occupancy}, {@link avboit_splat},
159
+ * {@link avboit_draw}). Those read the handles `execute` produced and composite into a voxel volume
160
+ * and a set of accumulators that are sums — so the particles need no depth ordering at all, and
161
+ * none is ever built.
162
+ *
163
+ * ## Allocation
164
+ *
165
+ * The per-frame path allocates the frame graph's records and nothing else: emitter rows are
166
+ * staged through one shared record, spawn commands go into a preallocated ring, and the
167
+ * per-dispatch settings are reused objects. A command context is opened only on frames that have
168
+ * something to upload.
169
+ */
170
+ export class GPUParticleSystem {
171
+
172
+ /**
173
+ * Engine GraphicsContext.
174
+ * @type {object}
175
+ */
176
+ graphics;
177
+
178
+ /**
179
+ * The scene whose rows the emitters are placed at, and whose database their world matrices
180
+ * come from.
181
+ * @type {import("../scene/GPUSceneContext.js").GPUSceneContext}
182
+ */
183
+ scene_context;
184
+
185
+ /**
186
+ * Maximum number of simultaneously live particles.
187
+ * @type {number}
188
+ */
189
+ capacity;
190
+
191
+ /**
192
+ * @type {EmitterRegistry}
193
+ */
194
+ registry;
195
+
196
+ /**
197
+ * Frames elapsed. The VM's frame builtin.
198
+ * @type {number}
199
+ */
200
+ frame = 0;
201
+
202
+ /**
203
+ * Seconds since start, the VM's `TIME` builtin. Advanced by `dt` each {@link execute}.
204
+ * @type {number}
205
+ */
206
+ time = 0;
207
+
208
+ /**
209
+ * Cap on how much missed time a culled emitter makes up for when it comes back into view.
210
+ * @type {number}
211
+ */
212
+ max_catchup_seconds = 1;
213
+
214
+ /**
215
+ * Sprite atlas the billboards sample, and the sampler they sample it with. A 1x1 white texel by
216
+ * default, so an emitter with no art draws as a plain quad rather than nothing.
217
+ * @type {GPUTextureView}
218
+ */
219
+ atlas;
220
+
221
+ /** @type {GPUSampler} */
222
+ atlas_sampler;
223
+
224
+ /**
225
+ * The persistent GPU buffers, by the names the graph functions bind them under. Exposed for
226
+ * inspection — {@link bufferSizes} measures them, {@link read_stats} reads the counts out of
227
+ * them — never for the frame loop, which reads nothing back. Entries that grow with the
228
+ * emitter set are replaced in place.
229
+ *
230
+ * `alive` and `alive_write` are one buffer under two names — what the feature holds, and what
231
+ * the frame writes — and `alive_read` is the previous frame's list, present only on the frames
232
+ * that read one. The list itself belongs to the allocator, not here; see {@link #swap_alive_list}.
233
+ *
234
+ * @type {object}
235
+ */
236
+ buffers = {};
237
+
238
+ /**
239
+ * Distinct programs the coherence scratch is currently sized for.
240
+ * @type {number}
241
+ */
242
+ #sim_bucket_capacity = 0;
243
+
244
+ /**
245
+ * What the alive list is asked for under: one `u32` per pool slot, touched by nothing but
246
+ * shaders, and deliberately not cleared — every entry a reader sees was appended this frame,
247
+ * from a counter the frame's first pass resets to zero, so whatever the last borrower left in
248
+ * it is unreadable by construction. One instance, reused: the pool reads a descriptor and does
249
+ * not keep it.
250
+ *
251
+ * @type {BufferResourceDescriptor}
252
+ */
253
+ #alive_descriptor;
254
+
255
+ /**
256
+ * The alive list the last recorded frame built — the next frame's `alive_read`. Borrowed from
257
+ * `graphics.buffer_allocator_main`, so it is released rather than destroyed.
258
+ * @type {GPUBuffer|null}
259
+ */
260
+ #alive = null;
261
+
262
+ /**
263
+ * An alive list a recorded frame still reads: the bucketing rebuild's input on a frame whose
264
+ * program assignments changed. Handed back on the next {@link execute}, by which point that
265
+ * frame's graph has been executed and submitted.
266
+ * @type {GPUBuffer|null}
267
+ */
268
+ #alive_retired = null;
269
+
270
+ /**
271
+ * The spawn-command ring: `[row, count]` pairs, filled by {@link spawn}, merged and uploaded on
272
+ * the next {@link execute}.
273
+ * @type {Uint32Array}
274
+ */
275
+ #spawn_commands;
276
+
277
+ /**
278
+ * The emitter each ring entry was issued for, so an entry whose emitter has since left the
279
+ * scene — or whose row has gone to somebody else — is dropped at upload rather than delivered
280
+ * to the row's next tenant.
281
+ * @type {import("./runtime/ParticleEmitter.js").ParticleEmitter[]}
282
+ */
283
+ #spawn_command_emitters;
284
+
285
+ /** @type {number} */
286
+ #spawn_command_count = 0;
287
+
288
+ /**
289
+ * Scratch for merging the ring: pending count by row. Grown with the emitter table, never per
290
+ * frame.
291
+ * @type {Uint32Array}
292
+ */
293
+ #spawn_by_row = new Uint32Array(0);
294
+
295
+ /**
296
+ * Emitters registered since the last frame with a pre-warm to run — the input of the warm-up
297
+ * pipeline (`warmup/`), consumed by the frame that records it. Filled where the emitter is
298
+ * registered ({@link add_emitter}), which is the one moment the host knows an emitter is
299
+ * new: the GPU decides everything else about spawning, but "this one has just arrived and asks
300
+ * to arrive warm" is a host-side fact.
301
+ *
302
+ * @type {import("./runtime/ParticleEmitter.js").ParticleEmitter[]}
303
+ */
304
+ #warmup_queue = [];
305
+
306
+ /**
307
+ * The queue as the GPU reads it, `PARTICLE_WARMUP_QUEUE_WORDS` per entry — see
308
+ * `warmup/shader_particle_warmup.js`. Grown to fit, never per frame.
309
+ * @type {Uint32Array}
310
+ */
311
+ #warmup_words = new Uint32Array(0);
312
+
313
+
314
+ /**
315
+ * The 1x1 white texture {@link atlas} points at until a caller replaces it. Held so
316
+ * {@link destroy} can free it; a caller that assigns its own atlas owns that one.
317
+ * @type {GPUTextureContext|null}
318
+ */
319
+ #default_atlas = null;
320
+
321
+
322
+ /**
323
+ * Program assignments used to build the previous frame's coherence list.
324
+ * @type {number}
325
+ */
326
+ #program_assignment_version = 0;
327
+
328
+ /**
329
+ * Per-frame scalars handed to {@link graph_particles_simulate}, reused rather than rebuilt.
330
+ * @type {object}
331
+ */
332
+ #frame = {
333
+ dt: 0, time: 0, index: 0, max_catchup: 1, spawn_command_count: 0,
334
+ warmup_count: 0, warmup_capacity: 0, warmup_schedule: null,
335
+ rebucket: false,
336
+ };
337
+
338
+ /** @type {{capacity: number, group_count: number}} */
339
+ #emitters = { capacity: 0, group_count: 0 };
340
+
341
+ /**
342
+ * This frame's simulation, for the side channels that draw it later in the frame: the graph it
343
+ * was recorded into, the scene database handle it read, and the handles it produced. Reused,
344
+ * never reallocated.
345
+ * @type {{graph: object|null, scene_database: number, emitter_group_count: number, handles: object|null}}
346
+ */
347
+ #simulated = { graph: null, scene_database: -1, emitter_group_count: 0, handles: null };
348
+
349
+ /**
350
+ * @param {object} graphics engine GraphicsContext (has `.device`)
351
+ * @param {import("../scene/GPUSceneContext.js").GPUSceneContext} scene_context
352
+ * @param {object} [options]
353
+ * @param {number} [options.capacity] max simultaneous particles
354
+ * @param {number} [options.spawn_command_capacity] most {@link spawn} calls one frame keeps
355
+ */
356
+ constructor(graphics, scene_context, { capacity = DEFAULT_CAPACITY, spawn_command_capacity = 256 } = {}) {
357
+ assert.defined(graphics, "graphics");
358
+ assert.defined(scene_context, "scene_context");
359
+ assert.equal(scene_context.isGPUSceneContext, true, "scene_context.isGPUSceneContext !== true");
360
+ assert.isPositiveInteger(capacity, "capacity");
361
+ assert.isPositiveInteger(spawn_command_capacity, "spawn_command_capacity");
362
+
363
+ this.graphics = graphics;
364
+ this.scene_context = scene_context;
365
+ this.capacity = capacity;
366
+
367
+ this.registry = new EmitterRegistry(graphics.device);
368
+
369
+ this.#spawn_commands = new Uint32Array(spawn_command_capacity * PARTICLE_SPAWN_COMMAND_WORDS);
370
+ this.#spawn_command_emitters = new Array(spawn_command_capacity).fill(null);
371
+
372
+ this.#create_buffers();
373
+ this.#create_default_atlas();
374
+ }
375
+
376
+ /**
377
+ * The buffer the emitter table lives in — what every emitter-reading pass binds.
378
+ * @returns {GPUBuffer}
379
+ */
380
+ get emitter_database() {
381
+ return this.registry.database.buffer;
382
+ }
383
+
384
+ /**
385
+ * The live emitters, in no particular order.
386
+ * @returns {import("./runtime/ParticleEmitter.js").ParticleEmitter[]}
387
+ */
388
+ get emitters() {
389
+ return this.registry.emitters;
390
+ }
391
+
392
+ /**
393
+ * Number of simulation-order buckets — one per program id the registry can hand out, which is
394
+ * its peak number of distinct compiled programs rather than its current one.
395
+ *
396
+ * This is the coherence pass's only host-provided number, and it is static topology rather than
397
+ * per-frame state: it changes when an effect the set has never held is registered, not as
398
+ * particles are born and die. Everything the pass measures per frame stays on the GPU. Sizing it
399
+ * from the peak rather than the live count is what keeps it from moving every time an effect is
400
+ * unregistered — the bins of released ids are empty, and an empty bin rounds up to zero
401
+ * workgroups and shifts nothing after it.
402
+ *
403
+ * @returns {number}
404
+ */
405
+ bucketCount() {
406
+ return Math.max(1, this.registry.program_id_capacity);
407
+ }
408
+
409
+ /**
410
+ * Ask for `count` particles from an emitter, on top of its rate, on the next frame. Goes
411
+ * through the GPU path — the tick pass folds it into the emitter's accumulator, the budget and
412
+ * the culling apply — so it is a request, not a guarantee.
413
+ *
414
+ * @param {import("./runtime/ParticleEmitter.js").ParticleEmitter} emitter a registered emitter
415
+ * @param {number} count
416
+ */
417
+ spawn(emitter, count) {
418
+ assert.defined(emitter, "emitter");
419
+ assert.isNonNegativeInteger(count, "count");
420
+
421
+ const context = this.registry.context(emitter);
422
+
423
+ assert.defined(context, "emitter is not registered");
424
+
425
+ const ring = this.#spawn_commands;
426
+ const base = this.#spawn_command_count * PARTICLE_SPAWN_COMMAND_WORDS;
427
+
428
+ if (base >= ring.length) {
429
+ // the text is the key warn_limited counts on, so it carries no burst size: one message
430
+ // per ring size, not one per number a caller happens to ask for
431
+ warn_limited(`GPUParticleSystem: spawn command ring is full (${ring.length / PARTICLE_SPAWN_COMMAND_WORDS} per frame); dropping this frame's remaining bursts`);
432
+ return;
433
+ }
434
+
435
+ ring[base] = context.row;
436
+ ring[base + 1] = count;
437
+ this.#spawn_command_emitters[this.#spawn_command_count] = emitter;
438
+ this.#spawn_command_count++;
439
+ }
440
+
441
+ /**
442
+ * The system's whole contribution to the frame, once per frame: reconcile the emitter set with
443
+ * the scene, push what changed to the GPU, and record every pass that moves particles — through
444
+ * to the indirect draw args.
445
+ *
446
+ * It draws nothing and touches no colour target. The handles it returns are what the AVBOIT
447
+ * side channels ({@link avboit_occupancy}, {@link avboit_splat}, {@link avboit_draw}) consume
448
+ * later in the same frame, off this same graph, once the transparent meshes have contributed
449
+ * theirs.
450
+ *
451
+ * Call after the scene context has built for the frame (`GPUSceneContext#update`), so every
452
+ * emitter's `transforms` row is uploaded, and after the hierarchy pass has run, so the rows'
453
+ * `global` matrices are this frame's.
454
+ *
455
+ * **Once per frame, not once per view.** This advances the simulation by `dt` and hands out
456
+ * handles into the graph it recorded into; a second view of the same scene records a second
457
+ * graph and would step the simulation twice. The renderer has one view per `render` call today,
458
+ * so this holds by construction; a second view of one scene needs the step and the handles
459
+ * separated — record the passes for the first graph, import the same buffers into the rest.
460
+ *
461
+ * @param {object} params
462
+ * @param {import("../../../engine/graphics/render/frame_graph/FrameGraph.js").FrameGraph} params.graph
463
+ * @param {number} params.camera graph handle of the camera uniform buffer
464
+ * @param {number} params.scene_database graph handle of the scene database — the one the
465
+ * hierarchy pass returned, so this reads the matrices it composed
466
+ * @param {number} params.dt seconds since the previous frame
467
+ * @returns {import("./graph_particles.js").ParticleSimulationHandles}
468
+ */
469
+ execute({ graph, camera, scene_database, dt }) {
470
+ assert.defined(graph, "graph");
471
+ assert.isNumber(dt, "dt");
472
+
473
+ this.#upload();
474
+
475
+ const frame = this.#frame;
476
+ frame.dt = dt;
477
+ frame.time = this.time;
478
+ frame.index = this.frame;
479
+ frame.max_catchup = this.max_catchup_seconds;
480
+ frame.spawn_command_count = this.#spawn_command_count;
481
+ frame.rebucket = this.#program_assignment_version !== this.registry.program_assignment_version;
482
+ this.#program_assignment_version = this.registry.program_assignment_version;
483
+
484
+ const table = this.registry.table;
485
+ const emitters = this.#emitters;
486
+ emitters.capacity = table.element_capacity;
487
+ emitters.group_count = table.dispatch_group_count(PARTICLE_EMITTER_TICK_WORKGROUP_SIZE);
488
+
489
+ const buffers = this.buffers;
490
+
491
+ this.#swap_alive_list(frame.rebucket);
492
+
493
+ // The table's database owns and grows its own buffer; it is re-read every frame for that
494
+ // reason.
495
+ buffers.emitter_database = this.registry.database.buffer;
496
+
497
+ const simulated = graph_particles_simulate({
498
+ graph,
499
+ camera,
500
+ scene_database,
501
+ gpu: buffers,
502
+ emitters,
503
+ capacity: this.capacity,
504
+ bucket_count: this.bucketCount(),
505
+ // Whose register file the VM runs on is decided here, once, for the whole system: the
506
+ // registry reports what the hungriest registered effect needs and this compares it
507
+ // against what the fast file holds. Re-read every frame because registering an emitter —
508
+ // or unregistering the one hungry effect — can flip it either way.
509
+ wide_registers: this.registry.max_register_count > PARTICLE_VM_FAST_REGISTER_SLOTS,
510
+ frame,
511
+ });
512
+
513
+ // Kept for the side channels that draw later in this frame, and for nothing past it.
514
+ const current = this.#simulated;
515
+ current.graph = graph;
516
+ current.scene_database = scene_database;
517
+ current.emitter_group_count = emitters.group_count;
518
+ current.handles = simulated;
519
+
520
+ // Consumed: the counts were recorded into the graph, the words were uploaded.
521
+ this.#spawn_command_count = 0;
522
+ this.#warmup_queue.length = 0;
523
+
524
+ this.frame++;
525
+ this.time += dt;
526
+
527
+ return simulated;
528
+ }
529
+
530
+ /**
531
+ * What this frame's {@link execute} left, checked against the graph a side-channel call names:
532
+ * the AVBOIT orchestrator records its passes into the same graph, later in the same frame.
533
+ *
534
+ * @param {import("../../../engine/graphics/render/frame_graph/FrameGraph.js").FrameGraph} graph
535
+ * @returns {{graph: object, scene_database: number, emitter_group_count: number, handles: import("./graph_particles.js").ParticleSimulationHandles}}
536
+ */
537
+ #simulated_for(graph) {
538
+ const current = this.#simulated;
539
+
540
+ assert.equal(current.graph, graph, "the particles were not simulated into this graph this frame — call execute() before the transparency pipeline records");
541
+
542
+ return current;
543
+ }
544
+
545
+ /**
546
+ * `AVBOITSideChannel#avboit_occupancy`: the emitters' measured bounding boxes, as depth slices.
547
+ *
548
+ * @param {object} args see `AVBOITSideChannel`
549
+ * @param {import("../../../engine/graphics/render/frame_graph/FrameGraph.js").FrameGraph} args.graph
550
+ * @param {number} args.camera graph handle of the camera uniform buffer
551
+ * @param {ArrayLike<number>} args.cluster_parameters the frame's depth curve
552
+ * @param {number} args.occupancy graph handle of the occupancy bits
553
+ * @returns {number} the occupancy handle after the write
554
+ */
555
+ avboit_occupancy({ graph, camera, cluster_parameters, occupancy }) {
556
+ const current = this.#simulated_for(graph);
557
+
558
+ return graph_particle_avboit_occupancy({
559
+ graph,
560
+ camera,
561
+ cluster_parameters,
562
+ occupancy,
563
+ emitter_database: current.handles.emitter_database,
564
+ emitter_group_count: current.emitter_group_count,
565
+ });
566
+ }
567
+
568
+ /**
569
+ * `AVBOITSideChannel#avboit_splat`: the live particles' extinction, into the volume.
570
+ *
571
+ * @param {object} args see `AVBOITSideChannel`
572
+ * @param {import("../../../engine/graphics/render/frame_graph/FrameGraph.js").FrameGraph} args.graph
573
+ * @param {number} args.camera graph handle of the camera uniform buffer
574
+ * @param {number} args.warp graph handle of the warp LUT
575
+ * @param {number} args.depth_texture graph handle of the scene depth
576
+ * @param {{extinction: number, overflow: number, dummy: number}} args.volume the voxel volume
577
+ * @returns {{extinction: number, overflow: number, dummy: number}} the volume after the write
578
+ */
579
+ avboit_splat({ graph, camera, warp, depth_texture, volume }) {
580
+ const { handles } = this.#simulated_for(graph);
581
+
582
+ return graph_particle_avboit_splat({
583
+ graph,
584
+ camera,
585
+ warp,
586
+ depth_texture,
587
+ volume,
588
+ atlas: this.atlas,
589
+ atlas_sampler: this.atlas_sampler,
590
+ pool: handles.pool,
591
+ alive: handles.alive,
592
+ emitter_database: handles.emitter_database,
593
+ draw_args: handles.draw_args,
594
+ });
595
+ }
596
+
597
+ /**
598
+ * `AVBOITSideChannel#avboit_draw`: the live particles, into the accumulators.
599
+ *
600
+ * @param {object} args see `AVBOITSideChannel`
601
+ * @param {import("../../../engine/graphics/render/frame_graph/FrameGraph.js").FrameGraph} args.graph
602
+ * @param {import("../view/GPUViewContext.js").GPUViewContext} args.view_ctx
603
+ * @param {number} args.camera graph handle of the camera uniform buffer
604
+ * @param {number} args.depth_texture graph handle of the scene depth
605
+ * @param {number} args.warp graph handle of the warp LUT
606
+ * @param {number} args.integral graph handle of the integral texture
607
+ * @param {number} args.volumetrics_lut graph handle
608
+ * @param {number} args.volumetrics_metadata graph handle
609
+ * @param {{Y: number, CoCg: number}} args.volumetrics_irradiance graph handles of the froxel
610
+ * irradiance volume, what an emitter flagged `EMITTER_FLAG.LIGHTING` is lit by
611
+ * @param {{color: number, norm: number, extinction: number, emission: number}} args.accumulators
612
+ * @returns {{color: number, norm: number, extinction: number, emission: number}} after the draw
613
+ */
614
+ avboit_draw({ graph, view_ctx, camera, depth_texture, warp, integral, volumetrics_lut, volumetrics_metadata, volumetrics_irradiance, accumulators }) {
615
+ const { handles } = this.#simulated_for(graph);
616
+
617
+ return graph_particle_avboit_draw({
618
+ graph,
619
+ view_ctx,
620
+ camera,
621
+ depth_texture,
622
+ warp,
623
+ integral,
624
+ volumetrics_lut,
625
+ volumetrics_metadata,
626
+ volumetrics_irradiance,
627
+ accumulators,
628
+ atlas: this.atlas,
629
+ atlas_sampler: this.atlas_sampler,
630
+ pool: handles.pool,
631
+ alive: handles.alive,
632
+ emitter_database: handles.emitter_database,
633
+ draw_args: handles.draw_args,
634
+ });
635
+ }
636
+
637
+ /**
638
+ * Register an emitter at a `transforms` row, and if it asks, warm it up before its first frame.
639
+ *
640
+ * The row is an entity's, one on (`row_of_entity`), and whoever owns the entity holds it through
641
+ * the scene context's rows for as long as the emitter is registered; this only records which
642
+ * row the emitter's particles are born at. Nothing is uploaded until the next {@link execute}.
643
+ *
644
+ * @param {import("./runtime/ParticleEmitter.js").ParticleEmitter} emitter
645
+ * @param {number} transform_row
646
+ */
647
+ add_emitter(emitter, transform_row) {
648
+ assert.defined(emitter, 'emitter');
649
+ assert.equal(emitter.isParticleEmitter, true, 'emitter.isParticleEmitter !== true');
650
+ assert.isNonNegativeInteger(transform_row, 'transform_row');
651
+
652
+ this.registry.add(emitter, transform_row);
653
+
654
+ // Just arrived, and asked to arrive warm: the warm-up pipeline runs its simulation forward
655
+ // this frame, before it is first drawn. A reused row is a new emitter as far as this is
656
+ // concerned — it is the emitter that asks, not the row.
657
+ if (emitter.prewarm > 0) {
658
+ this.#warmup_queue.push(emitter);
659
+ }
660
+ }
661
+
662
+ /**
663
+ * Retire an emitter's row; its particles go within a frame, carrying the row's generation.
664
+ *
665
+ * @param {import("./runtime/ParticleEmitter.js").ParticleEmitter} emitter
666
+ */
667
+ remove_emitter(emitter) {
668
+ assert.defined(emitter, 'emitter');
669
+
670
+ this.registry.remove(emitter);
671
+ }
672
+
673
+ /**
674
+ * Point a registered emitter at another row.
675
+ *
676
+ * @param {import("./runtime/ParticleEmitter.js").ParticleEmitter} emitter
677
+ * @param {number} transform_row
678
+ */
679
+ set_transform_row(emitter, transform_row) {
680
+ assert.isNonNegativeInteger(transform_row, 'transform_row');
681
+
682
+ this.registry.set_transform_row(emitter, transform_row);
683
+ }
684
+
685
+ /**
686
+ * Push everything the CPU changed to the GPU: dirty emitter rows, the state buffer's growth,
687
+ * rebuilt program buffers, the spawn-command ring, and the coherence scratch's growth. A
688
+ * command context is opened only if something needs one.
689
+ */
690
+ #upload() {
691
+ const registry = this.registry;
692
+ const device = this.graphics.device;
693
+
694
+ registry.flush();
695
+
696
+ // Both tables stage into the one database: the emitter rows the author changed, and the
697
+ // state rows claimed or released with them.
698
+ const rows_staged = registry.table.element_upload_buffer.position > 0
699
+ || registry.state_table.element_upload_buffer.position > 0;
700
+
701
+ if (rows_staged) {
702
+ const cmd = ShadeGPUCommandContext.create(this.graphics, "particles/upload");
703
+
704
+ registry.database.update(cmd);
705
+
706
+ cmd.finish();
707
+ }
708
+
709
+ if (registry.programs_dirty) {
710
+ this.buffers.program = replace_data_buffer(device, this.buffers.program, "particles/program", registry.build_program());
711
+ }
712
+
713
+ this.#merge_spawn_commands();
714
+
715
+ if (this.#spawn_command_count > 0) {
716
+ device.queue.writeBuffer(
717
+ this.buffers.spawn_commands, 0,
718
+ this.#spawn_commands.buffer, 0,
719
+ this.#spawn_command_count * PARTICLE_SPAWN_COMMAND_WORDS * WORD,
720
+ );
721
+ }
722
+
723
+ this.#upload_warmup_queue();
724
+
725
+ this.#ensure_sim_buffers(this.bucketCount());
726
+ this.#ensure_counters(registry.table.element_capacity);
727
+ }
728
+
729
+ /**
730
+ * Turn the emitters queued for a warm-up into the words the warm-up tick reads, and the numbers
731
+ * the frame graph sizes the loop by; upload the words. Nothing when nothing is queued, which is
732
+ * nearly every frame.
733
+ *
734
+ * Two per-emitter numbers are hashed here rather than left at zero, both to keep emitters warmed
735
+ * in one batch off each other's beat: the accumulator's starting fraction, so their first whole
736
+ * particles fall on different ticks, and the clock each one's tick runs at. See
737
+ * `warmup/shader_particle_warmup.js`.
738
+ *
739
+ * The transient pool is sized by what the batch can spawn in all — every emitter's rate over its
740
+ * own pre-warm, plus one each for the rounding — and no larger than the main pool it is bound
741
+ * for: what would not fit there is dropped at migration anyway.
742
+ */
743
+ #upload_warmup_queue() {
744
+ const queue = this.#warmup_queue;
745
+ const frame = this.#frame;
746
+
747
+ // A queued emitter that has since left the scene has no row to warm; drop it here.
748
+ let count = 0;
749
+
750
+ for (let i = 0; i < queue.length; i++) {
751
+ const emitter = queue[i];
752
+
753
+ if (this.registry.context(emitter) !== undefined) {
754
+ queue[count++] = emitter;
755
+ }
756
+ }
757
+
758
+ queue.length = count;
759
+
760
+ if (count === 0) {
761
+ frame.warmup_count = 0;
762
+ frame.warmup_capacity = 0;
763
+ frame.warmup_schedule = null;
764
+ return;
765
+ }
766
+
767
+ let words = this.#warmup_words;
768
+ const needed = count * PARTICLE_WARMUP_QUEUE_WORDS;
769
+
770
+ if (words.length < needed) {
771
+ words = new Uint32Array(Math.max(needed, words.length * 2));
772
+ this.#warmup_words = words;
773
+ }
774
+
775
+ const floats = new Float32Array(words.buffer, words.byteOffset, words.length);
776
+
777
+ let max_prewarm = 0;
778
+ let spawned = 0;
779
+
780
+ for (let i = 0; i < count; i++) {
781
+ const emitter = queue[i];
782
+ const context = this.registry.context(emitter);
783
+ const base = i * PARTICLE_WARMUP_QUEUE_WORDS;
784
+
785
+ // Two unit fractions from one hash of the row and the emitter's seed: the same emitter
786
+ // in the same row warms the same way twice, and two emitters do not.
787
+ const hash = hash_mix2(context.row, context.generation) >>> 0;
788
+ const phase = (hash >>> 8) / 16777216;
789
+ const beat = ((hash & 0xff) / 255) - 0.5;
790
+
791
+ words[base + WARMUP_QUEUE.ROW] = context.row;
792
+ floats[base + WARMUP_QUEUE.PREWARM] = emitter.prewarm;
793
+ floats[base + WARMUP_QUEUE.ACCUMULATOR] = phase;
794
+ floats[base + WARMUP_QUEUE.CLOCK] = 1 + PARTICLE_WARMUP_EMITTER_JITTER * beat;
795
+
796
+ max_prewarm = Math.max(max_prewarm, emitter.prewarm);
797
+ spawned += Math.ceil(emitter.spawn_rate * emitter.prewarm) + 1;
798
+ }
799
+
800
+ const device = this.graphics.device;
801
+ const bytes = needed * WORD;
802
+
803
+ if (this.buffers.warmup_queue.size < bytes) {
804
+ this.buffers.warmup_queue.destroy();
805
+ this.buffers.warmup_queue = device.createBuffer({ label: "particles/warmup_queue", size: bytes, usage: STORAGE_UPLOAD });
806
+ }
807
+
808
+ device.queue.writeBuffer(this.buffers.warmup_queue, 0, words.buffer, words.byteOffset, bytes);
809
+
810
+ frame.warmup_count = count;
811
+ frame.warmup_capacity = Math.max(1, Math.min(this.capacity, spawned));
812
+ frame.warmup_schedule = warmup_schedule(max_prewarm);
813
+ }
814
+
815
+ /**
816
+ * Fold the ring down to one entry per row, dropping entries whose emitter no longer holds the
817
+ * row it was issued against.
818
+ *
819
+ * The spawn-command pass adds to a state row with a plain read-modify-write, so two entries
820
+ * naming one row would lose one; and a burst issued to an emitter that left the scene this
821
+ * frame must not land on the row's next tenant. Both are settled here, in place, over a few
822
+ * dozen entries at most.
823
+ */
824
+ #merge_spawn_commands() {
825
+ const count = this.#spawn_command_count;
826
+
827
+ if (count === 0) {
828
+ return;
829
+ }
830
+
831
+ const ring = this.#spawn_commands;
832
+ const emitters = this.#spawn_command_emitters;
833
+
834
+ let rows = this.#spawn_by_row;
835
+ const needed = Math.max(1, this.registry.table.element_capacity);
836
+
837
+ if (rows.length < needed) {
838
+ rows = new Uint32Array(needed);
839
+ this.#spawn_by_row = rows;
840
+ }
841
+
842
+ for (let i = 0; i < count; i++) {
843
+ const emitter = emitters[i];
844
+ const row = ring[i * PARTICLE_SPAWN_COMMAND_WORDS];
845
+ const context = this.registry.context(emitter);
846
+
847
+ if (context !== undefined && context.row === row) {
848
+ rows[row] += ring[i * PARTICLE_SPAWN_COMMAND_WORDS + 1];
849
+ }
850
+
851
+ emitters[i] = null;
852
+ }
853
+
854
+ let merged = 0;
855
+
856
+ for (let i = 0; i < count; i++) {
857
+ const row = ring[i * PARTICLE_SPAWN_COMMAND_WORDS];
858
+ const pending = rows[row];
859
+
860
+ if (pending === 0) {
861
+ continue;
862
+ }
863
+
864
+ ring[merged * PARTICLE_SPAWN_COMMAND_WORDS] = row;
865
+ ring[merged * PARTICLE_SPAWN_COMMAND_WORDS + 1] = pending;
866
+ rows[row] = 0;
867
+ merged++;
868
+ }
869
+
870
+ this.#spawn_command_count = merged;
871
+ }
872
+
873
+ /**
874
+ * The counters buffer holds the per-emitter bounds accumulators after the counters themselves
875
+ * (`data/PARTICLE_COUNTERS.js`), so it follows the emitter table's element capacity.
876
+ *
877
+ * Grown by copy, not by reallocation: the counters at the head of it are live GPU state — the
878
+ * free-list length above all, which no CPU value can reconstruct — and the accumulators are this
879
+ * frame's measurements of emitters that have not moved. A fresh buffer would zero the free list
880
+ * and strand the whole pool.
881
+ *
882
+ * The rows past the old end are the only ones the copy does not answer for, and zero is the
883
+ * wrong answer for them: a zero accumulator reads back as a degenerate box at the world origin
884
+ * rather than as an absence, and the first box measured for such a row would be stretched to
885
+ * include the origin. They get the empty interval written over them instead — the same two words
886
+ * the publish pass resets a row to.
887
+ *
888
+ * @param {number} emitter_capacity element capacity of the emitter table
889
+ */
890
+ #ensure_counters(emitter_capacity) {
891
+ const size = particle_counters_word_count(emitter_capacity) * WORD;
892
+ const existing = this.buffers.counters;
893
+
894
+ if (existing.size >= size) {
895
+ return;
896
+ }
897
+
898
+ const device = this.graphics.device;
899
+ const grown = device.createBuffer({ label: "particles/counters", size, usage: STORAGE_GROWABLE });
900
+
901
+ const cmd = ShadeGPUCommandContext.create(this.graphics, "particles/counters grow");
902
+
903
+ cmd.gpu_encoder.copyBufferToBuffer(existing, 0, grown, 0, existing.size);
904
+
905
+ cmd.finish();
906
+
907
+ // The new region is a whole number of accumulators: the counters are the head of the buffer
908
+ // and every growth adds rows behind them.
909
+ const added_rows = (size - existing.size) / (PARTICLE_BOUNDS_WORDS * WORD);
910
+
911
+ device.queue.writeBuffer(grown, existing.size, particle_bounds_empty_words(added_rows));
912
+
913
+ existing.destroy();
914
+
915
+ this.buffers.counters = grown;
916
+ }
917
+
918
+ /**
919
+ * The coherence list follows the peak number of program ids. It is the one thing the bucketing
920
+ * leaves behind: the next simulate consumes its entries and padding with the previous frame's
921
+ * saved count, so it cannot be a transient and it cannot be reallocated empty. Growth copies it
922
+ * forward; the old count and dispatch stay valid because the entries keep their places.
923
+ *
924
+ * The bins, cursors, keys and dispatch args the bucketing needs alongside it live and die inside
925
+ * one frame graph, so `coherence/` declares them on the passes that write them and the graph
926
+ * owns them.
927
+ *
928
+ * @param {number} buckets
929
+ */
930
+ #ensure_sim_buffers(buckets) {
931
+ if (buckets <= this.#sim_bucket_capacity) {
932
+ return;
933
+ }
934
+
935
+ const device = this.graphics.device;
936
+ const buffers = this.buffers;
937
+
938
+ // Padding rounds each bucket up to a whole workgroup, so it can add at most `workgroup - 1`
939
+ // entries per bucket; one whole workgroup per bucket is the bound.
940
+ const previous = buffers.sim_list;
941
+ buffers.sim_list = device.createBuffer({
942
+ label: "particles/sim_list",
943
+ size: (this.capacity + buckets * PARTICLE_SIMULATE_WORKGROUP_SIZE) * WORD,
944
+ usage: STORAGE_GROWABLE,
945
+ });
946
+
947
+ if (previous !== undefined) {
948
+ const cmd = ShadeGPUCommandContext.create(this.graphics, "particles/coherence grow");
949
+ cmd.gpu_encoder.copyBufferToBuffer(previous, 0, buffers.sim_list, 0, previous.size);
950
+ cmd.finish();
951
+ previous.destroy();
952
+ }
953
+
954
+ this.#sim_bucket_capacity = buckets;
955
+ }
956
+
957
+ /**
958
+ * Give this frame its alive list, and hand back the ones nothing reads any more.
959
+ *
960
+ * The list is the allocator's memory on loan rather than a pair of buffers reserved here — see
961
+ * the class doc. Three things happen, in this order:
962
+ *
963
+ * 1. A list an earlier frame retired goes back. That frame's graph has been recorded, executed
964
+ * and submitted since, so the bucketing pass that was reading it is done.
965
+ * 2. The previous frame's list goes back too — BEFORE the new one is asked for, so that the
966
+ * pool can answer with the same memory and the steady-state footprint is one list. Skipped
967
+ * when this frame rebuilds the buckets, because that pass reads it; it is retired instead
968
+ * and goes back on the next call, one step behind.
969
+ * 3. The new list is taken. Uncleared: the frame appends to it from a counter its first pass
970
+ * zeroes, so no reader can reach what the last borrower left.
971
+ *
972
+ * The pool may answer with a larger buffer than asked for, which changes nothing — every
973
+ * reader is bounded by a count, never by the buffer's length.
974
+ *
975
+ * @param {boolean} rebucket whether this frame's bucketing rebuild reads the previous list
976
+ */
977
+ #swap_alive_list(rebucket) {
978
+ const allocator = this.graphics.buffer_allocator_main;
979
+ const buffers = this.buffers;
980
+ const previous = this.#alive;
981
+
982
+ // One is taken at construction and none is ever let go without another taking its place, so
983
+ // there is always a previous list — including on the first frame, which can already be a
984
+ // rebucket if a program was edited before it.
985
+ assert.notNull(previous, "the previous alive list");
986
+
987
+ if (this.#alive_retired !== null) {
988
+ allocator.release(this.#alive_retired);
989
+ this.#alive_retired = null;
990
+ }
991
+
992
+ if (rebucket) {
993
+ this.#alive_retired = previous;
994
+ } else {
995
+ allocator.release(previous);
996
+ }
997
+
998
+ const alive = allocator.get(this.#alive_descriptor);
999
+
1000
+ this.#alive = alive;
1001
+
1002
+ buffers.alive = alive;
1003
+ buffers.alive_write = alive;
1004
+ // Named only on the frames that read one: on every other frame `previous` has already gone
1005
+ // back to the pool, and may be somebody else's by the time the graph runs.
1006
+ buffers.alive_read = rebucket ? previous : undefined;
1007
+ }
1008
+
1009
+ /**
1010
+ * Hand every alive list back to the allocator and stop naming them.
1011
+ *
1012
+ * {@link destroy} cannot simply destroy them with the rest: the memory is the pool's, and a
1013
+ * pooled buffer destroyed behind the pool's back is a handle it will hand to the next caller.
1014
+ */
1015
+ #release_alive_lists() {
1016
+ const allocator = this.graphics.buffer_allocator_main;
1017
+
1018
+ if (this.#alive !== null) {
1019
+ allocator.release(this.#alive);
1020
+ this.#alive = null;
1021
+ }
1022
+
1023
+ if (this.#alive_retired !== null) {
1024
+ allocator.release(this.#alive_retired);
1025
+ this.#alive_retired = null;
1026
+ }
1027
+
1028
+ delete this.buffers.alive;
1029
+ delete this.buffers.alive_write;
1030
+ delete this.buffers.alive_read;
1031
+ }
1032
+
1033
+ /**
1034
+ * Fill the free list with every slot, on the GPU — see `shaders/shader_particle_init_free_list.js`
1035
+ * for why this is a dispatch and not a host write.
1036
+ *
1037
+ * The launch is clamped to what a dispatch dimension holds and the shader strides through the
1038
+ * rest: at the default capacity this is a couple of thousand workgroups, but the capacity is
1039
+ * the caller's to choose and a dispatch past `maxComputeWorkgroupsPerDimension` is refused.
1040
+ */
1041
+ #init_free_list() {
1042
+ const device = this.graphics.device;
1043
+
1044
+ const cmd = ShadeGPUCommandContext.create(this.graphics, "particles/free list init");
1045
+
1046
+ shader_particle_init_free_list.dispatch({
1047
+ encoder: cmd,
1048
+ group_count_x: Math.min(
1049
+ Math.ceil(this.capacity / PARTICLE_INIT_FREE_LIST_WORKGROUP_SIZE),
1050
+ device.limits.maxComputeWorkgroupsPerDimension,
1051
+ ),
1052
+ bindings: { dead: this.buffers.dead, u_capacity: this.capacity },
1053
+ });
1054
+
1055
+ cmd.finish();
1056
+ }
1057
+
1058
+ /**
1059
+ * The buffers whose size is fixed by the pool capacity, and the free list's initial contents.
1060
+ */
1061
+ #create_buffers() {
1062
+ const device = this.graphics.device;
1063
+ const capacity = this.capacity;
1064
+ const buffers = this.buffers;
1065
+
1066
+ buffers.pool = device.createBuffer({ label: "particles/pool", size: capacity * PARTICLE_RECORD_WORD_COUNT * WORD, usage: STORAGE });
1067
+
1068
+ this.#alive_descriptor = BufferResourceDescriptor.from(capacity * WORD, STORAGE);
1069
+ this.#alive = this.graphics.buffer_allocator_main.get(this.#alive_descriptor);
1070
+ buffers.alive = this.#alive;
1071
+
1072
+ buffers.dead = device.createBuffer({ label: "particles/dead", size: capacity * WORD, usage: STORAGE });
1073
+ // Just the counters to begin with. The per-emitter bounds accumulator that shares this
1074
+ // buffer follows the emitter table's capacity, and there is no table yet.
1075
+ buffers.counters = device.createBuffer({ label: "particles/counters", size: PARTICLE_COUNTER_COUNT * WORD, usage: STORAGE_GROWABLE });
1076
+
1077
+ // Simulate's workgroup count, written by the END of one frame's build-indirect and read by
1078
+ // the NEXT frame's dispatch — the one indirect buffer that crosses the boundary, and so the
1079
+ // one the graph cannot own. Zero-initialised (WebGPU spec), so the first frame's indirect
1080
+ // dispatch is a no-op until the first build-indirect has run.
1081
+ buffers.dispatch_args = device.createBuffer({ label: "particles/dispatch_args", size: 3 * WORD, usage: INDIRECT });
1082
+
1083
+ buffers.spawn_commands = device.createBuffer({ label: "particles/spawn_commands", size: this.#spawn_commands.byteLength, usage: STORAGE_UPLOAD });
1084
+
1085
+ // The warm-up queue: one entry to start with, grown on upload to what a frame registers.
1086
+ buffers.warmup_queue = device.createBuffer({ label: "particles/warmup_queue", size: PARTICLE_WARMUP_QUEUE_WORDS * WORD, usage: STORAGE_UPLOAD });
1087
+
1088
+ // The counters say every slot is free; the pass below makes it so. A handful of words is
1089
+ // the whole of what the host ever writes into either — the list itself is the GPU's.
1090
+ const counters = new Uint32Array(PARTICLE_COUNTER_COUNT);
1091
+ counters[PARTICLE_COUNTER.DEAD] = capacity;
1092
+
1093
+ device.queue.writeBuffer(buffers.counters, 0, counters);
1094
+
1095
+ this.#init_free_list();
1096
+ }
1097
+
1098
+ /**
1099
+ * A 1x1 white atlas and a linear sampler, so the system draws before anyone hands it art.
1100
+ * @returns {void}
1101
+ */
1102
+ #create_default_atlas() {
1103
+ const device = this.graphics.device;
1104
+
1105
+ const texture = new GPUTextureContext(device);
1106
+ texture.descriptor.label = "particles/default_atlas";
1107
+ texture.descriptor.size = [1, 1];
1108
+ texture.descriptor.format = "rgba8unorm";
1109
+ texture.descriptor.usage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST;
1110
+
1111
+ device.queue.writeTexture({ texture: texture.gpu_texture }, new Uint8Array([255, 255, 255, 255]), { bytesPerRow: 4 }, [1, 1]);
1112
+ texture.incrementVersion();
1113
+
1114
+ // kept, not just viewed: a texture nothing holds is a texture nothing can free
1115
+ this.#default_atlas = texture;
1116
+ this.atlas = texture.obtainView();
1117
+ this.atlas_sampler = device.createSampler({
1118
+ label: "particles/atlas_sampler",
1119
+ magFilter: "linear",
1120
+ minFilter: "linear",
1121
+ addressModeU: "clamp-to-edge",
1122
+ addressModeV: "clamp-to-edge",
1123
+ });
1124
+ }
1125
+
1126
+ /**
1127
+ * Sizes of the persistent buffers, in bytes, as they stand — one entry per distinct buffer, so
1128
+ * this is the feature's footprint and not a list of the names it binds them under.
1129
+ * `alive_write` is `alive` under the frame's name for it and would otherwise be counted twice;
1130
+ * `alive_read` is a second list, and appears only on the frames that hold one. Exposed for tests.
1131
+ *
1132
+ * @returns {Object<string, number>}
1133
+ */
1134
+ bufferSizes() {
1135
+ const sizes = {};
1136
+ const counted = new Set();
1137
+
1138
+ for (const [name, buffer] of Object.entries(this.buffers)) {
1139
+ if (buffer === undefined || buffer === null || counted.has(buffer)) {
1140
+ continue;
1141
+ }
1142
+
1143
+ counted.add(buffer);
1144
+ sizes[name] = buffer.size;
1145
+ }
1146
+
1147
+ return sizes;
1148
+ }
1149
+
1150
+ /**
1151
+ * Copy the counts the GPU keeps back to the host and decode them, for display and diagnostics.
1152
+ *
1153
+ * Read back to *look at*: nothing here may size a pass. Every number describes a frame that has
1154
+ * already been submitted, so by the time one arrives it is at best one frame stale — and sizing
1155
+ * a dispatch or a draw from a stale count is a correctness bug, not an inaccuracy, for the
1156
+ * reasons `shader_particle_build_indirect` sets out. This is the whole of what the feature ever
1157
+ * reads back, and the only reason {@link INDIRECT} carries `COPY_SRC` at all.
1158
+ *
1159
+ * The draw args are not here and cannot be: they never leave the frame graph, which allocates
1160
+ * them at the build-indirect pass and releases them after the last draw that reads them. Their
1161
+ * instance count is {@link ParticleSystemStats#alive}, which the counters carry anyway.
1162
+ *
1163
+ * Two copies, two submissions and a map round trip, so a caller polling this for an overlay
1164
+ * wants to rate-limit itself and to hold off while a previous read is still outstanding.
1165
+ *
1166
+ * @returns {Promise<ParticleSystemStats>}
1167
+ */
1168
+ async read_stats() {
1169
+ const device = this.graphics.device;
1170
+ const buffers = this.buffers;
1171
+
1172
+ const read = (buffer, words) => inspect_gpu_buffer({
1173
+ device,
1174
+ buffer,
1175
+ buffer_size: words * WORD,
1176
+ // The mapped range is only valid for the length of the callback, so the decode either
1177
+ // happens inside it or takes a copy. A copy, so one `Promise.all` sees both buffers.
1178
+ callback: bytes => new Uint32Array(bytes.slice(0)),
1179
+ });
1180
+
1181
+ const [counters, dispatch] = await Promise.all([
1182
+ // The head of the buffer only: what follows the counters is the per-emitter bounds
1183
+ // accumulator, which is boxes rather than counts and grows with the emitter table.
1184
+ read(buffers.counters, PARTICLE_COUNTER_COUNT),
1185
+ read(buffers.dispatch_args, 3),
1186
+ ]);
1187
+
1188
+ return {
1189
+ alive: counters[PARTICLE_COUNTER.ALIVE],
1190
+ dead: counters[PARTICLE_COUNTER.DEAD],
1191
+ spawn_budget: counters[PARTICLE_COUNTER.SPAWN_BUDGET],
1192
+ alive_in: counters[PARTICLE_COUNTER.ALIVE_IN],
1193
+ sim_padded: counters[PARTICLE_COUNTER.SIM_PADDED],
1194
+ sim_workgroups: dispatch[0],
1195
+ };
1196
+ }
1197
+
1198
+ destroy() {
1199
+ /*
1200
+ The emitters go first, and they matter more than the buffers: they belong to their owners,
1201
+ they outlive this system, and each is holding a row of the table about to be freed.
1202
+ Retiring them here puts every emitter back where it was before this system saw it, so its
1203
+ owner can register it with the next system.
1204
+ */
1205
+ for (const emitter of this.registry.emitters.slice()) {
1206
+ this.registry.remove(emitter);
1207
+ }
1208
+
1209
+ // Before the loop below, and not in it: the alive lists are the allocator's and go back to
1210
+ // it. Destroying one would leave the pool holding a handle it will hand to the next caller.
1211
+ this.#release_alive_lists();
1212
+
1213
+ for (const buffer of Object.values(this.buffers)) {
1214
+ if (buffer !== undefined && buffer !== null) {
1215
+ buffer.destroy();
1216
+ }
1217
+ }
1218
+
1219
+ this.buffers = {};
1220
+ this.registry.database.destroy();
1221
+
1222
+ // the default atlas is this system's own; a page that replaced it owns what it put there
1223
+ this.#default_atlas?.destroy();
1224
+ this.#default_atlas = null;
1225
+ }
1226
+ }
1227
+
1228
+ /**
1229
+ * A storage buffer holding `data`, replacing `previous` (destroyed) if it exists. The program
1230
+ * buffer is monolithic — a program is variable-length, so there is no table to page it into — and
1231
+ * is rebuilt whole on the rare frame the program heap changes.
1232
+ *
1233
+ * @param {GPUDevice} device
1234
+ * @param {GPUBuffer|undefined} previous
1235
+ * @param {string} label
1236
+ * @param {Uint32Array} data
1237
+ * @returns {GPUBuffer}
1238
+ */
1239
+ function replace_data_buffer(device, previous, label, data) {
1240
+ previous?.destroy();
1241
+
1242
+ const buffer = device.createBuffer({ label, size: Math.max(WORD, data.byteLength), usage: STORAGE_UPLOAD });
1243
+
1244
+ device.queue.writeBuffer(buffer, 0, data);
1245
+
1246
+ return buffer;
1247
+ }