@woosh/meep-engine 3.29.0 → 3.30.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 (226) hide show
  1. package/build/bundle-worker-image-decoder.js +1 -1
  2. package/build/bundle-worker-terrain.js +1 -1
  3. package/package.json +1 -1
  4. package/src/core/binary/BinaryBuffer.d.ts.map +1 -1
  5. package/src/core/binary/BinaryBuffer.js +14 -2
  6. package/src/core/bvh2/bvh3/BVH.d.ts.map +1 -1
  7. package/src/core/bvh2/bvh3/BVH.js +14 -1
  8. package/src/core/cache/FrequencySketch.d.ts +3 -1
  9. package/src/core/cache/FrequencySketch.d.ts.map +1 -1
  10. package/src/core/cache/FrequencySketch.js +10 -4
  11. package/src/core/cache/wtinylfu/CacheWTinylfu.d.ts.map +1 -1
  12. package/src/core/cache/wtinylfu/CacheWTinylfu.js +24 -2
  13. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_vertex_quadrics.d.ts +20 -0
  14. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_vertex_quadrics.d.ts.map +1 -1
  15. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_vertex_quadrics.js +69 -4
  16. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.d.ts +48 -18
  17. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.d.ts.map +1 -1
  18. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.js +262 -70
  19. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_collapse_fold_limit.d.ts +25 -0
  20. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_collapse_fold_limit.d.ts.map +1 -0
  21. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_collapse_fold_limit.js +228 -0
  22. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_collapse_would_fold.d.ts +13 -0
  23. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_collapse_would_fold.d.ts.map +1 -1
  24. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_collapse_would_fold.js +10 -128
  25. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_build_face_bvh.d.ts.map +1 -1
  26. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_build_face_bvh.js +11 -1
  27. package/src/core/graph/mn_graph_collapse_weighted_edge.js +6 -6
  28. package/src/core/math/lookup/ParameterLookupTable.d.ts.map +1 -1
  29. package/src/core/math/lookup/ParameterLookupTable.js +10 -5
  30. package/src/core/math/noise/sdnoise.d.ts.map +1 -1
  31. package/src/core/math/noise/sdnoise.js +16 -10
  32. package/src/core/model/object/ImmutableObjectPool.d.ts.map +1 -1
  33. package/src/core/model/object/ImmutableObjectPool.js +17 -7
  34. package/src/core/process/executor/ConcurrentExecutor.d.ts +1 -5
  35. package/src/core/process/executor/ConcurrentExecutor.d.ts.map +1 -1
  36. package/src/core/process/executor/ConcurrentExecutor.js +95 -1
  37. package/src/core/process/task/task_find_dependency_cycle.d.ts +17 -0
  38. package/src/core/process/task/task_find_dependency_cycle.d.ts.map +1 -0
  39. package/src/core/process/task/task_find_dependency_cycle.js +81 -0
  40. package/src/core/wfc/WFCCellChoice.d.ts +17 -0
  41. package/src/core/wfc/WFCCellChoice.d.ts.map +1 -0
  42. package/src/core/wfc/WFCCellChoice.js +36 -0
  43. package/src/core/wfc/WFCLattice.d.ts +105 -0
  44. package/src/core/wfc/WFCLattice.d.ts.map +1 -0
  45. package/src/core/wfc/WFCLattice.js +235 -0
  46. package/src/core/wfc/WFCRuleTable.d.ts +172 -0
  47. package/src/core/wfc/WFCRuleTable.d.ts.map +1 -0
  48. package/src/core/wfc/WFCRuleTable.js +334 -0
  49. package/src/core/wfc/WFCState.d.ts +273 -0
  50. package/src/core/wfc/WFCState.d.ts.map +1 -0
  51. package/src/core/wfc/WFCState.js +552 -0
  52. package/src/core/wfc/WFCStatus.d.ts +11 -0
  53. package/src/core/wfc/WFCStatus.d.ts.map +1 -0
  54. package/src/core/wfc/WFCStatus.js +28 -0
  55. package/src/core/wfc/WFCTileWeights.d.ts +48 -0
  56. package/src/core/wfc/WFCTileWeights.d.ts.map +1 -0
  57. package/src/core/wfc/WFCTileWeights.js +86 -0
  58. package/src/core/wfc/WFC_UNRESOLVED.d.ts +12 -0
  59. package/src/core/wfc/WFC_UNRESOLVED.d.ts.map +1 -0
  60. package/src/core/wfc/WFC_UNRESOLVED.js +11 -0
  61. package/src/core/wfc/wfc_estimate_bytes.d.ts +24 -0
  62. package/src/core/wfc/wfc_estimate_bytes.d.ts.map +1 -0
  63. package/src/core/wfc/wfc_estimate_bytes.js +47 -0
  64. package/src/core/wfc/wfc_rule_table_tile_components.d.ts +27 -0
  65. package/src/core/wfc/wfc_rule_table_tile_components.d.ts.map +1 -0
  66. package/src/core/wfc/wfc_rule_table_tile_components.js +80 -0
  67. package/src/core/wfc/wfc_solve.d.ts +21 -0
  68. package/src/core/wfc/wfc_solve.d.ts.map +1 -0
  69. package/src/core/wfc/wfc_solve.js +154 -0
  70. package/src/core/wfc/wfc_state_apply_boundary.d.ts +18 -0
  71. package/src/core/wfc/wfc_state_apply_boundary.d.ts.map +1 -0
  72. package/src/core/wfc/wfc_state_apply_boundary.js +61 -0
  73. package/src/core/wfc/wfc_state_assign.d.ts +11 -0
  74. package/src/core/wfc/wfc_state_assign.d.ts.map +1 -0
  75. package/src/core/wfc/wfc_state_assign.js +36 -0
  76. package/src/core/wfc/wfc_state_ban_bits.d.ts +17 -0
  77. package/src/core/wfc/wfc_state_ban_bits.d.ts.map +1 -0
  78. package/src/core/wfc/wfc_state_ban_bits.js +81 -0
  79. package/src/core/wfc/wfc_state_collapse.d.ts +12 -0
  80. package/src/core/wfc/wfc_state_collapse.d.ts.map +1 -0
  81. package/src/core/wfc/wfc_state_collapse.js +73 -0
  82. package/src/core/wfc/wfc_state_flush_dirty.d.ts +12 -0
  83. package/src/core/wfc/wfc_state_flush_dirty.d.ts.map +1 -0
  84. package/src/core/wfc/wfc_state_flush_dirty.js +39 -0
  85. package/src/core/wfc/wfc_state_propagate.d.ts +14 -0
  86. package/src/core/wfc/wfc_state_propagate.d.ts.map +1 -0
  87. package/src/core/wfc/wfc_state_propagate.js +155 -0
  88. package/src/core/wfc/wfc_state_read.d.ts +11 -0
  89. package/src/core/wfc/wfc_state_read.d.ts.map +1 -0
  90. package/src/core/wfc/wfc_state_read.js +38 -0
  91. package/src/core/wfc/wfc_state_restrict.d.ts +18 -0
  92. package/src/core/wfc/wfc_state_restrict.d.ts.map +1 -0
  93. package/src/core/wfc/wfc_state_restrict.js +39 -0
  94. package/src/core/wfc/wfc_state_set_eligible.d.ts +24 -0
  95. package/src/core/wfc/wfc_state_set_eligible.d.ts.map +1 -0
  96. package/src/core/wfc/wfc_state_set_eligible.js +49 -0
  97. package/src/core/wfc/wfc_state_undo_to.d.ts +13 -0
  98. package/src/core/wfc/wfc_state_undo_to.d.ts.map +1 -0
  99. package/src/core/wfc/wfc_state_undo_to.js +84 -0
  100. package/src/core/wfc/wfc_words_per_cell.d.ts +11 -0
  101. package/src/core/wfc/wfc_words_per_cell.d.ts.map +1 -0
  102. package/src/core/wfc/wfc_words_per_cell.js +16 -0
  103. package/src/engine/ecs/EntityComponentDataset.d.ts +8 -0
  104. package/src/engine/ecs/EntityComponentDataset.d.ts.map +1 -1
  105. package/src/engine/ecs/EntityComponentDataset.js +37 -14
  106. package/src/engine/ecs/fow/FogOfWarRevealerSystem.d.ts +8 -1
  107. package/src/engine/ecs/fow/FogOfWarRevealerSystem.d.ts.map +1 -1
  108. package/src/engine/ecs/fow/FogOfWarRevealerSystem.js +37 -2
  109. package/src/engine/ecs/guid/UUID.d.ts.map +1 -1
  110. package/src/engine/ecs/guid/UUID.js +45 -17
  111. package/src/engine/ecs/ik/OneBoneSurfaceAlignmentSolver.d.ts.map +1 -1
  112. package/src/engine/ecs/ik/OneBoneSurfaceAlignmentSolver.js +3 -1
  113. package/src/engine/ecs/terrain/ecs/cling/ClingToTerrainSystem.d.ts.map +1 -1
  114. package/src/engine/ecs/terrain/ecs/cling/ClingToTerrainSystem.js +366 -360
  115. package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_linear.d.ts.map +1 -1
  116. package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_linear.js +24 -10
  117. package/src/engine/grid/grid2transform/GridPosition2TransformSystem.d.ts.map +1 -1
  118. package/src/engine/grid/grid2transform/GridPosition2TransformSystem.js +24 -5
  119. package/src/engine/interpolation/Interpoland.d.ts +7 -1
  120. package/src/engine/interpolation/Interpoland.d.ts.map +1 -1
  121. package/src/engine/interpolation/Interpoland.js +8 -1
  122. package/src/engine/interpolation/InterpolationSystem.d.ts.map +1 -1
  123. package/src/engine/interpolation/InterpolationSystem.js +3 -0
  124. package/src/engine/interpolation/TransformPoseSerializationAdapter.d.ts +5 -3
  125. package/src/engine/interpolation/TransformPoseSerializationAdapter.d.ts.map +1 -1
  126. package/src/engine/interpolation/TransformPoseSerializationAdapter.js +14 -3
  127. package/src/engine/interpolation/pose_interpoland.d.ts.map +1 -1
  128. package/src/engine/interpolation/pose_interpoland.js +2 -0
  129. package/src/engine/network/orchestrator/NetworkPeer.d.ts.map +1 -1
  130. package/src/engine/network/orchestrator/NetworkPeer.js +15 -10
  131. package/src/engine/physics/cloth/ecs/ClothWorld.d.ts.map +1 -1
  132. package/src/engine/physics/cloth/ecs/ClothWorld.js +17 -0
  133. package/src/engine/physics/mls-mpm/MLS_MPM.d.ts.map +1 -1
  134. package/src/engine/physics/mls-mpm/MLS_MPM.js +14 -0
  135. package/src/engine/sound/sopra/definition/clip/AbstractAudioClip.d.ts +2 -1
  136. package/src/engine/sound/sopra/definition/clip/AbstractAudioClip.d.ts.map +1 -1
  137. package/src/engine/sound/sopra/definition/clip/AbstractAudioClip.js +1 -1
  138. package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClip.d.ts +1 -0
  139. package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClip.d.ts.map +1 -1
  140. package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClip.d.ts +1 -0
  141. package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClip.d.ts.map +1 -1
  142. package/src/engine/sound/sopra/definition/clip/SampleAudioClip.d.ts +2 -1
  143. package/src/engine/sound/sopra/definition/clip/SampleAudioClip.d.ts.map +1 -1
  144. package/src/engine/sound/sopra/definition/clip/SampleAudioClip.js +12 -3
  145. package/src/engine/sound/sopra/definition/clip/SequenceContainerAudioClip.d.ts +1 -0
  146. package/src/engine/sound/sopra/definition/clip/SequenceContainerAudioClip.d.ts.map +1 -1
  147. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.d.ts +1 -0
  148. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.d.ts.map +1 -1
  149. package/src/engine/sound/sopra/runtime/EventInstance.d.ts.map +1 -1
  150. package/src/engine/sound/sopra/runtime/EventInstance.js +4 -1
  151. package/src/format/image/avif/av1/decode/decode_coefficients.d.ts.map +1 -1
  152. package/src/format/image/avif/av1/decode/decode_coefficients.js +248 -259
  153. package/src/format/image/avif/av1/decode/read_golomb.d.ts +21 -0
  154. package/src/format/image/avif/av1/decode/read_golomb.d.ts.map +1 -0
  155. package/src/format/image/avif/av1/decode/read_golomb.js +49 -0
  156. package/src/format/image/avif/av1/filter/loop_restoration.js +461 -451
  157. package/src/format/image/avif/av1/util/round2.d.ts +19 -2
  158. package/src/format/image/avif/av1/util/round2.d.ts.map +1 -1
  159. package/src/format/image/avif/av1/util/round2.js +51 -27
  160. package/src/format/image/avif/heif/parse_avif_file.js +24 -0
  161. package/src/shade/renderer/buffer/table/GPUTypedTable.d.ts.map +1 -1
  162. package/src/shade/renderer/buffer/table/GPUTypedTable.js +14 -0
  163. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +46 -3
  164. package/src/shade/renderer/geometry/virtual/WEDGE_PLAN_2026_09_18.md +91 -1
  165. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts +0 -13
  166. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts.map +1 -1
  167. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.js +0 -15
  168. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_group_step.js +5 -5
  169. package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_DEFAULT_ATTRIBUTE_WEIGHT_RATIOS.d.ts +11 -1
  170. package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_DEFAULT_ATTRIBUTE_WEIGHT_RATIOS.d.ts.map +1 -1
  171. package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_DEFAULT_ATTRIBUTE_WEIGHT_RATIOS.js +11 -1
  172. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_normals.d.ts +18 -9
  173. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_normals.d.ts.map +1 -1
  174. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_normals.js +20 -26
  175. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_normalize_source.d.ts +1 -3
  176. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_normalize_source.d.ts.map +1 -1
  177. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_normalize_source.js +2 -5
  178. package/src/shade/renderer/geometry/virtual/build/vgeo_build.js +1 -1
  179. package/src/shade/renderer/geometry/virtual/runtime/VGeoResidency.d.ts.map +1 -1
  180. package/src/shade/renderer/geometry/virtual/runtime/VGeoResidency.js +14 -1
  181. package/src/shade/renderer/global_illumination/probe/octahedral/GPUProbeAtlas.d.ts.map +1 -1
  182. package/src/shade/renderer/global_illumination/probe/octahedral/GPUProbeAtlas.js +12 -3
  183. package/src/shade/renderer/light/environment/rgbe/RGBE_ReadPixels_RLE.d.ts.map +1 -1
  184. package/src/shade/renderer/light/environment/rgbe/RGBE_ReadPixels_RLE.js +7 -1
  185. package/src/shade/renderer/lightmap/GPULightMap.d.ts.map +1 -1
  186. package/src/shade/renderer/lightmap/GPULightMap.js +349 -336
  187. package/src/shade/renderer/loader/gltf/load_gltf.d.ts.map +1 -1
  188. package/src/shade/renderer/loader/gltf/load_gltf.js +1137 -1128
  189. package/src/shade/renderer/particles/DESIGN.md +14 -5
  190. package/src/shade/renderer/particles/GPUParticleSystem.d.ts +25 -10
  191. package/src/shade/renderer/particles/GPUParticleSystem.d.ts.map +1 -1
  192. package/src/shade/renderer/particles/GPUParticleSystem.js +182 -38
  193. package/src/shade/renderer/particles/graph/groups/simplex_reference.d.ts.map +1 -1
  194. package/src/shade/renderer/particles/graph/groups/simplex_reference.js +16 -10
  195. package/src/shade/renderer/particles/graph_particles.d.ts +4 -2
  196. package/src/shade/renderer/particles/graph_particles.d.ts.map +1 -1
  197. package/src/shade/renderer/particles/graph_particles.js +8 -2
  198. package/src/shade/renderer/particles/optimizer/particle_vm_semantics.d.ts.map +1 -1
  199. package/src/shade/renderer/particles/optimizer/particle_vm_semantics.js +8 -2
  200. package/src/shade/renderer/particles/optimizer/pass_fold_constants.d.ts.map +1 -1
  201. package/src/shade/renderer/particles/optimizer/pass_fold_constants.js +10 -4
  202. package/src/shade/renderer/particles/shaders/shader_particle_init_free_list.d.ts +31 -0
  203. package/src/shade/renderer/particles/shaders/shader_particle_init_free_list.d.ts.map +1 -0
  204. package/src/shade/renderer/particles/shaders/shader_particle_init_free_list.js +62 -0
  205. package/src/shade/renderer/particles/vm/ParticleVMReference.d.ts +13 -0
  206. package/src/shade/renderer/particles/vm/ParticleVMReference.d.ts.map +1 -1
  207. package/src/shade/renderer/particles/vm/ParticleVMReference.js +95 -17
  208. package/src/shade/renderer/rasterize/bucket/rasterization_has_opaque_bucket.d.ts +21 -0
  209. package/src/shade/renderer/rasterize/bucket/rasterization_has_opaque_bucket.d.ts.map +1 -0
  210. package/src/shade/renderer/rasterize/bucket/rasterization_has_opaque_bucket.js +28 -0
  211. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.d.ts.map +1 -1
  212. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.js +19 -2
  213. package/src/shade/renderer/rasterize/standard/graph_rasterize_scene.d.ts.map +1 -1
  214. package/src/shade/renderer/rasterize/standard/graph_rasterize_scene.js +19 -2
  215. package/src/shade/renderer/shader/type/WebGPUStruct.d.ts.map +1 -1
  216. package/src/shade/renderer/shader/type/WebGPUStruct.js +12 -0
  217. package/src/shade/renderer/texture/mipmaps/WebGPUMipmapGenerator.d.ts +8 -2
  218. package/src/shade/renderer/texture/mipmaps/WebGPUMipmapGenerator.d.ts.map +1 -1
  219. package/src/shade/renderer/texture/mipmaps/WebGPUMipmapGenerator.js +41 -6
  220. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +2383 -2356
  221. package/src/view/tooltip/DomTooltipManager.d.ts +7 -0
  222. package/src/view/tooltip/DomTooltipManager.d.ts.map +1 -1
  223. package/src/view/tooltip/DomTooltipManager.js +22 -0
  224. package/src/view/tooltip/DomTooltipObserver.d.ts.map +1 -1
  225. package/src/view/tooltip/DomTooltipObserver.js +13 -3
  226. package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_CREASE_ANGLE.js +0 -45
