@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,478 +1,477 @@
1
- import { assert } from "../../../../core/assert.js";
2
- import { PARTICLE_VM_INSTRUCTION_WORDS, PARTICLE_VM_REGISTER_SLOTS } from "../ParticleConstants.js";
3
- import { InstructionStream } from "../isa/InstructionStream.js";
4
- import { ParticleAssembler } from "../isa/ParticleAssembler.js";
5
- import { ParticleProgram } from "../isa/ParticleProgram.js";
6
- import { VM_OP, vm_decode_op, vm_decode_width } from "../isa/ParticleVMISA.js";
7
- import { PARTICLE_PASS_DEAD_CODE, pass_eliminate_dead_code } from "./pass_eliminate_dead_code.js";
8
- import {
9
- PARTICLE_PASS_DEDUP_CONSTANTS,
10
- pass_dedup_constants,
11
- visit_particle_constant_reads,
12
- } from "./pass_dedup_constants.js";
13
- import { PARTICLE_PASS_FOLD_CONSTANTS, pass_fold_constants } from "./pass_fold_constants.js";
14
- import { PARTICLE_PASS_NARROW_WIDTHS, pass_narrow_widths } from "./pass_narrow_widths.js";
15
- import { PARTICLE_PASS_PACK_REGISTERS, pass_pack_registers } from "./pass_pack_registers.js";
16
- import { PARTICLE_PASS_PEEPHOLE, pass_peephole } from "./pass_peephole.js";
17
- import { PARTICLE_PASS_PROPAGATE_COPIES, pass_propagate_copies } from "./pass_propagate_copies.js";
18
- import { PARTICLE_PASS_SHARE_SUBEXPRESSIONS, pass_share_subexpressions } from "./pass_share_subexpressions.js";
19
-
20
- /**
21
- * Binary-level optimizer for particle VM programs: instructions in, instructions out.
22
- *
23
- * It runs over a compiled {@link InstructionStream} rather than inside any one frontend, because
24
- * there is more than one frontend — the node-graph compiler, the assembly text pane in the editor,
25
- * and whatever comes next — and the same waste appears in all of them. The node compiler lowers a
26
- * value at a time and has no way to know that the broadcast it just emitted will be read one lane
27
- * wide; a pass over the finished stream can simply look.
28
- *
29
- * ## Shape
30
- *
31
- * Passes are ordinary functions from an instruction array to a new one, with `null` for "changed
32
- * nothing". They never reorder: every pass rewrites an instruction in place or drops it, which is
33
- * what makes the ordering constraints — `RANDOM`'s draw sequence, `STORE_ATTR`/`LOAD_ATTR`
34
- * aliasing — hold structurally rather than by each pass remembering them.
35
- *
36
- * The pipeline sweeps the enabled passes in order until a whole sweep changes nothing or
37
- * `max_iterations` sweeps have run, then packs the register file once. Passes that pay to repeat on
38
- * their own take their own `max_iterations`, so effort is the caller's to spend.
39
- *
40
- * ## What must not change
41
- *
42
- * A program's meaning is the record it leaves, the kill flag it sets, and the RNG state it ends on.
43
- * Every pass here preserves all three to the bit, with one deliberate exception: folding `x * 0` to
44
- * `+0` gives up the sign of that zero, and gives up `NaN` where something upstream overflowed. It is
45
- * on by default — see the argument in `pass_fold_constants.js` — and
46
- * `{ passes: { fold_constants: { fold_multiply_by_zero: false } } }` takes the optimizer back to
47
- * bit-identical. `optimize_particle_program.spec.js` and the differential harness beside it hold
48
- * both settings to their respective promises, over the real distribution compositions, the
49
- * hand-written spec programs and randomly generated ones.
50
- */
51
-
52
- /**
53
- * @callback ParticleOptimizerPass
54
- * @param {number[][]} instructions five-word tuples, in program order
55
- * @param {ParticleOptimizerContext} context
56
- * @returns {number[][]|null} the rewritten stream, or `null` when the pass changed nothing
57
- */
58
-
59
- /**
60
- * @typedef {object} ParticleOptimizerContext
61
- * @property {ArrayLike<number>} constants the shared constant pool, one float per word
62
- * @property {((values: number[]) => number)|null} intern_constant interns a run of constants and
63
- * returns its first word index — {@link ../isa/ParticleAssembler.js}'s `constant`. Without it a
64
- * pass can read the pool but not add to it, which only costs it the folds that need a new literal.
65
- * @property {number} register_slots size of the register file
66
- * @property {number} max_iterations how many times this pass may repeat itself
67
- */
68
-
69
- /**
70
- * The passes, in the order a sweep runs them. `final` passes run once, after the sweeps settle.
71
- *
72
- * @type {Array<{name: string, run: ParticleOptimizerPass, max_iterations: number, final?: boolean}>}
73
- */
74
- export const PARTICLE_OPTIMIZER_PASSES = [
75
- { name: PARTICLE_PASS_DEAD_CODE, run: pass_eliminate_dead_code, max_iterations: 1 },
76
- { name: PARTICLE_PASS_NARROW_WIDTHS, run: pass_narrow_widths, max_iterations: 1 },
77
- { name: PARTICLE_PASS_FOLD_CONSTANTS, run: pass_fold_constants, max_iterations: 1 },
78
- // Before sharing, and after the folds that intern new literals: two loads of one value are the
79
- // same subexpression only once they name the same pool word.
80
- { name: PARTICLE_PASS_DEDUP_CONSTANTS, run: pass_dedup_constants, max_iterations: 1 },
81
- { name: PARTICLE_PASS_SHARE_SUBEXPRESSIONS, run: pass_share_subexpressions, max_iterations: 1 },
82
- { name: PARTICLE_PASS_PROPAGATE_COPIES, run: pass_propagate_copies, max_iterations: 3 },
83
- { name: PARTICLE_PASS_PEEPHOLE, run: pass_peephole, max_iterations: 1 },
84
- { name: PARTICLE_PASS_PACK_REGISTERS, run: pass_pack_registers, max_iterations: 1, final: true },
85
- ];
86
-
87
- /**
88
- * Passes enabled unless the caller says otherwise. The switches are for bisecting and for measuring
89
- * what each pass is worth; the two rewrites that change the arithmetic rather than the program are
90
- * options of their passes rather than passes, so that the list stays a list of what to run and not a
91
- * list of what to trust. `fold_constants.fold_multiply_by_zero` is on, `peephole.fuse_multiply_add`
92
- * is off, and both are argued where they live.
93
- *
94
- * @type {Readonly<Object<string, boolean>>}
95
- */
96
- export const PARTICLE_OPTIMIZER_DEFAULT_PASSES = Object.freeze(
97
- Object.fromEntries(PARTICLE_OPTIMIZER_PASSES.map(pass => [pass.name, true]))
98
- );
99
-
100
- /**
101
- * How many times the whole pass list may sweep before the optimizer gives up on reaching a fixed
102
- * point. Passes feed each other — folding a `SELECT` makes a broadcast dead, deleting it lets the
103
- * value above narrow — so one sweep is never enough. Two settle every program in this tree and
104
- * three settle the worst of a hundred generated ones; the budget is well clear of that, because a
105
- * program that ran out of sweeps would be a program whose optimization depended on the budget.
106
- *
107
- * @type {number}
108
- */
109
- export const PARTICLE_OPTIMIZER_DEFAULT_ITERATIONS = 8;
110
-
111
- /**
112
- * Optimize one section of a particle program.
113
- *
114
- * @param {InstructionStream} stream
115
- * @param {object} [options]
116
- * @param {ArrayLike<number>} [options.constants] the shared constant pool; without it the passes
117
- * that reason about literal values do nothing
118
- * @param {function(number[]): number} [options.intern_constant] adds a run to that pool
119
- * @param {Object<string, boolean|object>} [options.passes] per-pass switches, overriding
120
- * {@link PARTICLE_OPTIMIZER_DEFAULT_PASSES}; an object value also carries that pass's options
121
- * @param {number} [options.max_iterations] sweeps of the whole pass list
122
- * @param {boolean} [options.speculate] whether to try lane peeling both ways and keep the better
123
- * program; on unless told otherwise. Off makes the pipeline a straight run of its passes, which is
124
- * what a caller measuring one pass in isolation wants.
125
- * @param {number} [options.register_slots]
126
- * @returns {InstructionStream} a fresh stream, whose `register_count` is re-derived from what the
127
- * optimized instructions actually touch
128
- */
129
- export function optimize_particle_program(stream, options = {}) {
130
- assert.ok(stream instanceof InstructionStream, "stream must be an InstructionStream");
131
-
132
- const register_slots = options.register_slots ?? PARTICLE_VM_REGISTER_SLOTS;
133
- const max_iterations = options.max_iterations ?? PARTICLE_OPTIMIZER_DEFAULT_ITERATIONS;
134
-
135
- assert.isNonNegativeInteger(max_iterations, "max_iterations");
136
-
137
- /** @type {number[][]} */
138
- const instructions = stream.instructions.map(words => words.slice());
139
-
140
- const base_context = {
141
- constants: options.constants ?? [],
142
- intern_constant: options.intern_constant ?? null,
143
- register_slots,
144
- };
145
-
146
- const speculative = to_stream(run_pipeline(instructions, base_context, options, max_iterations));
147
-
148
- if (options.speculate === false || !peeling_could_apply(options)) {
149
- return speculative;
150
- }
151
-
152
- // Peeling a constant tail off an instruction always costs one instruction where it fires, and
153
- // pays only through what dies above it — a mask's load, the broadcast feeding it, the arithmetic
154
- // that built them. Whether that cascade happens is not a local question, and a heuristic for it
155
- // would be wrong on somebody's effect. So the pipeline is run both ways and the better program
156
- // kept; these are at most a few hundred instructions and settling one twice costs nothing worth
157
- // measuring.
158
- const conservative = to_stream(
159
- run_pipeline(instructions, base_context, without_peeling(options), max_iterations)
160
- );
161
-
162
- return better_of(conservative, speculative);
163
- }
164
-
165
- /**
166
- * Sweep the enabled passes to a fixed point, then run the closing ones once.
167
- *
168
- * @param {number[][]} instructions
169
- * @param {object} base_context
170
- * @param {object} options
171
- * @param {number} max_iterations
172
- * @returns {number[][]}
173
- */
174
- function run_pipeline(instructions, base_context, options, max_iterations) {
175
- const sweep = PARTICLE_OPTIMIZER_PASSES.filter(pass => pass.final !== true);
176
- const closing = PARTICLE_OPTIMIZER_PASSES.filter(pass => pass.final === true);
177
-
178
- let current = instructions;
179
-
180
- for (let iteration = 0; iteration < max_iterations; iteration++) {
181
- let changed = false;
182
-
183
- for (const pass of sweep) {
184
- const result = run_pass(pass, current, base_context, options);
185
-
186
- if (result !== null) {
187
- current = result;
188
- changed = true;
189
- }
190
- }
191
-
192
- if (!changed) {
193
- break;
194
- }
195
- }
196
-
197
- for (const pass of closing) {
198
- const result = run_pass(pass, current, base_context, options);
199
-
200
- if (result !== null) {
201
- current = result;
202
- }
203
- }
204
-
205
- return current;
206
- }
207
-
208
- /**
209
- * Fewer instructions wins; a tie goes to the smaller register file, and a tie there to the
210
- * speculative one, whose instructions are the narrower of the two.
211
- *
212
- * @param {InstructionStream} conservative
213
- * @param {InstructionStream} speculative
214
- * @returns {InstructionStream}
215
- */
216
- function better_of(conservative, speculative) {
217
- if (conservative.instruction_count !== speculative.instruction_count) {
218
- return conservative.instruction_count < speculative.instruction_count ? conservative : speculative;
219
- }
220
-
221
- return conservative.register_count < speculative.register_count ? conservative : speculative;
222
- }
223
-
224
- /** @returns {boolean} whether the run just made could have peeled anything */
225
- function peeling_could_apply(options) {
226
- const setting = options.passes?.[PARTICLE_PASS_FOLD_CONSTANTS];
227
-
228
- if (setting === false) {
229
- return false;
230
- }
231
-
232
- return !(typeof setting === "object" && setting !== null && setting.peel_constant_lanes === false);
233
- }
234
-
235
- /** @returns {object} the same options with lane peeling turned off */
236
- function without_peeling(options) {
237
- const setting = options.passes?.[PARTICLE_PASS_FOLD_CONSTANTS];
238
- const fold = typeof setting === "object" && setting !== null ? setting : {};
239
-
240
- return {
241
- ...options,
242
- passes: {
243
- ...options.passes,
244
- [PARTICLE_PASS_FOLD_CONSTANTS]: { ...fold, peel_constant_lanes: false },
245
- },
246
- };
247
- }
248
-
249
- /**
250
- * @param {{name: string, run: ParticleOptimizerPass, max_iterations: number}} pass
251
- * @param {number[][]} instructions
252
- * @param {object} base_context
253
- * @param {object} options
254
- * @returns {number[][]|null}
255
- */
256
- function run_pass(pass, instructions, base_context, options) {
257
- const setting = options.passes?.[pass.name];
258
-
259
- if (setting === false) {
260
- return null;
261
- }
262
-
263
- const overrides = typeof setting === "object" && setting !== null ? setting : {};
264
-
265
- return pass.run(instructions, {
266
- ...base_context,
267
- max_iterations: pass.max_iterations,
268
- ...overrides,
269
- });
270
- }
271
-
272
- /**
273
- * A `passes` setting enabling only the named passes.
274
- *
275
- * The plain object form is additive — anything it does not mention keeps its default — which is what
276
- * a caller turning one pass off wants and the opposite of what a caller measuring one pass wants.
277
- *
278
- * @param {...string} names
279
- * @returns {Object<string, boolean>}
280
- */
281
- export function only_particle_passes(...names) {
282
- const switches = {};
283
-
284
- for (const pass of PARTICLE_OPTIMIZER_PASSES) {
285
- switches[pass.name] = names.includes(pass.name);
286
- }
287
-
288
- return switches;
289
- }
290
-
291
- /**
292
- * Optimize both sections of a compiled program against their shared constant pool.
293
- *
294
- * The pool is shared and may grow — a folded expression needs somewhere to put its literal — so it
295
- * is interned through a {@link ParticleAssembler}, which is the code that already knows how to reuse
296
- * a run that is anywhere in the pool rather than only one on a boundary.
297
- *
298
- * @param {ParticleProgram} program
299
- * @param {object} [options] as {@link optimize_particle_program}, minus the pool it supplies itself
300
- * @returns {ParticleProgram}
301
- */
302
- export function optimize_compiled_particle_program(program, options = {}) {
303
- const assembler = new ParticleAssembler();
304
-
305
- assembler.constants = Array.from(program.constants);
306
-
307
- const shared = {
308
- ...options,
309
- constants: assembler.constants,
310
- intern_constant: (values) => assembler.constant(...values),
311
- };
312
-
313
- const init = optimize_particle_program(stream_from_words(program.init), shared);
314
- const update = optimize_particle_program(stream_from_words(program.update), shared);
315
-
316
- const init_words = init.toWords();
317
- const update_words = update.toWords();
318
-
319
- // Last, and here rather than in a pass: which pool words are dead is a question about the whole
320
- // program, and a pass sees one section. Running it before the sections settled would be wrong
321
- // twice over — the folds are still interning, and the speculation below has not yet thrown away
322
- // the run whose literals are the dead ones.
323
- const constants = sweep_particle_constants(
324
- [init_words, update_words],
325
- assembler.constants,
326
- sweeping_is_enabled(options)
327
- );
328
-
329
- return new ParticleProgram({
330
- init: init_words,
331
- update: update_words,
332
- constants,
333
- register_count: Math.max(init.register_count, update.register_count),
334
- });
335
- }
336
-
337
- /**
338
- * Whether the caller left the constant-pool passes on. See the note in
339
- * {@link ./pass_dedup_constants.js} on why the compaction follows the rewrite's switch.
340
- *
341
- * @param {object} options
342
- * @returns {boolean}
343
- */
344
- function sweeping_is_enabled(options) {
345
- const setting = options.passes?.[PARTICLE_PASS_DEDUP_CONSTANTS];
346
-
347
- if (setting === false) {
348
- return false;
349
- }
350
-
351
- return !(typeof setting === "object" && setting !== null && setting.sweep === false);
352
- }
353
-
354
- /**
355
- * Drop the pool words no section loads any more, and move the loads onto what is left.
356
- *
357
- * Marking is per word rather than per run, and that is what makes compaction safe for runs that
358
- * overlap — a scalar sharing the leading word of a `vec4` is something the assembler deliberately
359
- * produces. Every word of a live run is marked, and compaction keeps marked words in order, so a run
360
- * whose words were consecutive still is; only its base moves.
361
- *
362
- * @param {Uint32Array[]} sections instruction words, rewritten in place
363
- * @param {ArrayLike<number>} constants the shared pool
364
- * @param {boolean} enabled
365
- * @returns {Float32Array} the pool the sections now address
366
- */
367
- function sweep_particle_constants(sections, constants, enabled) {
368
- if (!enabled) {
369
- return new Float32Array(constants);
370
- }
371
-
372
- const live = new Uint8Array(constants.length);
373
-
374
- let well_formed = true;
375
-
376
- for (const words of sections) {
377
- visit_particle_constant_reads(instruction_tuples(words), (index, base, span) => {
378
- if (base < 0 || base + span > constants.length) {
379
- // Out of the pool already. Compacting under it would turn a program that fails
380
- // validation into one that fails it somewhere else, so nothing is touched.
381
- well_formed = false;
382
- return;
383
- }
384
-
385
- for (let i = 0; i < span; i++) {
386
- live[base + i] = 1;
387
- }
388
- });
389
- }
390
-
391
- if (!well_formed) {
392
- return new Float32Array(constants);
393
- }
394
-
395
- /** @type {number[]} */
396
- const kept = [];
397
-
398
- /** @type {Int32Array} old word index -> new one; only meaningful where `live` */
399
- const remap = new Int32Array(constants.length);
400
-
401
- for (let i = 0; i < constants.length; i++) {
402
- remap[i] = kept.length;
403
-
404
- if (live[i] === 1) {
405
- kept.push(constants[i]);
406
- }
407
- }
408
-
409
- if (kept.length !== constants.length) {
410
- for (const words of sections) {
411
- for (let base = 0; base < words.length; base += PARTICLE_VM_INSTRUCTION_WORDS) {
412
- if (vm_decode_op(words[base]) !== VM_OP.LOAD_CONST) {
413
- continue;
414
- }
415
-
416
- words[base + 2] = remap[words[base + 2]];
417
- }
418
- }
419
- }
420
-
421
- return new Float32Array(kept);
422
- }
423
-
424
- /**
425
- * A packed section as the five-word tuples the pass helpers read.
426
- *
427
- * @param {Uint32Array} words
428
- * @returns {Uint32Array[]} views, so nothing is copied
429
- */
430
- function instruction_tuples(words) {
431
- const out = [];
432
-
433
- for (let base = 0; base < words.length; base += PARTICLE_VM_INSTRUCTION_WORDS) {
434
- out.push(words.subarray(base, base + PARTICLE_VM_INSTRUCTION_WORDS));
435
- }
436
-
437
- return out;
438
- }
439
-
440
- /**
441
- * @param {Uint32Array} words a packed instruction section
442
- * @returns {InstructionStream}
443
- */
444
- export function stream_from_words(words) {
445
- const out = new InstructionStream();
446
-
447
- for (let base = 0; base < words.length; base += PARTICLE_VM_INSTRUCTION_WORDS) {
448
- out.emit(
449
- vm_decode_op(words[base]),
450
- words[base + 1],
451
- words[base + 2],
452
- words[base + 3],
453
- words[base + 4],
454
- vm_decode_width(words[base])
455
- );
456
- }
457
-
458
- return out;
459
- }
460
-
461
- /**
462
- * Re-emit into a fresh stream, so the register high-water mark is the optimized program's rather
463
- * than the one it was built from. A file that shrank is itself part of the win — it is the
464
- * interpreter's dominant register cost (see the occupancy note in `vm/chunk_particle_vm.js`) — and
465
- * carrying the old count forward would hide it.
466
- *
467
- * @param {number[][]} instructions
468
- * @returns {InstructionStream}
469
- */
470
- function to_stream(instructions) {
471
- const out = new InstructionStream();
472
-
473
- for (const words of instructions) {
474
- out.emit(vm_decode_op(words[0]), words[1], words[2], words[3], words[4], vm_decode_width(words[0]));
475
- }
476
-
477
- return out;
478
- }
1
+ import { assert } from "../../../../core/assert.js";
2
+ import { PARTICLE_VM_INSTRUCTION_WORDS, PARTICLE_VM_REGISTER_SLOTS } from "../ParticleConstants.js";
3
+ import { InstructionStream } from "../isa/InstructionStream.js";
4
+ import { ParticleAssembler } from "../isa/ParticleAssembler.js";
5
+ import { ParticleProgram } from "../isa/ParticleProgram.js";
6
+ import { VM_OP, vm_decode_op } from "../isa/ParticleVMISA.js";
7
+ import { PARTICLE_PASS_ALLOCATE_REGISTERS, pass_allocate_registers } from "./pass_allocate_registers.js";
8
+ import { PARTICLE_PASS_DEAD_CODE, pass_eliminate_dead_code } from "./pass_eliminate_dead_code.js";
9
+ import {
10
+ PARTICLE_PASS_DEDUP_CONSTANTS,
11
+ pass_dedup_constants,
12
+ visit_particle_constant_reads,
13
+ } from "./pass_dedup_constants.js";
14
+ import { PARTICLE_PASS_FOLD_CONSTANTS, pass_fold_constants } from "./pass_fold_constants.js";
15
+ import { PARTICLE_PASS_INLINE_CONSTANTS, pass_inline_constants } from "./pass_inline_constants.js";
16
+ import { PARTICLE_PASS_NARROW_WIDTHS, pass_narrow_widths } from "./pass_narrow_widths.js";
17
+ import { PARTICLE_PASS_PACK_REGISTERS, pass_pack_registers } from "./pass_pack_registers.js";
18
+ import { PARTICLE_PASS_PEEPHOLE, pass_peephole } from "./pass_peephole.js";
19
+ import { PARTICLE_PASS_PROPAGATE_COPIES, pass_propagate_copies } from "./pass_propagate_copies.js";
20
+ import { PARTICLE_PASS_SHARE_SUBEXPRESSIONS, pass_share_subexpressions } from "./pass_share_subexpressions.js";
21
+ import { PARTICLE_PASS_SCHEDULE, pass_schedule } from "./pass_schedule.js";
22
+
23
+ /**
24
+ * Binary-level optimizer for particle VM programs: a {@link ParticleProgram} in, one out.
25
+ *
26
+ * {@link optimize_particle_program} is the entry point and its options are FLAT — the passes to run
27
+ * as a list of names, and every rewrite a caller might want to name as an option of its own. Which
28
+ * pass implements a rewrite is this file's business and not the caller's, so
29
+ * `{ fold_multiply_by_zero: false }` asks for exact arithmetic without naming the pass that would
30
+ * otherwise round. {@link ./dag/optimize_particle_program_vector.js the graph optimizer} has the
31
+ * same shape, so a caller can hold both and choose between them.
32
+ *
33
+ * It runs over a compiled {@link InstructionStream} rather than inside any one frontend, because
34
+ * there is more than one frontend — the node-graph compiler, the assembly text pane in the editor,
35
+ * and whatever comes next — and the same waste appears in all of them. The node compiler lowers a
36
+ * value at a time and has no way to know that the broadcast it just emitted will be read one lane
37
+ * wide; a pass over the finished stream can simply look.
38
+ *
39
+ * ## Shape
40
+ *
41
+ * Passes are ordinary functions from an instruction array to a new one, with `null` for "changed
42
+ * nothing". All but one never reorder: every pass rewrites an instruction in place or drops it,
43
+ * which is what makes the ordering constraints — `RANDOM`'s draw sequence, `STORE_ATTR`/`LOAD_ATTR`
44
+ * aliasing — hold structurally rather than by each pass remembering them. The one that does,
45
+ * `pass_schedule`, moves nothing across an instruction with side effects, which keeps the same
46
+ * constraints structural for it.
47
+ *
48
+ * The pipeline sweeps the enabled passes in order until a whole sweep changes nothing or
49
+ * `max_iterations` sweeps have run, then schedules, allocates and packs the register file — and
50
+ * sweeps again if that changed anything, since a reordered program is one the passes have not seen.
51
+ * Passes that pay to repeat on their own take their own `max_iterations`, so effort is the
52
+ * caller's to spend.
53
+ *
54
+ * ## What must not change
55
+ *
56
+ * A program's meaning is the record it leaves, the kill flag it sets, and the RNG state it ends on.
57
+ * Every pass here preserves all three to the bit, with one deliberate exception: folding `x * 0` to
58
+ * `+0` gives up the sign of that zero, and gives up `NaN` where something upstream overflowed. That
59
+ * substitution is made in a register, so what it does to the record is whatever the instructions
60
+ * below it do with a zero — which is not bounded, and is why the harness holds the fold to the
61
+ * record words a multiply can REACH rather than to a shape. It is on by default — see the argument
62
+ * in `pass_fold_constants.js` — and `{ fold_multiply_by_zero: false }`
63
+ * takes the optimizer back to bit-identical. `optimize_particle_program.spec.js` and the
64
+ * differential harness beside it hold both settings to their respective promises, over the real
65
+ * distribution compositions, the hand-written spec programs and randomly generated ones.
66
+ *
67
+ * Two further rewrites change the arithmetic and are off unless a caller names them:
68
+ * `fuse_multiply_add`, which is a rounding the reference can see, and `fold_inexact`, which is a
69
+ * rounding only a GPU can see — the fast-math fold of the transcendentals and `rsqrt`. Each is
70
+ * argued where it lives.
71
+ */
72
+
73
+ /**
74
+ * @callback ParticleOptimizerPass
75
+ * @param {number[][]} instructions five-word tuples, in program order
76
+ * @param {ParticleOptimizerContext} context
77
+ * @returns {number[][]|null} the rewritten stream, or `null` when the pass changed nothing
78
+ */
79
+
80
+ /**
81
+ * @typedef {object} ParticleOptimizerContext
82
+ * @property {ArrayLike<number>} constants the shared constant pool, one float per word
83
+ * @property {((values: number[]) => number)|null} intern_constant interns a run of constants and
84
+ * returns its first word index — {@link ../isa/ParticleAssembler.js}'s `constant`. Without it a
85
+ * pass can read the pool but not add to it, which only costs it the folds that need a new literal.
86
+ * @property {number} register_slots size of the register file
87
+ * @property {number} max_iterations how many times this pass may repeat itself
88
+ */
89
+
90
+ /**
91
+ * The passes, in the order a sweep runs them. `final` passes run once, after the sweeps settle.
92
+ * A pass marked `enabled: false` runs only when a caller switches it on by name.
93
+ *
94
+ * @type {Array<{name: string, run: ParticleOptimizerPass, max_iterations: number, final?: boolean, enabled?: boolean}>}
95
+ */
96
+ export const PARTICLE_OPTIMIZER_PASSES = [
97
+ { name: PARTICLE_PASS_DEAD_CODE, run: pass_eliminate_dead_code, max_iterations: 1 },
98
+ { name: PARTICLE_PASS_NARROW_WIDTHS, run: pass_narrow_widths, max_iterations: 1 },
99
+ { name: PARTICLE_PASS_FOLD_CONSTANTS, run: pass_fold_constants, max_iterations: 1 },
100
+ // Before sharing, and after the folds that intern new literals: two loads of one value are the
101
+ // same subexpression only once they name the same pool word.
102
+ { name: PARTICLE_PASS_DEDUP_CONSTANTS, run: pass_dedup_constants, max_iterations: 1 },
103
+ { name: PARTICLE_PASS_SHARE_SUBEXPRESSIONS, run: pass_share_subexpressions, max_iterations: 1 },
104
+ { name: PARTICLE_PASS_PROPAGATE_COPIES, run: pass_propagate_copies, max_iterations: 3 },
105
+ { name: PARTICLE_PASS_PEEPHOLE, run: pass_peephole, max_iterations: 1 },
106
+ // Last in the sweep: every pass above reasons about constants as loads into registers, and
107
+ // once a constant is an operand of its consumer there is nothing left for them to see. This is
108
+ // where the literal kind pays — a third of the simulate pass on a constant-heavy effect, see
109
+ // ../vm/chunk_particle_vm.js — so it is on like the rest.
110
+ { name: PARTICLE_PASS_INLINE_CONSTANTS, run: pass_inline_constants, max_iterations: 1 },
111
+ // Once the instructions have settled, their order: a value that lives no longer than it must
112
+ // is a slot the allocator below can give back.
113
+ { name: PARTICLE_PASS_SCHEDULE, run: pass_schedule, max_iterations: 1, final: true },
114
+ // Allocation packs after every placement itself, so packing on its own is what runs when the
115
+ // allocator is switched off — and a no-op after it.
116
+ { name: PARTICLE_PASS_ALLOCATE_REGISTERS, run: pass_allocate_registers, max_iterations: 1, final: true },
117
+ { name: PARTICLE_PASS_PACK_REGISTERS, run: pass_pack_registers, max_iterations: 1, final: true },
118
+ ];
119
+
120
+ /**
121
+ * The passes that run unless the caller names a different set. For bisecting, and for measuring what
122
+ * each pass is worth; the rewrites that change the ARITHMETIC rather than the program are flat
123
+ * options instead of passes, so that this stays a list of what to run and not a list of what to
124
+ * trust. `fold_multiply_by_zero` is on, `fuse_multiply_add` and `fold_inexact` are off, and each is
125
+ * argued where it lives.
126
+ *
127
+ * @type {ReadonlyArray<string>}
128
+ */
129
+ export const PARTICLE_OPTIMIZER_DEFAULT_PASSES = Object.freeze(
130
+ PARTICLE_OPTIMIZER_PASSES.filter(pass => pass.enabled !== false).map(pass => pass.name)
131
+ );
132
+
133
+ /**
134
+ * How many times the whole pass list may sweep before the optimizer gives up on reaching a fixed
135
+ * point. Passes feed each other — folding a `SELECT` makes a broadcast dead, deleting it lets the
136
+ * value above narrow — so one sweep is never enough. Two settle every program in this tree and
137
+ * three settle the worst of a hundred generated ones; the budget is well clear of that, because a
138
+ * program that ran out of sweeps would be a program whose optimization depended on the budget.
139
+ *
140
+ * @type {number}
141
+ */
142
+ export const PARTICLE_OPTIMIZER_DEFAULT_ITERATIONS = 8;
143
+
144
+ /**
145
+ * Optimize one section of a particle program.
146
+ *
147
+ * The options are flat, and the ones that are not about the pipeline itself are handed to every pass
148
+ * — each takes the ones it knows and ignores the rest, which is why a rewrite like
149
+ * `fold_multiply_by_zero` is named here rather than buried under the pass that happens to implement
150
+ * it. A caller asking for exact arithmetic should not have to know which pass would otherwise round.
151
+ *
152
+ * @param {InstructionStream} stream
153
+ * @param {object} [options]
154
+ * @param {ArrayLike<number>} [options.constants] the shared constant pool; without it the passes
155
+ * that reason about literal values do nothing
156
+ * @param {function(number[]): number} [options.intern_constant] adds a run to that pool
157
+ * @param {ReadonlyArray<string>} [options.passes] the passes to run, defaulting to
158
+ * {@link PARTICLE_OPTIMIZER_DEFAULT_PASSES}; see {@link particle_passes_except}
159
+ * @param {number} [options.max_iterations] sweeps of the whole pass list
160
+ * @param {boolean} [options.speculate] whether to try lane peeling both ways and keep the better
161
+ * program; on unless told otherwise. Off makes the pipeline a straight run of its passes, which is
162
+ * what a caller measuring one pass in isolation wants.
163
+ * @param {number} [options.register_slots]
164
+ * @param {boolean} [options.fold_multiply_by_zero] a known zero absorbs a multiply
165
+ * @param {boolean} [options.fold_inexact] fold what the two executors may round apart
166
+ * @param {boolean} [options.peel_constant_lanes] split a lane whose value is known off its
167
+ * instruction
168
+ * @param {number} [options.max_select_moves] how many copies folding a `SELECT` may cost
169
+ * @param {boolean} [options.fuse_multiply_add] fuse where the rounding may differ
170
+ * @param {boolean} [options.fuse_exact_multiply_add] fuse where it cannot
171
+ * @param {boolean} [options.merge_constant_loads] join adjacent pool loads into one run
172
+ * @param {boolean} [options.merge_moves] join adjacent copies into one
173
+ * @returns {InstructionStream} a fresh stream, whose `register_count` is re-derived from what the
174
+ * optimized instructions actually touch
175
+ */
176
+ export function optimize_particle_section(stream, {
177
+ constants = [],
178
+ intern_constant = null,
179
+ register_slots = PARTICLE_VM_REGISTER_SLOTS,
180
+ passes = PARTICLE_OPTIMIZER_DEFAULT_PASSES,
181
+ max_iterations = PARTICLE_OPTIMIZER_DEFAULT_ITERATIONS,
182
+ speculate = true,
183
+ ...settings
184
+ } = {}) {
185
+ assert.ok(stream instanceof InstructionStream, "stream must be an InstructionStream");
186
+ assert.isNonNegativeInteger(max_iterations, "max_iterations");
187
+
188
+ /** @type {number[][]} */
189
+ const instructions = stream.instructions.map(words => words.slice());
190
+ const enabled = new Set(passes);
191
+ const base_context = { ...settings, constants, intern_constant, register_slots };
192
+
193
+ const speculative = to_stream(run_pipeline(instructions, base_context, enabled, max_iterations));
194
+
195
+ if (speculate === false || !enabled.has(PARTICLE_PASS_FOLD_CONSTANTS) || settings.peel_constant_lanes === false) {
196
+ return speculative;
197
+ }
198
+
199
+ // Peeling a constant tail off an instruction always costs one instruction where it fires, and
200
+ // pays only through what dies above it — a mask's load, the broadcast feeding it, the arithmetic
201
+ // that built them. Whether that cascade happens is not a local question, and a heuristic for it
202
+ // would be wrong on somebody's effect. So the pipeline is run both ways and the better program
203
+ // kept; these are at most a few hundred instructions and settling one twice costs nothing worth
204
+ // measuring.
205
+ const conservative = to_stream(
206
+ run_pipeline(instructions, { ...base_context, peel_constant_lanes: false }, enabled, max_iterations)
207
+ );
208
+
209
+ return better_of(conservative, speculative);
210
+ }
211
+
212
+ /**
213
+ * Sweep the enabled passes to a fixed point, then the closing ones to theirs — and round again
214
+ * while the closing passes keep finding something.
215
+ *
216
+ * The two halves feed each other. A schedule frees slots the allocator then reuses, and an
217
+ * allocated file constrains the next schedule; and a program in a new order is a program the sweep
218
+ * passes have not seen — a copy whose original is now intact until the read, two loads that are now
219
+ * adjacent. Each closing acceptance shrinks the file or its peak, so the rounds are bounded by the
220
+ * file's size, and a program that comes back unchanged from a round is one a second run of the
221
+ * optimizer would also leave alone.
222
+ *
223
+ * @param {number[][]} instructions
224
+ * @param {object} base_context
225
+ * @param {Set<string>} enabled
226
+ * @param {number} max_iterations sweeps of the pass list per round
227
+ * @returns {number[][]}
228
+ */
229
+ function run_pipeline(instructions, base_context, enabled, max_iterations) {
230
+ const running = PARTICLE_OPTIMIZER_PASSES.filter(pass => enabled.has(pass.name));
231
+ const sweep = running.filter(pass => pass.final !== true);
232
+ const closing = running.filter(pass => pass.final === true);
233
+
234
+ let current = instructions;
235
+
236
+ for (let round = 0; round < 2 * base_context.register_slots; round++) {
237
+ current = run_to_fixed_point(sweep, current, base_context, max_iterations);
238
+
239
+ const closed = run_to_fixed_point(closing, current, base_context, 2 * base_context.register_slots);
240
+
241
+ if (closed === current) {
242
+ break;
243
+ }
244
+
245
+ current = closed;
246
+ }
247
+
248
+ return current;
249
+ }
250
+
251
+ /**
252
+ * @param {Array<{name: string, run: ParticleOptimizerPass, max_iterations: number}>} passes
253
+ * @param {number[][]} instructions
254
+ * @param {object} base_context
255
+ * @param {number} max_iterations
256
+ * @returns {number[][]} the settled stream — `instructions` itself when nothing changed
257
+ */
258
+ function run_to_fixed_point(passes, instructions, base_context, max_iterations) {
259
+ let current = instructions;
260
+
261
+ for (let iteration = 0; iteration < max_iterations; iteration++) {
262
+ let changed = false;
263
+
264
+ for (const pass of passes) {
265
+ const result = pass.run(current, { ...base_context, max_iterations: pass.max_iterations });
266
+
267
+ if (result !== null) {
268
+ current = result;
269
+ changed = true;
270
+ }
271
+ }
272
+
273
+ if (!changed) {
274
+ break;
275
+ }
276
+ }
277
+
278
+ return current;
279
+ }
280
+
281
+ /**
282
+ * Fewer instructions wins; a tie goes to the smaller register file, and a tie there to the
283
+ * speculative one, whose instructions are the narrower of the two.
284
+ *
285
+ * @param {InstructionStream} conservative
286
+ * @param {InstructionStream} speculative
287
+ * @returns {InstructionStream}
288
+ */
289
+ function better_of(conservative, speculative) {
290
+ if (conservative.instruction_count !== speculative.instruction_count) {
291
+ return conservative.instruction_count < speculative.instruction_count ? conservative : speculative;
292
+ }
293
+
294
+ return conservative.register_count < speculative.register_count ? conservative : speculative;
295
+ }
296
+
297
+ /**
298
+ * The default passes with these ones left out — what a caller turning ONE pass off wants, without
299
+ * having to write down the ten it is keeping.
300
+ *
301
+ * @param {...string} names
302
+ * @returns {string[]}
303
+ */
304
+ export function particle_passes_except(...names) {
305
+ return PARTICLE_OPTIMIZER_DEFAULT_PASSES.filter(name => !names.includes(name));
306
+ }
307
+
308
+ /**
309
+ * Optimize a compiled program: `ParticleProgram` in, `ParticleProgram` out.
310
+ *
311
+ * The entry point, and the same shape
312
+ * {@link ./dag/optimize_particle_program_vector.js the graph optimizer} has — one program in, one
313
+ * out, flat options — so that a caller can hold both and choose.
314
+ *
315
+ * Both sections are optimized against one constant pool, which is shared and may grow: a folded
316
+ * expression needs somewhere to put its literal. It is interned through a
317
+ * {@link ParticleAssembler}, which is the code that already knows how to reuse a run anywhere in the
318
+ * pool rather than only one on a boundary.
319
+ *
320
+ * @param {ParticleProgram} program
321
+ * @param {object} [options] as {@link optimize_particle_section}, minus the pool it supplies itself
322
+ * @returns {ParticleProgram}
323
+ */
324
+ export function optimize_particle_program(program, options = {}) {
325
+ assert.ok(program instanceof ParticleProgram, "program must be a ParticleProgram");
326
+
327
+ const assembler = new ParticleAssembler();
328
+
329
+ assembler.constants = Array.from(program.constants);
330
+
331
+ const shared = {
332
+ ...options,
333
+ constants: assembler.constants,
334
+ intern_constant: (values) => assembler.constant(...values),
335
+ };
336
+
337
+ const init = optimize_particle_section(stream_from_words(program.init), shared);
338
+ const update = optimize_particle_section(stream_from_words(program.update), shared);
339
+
340
+ const init_words = init.toWords();
341
+ const update_words = update.toWords();
342
+
343
+ // Last, and here rather than in a pass: which pool words are dead is a question about the whole
344
+ // program, and a pass sees one section. Running it before the sections settled would be wrong
345
+ // twice over — the folds are still interning, and the speculation below has not yet thrown away
346
+ // the run whose literals are the dead ones.
347
+ const constants = sweep_particle_constants(
348
+ [init_words, update_words],
349
+ assembler.constants,
350
+ (options.passes ?? PARTICLE_OPTIMIZER_DEFAULT_PASSES).includes(PARTICLE_PASS_DEDUP_CONSTANTS)
351
+ && options.sweep !== false
352
+ );
353
+
354
+ return new ParticleProgram({
355
+ init: init_words,
356
+ update: update_words,
357
+ constants,
358
+ register_count: Math.max(init.register_count, update.register_count),
359
+ });
360
+ }
361
+
362
+ /**
363
+ * Drop the pool words no section loads any more, and move the loads onto what is left.
364
+ *
365
+ * Marking is per word rather than per run, and that is what makes compaction safe for runs that
366
+ * overlap — a scalar sharing the leading word of a `vec4` is something the assembler deliberately
367
+ * produces. Every word of a live run is marked, and compaction keeps marked words in order, so a run
368
+ * whose words were consecutive still is; only its base moves.
369
+ *
370
+ * @param {Uint32Array[]} sections instruction words, rewritten in place
371
+ * @param {ArrayLike<number>} constants the shared pool
372
+ * @param {boolean} enabled
373
+ * @returns {Float32Array} the pool the sections now address
374
+ */
375
+ function sweep_particle_constants(sections, constants, enabled) {
376
+ if (!enabled) {
377
+ return new Float32Array(constants);
378
+ }
379
+
380
+ const live = new Uint8Array(constants.length);
381
+
382
+ let well_formed = true;
383
+
384
+ for (const words of sections) {
385
+ visit_particle_constant_reads(instruction_tuples(words), (index, base, span) => {
386
+ if (base < 0 || base + span > constants.length) {
387
+ // Out of the pool already. Compacting under it would turn a program that fails
388
+ // validation into one that fails it somewhere else, so nothing is touched.
389
+ well_formed = false;
390
+ return;
391
+ }
392
+
393
+ for (let i = 0; i < span; i++) {
394
+ live[base + i] = 1;
395
+ }
396
+ });
397
+ }
398
+
399
+ if (!well_formed) {
400
+ return new Float32Array(constants);
401
+ }
402
+
403
+ /** @type {number[]} */
404
+ const kept = [];
405
+
406
+ /** @type {Int32Array} old word index -> new one; only meaningful where `live` */
407
+ const remap = new Int32Array(constants.length);
408
+
409
+ for (let i = 0; i < constants.length; i++) {
410
+ remap[i] = kept.length;
411
+
412
+ if (live[i] === 1) {
413
+ kept.push(constants[i]);
414
+ }
415
+ }
416
+
417
+ if (kept.length !== constants.length) {
418
+ for (const words of sections) {
419
+ for (let base = 0; base < words.length; base += PARTICLE_VM_INSTRUCTION_WORDS) {
420
+ if (vm_decode_op(words[base]) === VM_OP.LOAD_CONST) {
421
+ words[base + 1] = remap[words[base + 1]];
422
+ }
423
+ }
424
+ }
425
+ }
426
+
427
+ return new Float32Array(kept);
428
+ }
429
+
430
+ /**
431
+ * A packed section as the four-word tuples the pass helpers read.
432
+ *
433
+ * @param {Uint32Array} words
434
+ * @returns {Uint32Array[]} views, so nothing is copied
435
+ */
436
+ function instruction_tuples(words) {
437
+ const out = [];
438
+
439
+ for (let base = 0; base < words.length; base += PARTICLE_VM_INSTRUCTION_WORDS) {
440
+ out.push(words.subarray(base, base + PARTICLE_VM_INSTRUCTION_WORDS));
441
+ }
442
+
443
+ return out;
444
+ }
445
+
446
+ /**
447
+ * @param {Uint32Array} words a packed instruction section
448
+ * @returns {InstructionStream}
449
+ */
450
+ export function stream_from_words(words) {
451
+ const out = new InstructionStream();
452
+
453
+ for (let base = 0; base < words.length; base += PARTICLE_VM_INSTRUCTION_WORDS) {
454
+ out.emitWords(words.subarray(base, base + PARTICLE_VM_INSTRUCTION_WORDS));
455
+ }
456
+
457
+ return out;
458
+ }
459
+
460
+ /**
461
+ * Re-emit into a fresh stream, so the register high-water mark is the optimized program's rather
462
+ * than the one it was built from. A file that shrank is itself part of the win — it is the
463
+ * interpreter's dominant register cost (see the occupancy note in `vm/chunk_particle_vm.js`) — and
464
+ * carrying the old count forward would hide it.
465
+ *
466
+ * @param {number[][]} instructions
467
+ * @returns {InstructionStream}
468
+ */
469
+ function to_stream(instructions) {
470
+ const out = new InstructionStream();
471
+
472
+ for (const words of instructions) {
473
+ out.emitWords(words);
474
+ }
475
+
476
+ return out;
477
+ }