@codexo/exojs-particles 0.15.3 → 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.
Files changed (211) hide show
  1. package/README.md +21 -43
  2. package/dist/esm/ParticleStorage.d.ts +188 -0
  3. package/dist/esm/ParticleStorage.d.ts.map +1 -0
  4. package/dist/esm/ParticleStorage.js +208 -0
  5. package/dist/esm/ParticleStorage.js.map +1 -0
  6. package/dist/esm/ParticleSystem.d.ts +189 -60
  7. package/dist/esm/ParticleSystem.d.ts.map +1 -0
  8. package/dist/esm/ParticleSystem.js +660 -560
  9. package/dist/esm/ParticleSystem.js.map +1 -1
  10. package/dist/esm/distributions/BoxArea.d.ts +1 -0
  11. package/dist/esm/distributions/BoxArea.d.ts.map +1 -0
  12. package/dist/esm/distributions/BoxArea.js +38 -44
  13. package/dist/esm/distributions/BoxArea.js.map +1 -1
  14. package/dist/esm/distributions/CircleArea.d.ts +1 -0
  15. package/dist/esm/distributions/CircleArea.d.ts.map +1 -0
  16. package/dist/esm/distributions/CircleArea.js +30 -28
  17. package/dist/esm/distributions/CircleArea.js.map +1 -1
  18. package/dist/esm/distributions/ColorGradient.d.ts +3 -2
  19. package/dist/esm/distributions/ColorGradient.d.ts.map +1 -0
  20. package/dist/esm/distributions/ColorGradient.js +70 -75
  21. package/dist/esm/distributions/ColorGradient.js.map +1 -1
  22. package/dist/esm/distributions/ConeDirection.d.ts +2 -1
  23. package/dist/esm/distributions/ConeDirection.d.ts.map +1 -0
  24. package/dist/esm/distributions/ConeDirection.js +41 -39
  25. package/dist/esm/distributions/ConeDirection.js.map +1 -1
  26. package/dist/esm/distributions/Constant.d.ts +1 -0
  27. package/dist/esm/distributions/Constant.d.ts.map +1 -0
  28. package/dist/esm/distributions/Constant.js +32 -32
  29. package/dist/esm/distributions/Constant.js.map +1 -1
  30. package/dist/esm/distributions/Curve.d.ts +2 -1
  31. package/dist/esm/distributions/Curve.d.ts.map +1 -0
  32. package/dist/esm/distributions/Curve.js +46 -55
  33. package/dist/esm/distributions/Curve.js.map +1 -1
  34. package/dist/esm/distributions/Distribution.d.ts +4 -3
  35. package/dist/esm/distributions/Distribution.d.ts.map +1 -0
  36. package/dist/esm/distributions/LineSegment.d.ts +1 -0
  37. package/dist/esm/distributions/LineSegment.d.ts.map +1 -0
  38. package/dist/esm/distributions/LineSegment.js +25 -23
  39. package/dist/esm/distributions/LineSegment.js.map +1 -1
  40. package/dist/esm/distributions/Range.d.ts +1 -0
  41. package/dist/esm/distributions/Range.d.ts.map +1 -0
  42. package/dist/esm/distributions/Range.js +18 -16
  43. package/dist/esm/distributions/Range.js.map +1 -1
  44. package/dist/esm/distributions/VectorRange.d.ts +1 -0
  45. package/dist/esm/distributions/VectorRange.d.ts.map +1 -0
  46. package/dist/esm/distributions/VectorRange.js +29 -27
  47. package/dist/esm/distributions/VectorRange.js.map +1 -1
  48. package/dist/esm/distributions/index.d.ts +1 -0
  49. package/dist/esm/distributions/index.d.ts.map +1 -0
  50. package/dist/esm/gpu/ParticleGpuState.d.ts +143 -18
  51. package/dist/esm/gpu/ParticleGpuState.d.ts.map +1 -0
  52. package/dist/esm/gpu/ParticleGpuState.js +655 -497
  53. package/dist/esm/gpu/ParticleGpuState.js.map +1 -1
  54. package/dist/esm/gpu/shaders/particle-simulate.wgsl.js +6 -0
  55. package/dist/esm/gpu/shaders/particle-simulate.wgsl.js.map +1 -0
  56. package/dist/esm/index.d.ts +1 -0
  57. package/dist/esm/index.d.ts.map +1 -0
  58. package/dist/esm/index.js +39 -32
  59. package/dist/esm/modules/AlphaFadeOverLifetime.d.ts +4 -3
  60. package/dist/esm/modules/AlphaFadeOverLifetime.d.ts.map +1 -0
  61. package/dist/esm/modules/AlphaFadeOverLifetime.js +68 -55
  62. package/dist/esm/modules/AlphaFadeOverLifetime.js.map +1 -1
  63. package/dist/esm/modules/ApplyForce.d.ts +4 -3
  64. package/dist/esm/modules/ApplyForce.d.ts.map +1 -0
  65. package/dist/esm/modules/ApplyForce.js +49 -43
  66. package/dist/esm/modules/ApplyForce.js.map +1 -1
  67. package/dist/esm/modules/AttractToPoint.d.ts +4 -3
  68. package/dist/esm/modules/AttractToPoint.d.ts.map +1 -0
  69. package/dist/esm/modules/AttractToPoint.js +73 -62
  70. package/dist/esm/modules/AttractToPoint.js.map +1 -1
  71. package/dist/esm/modules/BurstSpawn.d.ts +6 -18
  72. package/dist/esm/modules/BurstSpawn.d.ts.map +1 -0
  73. package/dist/esm/modules/BurstSpawn.js +56 -91
  74. package/dist/esm/modules/BurstSpawn.js.map +1 -1
  75. package/dist/esm/modules/ColorOverLifetime.d.ts +4 -3
  76. package/dist/esm/modules/ColorOverLifetime.d.ts.map +1 -0
  77. package/dist/esm/modules/ColorOverLifetime.js +65 -53
  78. package/dist/esm/modules/ColorOverLifetime.js.map +1 -1
  79. package/dist/esm/modules/ColorOverSpeed.d.ts +5 -4
  80. package/dist/esm/modules/ColorOverSpeed.d.ts.map +1 -0
  81. package/dist/esm/modules/ColorOverSpeed.js +90 -75
  82. package/dist/esm/modules/ColorOverSpeed.js.map +1 -1
  83. package/dist/esm/modules/DeathModule.d.ts +26 -10
  84. package/dist/esm/modules/DeathModule.d.ts.map +1 -0
  85. package/dist/esm/modules/DeathModule.js +38 -22
  86. package/dist/esm/modules/DeathModule.js.map +1 -1
  87. package/dist/esm/modules/Drag.d.ts +3 -2
  88. package/dist/esm/modules/Drag.d.ts.map +1 -0
  89. package/dist/esm/modules/Drag.js +43 -37
  90. package/dist/esm/modules/Drag.js.map +1 -1
  91. package/dist/esm/modules/OrbitalForce.d.ts +4 -3
  92. package/dist/esm/modules/OrbitalForce.d.ts.map +1 -0
  93. package/dist/esm/modules/OrbitalForce.js +70 -58
  94. package/dist/esm/modules/OrbitalForce.js.map +1 -1
  95. package/dist/esm/modules/RateSpawn.d.ts +7 -22
  96. package/dist/esm/modules/RateSpawn.d.ts.map +1 -0
  97. package/dist/esm/modules/RateSpawn.js +39 -71
  98. package/dist/esm/modules/RateSpawn.js.map +1 -1
  99. package/dist/esm/modules/RepelFromPoint.d.ts +4 -3
  100. package/dist/esm/modules/RepelFromPoint.d.ts.map +1 -0
  101. package/dist/esm/modules/RepelFromPoint.js +72 -62
  102. package/dist/esm/modules/RepelFromPoint.js.map +1 -1
  103. package/dist/esm/modules/RotateOverLifetime.d.ts +3 -2
  104. package/dist/esm/modules/RotateOverLifetime.d.ts.map +1 -0
  105. package/dist/esm/modules/RotateOverLifetime.js +40 -36
  106. package/dist/esm/modules/RotateOverLifetime.js.map +1 -1
  107. package/dist/esm/modules/ScaleOverLifetime.d.ts +5 -4
  108. package/dist/esm/modules/ScaleOverLifetime.d.ts.map +1 -0
  109. package/dist/esm/modules/ScaleOverLifetime.js +60 -50
  110. package/dist/esm/modules/ScaleOverLifetime.js.map +1 -1
  111. package/dist/esm/modules/SpawnModule.d.ts +15 -12
  112. package/dist/esm/modules/SpawnModule.d.ts.map +1 -0
  113. package/dist/esm/modules/SpawnModule.js +32 -28
  114. package/dist/esm/modules/SpawnModule.js.map +1 -1
  115. package/dist/esm/modules/SpawnOnDeath.d.ts +13 -9
  116. package/dist/esm/modules/SpawnOnDeath.d.ts.map +1 -0
  117. package/dist/esm/modules/SpawnOnDeath.js +41 -43
  118. package/dist/esm/modules/SpawnOnDeath.js.map +1 -1
  119. package/dist/esm/modules/Turbulence.d.ts +4 -3
  120. package/dist/esm/modules/Turbulence.d.ts.map +1 -0
  121. package/dist/esm/modules/Turbulence.js +101 -89
  122. package/dist/esm/modules/Turbulence.js.map +1 -1
  123. package/dist/esm/modules/UpdateModule.d.ts +21 -13
  124. package/dist/esm/modules/UpdateModule.d.ts.map +1 -0
  125. package/dist/esm/modules/UpdateModule.js +65 -63
  126. package/dist/esm/modules/UpdateModule.js.map +1 -1
  127. package/dist/esm/modules/VelocityOverLifetime.d.ts +7 -6
  128. package/dist/esm/modules/VelocityOverLifetime.d.ts.map +1 -0
  129. package/dist/esm/modules/VelocityOverLifetime.js +75 -73
  130. package/dist/esm/modules/VelocityOverLifetime.js.map +1 -1
  131. package/dist/esm/modules/WgslContribution.d.ts +16 -15
  132. package/dist/esm/modules/WgslContribution.d.ts.map +1 -0
  133. package/dist/esm/modules/WgslContribution.js +37 -29
  134. package/dist/esm/modules/WgslContribution.js.map +1 -1
  135. package/dist/esm/modules/index.d.ts +3 -1
  136. package/dist/esm/modules/index.d.ts.map +1 -0
  137. package/dist/esm/modules/spawnFields.d.ts +32 -0
  138. package/dist/esm/modules/spawnFields.d.ts.map +1 -0
  139. package/dist/esm/modules/spawnFields.js +31 -0
  140. package/dist/esm/modules/spawnFields.js.map +1 -0
  141. package/dist/esm/particlesBuildInfo.d.ts +1 -0
  142. package/dist/esm/particlesBuildInfo.d.ts.map +1 -0
  143. package/dist/esm/particlesBuildInfo.js +6 -4
  144. package/dist/esm/particlesBuildInfo.js.map +1 -1
  145. package/dist/esm/particlesExtension.d.ts +13 -5
  146. package/dist/esm/particlesExtension.d.ts.map +1 -0
  147. package/dist/esm/particlesExtension.js +38 -42
  148. package/dist/esm/particlesExtension.js.map +1 -1
  149. package/dist/esm/public.d.ts +11 -0
  150. package/dist/esm/public.d.ts.map +1 -0
  151. package/dist/esm/renderModes/MeshParticles.d.ts +128 -0
  152. package/dist/esm/renderModes/MeshParticles.d.ts.map +1 -0
  153. package/dist/esm/renderModes/MeshParticles.js +263 -0
  154. package/dist/esm/renderModes/MeshParticles.js.map +1 -0
  155. package/dist/esm/renderModes/ParticleBufferLayout.d.ts +59 -0
  156. package/dist/esm/renderModes/ParticleBufferLayout.d.ts.map +1 -0
  157. package/dist/esm/renderModes/ParticleBufferLayout.js +99 -0
  158. package/dist/esm/renderModes/ParticleBufferLayout.js.map +1 -0
  159. package/dist/esm/renderModes/ParticleInstanceWriter.d.ts +58 -0
  160. package/dist/esm/renderModes/ParticleInstanceWriter.d.ts.map +1 -0
  161. package/dist/esm/renderModes/ParticleInstanceWriter.js +173 -0
  162. package/dist/esm/renderModes/ParticleInstanceWriter.js.map +1 -0
  163. package/dist/esm/renderModes/ParticleMaterial.d.ts +18 -0
  164. package/dist/esm/renderModes/ParticleMaterial.d.ts.map +1 -0
  165. package/dist/esm/renderModes/ParticleMaterial.js +24 -0
  166. package/dist/esm/renderModes/ParticleMaterial.js.map +1 -0
  167. package/dist/esm/renderModes/ParticleRenderMode.d.ts +114 -0
  168. package/dist/esm/renderModes/ParticleRenderMode.d.ts.map +1 -0
  169. package/dist/esm/renderModes/ParticleRenderMode.js +99 -0
  170. package/dist/esm/renderModes/ParticleRenderMode.js.map +1 -0
  171. package/dist/esm/renderModes/QuadParticles.d.ts +55 -0
  172. package/dist/esm/renderModes/QuadParticles.d.ts.map +1 -0
  173. package/dist/esm/renderModes/QuadParticles.js +95 -0
  174. package/dist/esm/renderModes/QuadParticles.js.map +1 -0
  175. package/dist/esm/renderModes/RibbonParticles.d.ts +96 -0
  176. package/dist/esm/renderModes/RibbonParticles.d.ts.map +1 -0
  177. package/dist/esm/renderModes/RibbonParticles.js +202 -0
  178. package/dist/esm/renderModes/RibbonParticles.js.map +1 -0
  179. package/dist/esm/renderModes/shaders/mesh-particles.wgsl.js +6 -0
  180. package/dist/esm/renderModes/shaders/mesh-particles.wgsl.js.map +1 -0
  181. package/dist/esm/renderModes/shaders/mesh.vert.js +6 -0
  182. package/dist/esm/renderModes/shaders/mesh.vert.js.map +1 -0
  183. package/dist/esm/renderModes/shaders/quad-particles.wgsl.js +6 -0
  184. package/dist/esm/renderModes/shaders/quad-particles.wgsl.js.map +1 -0
  185. package/dist/esm/renderModes/shaders/ribbon-particles.wgsl.js +6 -0
  186. package/dist/esm/renderModes/shaders/ribbon-particles.wgsl.js.map +1 -0
  187. package/dist/esm/renderModes/shaders/ribbon.frag.js +6 -0
  188. package/dist/esm/renderModes/shaders/ribbon.frag.js.map +1 -0
  189. package/dist/esm/renderModes/shaders/ribbon.vert.js +6 -0
  190. package/dist/esm/renderModes/shaders/ribbon.vert.js.map +1 -0
  191. package/dist/esm/renderers/WebGl2ParticleRenderer.d.ts +48 -20
  192. package/dist/esm/renderers/WebGl2ParticleRenderer.d.ts.map +1 -0
  193. package/dist/esm/renderers/WebGl2ParticleRenderer.js +317 -321
  194. package/dist/esm/renderers/WebGl2ParticleRenderer.js.map +1 -1
  195. package/dist/esm/renderers/WebGpuParticleRenderer.d.ts +99 -18
  196. package/dist/esm/renderers/WebGpuParticleRenderer.d.ts.map +1 -0
  197. package/dist/esm/renderers/WebGpuParticleRenderer.js +542 -551
  198. package/dist/esm/renderers/WebGpuParticleRenderer.js.map +1 -1
  199. package/dist/esm/renderers/shaders/particle.frag.js +6 -0
  200. package/dist/esm/renderers/shaders/particle.frag.js.map +1 -0
  201. package/dist/esm/renderers/shaders/particle.vert.js +6 -0
  202. package/dist/esm/renderers/shaders/particle.vert.js.map +1 -0
  203. package/package.json +8 -15
  204. package/dist/esm/index.js.map +0 -1
  205. package/dist/esm/register.d.ts +0 -1
  206. package/dist/esm/register.js +0 -43
  207. package/dist/esm/register.js.map +0 -1
  208. package/dist/esm/renderers/glsl/particle.frag.js +0 -4
  209. package/dist/esm/renderers/glsl/particle.frag.js.map +0 -1
  210. package/dist/esm/renderers/glsl/particle.vert.js +0 -4
  211. package/dist/esm/renderers/glsl/particle.vert.js.map +0 -1