@@ -62,14 +62,23 @@ words = 128 B). Word 0 is a reserved header holding the owning **emitter row** a
62
62
  pool + single alive/dead lists means **one** emit dispatch and **one** simulate dispatch cover every
63
63
  emitter — the Destiny single-shader property.
64
64
 
65
- Lifecycle uses the canonical GPU dead-list / alive-list ping-pong:
66
- - `dead_list`: stack of free slot indices (+ atomic count).
67
- - `alive_list[2]`: ping-pong compact lists of live slot indices (+ counts).
65
+ Lifecycle uses the canonical GPU dead-list / alive-list pair:
66
+ - `dead_list`: stack of free slot indices (+ atomic count). Filled `dead[i] = i` at construction by
67
+ `shaders/shader_particle_init_free_list.js` — a dispatch, not a host upload, which is why the
68
+ buffer carries no `COPY_DST`.
69
+ - `alive_list`: a compact list of live slot indices (+ count), rebuilt from scratch every frame.
68
70
  - `counters`: atomics {alive_count, dead_count, spawn budget, …} + derived indirect dispatch/draw args.
69
71
 
70
72
  Emit: pop dead → run INIT → append `alive_new`. Simulate: read the **coherence list** last frame's
71
73
  bucketing pass built from `alive_current` → run UPDATE → kill pushes to `dead`, survival appends to
