@codexo/exojs-particles 0.15.2 → 0.16.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.
- package/README.md +21 -43
- package/dist/esm/ParticleStorage.d.ts +188 -0
- package/dist/esm/ParticleStorage.d.ts.map +1 -0
- package/dist/esm/ParticleStorage.js +208 -0
- package/dist/esm/ParticleStorage.js.map +1 -0
- package/dist/esm/ParticleSystem.d.ts +189 -60
- package/dist/esm/ParticleSystem.d.ts.map +1 -0
- package/dist/esm/ParticleSystem.js +660 -560
- package/dist/esm/ParticleSystem.js.map +1 -1
- package/dist/esm/distributions/BoxArea.d.ts +1 -0
- package/dist/esm/distributions/BoxArea.d.ts.map +1 -0
- package/dist/esm/distributions/BoxArea.js +38 -44
- package/dist/esm/distributions/BoxArea.js.map +1 -1
- package/dist/esm/distributions/CircleArea.d.ts +1 -0
- package/dist/esm/distributions/CircleArea.d.ts.map +1 -0
- package/dist/esm/distributions/CircleArea.js +30 -28
- package/dist/esm/distributions/CircleArea.js.map +1 -1
- package/dist/esm/distributions/ColorGradient.d.ts +3 -2
- package/dist/esm/distributions/ColorGradient.d.ts.map +1 -0
- package/dist/esm/distributions/ColorGradient.js +70 -75
- package/dist/esm/distributions/ColorGradient.js.map +1 -1
- package/dist/esm/distributions/ConeDirection.d.ts +2 -1
- package/dist/esm/distributions/ConeDirection.d.ts.map +1 -0
- package/dist/esm/distributions/ConeDirection.js +41 -39
- package/dist/esm/distributions/ConeDirection.js.map +1 -1
- package/dist/esm/distributions/Constant.d.ts +1 -0
- package/dist/esm/distributions/Constant.d.ts.map +1 -0
- package/dist/esm/distributions/Constant.js +32 -32
- package/dist/esm/distributions/Constant.js.map +1 -1
- package/dist/esm/distributions/Curve.d.ts +2 -1
- package/dist/esm/distributions/Curve.d.ts.map +1 -0
- package/dist/esm/distributions/Curve.js +46 -55
- package/dist/esm/distributions/Curve.js.map +1 -1
- package/dist/esm/distributions/Distribution.d.ts +4 -3
- package/dist/esm/distributions/Distribution.d.ts.map +1 -0
- package/dist/esm/distributions/LineSegment.d.ts +1 -0
- package/dist/esm/distributions/LineSegment.d.ts.map +1 -0
- package/dist/esm/distributions/LineSegment.js +25 -23
- package/dist/esm/distributions/LineSegment.js.map +1 -1
- package/dist/esm/distributions/Range.d.ts +1 -0
- package/dist/esm/distributions/Range.d.ts.map +1 -0
- package/dist/esm/distributions/Range.js +18 -16
- package/dist/esm/distributions/Range.js.map +1 -1
- package/dist/esm/distributions/VectorRange.d.ts +1 -0
- package/dist/esm/distributions/VectorRange.d.ts.map +1 -0
- package/dist/esm/distributions/VectorRange.js +29 -27
- package/dist/esm/distributions/VectorRange.js.map +1 -1
- package/dist/esm/distributions/index.d.ts +1 -0
- package/dist/esm/distributions/index.d.ts.map +1 -0
- package/dist/esm/gpu/ParticleGpuState.d.ts +143 -18
- package/dist/esm/gpu/ParticleGpuState.d.ts.map +1 -0
- package/dist/esm/gpu/ParticleGpuState.js +655 -497
- package/dist/esm/gpu/ParticleGpuState.js.map +1 -1
- package/dist/esm/gpu/shaders/particle-simulate.wgsl.js +6 -0
- package/dist/esm/gpu/shaders/particle-simulate.wgsl.js.map +1 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +39 -32
- package/dist/esm/modules/AlphaFadeOverLifetime.d.ts +4 -3
- package/dist/esm/modules/AlphaFadeOverLifetime.d.ts.map +1 -0
- package/dist/esm/modules/AlphaFadeOverLifetime.js +68 -55
- package/dist/esm/modules/AlphaFadeOverLifetime.js.map +1 -1
- package/dist/esm/modules/ApplyForce.d.ts +4 -3
- package/dist/esm/modules/ApplyForce.d.ts.map +1 -0
- package/dist/esm/modules/ApplyForce.js +49 -43
- package/dist/esm/modules/ApplyForce.js.map +1 -1
- package/dist/esm/modules/AttractToPoint.d.ts +4 -3
- package/dist/esm/modules/AttractToPoint.d.ts.map +1 -0
- package/dist/esm/modules/AttractToPoint.js +73 -62
- package/dist/esm/modules/AttractToPoint.js.map +1 -1
- package/dist/esm/modules/BurstSpawn.d.ts +6 -18
- package/dist/esm/modules/BurstSpawn.d.ts.map +1 -0
- package/dist/esm/modules/BurstSpawn.js +56 -91
- package/dist/esm/modules/BurstSpawn.js.map +1 -1
- package/dist/esm/modules/ColorOverLifetime.d.ts +4 -3
- package/dist/esm/modules/ColorOverLifetime.d.ts.map +1 -0
- package/dist/esm/modules/ColorOverLifetime.js +65 -53
- package/dist/esm/modules/ColorOverLifetime.js.map +1 -1
- package/dist/esm/modules/ColorOverSpeed.d.ts +5 -4
- package/dist/esm/modules/ColorOverSpeed.d.ts.map +1 -0
- package/dist/esm/modules/ColorOverSpeed.js +90 -75
- package/dist/esm/modules/ColorOverSpeed.js.map +1 -1
- package/dist/esm/modules/DeathModule.d.ts +26 -10
- package/dist/esm/modules/DeathModule.d.ts.map +1 -0
- package/dist/esm/modules/DeathModule.js +38 -22
- package/dist/esm/modules/DeathModule.js.map +1 -1
- package/dist/esm/modules/Drag.d.ts +3 -2
- package/dist/esm/modules/Drag.d.ts.map +1 -0
- package/dist/esm/modules/Drag.js +43 -37
- package/dist/esm/modules/Drag.js.map +1 -1
- package/dist/esm/modules/OrbitalForce.d.ts +4 -3
- package/dist/esm/modules/OrbitalForce.d.ts.map +1 -0
- package/dist/esm/modules/OrbitalForce.js +70 -58
- package/dist/esm/modules/OrbitalForce.js.map +1 -1
- package/dist/esm/modules/RateSpawn.d.ts +7 -22
- package/dist/esm/modules/RateSpawn.d.ts.map +1 -0
- package/dist/esm/modules/RateSpawn.js +39 -71
- package/dist/esm/modules/RateSpawn.js.map +1 -1
- package/dist/esm/modules/RepelFromPoint.d.ts +4 -3
- package/dist/esm/modules/RepelFromPoint.d.ts.map +1 -0
- package/dist/esm/modules/RepelFromPoint.js +72 -62
- package/dist/esm/modules/RepelFromPoint.js.map +1 -1
- package/dist/esm/modules/RotateOverLifetime.d.ts +3 -2
- package/dist/esm/modules/RotateOverLifetime.d.ts.map +1 -0
- package/dist/esm/modules/RotateOverLifetime.js +40 -36
- package/dist/esm/modules/RotateOverLifetime.js.map +1 -1
- package/dist/esm/modules/ScaleOverLifetime.d.ts +5 -4
- package/dist/esm/modules/ScaleOverLifetime.d.ts.map +1 -0
- package/dist/esm/modules/ScaleOverLifetime.js +60 -50
- package/dist/esm/modules/ScaleOverLifetime.js.map +1 -1
- package/dist/esm/modules/SpawnModule.d.ts +15 -12
- package/dist/esm/modules/SpawnModule.d.ts.map +1 -0
- package/dist/esm/modules/SpawnModule.js +32 -28
- package/dist/esm/modules/SpawnModule.js.map +1 -1
- package/dist/esm/modules/SpawnOnDeath.d.ts +13 -9
- package/dist/esm/modules/SpawnOnDeath.d.ts.map +1 -0
- package/dist/esm/modules/SpawnOnDeath.js +41 -43
- package/dist/esm/modules/SpawnOnDeath.js.map +1 -1
- package/dist/esm/modules/Turbulence.d.ts +4 -3
- package/dist/esm/modules/Turbulence.d.ts.map +1 -0
- package/dist/esm/modules/Turbulence.js +101 -89
- package/dist/esm/modules/Turbulence.js.map +1 -1
- package/dist/esm/modules/UpdateModule.d.ts +21 -13
- package/dist/esm/modules/UpdateModule.d.ts.map +1 -0
- package/dist/esm/modules/UpdateModule.js +65 -63
- package/dist/esm/modules/UpdateModule.js.map +1 -1
- package/dist/esm/modules/VelocityOverLifetime.d.ts +7 -6
- package/dist/esm/modules/VelocityOverLifetime.d.ts.map +1 -0
- package/dist/esm/modules/VelocityOverLifetime.js +75 -73
- package/dist/esm/modules/VelocityOverLifetime.js.map +1 -1
- package/dist/esm/modules/WgslContribution.d.ts +16 -15
- package/dist/esm/modules/WgslContribution.d.ts.map +1 -0
- package/dist/esm/modules/WgslContribution.js +37 -29
- package/dist/esm/modules/WgslContribution.js.map +1 -1
- package/dist/esm/modules/index.d.ts +3 -1
- package/dist/esm/modules/index.d.ts.map +1 -0
- package/dist/esm/modules/spawnFields.d.ts +32 -0
- package/dist/esm/modules/spawnFields.d.ts.map +1 -0
- package/dist/esm/modules/spawnFields.js +31 -0
- package/dist/esm/modules/spawnFields.js.map +1 -0
- package/dist/esm/particlesBuildInfo.d.ts +1 -0
- package/dist/esm/particlesBuildInfo.d.ts.map +1 -0
- package/dist/esm/particlesBuildInfo.js +6 -4
- package/dist/esm/particlesBuildInfo.js.map +1 -1
- package/dist/esm/particlesExtension.d.ts +13 -5
- package/dist/esm/particlesExtension.d.ts.map +1 -0
- package/dist/esm/particlesExtension.js +38 -42
- package/dist/esm/particlesExtension.js.map +1 -1
- package/dist/esm/public.d.ts +11 -0
- package/dist/esm/public.d.ts.map +1 -0
- package/dist/esm/renderModes/MeshParticles.d.ts +128 -0
- package/dist/esm/renderModes/MeshParticles.d.ts.map +1 -0
- package/dist/esm/renderModes/MeshParticles.js +263 -0
- package/dist/esm/renderModes/MeshParticles.js.map +1 -0
- package/dist/esm/renderModes/ParticleBufferLayout.d.ts +59 -0
- package/dist/esm/renderModes/ParticleBufferLayout.d.ts.map +1 -0
- package/dist/esm/renderModes/ParticleBufferLayout.js +99 -0
- package/dist/esm/renderModes/ParticleBufferLayout.js.map +1 -0
- package/dist/esm/renderModes/ParticleInstanceWriter.d.ts +58 -0
- package/dist/esm/renderModes/ParticleInstanceWriter.d.ts.map +1 -0
- package/dist/esm/renderModes/ParticleInstanceWriter.js +173 -0
- package/dist/esm/renderModes/ParticleInstanceWriter.js.map +1 -0
- package/dist/esm/renderModes/ParticleMaterial.d.ts +18 -0
- package/dist/esm/renderModes/ParticleMaterial.d.ts.map +1 -0
- package/dist/esm/renderModes/ParticleMaterial.js +24 -0
- package/dist/esm/renderModes/ParticleMaterial.js.map +1 -0
- package/dist/esm/renderModes/ParticleRenderMode.d.ts +114 -0
- package/dist/esm/renderModes/ParticleRenderMode.d.ts.map +1 -0
- package/dist/esm/renderModes/ParticleRenderMode.js +99 -0
- package/dist/esm/renderModes/ParticleRenderMode.js.map +1 -0
- package/dist/esm/renderModes/QuadParticles.d.ts +55 -0
- package/dist/esm/renderModes/QuadParticles.d.ts.map +1 -0
- package/dist/esm/renderModes/QuadParticles.js +95 -0
- package/dist/esm/renderModes/QuadParticles.js.map +1 -0
- package/dist/esm/renderModes/RibbonParticles.d.ts +96 -0
- package/dist/esm/renderModes/RibbonParticles.d.ts.map +1 -0
- package/dist/esm/renderModes/RibbonParticles.js +202 -0
- package/dist/esm/renderModes/RibbonParticles.js.map +1 -0
- package/dist/esm/renderModes/shaders/mesh-particles.wgsl.js +6 -0
- package/dist/esm/renderModes/shaders/mesh-particles.wgsl.js.map +1 -0
- package/dist/esm/renderModes/shaders/mesh.vert.js +6 -0
- package/dist/esm/renderModes/shaders/mesh.vert.js.map +1 -0
- package/dist/esm/renderModes/shaders/quad-particles.wgsl.js +6 -0
- package/dist/esm/renderModes/shaders/quad-particles.wgsl.js.map +1 -0
- package/dist/esm/renderModes/shaders/ribbon-particles.wgsl.js +6 -0
- package/dist/esm/renderModes/shaders/ribbon-particles.wgsl.js.map +1 -0
- package/dist/esm/renderModes/shaders/ribbon.frag.js +6 -0
- package/dist/esm/renderModes/shaders/ribbon.frag.js.map +1 -0
- package/dist/esm/renderModes/shaders/ribbon.vert.js +6 -0
- package/dist/esm/renderModes/shaders/ribbon.vert.js.map +1 -0
- package/dist/esm/renderers/WebGl2ParticleRenderer.d.ts +48 -20
- package/dist/esm/renderers/WebGl2ParticleRenderer.d.ts.map +1 -0
- package/dist/esm/renderers/WebGl2ParticleRenderer.js +317 -321
- package/dist/esm/renderers/WebGl2ParticleRenderer.js.map +1 -1
- package/dist/esm/renderers/WebGpuParticleRenderer.d.ts +99 -18
- package/dist/esm/renderers/WebGpuParticleRenderer.d.ts.map +1 -0
- package/dist/esm/renderers/WebGpuParticleRenderer.js +542 -551
- package/dist/esm/renderers/WebGpuParticleRenderer.js.map +1 -1
- package/dist/esm/renderers/shaders/particle.frag.js +6 -0
- package/dist/esm/renderers/shaders/particle.frag.js.map +1 -0
- package/dist/esm/renderers/shaders/particle.vert.js +6 -0
- package/dist/esm/renderers/shaders/particle.vert.js.map +1 -0
- package/package.json +7 -14
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/register.d.ts +0 -1
- package/dist/esm/register.js +0 -43
- package/dist/esm/register.js.map +0 -1
- package/dist/esm/renderers/glsl/particle.frag.js +0 -4
- package/dist/esm/renderers/glsl/particle.frag.js.map +0 -1
- package/dist/esm/renderers/glsl/particle.vert.js +0 -4
- package/dist/esm/renderers/glsl/particle.vert.js.map +0 -1
|
@@ -1,417 +1,569 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getWgslUniformByteSize } from "../modules/WgslContribution.js";
|
|
2
|
+
import particle_simulate_default from "./shaders/particle-simulate.wgsl.js";
|
|
3
|
+
import { WebGpuComputePipeline, WebGpuStorageBuffer, WebGpuUniformBuffer, fillShaderSource, reflectComputeBindings } from "@codexo/exojs/renderer-sdk";
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
//#region src/gpu/ParticleGpuState.ts
|
|
4
6
|
/**
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
7
|
+
* GPU-side mirror of one {@link ParticleSystem}. Owns:
|
|
8
|
+
*
|
|
9
|
+
* - **8 packed storage buffers** for the per-particle SoA data:
|
|
10
|
+
* positions/velocities/scales/rotInfo/timing as `vec2<f32>`, color and
|
|
11
|
+
* textureIndex as `u32`, plus the instance output buffer. Sits at the
|
|
12
|
+
* default WebGPU `maxStorageBuffersPerShaderStage = 8` limit. Built on
|
|
13
|
+
* the shared {@link WebGpuStorageBuffer} SDK primitive.
|
|
14
|
+
* - **Three uniform buffers** (sim state `dt`/`liveCount`, module configs -
|
|
15
|
+
* concatenated per-module structs with WGSL std140-ish alignment - and
|
|
16
|
+
* frame UVs, `array<vec4<f32>, N>` where N is the system's frame count or
|
|
17
|
+
* 1 when no atlas is declared, each vec4 `(uvMinX, uvMinY, uvMaxX, uvMaxY)`
|
|
18
|
+
* already flipY-adjusted), built on the shared {@link WebGpuUniformBuffer}
|
|
19
|
+
* SDK primitive.
|
|
20
|
+
* - **N 1D textures** for modules that use lookup tables (Curve / ColorGradient).
|
|
21
|
+
* - **Composite compute pipeline** built once at construction by
|
|
22
|
+
* concatenating the integration step + every registered module body +
|
|
23
|
+
* the pack-instances step into a single shader, via the shared
|
|
24
|
+
* {@link WebGpuComputePipeline} SDK primitive (two bind groups: group 0
|
|
25
|
+
* holds uniforms + module lookup textures/samplers, group 1 holds the
|
|
26
|
+
* 8 SoA storage buffers). The bind-group *layouts* are derived straight
|
|
27
|
+
* from the shader's own `@group`/`@binding` declarations via
|
|
28
|
+
* {@link reflectComputeBindings} - no hand-written binding list kept in
|
|
29
|
+
* sync with the WGSL text by hand.
|
|
30
|
+
*
|
|
31
|
+
* The compute shader's pack-instances step reads `textureIndex[i]`, looks
|
|
32
|
+
* up the matching frame UV, and writes a 40-byte interleaved record into
|
|
33
|
+
* the instance output buffer (`STORAGE | VERTEX`). The renderer binds that
|
|
34
|
+
* buffer directly as instanced vertex source - no readback.
|
|
35
|
+
*/
|
|
27
36
|
const workgroupSize = 64;
|
|
28
|
-
const instanceBytes = 40;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
37
|
+
const instanceBytes = 40;
|
|
38
|
+
/** 8 x f32 (position, velocity, rotation, scale, elapsed) + 2 x u32 (color, slot). */
|
|
39
|
+
const deathRecordBytes = 40;
|
|
40
|
+
const deathStagingSlots = 3;
|
|
41
|
+
const minDeathStagingRecords = 32;
|
|
42
|
+
const deathRecordFloats = 10;
|
|
43
|
+
var ParticleGpuState = class {
|
|
44
|
+
device;
|
|
45
|
+
capacity;
|
|
46
|
+
/** GPU buffer holding interleaved per-instance vertex data, written by compute, read as VERTEX by the renderer. */
|
|
47
|
+
instanceBuffer;
|
|
48
|
+
_positions;
|
|
49
|
+
_velocities;
|
|
50
|
+
_scales;
|
|
51
|
+
_rotInfo;
|
|
52
|
+
_timing;
|
|
53
|
+
_color;
|
|
54
|
+
_instanceStorageBuffer;
|
|
55
|
+
/**
|
|
56
|
+
* Death records the compute shader appended, plus the atomic append counter
|
|
57
|
+
* in its first four bytes. Allocated only while the system has death modules,
|
|
58
|
+
* so a system without them pays neither the memory nor the copy.
|
|
59
|
+
*
|
|
60
|
+
* Detecting a death and reporting one have separate lifetimes, and the path
|
|
61
|
+
* between them has four stages: the shader appends here; records the copy has
|
|
62
|
+
* not taken yet stay here as a device-side backlog; a copy moves a batch into
|
|
63
|
+
* a staging slot; and the mapped batches are handed on in submission order.
|
|
64
|
+
*/
|
|
65
|
+
_deathBuffer = null;
|
|
66
|
+
_deathCounterReset = /* @__PURE__ */ new Uint32Array(1);
|
|
67
|
+
/**
|
|
68
|
+
* Readback slots for the death buffer. A slot stays unavailable from the
|
|
69
|
+
* submit that copies into it until its map resolves, so a single slot would
|
|
70
|
+
* make every death that happens while a readback is in flight either a
|
|
71
|
+
* validation error or a lost record. Three slots cover the queue depth a
|
|
72
|
+
* frame loop runs at; a fourth death batch waits on the device instead.
|
|
73
|
+
*/
|
|
74
|
+
_deathStaging = [];
|
|
75
|
+
/** Copies submitted into a staging slot, oldest first, awaiting delivery. */
|
|
76
|
+
_stagedDeaths = [];
|
|
77
|
+
/**
|
|
78
|
+
* Whether the death buffer holds records no readback has claimed yet. The
|
|
79
|
+
* append counter is reset only once a copy has taken them, so a step that
|
|
80
|
+
* finds no free staging slot leaves its records in place and the next step
|
|
81
|
+
* appends behind them.
|
|
82
|
+
*/
|
|
83
|
+
_deathBufferDirty = false;
|
|
84
|
+
/** Tail of the delivery chain, so batches are reported in submission order. */
|
|
85
|
+
_deathDelivery = Promise.resolve();
|
|
86
|
+
_simUniformBuffer;
|
|
87
|
+
_simUniformData = /* @__PURE__ */ new ArrayBuffer(16);
|
|
88
|
+
_simUniformView;
|
|
89
|
+
_moduleUniformBuffer = null;
|
|
90
|
+
_moduleUniformData = null;
|
|
91
|
+
_moduleUniformView = null;
|
|
92
|
+
_moduleSlots = [];
|
|
93
|
+
_framesUniformBuffer;
|
|
94
|
+
_framesUniformData;
|
|
95
|
+
_framesUniformView;
|
|
96
|
+
_frameCount;
|
|
97
|
+
_moduleTextures = /* @__PURE__ */ new Map();
|
|
98
|
+
_samplerFiltering;
|
|
99
|
+
_samplerNonFiltering;
|
|
100
|
+
_pipelineWrapper = null;
|
|
101
|
+
_bindGroup0 = null;
|
|
102
|
+
_bindGroup1 = null;
|
|
103
|
+
_reportsDeaths = false;
|
|
104
|
+
_destroyed = false;
|
|
105
|
+
constructor(device, capacity, modules, frames, texture, reportsDeaths = false) {
|
|
106
|
+
this.device = device;
|
|
107
|
+
this.capacity = capacity;
|
|
108
|
+
this._frameCount = Math.max(1, frames.length);
|
|
109
|
+
this._framesUniformData = /* @__PURE__ */ new ArrayBuffer(this._frameCount * 16);
|
|
110
|
+
this._framesUniformView = new Float32Array(this._framesUniformData);
|
|
111
|
+
this._framesUniformBuffer = new WebGpuUniformBuffer(device, this._framesUniformData.byteLength, "particle-frames-uniforms");
|
|
112
|
+
this._writeFrames(frames, texture);
|
|
113
|
+
const vec2Bytes = capacity * 8;
|
|
114
|
+
const vec4Bytes = capacity * 16;
|
|
115
|
+
const u32Bytes = capacity * 4;
|
|
116
|
+
this._positions = new WebGpuStorageBuffer(device, vec2Bytes, "particle-positions");
|
|
117
|
+
this._velocities = new WebGpuStorageBuffer(device, vec2Bytes, "particle-velocities");
|
|
118
|
+
this._scales = new WebGpuStorageBuffer(device, vec2Bytes, "particle-scales");
|
|
119
|
+
this._rotInfo = new WebGpuStorageBuffer(device, vec4Bytes, "particle-rotInfo");
|
|
120
|
+
this._timing = new WebGpuStorageBuffer(device, vec2Bytes, "particle-timing");
|
|
121
|
+
this._color = new WebGpuStorageBuffer(device, u32Bytes, "particle-color");
|
|
122
|
+
this._instanceStorageBuffer = new WebGpuStorageBuffer(device, capacity * instanceBytes, "particle-instance-output", GPUBufferUsage.VERTEX);
|
|
123
|
+
this.instanceBuffer = this._instanceStorageBuffer.buffer;
|
|
124
|
+
this._simUniformView = new DataView(this._simUniformData);
|
|
125
|
+
this._simUniformBuffer = new WebGpuUniformBuffer(device, 16, "particle-sim-uniforms");
|
|
126
|
+
this._samplerFiltering = device.createSampler({
|
|
127
|
+
label: "particle-lookup-sampler-filtering",
|
|
128
|
+
minFilter: "linear",
|
|
129
|
+
magFilter: "linear",
|
|
130
|
+
addressModeU: "clamp-to-edge"
|
|
131
|
+
});
|
|
132
|
+
this._samplerNonFiltering = device.createSampler({
|
|
133
|
+
label: "particle-lookup-sampler-non-filtering",
|
|
134
|
+
minFilter: "nearest",
|
|
135
|
+
magFilter: "nearest",
|
|
136
|
+
addressModeU: "clamp-to-edge"
|
|
137
|
+
});
|
|
138
|
+
this.setProgram(modules, reportsDeaths);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Compiles the pipeline for `modules` and binds it to the buffers this state
|
|
142
|
+
* already owns.
|
|
143
|
+
*
|
|
144
|
+
* The simulation lives in those buffers, not in the program, so changing the
|
|
145
|
+
* module list rebuilds the shader, its uniforms and its lookup textures while
|
|
146
|
+
* every live particle keeps the position and velocity the GPU last integrated.
|
|
147
|
+
*/
|
|
148
|
+
setProgram(modules, reportsDeaths) {
|
|
149
|
+
for (const m of modules) if (!m.wgsl) throw new Error(`ParticleGpuState: module ${m.constructor.name} has no wgsl() - all registered UpdateModules must be GPU-eligible.`);
|
|
150
|
+
this._destroyProgram();
|
|
151
|
+
this._reportsDeaths = reportsDeaths;
|
|
152
|
+
const slots = [];
|
|
153
|
+
let uniformOffset = 0;
|
|
154
|
+
for (const m of modules) {
|
|
155
|
+
const c = m.wgsl();
|
|
156
|
+
const fields = c.uniforms ?? [];
|
|
157
|
+
const size = getWgslUniformByteSize(fields);
|
|
158
|
+
uniformOffset = Math.ceil(uniformOffset / 16) * 16;
|
|
159
|
+
slots.push({
|
|
160
|
+
module: m,
|
|
161
|
+
contribution: c,
|
|
162
|
+
uniformByteOffset: uniformOffset,
|
|
163
|
+
uniformByteSize: size
|
|
164
|
+
});
|
|
165
|
+
uniformOffset += size;
|
|
166
|
+
}
|
|
167
|
+
const totalUniformBytes = Math.max(16, Math.ceil(uniformOffset / 16) * 16);
|
|
168
|
+
this._moduleSlots = slots;
|
|
169
|
+
if (uniformOffset > 0) {
|
|
170
|
+
this._moduleUniformData = new ArrayBuffer(totalUniformBytes);
|
|
171
|
+
this._moduleUniformView = new DataView(this._moduleUniformData);
|
|
172
|
+
this._moduleUniformBuffer = new WebGpuUniformBuffer(this.device, totalUniformBytes, "particle-module-uniforms");
|
|
173
|
+
}
|
|
174
|
+
if (reportsDeaths && this._deathBuffer === null) this._deathBuffer = new WebGpuStorageBuffer(this.device, 4 + this.capacity * deathRecordBytes, "particle-deaths", GPUBufferUsage.COPY_SRC);
|
|
175
|
+
for (const slot of slots) {
|
|
176
|
+
const c = slot.contribution;
|
|
177
|
+
if (!c.textures) continue;
|
|
178
|
+
for (const t of c.textures) {
|
|
179
|
+
const tex = this.device.createTexture({
|
|
180
|
+
label: `particle-tex-${c.key}-${t.name}`,
|
|
181
|
+
size: {
|
|
182
|
+
width: 256,
|
|
183
|
+
height: 1,
|
|
184
|
+
depthOrArrayLayers: 1
|
|
185
|
+
},
|
|
186
|
+
format: t.format,
|
|
187
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
188
|
+
dimension: "1d"
|
|
189
|
+
});
|
|
190
|
+
this._moduleTextures.set(`${c.key}_${t.name}`, tex);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const wgsl = this._buildShader(slots);
|
|
194
|
+
this._pipelineWrapper = WebGpuComputePipeline.create(this.device, {
|
|
195
|
+
wgsl,
|
|
196
|
+
workgroupSize,
|
|
197
|
+
bindingGroups: reflectComputeBindings(wgsl, { nonFilteringResources: this._nonFilteringResourceNames(slots) }),
|
|
198
|
+
label: "particle-compute"
|
|
199
|
+
});
|
|
200
|
+
this._bindGroup0 = this._pipelineWrapper.createBindGroup(0, this._buildBindGroup0Entries(slots), "particle-uniforms-bg");
|
|
201
|
+
this._bindGroup1 = this._pipelineWrapper.createBindGroup(1, this._buildSoaBindGroupEntries(), "particle-soa-bg");
|
|
202
|
+
for (const slot of slots) {
|
|
203
|
+
if (!slot.module.uploadTextures) continue;
|
|
204
|
+
const moduleTextures = /* @__PURE__ */ new Map();
|
|
205
|
+
for (const t of slot.contribution.textures ?? []) {
|
|
206
|
+
const tex = this._moduleTextures.get(`${slot.contribution.key}_${t.name}`);
|
|
207
|
+
if (tex !== void 0) moduleTextures.set(t.name, tex);
|
|
208
|
+
}
|
|
209
|
+
slot.module.uploadTextures(this.device, moduleTextures);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/** Releases everything the current program owns, leaving the simulation buffers untouched. */
|
|
213
|
+
_destroyProgram() {
|
|
214
|
+
this._moduleUniformBuffer?.destroy();
|
|
215
|
+
this._moduleUniformBuffer = null;
|
|
216
|
+
this._moduleUniformData = null;
|
|
217
|
+
this._moduleUniformView = null;
|
|
218
|
+
this._moduleSlots = [];
|
|
219
|
+
for (const tex of this._moduleTextures.values()) tex.destroy();
|
|
220
|
+
this._moduleTextures.clear();
|
|
221
|
+
this._pipelineWrapper = null;
|
|
222
|
+
this._bindGroup0 = null;
|
|
223
|
+
this._bindGroup1 = null;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Runs one simulation step over `[0, dispatchCount)`.
|
|
227
|
+
*
|
|
228
|
+
* `pendingDeaths` is how many expired particles the caller is still holding,
|
|
229
|
+
* including the ones it marked for this step. The shader appends exactly one
|
|
230
|
+
* record per marked particle, so the readback copies the used prefix of the
|
|
231
|
+
* death buffer without first having to read its counter back - the count is
|
|
232
|
+
* already known on this side.
|
|
233
|
+
*
|
|
234
|
+
* Returns whether a death batch was staged for readback. Deaths stay on the
|
|
235
|
+
* device while every staging slot is in flight, so a step can report deaths
|
|
236
|
+
* without staging any, and a later step can stage a batch spanning several
|
|
237
|
+
* steps. A caller keeping per-death state has to hold it, and keep counting it
|
|
238
|
+
* into `pendingDeaths`, until a step stages - then hand over all of it.
|
|
239
|
+
*/
|
|
240
|
+
dispatch(dt, dispatchCount, pendingDeaths = 0) {
|
|
241
|
+
const pipeline = this._pipelineWrapper;
|
|
242
|
+
const bindGroup0 = this._bindGroup0;
|
|
243
|
+
const bindGroup1 = this._bindGroup1;
|
|
244
|
+
if (pipeline === null || bindGroup0 === null || bindGroup1 === null) return false;
|
|
245
|
+
const simulating = dispatchCount > 0;
|
|
246
|
+
const reporting = pendingDeaths > 0 && this._reportsDeaths && this._deathBuffer !== null;
|
|
247
|
+
if (!simulating && !reporting) return false;
|
|
248
|
+
if (simulating) {
|
|
249
|
+
this._writeSimUniforms(dt, dispatchCount);
|
|
250
|
+
this._writeModuleUniforms(dt);
|
|
251
|
+
}
|
|
252
|
+
if (reporting && !this._deathBufferDirty) {
|
|
253
|
+
this._deathCounterReset[0] = 0;
|
|
254
|
+
this._deathBuffer.write(this._deathCounterReset, 0);
|
|
255
|
+
}
|
|
256
|
+
const encoder = this.device.createCommandEncoder({ label: "particle-compute" });
|
|
257
|
+
if (simulating) {
|
|
258
|
+
const pass = encoder.beginComputePass({ label: "particle-compute-pass" });
|
|
259
|
+
pipeline.dispatch(pass, dispatchCount, [bindGroup0, bindGroup1]);
|
|
260
|
+
pass.end();
|
|
261
|
+
}
|
|
262
|
+
let staged = false;
|
|
263
|
+
if (reporting) {
|
|
264
|
+
this._deathBufferDirty = true;
|
|
265
|
+
const count = Math.min(pendingDeaths, this.capacity);
|
|
266
|
+
const slot = this._acquireDeathStaging(count);
|
|
267
|
+
if (slot !== null) {
|
|
268
|
+
encoder.copyBufferToBuffer(this._deathBuffer.buffer, 4, slot.buffer, 0, count * deathRecordBytes);
|
|
269
|
+
slot.busy = true;
|
|
270
|
+
this._stagedDeaths.push({
|
|
271
|
+
slot,
|
|
272
|
+
count
|
|
273
|
+
});
|
|
274
|
+
this._deathBufferDirty = false;
|
|
275
|
+
staged = true;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
this.device.queue.submit([encoder.finish()]);
|
|
279
|
+
return staged;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Resolves the oldest staged death batch and hands its records to `receive`
|
|
283
|
+
* once the copy has landed.
|
|
284
|
+
*
|
|
285
|
+
* The mapping is asynchronous by nature, so a death is delivered no earlier
|
|
286
|
+
* than the frame after it happened. Batches are delivered in the order they
|
|
287
|
+
* were submitted, and nothing blocks on a map: further steps keep staging
|
|
288
|
+
* into other slots while one is in flight. A device that goes away mid-map
|
|
289
|
+
* resolves to nothing rather than throwing: the deaths are lost with the
|
|
290
|
+
* simulation that produced them.
|
|
291
|
+
*/
|
|
292
|
+
readDeaths(receive) {
|
|
293
|
+
const batch = this._stagedDeaths.shift();
|
|
294
|
+
if (batch === void 0 || this._destroyed) return Promise.resolve();
|
|
295
|
+
const previous = this._deathDelivery;
|
|
296
|
+
let settled;
|
|
297
|
+
this._deathDelivery = new Promise((resolve) => {
|
|
298
|
+
settled = resolve;
|
|
299
|
+
});
|
|
300
|
+
return this._deliverDeaths(batch, previous, receive).finally(() => settled());
|
|
301
|
+
}
|
|
302
|
+
async _deliverDeaths(batch, previous, receive) {
|
|
303
|
+
const { slot, count } = batch;
|
|
304
|
+
const bytes = count * deathRecordBytes;
|
|
305
|
+
try {
|
|
306
|
+
await slot.buffer.mapAsync(GPUMapMode.READ, 0, bytes);
|
|
307
|
+
} catch {
|
|
308
|
+
slot.busy = false;
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (this._destroyed) {
|
|
312
|
+
slot.busy = false;
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const records = [];
|
|
316
|
+
const mapped = slot.buffer.getMappedRange(0, bytes);
|
|
317
|
+
const floats = new Float32Array(mapped);
|
|
318
|
+
const uints = new Uint32Array(mapped);
|
|
319
|
+
for (let i = 0; i < count; i++) {
|
|
320
|
+
const base = i * deathRecordFloats;
|
|
321
|
+
records.push({
|
|
322
|
+
x: floats[base + 0],
|
|
323
|
+
y: floats[base + 1],
|
|
324
|
+
velocityX: floats[base + 2],
|
|
325
|
+
velocityY: floats[base + 3],
|
|
326
|
+
rotation: floats[base + 4],
|
|
327
|
+
scaleX: floats[base + 5],
|
|
328
|
+
scaleY: floats[base + 6],
|
|
329
|
+
elapsed: floats[base + 7],
|
|
330
|
+
color: uints[base + 8],
|
|
331
|
+
slot: uints[base + 9]
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
slot.buffer.unmap();
|
|
335
|
+
slot.busy = false;
|
|
336
|
+
records.sort((a, b) => a.slot - b.slot);
|
|
337
|
+
await previous;
|
|
338
|
+
if (this._destroyed) return;
|
|
339
|
+
receive(records);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Claims a staging slot able to hold `records`, or null while every slot of
|
|
343
|
+
* the ring is still in flight.
|
|
344
|
+
*/
|
|
345
|
+
_acquireDeathStaging(records) {
|
|
346
|
+
let reusable = null;
|
|
347
|
+
for (const slot of this._deathStaging) {
|
|
348
|
+
if (slot.busy) continue;
|
|
349
|
+
if (slot.records >= records) return slot;
|
|
350
|
+
reusable = slot;
|
|
351
|
+
}
|
|
352
|
+
if (reusable === null && this._deathStaging.length >= deathStagingSlots) return null;
|
|
353
|
+
const size = Math.max(records, (reusable?.records ?? 0) * 2, minDeathStagingRecords);
|
|
354
|
+
const buffer = this.device.createBuffer({
|
|
355
|
+
label: "particle-deaths-staging",
|
|
356
|
+
size: size * deathRecordBytes,
|
|
357
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
358
|
+
});
|
|
359
|
+
if (reusable !== null) {
|
|
360
|
+
reusable.buffer.destroy();
|
|
361
|
+
reusable.buffer = buffer;
|
|
362
|
+
reusable.records = size;
|
|
363
|
+
return reusable;
|
|
364
|
+
}
|
|
365
|
+
const slot = {
|
|
366
|
+
buffer,
|
|
367
|
+
records: size,
|
|
368
|
+
busy: false
|
|
369
|
+
};
|
|
370
|
+
this._deathStaging.push(slot);
|
|
371
|
+
return slot;
|
|
372
|
+
}
|
|
373
|
+
_releaseDeathStaging() {
|
|
374
|
+
for (const slot of this._deathStaging) slot.buffer.destroy();
|
|
375
|
+
this._deathStaging.length = 0;
|
|
376
|
+
this._stagedDeaths.length = 0;
|
|
377
|
+
this._deathBufferDirty = false;
|
|
378
|
+
}
|
|
379
|
+
destroy() {
|
|
380
|
+
this._destroyed = true;
|
|
381
|
+
this._destroyProgram();
|
|
382
|
+
this._positions.destroy();
|
|
383
|
+
this._velocities.destroy();
|
|
384
|
+
this._scales.destroy();
|
|
385
|
+
this._rotInfo.destroy();
|
|
386
|
+
this._timing.destroy();
|
|
387
|
+
this._color.destroy();
|
|
388
|
+
this._instanceStorageBuffer.destroy();
|
|
389
|
+
this._simUniformBuffer.destroy();
|
|
390
|
+
this._framesUniformBuffer.destroy();
|
|
391
|
+
this._deathBuffer?.destroy();
|
|
392
|
+
this._deathBuffer = null;
|
|
393
|
+
this._releaseDeathStaging();
|
|
394
|
+
}
|
|
395
|
+
_writeFrames(frames, texture) {
|
|
396
|
+
const view = this._framesUniformView;
|
|
397
|
+
const w = texture.width;
|
|
398
|
+
const h = texture.height;
|
|
399
|
+
const flipY = texture.flipY;
|
|
400
|
+
if (frames.length === 0) {
|
|
401
|
+
view[0] = 0;
|
|
402
|
+
view[1] = flipY ? 1 : 0;
|
|
403
|
+
view[2] = 1;
|
|
404
|
+
view[3] = flipY ? 0 : 1;
|
|
405
|
+
} else for (let i = 0; i < frames.length; i++) {
|
|
406
|
+
const f = frames[i];
|
|
407
|
+
const o = i * 4;
|
|
408
|
+
const minU = f.left / w;
|
|
409
|
+
const maxU = f.right / w;
|
|
410
|
+
const topV = f.top / h;
|
|
411
|
+
const bottomV = f.bottom / h;
|
|
412
|
+
view[o + 0] = minU;
|
|
413
|
+
view[o + 1] = flipY ? bottomV : topV;
|
|
414
|
+
view[o + 2] = maxU;
|
|
415
|
+
view[o + 3] = flipY ? topV : bottomV;
|
|
416
|
+
}
|
|
417
|
+
this._framesUniformBuffer.write(this._framesUniformView);
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Push the listed CPU SoA slots to the GPU. Called by `ParticleSystem`
|
|
421
|
+
* with newly-spawned slots and just-expired slots (lifetime sentinel).
|
|
422
|
+
* Slots not in the dirty set are left alone - GPU keeps the integrated
|
|
423
|
+
* state from previous compute dispatches.
|
|
424
|
+
*
|
|
425
|
+
* Each dirty slot triggers 7 small writes (one per SoA channel, via
|
|
426
|
+
* {@link WebGpuStorageBuffer.write}). For typical spawn rates (≤200/s)
|
|
427
|
+
* this is negligible (≤1400 calls/s); contiguous-range batching is a
|
|
428
|
+
* future optimisation.
|
|
429
|
+
*/
|
|
430
|
+
uploadDirty(system, slots) {
|
|
431
|
+
const scratch2 = this._dirtyScratchVec2;
|
|
432
|
+
const scratch4 = this._dirtyScratchVec4;
|
|
433
|
+
const scratch1 = this._dirtyScratchU32;
|
|
434
|
+
const storage = system._storage;
|
|
435
|
+
for (const slot of slots) {
|
|
436
|
+
const byteOffset2 = slot * 8;
|
|
437
|
+
const byteOffset1 = slot * 4;
|
|
438
|
+
scratch2[0] = storage.posX[slot];
|
|
439
|
+
scratch2[1] = storage.posY[slot];
|
|
440
|
+
this._positions.write(scratch2, byteOffset2);
|
|
441
|
+
scratch2[0] = storage.velX[slot];
|
|
442
|
+
scratch2[1] = storage.velY[slot];
|
|
443
|
+
this._velocities.write(scratch2, byteOffset2);
|
|
444
|
+
scratch2[0] = storage.scaleX[slot];
|
|
445
|
+
scratch2[1] = storage.scaleY[slot];
|
|
446
|
+
this._scales.write(scratch2, byteOffset2);
|
|
447
|
+
scratch4[0] = storage.rotations[slot];
|
|
448
|
+
scratch4[1] = storage.rotationSpeeds[slot];
|
|
449
|
+
scratch4[2] = storage.frame[slot];
|
|
450
|
+
scratch4[3] = 0;
|
|
451
|
+
this._rotInfo.write(scratch4, slot * 16);
|
|
452
|
+
scratch2[0] = storage.elapsed[slot];
|
|
453
|
+
scratch2[1] = storage.lifetime[slot];
|
|
454
|
+
this._timing.write(scratch2, byteOffset2);
|
|
455
|
+
scratch1[0] = storage.color[slot];
|
|
456
|
+
this._color.write(scratch1, byteOffset1);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Marks `slot` expired for the device without touching anything else about
|
|
461
|
+
* it. Only the lifetime lane is written: a full slot upload would push the
|
|
462
|
+
* CPU's stale position and velocity over the values the device integrated,
|
|
463
|
+
* and those are exactly what the death record is supposed to carry.
|
|
464
|
+
*/
|
|
465
|
+
uploadExpiry(slot) {
|
|
466
|
+
this._expiryScratch[0] = -1;
|
|
467
|
+
this._timing.write(this._expiryScratch, slot * 8 + 4);
|
|
468
|
+
}
|
|
469
|
+
_expiryScratch = /* @__PURE__ */ new Float32Array(1);
|
|
470
|
+
_dirtyScratchVec2 = /* @__PURE__ */ new Float32Array(2);
|
|
471
|
+
_dirtyScratchVec4 = /* @__PURE__ */ new Float32Array(4);
|
|
472
|
+
_dirtyScratchU32 = /* @__PURE__ */ new Uint32Array(1);
|
|
473
|
+
_writeSimUniforms(dt, liveCount) {
|
|
474
|
+
this._simUniformView.setFloat32(0, dt, true);
|
|
475
|
+
this._simUniformView.setUint32(4, liveCount, true);
|
|
476
|
+
this._simUniformBuffer.write(this._simUniformView);
|
|
477
|
+
}
|
|
478
|
+
_writeModuleUniforms(dt) {
|
|
479
|
+
if (this._moduleUniformView === null || this._moduleUniformBuffer === null || this._moduleUniformData === null) return;
|
|
480
|
+
for (const slot of this._moduleSlots) slot.module.writeUniforms?.(this._moduleUniformView, slot.uniformByteOffset, dt);
|
|
481
|
+
this._moduleUniformBuffer.write(this._moduleUniformView);
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* WGSL variable names of module lookup textures/samplers whose format isn't natively
|
|
485
|
+
* filterable (`r32float` Curve LUTs) - fed to {@link reflectComputeBindings} so it declares
|
|
486
|
+
* `'unfilterable-float'`/`'non-filtering'` for those specific bindings instead of the default
|
|
487
|
+
* `'float'`/`'filtering'`, an ambiguity the WGSL text itself can't resolve (see that
|
|
488
|
+
* function's doc comment).
|
|
489
|
+
*/
|
|
490
|
+
_nonFilteringResourceNames(slots) {
|
|
491
|
+
const names = /* @__PURE__ */ new Set();
|
|
492
|
+
for (const slot of slots) for (const t of slot.contribution.textures ?? []) if (t.format === "r32float") {
|
|
493
|
+
names.add(`u_${slot.contribution.key}_${t.name}`);
|
|
494
|
+
names.add(`u_${slot.contribution.key}_${t.name}_sampler`);
|
|
495
|
+
}
|
|
496
|
+
return names;
|
|
497
|
+
}
|
|
498
|
+
/** Group-0 bind-group entries, matching group 0's bindings (reflected from the shader text) one-to-one. */
|
|
499
|
+
_buildBindGroup0Entries(slots) {
|
|
500
|
+
const entries = [{
|
|
501
|
+
binding: 0,
|
|
502
|
+
buffer: this._simUniformBuffer.buffer
|
|
503
|
+
}, {
|
|
504
|
+
binding: 1,
|
|
505
|
+
buffer: this._framesUniformBuffer.buffer
|
|
506
|
+
}];
|
|
507
|
+
if (this._moduleUniformBuffer !== null) entries.push({
|
|
508
|
+
binding: 2,
|
|
509
|
+
buffer: this._moduleUniformBuffer.buffer
|
|
510
|
+
});
|
|
511
|
+
let textureBindingIndex = this._moduleUniformBuffer !== null ? 3 : 2;
|
|
512
|
+
for (const slot of slots) for (const t of slot.contribution.textures ?? []) {
|
|
513
|
+
const tex = this._moduleTextures.get(`${slot.contribution.key}_${t.name}`);
|
|
514
|
+
const sampler = t.format !== "r32float" ? this._samplerFiltering : this._samplerNonFiltering;
|
|
515
|
+
entries.push({
|
|
516
|
+
binding: textureBindingIndex++,
|
|
517
|
+
textureView: tex.createView({ dimension: "1d" })
|
|
518
|
+
});
|
|
519
|
+
entries.push({
|
|
520
|
+
binding: textureBindingIndex++,
|
|
521
|
+
sampler
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
return entries;
|
|
525
|
+
}
|
|
526
|
+
/** Group-1 bind-group entries: the 8 SoA storage buffers, matching group 1's bindings (reflected from the shader text) one-to-one. */
|
|
527
|
+
_buildSoaBindGroupEntries() {
|
|
528
|
+
const entries = [
|
|
529
|
+
{
|
|
530
|
+
binding: 0,
|
|
531
|
+
buffer: this._positions.buffer
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
binding: 1,
|
|
535
|
+
buffer: this._velocities.buffer
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
binding: 2,
|
|
539
|
+
buffer: this._scales.buffer
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
binding: 3,
|
|
543
|
+
buffer: this._rotInfo.buffer
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
binding: 4,
|
|
547
|
+
buffer: this._timing.buffer
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
binding: 5,
|
|
551
|
+
buffer: this._color.buffer
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
binding: 6,
|
|
555
|
+
buffer: this._instanceStorageBuffer.buffer
|
|
556
|
+
}
|
|
557
|
+
];
|
|
558
|
+
if (this._reportsDeaths && this._deathBuffer !== null) entries.push({
|
|
559
|
+
binding: 7,
|
|
560
|
+
buffer: this._deathBuffer.buffer
|
|
561
|
+
});
|
|
562
|
+
return entries;
|
|
563
|
+
}
|
|
564
|
+
_buildShader(slots) {
|
|
565
|
+
const sections = [];
|
|
566
|
+
sections.push(`
|
|
415
567
|
struct SimUniforms {
|
|
416
568
|
dt: f32,
|
|
417
569
|
liveCount: u32,
|
|
@@ -426,110 +578,116 @@ struct FrameUniforms {
|
|
|
426
578
|
@group(0) @binding(0) var<uniform> sim: SimUniforms;
|
|
427
579
|
@group(0) @binding(1) var<uniform> frameUv: FrameUniforms;
|
|
428
580
|
`);
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
439
|
-
if (moduleStructFields.length > 0) {
|
|
440
|
-
sections.push(`
|
|
581
|
+
const moduleStructFields = [];
|
|
582
|
+
for (const slot of slots) {
|
|
583
|
+
const c = slot.contribution;
|
|
584
|
+
const fields = c.uniforms ?? [];
|
|
585
|
+
if (fields.length === 0) continue;
|
|
586
|
+
sections.push(this._renderModuleStruct(c.key, fields));
|
|
587
|
+
moduleStructFields.push(`u_${c.key}: ${c.key}Uniforms,`);
|
|
588
|
+
}
|
|
589
|
+
if (moduleStructFields.length > 0) sections.push(`
|
|
441
590
|
struct ModuleUniforms {
|
|
442
|
-
${moduleStructFields.map(s => ` ${s}`).join(
|
|
591
|
+
${moduleStructFields.map((s) => ` ${s}`).join("\n")}
|
|
443
592
|
}
|
|
444
593
|
|
|
445
594
|
@group(0) @binding(2) var<uniform> modules: ModuleUniforms;
|
|
446
595
|
`);
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
for (const slot of slots) {
|
|
450
|
-
for (const t of slot.contribution.textures ?? []) {
|
|
451
|
-
sections.push(`
|
|
596
|
+
let textureBindingIndex = moduleStructFields.length > 0 ? 3 : 2;
|
|
597
|
+
for (const slot of slots) for (const t of slot.contribution.textures ?? []) sections.push(`
|
|
452
598
|
@group(0) @binding(${textureBindingIndex++}) var u_${slot.contribution.key}_${t.name}: texture_1d<f32>;
|
|
453
599
|
@group(0) @binding(${textureBindingIndex++}) var u_${slot.contribution.key}_${t.name}_sampler: sampler;
|
|
454
600
|
`);
|
|
455
|
-
|
|
456
|
-
}
|
|
457
|
-
sections.push(`
|
|
601
|
+
sections.push(`
|
|
458
602
|
@group(1) @binding(0) var<storage, read_write> positions: array<vec2<f32>>;
|
|
459
603
|
@group(1) @binding(1) var<storage, read_write> velocities: array<vec2<f32>>;
|
|
460
604
|
@group(1) @binding(2) var<storage, read_write> scales: array<vec2<f32>>;
|
|
461
|
-
@group(1) @binding(3) var<storage, read_write> rotInfo: array<
|
|
605
|
+
@group(1) @binding(3) var<storage, read_write> rotInfo: array<vec4<f32>>;
|
|
462
606
|
@group(1) @binding(4) var<storage, read_write> timing: array<vec2<f32>>;
|
|
463
607
|
@group(1) @binding(5) var<storage, read_write> color: array<u32>;
|
|
464
|
-
@group(1) @binding(6) var<storage,
|
|
465
|
-
@group(1) @binding(7) var<storage, read_write> instanceOutput: array<u32>;
|
|
608
|
+
@group(1) @binding(6) var<storage, read_write> instanceOutput: array<u32>;
|
|
466
609
|
`);
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
}
|
|
481
|
-
const moduleBodies = slots.map(s => s.contribution.body).join('\n');
|
|
482
|
-
const frameCountConst = this._frameCount;
|
|
483
|
-
sections.push(`
|
|
484
|
-
@compute @workgroup_size(${workgroupSize})
|
|
485
|
-
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
486
|
-
let idx = gid.x;
|
|
487
|
-
if (idx >= sim.liveCount) { return; }
|
|
488
|
-
|
|
489
|
-
let dt = sim.dt;
|
|
610
|
+
if (this._reportsDeaths) sections.push(`
|
|
611
|
+
struct DeathRecord {
|
|
612
|
+
x: f32,
|
|
613
|
+
y: f32,
|
|
614
|
+
velocityX: f32,
|
|
615
|
+
velocityY: f32,
|
|
616
|
+
rotation: f32,
|
|
617
|
+
scaleX: f32,
|
|
618
|
+
scaleY: f32,
|
|
619
|
+
elapsed: f32,
|
|
620
|
+
color: u32,
|
|
621
|
+
slot: u32,
|
|
622
|
+
}
|
|
490
623
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
for (var k: u32 = 0u; k < 10u; k++) { instanceOutput[outBaseDead + k] = 0u; }
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
624
|
+
struct DeathBuffer {
|
|
625
|
+
count: atomic<u32>,
|
|
626
|
+
records: array<DeathRecord>,
|
|
627
|
+
}
|
|
498
628
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
629
|
+
@group(1) @binding(7) var<storage, read_write> deaths: DeathBuffer;
|
|
630
|
+
`);
|
|
631
|
+
const seenPreludeKeys = /* @__PURE__ */ new Set();
|
|
632
|
+
for (const slot of slots) {
|
|
633
|
+
const prelude = slot.contribution.prelude;
|
|
634
|
+
if (prelude === void 0 || prelude.trim() === "") continue;
|
|
635
|
+
if (seenPreludeKeys.has(slot.contribution.key)) continue;
|
|
636
|
+
seenPreludeKeys.add(slot.contribution.key);
|
|
637
|
+
sections.push(prelude);
|
|
638
|
+
}
|
|
639
|
+
const moduleBodies = slots.map((s) => s.contribution.body).join("\n");
|
|
640
|
+
sections.push(`
|
|
641
|
+
${fillShaderSource(particle_simulate_default, {
|
|
642
|
+
workgroupSize,
|
|
643
|
+
moduleBodies,
|
|
644
|
+
frameCount: this._frameCount,
|
|
645
|
+
deathReport: this._deathReportSource()
|
|
646
|
+
})} `);
|
|
647
|
+
return sections.join("\n\n");
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* The block that captures a particle's state the first time the shader sees
|
|
651
|
+
* its expiry sentinel, or nothing when the system has no death modules.
|
|
652
|
+
*
|
|
653
|
+
* The CPU marks an expired particle with `lifetime = -1` and the shader
|
|
654
|
+
* rewrites that to `-2` once captured, so exactly one record is appended per
|
|
655
|
+
* death even though a dead slot is visited every frame until it is reused.
|
|
656
|
+
*/
|
|
657
|
+
_deathReportSource() {
|
|
658
|
+
if (!this._reportsDeaths) return "";
|
|
659
|
+
return `
|
|
660
|
+
if (timing[idx].y > -1.5) {
|
|
661
|
+
// The CPU found this particle expired from an elapsed time it had
|
|
662
|
+
// already advanced for this frame, so the device owes it the matching
|
|
663
|
+
// integration step before the snapshot is taken. Without it a death
|
|
664
|
+
// reported from the GPU would sit one step behind the same death
|
|
665
|
+
// reported from the CPU pipeline.
|
|
666
|
+
positions[idx] = positions[idx] + velocities[idx] * dt;
|
|
667
|
+
rotInfo[idx].x = rotInfo[idx].x + rotInfo[idx].y * dt;
|
|
668
|
+
timing[idx].x = timing[idx].x + dt;
|
|
503
669
|
|
|
504
|
-
|
|
505
|
-
${moduleBodies}
|
|
670
|
+
let at = atomicAdd(&deaths.count, 1u);
|
|
506
671
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
672
|
+
deaths.records[at].x = positions[idx].x;
|
|
673
|
+
deaths.records[at].y = positions[idx].y;
|
|
674
|
+
deaths.records[at].velocityX = velocities[idx].x;
|
|
675
|
+
deaths.records[at].velocityY = velocities[idx].y;
|
|
676
|
+
deaths.records[at].rotation = rotInfo[idx].x;
|
|
677
|
+
deaths.records[at].scaleX = scales[idx].x;
|
|
678
|
+
deaths.records[at].scaleY = scales[idx].y;
|
|
679
|
+
deaths.records[at].elapsed = timing[idx].x;
|
|
680
|
+
deaths.records[at].color = color[idx];
|
|
681
|
+
deaths.records[at].slot = idx;
|
|
510
682
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
instanceOutput[outBase + 4u] = bitcast<u32>(rotInfo[idx].x);
|
|
519
|
-
instanceOutput[outBase + 5u] = color[idx];
|
|
520
|
-
instanceOutput[outBase + 6u] = bitcast<u32>(frameUvBounds.x);
|
|
521
|
-
instanceOutput[outBase + 7u] = bitcast<u32>(frameUvBounds.y);
|
|
522
|
-
instanceOutput[outBase + 8u] = bitcast<u32>(frameUvBounds.z);
|
|
523
|
-
instanceOutput[outBase + 9u] = bitcast<u32>(frameUvBounds.w);
|
|
524
|
-
}
|
|
525
|
-
`);
|
|
526
|
-
return sections.join('\n\n');
|
|
527
|
-
}
|
|
528
|
-
_renderModuleStruct(key, fields) {
|
|
529
|
-
const lines = fields.map(f => ` ${f.name}: ${f.type},`).join('\n');
|
|
530
|
-
return `struct ${key}Uniforms {\n${lines}\n}`;
|
|
531
|
-
}
|
|
532
|
-
}
|
|
683
|
+
timing[idx].y = -2.0;
|
|
684
|
+
}`;
|
|
685
|
+
}
|
|
686
|
+
_renderModuleStruct(key, fields) {
|
|
687
|
+
return `struct ${key}Uniforms {\n${fields.map((f) => ` ${f.name}: ${f.type},`).join("\n")}\n}`;
|
|
688
|
+
}
|
|
689
|
+
};
|
|
533
690
|
|
|
691
|
+
//#endregion
|
|
534
692
|
export { ParticleGpuState };
|
|
535
|
-
//# sourceMappingURL=ParticleGpuState.js.map
|
|
693
|
+
//# sourceMappingURL=ParticleGpuState.js.map
|