@woosh/meep-engine 3.16.0 → 3.17.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 (410) hide show
  1. package/editor/particles/effect/ParticleEffectDocument.d.ts +21 -6
  2. package/editor/particles/effect/ParticleEffectDocument.d.ts.map +1 -1
  3. package/editor/particles/effect/ParticleEffectDocument.js +38 -17
  4. package/editor/view/node-graph/NodeGraphClipboard.d.ts +186 -0
  5. package/editor/view/node-graph/NodeGraphClipboard.d.ts.map +1 -0
  6. package/editor/view/node-graph/NodeGraphClipboard.js +231 -0
  7. package/editor/view/node-graph/NodeGraphEditorView.d.ts +59 -1
  8. package/editor/view/node-graph/NodeGraphEditorView.d.ts.map +1 -1
  9. package/editor/view/node-graph/NodeGraphEditorView.js +248 -5
  10. package/editor/view/node-graph/NodeGraphView.d.ts.map +1 -1
  11. package/editor/view/node-graph/NodeGraphView.js +84 -8
  12. package/editor/view/node-graph/actions/connection_bind_actions.d.ts +23 -0
  13. package/editor/view/node-graph/actions/connection_bind_actions.d.ts.map +1 -0
  14. package/editor/view/node-graph/actions/connection_bind_actions.js +39 -0
  15. package/editor/view/particles/effect/ParticleCodeEditorView.d.ts.map +1 -1
  16. package/editor/view/particles/effect/ParticleCodeEditorView.js +4 -3
  17. package/editor/view/particles/effect/ParticleCurveEditorView.d.ts +20 -0
  18. package/editor/view/particles/effect/ParticleCurveEditorView.d.ts.map +1 -0
  19. package/editor/view/particles/effect/ParticleCurveEditorView.js +574 -0
  20. package/editor/view/particles/effect/ParticleEmitterInspectorView.d.ts.map +1 -1
  21. package/editor/view/particles/effect/ParticleEmitterInspectorView.js +27 -1
  22. package/editor/view/particles/effect/ParticleGradientEditorView.d.ts +24 -0
  23. package/editor/view/particles/effect/ParticleGradientEditorView.d.ts.map +1 -0
  24. package/editor/view/particles/effect/ParticleGradientEditorView.js +472 -0
  25. package/editor/view/particles/effect/ParticleGraphEditorView.d.ts +5 -0
  26. package/editor/view/particles/effect/ParticleGraphEditorView.d.ts.map +1 -1
  27. package/editor/view/particles/effect/ParticleGraphEditorView.js +61 -31
  28. package/editor/view/particles/effect/ParticleNodeParametersView.d.ts.map +1 -1
  29. package/editor/view/particles/effect/ParticleNodeParametersView.js +33 -0
  30. package/editor/view/particles/effect/particle-editor.css +123 -35
  31. package/package.json +1 -1
  32. package/src/core/binary/allocator/OffsetAllocator.d.ts +25 -16
  33. package/src/core/binary/allocator/OffsetAllocator.d.ts.map +1 -1
  34. package/src/core/binary/allocator/OffsetAllocator.js +10 -1
  35. package/src/core/collection/array/typed/float32_array_hash.d.ts +21 -0
  36. package/src/core/collection/array/typed/float32_array_hash.d.ts.map +1 -0
  37. package/src/core/collection/array/typed/float32_array_hash.js +38 -0
  38. package/src/core/graph/csr/CSRGraph.d.ts +8 -1
  39. package/src/core/graph/csr/CSRGraph.d.ts.map +1 -1
  40. package/src/core/graph/csr/CSRGraph.js +14 -6
  41. package/src/core/model/node-graph/visual/layout/LayoutProblem.d.ts +24 -0
  42. package/src/core/model/node-graph/visual/layout/LayoutProblem.d.ts.map +1 -1
  43. package/src/core/model/node-graph/visual/layout/LayoutProblem.js +27 -0
  44. package/src/core/model/node-graph/visual/layout/NodeGraphLayoutOptions.d.ts +26 -0
  45. package/src/core/model/node-graph/visual/layout/NodeGraphLayoutOptions.d.ts.map +1 -1
  46. package/src/core/model/node-graph/visual/layout/NodeGraphLayoutOptions.js +28 -0
  47. package/src/core/model/node-graph/visual/layout/RouteGrid.d.ts +92 -3
  48. package/src/core/model/node-graph/visual/layout/RouteGrid.d.ts.map +1 -1
  49. package/src/core/model/node-graph/visual/layout/RouteGrid.js +405 -242
  50. package/src/core/model/node-graph/visual/layout/RouteSearch.d.ts +50 -5
  51. package/src/core/model/node-graph/visual/layout/RouteSearch.d.ts.map +1 -1
  52. package/src/core/model/node-graph/visual/layout/RouteSearch.js +160 -24
  53. package/src/core/model/node-graph/visual/layout/RouteSet.d.ts +7 -0
  54. package/src/core/model/node-graph/visual/layout/RouteSet.d.ts.map +1 -1
  55. package/src/core/model/node-graph/visual/layout/RouteSet.js +8 -0
  56. package/src/core/model/node-graph/visual/layout/layout_assign_coordinates.js +9 -3
  57. package/src/core/model/node-graph/visual/layout/layout_build_problem.d.ts.map +1 -1
  58. package/src/core/model/node-graph/visual/layout/layout_build_problem.js +7 -0
  59. package/src/core/model/node-graph/visual/layout/layout_insert_lanes.d.ts +9 -3
  60. package/src/core/model/node-graph/visual/layout/layout_insert_lanes.d.ts.map +1 -1
  61. package/src/core/model/node-graph/visual/layout/layout_insert_lanes.js +17 -3
  62. package/src/core/model/node-graph/visual/layout/layout_node_graph.d.ts +9 -6
  63. package/src/core/model/node-graph/visual/layout/layout_node_graph.d.ts.map +1 -1
  64. package/src/core/model/node-graph/visual/layout/layout_node_graph.js +49 -7
  65. package/src/core/model/node-graph/visual/layout/layout_order_layers.d.ts +11 -1
  66. package/src/core/model/node-graph/visual/layout/layout_order_layers.d.ts.map +1 -1
  67. package/src/core/model/node-graph/visual/layout/layout_order_layers.js +135 -16
  68. package/src/core/model/node-graph/visual/layout/route_connections.d.ts +29 -0
  69. package/src/core/model/node-graph/visual/layout/route_connections.d.ts.map +1 -1
  70. package/src/core/model/node-graph/visual/layout/route_connections.js +716 -309
  71. package/src/core/model/node-graph/visual/layout/route_grid_build.d.ts +3 -2
  72. package/src/core/model/node-graph/visual/layout/route_grid_build.d.ts.map +1 -1
  73. package/src/core/model/node-graph/visual/layout/route_grid_build.js +7 -15
  74. package/src/core/model/node-graph/visual/layout/route_nudge.d.ts +6 -0
  75. package/src/core/model/node-graph/visual/layout/route_nudge.d.ts.map +1 -1
  76. package/src/core/model/node-graph/visual/layout/route_nudge.js +42 -13
  77. package/src/engine/animation/AnimationUtils.js +246 -246
  78. package/src/engine/animation/Animations.js +74 -74
  79. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.js +2078 -2078
  80. package/src/engine/ecs/EventType.d.ts +0 -1
  81. package/src/engine/ecs/EventType.js +1 -26
  82. package/src/engine/ecs/ik/ik_write_local_rotation.d.ts +1 -1
  83. package/src/engine/ecs/ik/ik_write_local_rotation.d.ts.map +1 -1
  84. package/src/engine/ecs/ik/ik_write_local_rotation.js +3 -7
  85. package/src/engine/ecs/parent/EntityNode.d.ts.map +1 -1
  86. package/src/engine/ecs/parent/EntityNode.js +4 -9
  87. package/src/engine/ecs/terrain/ecs/cling/ClingToTerrainSystem.d.ts.map +1 -1
  88. package/src/engine/ecs/terrain/ecs/cling/ClingToTerrainSystem.js +360 -364
  89. package/src/engine/ecs/transform/TRANSFORM64_EVENT_CHANGE.d.ts +41 -0
  90. package/src/engine/ecs/transform/TRANSFORM64_EVENT_CHANGE.d.ts.map +1 -0
  91. package/src/engine/ecs/transform/TRANSFORM64_EVENT_CHANGE.js +40 -0
  92. package/src/engine/ecs/transform/TRANSFORM64_MIGRATION_PLAN_2026_09_04.md +8 -0
  93. package/src/engine/ecs/transform/t64_announce_change.d.ts +10 -7
  94. package/src/engine/ecs/transform/t64_announce_change.d.ts.map +1 -1
  95. package/src/engine/ecs/transform/t64_announce_change.js +12 -12
  96. package/src/engine/ecs/transform-attachment/TRANSFORM_ATTACHMENT_EVENT_CHANGE.d.ts +30 -0
  97. package/src/engine/ecs/transform-attachment/TRANSFORM_ATTACHMENT_EVENT_CHANGE.d.ts.map +1 -0
  98. package/src/engine/ecs/transform-attachment/TRANSFORM_ATTACHMENT_EVENT_CHANGE.js +29 -0
  99. package/src/engine/ecs/transform-attachment/TransformAttachmentSystem.d.ts.map +1 -1
  100. package/src/engine/ecs/transform-attachment/TransformAttachmentSystem.js +13 -21
  101. package/src/engine/graphics/ecs/sprite/SpriteSystemPE.d.ts.map +1 -1
  102. package/src/engine/graphics/ecs/sprite/SpriteSystemPE.js +4 -8
  103. package/src/engine/graphics3/LightSystem.d.ts.map +1 -1
  104. package/src/engine/graphics3/LightSystem.js +4 -8
  105. package/src/engine/graphics3/MeshSystem.d.ts.map +1 -1
  106. package/src/engine/graphics3/MeshSystem.js +9 -19
  107. package/src/engine/graphics3/ParticleEmitterSystem.d.ts.map +1 -1
  108. package/src/engine/graphics3/ParticleEmitterSystem.js +4 -8
  109. package/src/engine/graphics3/ShadedGeometrySystem.d.ts.map +1 -1
  110. package/src/engine/graphics3/ShadedGeometrySystem.js +4 -10
  111. package/src/engine/grid/transform2grid/Transform2GridPositionSystem.d.ts +1 -1
  112. package/src/engine/grid/transform2grid/Transform2GridPositionSystem.d.ts.map +1 -1
  113. package/src/engine/grid/transform2grid/Transform2GridPositionSystem.js +4 -8
  114. package/src/engine/intelligence/behavior/ecs/OrbitingBehavior.js +218 -218
  115. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  116. package/src/engine/physics/ecs/PhysicsSystem.js +2 -5
  117. package/src/engine/sound/ecs/SoundListenerSystem.d.ts.map +1 -1
  118. package/src/engine/sound/ecs/SoundListenerSystem.js +6 -8
  119. package/src/engine/sound/simulation/ecs/AcousticSimulationSystem.d.ts +0 -4
  120. package/src/engine/sound/simulation/ecs/AcousticSimulationSystem.d.ts.map +1 -1
  121. package/src/engine/sound/simulation/ecs/AcousticSimulationSystem.js +5 -11
  122. package/src/shade/playground/particle_editor/README.md +43 -2
  123. package/src/shade/playground/particle_system/particle_prototype.d.ts.map +1 -1
  124. package/src/shade/playground/particle_system/particle_prototype.js +4 -5
  125. package/src/shade/playground/particle_system/particle_scene.js +4 -4
  126. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  127. package/src/shade/renderer/Renderer.js +13 -1
  128. package/src/shade/renderer/particles/DESIGN.md +104 -19
  129. package/src/shade/renderer/particles/GPUParticleSystem.d.ts +64 -50
  130. package/src/shade/renderer/particles/GPUParticleSystem.d.ts.map +1 -1
  131. package/src/shade/renderer/particles/GPUParticleSystem.js +176 -78
  132. package/src/shade/renderer/particles/ParticleConstants.d.ts +59 -23
  133. package/src/shade/renderer/particles/ParticleConstants.d.ts.map +1 -1
  134. package/src/shade/renderer/particles/ParticleConstants.js +66 -23
  135. package/src/shade/renderer/particles/ParticleRenderMode.d.ts +19 -0
  136. package/src/shade/renderer/particles/ParticleRenderMode.d.ts.map +1 -0
  137. package/src/shade/renderer/particles/ParticleRenderMode.js +33 -0
  138. package/src/shade/renderer/particles/data/PARTICLE_EMITTER_STRUCT.d.ts.map +1 -1
  139. package/src/shade/renderer/particles/data/PARTICLE_EMITTER_STRUCT.js +167 -166
  140. package/src/shade/renderer/particles/data/particle_emitter_record.d.ts +37 -0
  141. package/src/shade/renderer/particles/data/particle_emitter_record.d.ts.map +1 -0
  142. package/src/shade/renderer/particles/data/particle_emitter_record.js +109 -0
  143. package/src/shade/renderer/particles/graph/ParticleGroupBuilder.d.ts +3 -3
  144. package/src/shade/renderer/particles/graph/ParticleGroupBuilder.js +303 -303
  145. package/src/shade/renderer/particles/graph/ParticleNodeDescription.d.ts +18 -0
  146. package/src/shade/renderer/particles/graph/ParticleNodeDescription.d.ts.map +1 -1
  147. package/src/shade/renderer/particles/graph/ParticleNodeDescription.js +152 -133
  148. package/src/shade/renderer/particles/graph/ParticleNodeRegistry.d.ts +0 -11
  149. package/src/shade/renderer/particles/graph/ParticleNodeRegistry.d.ts.map +1 -1
  150. package/src/shade/renderer/particles/graph/ParticleNodeRegistry.js +720 -621
  151. package/src/shade/renderer/particles/graph/compile_particle_graph.d.ts.map +1 -1
  152. package/src/shade/renderer/particles/graph/compile_particle_graph.js +8 -3
  153. package/src/shade/renderer/particles/graph/flatten_particle_graph.d.ts.map +1 -1
  154. package/src/shade/renderer/particles/graph/flatten_particle_graph.js +244 -213
  155. package/src/shade/renderer/particles/graph/groups/particle_builtin_groups.d.ts.map +1 -1
  156. package/src/shade/renderer/particles/graph/groups/particle_builtin_groups.js +6 -0
  157. package/src/shade/renderer/particles/graph/groups/particle_groups_geometry.d.ts.map +1 -1
  158. package/src/shade/renderer/particles/graph/groups/particle_groups_geometry.js +294 -319
  159. package/src/shade/renderer/particles/graph/groups/particle_groups_noise.d.ts.map +1 -1
  160. package/src/shade/renderer/particles/graph/groups/particle_groups_noise.js +524 -517
  161. package/src/shade/renderer/particles/graph/groups/particle_groups_psrdnoise.d.ts +22 -0
  162. package/src/shade/renderer/particles/graph/groups/particle_groups_psrdnoise.d.ts.map +1 -0
  163. package/src/shade/renderer/particles/graph/groups/particle_groups_psrdnoise.js +517 -0
  164. package/src/shade/renderer/particles/graph/groups/simplex_reference.d.ts.map +1 -1
  165. package/src/shade/renderer/particles/graph/groups/simplex_reference.js +185 -159
  166. package/src/shade/renderer/particles/graph/make_particle_group.d.ts.map +1 -1
  167. package/src/shade/renderer/particles/graph/make_particle_group.js +2 -20
  168. package/src/shade/renderer/particles/graph/particle_compare_ops.d.ts +17 -0
  169. package/src/shade/renderer/particles/graph/particle_compare_ops.d.ts.map +1 -0
  170. package/src/shade/renderer/particles/graph/particle_compare_ops.js +22 -0
  171. package/src/shade/renderer/particles/graph/particle_graph_diagnostic.d.ts +28 -8
  172. package/src/shade/renderer/particles/graph/particle_graph_diagnostic.d.ts.map +1 -1
  173. package/src/shade/renderer/particles/graph/particle_graph_diagnostic.js +49 -25
  174. package/src/shade/renderer/particles/graph/particle_node_parameters.d.ts +40 -0
  175. package/src/shade/renderer/particles/graph/particle_node_parameters.d.ts.map +1 -1
  176. package/src/shade/renderer/particles/graph/particle_node_parameters.js +312 -234
  177. package/src/shade/renderer/particles/graph/particle_node_presentation.d.ts.map +1 -1
  178. package/src/shade/renderer/particles/graph/particle_node_presentation.js +59 -3
  179. package/src/shade/renderer/particles/graph/particle_ramp.d.ts +230 -0
  180. package/src/shade/renderer/particles/graph/particle_ramp.d.ts.map +1 -0
  181. package/src/shade/renderer/particles/graph/particle_ramp.js +403 -0
  182. package/src/shade/renderer/particles/graph/validate_particle_graph.d.ts +7 -1
  183. package/src/shade/renderer/particles/graph/validate_particle_graph.d.ts.map +1 -1
  184. package/src/shade/renderer/particles/graph/validate_particle_graph.js +66 -10
  185. package/src/shade/renderer/particles/graph_particles.d.ts +23 -69
  186. package/src/shade/renderer/particles/graph_particles.d.ts.map +1 -1
  187. package/src/shade/renderer/particles/graph_particles.js +105 -262
  188. package/src/shade/renderer/particles/graph_particles_avboit.d.ts.map +1 -1
  189. package/src/shade/renderer/particles/graph_particles_avboit.js +3 -2
  190. package/src/shade/renderer/particles/graph_particles_billboard.d.ts +37 -0
  191. package/src/shade/renderer/particles/graph_particles_billboard.d.ts.map +1 -0
  192. package/src/shade/renderer/particles/graph_particles_billboard.js +93 -0
  193. package/src/shade/renderer/particles/isa/InstructionStream.d.ts +51 -22
  194. package/src/shade/renderer/particles/isa/InstructionStream.d.ts.map +1 -1
  195. package/src/shade/renderer/particles/isa/InstructionStream.js +98 -38
  196. package/src/shade/renderer/particles/isa/ParticleProgram.d.ts +49 -8
  197. package/src/shade/renderer/particles/isa/ParticleProgram.d.ts.map +1 -1
  198. package/src/shade/renderer/particles/isa/ParticleProgram.js +95 -150
  199. package/src/shade/renderer/particles/isa/ParticleVMISA.d.ts +306 -64
  200. package/src/shade/renderer/particles/isa/ParticleVMISA.d.ts.map +1 -1
  201. package/src/shade/renderer/particles/isa/ParticleVMISA.js +505 -136
  202. package/src/shade/renderer/particles/isa/particle_assembly.d.ts +36 -12
  203. package/src/shade/renderer/particles/isa/particle_assembly.d.ts.map +1 -1
  204. package/src/shade/renderer/particles/isa/particle_assembly.js +354 -139
  205. package/src/shade/renderer/particles/isa/validate_particle_program.d.ts +9 -0
  206. package/src/shade/renderer/particles/isa/validate_particle_program.d.ts.map +1 -0
  207. package/src/shade/renderer/particles/isa/validate_particle_program.js +128 -0
  208. package/src/shade/renderer/particles/layout/ParticleLayout.d.ts +23 -0
  209. package/src/shade/renderer/particles/layout/ParticleLayout.d.ts.map +1 -1
  210. package/src/shade/renderer/particles/layout/ParticleLayout.js +68 -0
  211. package/src/shade/renderer/particles/optimizer/dag/optimize_particle_program_vector.d.ts +77 -0
  212. package/src/shade/renderer/particles/optimizer/dag/optimize_particle_program_vector.d.ts.map +1 -0
  213. package/src/shade/renderer/particles/optimizer/dag/optimize_particle_program_vector.js +219 -0
  214. package/src/shade/renderer/particles/optimizer/dag/particle_vector_extract.d.ts +83 -0
  215. package/src/shade/renderer/particles/optimizer/dag/particle_vector_extract.d.ts.map +1 -0
  216. package/src/shade/renderer/particles/optimizer/dag/particle_vector_extract.js +283 -0
  217. package/src/shade/renderer/particles/optimizer/dag/particle_vector_graph.d.ts +133 -0
  218. package/src/shade/renderer/particles/optimizer/dag/particle_vector_graph.d.ts.map +1 -0
  219. package/src/shade/renderer/particles/optimizer/dag/particle_vector_graph.js +334 -0
  220. package/src/shade/renderer/particles/optimizer/dag/particle_vector_groups.d.ts +117 -0
  221. package/src/shade/renderer/particles/optimizer/dag/particle_vector_groups.d.ts.map +1 -0
  222. package/src/shade/renderer/particles/optimizer/dag/particle_vector_groups.js +70 -0
  223. package/src/shade/renderer/particles/optimizer/dag/particle_vector_lift.d.ts +97 -0
  224. package/src/shade/renderer/particles/optimizer/dag/particle_vector_lift.d.ts.map +1 -0
  225. package/src/shade/renderer/particles/optimizer/dag/particle_vector_lift.js +462 -0
  226. package/src/shade/renderer/particles/optimizer/dag/particle_vector_lower.d.ts +116 -0
  227. package/src/shade/renderer/particles/optimizer/dag/particle_vector_lower.d.ts.map +1 -0
  228. package/src/shade/renderer/particles/optimizer/dag/particle_vector_lower.js +687 -0
  229. package/src/shade/renderer/particles/optimizer/dag/particle_vector_pack.d.ts +57 -0
  230. package/src/shade/renderer/particles/optimizer/dag/particle_vector_pack.d.ts.map +1 -0
  231. package/src/shade/renderer/particles/optimizer/dag/particle_vector_pack.js +860 -0
  232. package/src/shade/renderer/particles/optimizer/dag/particle_vector_rules.d.ts +115 -0
  233. package/src/shade/renderer/particles/optimizer/dag/particle_vector_rules.d.ts.map +1 -0
  234. package/src/shade/renderer/particles/optimizer/dag/particle_vector_rules.js +264 -0
  235. package/src/shade/renderer/particles/optimizer/optimize_particle_program.d.ts +81 -42
  236. package/src/shade/renderer/particles/optimizer/optimize_particle_program.d.ts.map +1 -1
  237. package/src/shade/renderer/particles/optimizer/optimize_particle_program.js +477 -478
  238. package/src/shade/renderer/particles/optimizer/particle_integer_bounds.d.ts +100 -0
  239. package/src/shade/renderer/particles/optimizer/particle_integer_bounds.d.ts.map +1 -0
  240. package/src/shade/renderer/particles/optimizer/particle_integer_bounds.js +236 -0
  241. package/src/shade/renderer/particles/optimizer/particle_program_analysis.d.ts +139 -59
  242. package/src/shade/renderer/particles/optimizer/particle_program_analysis.d.ts.map +1 -1
  243. package/src/shade/renderer/particles/optimizer/particle_program_analysis.js +764 -647
  244. package/src/shade/renderer/particles/optimizer/particle_program_equivalence.d.ts +40 -7
  245. package/src/shade/renderer/particles/optimizer/particle_program_equivalence.d.ts.map +1 -1
  246. package/src/shade/renderer/particles/optimizer/particle_program_equivalence.js +376 -213
  247. package/src/shade/renderer/particles/optimizer/particle_program_fuzz.d.ts +30 -3
  248. package/src/shade/renderer/particles/optimizer/particle_program_fuzz.d.ts.map +1 -1
  249. package/src/shade/renderer/particles/optimizer/particle_program_fuzz.js +337 -223
  250. package/src/shade/renderer/particles/optimizer/particle_vm_semantics.d.ts +24 -38
  251. package/src/shade/renderer/particles/optimizer/particle_vm_semantics.d.ts.map +1 -1
  252. package/src/shade/renderer/particles/optimizer/particle_vm_semantics.js +166 -206
  253. package/src/shade/renderer/particles/optimizer/pass_allocate_registers.d.ts +99 -0
  254. package/src/shade/renderer/particles/optimizer/pass_allocate_registers.d.ts.map +1 -0
  255. package/src/shade/renderer/particles/optimizer/pass_allocate_registers.js +313 -0
  256. package/src/shade/renderer/particles/optimizer/pass_dedup_constants.d.ts +3 -2
  257. package/src/shade/renderer/particles/optimizer/pass_dedup_constants.d.ts.map +1 -1
  258. package/src/shade/renderer/particles/optimizer/pass_dedup_constants.js +198 -197
  259. package/src/shade/renderer/particles/optimizer/pass_eliminate_dead_code.d.ts.map +1 -1
  260. package/src/shade/renderer/particles/optimizer/pass_fold_constants.d.ts +19 -0
  261. package/src/shade/renderer/particles/optimizer/pass_fold_constants.d.ts.map +1 -1
  262. package/src/shade/renderer/particles/optimizer/pass_fold_constants.js +678 -561
  263. package/src/shade/renderer/particles/optimizer/pass_inline_constants.d.ts +22 -0
  264. package/src/shade/renderer/particles/optimizer/pass_inline_constants.d.ts.map +1 -0
  265. package/src/shade/renderer/particles/optimizer/pass_inline_constants.js +221 -0
  266. package/src/shade/renderer/particles/optimizer/pass_narrow_widths.d.ts.map +1 -1
  267. package/src/shade/renderer/particles/optimizer/pass_narrow_widths.js +63 -72
  268. package/src/shade/renderer/particles/optimizer/pass_pack_registers.d.ts.map +1 -1
  269. package/src/shade/renderer/particles/optimizer/pass_pack_registers.js +87 -86
  270. package/src/shade/renderer/particles/optimizer/pass_peephole.d.ts.map +1 -1
  271. package/src/shade/renderer/particles/optimizer/pass_peephole.js +392 -271
  272. package/src/shade/renderer/particles/optimizer/pass_propagate_copies.d.ts.map +1 -1
  273. package/src/shade/renderer/particles/optimizer/pass_propagate_copies.js +361 -268
  274. package/src/shade/renderer/particles/optimizer/pass_schedule.d.ts +4 -0
  275. package/src/shade/renderer/particles/optimizer/pass_schedule.d.ts.map +1 -0
  276. package/src/shade/renderer/particles/optimizer/pass_schedule.js +244 -0
  277. package/src/shade/renderer/particles/optimizer/pass_share_subexpressions.d.ts.map +1 -1
  278. package/src/shade/renderer/particles/optimizer/pass_share_subexpressions.js +154 -151
  279. package/src/shade/renderer/particles/particle_test_fixtures.d.ts +20 -0
  280. package/src/shade/renderer/particles/particle_test_fixtures.d.ts.map +1 -1
  281. package/src/shade/renderer/particles/particle_test_fixtures.js +229 -203
  282. package/src/shade/renderer/particles/runtime/EmitterRegistry.d.ts +99 -25
  283. package/src/shade/renderer/particles/runtime/EmitterRegistry.d.ts.map +1 -1
  284. package/src/shade/renderer/particles/runtime/EmitterRegistry.js +438 -286
  285. package/src/shade/renderer/particles/runtime/GPUParticleEmitterContext.d.ts +109 -0
  286. package/src/shade/renderer/particles/runtime/GPUParticleEmitterContext.d.ts.map +1 -0
  287. package/src/shade/renderer/particles/runtime/GPUParticleEmitterContext.js +127 -0
  288. package/src/shade/renderer/particles/runtime/ParticleEmitter.d.ts +87 -102
  289. package/src/shade/renderer/particles/runtime/ParticleEmitter.d.ts.map +1 -1
  290. package/src/shade/renderer/particles/runtime/ParticleEmitter.js +184 -163
  291. package/src/shade/renderer/particles/runtime/ProgramArena.d.ts +26 -0
  292. package/src/shade/renderer/particles/runtime/ProgramArena.d.ts.map +1 -1
  293. package/src/shade/renderer/particles/runtime/ProgramHeap.d.ts +130 -0
  294. package/src/shade/renderer/particles/runtime/ProgramHeap.d.ts.map +1 -0
  295. package/src/shade/renderer/particles/runtime/ProgramHeap.js +595 -0
  296. package/src/shade/renderer/particles/runtime/ProgramPlacement.d.ts +91 -0
  297. package/src/shade/renderer/particles/runtime/ProgramPlacement.d.ts.map +1 -0
  298. package/src/shade/renderer/particles/runtime/ProgramPlacement.js +106 -0
  299. package/src/shade/renderer/particles/runtime/create_particle_effect.d.ts +5 -2
  300. package/src/shade/renderer/particles/runtime/create_particle_effect.d.ts.map +1 -1
  301. package/src/shade/renderer/particles/runtime/create_particle_effect.js +22 -19
  302. package/src/shade/renderer/particles/shaders/chunk_particle_emit_step.d.ts +28 -0
  303. package/src/shade/renderer/particles/shaders/chunk_particle_emit_step.d.ts.map +1 -0
  304. package/src/shade/renderer/particles/shaders/chunk_particle_emit_step.js +136 -0
  305. package/src/shade/renderer/particles/shaders/chunk_particle_simulate_step.d.ts +35 -0
  306. package/src/shade/renderer/particles/shaders/chunk_particle_simulate_step.d.ts.map +1 -0
  307. package/src/shade/renderer/particles/shaders/chunk_particle_simulate_step.js +133 -0
  308. package/src/shade/renderer/particles/shaders/shader_particle_emit.d.ts +18 -9
  309. package/src/shade/renderer/particles/shaders/shader_particle_emit.d.ts.map +1 -1
  310. package/src/shade/renderer/particles/shaders/shader_particle_emit.js +20 -80
  311. package/src/shade/renderer/particles/shaders/shader_particle_emit_wide.d.ts +33 -0
  312. package/src/shade/renderer/particles/shaders/shader_particle_emit_wide.d.ts.map +1 -0
  313. package/src/shade/renderer/particles/shaders/shader_particle_emit_wide.js +114 -0
  314. package/src/shade/renderer/particles/shaders/shader_particle_simulate.d.ts +8 -0
  315. package/src/shade/renderer/particles/shaders/shader_particle_simulate.d.ts.map +1 -1
  316. package/src/shade/renderer/particles/shaders/shader_particle_simulate.js +33 -101
  317. package/src/shade/renderer/particles/shaders/shader_particle_simulate_wide.d.ts +45 -0
  318. package/src/shade/renderer/particles/shaders/shader_particle_simulate_wide.d.ts.map +1 -0
  319. package/src/shade/renderer/particles/shaders/shader_particle_simulate_wide.js +123 -0
  320. package/src/shade/renderer/particles/sort/graph_particle_sort.d.ts +51 -0
  321. package/src/shade/renderer/particles/sort/graph_particle_sort.d.ts.map +1 -0
  322. package/src/shade/renderer/particles/sort/graph_particle_sort.js +120 -0
  323. package/src/shade/renderer/particles/vm/PARTICLE_VM_CONTEXT_STRUCT.d.ts +6 -0
  324. package/src/shade/renderer/particles/vm/PARTICLE_VM_CONTEXT_STRUCT.d.ts.map +1 -0
  325. package/src/shade/renderer/particles/vm/PARTICLE_VM_CONTEXT_STRUCT.js +16 -0
  326. package/src/shade/renderer/particles/vm/ParticleVMReference.d.ts +4 -1
  327. package/src/shade/renderer/particles/vm/ParticleVMReference.d.ts.map +1 -1
  328. package/src/shade/renderer/particles/vm/ParticleVMReference.js +261 -140
  329. package/src/shade/renderer/particles/vm/chunk_particle_vm.d.ts.map +1 -1
  330. package/src/shade/renderer/particles/vm/chunk_particle_vm.js +217 -255
  331. package/src/shade/renderer/particles/vm/chunk_particle_vm_registers_fast.d.ts +43 -0
  332. package/src/shade/renderer/particles/vm/chunk_particle_vm_registers_fast.d.ts.map +1 -0
  333. package/src/shade/renderer/particles/vm/chunk_particle_vm_registers_fast.js +55 -0
  334. package/src/shade/renderer/particles/vm/chunk_particle_vm_registers_wide.d.ts +62 -0
  335. package/src/shade/renderer/particles/vm/chunk_particle_vm_registers_wide.d.ts.map +1 -0
  336. package/src/shade/renderer/particles/vm/chunk_particle_vm_registers_wide.js +86 -0
  337. package/src/shade/renderer/shader/chunk/math/chunk_mod_v3f.d.ts +22 -0
  338. package/src/shade/renderer/shader/chunk/math/chunk_mod_v3f.d.ts.map +1 -0
  339. package/src/shade/renderer/shader/chunk/math/chunk_mod_v3f.js +32 -0
  340. package/src/shade/renderer/shader/chunk/math/chunk_mod_v4f.d.ts +9 -0
  341. package/src/shade/renderer/shader/chunk/math/chunk_mod_v4f.d.ts.map +1 -0
  342. package/src/shade/renderer/shader/chunk/math/chunk_mod_v4f.js +19 -0
  343. package/src/shade/renderer/shader/chunk/noise/psrdnoise/README.md +65 -0
  344. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise2.d.ts +24 -0
  345. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise2.d.ts.map +1 -0
  346. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise2.js +83 -0
  347. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise2_hash.d.ts +25 -0
  348. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise2_hash.d.ts.map +1 -0
  349. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise2_hash.js +42 -0
  350. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise2_kernel.d.ts +31 -0
  351. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise2_kernel.d.ts.map +1 -0
  352. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise2_kernel.js +80 -0
  353. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise3.d.ts +26 -0
  354. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise3.d.ts.map +1 -0
  355. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise3.js +109 -0
  356. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise3_kernel.d.ts +38 -0
  357. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise3_kernel.d.ts.map +1 -0
  358. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise3_kernel.js +129 -0
  359. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise3_permute.d.ts +21 -0
  360. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise3_permute.d.ts.map +1 -0
  361. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise3_permute.js +35 -0
  362. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise3_skew.d.ts +19 -0
  363. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise3_skew.d.ts.map +1 -0
  364. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise3_skew.js +28 -0
  365. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise3_unskew.d.ts +13 -0
  366. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise3_unskew.d.ts.map +1 -0
  367. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_psrdnoise3_unskew.js +22 -0
  368. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_srdnoise2.d.ts +22 -0
  369. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_srdnoise2.d.ts.map +1 -0
  370. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_srdnoise2.js +59 -0
  371. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_srdnoise3.d.ts +23 -0
  372. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_srdnoise3.d.ts.map +1 -0
  373. package/src/shade/renderer/shader/chunk/noise/psrdnoise/chunk_srdnoise3.js +74 -0
  374. package/src/shade/wgsl/emulator/ComputeShaderEmulator.d.ts +34 -1
  375. package/src/shade/wgsl/emulator/ComputeShaderEmulator.d.ts.map +1 -1
  376. package/src/shade/wgsl/emulator/ComputeShaderEmulator.js +41 -1
  377. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.d.ts +3 -2
  378. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.d.ts.map +1 -1
  379. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +49 -10
  380. package/src/view/minimap/dom/MinimapCameraView.d.ts.map +1 -1
  381. package/src/view/minimap/dom/MinimapCameraView.js +4 -9
  382. package/src/core/model/node-graph/visual/layout/BoxLayoutSpec.d.ts +0 -15
  383. package/src/core/model/node-graph/visual/layout/BoxLayoutSpec.d.ts.map +0 -1
  384. package/src/core/model/node-graph/visual/layout/ConnectedBoxLayouter.d.ts +0 -45
  385. package/src/core/model/node-graph/visual/layout/ConnectedBoxLayouter.d.ts.map +0 -1
  386. package/src/core/model/node-graph/visual/layout/ConnectionEndpointLayoutSpec.d.ts +0 -24
  387. package/src/core/model/node-graph/visual/layout/ConnectionEndpointLayoutSpec.d.ts.map +0 -1
  388. package/src/core/model/node-graph/visual/layout/ConnectionLayoutSpec.d.ts +0 -19
  389. package/src/core/model/node-graph/visual/layout/ConnectionLayoutSpec.d.ts.map +0 -1
  390. package/src/engine/ecs/transform/__probe.d.ts +0 -4
  391. package/src/engine/ecs/transform/__probe.d.ts.map +0 -1
  392. package/src/engine/ecs/util/hideEntityGracefully.d.ts +0 -9
  393. package/src/engine/ecs/util/hideEntityGracefully.d.ts.map +0 -1
  394. package/src/shade/renderer/buffer/table/gpu_database_emulator_image.d.ts +0 -27
  395. package/src/shade/renderer/buffer/table/gpu_database_emulator_image.d.ts.map +0 -1
  396. package/src/shade/renderer/particles/cull/chunk_particle_cull.d.ts +0 -14
  397. package/src/shade/renderer/particles/cull/chunk_particle_cull.d.ts.map +0 -1
  398. package/src/shade/renderer/particles/data/PARTICLE_EMITTER_LAYOUT.d.ts +0 -156
  399. package/src/shade/renderer/particles/data/PARTICLE_EMITTER_LAYOUT.d.ts.map +0 -1
  400. package/src/shade/renderer/particles/runtime/Emitter.d.ts +0 -219
  401. package/src/shade/renderer/particles/runtime/Emitter.d.ts.map +0 -1
  402. package/src/shade/renderer/particles/runtime/ParticleSpawnScheduler.d.ts +0 -77
  403. package/src/shade/renderer/particles/runtime/ParticleSpawnScheduler.d.ts.map +0 -1
  404. package/src/shade/renderer/particles/runtime/ProgramArena.js +0 -155
  405. package/src/shade/renderer/particles/runtime/emitter_database_image.d.ts +0 -15
  406. package/src/shade/renderer/particles/runtime/emitter_database_image.d.ts.map +0 -1
  407. package/src/shade/renderer/particles/runtime/spawn_request_encoding.d.ts +0 -34
  408. package/src/shade/renderer/particles/runtime/spawn_request_encoding.d.ts.map +0 -1
  409. package/src/shade/renderer/particles/shaders/chunk_particle_context.d.ts +0 -32
  410. package/src/shade/renderer/particles/shaders/chunk_particle_context.d.ts.map +0 -1