72
- `alive_new`. Swap lists each frame.
74
+ `alive_new`.
75
+
76
+ There is no ping-pong pair, although the shape invites one. The frame appends to the new list from a
77
+ counter its first pass zeroes and the draws walk that same new list, so the previous frame's list has
78
+ exactly one reader in the whole design: the bucketing rebuild on a frame whose program assignments
79
+ changed. So the list is taken from the graphics context's buffer allocator and given back every
80
+ frame, before the next is asked for — the pool answers with the same memory, the reserved footprint
81
+ is one list, and only a rebuild frame holds two. See `GPUParticleSystem#swap_alive_list`.
73
82
 
74
83
  **The loop is strictly GPU-driven.** The build-indirect pass snapshots the padded length of the
75
84
  coherence list into `counters[ALIVE_IN]` and writes the indirect dispatch args next frame's simulate
@@ -644,7 +653,7 @@ through `GPUTextureContext`.
644
653
 
645
654
  Constructed once against a `GPUSceneContext`, in the mould of `ReSTIRDI`. It owns persistent buffers
646
655
  and only those — a buffer is reserved here if and only if its contents cross the frame boundary:
647
- pool, alive lists, dead list, counters, the coherence list, simulate's indirect dispatch args, the
656
+ pool, alive list, dead list, counters, the coherence list, simulate's indirect dispatch args, the
648
657
  emitter database (both tables), the packed program buffer, the spawn-command ring and the warm-up