@@ -1,557 +1,548 @@
1
- import { AbstractWebGpuRenderer, Texture, getWebGpuBlendState, stencilContentDepthStencilState } from '@codexo/exojs/renderer-sdk';
1
+ import { assertVertexGeometryCompatible } from "../renderModes/ParticleBufferLayout.js";
2
+ import { AbstractWebGpuRenderer, DataTexture, Texture, getWebGpuBlendState, stencilContentDepthStencilState } from "@codexo/exojs/renderer-sdk";
2
3
 
3
- /// <reference types="@webgpu/types" />
4
- const particleShaderSource = `
5
- struct ProjectionUniforms {
6
- projection: mat4x4<f32>,
7
- translation: mat4x4<f32>,
8
- flags: vec4<f32>,
9
- localBounds: vec4<f32>, // quadMin.xy, quadSize.xy
10
- uvBounds: vec4<f32>, // uvMin.xy, uvMax.xy
4
+ //#region src/renderers/WebGpuParticleRenderer.ts
5
+ const uniformByteLength = 176;
6
+ /**
7
+ * Stride of one draw call's slot in the uniform ring. `setBindGroup`'s dynamic
8
+ * offset must be a multiple of the device's `minUniformBufferOffsetAlignment`,
9
+ * whose spec-mandated maximum is 256 - so a fixed 256 is valid on every device
10
+ * and needs no limit query.
11
+ */
12
+ const uniformSlotStride = 256;
13
+ /**
14
+ * WebGPU vertex formats by `<n?><type>x<size>` key, where the leading `n`
15
+ * marks a normalised integer attribute. WebGPU has no 1- or 3-component 8/16-bit
16
+ * formats, so those combinations are deliberately absent and reported as an
17
+ * error rather than silently widened.
18
+ */
19
+ const vertexFormatsByKey = {
20
+ f32x1: "float32",
21
+ f32x2: "float32x2",
22
+ f32x3: "float32x3",
23
+ f32x4: "float32x4",
24
+ u8x2: "uint8x2",
25
+ u8x4: "uint8x4",
26
+ nu8x2: "unorm8x2",
27
+ nu8x4: "unorm8x4",
28
+ u16x2: "uint16x2",
29
+ u16x4: "uint16x4",
30
+ nu16x2: "unorm16x2",
31
+ nu16x4: "unorm16x4",
32
+ u32x1: "uint32",
33
+ u32x2: "uint32x2",
34
+ u32x3: "uint32x3",
35
+ u32x4: "uint32x4",
36
+ i32x1: "sint32",
37
+ i32x2: "sint32x2",
38
+ i32x3: "sint32x3",
39
+ i32x4: "sint32x4"
11
40
  };
12
-
13
- @group(0) @binding(0)
14
- var<uniform> uniforms: ProjectionUniforms;
15
-
16
- @group(1) @binding(0)
17
- var particleTexture: texture_2d<f32>;
18
-
19
- @group(1) @binding(1)
20
- var particleSampler: sampler;
21
-
22
- // Per-instance attributes (one entry per particle, 40 bytes total).
23
- struct VertexInput {
24
- @location(0) unitPosition: vec2<f32>, // per-vertex (static unit quad)
25
- @location(1) translation: vec2<f32>,
26
- @location(2) scale: vec2<f32>,
27
- @location(3) rotation: f32,
28
- @location(4) color: vec4<f32>,
29
- @location(5) uvMin: vec2<f32>, // pre-resolved frame UV (top-left)
30
- @location(6) uvMax: vec2<f32>, // pre-resolved frame UV (bottom-right)
41
+ const resolveVertexFormat = (attribute) => {
42
+ const normalized = attribute.normalized && attribute.type !== "f32";
43
+ const format = vertexFormatsByKey[`${normalized ? "n" : ""}${attribute.type}x${attribute.size}`];
44
+ if (format === void 0) throw new Error(`WebGpuParticleRenderer: attribute "${attribute.name}" (${attribute.type} x${attribute.size}) has no WebGPU vertex format.`);
45
+ return format;
31
46
  };
32
-
33
- struct VertexOutput {
34
- @builtin(position) position: vec4<f32>,
35
- @location(0) texcoord: vec2<f32>,
36
- @location(1) color: vec4<f32>,
47
+ /**
48
+ * Particle renderer for WebGPU.
49
+ *
50
+ * One ParticleSystem = one draw call. The system's {@link ParticleRenderMode}
51
+ * owns the *how* - vertex layout, shader, draw model and the loop that fills
52
+ * the buffer - and this renderer is the executor: it holds the system-level
53
+ * uniforms (projection, transform, local bounds, texture flags), uploads what
54
+ * the mode built and issues the draw the mode declares.
55
+ *
56
+ * Everything mode-specific is read off the mode's `dataLayout`/`Material`
57
+ * rather than hard-coded: the render pipeline's vertex layout comes from the
58
+ * layout's attributes and stride, its shader from the material's WGSL, and the
59
+ * draw is instanced or plain per `ParticleRenderMode.instanced`. A mode
60
+ * declaring a `vertexGeometry` gets a second buffer stepping per vertex, and
61
+ * that geometry supplies the topology and indices instead.
62
+ *
63
+ * WGSL binds vertex inputs by number rather than by name, and `GeometryAttribute`
64
+ * carries no location, so the binding rule is positional: `@location(i)` is
65
+ * `dataLayout.attributes[i]`, with any per-vertex attributes taking the
66
+ * locations after them. This is the WebGPU counterpart of the WebGL2 renderer's
67
+ * name lookup through the compiled program.
68
+ *
69
+ * A system running in GPU compute mode bypasses the mode's builder entirely:
70
+ * its compute pipeline has already written the interleaved instance data
71
+ * GPU-side, so that buffer is bound directly.
72
+ */
73
+ var WebGpuParticleRenderer = class extends AbstractWebGpuRenderer {
74
+ /**
75
+ * The particle system's transform is bound as a uniform and each particle is
76
+ * positioned system-locally, so this renderer never reads the shared transform
77
+ * storage; the plan player skips writing transform records for particle draws.
78
+ * @internal
79
+ */
80
+ _consumesSharedTransform = false;
81
+ _drawCalls = [];
82
+ _drawCallCount = 0;
83
+ _uniformData = new Float32Array(uniformByteLength / Float32Array.BYTES_PER_ELEMENT);
84
+ _device = null;
85
+ _uniformBindGroupLayout = null;
86
+ _textureBindGroupLayout = null;
87
+ _pipelineLayout = null;
88
+ _uniformBuffer = null;
89
+ _uniformBindGroup = null;
90
+ _uniformBufferCapacity = 0;
91
+ /**
92
+ * Slots of the uniform ring the open pass has consumed, and the pass they are
93
+ * scoped to. One draw call takes one slot; the base is added as the bind
94
+ * group's dynamic offset, so consecutive flushes append instead of all
95
+ * rewriting slot 0. Same identity-comparison rule as
96
+ * {@link ParticleModeResources.passRef}, one level up: the ring is shared by
97
+ * every mode this renderer draws.
98
+ */
99
+ _uniformPassSlots = 0;
100
+ _ownDrawsPass = null;
101
+ _resources = /* @__PURE__ */ new Map();
102
+ /**
103
+ * Materials this renderer already registered a dispose listener on.
104
+ *
105
+ * `Material` has no unsubscribe, and a disconnect clears {@link _resources}
106
+ * without clearing the material's callback set - so registering on every
107
+ * resource creation would leave one dead closure per material behind on
108
+ * every device-loss/reconnect cycle. The listener stays valid across those
109
+ * cycles (it resolves the entry through the map when it fires), so it is
110
+ * registered once and this set remembers that. Weak, so a material dropped
111
+ * without `destroy()` stays collectable.
112
+ */
113
+ _disposeListenerRegistered = /* @__PURE__ */ new WeakSet();
114
+ render(system) {
115
+ const backend = this._backend;
116
+ const texture = system.texture;
117
+ const awaitingImage = texture instanceof Texture && !(texture instanceof DataTexture) && texture.source === null;
118
+ if (backend === null || !(texture instanceof Texture) || awaitingImage || texture.width === 0 || texture.height === 0 || system.liveCount === 0) return;
119
+ backend.setBlendMode(system.blendMode);
120
+ const drawCallIndex = this._drawCallCount++;
121
+ const drawCall = this._drawCalls[drawCallIndex];
122
+ if (drawCall) {
123
+ drawCall.system = system;
124
+ drawCall.texture = texture;
125
+ drawCall.blendMode = system.blendMode;
126
+ } else this._drawCalls.push({
127
+ system,
128
+ texture,
129
+ blendMode: system.blendMode
130
+ });
131
+ }
132
+ flush() {
133
+ const backend = this._backend;
134
+ const device = this._device;
135
+ if (!backend || !device || this._uniformBuffer === null || this._uniformBindGroup === null) return;
136
+ if (this._drawCallCount === 0 && !backend.clearRequested) return;
137
+ const scissor = backend.getScissorRect();
138
+ const maskClipsAll = scissor !== null && (scissor.width <= 0 || scissor.height <= 0);
139
+ if (this._drawCallCount === 0 || maskClipsAll) {
140
+ if (backend.clearRequested) {
141
+ backend._passCoordinator.acquirePass();
142
+ backend._passCoordinator.endPass();
143
+ }
144
+ this._drawCallCount = 0;
145
+ return;
146
+ }
147
+ for (let drawCallIndex = 0; drawCallIndex < this._drawCallCount; drawCallIndex++) {
148
+ const drawCall = this._drawCalls[drawCallIndex];
149
+ if (drawCall.system.liveCount === 0) continue;
150
+ this._drawSystem(backend, device, drawCall);
151
+ }
152
+ this._drawCallCount = 0;
153
+ }
154
+ destroy() {
155
+ this.disconnect();
156
+ }
157
+ onConnect(backend) {
158
+ this._backend = backend;
159
+ this._device = this._backend.device;
160
+ this._uniformBindGroupLayout = this._device.createBindGroupLayout({ entries: [{
161
+ binding: 0,
162
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
163
+ buffer: {
164
+ type: "uniform",
165
+ hasDynamicOffset: true
166
+ }
167
+ }] });
168
+ this._textureBindGroupLayout = this._device.createBindGroupLayout({ entries: [{
169
+ binding: 0,
170
+ visibility: GPUShaderStage.FRAGMENT,
171
+ texture: { sampleType: "float" }
172
+ }, {
173
+ binding: 1,
174
+ visibility: GPUShaderStage.FRAGMENT,
175
+ sampler: { type: "filtering" }
176
+ }] });
177
+ this._pipelineLayout = this._device.createPipelineLayout({ bindGroupLayouts: [this._uniformBindGroupLayout, this._textureBindGroupLayout] });
178
+ this._createUniformResources(this._device, uniformSlotStride);
179
+ }
180
+ onDisconnect() {
181
+ this.flush();
182
+ for (const resources of this._resources.values()) this._destroyResources(resources);
183
+ this._resources.clear();
184
+ this._uniformBuffer?.destroy();
185
+ this._uniformBindGroup = null;
186
+ this._uniformBuffer = null;
187
+ this._uniformBufferCapacity = 0;
188
+ this._uniformPassSlots = 0;
189
+ this._ownDrawsPass = null;
190
+ this._pipelineLayout = null;
191
+ this._textureBindGroupLayout = null;
192
+ this._uniformBindGroupLayout = null;
193
+ this._device = null;
194
+ this._backend = null;
195
+ this._drawCallCount = 0;
196
+ }
197
+ /**
198
+ * Append one system's draw at the open pass's cursors and record it, leaving
199
+ * the pass open. Ends (submits) the pass first only where appending cannot
200
+ * cover the hazard - see {@link _appendWouldAlias}.
201
+ */
202
+ _drawSystem(backend, device, drawCall) {
203
+ const system = drawCall.system;
204
+ const mode = system.renderMode;
205
+ const resources = this._getOrCreateResources(mode, device);
206
+ const coordinator = backend._passCoordinator;
207
+ const gpuState = system.gpuMode ? system.gpuState : null;
208
+ let drawCount = system.liveCount;
209
+ let appendBytes = 0;
210
+ if (gpuState === null) {
211
+ mode.build(system, system._storage);
212
+ drawCount = mode.count;
213
+ appendBytes = Math.max(drawCount, 1) * resources.stride;
214
+ }
215
+ const meshGeometry = mode.vertexGeometry;
216
+ const meshSyncRequired = resources.meshBuffer !== null && meshGeometry !== null && resources.meshVersion !== meshGeometry.version;
217
+ const targetVertexBytes = this._modePassBytes(coordinator.activePass, resources) + appendBytes;
218
+ const targetUniformSlots = this._uniformPassBase(coordinator.activePass) + 1;
219
+ if (this._appendWouldAlias(backend, drawCall, resources, meshSyncRequired, targetVertexBytes, targetUniformSlots)) coordinator.endPass();
220
+ const vertexByteOffset = this._modePassBytes(coordinator.activePass, resources);
221
+ const uniformSlot = this._uniformPassBase(coordinator.activePass);
222
+ this._ensureUniformCapacity(device, targetUniformSlots);
223
+ const pipeline = this._getPipeline(resources, drawCall.blendMode, backend.renderTargetFormat, coordinator.stencilActive);
224
+ const textureBinding = backend.getTextureBinding(drawCall.texture, mode.material.sampler);
225
+ const textureBindGroup = device.createBindGroup({
226
+ layout: this._textureBindGroupLayout,
227
+ entries: [{
228
+ binding: 0,
229
+ resource: textureBinding.view
230
+ }, {
231
+ binding: 1,
232
+ resource: textureBinding.sampler
233
+ }]
234
+ });
235
+ this._writeUniformData(backend, system, drawCall.texture);
236
+ const vertexBuffer = gpuState !== null ? gpuState.instanceBuffer : this._uploadModeData(device, resources, mode, targetVertexBytes, vertexByteOffset, drawCount);
237
+ device.queue.writeBuffer(this._uniformBuffer, uniformSlot * uniformSlotStride, this._uniformData.buffer, this._uniformData.byteOffset, this._uniformData.byteLength);
238
+ if (meshSyncRequired) this._syncMeshBuffer(device, resources, mode);
239
+ const active = coordinator.acquirePass();
240
+ const pass = active.pass;
241
+ pass.setBindGroup(0, this._uniformBindGroup, [uniformSlot * uniformSlotStride]);
242
+ pass.setPipeline(pipeline);
243
+ pass.setBindGroup(1, textureBindGroup);
244
+ pass.setVertexBuffer(0, vertexBuffer, gpuState !== null ? 0 : vertexByteOffset);
245
+ if (resources.meshBuffer !== null) pass.setVertexBuffer(1, resources.meshBuffer);
246
+ const instanceCount = resources.instanced ? drawCount : 1;
247
+ if (resources.indexBuffer !== null) {
248
+ pass.setIndexBuffer(resources.indexBuffer, resources.indexFormat);
249
+ pass.drawIndexed(resources.indexCount, instanceCount, 0, 0, 0);
250
+ } else pass.draw(resources.instanced ? resources.indexCount : drawCount, instanceCount, 0, 0);
251
+ coordinator.markPassDraws();
252
+ backend.stats.batches++;
253
+ backend.stats.drawCalls++;
254
+ this._ownDrawsPass = active;
255
+ this._uniformPassSlots = uniformSlot + 1;
256
+ resources.passRef = active;
257
+ resources.vertexPassBytes = vertexByteOffset + appendBytes;
258
+ }
259
+ /**
260
+ * Whether appending this draw would retroactively change what a draw already
261
+ * recorded into the open pass reads - in which case the caller ends (submits)
262
+ * that pass first, which restarts every cursor at 0.
263
+ */
264
+ _appendWouldAlias(backend, drawCall, resources, meshSyncRequired, targetVertexBytes, targetUniformSlots) {
265
+ const coordinator = backend._passCoordinator;
266
+ const active = coordinator.activePass;
267
+ if (coordinator.passHasDraws && backend._textureUploadWouldMutate(drawCall.texture)) return true;
268
+ if (active === null) return false;
269
+ if (active.viewUpdateId !== backend.view.updateId) return true;
270
+ if (resources.passRef === active && (meshSyncRequired || targetVertexBytes > resources.vertexBufferByteLength)) return true;
271
+ return this._ownDrawsPass === active && targetUniformSlots * uniformSlotStride > this._uniformBufferCapacity;
272
+ }
273
+ /**
274
+ * Bytes of the mode's vertex buffer `active` has consumed, or 0 when it holds
275
+ * none of that mode's draws (a different pass, or none open).
276
+ */
277
+ _modePassBytes(active, resources) {
278
+ return active !== null && resources.passRef === active ? resources.vertexPassBytes : 0;
279
+ }
280
+ /** Uniform ring slots `active` has consumed, or 0 when it holds no draws of ours. */
281
+ _uniformPassBase(active) {
282
+ return active !== null && this._ownDrawsPass === active ? this._uniformPassSlots : 0;
283
+ }
284
+ /**
285
+ * Upload what the mode just built into this draw's sub-range of the mode's
286
+ * vertex buffer, growing it to the pass total first. Returns the buffer to
287
+ * bind, since growth replaces it.
288
+ */
289
+ _uploadModeData(device, resources, mode, targetByteLength, byteOffset, elementCount) {
290
+ const buffer = this._ensureCapacity(device, resources, targetByteLength);
291
+ device.queue.writeBuffer(buffer, byteOffset, mode.data, 0, elementCount * resources.stride);
292
+ return buffer;
293
+ }
294
+ _getOrCreateResources(mode, device) {
295
+ const material = mode.material;
296
+ const cached = this._resources.get(material);
297
+ if (cached !== void 0) return cached;
298
+ const created = this._createResources(mode, material, device);
299
+ this._resources.set(material, created);
300
+ if (!this._disposeListenerRegistered.has(material)) {
301
+ this._disposeListenerRegistered.add(material);
302
+ material._onDispose(() => {
303
+ this._disposeListenerRegistered.delete(material);
304
+ const stored = this._resources.get(material);
305
+ if (stored === void 0) return;
306
+ this._destroyResources(stored);
307
+ this._resources.delete(material);
308
+ });
309
+ }
310
+ return created;
311
+ }
312
+ _createResources(mode, material, device) {
313
+ const wgsl = material.shader.wgsl;
314
+ if (wgsl === null) throw new Error("Particle material shader has no `wgsl` source; cannot render through the WebGPU backend.");
315
+ const layout = mode.dataLayout;
316
+ const meshGeometry = mode.vertexGeometry;
317
+ assertVertexGeometryCompatible(layout, meshGeometry, mode.instanced, mode.constructor.name);
318
+ const indices = meshGeometry !== null ? meshGeometry.indices : layout.indices;
319
+ let indexBuffer = null;
320
+ if (indices !== null) {
321
+ const indexData = new Uint8Array(Math.ceil(indices.byteLength / 4) * 4);
322
+ indexData.set(new Uint8Array(indices.buffer, indices.byteOffset, indices.byteLength));
323
+ indexBuffer = device.createBuffer({
324
+ size: indexData.byteLength,
325
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST
326
+ });
327
+ device.queue.writeBuffer(indexBuffer, 0, indexData.buffer, indexData.byteOffset, indexData.byteLength);
328
+ }
329
+ const topology = meshGeometry !== null ? meshGeometry.topology : layout.topology;
330
+ const indexFormat = indices instanceof Uint32Array ? "uint32" : "uint16";
331
+ const indexedStrip = topology === "triangle-strip" && indexBuffer !== null;
332
+ let meshBuffer = null;
333
+ let meshLayout = null;
334
+ if (meshGeometry !== null) {
335
+ const meshData = meshGeometry.vertexData;
336
+ meshLayout = {
337
+ arrayStride: meshGeometry.stride,
338
+ stepMode: "vertex",
339
+ attributes: meshGeometry.attributes.map((attribute, index) => ({
340
+ shaderLocation: layout.attributes.length + index,
341
+ offset: attribute.offset,
342
+ format: resolveVertexFormat(attribute)
343
+ }))
344
+ };
345
+ meshBuffer = device.createBuffer({
346
+ size: Math.ceil(meshData.byteLength / 4) * 4,
347
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
348
+ });
349
+ device.queue.writeBuffer(meshBuffer, 0, meshData instanceof Float32Array ? meshData.buffer : meshData, 0, meshData.byteLength);
350
+ }
351
+ return {
352
+ shaderModule: device.createShaderModule({ code: wgsl }),
353
+ vertexLayout: {
354
+ arrayStride: layout.stride,
355
+ stepMode: mode.instanced ? "instance" : "vertex",
356
+ attributes: layout.attributes.map((attribute, location) => ({
357
+ shaderLocation: location,
358
+ offset: attribute.offset,
359
+ format: resolveVertexFormat(attribute)
360
+ }))
361
+ },
362
+ meshLayout,
363
+ meshBuffer,
364
+ meshVersion: meshGeometry?.version ?? -1,
365
+ stride: layout.stride,
366
+ topology,
367
+ stripIndexFormat: indexedStrip ? indexFormat : void 0,
368
+ indexBuffer,
369
+ indexFormat,
370
+ indexCount: meshGeometry !== null ? meshGeometry.indexCount : layout.indexCount,
371
+ instanced: mode.instanced,
372
+ pipelines: /* @__PURE__ */ new Map(),
373
+ vertexBuffer: null,
374
+ vertexBufferByteLength: 0,
375
+ passRef: null,
376
+ vertexPassBytes: 0
377
+ };
378
+ }
379
+ /**
380
+ * Re-write the mode's own geometry only when it was mutated since the last
381
+ * draw. One integer comparison keeps an unchanging mesh off the bus.
382
+ */
383
+ _syncMeshBuffer(device, resources, mode) {
384
+ const meshGeometry = mode.vertexGeometry;
385
+ if (meshGeometry === null || resources.meshBuffer === null || resources.meshVersion === meshGeometry.version) return;
386
+ const meshData = meshGeometry.vertexData;
387
+ resources.meshVersion = meshGeometry.version;
388
+ device.queue.writeBuffer(resources.meshBuffer, 0, meshData instanceof Float32Array ? meshData.buffer : meshData, 0, meshData.byteLength);
389
+ }
390
+ _destroyResources(resources) {
391
+ resources.vertexBuffer?.destroy();
392
+ resources.meshBuffer?.destroy();
393
+ resources.indexBuffer?.destroy();
394
+ resources.pipelines.clear();
395
+ resources.vertexBuffer = null;
396
+ resources.meshBuffer = null;
397
+ resources.vertexBufferByteLength = 0;
398
+ resources.passRef = null;
399
+ resources.vertexPassBytes = 0;
400
+ }
401
+ /**
402
+ * Grow the mode's vertex buffer to `requiredByteLength`, which is the whole
403
+ * open pass's consumption of it (this draw's append plus everything already
404
+ * written for the draws it holds), not just this draw's own bytes. Grow-only
405
+ * and doubling, matching the mode's own scratch-buffer policy; the returned
406
+ * buffer is the one this draw must bind, since growth replaces it.
407
+ */
408
+ _ensureCapacity(device, resources, requiredByteLength) {
409
+ const stride = resources.stride;
410
+ const required = Math.max(requiredByteLength, stride);
411
+ if (resources.vertexBuffer !== null && required <= resources.vertexBufferByteLength) return resources.vertexBuffer;
412
+ let byteLength = resources.vertexBufferByteLength || stride;
413
+ while (byteLength < required) byteLength *= 2;
414
+ resources.vertexBuffer?.destroy();
415
+ resources.vertexBuffer = device.createBuffer({
416
+ size: byteLength,
417
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
418
+ });
419
+ resources.vertexBufferByteLength = byteLength;
420
+ return resources.vertexBuffer;
421
+ }
422
+ /**
423
+ * Grow the uniform ring to `slots` draw-call slots - again the open pass's
424
+ * total, not this draw's one slot. Doubling and grow-only; growth frees the
425
+ * current buffer, so the caller must have ended any pass holding draws of
426
+ * ours that read it.
427
+ */
428
+ _ensureUniformCapacity(device, slots) {
429
+ const requiredBytes = slots * uniformSlotStride;
430
+ if (requiredBytes <= this._uniformBufferCapacity) return;
431
+ this._uniformBuffer?.destroy();
432
+ this._createUniformResources(device, Math.max(requiredBytes, this._uniformBufferCapacity * 2));
433
+ }
434
+ /** (Re-)create the uniform ring and its bind group at `capacityBytes`. */
435
+ _createUniformResources(device, capacityBytes) {
436
+ this._uniformBufferCapacity = capacityBytes;
437
+ this._uniformBuffer = device.createBuffer({
438
+ label: "particles:uniform-ring",
439
+ size: capacityBytes,
440
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
441
+ });
442
+ this._uniformBindGroup = device.createBindGroup({
443
+ label: "particles:uniform-bind-group",
444
+ layout: this._uniformBindGroupLayout,
445
+ entries: [{
446
+ binding: 0,
447
+ resource: {
448
+ buffer: this._uniformBuffer,
449
+ size: uniformByteLength
450
+ }
451
+ }]
452
+ });
453
+ }
454
+ _writeUniformData(backend, system, texture) {
455
+ const projection = backend.view.getTransform().toArray(false);
456
+ const transform = system.getGlobalTransform().toArray(false);
457
+ const shouldPremultiplySample = backend.shouldPremultiplyTextureSample(texture);
458
+ const vertices = system.vertices;
459
+ const texCoords = system.texCoords;
460
+ const quadMinX = vertices[0];
461
+ const quadMinY = vertices[1];
462
+ const quadSizeX = vertices[2] - vertices[0];
463
+ const quadSizeY = vertices[3] - vertices[1];
464
+ const uvMinX = (texCoords[0] & 65535) / 65535;
465
+ const uvMinY = (texCoords[0] >>> 16 & 65535) / 65535;
466
+ const uvMaxX = (texCoords[2] & 65535) / 65535;
467
+ const uvMaxY = (texCoords[2] >>> 16 & 65535) / 65535;
468
+ const u = this._uniformData;
469
+ u[0] = projection[0];
470
+ u[1] = projection[1];
471
+ u[2] = 0;
472
+ u[3] = 0;
473
+ u[4] = projection[3];
474
+ u[5] = projection[4];
475
+ u[6] = 0;
476
+ u[7] = 0;
477
+ u[8] = 0;
478
+ u[9] = 0;
479
+ u[10] = 1;
480
+ u[11] = 0;
481
+ u[12] = projection[6];
482
+ u[13] = projection[7];
483
+ u[14] = 0;
484
+ u[15] = projection[8];
485
+ u[16] = transform[0];
486
+ u[17] = transform[1];
487
+ u[18] = 0;
488
+ u[19] = 0;
489
+ u[20] = transform[3];
490
+ u[21] = transform[4];
491
+ u[22] = 0;
492
+ u[23] = 0;
493
+ u[24] = 0;
494
+ u[25] = 0;
495
+ u[26] = 1;
496
+ u[27] = 0;
497
+ u[28] = transform[6];
498
+ u[29] = transform[7];
499
+ u[30] = 0;
500
+ u[31] = transform[8];
501
+ u[32] = shouldPremultiplySample ? 1 : 0;
502
+ u[33] = 0;
503
+ u[34] = 0;
504
+ u[35] = 0;
505
+ u[36] = quadMinX;
506
+ u[37] = quadMinY;
507
+ u[38] = quadSizeX;
508
+ u[39] = quadSizeY;
509
+ u[40] = uvMinX;
510
+ u[41] = uvMinY;
511
+ u[42] = uvMaxX;
512
+ u[43] = uvMaxY;
513
+ }
514
+ _getPipeline(resources, blendMode, format, stencil) {
515
+ const pipelineKey = `${blendMode}:${format}:${stencil ? "s" : "n"}`;
516
+ const existingPipeline = resources.pipelines.get(pipelineKey);
517
+ if (existingPipeline) return existingPipeline;
518
+ const descriptor = {
519
+ layout: this._pipelineLayout,
520
+ vertex: {
521
+ module: resources.shaderModule,
522
+ entryPoint: "vertexMain",
523
+ buffers: resources.meshLayout !== null ? [resources.vertexLayout, resources.meshLayout] : [resources.vertexLayout]
524
+ },
525
+ fragment: {
526
+ module: resources.shaderModule,
527
+ entryPoint: "fragmentMain",
528
+ targets: [{
529
+ format,
530
+ blend: getWebGpuBlendState(blendMode),
531
+ writeMask: GPUColorWrite.ALL
532
+ }]
533
+ },
534
+ primitive: resources.stripIndexFormat === void 0 ? { topology: resources.topology } : {
535
+ topology: resources.topology,
536
+ stripIndexFormat: resources.stripIndexFormat
537
+ }
538
+ };
539
+ if (stencil) descriptor.depthStencil = stencilContentDepthStencilState();
540
+ const pipeline = this._device.createRenderPipeline(descriptor);
541
+ resources.pipelines.set(pipelineKey, pipeline);
542
+ return pipeline;
543
+ }
37
544
  };
38
545
 
39
- @vertex
40
- fn vertexMain(input: VertexInput) -> VertexOutput {
41
- let quadMin = uniforms.localBounds.xy;
42
- let quadSize = uniforms.localBounds.zw;
43
-
44
- let localPosition = quadMin + (input.unitPosition * quadSize);
45
- let radians = radians(input.rotation);
46
- let sinValue = sin(radians);
47
- let cosValue = cos(radians);
48
- let rotated = vec2<f32>(
49
- (localPosition.x * (input.scale.x * cosValue)) + (localPosition.y * (input.scale.y * sinValue)) + input.translation.x,
50
- (localPosition.x * (input.scale.x * -sinValue)) + (localPosition.y * (input.scale.y * cosValue)) + input.translation.y
51
- );
52
-
53
- var output: VertexOutput;
54
-
55
- output.position = uniforms.projection * uniforms.translation * vec4<f32>(rotated, 0.0, 1.0);
56
- output.texcoord = input.uvMin + ((input.uvMax - input.uvMin) * input.unitPosition);
57
- output.color = vec4(input.color.rgb * input.color.a, input.color.a);
58
-
59
- return output;
60
- }
61
-
62
- @fragment
63
- fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
64
- let sample = textureSample(particleTexture, particleSampler, input.texcoord);
65
- let premultipliedSample = select(sample, vec4(sample.rgb * sample.a, sample.a), uniforms.flags.x > 0.5);
66
-
67
- return premultipliedSample * input.color;
68
- }
69
- `;
70
- const staticVertexStrideBytes = 8;
71
- const instanceWords = 10;
72
- const instanceStrideBytes = 40;
73
- const indicesPerParticle = 6;
74
- const uniformByteLength = 176;
75
- const initialParticleCapacity = 1;
76
- const staticVertexData = new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]);
77
- const staticIndexData = new Uint16Array([0, 1, 2, 0, 2, 3]);
78
- class WebGpuParticleRenderer extends AbstractWebGpuRenderer {
79
- /**
80
- * The particle system's transform is bound as a uniform and each particle is
81
- * positioned system-locally, so this renderer never reads the shared transform
82
- * storage; the plan player skips writing transform records for particle draws.
83
- * @internal
84
- */
85
- _consumesSharedTransform = false;
86
- _drawCalls = [];
87
- _drawCallCount = 0;
88
- _uniformData = new Float32Array(uniformByteLength / Float32Array.BYTES_PER_ELEMENT);
89
- _device = null;
90
- _shaderModule = null;
91
- _uniformBindGroupLayout = null;
92
- _textureBindGroupLayout = null;
93
- _pipelineLayout = null;
94
- _uniformBuffer = null;
95
- _uniformBindGroup = null;
96
- _staticVertexBuffer = null;
97
- _instanceBuffer = null;
98
- _indexBuffer = null;
99
- _instanceBufferByteLength = 0;
100
- _instanceData = new ArrayBuffer(instanceStrideBytes * initialParticleCapacity);
101
- _float32View = new Float32Array(this._instanceData);
102
- _uint32View = new Uint32Array(this._instanceData);
103
- _pipelines = new Map();
104
- render(system) {
105
- const backend = this._backend;
106
- const texture = system.texture;
107
- if (backend === null || !(texture instanceof Texture) || texture.source === null || texture.width === 0 || texture.height === 0 || system.liveCount === 0) {
108
- return;
109
- }
110
- backend.setBlendMode(system.blendMode);
111
- const drawCallIndex = this._drawCallCount++;
112
- const drawCall = this._drawCalls[drawCallIndex];
113
- if (drawCall) {
114
- drawCall.system = system;
115
- drawCall.texture = texture;
116
- drawCall.blendMode = system.blendMode;
117
- }
118
- else {
119
- this._drawCalls.push({
120
- system,
121
- texture,
122
- blendMode: system.blendMode,
123
- });
124
- }
125
- }
126
- flush() {
127
- const backend = this._backend;
128
- const device = this._device;
129
- const uniformBuffer = this._uniformBuffer;
130
- const uniformBindGroup = this._uniformBindGroup;
131
- const staticVertexBuffer = this._staticVertexBuffer;
132
- const indexBuffer = this._indexBuffer;
133
- if (!backend || !device || !uniformBuffer || !uniformBindGroup || !staticVertexBuffer || !this._instanceBuffer || !indexBuffer) {
134
- return;
135
- }
136
- if (this._drawCallCount === 0 && !backend.clearRequested) {
137
- return;
138
- }
139
- const scissor = backend.getScissorRect();
140
- const maskClipsAll = scissor !== null && (scissor.width <= 0 || scissor.height <= 0);
141
- // If no drawcalls will actually render (none queued, or the scissor
142
- // clips everything), but a clear is pending, open a single empty
143
- // pass so createColorAttachment consumes the clear state.
144
- if (this._drawCallCount === 0 || maskClipsAll) {
145
- if (backend.clearRequested) {
146
- backend._passCoordinator.acquirePass();
147
- backend._passCoordinator.endPass();
148
- }
149
- this._drawCallCount = 0;
150
- return;
151
- }
152
- // One command encoder / pass per drawcall. Each particle system's
153
- // queue.writeBuffer calls target offset 0 of the instance and uniform
154
- // buffers — a single pass with multiple systems would see all
155
- // writeBuffers serialize before submit, leaving only the last
156
- // system's data in those buffers and making every earlier draw read
157
- // the wrong data. Also: _ensureCapacity may destroy and recreate the
158
- // instance buffer on growth; keeping one drawcall per pass means
159
- // that destroy happens strictly between submits, so no pass holds a
160
- // reference to a buffer that has since been destroyed.
161
- for (let drawCallIndex = 0; drawCallIndex < this._drawCallCount; drawCallIndex++) {
162
- const drawCall = this._drawCalls[drawCallIndex];
163
- const system = drawCall.system;
164
- const particleCount = system.liveCount;
165
- if (particleCount === 0) {
166
- continue;
167
- }
168
- const pipeline = this._getPipeline(drawCall.blendMode, backend.renderTargetFormat, backend._passCoordinator.stencilActive);
169
- const textureBinding = backend.getTextureBinding(drawCall.texture);
170
- const textureBindGroup = device.createBindGroup({
171
- layout: this._textureBindGroupLayout,
172
- entries: [
173
- {
174
- binding: 0,
175
- resource: textureBinding.view,
176
- },
177
- {
178
- binding: 1,
179
- resource: textureBinding.sampler,
180
- },
181
- ],
182
- });
183
- this._writeUniformData(backend, system, drawCall.texture);
184
- // GPU mode: the system's compute pipeline already wrote the
185
- // interleaved instance data into its own buffer. Bind it
186
- // directly — no CPU pack, no writeBuffer for instance data.
187
- // CPU mode: pack from CPU SoA into our owned instance buffer.
188
- let drawInstanceCount = particleCount;
189
- const instanceBuffer = (() => {
190
- if (system.gpuMode && system.gpuState !== null) {
191
- return system.gpuState.instanceBuffer;
192
- }
193
- this._ensureCapacity(particleCount);
194
- drawInstanceCount = this._writeInstanceData(system);
195
- device.queue.writeBuffer(this._instanceBuffer, 0, this._instanceData, 0, drawInstanceCount * instanceStrideBytes);
196
- return this._instanceBuffer;
197
- })();
198
- device.queue.writeBuffer(uniformBuffer, 0, this._uniformData.buffer, this._uniformData.byteOffset, this._uniformData.byteLength);
199
- // One coordinator-owned pass per drawcall: each system's writeBuffers
200
- // target offset 0, so the pass must be submitted before the next system
201
- // overwrites those buffers. acquirePass/endPass preserve that 1:1 ratio.
202
- const pass = backend._passCoordinator.acquirePass().pass;
203
- pass.setBindGroup(0, uniformBindGroup);
204
- pass.setPipeline(pipeline);
205
- pass.setBindGroup(1, textureBindGroup);
206
- pass.setVertexBuffer(0, staticVertexBuffer);
207
- pass.setVertexBuffer(1, instanceBuffer);
208
- pass.setIndexBuffer(indexBuffer, 'uint16');
209
- pass.drawIndexed(indicesPerParticle, drawInstanceCount, 0, 0, 0);
210
- backend.stats.batches++;
211
- backend.stats.drawCalls++;
212
- backend._passCoordinator.endPass();
213
- }
214
- this._drawCallCount = 0;
215
- }
216
- destroy() {
217
- this.disconnect();
218
- }
219
- onConnect(backend) {
220
- this._backend = backend;
221
- this._device = this._backend.device;
222
- this._shaderModule = this._device.createShaderModule({ code: particleShaderSource });
223
- this._uniformBindGroupLayout = this._device.createBindGroupLayout({
224
- entries: [
225
- {
226
- binding: 0,
227
- visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
228
- buffer: {
229
- type: 'uniform',
230
- },
231
- },
232
- ],
233
- });
234
- this._textureBindGroupLayout = this._device.createBindGroupLayout({
235
- entries: [
236
- {
237
- binding: 0,
238
- visibility: GPUShaderStage.FRAGMENT,
239
- texture: {
240
- sampleType: 'float',
241
- },
242
- },
243
- {
244
- binding: 1,
245
- visibility: GPUShaderStage.FRAGMENT,
246
- sampler: {
247
- type: 'filtering',
248
- },
249
- },
250
- ],
251
- });
252
- this._pipelineLayout = this._device.createPipelineLayout({
253
- bindGroupLayouts: [this._uniformBindGroupLayout, this._textureBindGroupLayout],
254
- });
255
- this._uniformBuffer = this._device.createBuffer({
256
- size: uniformByteLength,
257
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
258
- });
259
- this._uniformBindGroup = this._device.createBindGroup({
260
- layout: this._uniformBindGroupLayout,
261
- entries: [
262
- {
263
- binding: 0,
264
- resource: {
265
- buffer: this._uniformBuffer,
266
- },
267
- },
268
- ],
269
- });
270
- this._staticVertexBuffer = this._device.createBuffer({
271
- size: staticVertexData.byteLength,
272
- usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
273
- });
274
- this._device.queue.writeBuffer(this._staticVertexBuffer, 0, staticVertexData.buffer, staticVertexData.byteOffset, staticVertexData.byteLength);
275
- this._indexBuffer = this._device.createBuffer({
276
- size: staticIndexData.byteLength,
277
- usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
278
- });
279
- this._device.queue.writeBuffer(this._indexBuffer, 0, staticIndexData.buffer, staticIndexData.byteOffset, staticIndexData.byteLength);
280
- this._ensureCapacity(initialParticleCapacity);
281
- }
282
- onDisconnect() {
283
- this.flush();
284
- this._staticVertexBuffer?.destroy();
285
- this._instanceBuffer?.destroy();
286
- this._indexBuffer?.destroy();
287
- this._uniformBuffer?.destroy();
288
- this._pipelines.clear();
289
- this._indexBuffer = null;
290
- this._staticVertexBuffer = null;
291
- this._instanceBuffer = null;
292
- this._uniformBindGroup = null;
293
- this._uniformBuffer = null;
294
- this._pipelineLayout = null;
295
- this._textureBindGroupLayout = null;
296
- this._uniformBindGroupLayout = null;
297
- this._shaderModule = null;
298
- this._device = null;
299
- this._backend = null;
300
- this._instanceBufferByteLength = 0;
301
- this._instanceData = new ArrayBuffer(instanceStrideBytes * initialParticleCapacity);
302
- this._float32View = new Float32Array(this._instanceData);
303
- this._uint32View = new Uint32Array(this._instanceData);
304
- this._drawCallCount = 0;
305
- }
306
- _ensureCapacity(particleCount) {
307
- const requiredInstanceBytes = particleCount * instanceStrideBytes;
308
- if (requiredInstanceBytes > this._instanceData.byteLength) {
309
- let byteLength = this._instanceData.byteLength;
310
- while (byteLength < requiredInstanceBytes) {
311
- byteLength *= 2;
312
- }
313
- this._instanceData = new ArrayBuffer(byteLength);
314
- this._float32View = new Float32Array(this._instanceData);
315
- this._uint32View = new Uint32Array(this._instanceData);
316
- }
317
- if (requiredInstanceBytes > this._instanceBufferByteLength) {
318
- let byteLength = this._instanceBufferByteLength || instanceStrideBytes;
319
- while (byteLength < requiredInstanceBytes) {
320
- byteLength *= 2;
321
- }
322
- this._instanceBuffer?.destroy();
323
- this._instanceBuffer = this._device.createBuffer({
324
- size: byteLength,
325
- usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
326
- });
327
- this._instanceBufferByteLength = byteLength;
328
- }
329
- }
330
- _writeUniformData(backend, system, texture) {
331
- const projection = backend.view.getTransform().toArray(false);
332
- const transform = system.getGlobalTransform().toArray(false);
333
- const shouldPremultiplySample = backend.shouldPremultiplyTextureSample(texture);
334
- const vertices = system.vertices;
335
- const texCoords = system.texCoords;
336
- const quadMinX = vertices[0];
337
- const quadMinY = vertices[1];
338
- const quadSizeX = vertices[2] - vertices[0];
339
- const quadSizeY = vertices[3] - vertices[1];
340
- const uvMinX = (texCoords[0] & 0xffff) / 0xffff;
341
- const uvMinY = ((texCoords[0] >>> 16) & 0xffff) / 0xffff;
342
- const uvMaxX = (texCoords[2] & 0xffff) / 0xffff;
343
- const uvMaxY = ((texCoords[2] >>> 16) & 0xffff) / 0xffff;
344
- const u = this._uniformData;
345
- // projection mat4 (col-major, padded to 4×4)
346
- u[0] = projection[0];
347
- u[1] = projection[1];
348
- u[2] = 0;
349
- u[3] = 0;
350
- u[4] = projection[3];
351
- u[5] = projection[4];
352
- u[6] = 0;
353
- u[7] = 0;
354
- u[8] = 0;
355
- u[9] = 0;
356
- u[10] = 1;
357
- u[11] = 0;
358
- u[12] = projection[6];
359
- u[13] = projection[7];
360
- u[14] = 0;
361
- u[15] = projection[8];
362
- // transform mat4 (col-major, padded to 4×4)
363
- u[16] = transform[0];
364
- u[17] = transform[1];
365
- u[18] = 0;
366
- u[19] = 0;
367
- u[20] = transform[3];
368
- u[21] = transform[4];
369
- u[22] = 0;
370
- u[23] = 0;
371
- u[24] = 0;
372
- u[25] = 0;
373
- u[26] = 1;
374
- u[27] = 0;
375
- u[28] = transform[6];
376
- u[29] = transform[7];
377
- u[30] = 0;
378
- u[31] = transform[8];
379
- // flags vec4
380
- u[32] = shouldPremultiplySample ? 1 : 0;
381
- u[33] = 0;
382
- u[34] = 0;
383
- u[35] = 0;
384
- // localBounds vec4
385
- u[36] = quadMinX;
386
- u[37] = quadMinY;
387
- u[38] = quadSizeX;
388
- u[39] = quadSizeY;
389
- // uvBounds vec4
390
- u[40] = uvMinX;
391
- u[41] = uvMinY;
392
- u[42] = uvMaxX;
393
- u[43] = uvMaxY;
394
- }
395
- _writeInstanceData(system) {
396
- const { posX, posY, scaleX, scaleY, rotations, color, textureIndex, alive, liveCount } = system;
397
- const f32 = this._float32View;
398
- const u32 = this._uint32View;
399
- const { uvMins, uvMaxs } = this._computeFrameUvs(system);
400
- const frameCount = uvMins.length / 2;
401
- let writeIndex = 0;
402
- for (let particleIndex = 0; particleIndex < liveCount; particleIndex++) {
403
- // Skip dead slots — present in GPU-mode systems where the live
404
- // range can carry holes filled in on next spawn.
405
- if (alive[particleIndex] === 0) {
406
- continue;
407
- }
408
- const targetIndex = writeIndex * instanceWords;
409
- const frame = textureIndex[particleIndex] < frameCount ? textureIndex[particleIndex] : 0;
410
- const uvBase = frame * 2;
411
- f32[targetIndex + 0] = posX[particleIndex];
412
- f32[targetIndex + 1] = posY[particleIndex];
413
- f32[targetIndex + 2] = scaleX[particleIndex];
414
- f32[targetIndex + 3] = scaleY[particleIndex];
415
- f32[targetIndex + 4] = rotations[particleIndex];
416
- u32[targetIndex + 5] = color[particleIndex];
417
- f32[targetIndex + 6] = uvMins[uvBase + 0];
418
- f32[targetIndex + 7] = uvMins[uvBase + 1];
419
- f32[targetIndex + 8] = uvMaxs[uvBase + 0];
420
- f32[targetIndex + 9] = uvMaxs[uvBase + 1];
421
- writeIndex++;
422
- }
423
- return writeIndex;
424
- }
425
- /**
426
- * Same atlas/UV-resolution as the WebGL2 path. Returns the per-frame
427
- * (uvMin, uvMax) pairs derived from `system.frames` (or fallback to
428
- * `system.textureFrame` when no atlas is declared), already flipY-
429
- * adjusted for the current texture.
430
- */
431
- _uvMinsScratch = new Float32Array(2);
432
- _uvMaxsScratch = new Float32Array(2);
433
- _computeFrameUvs(system) {
434
- const frames = system.frames;
435
- const tex = system.texture;
436
- const texW = tex.width;
437
- const texH = tex.height;
438
- const flipY = tex.flipY;
439
- const count = frames.length === 0 ? 1 : frames.length;
440
- if (this._uvMinsScratch.length < count * 2) {
441
- this._uvMinsScratch = new Float32Array(count * 2);
442
- this._uvMaxsScratch = new Float32Array(count * 2);
443
- }
444
- const mins = this._uvMinsScratch;
445
- const maxs = this._uvMaxsScratch;
446
- if (frames.length === 0) {
447
- const f = system.textureFrame;
448
- const minU = f.left / texW;
449
- const maxU = f.right / texW;
450
- const topV = f.top / texH;
451
- const bottomV = f.bottom / texH;
452
- mins[0] = minU;
453
- mins[1] = flipY ? bottomV : topV;
454
- maxs[0] = maxU;
455
- maxs[1] = flipY ? topV : bottomV;
456
- return { uvMins: mins, uvMaxs: maxs };
457
- }
458
- for (let i = 0; i < frames.length; i++) {
459
- const f = frames[i];
460
- const o = i * 2;
461
- const minU = f.left / texW;
462
- const maxU = f.right / texW;
463
- const topV = f.top / texH;
464
- const bottomV = f.bottom / texH;
465
- mins[o + 0] = minU;
466
- mins[o + 1] = flipY ? bottomV : topV;
467
- maxs[o + 0] = maxU;
468
- maxs[o + 1] = flipY ? topV : bottomV;
469
- }
470
- return { uvMins: mins, uvMaxs: maxs };
471
- }
472
- _getPipeline(blendMode, format, stencil) {
473
- const pipelineKey = `${blendMode}:${format}:${stencil ? 's' : 'n'}`;
474
- const existingPipeline = this._pipelines.get(pipelineKey);
475
- if (existingPipeline) {
476
- return existingPipeline;
477
- }
478
- const descriptor = {
479
- layout: this._pipelineLayout,
480
- vertex: {
481
- module: this._shaderModule,
482
- entryPoint: 'vertexMain',
483
- buffers: [
484
- {
485
- arrayStride: staticVertexStrideBytes,
486
- attributes: [
487
- {
488
- shaderLocation: 0,
489
- offset: 0,
490
- format: 'float32x2',
491
- },
492
- ],
493
- },
494
- {
495
- arrayStride: instanceStrideBytes,
496
- stepMode: 'instance',
497
- attributes: [
498
- {
499
- shaderLocation: 1,
500
- offset: 0,
501
- format: 'float32x2',
502
- },
503
- {
504
- shaderLocation: 2,
505
- offset: 8,
506
- format: 'float32x2',
507
- },
508
- {
509
- shaderLocation: 3,
510
- offset: 16,
511
- format: 'float32',
512
- },
513
- {
514
- shaderLocation: 4,
515
- offset: 20,
516
- format: 'unorm8x4',
517
- },
518
- {
519
- shaderLocation: 5,
520
- offset: 24,
521
- format: 'float32x2',
522
- },
523
- {
524
- shaderLocation: 6,
525
- offset: 32,
526
- format: 'float32x2',
527
- },
528
- ],
529
- },
530
- ],
531
- },
532
- fragment: {
533
- module: this._shaderModule,
534
- entryPoint: 'fragmentMain',
535
- targets: [
536
- {
537
- format,
538
- blend: getWebGpuBlendState(blendMode),
539
- writeMask: GPUColorWrite.ALL,
540
- },
541
- ],
542
- },
543
- primitive: {
544
- topology: 'triangle-list',
545
- },
546
- };
547
- if (stencil) {
548
- descriptor.depthStencil = stencilContentDepthStencilState();
549
- }
550
- const pipeline = this._device.createRenderPipeline(descriptor);
551
- this._pipelines.set(pipelineKey, pipeline);
552
- return pipeline;
553
- }
554
- }
555
-
546
+ //#endregion
556
547
  export { WebGpuParticleRenderer };
557
- //# sourceMappingURL=WebGpuParticleRenderer.js.map
548
+ //# sourceMappingURL=WebGpuParticleRenderer.js.map