@@ -1,561 +1,678 @@
1
- import { pack_swizzle, VM_OP, vm_dest_width, vm_encode_word0 } from "../isa/ParticleVMISA.js";
2
- import { decode_particle_instructions, particle_instruction_is_well_formed } from "./particle_program_analysis.js";
3
- import { vm_source_lane } from "./particle_vm_semantics.js";
4
-
5
- /** @type {string} */
6
- export const PARTICLE_PASS_FOLD_CONSTANTS = "fold_constants";
7
-
8
- /**
9
- * Track what each register lane is known to hold, and rewrite the instructions that turn out not to
10
- * need computing.
11
- *
12
- * Two things come out of this. Instructions whose every output lane is a known number become a
13
- * `LOAD_CONST`, which usually makes everything feeding them dead. And instructions that are a known
14
- * *shape* rather than a known value collapse: a `SELECT` whose condition is a compile-time mask is
15
- * per-lane copying, and a multiply by one is a copy.
16
- *
17
- * The graph compiler emits both shapes constantly. It has no notion of a compile-time-known lane
18
- * mask, so `graph/particle_graph_distributions.js` assembling `(cos, sin, 0)` out of a `select` on
19
- * the literal `[0, 1, 0]` lowers to a broadcast, a second broadcast, a constant load and a `SELECT`
20
- * — four instructions for what is one `MOV`, and this pass is what notices.
21
- *
22
- * ## What is not folded, and why
23
- *
24
- * A lane is tracked only while its value is FINITE. `NaN` and `±Inf` are dropped from the analysis
25
- * rather than propagated, which removes at a stroke every rewrite that is right for ordinary numbers
26
- * and wrong at the edges — `min(NaN, x)` is the reference's `NaN` and the GPU's anything, and a
27
- * baked `NaN` constant is at the mercy of a driver that assumes there are none.
28
- *
29
- * Only operations both executors are obliged to round identically are evaluated here — see
30
- * `foldable` in {@link ./particle_vm_semantics.js}. Folding `sin` would quietly hand the GPU the
31
- * CPU's answer.
32
- *
33
- * Every identity here is exact — `x * 1`, `x / 1` and `x - (+0)` are `x` for every value a register
34
- * can hold — with one deliberate exception, below.
35
- *
36
- * ## Multiplying by zero: the one place this is not exact
37
- *
38
- * `x * 0` is `+0` for finite non-negative `x`, `-0` for finite negative `x`, and `NaN` for `±Inf`
39
- * and `NaN`. Folding it to `+0` is therefore a fast-math relaxation rather than a rewrite, and it is
40
- * on by default (`fold_multiply_by_zero`) because a particle effect essentially never needs the
41
- * distinction and the shape it unlocks is everywhere.
42
- *
43
- * That shape is the lane mask. `graph/particle_graph_distributions.js` clears the lanes a disk has
44
- * nothing to say about by multiplying through `[1, 1, 0]`, and the lowering has no way to know the
45
- * result of that lane is a constant. With the fold it becomes one, the tail of the instruction is
46
- * peeled off as a constant load, and the multiply, the mask's own load and the broadcast feeding it
47
- * all go dead behind it.
48
- *
49
- * What it costs, precisely: a `-0` in the record becomes a `+0` — which is what the deleted `RANDOM`
50
- * distribution modes wrote, so the disk goes back to the sign it had before the samplers moved into
51
- * the graph — and a lane that was `NaN` because something upstream overflowed becomes zero. Switch
52
- * it off (`{ passes: { fold_constants: { fold_multiply_by_zero: false } } }`) and every pass in the
53
- * optimizer is bit-exact again.
54
- *
55
- * @type {import("./optimize_particle_program.js").ParticleOptimizerPass}
56
- */
57
- export function pass_fold_constants(instructions, context) {
58
- const decoded = decode_particle_instructions(instructions);
59
- const slots = context.register_slots;
60
-
61
- /** Whether a slot's value is known — and finite, which is the only kind that is tracked. */
62
- const known = new Uint8Array(slots);
63
- /** f32, so a tracked value is exactly the one the register would hold. */
64
- const value = new Float32Array(slots);
65
-
66
- const options = {
67
- max_select_moves: 2,
68
- fold_multiply_by_zero: true,
69
- peel_constant_lanes: true,
70
- ...context,
71
- };
72
-
73
- /** @type {number[][]} */
74
- const out = [];
75
-
76
- let changed = false;
77
-
78
- for (const instruction of decoded) {
79
- const lanes = evaluate_lanes(instruction, known, value, options);
80
- const replacement = rewrite(instruction, lanes, known, value, options);
81
-
82
- if (replacement === null) {
83
- out.push(instruction.words);
84
- } else {
85
- out.push(...replacement);
86
- changed = true;
87
- }
88
-
89
- publish(instruction, lanes, known, value);
90
- }
91
-
92
- return changed ? out : null;
93
- }
94
-
95
- /**
96
- * What each lane of the destination will hold, as far as it can be worked out here.
97
- *
98
- * @param {import("./particle_program_analysis.js").ParticleDecodedInstruction} instruction
99
- * @param {Uint8Array} known
100
- * @param {Float32Array} value
101
- * @param {object} options
102
- * @returns {Array<number|null>} one entry per destination lane; `null` where the value is not known
103
- */
104
- function evaluate_lanes(instruction, known, value, options) {
105
- if (instruction.write === null) {
106
- return [];
107
- }
108
-
109
- const count = vm_dest_width(instruction.op, instruction.width);
110
- /** @type {Array<number|null>} */
111
- const lanes = new Array(count).fill(null);
112
-
113
- if (instruction.op === VM_OP.LOAD_CONST) {
114
- for (let lane = 0; lane < count; lane++) {
115
- lanes[lane] = finite(options.constants[instruction.words[2] + lane]);
116
- }
117
-
118
- return lanes;
119
- }
120
-
121
- if (instruction.op === VM_OP.LOAD_ATTR) {
122
- // Lanes past the component count are the zero an over-wide load is defined to read. The
123
- // ones inside it are whatever the record holds, which is not knowable here.
124
- for (let lane = instruction.words[3]; lane < count; lane++) {
125
- lanes[lane] = 0;
126
- }
127
-
128
- return lanes;
129
- }
130
-
131
- if (!instruction.semantics.foldable) {
132
- return lanes;
133
- }
134
-
135
- for (let lane = 0; lane < count; lane++) {
136
- /** @type {Array<number|undefined>} */
137
- const inputs = [];
138
- let complete = true;
139
-
140
- for (const read of instruction.reads) {
141
- const source_lane = vm_source_lane(
142
- instruction.op,
143
- lane,
144
- instruction.meta.scalar_operands.includes(read.operand),
145
- instruction.words[3]
146
- );
147
- const slot = read.base + source_lane;
148
-
149
- if (slot >= known.length || !known[slot]) {
150
- complete = false;
151
- continue;
152
- }
153
-
154
- inputs[read.operand] = value[slot];
155
- }
156
-
157
- if (complete) {
158
- lanes[lane] = finite(evaluate(instruction.op, inputs[0], inputs[1], inputs[2]));
159
- } else if (options.fold_multiply_by_zero) {
160
- lanes[lane] = absorbed_by_zero(instruction.op, inputs);
161
- }
162
- }
163
-
164
- return lanes;
165
- }
166
-
167
- /**
168
- * Zero as an absorbing element: a known zero on either side of a multiply gives zero, whatever the
169
- * other operand turns out to be.
170
- *
171
- * This is the one thing here that is not true of every value a register can hold — `±Inf * 0` and
172
- * `NaN * 0` are `NaN`, and a negative operand gives `-0` — and it is what the module comment above
173
- * argues the trade for. A known `-0` absorbs as readily as a known `+0`: once the sign of the result
174
- * is being given up, it is being given up either way.
175
- *
176
- * @param {number} op
177
- * @param {Array<number|undefined>} inputs by operand slot, `undefined` where the value is unknown
178
- * @returns {number|null}
179
- */
180
- function absorbed_by_zero(op, inputs) {
181
- if (op !== VM_OP.MUL && op !== VM_OP.SCALE) {
182
- return null;
183
- }
184
-
185
- return inputs[0] === 0 || inputs[1] === 0 ? 0 : null;
186
- }
187
-
188
- /**
189
- * One lane of an operation, in the arithmetic the reference executes it in: `f32` operands, an
190
- * `f64` computation, and the result rounded back to `f32` — which for a single correctly-rounded
191
- * operation is the same number an `f32` machine produces.
192
- *
193
- * @param {number} op
194
- * @param {number} a
195
- * @param {number} b
196
- * @param {number} c
197
- * @returns {number}
198
- */
199
- function evaluate(op, a, b, c) {
200
- switch (op) {
201
- case VM_OP.MOV: return a;
202
- case VM_OP.SWIZZLE: return a;
203
- case VM_OP.ADD: return Math.fround(a + b);
204
- case VM_OP.SUB: return Math.fround(a - b);
205
- case VM_OP.MUL: return Math.fround(a * b);
206
- case VM_OP.DIV: return Math.fround(a / b);
207
- case VM_OP.SCALE: return Math.fround(a * b);
208
- // `Math.min(-0, 0)` is `-0` and WGSL's `min`, spelled `select(e1, e2, e2 < e1)`, is `0` —
209
- // the one place these two executors are allowed to disagree bit for bit. Refusing the fold
210
- // wherever a zero is involved closes it without pretending the rest of `min` is unreliable.
211
- case VM_OP.MIN: return a === 0 || b === 0 ? NaN : Math.min(a, b);
212
- case VM_OP.MAX: return a === 0 || b === 0 ? NaN : Math.max(a, b);
213
- case VM_OP.CLAMP: return a === 0 || b === 0 || c === 0 ? NaN : Math.min(Math.max(a, b), c);
214
- case VM_OP.NEG: return -a;
215
- case VM_OP.ABS: return Math.abs(a);
216
- case VM_OP.FLOOR: return Math.floor(a);
217
- case VM_OP.FRACT: return Math.fround(a - Math.floor(a));
218
- case VM_OP.SQRT: return Math.fround(Math.sqrt(a));
219
- case VM_OP.STEP: return b < a ? 0 : 1;
220
- case VM_OP.CMP_LT: return a < b ? 1 : 0;
221
- case VM_OP.CMP_LE: return a <= b ? 1 : 0;
222
- case VM_OP.CMP_GT: return a > b ? 1 : 0;
223
- case VM_OP.CMP_GE: return a >= b ? 1 : 0;
224
- case VM_OP.CMP_EQ: return a === b ? 1 : 0;
225
- case VM_OP.SELECT: return c > 0.5 ? b : a;
226
- default: return NaN;
227
- }
228
- }
229
-
230
- /**
231
- * Only finite values are tracked. Dropping `NaN` and `±Inf` here is what keeps every rewrite below
232
- * from having to restate its own edge cases.
233
- *
234
- * @param {number} x
235
- * @returns {number|null}
236
- */
237
- function finite(x) {
238
- return Number.isFinite(x) ? Math.fround(x) : null;
239
- }
240
-
241
- /**
242
- * @param {import("./particle_program_analysis.js").ParticleDecodedInstruction} instruction
243
- * @param {Array<number|null>} lanes
244
- * @param {Uint8Array} known
245
- * @param {Float32Array} value
246
- */
247
- function publish(instruction, lanes, known, value) {
248
- if (instruction.write === null) {
249
- return;
250
- }
251
-
252
- for (let lane = 0; lane < lanes.length; lane++) {
253
- const slot = instruction.write.base + lane;
254
-
255
- if (slot >= known.length) {
256
- continue;
257
- }
258
-
259
- known[slot] = lanes[lane] === null ? 0 : 1;
260
- value[slot] = lanes[lane] ?? 0;
261
- }
262
- }
263
-
264
- /**
265
- * @param {import("./particle_program_analysis.js").ParticleDecodedInstruction} instruction
266
- * @param {Array<number|null>} lanes
267
- * @param {Uint8Array} known
268
- * @param {Float32Array} value
269
- * @param {object} context
270
- * @param {number} max_select_moves
271
- * @returns {number[][]|null} what to emit instead, or `null` to keep the instruction
272
- */
273
- function rewrite(instruction, lanes, known, value, options) {
274
- if (instruction.semantics.side_effects) {
275
- return null;
276
- }
277
-
278
- const materialised = materialise(instruction, lanes, options);
279
-
280
- if (materialised !== null) {
281
- return materialised;
282
- }
283
-
284
- if (instruction.op === VM_OP.SELECT) {
285
- const collapsed = collapse_select(instruction, known, value, options.max_select_moves);
286
-
287
- if (collapsed !== null) {
288
- return collapsed;
289
- }
290
- }
291
-
292
- const identity = collapse_identity(instruction, known, value);
293
-
294
- if (identity !== null) {
295
- return identity;
296
- }
297
-
298
- return options.peel_constant_lanes ? peel_constant_tail(instruction, lanes, options) : null;
299
- }
300
-
301
- /**
302
- * Split an instruction whose trailing lanes turn out to be constants into the narrower instruction
303
- * that computes the rest, plus one load for the tail.
304
- *
305
- * One instruction becomes two, which only pays for itself through what it frees: the narrowed
306
- * instruction stops reading the upper lanes of its sources, and those are usually a broadcast, a
307
- * mask, and the arithmetic that built them. A distribution's mask lane is exactly this shape.
308
- *
309
- * Only for opcodes that compute their result from registers. `LOAD_ATTR`'s lanes past its component
310
- * count are already the zero it reads for free, and peeling those would be a load in place of
311
- * nothing at all.
312
- *
313
- * @param {import("./particle_program_analysis.js").ParticleDecodedInstruction} instruction
314
- * @param {Array<number|null>} lanes
315
- * @param {object} options
316
- * @returns {number[][]|null}
317
- */
318
- function peel_constant_tail(instruction, lanes, options) {
319
- if (
320
- options.intern_constant === null
321
- || instruction.write === null
322
- || instruction.reads.length === 0
323
- || !instruction.semantics.narrowable
324
- || instruction.write.span !== instruction.operation_width
325
- ) {
326
- return null;
327
- }
328
-
329
- // The longest run of known lanes at the top, leaving at least one below it for the instruction
330
- // to still be worth issuing — one that is constant all the way down was materialised above.
331
- let first = lanes.length;
332
-
333
- while (first > 0 && lanes[first - 1] !== null) {
334
- first--;
335
- }
336
-
337
- if (first === lanes.length || first < Math.max(1, instruction.meta.min_width)) {
338
- return null;
339
- }
340
-
341
- const narrowed = instruction.words.slice();
342
-
343
- narrowed[0] = vm_encode_word0(instruction.op, first);
344
-
345
- const load = [
346
- vm_encode_word0(VM_OP.LOAD_CONST, lanes.length - first),
347
- instruction.write.base + first,
348
- options.intern_constant(lanes.slice(first)),
349
- 0,
350
- 0,
351
- ];
352
-
353
- return particle_instruction_is_well_formed(narrowed) && particle_instruction_is_well_formed(load)
354
- ? [narrowed, load]
355
- : null;
356
- }
357
-
358
- /**
359
- * Replace an instruction whose every output lane is a known number with the load of that number.
360
- *
361
- * Neutral in instruction count on its own; the win is that it cuts the instruction loose from
362
- * everything that fed it, which is usually then dead.
363
- *
364
- * @returns {number[][]|null}
365
- */
366
- function materialise(instruction, lanes, options) {
367
- if (
368
- instruction.op === VM_OP.LOAD_CONST
369
- || options.intern_constant === null
370
- || lanes.length === 0
371
- || lanes.some(lane => lane === null)
372
- ) {
373
- return null;
374
- }
375
-
376
- // A reduction writes one lane whatever it reads, so the load that replaces it is one lane wide.
377
- const width = lanes.length;
378
- const words = [
379
- vm_encode_word0(VM_OP.LOAD_CONST, width),
380
- instruction.write.base,
381
- options.intern_constant(lanes),
382
- 0,
383
- 0,
384
- ];
385
-
386
- return particle_instruction_is_well_formed(words) ? [words] : null;
387
- }
388
-
389
- /**
390
- * A `SELECT` whose condition is known lane by lane is not a choice, it is copying — and most of
391
- * those copies are a register into itself.
392
- *
393
- * The replacement is capped at `max_select_moves` instructions: what pays for the split is the
394
- * constant load feeding the condition going dead, so a mask that alternates lane by lane and needs
395
- * a `MOV` for each run is not worth taking apart.
396
- *
397
- * The runs are safe to emit in sequence for the same reason the `SELECT` was legal: an operand
398
- * either names the destination's slots exactly — in which case its lanes are copies of themselves
399
- * and are dropped — or names none of them, so no `MOV` here can clobber a later one's source.
400
- *
401
- * @returns {number[][]|null}
402
- */
403
- function collapse_select(instruction, known, value, max_select_moves) {
404
- const condition = instruction.reads[2];
405
- const width = instruction.operation_width;
406
- /** @type {number[]} the source operand's base, per lane */
407
- const chosen = new Array(width);
408
-
409
- for (let lane = 0; lane < width; lane++) {
410
- const slot = condition.base + lane;
411
-
412
- if (slot >= known.length || !known[slot]) {
413
- return null;
414
- }
415
-
416
- chosen[lane] = value[slot] > 0.5 ? instruction.reads[1].base : instruction.reads[0].base;
417
- }
418
-
419
- /** @type {number[][]} */
420
- const moves = [];
421
-
422
- for (let lane = 0; lane < width;) {
423
- let end = lane + 1;
424
-
425
- while (end < width && chosen[end] === chosen[lane]) {
426
- end++;
427
- }
428
-
429
- // A run taken from the destination itself is a copy of a register into itself.
430
- if (chosen[lane] !== instruction.write.base) {
431
- const words = [
432
- vm_encode_word0(VM_OP.MOV, end - lane),
433
- instruction.write.base + lane,
434
- chosen[lane] + lane,
435
- 0,
436
- 0,
437
- ];
438
-
439
- if (!particle_instruction_is_well_formed(words)) {
440
- return null;
441
- }
442
-
443
- moves.push(words);
444
- }
445
-
446
- lane = end;
447
- }
448
-
449
- return moves.length <= max_select_moves ? moves : null;
450
- }
451
-
452
- /**
453
- * The arithmetic identities that hold for every value a register can hold, not merely the ordinary
454
- * ones.
455
- *
456
- * `x * 1`, `x / 1` and `x - (+0)` are `x` bit for bit — including `-0`, `±Inf` and `NaN` — so they
457
- * are copies. `x + 0` is not: `-0 + 0` is `+0`. `x * 0` is not: it is `-0` for negative `x` and
458
- * `NaN` for a non-finite one. The line is drawn at what is exactly true, because the record a
459
- * program leaves is compared to the bit.
460
- *
461
- * `MAD` with a multiplier of one becomes an `ADD` for the same reason: `a * 1` is exact, so a
462
- * contracted `fma(a, 1, c)` and a plain `a + c` are the same number.
463
- *
464
- * @returns {number[][]|null}
465
- */
466
- function collapse_identity(instruction, known, value) {
467
- const op = instruction.op;
468
- const width = instruction.operation_width;
469
- const is = (read, expected) => read !== undefined && all_lanes_are(read, width, expected, known, value);
470
-
471
- /** Rebuild as a copy of one source. */
472
- const copy_of = (read) => {
473
- const words = [vm_encode_word0(VM_OP.MOV, width), instruction.write.base, read.base, 0, 0];
474
-
475
- return particle_instruction_is_well_formed(words) ? [words] : null;
476
- };
477
-
478
- switch (op) {
479
- case VM_OP.MUL:
480
- if (is(instruction.reads[1], 1)) return copy_of(instruction.reads[0]);
481
- if (is(instruction.reads[0], 1)) return copy_of(instruction.reads[1]);
482
- return null;
483
-
484
- case VM_OP.DIV:
485
- return is(instruction.reads[1], 1) ? copy_of(instruction.reads[0]) : null;
486
-
487
- case VM_OP.SCALE: {
488
- // The scalar is one slot whatever the width, which `is_scalar_one` reads through the
489
- // ISA's own scalar marking rather than assuming.
490
- if (is_scalar_one(instruction, known, value)) {
491
- return copy_of(instruction.reads[0]);
492
- }
493
-
494
- // The other way round: scaling a vector of ones is that scalar in every lane, which is a
495
- // broadcast — and `SWIZZLE` is the opcode the VM has for one. Exact, and it is what the
496
- // mask left behind by a peeled instruction turns into on its way out.
497
- if (is(instruction.reads[0], 1)) {
498
- const words = [
499
- vm_encode_word0(VM_OP.SWIZZLE, instruction.width),
500
- instruction.write.base,
501
- instruction.reads[1].base,
502
- pack_swizzle(0, 0, 0, 0),
503
- 0,
504
- ];
505
-
506
- return particle_instruction_is_well_formed(words) ? [words] : null;
507
- }
508
-
509
- return null;
510
- }
511
-
512
- case VM_OP.SUB:
513
- // Positive zero only, which is what `Object.is` in `all_lanes_are` gets us: `x - (-0)`
514
- // is `x + 0`, and that turns a `-0` into a `+0`.
515
- return is(instruction.reads[1], 0) ? copy_of(instruction.reads[0]) : null;
516
-
517
- case VM_OP.MAD: {
518
- const add = (a, b) => {
519
- const words = [vm_encode_word0(VM_OP.ADD, width), instruction.write.base, a.base, b.base, 0];
520
-
521
- return particle_instruction_is_well_formed(words) ? [words] : null;
522
- };
523
-
524
- if (is(instruction.reads[1], 1)) return add(instruction.reads[0], instruction.reads[2]);
525
- if (is(instruction.reads[0], 1)) return add(instruction.reads[1], instruction.reads[2]);
526
- return null;
527
- }
528
-
529
- default:
530
- return null;
531
- }
532
- }
533
-
534
- /**
535
- * @param {import("./particle_program_analysis.js").ParticleOperandRun} read
536
- * @param {number} width
537
- * @param {number} expected
538
- * @param {Uint8Array} known
539
- * @param {Float32Array} value
540
- * @returns {boolean}
541
- */
542
- function all_lanes_are(read, width, expected, known, value) {
543
- for (let lane = 0; lane < width; lane++) {
544
- const slot = read.base + lane;
545
-
546
- // `Object.is`, so a `-0` never passes for the `+0` the identity actually needs.
547
- if (slot >= known.length || !known[slot] || !Object.is(value[slot], expected)) {
548
- return false;
549
- }
550
- }
551
-
552
- return true;
553
- }
554
-
555
- /** @returns {boolean} whether `SCALE`'s scalar source is known to be exactly one */
556
- function is_scalar_one(instruction, known, value) {
557
- const scalar = instruction.reads[1];
558
- const slot = scalar.base;
559
-
560
- return slot < known.length && known[slot] === 1 && Object.is(value[slot], 1);
561
- }
1
+ import {
2
+ VM_KIND,
3
+ VM_OP,
4
+ swizzle_selector,
5
+ vm_dest_width,
6
+ vm_f32_value,
7
+ vm_pack_instruction,
8
+ vm_register,
9
+ vm_with_width,
10
+ } from "../isa/ParticleVMISA.js";
11
+ import {
12
+ decode_particle_instructions,
13
+ particle_instruction_is_well_formed,
14
+ particle_read_slots,
15
+ } from "./particle_program_analysis.js";
16
+
17
+ /** @type {string} */
18
+ export const PARTICLE_PASS_FOLD_CONSTANTS = "fold_constants";
19
+
20
+ /**
21
+ * The opcodes whose result a known zero operand decides on its own — the ones
22
+ * `fold_multiply_by_zero` rewrites, and therefore the ones whose results the equivalence harness's
23
+ * reach mode has to follow. Stated once so the two cannot drift.
24
+ * @type {Set<number>}
25
+ */
26
+ export const PARTICLE_ZERO_ABSORBING_OPS = new Set([VM_OP.MUL]);
27
+
28
+ /**
29
+ * Track what each register lane is known to hold, and rewrite the instructions that turn out not to
30
+ * need computing.
31
+ *
32
+ * Two things come out of this. Instructions whose every output lane is a known number become a
33
+ * `LOAD_CONST`, which usually makes everything feeding them dead. And instructions that are a known
34
+ * *shape* rather than a known value collapse: a `SELECT` whose condition is a compile-time mask is
35
+ * per-lane copying, and a multiply by one is a copy.
36
+ *
37
+ * The graph compiler emits both shapes constantly. It has no notion of a compile-time-known lane
38
+ * mask, so `graph/particle_graph_distributions.js` assembling `(cos, sin, 0)` out of a `select` on
39
+ * the literal `[0, 1, 0]` lowers to a broadcast, a second broadcast, a constant load and a `SELECT`
40
+ * — four instructions for what is one `MOV`, and this pass is what notices.
41
+ *
42
+ * ## What is not folded, and why
43
+ *
44
+ * A lane is tracked only while its value is FINITE. `NaN` and `±Inf` are dropped from the analysis
45
+ * rather than propagated, which removes at a stroke every rewrite that is right for ordinary numbers
46
+ * and wrong at the edges — `min(NaN, x)` is the reference's `NaN` and the GPU's anything, and a
47
+ * baked `NaN` constant is at the mercy of a driver that assumes there are none.
48
+ *
49
+ * Only operations both executors are obliged to round identically are evaluated here — see
50
+ * `foldable` in {@link ./particle_vm_semantics.js}. Folding `sin` would quietly hand the GPU the
51
+ * CPU's answer, so it happens only when a caller asks for it by name: see `fold_inexact` below.
52
+ *
53
+ * Every identity here is exact — `x * 1`, `x / 1` and `x - (+0)` are `x` for every value a register
54
+ * can hold — with one deliberate exception, below.
55
+ *
56
+ * ## Multiplying by zero: the one place this is not exact
57
+ *
58
+ * `x * 0` is `+0` for finite non-negative `x`, `-0` for finite negative `x`, and `NaN` for `±Inf`
59
+ * and `NaN`. Folding it to `+0` is therefore a fast-math relaxation rather than a rewrite, and it is
60
+ * on by default (`fold_multiply_by_zero`) because a particle effect essentially never needs the
61
+ * distinction and the shape it unlocks is everywhere.
62
+ *
63
+ * That shape is the lane mask. `graph/particle_graph_distributions.js` clears the lanes a disk has
64
+ * nothing to say about by multiplying through `[1, 1, 0]`, and the lowering has no way to know the
65
+ * result of that lane is a constant. With the fold it becomes one, the tail of the instruction is
66
+ * peeled off as a constant load, and the multiply, the mask's own load and the broadcast feeding it
67
+ * all go dead behind it.
68
+ *
69
+ * What it costs is not bounded by a shape — a substituted zero is read by whatever comes next, and
70
+ * a comparison or a division downstream can turn the difference into any number. The equivalence
71
+ * harness bounds its REACH instead: see `reached_by_zero_fold` in
72
+ * {@link ./particle_program_equivalence.js}. Switch the fold off
73
+ * (`{ fold_multiply_by_zero: false }`) and every pass in the
74
+ * optimizer is bit-exact again.
75
+ *
76
+ * ## `fold_inexact`: the transcendentals, off by default
77
+ *
78
+ * `{ fold_inexact: true }` also evaluates the operations whose two
79
+ * executors are allowed to round apart — `foldable_inexact` in
80
+ * {@link ./particle_vm_semantics.js}: the trigonometry and its inverses, `exp`, `log`, `pow`,
81
+ * `smoothstep`, `mix`, `mad` and `rsqrt`. That is HLSL's fast-math trade and it is worth naming
82
+ * rather than assuming: what is bought is a literal instead of a transcendental, and what is sold
83
+ * is the GPU's own last bits — `Math.sin` and a GPU's `sin` agree to about a part in 10^7, and
84
+ * with the fold on it is the CPU's answer that ships.
85
+ *
86
+ * It is off because that is not a trade the compiler gets to make on an author's behalf, and
87
+ * because there is nothing here to buy with it. The fold only fires where an argument is already a
88
+ * compile-time constant, and over all eighteen built-in groups — `curl4`, the simplex noises, the
89
+ * quaternion and sampling groups, 6,523 instructions optimized — turning it on changes not one
90
+ * instruction: every `sin` in the tree takes its argument from a `random`, an attribute or a
91
+ * builtin. On the fuzzer's programs, where the opcode mix is uniform and operands are often
92
+ * literals, it takes another 8.8% off what the default pipeline leaves (5,443 to 4,963 over 200
93
+ * programs). So the option is real and tested and worth having for a frontend that folds harder
94
+ * than this one — and on today's effects it is a switch with nothing behind it.
95
+ *
96
+ * What it does NOT do is turn a hardware `NaN` into a number. Only finite values are tracked, so
97
+ * `log` of zero, `asin` of two and `rsqrt` of a negative are all left alone by the same rule that
98
+ * leaves every other edge alone; the two operations whose reference spelling is defined where the
99
+ * hardware's is not — `pow` of a non-positive base and `smoothstep` with equal edges, both of which
100
+ * WGSL computes through a division or a `log2` that the reference does not — are refused in
101
+ * {@link evaluate} rather than relaxed. The relaxation is a rounding, and stays one.
102
+ *
103
+ * ## Operands
104
+ *
105
+ * A literal operand is a known value by construction, and a register operand's lanes are looked up
106
+ * through its swizzle — so a broadcast of a known register is known in every lane, and a `SELECT`
107
+ * between a register and a literal collapses to a `MOV` from whichever the mask picks, with the
108
+ * operand's kind and swizzle carried along.
109
+ *
110
+ * @type {import("./optimize_particle_program.js").ParticleOptimizerPass}
111
+ */
112
+ export function pass_fold_constants(instructions, context) {
113
+ const decoded = decode_particle_instructions(instructions);
114
+ const slots = context.register_slots;
115
+
116
+ /** Whether a slot's value is known — and finite, which is the only kind that is tracked. */
117
+ const known = new Uint8Array(slots);
118
+ /** f32, so a tracked value is exactly the one the register would hold. */
119
+ const value = new Float32Array(slots);
120
+
121
+ const options = {
122
+ max_select_moves: 2,
123
+ fold_multiply_by_zero: true,
124
+ fold_inexact: false,
125
+ peel_constant_lanes: true,
126
+ ...context,
127
+ };
128
+
129
+ /** @type {number[][]} */
130
+ const out = [];
131
+
132
+ let changed = false;
133
+
134
+ for (const instruction of decoded) {
135
+ const lanes = evaluate_lanes(instruction, known, value, options);
136
+ const replacement = rewrite(instruction, lanes, known, value, options);
137
+
138
+ if (replacement === null) {
139
+ out.push(instruction.words);
140
+ } else {
141
+ out.push(...replacement);
142
+ changed = true;
143
+ }
144
+
145
+ publish(instruction, lanes, known, value);
146
+ }
147
+
148
+ return changed ? out : null;
149
+ }
150
+
151
+ /**
152
+ * What each lane of the destination will hold, as far as it can be worked out here.
153
+ *
154
+ * @param {import("./particle_program_analysis.js").ParticleDecodedInstruction} instruction
155
+ * @param {Uint8Array} known
156
+ * @param {Float32Array} value
157
+ * @param {object} options
158
+ * @returns {Array<number|null>} one entry per destination lane; `null` where the value is not known
159
+ */
160
+ function evaluate_lanes(instruction, known, value, options) {
161
+ if (instruction.write === null) {
162
+ return [];
163
+ }
164
+
165
+ const count = vm_dest_width(instruction.op, instruction.width);
166
+ /** @type {Array<number|null>} */
167
+ const lanes = new Array(count).fill(null);
168
+
169
+ if (instruction.op === VM_OP.LOAD_CONST) {
170
+ for (let lane = 0; lane < count; lane++) {
171
+ lanes[lane] = finite(options.constants[instruction.words[1] + lane]);
172
+ }
173
+
174
+ return lanes;
175
+ }
176
+
177
+ if (instruction.op === VM_OP.LOAD_ATTR) {
178
+ // Lanes past the component count are the zero an over-wide load is defined to read. The
179
+ // ones inside it are whatever the record holds, which is not knowable here.
180
+ for (let lane = instruction.words[2]; lane < count; lane++) {
181
+ lanes[lane] = 0;
182
+ }
183
+
184
+ return lanes;
185
+ }
186
+
187
+ // Exact always; the ones the two executors may round apart only when the caller has asked for
188
+ // that trade by name.
189
+ const foldable = instruction.semantics.foldable
190
+ || (options.fold_inexact && instruction.semantics.foldable_inexact);
191
+
192
+ if (!foldable) {
193
+ return lanes;
194
+ }
195
+
196
+ for (let lane = 0; lane < count; lane++) {
197
+ /** @type {Array<number|undefined>} */
198
+ const inputs = [];
199
+ let complete = true;
200
+
201
+ for (const source of instruction.sources) {
202
+ const held = source_lane_value(source, lane, known, value);
203
+
204
+ if (held === null) {
205
+ complete = false;
206
+ continue;
207
+ }
208
+
209
+ inputs[source.operand] = held;
210
+ }
211
+
212
+ if (complete) {
213
+ lanes[lane] = finite(evaluate_particle_operation(instruction.op, inputs[0], inputs[1], inputs[2]));
214
+ } else if (options.fold_multiply_by_zero) {
215
+ lanes[lane] = absorbed_by_zero(instruction.op, inputs);
216
+ }
217
+ }
218
+
219
+ return lanes;
220
+ }
221
+
222
+ /**
223
+ * Zero as an absorbing element: a known zero on either side of a multiply gives zero, whatever the
224
+ * other operand turns out to be.
225
+ *
226
+ * This is the one thing here that is not true of every value a register can hold — `±Inf * 0` and
227
+ * `NaN * 0` are `NaN`, and a negative operand gives `-0` — and it is what the module comment above
228
+ * argues the trade for. A known `-0` absorbs as readily as a known `+0`: once the sign of the result
229
+ * is being given up, it is being given up either way.
230
+ *
231
+ * @param {number} op
232
+ * @param {Array<number|undefined>} inputs by operand slot, `undefined` where the value is unknown
233
+ * @returns {number|null}
234
+ */
235
+ function absorbed_by_zero(op, inputs) {
236
+ if (!PARTICLE_ZERO_ABSORBING_OPS.has(op)) {
237
+ return null;
238
+ }
239
+
240
+ return inputs[0] === 0 || inputs[1] === 0 ? 0 : null;
241
+ }
242
+
243
+ /**
244
+ * One lane of an operation, in the arithmetic the reference executes it in: `f32` operands, an
245
+ * `f64` computation, and the result rounded back to `f32` — which for a single correctly-rounded
246
+ * operation is the same number an `f32` machine produces.
247
+ *
248
+ * @param {number} op
249
+ * @param {number} a
250
+ * @param {number} b
251
+ * @param {number} c
252
+ * @returns {number}
253
+ */
254
+ export function evaluate_particle_operation(op, a, b, c) {
255
+ switch (op) {
256
+ case VM_OP.MOV: return a;
257
+ case VM_OP.ADD: return Math.fround(a + b);
258
+ case VM_OP.SUB: return Math.fround(a - b);
259
+ case VM_OP.MUL: return Math.fround(a * b);
260
+ case VM_OP.DIV: return Math.fround(a / b);
261
+ // `Math.min(-0, 0)` is `-0` and WGSL's `min`, spelled `select(e1, e2, e2 < e1)`, is `0` —
262
+ // the one place these two executors are allowed to disagree bit for bit. Refusing the fold
263
+ // wherever a zero is involved closes it without pretending the rest of `min` is unreliable.
264
+ case VM_OP.MIN: return a === 0 || b === 0 ? NaN : Math.min(a, b);
265
+ case VM_OP.MAX: return a === 0 || b === 0 ? NaN : Math.max(a, b);
266
+ case VM_OP.CLAMP: return a === 0 || b === 0 || c === 0 ? NaN : Math.min(Math.max(a, b), c);
267
+ case VM_OP.NEG: return -a;
268
+ case VM_OP.ABS: return Math.abs(a);
269
+ case VM_OP.FLOOR: return Math.floor(a);
270
+ case VM_OP.FRACT: return Math.fround(a - Math.floor(a));
271
+ case VM_OP.SQRT: return Math.fround(Math.sqrt(a));
272
+ case VM_OP.STEP: return b < a ? 0 : 1;
273
+ case VM_OP.CMP_LT: return a < b ? 1 : 0;
274
+ case VM_OP.CMP_LE: return a <= b ? 1 : 0;
275
+ case VM_OP.CMP_GT: return a > b ? 1 : 0;
276
+ case VM_OP.CMP_GE: return a >= b ? 1 : 0;
277
+ case VM_OP.CMP_EQ: return a === b ? 1 : 0;
278
+ case VM_OP.SELECT: return c > 0.5 ? b : a;
279
+
280
+ // --- reached only under `fold_inexact` ---
281
+ //
282
+ // Each of these is spelled exactly as `../vm/ParticleVMReference.js` spells it, so the fold
283
+ // and the reference agree to the bit and it is the GPU that is allowed to differ. That is
284
+ // the whole of what the option trades; anything an executor could disagree about by more
285
+ // than a rounding is refused below rather than folded.
286
+ case VM_OP.MAD: return Math.fround(a * b + c);
287
+ case VM_OP.MIX: return Math.fround(a + (b - a) * c);
288
+ case VM_OP.SIN: return Math.fround(Math.sin(a));
289
+ case VM_OP.COS: return Math.fround(Math.cos(a));
290
+ case VM_OP.TAN: return Math.fround(Math.tan(a));
291
+ case VM_OP.ASIN: return Math.fround(Math.asin(a));
292
+ case VM_OP.ACOS: return Math.fround(Math.acos(a));
293
+ case VM_OP.EXP: return Math.fround(Math.exp(a));
294
+ case VM_OP.LOG: return Math.fround(Math.log(a));
295
+ // `1 / sqrt(a)` for `a <= 0` is an infinity or a `NaN`, and `finite` drops both — the same
296
+ // rule that keeps `SQRT` next door out of trouble, so there is nothing to say here.
297
+ case VM_OP.RSQRT: return Math.fround(1 / Math.sqrt(a));
298
+ // WGSL's `pow` is `exp2(e2 * log2(e1))`, which is a `NaN` for every base at or below zero
299
+ // — including `pow(0, 0)`, where JS says 1. A fold that turned the hardware's `NaN` into a
300
+ // number would not be a rounding, so the non-positive bases are not folded at all.
301
+ case VM_OP.POW: return a > 0 ? Math.fround(Math.pow(a, b)) : NaN;
302
+ // Equal edges divide by zero on the GPU; the reference answers 0 or 1 instead. Same
303
+ // argument as `pow`: refuse the case rather than ship the reference's answer for it.
304
+ case VM_OP.SMOOTHSTEP: {
305
+ if (a === b) {
306
+ return NaN;
307
+ }
308
+
309
+ const t = Math.min(Math.max((c - a) / (b - a), 0), 1);
310
+
311
+ return Math.fround(t * t * (3 - 2 * t));
312
+ }
313
+ // `atan2(0, 0)` is 0 in JS and left to the implementation in WGSL. Every other pair is an
314
+ // angle both agree on to a rounding.
315
+ case VM_OP.ATAN2: return a === 0 && b === 0 ? NaN : Math.fround(Math.atan2(a, b));
316
+
317
+ default: return NaN;
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Only finite values are tracked. Dropping `NaN` and `±Inf` here is what keeps every rewrite below
323
+ * from having to restate its own edge cases.
324
+ *
325
+ * @param {number} x
326
+ * @returns {number|null}
327
+ */
328
+ function finite(x) {
329
+ return Number.isFinite(x) ? Math.fround(x) : null;
330
+ }
331
+
332
+ /**
333
+ * What one lane of a source is known to hold: a literal is its own value, a register operand is
334
+ * whatever the analysis has tracked into the slot its swizzle names for that lane. `null` where the
335
+ * value is not known — or not finite, since only finite values are ever tracked.
336
+ *
337
+ * @param {import("./particle_program_analysis.js").ParticleOperandRun} source
338
+ * @param {number} lane output lane
339
+ * @param {Uint8Array} known
340
+ * @param {Float32Array} value
341
+ * @returns {number|null}
342
+ */
343
+ function source_lane_value(source, lane, known, value) {
344
+ if (source.kind === VM_KIND.IMM) {
345
+ return finite(vm_f32_value(source.word));
346
+ }
347
+
348
+ // A scalar source feeds every lane from its first; everything else reads straight across.
349
+ const slot = source.base + swizzle_selector(source.pattern, source.lanes === 1 ? 0 : lane);
350
+
351
+ return slot < known.length && known[slot] ? value[slot] : null;
352
+ }
353
+
354
+ /**
355
+ * A `MOV` of `width` lanes into `dst` from a source operand, from `first` lanes into it, keeping the
356
+ * source's kind: a copy of a register is that register read through the selectors those lanes use,
357
+ * a copy of a literal is that literal.
358
+ *
359
+ * @param {number} width
360
+ * @param {number} dst
361
+ * @param {import("./particle_program_analysis.js").ParticleOperandRun} source
362
+ * @param {number} [first]
363
+ * @returns {number[]}
364
+ */
365
+ function move_from(width, dst, source, first = 0) {
366
+ if (source.kind === VM_KIND.IMM) {
367
+ return vm_pack_instruction(VM_OP.MOV, width, dst, [VM_KIND.IMM, VM_KIND.NONE, VM_KIND.NONE], [source.word, 0, 0]);
368
+ }
369
+
370
+ let pattern = 0;
371
+
372
+ for (let lane = 0; lane < 4; lane++) {
373
+ pattern |= swizzle_selector(source.pattern, first + lane) << (2 * lane);
374
+ }
375
+
376
+ return vm_pack_instruction(
377
+ VM_OP.MOV, width, dst,
378
+ [VM_KIND.REG, VM_KIND.NONE, VM_KIND.NONE],
379
+ [vm_register(source.base, pattern), 0, 0]
380
+ );
381
+ }
382
+
383
+ /**
384
+ * @param {import("./particle_program_analysis.js").ParticleDecodedInstruction} instruction
385
+ * @param {Array<number|null>} lanes
386
+ * @param {Uint8Array} known
387
+ * @param {Float32Array} value
388
+ */
389
+ function publish(instruction, lanes, known, value) {
390
+ if (instruction.write === null) {
391
+ return;
392
+ }
393
+
394
+ for (let lane = 0; lane < lanes.length; lane++) {
395
+ const slot = instruction.write.base + lane;
396
+
397
+ if (slot >= known.length) {
398
+ continue;
399
+ }
400
+
401
+ known[slot] = lanes[lane] === null ? 0 : 1;
402
+ value[slot] = lanes[lane] ?? 0;
403
+ }
404
+ }
405
+
406
+ /**
407
+ * @param {import("./particle_program_analysis.js").ParticleDecodedInstruction} instruction
408
+ * @param {Array<number|null>} lanes
409
+ * @param {Uint8Array} known
410
+ * @param {Float32Array} value
411
+ * @param {object} options
412
+ * @returns {number[][]|null} what to emit instead, or `null` to keep the instruction
413
+ */
414
+ function rewrite(instruction, lanes, known, value, options) {
415
+ if (instruction.semantics.side_effects) {
416
+ return null;
417
+ }
418
+
419
+ const materialised = materialise(instruction, lanes, options);
420
+
421
+ if (materialised !== null) {
422
+ return materialised;
423
+ }
424
+
425
+ if (instruction.op === VM_OP.SELECT) {
426
+ const collapsed = collapse_select(instruction, known, value, options.max_select_moves);
427
+
428
+ if (collapsed !== null) {
429
+ return collapsed;
430
+ }
431
+ }
432
+
433
+ const identity = collapse_identity(instruction, known, value);
434
+
435
+ if (identity !== null) {
436
+ return identity;
437
+ }
438
+
439
+ return options.peel_constant_lanes ? peel_constant_tail(instruction, lanes, options) : null;
440
+ }
441
+
442
+ /**
443
+ * Split an instruction whose trailing lanes turn out to be constants into the narrower instruction
444
+ * that computes the rest, plus one load for the tail.
445
+ *
446
+ * One instruction becomes two, which only pays for itself through what it frees: the narrowed
447
+ * instruction stops reading the upper lanes of its sources, and those are usually a broadcast, a
448
+ * mask, and the arithmetic that built them. A distribution's mask lane is exactly this shape.
449
+ *
450
+ * Only for opcodes that compute their result from registers. `LOAD_ATTR`'s lanes past its component
451
+ * count are already the zero it reads for free, and peeling those would be a load in place of
452
+ * nothing at all.
453
+ *
454
+ * @param {import("./particle_program_analysis.js").ParticleDecodedInstruction} instruction
455
+ * @param {Array<number|null>} lanes
456
+ * @param {object} options
457
+ * @returns {number[][]|null}
458
+ */
459
+ function peel_constant_tail(instruction, lanes, options) {
460
+ if (
461
+ options.intern_constant === null
462
+ || instruction.write === null
463
+ || instruction.sources.length === 0
464
+ || !instruction.semantics.narrowable
465
+ || instruction.write.span !== instruction.operation_width
466
+ ) {
467
+ return null;
468
+ }
469
+
470
+ // The longest run of known lanes at the top, leaving at least one below it for the instruction
471
+ // to still be worth issuing — one that is constant all the way down was materialised above.
472
+ let first = lanes.length;
473
+
474
+ while (first > 0 && lanes[first - 1] !== null) {
475
+ first--;
476
+ }
477
+
478
+ if (first === lanes.length || first < Math.max(1, instruction.meta.min_width)) {
479
+ return null;
480
+ }
481
+
482
+ const narrowed = instruction.words.slice();
483
+
484
+ narrowed[0] = vm_with_width(narrowed[0], first);
485
+
486
+ const load = vm_pack_instruction(
487
+ VM_OP.LOAD_CONST, lanes.length - first, instruction.write.base + first,
488
+ [VM_KIND.NONE, VM_KIND.NONE, VM_KIND.NONE],
489
+ [options.intern_constant(lanes.slice(first)), 0, 0]
490
+ );
491
+
492
+ return particle_instruction_is_well_formed(narrowed) && particle_instruction_is_well_formed(load)
493
+ ? [narrowed, load]
494
+ : null;
495
+ }
496
+
497
+ /**
498
+ * Replace an instruction whose every output lane is a known number with the load of that number.
499
+ *
500
+ * Neutral in instruction count on its own; the win is that it cuts the instruction loose from
501
+ * everything that fed it, which is usually then dead.
502
+ *
503
+ * @returns {number[][]|null}
504
+ */
505
+ function materialise(instruction, lanes, options) {
506
+ if (
507
+ instruction.op === VM_OP.LOAD_CONST
508
+ || options.intern_constant === null
509
+ || lanes.length === 0
510
+ || lanes.some(lane => lane === null)
511
+ ) {
512
+ return null;
513
+ }
514
+
515
+ // A reduction writes one lane whatever it reads, so the load that replaces it is one lane wide.
516
+ const words = vm_pack_instruction(
517
+ VM_OP.LOAD_CONST, lanes.length, instruction.write.base,
518
+ [VM_KIND.NONE, VM_KIND.NONE, VM_KIND.NONE],
519
+ [options.intern_constant(lanes), 0, 0]
520
+ );
521
+
522
+ return particle_instruction_is_well_formed(words) ? [words] : null;
523
+ }
524
+
525
+ /**
526
+ * A `SELECT` whose condition is known lane by lane is not a choice, it is copying — and most of
527
+ * those copies are a register into itself.
528
+ *
529
+ * The replacement is capped at `max_select_moves` instructions: what pays for the split is the
530
+ * constant load feeding the condition going dead, so a mask that alternates lane by lane and needs
531
+ * a `MOV` for each run is not worth taking apart.
532
+ *
533
+ * The `SELECT` read every source before it wrote; the `MOV`s run one after another. So a copy is
534
+ * refused where a later one would read a slot an earlier one has already written — the case the
535
+ * split cannot express — and a run taken from the destination itself is a copy of a register into
536
+ * itself and is left out.
537
+ *
538
+ * @returns {number[][]|null}
539
+ */
540
+ function collapse_select(instruction, known, value, max_select_moves) {
541
+ const [when_false, when_true, condition] = instruction.sources;
542
+ const width = instruction.operation_width;
543
+ /** @type {import("./particle_program_analysis.js").ParticleOperandRun[]} the source picked, per lane */
544
+ const chosen = new Array(width);
545
+
546
+ for (let lane = 0; lane < width; lane++) {
547
+ const mask = source_lane_value(condition, lane, known, value);
548
+
549
+ if (mask === null) {
550
+ return null;
551
+ }
552
+
553
+ chosen[lane] = mask > 0.5 ? when_true : when_false;
554
+ }
555
+
556
+ /** @type {number[][]} */
557
+ const moves = [];
558
+ /** @type {Set<number>} slots the copies emitted so far have written */
559
+ const written = new Set();
560
+
561
+ for (let lane = 0; lane < width;) {
562
+ let end = lane + 1;
563
+
564
+ while (end < width && chosen[end] === chosen[lane]) {
565
+ end++;
566
+ }
567
+
568
+ const picked = chosen[lane];
569
+ const is_itself = picked.kind === VM_KIND.REG
570
+ && particle_read_slots(picked, width).slice(lane, end).every((slot, i) => slot === instruction.write.base + lane + i);
571
+
572
+ if (!is_itself) {
573
+ const words = move_from(end - lane, instruction.write.base + lane, picked, lane);
574
+
575
+ if (!particle_instruction_is_well_formed(words)) {
576
+ return null;
577
+ }
578
+
579
+ if (picked.kind === VM_KIND.REG
580
+ && particle_read_slots(picked, width).slice(lane, end).some(slot => written.has(slot))) {
581
+ return null;
582
+ }
583
+
584
+ moves.push(words);
585
+ }
586
+
587
+ for (let slot = lane; slot < end; slot++) {
588
+ written.add(instruction.write.base + slot);
589
+ }
590
+
591
+ lane = end;
592
+ }
593
+
594
+ return moves.length <= max_select_moves ? moves : null;
595
+ }
596
+
597
+ /**
598
+ * The arithmetic identities that hold for every value a register can hold, not merely the ordinary
599
+ * ones.
600
+ *
601
+ * `x * 1`, `x / 1` and `x - (+0)` are `x` bit for bit — including `-0`, `±Inf` and `NaN` — so they
602
+ * are copies. `x + 0` is not: `-0 + 0` is `+0`. `x * 0` is not: it is `-0` for negative `x` and
603
+ * `NaN` for a non-finite one. The line is drawn at what is exactly true, because the record a
604
+ * program leaves is compared to the bit.
605
+ *
606
+ * `MAD` with a multiplier of one becomes an `ADD` for the same reason: `a * 1` is exact, so a
607
+ * contracted `fma(a, 1, c)` and a plain `a + c` are the same number.
608
+ *
609
+ * @returns {number[][]|null}
610
+ */
611
+ function collapse_identity(instruction, known, value) {
612
+ const op = instruction.op;
613
+ const width = instruction.operation_width;
614
+ const sources = instruction.sources;
615
+ const is = (source, expected) => source !== undefined && all_lanes_are(source, width, expected, known, value);
616
+
617
+ /** Rebuild as a copy of one source, whatever kind it is and however it is swizzled. */
618
+ const copy_of = (source) => {
619
+ const words = move_from(width, instruction.write.base, source);
620
+
621
+ return particle_instruction_is_well_formed(words) ? [words] : null;
622
+ };
623
+
624
+ switch (op) {
625
+ case VM_OP.MUL:
626
+ if (is(sources[1], 1)) return copy_of(sources[0]);
627
+ if (is(sources[0], 1)) return copy_of(sources[1]);
628
+ return null;
629
+
630
+ case VM_OP.DIV:
631
+ return is(sources[1], 1) ? copy_of(sources[0]) : null;
632
+
633
+ case VM_OP.SUB:
634
+ // Positive zero only, which is what `Object.is` in `all_lanes_are` gets us: `x - (-0)`
635
+ // is `x + 0`, and that turns a `-0` into a `+0`.
636
+ return is(sources[1], 0) ? copy_of(sources[0]) : null;
637
+
638
+ case VM_OP.MAD: {
639
+ const add = (a, b) => {
640
+ const words = vm_pack_instruction(
641
+ VM_OP.ADD, width, instruction.write.base,
642
+ [a.kind, b.kind, VM_KIND.NONE],
643
+ [a.word, b.word, 0]
644
+ );
645
+
646
+ return particle_instruction_is_well_formed(words) ? [words] : null;
647
+ };
648
+
649
+ if (is(sources[1], 1)) return add(sources[0], sources[2]);
650
+ if (is(sources[0], 1)) return add(sources[1], sources[2]);
651
+ return null;
652
+ }
653
+
654
+ default:
655
+ return null;
656
+ }
657
+ }
658
+
659
+ /**
660
+ * @param {import("./particle_program_analysis.js").ParticleOperandRun} source
661
+ * @param {number} width
662
+ * @param {number} expected
663
+ * @param {Uint8Array} known
664
+ * @param {Float32Array} value
665
+ * @returns {boolean}
666
+ */
667
+ function all_lanes_are(source, width, expected, known, value) {
668
+ for (let lane = 0; lane < width; lane++) {
669
+ const held = source_lane_value(source, lane, known, value);
670
+
671
+ // `Object.is`, so a `-0` never passes for the `+0` the identity actually needs.
672
+ if (held === null || !Object.is(held, expected)) {
673
+ return false;
674
+ }
675
+ }
676
+
677
+ return true;
678
+ }