649
658
  queue — and grows the ones that depend on the emitter set inside `execute`. Everything else a frame
650
659
  touches is the frame graph's, declared by the pass that first writes it: the bucketing scratch, the
@@ -20,7 +20,7 @@
20
20
  * ## What it owns
21
21
  *
22
22
  * Persistent GPU state, and only that: a buffer is reserved here if and only if its contents have
23
- * to survive to the next frame. That is the particle pool, the ping-pong alive lists, the dead
23
+ * to survive to the next frame. That is the particle pool, the alive list, the dead
24
24
  * free-list, the counters, the simulation-order (coherence) list, simulate's indirect dispatch args
25
25
  * (written at the end of one frame, read by the next), the emitter database (the emitter table and
26
26
  * the GPU-owned state table, one buffer), the packed program buffer, and the two CPU-uploaded
@@ -37,6 +37,22 @@
37
37
  * Reserving them here instead would be megabytes at the default capacity that no other feature
38
38
  * could ever borrow, and most of them are cleared by a reset pass every frame regardless.
39
39
  *
40
+ * ## The alive list is borrowed, not reserved
41
+ *
42
+ * The alive list crosses the frame boundary and so cannot be the graph's — but it does not have to
43
+ * be the feature's either. It is taken from the graphics context's buffer allocator and given back
44
+ * every frame ({@link #swap_alive_list}), which is the difference between one list and two.
45
+ *
46
+ * Two was the obvious reading of the ping-pong: the frame builds a new list while the previous one
47
+ * is still there to be read. But nothing reads the previous one. Emit and simulate append to the
48
+ * new list from a counter the frame resets to zero, the draws walk that same new list, and the only
49
+ * pass that ever looks at the one before it is the bucketing rebuild on a frame whose program
50
+ * assignments changed — a live program edit, not a frame. So on an ordinary frame the previous list
51
+ * goes back to the pool BEFORE the new one is asked for, the pool answers with the same memory, and
52
+ * the feature's reserved footprint is one list rather than two. On the rare frame that does rebuild,
53
+ * the old list is held one frame longer — until the graph recorded around it has been submitted —
54
+ * and the feature briefly costs what it used to cost always.
55
+ *
40
56
  * Every pass stays within the eight storage buffers a stage gets by default: the emitter record
41
57
  * and the GPU-owned state are two tables of one database buffer, and the VM's constants and code
42
58
  * are one packed buffer. The one exception is the wide-register fallback (below), whose emit and
@@ -123,7 +139,7 @@ export class GPUParticleSystem {
123
139
  */
124
140
  registry: EmitterRegistry;
125
141
  /**
126
- * Frames elapsed. Drives the alive-list ping-pong and the VM's frame builtin.
142
+ * Frames elapsed. The VM's frame builtin.
127
143
  * @type {number}
128
144
  */
129
145
  frame: number;
@@ -151,6 +167,10 @@ export class GPUParticleSystem {
151
167
  * them — never for the frame loop, which reads nothing back. Entries that grow with the
152
168
  * emitter set are replaced in place.
153
169
  *
170
+ * `alive` and `alive_write` are one buffer under two names — what the feature holds, and what
171
+ * the frame writes — and `alive_read` is the previous frame's list, present only on the frames
172
+ * that read one. The list itself belongs to the allocator, not here; see {@link #swap_alive_list}.
173
+ *
154
174
  * @type {object}
155
175
  */
156
176
  buffers: object;
@@ -178,11 +198,6 @@ export class GPUParticleSystem {
178
198
  * @returns {number}
179
199
  */
180
200
  bucketCount(): number;
181
- /**
182
- * Index of the alive list this frame reads from / writes to (ping-pong by frame parity).
183
- * @returns {number}
184
- */
185
- get alive_write_index(): number;
186
201
  /**
187
202
  * Ask for `count` particles from an emitter, on top of its rate, on the next frame. Goes
188
203
  * through the GPU path — the tick pass folds it into the emitter's accumulator, the budget and
@@ -330,9 +345,9 @@ export class GPUParticleSystem {
330
345
  set_transform_row(emitter: import("./runtime/ParticleEmitter.js").ParticleEmitter, transform_row: number): void;
331
346
  /**
332
347
  * Sizes of the persistent buffers, in bytes, as they stand — one entry per distinct buffer, so
333
- * this is the feature's reserved footprint and not a list of the names it binds them under.
334
- * `alive_read` and `alive_write` are the two `alive` entries under the frame's names for them,
335
- * and would otherwise be counted a second time each. Exposed for tests.
348
+ * this is the feature's footprint and not a list of the names it binds them under.
349
+ * `alive_write` is `alive` under the frame's name for it and would otherwise be counted twice;
350
+ * `alive_read` is a second list, and appears only on the frames that hold one. Exposed for tests.
336
351
  *
337
352
  * @returns {Object<string, number>}
338
353
  */
@@ -1 +1 @@
1
- {"version":3,"file":"GPUParticleSystem.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/particles/GPUParticleSystem.js"],"names":[],"mappings":"AAuDA;;;;;;;;;;;;;GAaG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6EG;AACH;IAqJI;;;;;;OAMG;IACH,sBANW,MAAM,iBACN,OAAO,6BAA6B,EAAE,eAAe;QAEpC,QAAQ,GAAzB,MAAM;QACW,sBAAsB,GAAvC,MAAM;OAoBhB;IA5KD;;;OAGG;IACH,UAFU,MAAM,CAEP;IAET;;;;OAIG;IACH,eAFU,OAAO,6BAA6B,EAAE,eAAe,CAEjD;IAEd;;;OAGG;IACH,UAFU,MAAM,CAEP;IAET;;OAEG;IACH,UAFU,eAAe,CAEhB;IAET;;;OAGG;IACH,OAFU,MAAM,CAEN;IAEV;;;OAGG;IACH,MAFU,MAAM,CAEP;IAET;;;OAGG;IACH,qBAFU,MAAM,CAEQ;IAExB;;;;OAIG;IACH,sBAAM;IAEN,yBAAyB;IACzB,0BAAc;IAEd;;;;;;;OAOG;IACH,SAFU,MAAM,CAEH;IAkHb;;;OAGG;IACH,kCAEC;IAED;;;OAGG;IACH,yEAEC;IAED;;;;;;;;;;;;OAYG;IACH,eAFa,MAAM,CAIlB;IAED;;;OAGG;IACH,gCAEC;IAED;;;;;;;OAOG;IACH,eAHW,OAAO,8BAA8B,EAAE,eAAe,SACtD,MAAM,QAwBhB;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACH;QAPkG,KAAK,EAA5F,OAAO,2DAA2D,EAAE,UAAU;QAC/D,MAAM,EAArB,MAAM;QACS,cAAc,EAA7B,MAAM;QAES,EAAE,EAAjB,MAAM;QACJ,OAAO,sBAAsB,EAAE,yBAAyB,CA4DpE;IAiBD;;;;;;;;;OASG;IACH;QANgG,KAAK,EAA1F,OAAO,2DAA2D,EAAE,UAAU;QACjE,MAAM,EAAnB,MAAM;QACkB,kBAAkB,EAA1C,UAAU,MAAM,CAAC;QACJ,SAAS,EAAtB,MAAM;QACJ,MAAM,CAalB;IAED;;;;;;;;;;OAUG;IACH;QAPgG,KAAK,EAA1F,OAAO,2DAA2D,EAAE,UAAU;QACjE,MAAM,EAAnB,MAAM;QACO,IAAI,EAAjB,MAAM;QACO,aAAa,EAA1B,MAAM;QACsD,MAAM,EAAlE;YAAC,UAAU,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAC;QACnD;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAC,CAkBjE;IAED;;;;;;;;;;;;;;OAcG;IACH;QAXgG,KAAK,EAA1F,OAAO,2DAA2D,EAAE,UAAU;QACrB,QAAQ,EAAjE,OAAO,2BAA2B,EAAE,cAAc;QACrC,MAAM,EAAnB,MAAM;QACO,aAAa,EAA1B,MAAM;QACO,IAAI,EAAjB,MAAM;QACO,QAAQ,EAArB,MAAM;QACO,eAAe,EAA5B,MAAM;QACO,oBAAoB,EAAjC,MAAM;QACoE,YAAY,EAAtF;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAC;YAAC,UAAU,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAA;SAAC;QACjE;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAC,CAsB/E;IAED;;;;;;;;;OASG;IACH,qBAHW,OAAO,8BAA8B,EAAE,eAAe,iBACtD,MAAM,QAehB;IAED;;;;OAIG;IACH,wBAFW,OAAO,8BAA8B,EAAE,eAAe,QAMhE;IAED;;;;;OAKG;IACH,2BAHW,OAAO,8BAA8B,EAAE,eAAe,iBACtD,MAAM,QAMhB;IAuVD;;;;;;;OAOG;IACH;YAFoB,MAAM,GAAE,MAAM;MAkBjC;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,cAFa,QAAQ,mBAAmB,CAAC,CA8BxC;IAED,gBAyBC;;CACJ;;;;;;;;WA3/Ba,MAAM;;;;UACN,MAAM;;;;kBACN,MAAM;;;;;cACN,MAAM;;;;;gBAEN,MAAM;;;;;oBAEN,MAAM;;gCAxCY,8BAA8B"}
1
+ {"version":3,"file":"GPUParticleSystem.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/particles/GPUParticleSystem.js"],"names":[],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { assert } from "../../../core/assert.js";
2
2
  import { ShadeGPUCommandContext } from "../../device/ShadeGPUCommandContext.js";
3
+ import { BufferResourceDescriptor } from "../../device/graph/BufferResourceDescriptor.js";
3
4
  import { warn_limited } from "../../util/warn_limited.js";
4
5
  import { hash_mix2 } from "../../../core/math/hash/hash_mix2.js";
5
6
  import {
@@ -19,6 +20,10 @@ import {
19
20
  particle_counters_word_count,
20
21
  } from "./data/PARTICLE_COUNTERS.js";
21
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";
22
27
  import {
23
28
  graph_particle_avboit_draw,
24
29
  graph_particle_avboit_occupancy,
@@ -38,7 +43,7 @@ const WORD = Uint32Array.BYTES_PER_ELEMENT;
38
43
  big ones and are touched by nothing but shaders. Adding a flag "just in case" moves 64 MiB onto a
39
44
  slower path for a copy nobody makes — see `BufferResourceDescriptor`.
40
45
  */
41
- /** Written only by shaders. */
46
+ /** Written only by shaders. The free list is one of these: `init_free_list` fills it on-device. */
42
47
  const STORAGE = GPUBufferUsage.STORAGE;
43
48
  /** Seeded or refilled from the host with `writeBuffer`. */
44
49
  const STORAGE_UPLOAD = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST;
@@ -76,7 +81,7 @@ const DEFAULT_CAPACITY = 524288;
76
81
  * ## What it owns
77
82
  *
78
83
  * Persistent GPU state, and only that: a buffer is reserved here if and only if its contents have
79
- * to survive to the next frame. That is the particle pool, the ping-pong alive lists, the dead
84
+ * to survive to the next frame. That is the particle pool, the alive list, the dead
80
85
  * free-list, the counters, the simulation-order (coherence) list, simulate's indirect dispatch args
81
86
  * (written at the end of one frame, read by the next), the emitter database (the emitter table and
82
87
  * the GPU-owned state table, one buffer), the packed program buffer, and the two CPU-uploaded
@@ -93,6 +98,22 @@ const DEFAULT_CAPACITY = 524288;
93
98
  * Reserving them here instead would be megabytes at the default capacity that no other feature
94
99
  * could ever borrow, and most of them are cleared by a reset pass every frame regardless.
95
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
+ *
96
117
  * Every pass stays within the eight storage buffers a stage gets by default: the emitter record
97
118
  * and the GPU-owned state are two tables of one database buffer, and the VM's constants and code
98
119
  * are one packed buffer. The one exception is the wide-register fallback (below), whose emit and
@@ -173,7 +194,7 @@ export class GPUParticleSystem {
173
194
  registry;
174
195
 
175
196
  /**
176
- * Frames elapsed. Drives the alive-list ping-pong and the VM's frame builtin.
197
+ * Frames elapsed. The VM's frame builtin.
177
198
  * @type {number}
178
199
  */
179
200
  frame = 0;
@@ -206,6 +227,10 @@ export class GPUParticleSystem {
206
227
  * them — never for the frame loop, which reads nothing back. Entries that grow with the
207
228
  * emitter set are replaced in place.
208
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
+ *
209
234
  * @type {object}
210
235
  */
211
236
  buffers = {};
@@ -216,6 +241,32 @@ export class GPUParticleSystem {
216
241
  */
217
242
  #sim_bucket_capacity = 0;
218
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
+
219
270
  /**
220
271
  * The spawn-command ring: `[row, count]` pairs, filled by {@link spawn}, merged and uploaded on
221
272
  * the next {@link execute}.
@@ -355,14 +406,6 @@ export class GPUParticleSystem {
355
406
  return Math.max(1, this.registry.program_id_capacity);
356
407
  }
357
408
 
358
- /**
359
- * Index of the alive list this frame reads from / writes to (ping-pong by frame parity).
360
- * @returns {number}
361
- */
362
- get alive_write_index() {
363
- return 1 - (this.frame % 2);
364
- }
365
-
366
409
  /**
367
410
  * Ask for `count` particles from an emitter, on top of its rate, on the next frame. Goes
368
411
  * through the GPU path — the tick pass folds it into the emitter's accumulator, the budget and
@@ -444,8 +487,9 @@ export class GPUParticleSystem {
444
487
  emitters.group_count = table.dispatch_group_count(PARTICLE_EMITTER_TICK_WORKGROUP_SIZE);
445
488
 
446
489
  const buffers = this.buffers;
447
- buffers.alive_write = buffers.alive[this.alive_write_index];
448
- buffers.alive_read = buffers.alive[1 - this.alive_write_index];
490
+
491
+ this.#swap_alive_list(frame.rebucket);
492
+
449
493
  // The table's database owns and grows its own buffer; it is re-read every frame for that
450
494
  // reason.
451
495
  buffers.emitter_database = this.registry.database.buffer;
@@ -907,6 +951,107 @@ export class GPUParticleSystem {
907
951
  this.#sim_bucket_capacity = buckets;
908
952
  }
909
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
+
910
1055
  /**
911
1056
  * The buffers whose size is fixed by the pool capacity, and the free list's initial contents.
912
1057
  */
@@ -916,11 +1061,12 @@ export class GPUParticleSystem {
916
1061
  const buffers = this.buffers;
917
1062
 
918
1063
  buffers.pool = device.createBuffer({ label: "particles/pool", size: capacity * PARTICLE_RECORD_WORD_COUNT * WORD, usage: STORAGE });
919
- buffers.alive = [
920
- device.createBuffer({ label: "particles/alive_a", size: capacity * WORD, usage: STORAGE }),
921
- device.createBuffer({ label: "particles/alive_b", size: capacity * WORD, usage: STORAGE }),
922
- ];
923
- buffers.dead = device.createBuffer({ label: "particles/dead", size: capacity * WORD, usage: STORAGE_UPLOAD });
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 });
924
1070
  // Just the counters to begin with. The per-emitter bounds accumulator that shares this
925
1071
  // buffer follows the emitter table's capacity, and there is no table yet.
926
1072
  buffers.counters = device.createBuffer({ label: "particles/counters", size: PARTICLE_COUNTER_COUNT * WORD, usage: STORAGE_GROWABLE });
@@ -936,16 +1082,14 @@ export class GPUParticleSystem {
936
1082
  // The warm-up queue: one entry to start with, grown on upload to what a frame registers.
937
1083
  buffers.warmup_queue = device.createBuffer({ label: "particles/warmup_queue", size: PARTICLE_WARMUP_QUEUE_WORDS * WORD, usage: STORAGE_UPLOAD });
938
1084
 
939
- // Every slot free, and the counters saying so.
940
- const dead = new Uint32Array(capacity);
941
- for (let i = 0; i < capacity; i++) {
942
- dead[i] = i;
943
- }
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.
944
1087
  const counters = new Uint32Array(PARTICLE_COUNTER_COUNT);
945
1088
  counters[PARTICLE_COUNTER.DEAD] = capacity;
946
1089
 
947
- device.queue.writeBuffer(buffers.dead, 0, dead);
948
1090
  device.queue.writeBuffer(buffers.counters, 0, counters);
1091
+
1092
+ this.#init_free_list();
949
1093
  }
950
1094
 
951
1095
  /**
@@ -978,9 +1122,9 @@ export class GPUParticleSystem {
978
1122
 
979
1123
  /**
980
1124
  * Sizes of the persistent buffers, in bytes, as they stand — one entry per distinct buffer, so
981
- * this is the feature's reserved footprint and not a list of the names it binds them under.
982
- * `alive_read` and `alive_write` are the two `alive` entries under the frame's names for them,
983
- * and would otherwise be counted a second time each. Exposed for tests.
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.
984
1128
  *
985
1129
  * @returns {Object<string, number>}
986
1130
  */
@@ -988,15 +1132,13 @@ export class GPUParticleSystem {
988
1132
  const sizes = {};
989
1133
  const counted = new Set();
990
1134
 
991
- for (const [name, value] of Object.entries(this.buffers)) {
992
- for (const buffer of Array.isArray(value) ? value : [value]) {
993
- if (buffer === undefined || buffer === null || counted.has(buffer)) {
994
- continue;
995
- }
996
-
997
- counted.add(buffer);
998
- sizes[name] = buffer.size;
1135
+ for (const [name, buffer] of Object.entries(this.buffers)) {
1136
+ if (buffer === undefined || buffer === null || counted.has(buffer)) {
1137
+ continue;
999
1138
  }
1139
+
1140
+ counted.add(buffer);
1141
+ sizes[name] = buffer.size;
1000
1142
  }
1001
1143
 
1002
1144
  return sizes;
@@ -1061,10 +1203,12 @@ export class GPUParticleSystem {
1061
1203
  this.registry.remove(emitter);
1062
1204
  }
1063
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
+
1064
1210
  for (const buffer of Object.values(this.buffers)) {
1065
- if (Array.isArray(buffer)) {
1066
- buffer.forEach((b) => b.destroy());
1067
- } else if (buffer !== undefined && buffer !== null) {
1211
+ if (buffer !== undefined && buffer !== null) {
1068
1212
  buffer.destroy();
1069
1213
  }
1070
1214
  }
@@ -1 +1 @@
1
- {"version":3,"file":"simplex_reference.d.ts","sourceRoot":"","sources":["../../../../../../../src/shade/renderer/particles/graph/groups/simplex_reference.js"],"names":[],"mappings":"AAyGA;;;;;GAKG;AACH,0CAHW,MAAM,EAAE,GACN,MAAM,CA2ElB"}
1
+ {"version":3,"file":"simplex_reference.d.ts","sourceRoot":"","sources":["../../../../../../../src/shade/renderer/particles/graph/groups/simplex_reference.js"],"names":[],"mappings":"AA+GA;;;;;GAKG;AACH,0CAHW,MAAM,EAAE,GACN,MAAM,CA2ElB"}
@@ -18,8 +18,14 @@
18
18
  * double-precision oracle takes the other branch there and disagrees with the VM about a third of the
19
19
  * time, and it would be the one that was wrong: a GPU is `f32` too.
20
20
  *
21
- * So `fr` is applied wherever the VM would have stored a register, and nowhere else. Sums inside a
22
- * dot product are not rounded per term, because the opcode accumulates and stores once.
21
+ * So `fr` is applied wherever the VM would have stored a register — and inside `DOT` and
22
+ * `NORMALIZE` as well, because the shader computes those entirely in f32 and the reference executor
23
+ * now does the same. This file used to say the sum was accumulated wide and rounded once, which was
24
+ * a faithful transcription of a reference VM that summed in f64: f32 results without f32 range.
25
+ *
26
+ * The one place `fr` is deliberately absent from an intermediate is a product the group spells as a
27
+ * `MAD` — `row * ns_x + ns_y`, and the two reflections below it. `MAD` models the fused form, so
28
+ * its product is not rounded on either executor; see the opcode in {@link ../../vm/ParticleVMReference.js}.
23
29
  */
24
30
 
25
31
  /** @type {function(number):number} */
@@ -34,16 +40,16 @@ const G3 = 1 / 6;
34
40
  /**
35
41
  * @param {number[]} a
36
42
  * @param {number[]} b
37
- * @returns {number} the dot product, accumulated wide and rounded once, as `DOT` does
43
+ * @returns {number} the dot product, every step of it in f32, as `DOT` does
38
44
  */
39
45
  function dot(a, b) {
40
46
  let sum = 0;
41
47
 
42
48
  for (let i = 0; i < a.length; i++) {
43
- sum += a[i] * b[i];
49
+ sum = fr(sum + fr(a[i] * b[i]));
44
50
  }
45
51
 
46
- return fr(sum);
52
+ return sum;
47
53
  }
48
54
 
49
55
  /**
@@ -58,8 +64,8 @@ function step(edge, x) {
58
64
  }
59
65
 
60
66
  /**
61
- * `NORMALIZE`, as the opcode does it: the length is accumulated and taken wide and only the lanes
62
- * are rounded, and a zero vector normalises to zero rather than to nothing.
67
+ * `NORMALIZE`, as the opcode does it: every step of the length in f32, and a zero vector normalises
68
+ * to zero rather than to nothing.
63
69
  *
64
70
  * The group used to spell this as Gustavson's `taylorInvSqrt` — a line through the band the gradient
65
71
  * table's squared lengths fall in — and now calls the opcode, which is one instruction rather than
@@ -73,12 +79,12 @@ function normalize(v) {
73
79
  let sum = 0;
74
80
 
75
81
  for (const component of v) {
76
- sum += component * component;
82
+ sum = fr(sum + fr(component * component));
77
83
  }
78
84
 
79
- const length = Math.sqrt(sum);
85
+ const length = fr(Math.sqrt(sum));
80
86
 
81
- const inverse = length > 0 ? 1 / length : 0;
87
+ const inverse = length > 0 ? fr(1 / length) : 0;
82
88
 
83
89
  return v.map(component => fr(component * inverse));
84
90
  }
@@ -59,7 +59,7 @@
59
59
  * ## What is imported, and what the graph owns
60
60
  *
61
61
  * A buffer is imported only if its contents cross the frame boundary: the pool, the free list, the
62
- * counters, the alive lists, `sim_list` (written here, read by next frame's simulate),
62
+ * counters, the alive list, `sim_list` (written here, read by next frame's simulate),
63
63
  * `dispatch_args` (the same), the program blob and the two CPU-uploaded queues. Everything else is
64
64
  * produced and consumed inside one graph, so the pass that first writes it declares it with a
65
65
  * {@link BufferResourceDescriptor} in place of a handle and the graph allocates it just in time and
@@ -85,7 +85,9 @@
85
85
  * @param {import("../../../engine/graphics/render/frame_graph/FrameGraph.js").FrameGraph} params.graph
86
86
  * @param {number} params.camera graph handle of the view's camera uniform (`CAMERA_UNIFORM_STRUCT`)
87
87
  * @param {number} params.scene_database graph handle of the scene database — the emitters' nodes
88
- * @param {object} params.gpu the persistent buffers, see {@link GPUParticleSystem}
88
+ * @param {object} params.gpu the persistent buffers, see {@link GPUParticleSystem}. `gpu.alive_read`
89
+ * is the exception: it is the previous frame's alive list and is present only when
90
+ * `frame.rebucket` says this frame reads one
89
91
  * @param {object} params.emitters the emitter table's shape this frame
90
92
  * @param {number} params.emitters.capacity the table's element capacity — the length of the count
91
93
  * list the scan consumes
@@ -1 +1 @@
1
- {"version":3,"file":"graph_particles.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/particles/graph_particles.js"],"names":[],"mappings":"AAuCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuEG;AAEH;;;;;;;GAOG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH;IA7BkG,KAAK,EAA5F,OAAO,2DAA2D,EAAE,UAAU;IAC/D,MAAM,EAArB,MAAM;IACS,cAAc,EAA7B,MAAM;IACS,GAAG,EAAlB,MAAM;IACS,QAAQ;QACC,QAAQ,EAAhC,MAAM;QAEkB,WAAW,EAAnC,MAAM;;IACS,QAAQ,EAAvB,MAAM;IACS,YAAY,EAA3B,MAAM;IAEU,cAAc,EAA9B,OAAO;IAGQ,KAAK;QACC,EAAE,EAAvB,MAAM;QACe,IAAI,EAAzB,MAAM;QACe,KAAK,EAA1B,MAAM;QACiB,QAAQ,GAA/B,OAAO;QACc,WAAW,EAAhC,MAAM;QACe,mBAAmB,EAAxC,MAAM;QACe,YAAY,EAAjC,MAAM;QAGe,eAAe,EAApC,MAAM;QAC6D,eAAe,EAAlF,OAAO,6BAA6B,EAAE,cAAc;;IAElD,yBAAyB,CA4TrC;;;;;UAlWa,MAAM;;;;WACN,MAAM;;;;eACN,MAAM;;;;;sBACN,MAAM"}
1
+ {"version":3,"file":"graph_particles.d.ts","sourceRoot":"","sources":["../../../../../src/shade/renderer/particles/graph_particles.js"],"names":[],"mappings":"AAuCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuEG;AAEH;;;;;;;GAOG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH;IA/BkG,KAAK,EAA5F,OAAO,2DAA2D,EAAE,UAAU;IAC/D,MAAM,EAArB,MAAM;IACS,cAAc,EAA7B,MAAM;IACS,GAAG,EAAlB,MAAM;IAGS,QAAQ;QACC,QAAQ,EAAhC,MAAM;QAEkB,WAAW,EAAnC,MAAM;;IACS,QAAQ,EAAvB,MAAM;IACS,YAAY,EAA3B,MAAM;IAEU,cAAc,EAA9B,OAAO;IAGQ,KAAK;QACC,EAAE,EAAvB,MAAM;QACe,IAAI,EAAzB,MAAM;QACe,KAAK,EAA1B,MAAM;QACiB,QAAQ,GAA/B,OAAO;QACc,WAAW,EAAhC,MAAM;QACe,mBAAmB,EAAxC,MAAM;QACe,YAAY,EAAjC,MAAM;QAGe,eAAe,EAApC,MAAM;QAC6D,eAAe,EAAlF,OAAO,6BAA6B,EAAE,cAAc;;IAElD,yBAAyB,CAgUrC;;;;;UAxWa,MAAM;;;;WACN,MAAM;;;;eACN,MAAM;;;;;sBACN,MAAM"}
@@ -98,7 +98,7 @@ import { graph_particle_warmup } from "./warmup/graph_particle_warmup.js";
98
98
  * ## What is imported, and what the graph owns
99
99
  *
100
100
  * A buffer is imported only if its contents cross the frame boundary: the pool, the free list, the
101
- * counters, the alive lists, `sim_list` (written here, read by next frame's simulate),
101
+ * counters, the alive list, `sim_list` (written here, read by next frame's simulate),
102
102
  * `dispatch_args` (the same), the program blob and the two CPU-uploaded queues. Everything else is
103
103
  * produced and consumed inside one graph, so the pass that first writes it declares it with a
104
104
  * {@link BufferResourceDescriptor} in place of a handle and the graph allocates it just in time and
@@ -126,7 +126,9 @@ import { graph_particle_warmup } from "./warmup/graph_particle_warmup.js";
126
126
  * @param {import("../../../engine/graphics/render/frame_graph/FrameGraph.js").FrameGraph} params.graph
127
127
  * @param {number} params.camera graph handle of the view's camera uniform (`CAMERA_UNIFORM_STRUCT`)
128
128
  * @param {number} params.scene_database graph handle of the scene database — the emitters' nodes
129
- * @param {object} params.gpu the persistent buffers, see {@link GPUParticleSystem}
129
+ * @param {object} params.gpu the persistent buffers, see {@link GPUParticleSystem}. `gpu.alive_read`
130
+ * is the exception: it is the previous frame's alive list and is present only when
131
+ * `frame.rebucket` says this frame reads one
130
132
  * @param {object} params.emitters the emitter table's shape this frame
131
133
  * @param {number} params.emitters.capacity the table's element capacity — the length of the count
132
134
  * list the scan consumes
@@ -197,6 +199,10 @@ export function graph_particles_simulate({
197
199
  if (frame.rebucket) {
198
200
  // Live program edits can split a previously uniform wave. Rebuild from the previous
199
201
  // compact alive list BEFORE reset discards its count and before simulate broadcasts code.
202
+ //
203
+ // `gpu.alive_read` exists only here. It is the one reader the previous frame's list ever
204
+ // has, which is why the feature holds one list rather than a ping-pong pair and keeps the
205
+ // old one only across a frame that rebuckets — see `GPUParticleSystem#swap_alive_list`.
200
206
  const bucketed = graph_particle_bucket({
201
207
  graph, emitter_database, pool, counters, capacity, bucket_count,
202
208
  alive: graph_import_buffer(graph, gpu.alive_read, "particles/alive_read"),
@@ -1 +1 @@
1
- {"version":3,"file":"particle_vm_semantics.d.ts","sourceRoot":"","sources":["../../../../../../src/shade/renderer/particles/optimizer/particle_vm_semantics.js"],"names":[],"mappings":"AA4IA;;;;;;GAMG;AACH,oCAHW,MAAM,GACJ,mBAAmB,CAW/B;AA1JD;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,+CAA+C;AAC/C,2BADW,IAAI,MAAM,EAAE,mBAAmB,CAAC,CACL;;;;;;;kBA1BxB,OAAO;;;;;cAGP,OAAO;;;;;;gBAEP,OAAO;;;;;;eAGP,OAAO;;;;;;;;cAGP,OAAO;;;;;;;;;;sBAKP,OAAO"}
1
+ {"version":3,"file":"particle_vm_semantics.d.ts","sourceRoot":"","sources":["../../../../../../src/shade/renderer/particles/optimizer/particle_vm_semantics.js"],"names":[],"mappings":"AAkJA;;;;;;GAMG;AACH,oCAHW,MAAM,GACJ,mBAAmB,CAW/B;AAhKD;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,+CAA+C;AAC/C,2BADW,IAAI,MAAM,EAAE,mBAAmB,CAAC,CACL;;;;;;;kBA1BxB,OAAO;;;;;cAGP,OAAO;;;;;;gBAEP,OAAO;;;;;;eAGP,OAAO;;;;;;;;cAGP,OAAO;;;;;;;;;;sBAKP,OAAO"}
@@ -89,11 +89,17 @@ semantics(VM_OP.ADD, EXACT);
89
89
  semantics(VM_OP.SUB, EXACT);
90
90
  semantics(VM_OP.MUL, EXACT);
91
91
  semantics(VM_OP.DIV, EXACT);
92
- // `a * b + c` is one rounding on hardware that contracts it into an FMA and two in the reference.
92
+ // The reference leaves the product unrounded — it models the fused form — and the shader spells
93
+ // `a * b + c`, which a driver may contract into an FMA or may round in between. The two executors
94
+ // agree exactly on the first reading and not on the second, and nothing in WGSL says which one a
95
+ // device takes.
93
96
  semantics(VM_OP.MAD, INEXACT);
94
97
  semantics(VM_OP.MIN, EXACT);
95
98
  semantics(VM_OP.MAX, EXACT);
96
- // The reference spells it `a + (b - a) * t`; WGSL's `mix` is free to round it differently.
99
+ // The reference now spells it as WGSL defines it, `a * (1 - t) + b * t`, rather than as the lerp
100
+ // `a + (b - a) * t` — those are different numbers, not different roundings, wherever the span
101
+ // `b - a` overflows or swallows an end. What is left is that a driver may contract either product
102
+ // with the sum, which is the same slack `MAD` has.
97
103
  semantics(VM_OP.MIX, INEXACT);
98
104
  semantics(VM_OP.CLAMP, EXACT);
99
105
 
@@ -1 +1 @@
1
- {"version":3,"file":"pass_fold_constants.d.ts","sourceRoot":"","sources":["../../../../../../src/shade/renderer/particles/optimizer/pass_fold_constants.js"],"names":[],"mappings":"AAyF4B,sJAEwD;AAuJpF;;;;;;;;;;GAUG;AACH,gDANW,MAAM,KACN,MAAM,KACN,MAAM,KACN,MAAM,GACJ,MAAM,CAmElB;AA9SD,qBAAqB;AACrB,2CADW,MAAM,CAC4C;AAE7D;;;;;GAKG;AACH,0CAFU,IAAI,MAAM,CAAC,CAE2C"}
1
+ {"version":3,"file":"pass_fold_constants.d.ts","sourceRoot":"","sources":["../../../../../../src/shade/renderer/particles/optimizer/pass_fold_constants.js"],"names":[],"mappings":"AAyF4B,sJAEwD;AAuJpF;;;;;;;;;;GAUG;AACH,gDANW,MAAM,KACN,MAAM,KACN,MAAM,KACN,MAAM,GACJ,MAAM,CAyElB;AApTD,qBAAqB;AACrB,2CADW,MAAM,CAC4C;AAE7D;;;;;GAKG;AACH,0CAFU,IAAI,MAAM,CAAC,CAE2C"}