@codexo/exojs 0.9.0 → 0.11.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 (242) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +14 -14
  3. package/dist/esm/core/Application.d.ts +25 -6
  4. package/dist/esm/core/Application.js +42 -8
  5. package/dist/esm/core/Application.js.map +1 -1
  6. package/dist/esm/core/Perf.d.ts +23 -0
  7. package/dist/esm/core/Perf.js +49 -0
  8. package/dist/esm/core/Perf.js.map +1 -0
  9. package/dist/esm/core/Scene.d.ts +8 -8
  10. package/dist/esm/core/Scene.js +7 -7
  11. package/dist/esm/core/Scene.js.map +1 -1
  12. package/dist/esm/core/SceneManager.js +2 -2
  13. package/dist/esm/core/SceneManager.js.map +1 -1
  14. package/dist/esm/core/SceneNode.d.ts +0 -3
  15. package/dist/esm/core/SceneNode.js +0 -9
  16. package/dist/esm/core/SceneNode.js.map +1 -1
  17. package/dist/esm/core/capabilities.d.ts +2 -0
  18. package/dist/esm/core/capabilities.js +15 -0
  19. package/dist/esm/core/capabilities.js.map +1 -1
  20. package/dist/esm/core/dev.d.ts +21 -0
  21. package/dist/esm/core/dev.js +18 -0
  22. package/dist/esm/core/dev.js.map +1 -0
  23. package/dist/esm/core/index.d.ts +1 -0
  24. package/dist/esm/core/types.d.ts +1 -1
  25. package/dist/esm/core/utils.d.ts +12 -0
  26. package/dist/esm/core/utils.js +18 -1
  27. package/dist/esm/core/utils.js.map +1 -1
  28. package/dist/esm/index.js +14 -3
  29. package/dist/esm/index.js.map +1 -1
  30. package/dist/esm/particles/ParticleSystem.d.ts +8 -5
  31. package/dist/esm/particles/ParticleSystem.js +9 -5
  32. package/dist/esm/particles/ParticleSystem.js.map +1 -1
  33. package/dist/esm/particles/distributions/{Gradient.d.ts → ColorGradient.d.ts} +5 -5
  34. package/dist/esm/particles/distributions/{Gradient.js → ColorGradient.js} +5 -5
  35. package/dist/esm/particles/distributions/ColorGradient.js.map +1 -0
  36. package/dist/esm/particles/distributions/Distribution.d.ts +2 -2
  37. package/dist/esm/particles/distributions/index.d.ts +2 -2
  38. package/dist/esm/particles/gpu/ParticleGpuState.js +1 -1
  39. package/dist/esm/particles/modules/AlphaFadeOverLifetime.d.ts +2 -2
  40. package/dist/esm/particles/modules/AlphaFadeOverLifetime.js +5 -1
  41. package/dist/esm/particles/modules/AlphaFadeOverLifetime.js.map +1 -1
  42. package/dist/esm/particles/modules/ColorOverLifetime.d.ts +3 -3
  43. package/dist/esm/particles/modules/ColorOverLifetime.js.map +1 -1
  44. package/dist/esm/particles/modules/ColorOverSpeed.d.ts +3 -3
  45. package/dist/esm/particles/modules/ColorOverSpeed.js.map +1 -1
  46. package/dist/esm/particles/modules/UpdateModule.d.ts +2 -2
  47. package/dist/esm/particles/modules/UpdateModule.js +1 -1
  48. package/dist/esm/particles/modules/WgslContribution.d.ts +2 -2
  49. package/dist/esm/rendering/Camera.d.ts +33 -0
  50. package/dist/esm/rendering/Camera.js +38 -0
  51. package/dist/esm/rendering/Camera.js.map +1 -0
  52. package/dist/esm/rendering/Container.d.ts +5 -24
  53. package/dist/esm/rendering/Container.js +8 -71
  54. package/dist/esm/rendering/Container.js.map +1 -1
  55. package/dist/esm/rendering/Drawable.d.ts +8 -10
  56. package/dist/esm/rendering/Drawable.js +12 -20
  57. package/dist/esm/rendering/Drawable.js.map +1 -1
  58. package/dist/esm/rendering/RenderBackend.d.ts +18 -0
  59. package/dist/esm/rendering/RenderNode.d.ts +81 -8
  60. package/dist/esm/rendering/RenderNode.js +121 -144
  61. package/dist/esm/rendering/RenderNode.js.map +1 -1
  62. package/dist/esm/rendering/RenderTarget.d.ts +13 -0
  63. package/dist/esm/rendering/RenderTarget.js +13 -0
  64. package/dist/esm/rendering/RenderTarget.js.map +1 -1
  65. package/dist/esm/rendering/RenderTargetPass.js +17 -0
  66. package/dist/esm/rendering/RenderTargetPass.js.map +1 -1
  67. package/dist/esm/rendering/RenderingContext.d.ts +87 -0
  68. package/dist/esm/rendering/RenderingContext.js +157 -0
  69. package/dist/esm/rendering/RenderingContext.js.map +1 -0
  70. package/dist/esm/rendering/TransformBuffer.d.ts +82 -0
  71. package/dist/esm/rendering/TransformBuffer.js +180 -0
  72. package/dist/esm/rendering/TransformBuffer.js.map +1 -0
  73. package/dist/esm/rendering/filters/WebGpuShaderFilter.js +5 -12
  74. package/dist/esm/rendering/filters/WebGpuShaderFilter.js.map +1 -1
  75. package/dist/esm/rendering/geometry/Geometry.d.ts +40 -0
  76. package/dist/esm/rendering/geometry/Geometry.js +228 -0
  77. package/dist/esm/rendering/geometry/Geometry.js.map +1 -0
  78. package/dist/esm/rendering/geometry/GeometryAttribute.d.ts +32 -0
  79. package/dist/esm/rendering/geometry/QuadGeometry.d.ts +5 -0
  80. package/dist/esm/rendering/gradient/Gradient.d.ts +67 -0
  81. package/dist/esm/rendering/gradient/Gradient.js +160 -0
  82. package/dist/esm/rendering/gradient/Gradient.js.map +1 -0
  83. package/dist/esm/rendering/gradient/LinearGradient.d.ts +18 -0
  84. package/dist/esm/rendering/gradient/LinearGradient.js +49 -0
  85. package/dist/esm/rendering/gradient/LinearGradient.js.map +1 -0
  86. package/dist/esm/rendering/gradient/RadialGradient.d.ts +18 -0
  87. package/dist/esm/rendering/gradient/RadialGradient.js +44 -0
  88. package/dist/esm/rendering/gradient/RadialGradient.js.map +1 -0
  89. package/dist/esm/rendering/index.d.ts +16 -2
  90. package/dist/esm/rendering/material/Material.d.ts +114 -0
  91. package/dist/esm/rendering/material/Material.js +111 -0
  92. package/dist/esm/rendering/material/Material.js.map +1 -0
  93. package/dist/esm/rendering/material/MaterialKey.d.ts +18 -0
  94. package/dist/esm/rendering/material/MaterialKey.js +82 -0
  95. package/dist/esm/rendering/material/MaterialKey.js.map +1 -0
  96. package/dist/esm/rendering/material/MeshMaterial.d.ts +16 -0
  97. package/dist/esm/rendering/material/MeshMaterial.js +21 -0
  98. package/dist/esm/rendering/material/MeshMaterial.js.map +1 -0
  99. package/dist/esm/rendering/{mesh/MeshShader.d.ts → material/ShaderSource.d.ts} +29 -62
  100. package/dist/esm/rendering/{mesh/MeshShader.js → material/ShaderSource.js} +35 -62
  101. package/dist/esm/rendering/material/ShaderSource.js.map +1 -0
  102. package/dist/esm/rendering/material/SpriteMaterial.d.ts +15 -0
  103. package/dist/esm/rendering/material/SpriteMaterial.js +20 -0
  104. package/dist/esm/rendering/material/SpriteMaterial.js.map +1 -0
  105. package/dist/esm/rendering/mesh/Mesh.d.ts +29 -12
  106. package/dist/esm/rendering/mesh/Mesh.js +122 -3
  107. package/dist/esm/rendering/mesh/Mesh.js.map +1 -1
  108. package/dist/esm/rendering/pass/RenderPassCoordinator.d.ts +63 -0
  109. package/dist/esm/rendering/pass/RenderPassDescriptor.d.ts +48 -0
  110. package/dist/esm/rendering/pass/RenderPassDescriptor.js +16 -0
  111. package/dist/esm/rendering/pass/RenderPassDescriptor.js.map +1 -0
  112. package/dist/esm/rendering/plan/RenderCommand.d.ts +86 -0
  113. package/dist/esm/rendering/plan/RenderCommand.js +127 -0
  114. package/dist/esm/rendering/plan/RenderCommand.js.map +1 -0
  115. package/dist/esm/rendering/plan/RenderEffectExecutor.d.ts +10 -0
  116. package/dist/esm/rendering/plan/RenderEffectExecutor.js +159 -0
  117. package/dist/esm/rendering/plan/RenderEffectExecutor.js.map +1 -0
  118. package/dist/esm/rendering/plan/RenderInstruction.d.ts +51 -0
  119. package/dist/esm/rendering/plan/RenderInstruction.js +45 -0
  120. package/dist/esm/rendering/plan/RenderInstruction.js.map +1 -0
  121. package/dist/esm/rendering/plan/RenderPlan.d.ts +23 -0
  122. package/dist/esm/rendering/plan/RenderPlan.js +12 -0
  123. package/dist/esm/rendering/plan/RenderPlan.js.map +1 -0
  124. package/dist/esm/rendering/plan/RenderPlanBuilder.d.ts +31 -0
  125. package/dist/esm/rendering/plan/RenderPlanBuilder.js +242 -0
  126. package/dist/esm/rendering/plan/RenderPlanBuilder.js.map +1 -0
  127. package/dist/esm/rendering/plan/RenderPlanOptimizer.d.ts +10 -0
  128. package/dist/esm/rendering/plan/RenderPlanOptimizer.js +180 -0
  129. package/dist/esm/rendering/plan/RenderPlanOptimizer.js.map +1 -0
  130. package/dist/esm/rendering/plan/RenderPlanPlayer.d.ts +13 -0
  131. package/dist/esm/rendering/plan/RenderPlanPlayer.js +107 -0
  132. package/dist/esm/rendering/plan/RenderPlanPlayer.js.map +1 -0
  133. package/dist/esm/rendering/plan/RenderScope.d.ts +70 -0
  134. package/dist/esm/rendering/plan/RenderScope.js +16 -0
  135. package/dist/esm/rendering/plan/RenderScope.js.map +1 -0
  136. package/dist/esm/rendering/plan/playRenderTree.d.ts +4 -0
  137. package/dist/esm/rendering/plan/playRenderTree.js +19 -0
  138. package/dist/esm/rendering/plan/playRenderTree.js.map +1 -0
  139. package/dist/esm/rendering/primitives/Graphics.d.ts +70 -5
  140. package/dist/esm/rendering/primitives/Graphics.js +172 -14
  141. package/dist/esm/rendering/primitives/Graphics.js.map +1 -1
  142. package/dist/esm/rendering/sprite/Sprite.d.ts +22 -1
  143. package/dist/esm/rendering/sprite/Sprite.js +33 -2
  144. package/dist/esm/rendering/sprite/Sprite.js.map +1 -1
  145. package/dist/esm/rendering/sprite/spriteMaterialSources.d.ts +41 -0
  146. package/dist/esm/rendering/sprite/spriteMaterialSources.js +149 -0
  147. package/dist/esm/rendering/sprite/spriteMaterialSources.js.map +1 -0
  148. package/dist/esm/rendering/text/BitmapText.d.ts +2 -0
  149. package/dist/esm/rendering/text/BitmapText.js +8 -1
  150. package/dist/esm/rendering/text/BitmapText.js.map +1 -1
  151. package/dist/esm/rendering/text/BmFont.js +3 -0
  152. package/dist/esm/rendering/text/BmFont.js.map +1 -1
  153. package/dist/esm/rendering/text/GlyphSdf.d.ts +14 -0
  154. package/dist/esm/rendering/text/GlyphSdf.js +41 -11
  155. package/dist/esm/rendering/text/GlyphSdf.js.map +1 -1
  156. package/dist/esm/rendering/text/TextStyle.d.ts +6 -1
  157. package/dist/esm/rendering/text/TextStyle.js +1 -1
  158. package/dist/esm/rendering/text/TextStyle.js.map +1 -1
  159. package/dist/esm/rendering/texture/DataTexture.d.ts +5 -0
  160. package/dist/esm/rendering/texture/DataTexture.js +7 -0
  161. package/dist/esm/rendering/texture/DataTexture.js.map +1 -1
  162. package/dist/esm/rendering/texture/RenderTexture.js.map +1 -1
  163. package/dist/esm/rendering/video/Video.d.ts +3 -7
  164. package/dist/esm/rendering/video/Video.js +3 -8
  165. package/dist/esm/rendering/video/Video.js.map +1 -1
  166. package/dist/esm/rendering/webgl2/WebGl2Backend.d.ts +62 -0
  167. package/dist/esm/rendering/webgl2/WebGl2Backend.js +352 -21
  168. package/dist/esm/rendering/webgl2/WebGl2Backend.js.map +1 -1
  169. package/dist/esm/rendering/webgl2/WebGl2MeshRenderer.d.ts +22 -2
  170. package/dist/esm/rendering/webgl2/WebGl2MeshRenderer.js +404 -112
  171. package/dist/esm/rendering/webgl2/WebGl2MeshRenderer.js.map +1 -1
  172. package/dist/esm/rendering/webgl2/WebGl2ParticleRenderer.d.ts +8 -0
  173. package/dist/esm/rendering/webgl2/WebGl2ParticleRenderer.js +8 -0
  174. package/dist/esm/rendering/webgl2/WebGl2ParticleRenderer.js.map +1 -1
  175. package/dist/esm/rendering/webgl2/WebGl2PassCoordinator.d.ts +57 -0
  176. package/dist/esm/rendering/webgl2/WebGl2PassCoordinator.js +79 -0
  177. package/dist/esm/rendering/webgl2/WebGl2PassCoordinator.js.map +1 -0
  178. package/dist/esm/rendering/webgl2/WebGl2SpriteRenderer.d.ts +14 -0
  179. package/dist/esm/rendering/webgl2/WebGl2SpriteRenderer.js +257 -78
  180. package/dist/esm/rendering/webgl2/WebGl2SpriteRenderer.js.map +1 -1
  181. package/dist/esm/rendering/webgl2/WebGl2StencilClipper.d.ts +34 -0
  182. package/dist/esm/rendering/webgl2/WebGl2StencilClipper.js +169 -0
  183. package/dist/esm/rendering/webgl2/WebGl2StencilClipper.js.map +1 -0
  184. package/dist/esm/rendering/webgl2/WebGl2TextRenderer.d.ts +7 -0
  185. package/dist/esm/rendering/webgl2/WebGl2TextRenderer.js +11 -0
  186. package/dist/esm/rendering/webgl2/WebGl2TextRenderer.js.map +1 -1
  187. package/dist/esm/rendering/webgl2/glsl/mesh.frag.js +1 -1
  188. package/dist/esm/rendering/webgl2/glsl/mesh.vert.js +1 -1
  189. package/dist/esm/rendering/webgl2/glsl/sprite.vert.js +1 -1
  190. package/dist/esm/rendering/webgl2/glsl/stencil-clip.frag.js +4 -0
  191. package/dist/esm/rendering/webgl2/glsl/stencil-clip.frag.js.map +1 -0
  192. package/dist/esm/rendering/webgl2/glsl/stencil-clip.vert.js +4 -0
  193. package/dist/esm/rendering/webgl2/glsl/stencil-clip.vert.js.map +1 -0
  194. package/dist/esm/rendering/webgl2/glsl/text-color.frag.js +1 -1
  195. package/dist/esm/rendering/webgpu/WebGpuBackend.d.ts +63 -0
  196. package/dist/esm/rendering/webgpu/WebGpuBackend.js +180 -19
  197. package/dist/esm/rendering/webgpu/WebGpuBackend.js.map +1 -1
  198. package/dist/esm/rendering/webgpu/WebGpuMaskCompositor.js +22 -17
  199. package/dist/esm/rendering/webgpu/WebGpuMaskCompositor.js.map +1 -1
  200. package/dist/esm/rendering/webgpu/WebGpuMeshRenderer.d.ts +24 -0
  201. package/dist/esm/rendering/webgpu/WebGpuMeshRenderer.js +524 -98
  202. package/dist/esm/rendering/webgpu/WebGpuMeshRenderer.js.map +1 -1
  203. package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.d.ts +7 -0
  204. package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.js +24 -22
  205. package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.js.map +1 -1
  206. package/dist/esm/rendering/webgpu/WebGpuPassCoordinator.d.ts +141 -0
  207. package/dist/esm/rendering/webgpu/WebGpuPassCoordinator.js +270 -0
  208. package/dist/esm/rendering/webgpu/WebGpuPassCoordinator.js.map +1 -0
  209. package/dist/esm/rendering/webgpu/WebGpuSpriteRenderer.d.ts +25 -1
  210. package/dist/esm/rendering/webgpu/WebGpuSpriteRenderer.js +430 -76
  211. package/dist/esm/rendering/webgpu/WebGpuSpriteRenderer.js.map +1 -1
  212. package/dist/esm/rendering/webgpu/WebGpuStencilClipper.d.ts +57 -0
  213. package/dist/esm/rendering/webgpu/WebGpuStencilClipper.js +257 -0
  214. package/dist/esm/rendering/webgpu/WebGpuStencilClipper.js.map +1 -0
  215. package/dist/esm/rendering/webgpu/WebGpuStencilState.d.ts +14 -0
  216. package/dist/esm/rendering/webgpu/WebGpuStencilState.js +36 -0
  217. package/dist/esm/rendering/webgpu/WebGpuStencilState.js.map +1 -0
  218. package/dist/esm/rendering/webgpu/WebGpuTextRenderer.d.ts +7 -0
  219. package/dist/esm/rendering/webgpu/WebGpuTextRenderer.js +30 -19
  220. package/dist/esm/rendering/webgpu/WebGpuTextRenderer.js.map +1 -1
  221. package/dist/esm/rendering/webgpu/WebGpuTransformStorage.d.ts +48 -0
  222. package/dist/esm/rendering/webgpu/WebGpuTransformStorage.js +103 -0
  223. package/dist/esm/rendering/webgpu/WebGpuTransformStorage.js.map +1 -0
  224. package/dist/esm/resources/JsonStore.d.ts +18 -0
  225. package/dist/esm/resources/JsonStore.js +62 -0
  226. package/dist/esm/resources/JsonStore.js.map +1 -0
  227. package/dist/esm/resources/Loader.js +1 -1
  228. package/dist/esm/resources/Loader.js.map +1 -1
  229. package/dist/esm/resources/factories/ImageFactory.d.ts +14 -8
  230. package/dist/esm/resources/factories/ImageFactory.js +13 -6
  231. package/dist/esm/resources/factories/ImageFactory.js.map +1 -1
  232. package/dist/esm/resources/factories/TextureFactory.d.ts +4 -4
  233. package/dist/esm/resources/factories/TextureFactory.js +8 -4
  234. package/dist/esm/resources/factories/TextureFactory.js.map +1 -1
  235. package/dist/esm/resources/index.d.ts +1 -0
  236. package/dist/exo.esm.js +6326 -2350
  237. package/dist/exo.esm.js.map +1 -1
  238. package/package.json +34 -24
  239. package/dist/esm/particles/distributions/Gradient.js.map +0 -1
  240. package/dist/esm/rendering/mesh/MeshShader.js.map +0 -1
  241. package/dist/esm/vendor/webgl-debug.js +0 -1160
  242. package/dist/esm/vendor/webgl-debug.js.map +0 -1
@@ -1,8 +1,10 @@
1
+ import { spriteVertexWgsl } from '../sprite/spriteMaterialSources.js';
1
2
  import { RenderTexture } from '../texture/RenderTexture.js';
2
3
  import { Texture } from '../texture/Texture.js';
3
4
  import { BlendModes } from '../types.js';
4
5
  import { AbstractWebGpuRenderer } from './AbstractWebGpuRenderer.js';
5
6
  import { getWebGpuBlendState } from './WebGpuBlendState.js';
7
+ import { stencilContentDepthStencilState } from './WebGpuStencilState.js';
6
8
 
7
9
  /// <reference types="@webgpu/types" />
8
10
  const spriteShaderSource = `
@@ -10,8 +12,16 @@ struct ProjectionUniforms {
10
12
  matrix: mat4x4<f32>,
11
13
  };
12
14
 
15
+ struct TransformSlot {
16
+ m0: vec4<f32>,
17
+ m1: vec4<f32>,
18
+ m2: vec4<f32>,
19
+ };
20
+
13
21
  @group(0) @binding(0)
14
22
  var<uniform> projection: ProjectionUniforms;
23
+ @group(0) @binding(1)
24
+ var<storage, read> transforms: array<TransformSlot>;
15
25
 
16
26
  @group(1) @binding(0)
17
27
  var spriteTexture0: texture_2d<f32>;
@@ -47,16 +57,17 @@ var spriteSampler6: sampler;
47
57
  @group(1) @binding(15)
48
58
  var spriteSampler7: sampler;
49
59
 
50
- // Per-instance vertex layout (56 bytes per sprite). The four corners
60
+ // Per-instance vertex layout (36 bytes per sprite). The four corners
51
61
  // of the quad are derived from @builtin(vertex_index) 0..3 inside the
52
- // vertex shader — there is no per-vertex stream.
62
+ // vertex shader — there is no per-vertex stream. The world transform is
63
+ // fetched from the shared transform storage buffer keyed by nodeIndex
64
+ // instead of being packed inline.
53
65
  struct VertexInput {
54
66
  @location(0) localBounds: vec4<f32>, // left, top, right, bottom (local space)
55
- @location(1) transformAB: vec3<f32>, // first row of 2D affine
56
- @location(2) transformCD: vec3<f32>, // second row of 2D affine
57
67
  @location(3) uvBounds: vec4<f32>, // uMin, vMin, uMax, vMax (CPU pre-swaps for flipY)
58
68
  @location(4) color: vec4<f32>, // RGBA tint
59
69
  @location(5) packedSlotFlags: u32, // bits 0..7 = slot, bit 8 = premultiply
70
+ @location(6) nodeIndex: u32, // row into the shared transform storage buffer
60
71
  };
61
72
 
62
73
  struct VertexOutput {
@@ -79,8 +90,12 @@ fn vertexMain(input: VertexInput, @builtin(vertex_index) vid: u32) -> VertexOutp
79
90
  let localX = select(input.localBounds.x, input.localBounds.z, cornerX == 1u);
80
91
  let localY = select(input.localBounds.y, input.localBounds.w, cornerY == 1u);
81
92
 
82
- let worldX = input.transformAB.x * localX + input.transformAB.y * localY + input.transformAB.z;
83
- let worldY = input.transformCD.x * localX + input.transformCD.y * localY + input.transformCD.z;
93
+ // Fetch this instance's world transform from the shared storage buffer,
94
+ // keyed by nodeIndex: m0 = (a, b, c, d), m1 = (tx, ty, 0, 0). (m2 carries the
95
+ // node tint, unused here — the sprite keeps its own per-instance color.)
96
+ let slot = transforms[input.nodeIndex];
97
+ let worldX = slot.m0.x * localX + slot.m0.y * localY + slot.m1.x;
98
+ let worldY = slot.m0.z * localX + slot.m0.w * localY + slot.m1.y;
84
99
 
85
100
  output.position = projection.matrix * vec4<f32>(worldX, worldY, 0.0, 1.0);
86
101
 
@@ -140,11 +155,12 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
140
155
  return resolvedSample * input.color;
141
156
  }
142
157
  `;
143
- const instanceStrideBytes = 56;
158
+ const instanceStrideBytes = 36;
144
159
  const wordsPerInstance = instanceStrideBytes / Uint32Array.BYTES_PER_ELEMENT;
145
160
  const projectionByteLength = 64;
146
161
  const initialBatchCapacity = 32;
147
162
  const maxBatchTextures = 8;
163
+ const maxCustomTextureSlots = 7; // user texture uniforms; group(2) binding 1..N
148
164
  const indicesPerSprite = 6;
149
165
  // Static index buffer: two triangles forming a quad, vertex IDs 0..3 in
150
166
  // TL/TR/BR/BL order so the WGSL `cornerX/cornerY` derivation matches.
@@ -157,7 +173,10 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
157
173
  _textureBindGroupLayout = null;
158
174
  _pipelineLayout = null;
159
175
  _uniformBuffer = null;
160
- _uniformBindGroup = null;
176
+ // group(0) bind group = projection UBO + shared transform storage buffer.
177
+ // Recreated whenever the storage buffer identity changes (capacity growth).
178
+ _transformBindGroup = null;
179
+ _transformStorageBuffer = null;
161
180
  _indexBuffer = null;
162
181
  _instanceBuffer = null;
163
182
  _instanceCapacity = 0;
@@ -169,7 +188,16 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
169
188
  _textureSlots = new Map();
170
189
  _slotCount = 0;
171
190
  _instanceCount = 0;
191
+ // Highest transform-storage row referenced by the pending batch; drives the
192
+ // minimum row count uploaded for the storage buffer at flush time.
193
+ _maxNodeIndex = 0;
172
194
  _currentBlendMode = null;
195
+ // Custom-material state. Per-material pipelines/bind groups are cached; the
196
+ // current batch's material/base-texture decide when to flush.
197
+ _customMaterials = new Map();
198
+ _customBaseTextureLayout = null;
199
+ _currentMaterial = null;
200
+ _currentBaseTexture = null;
173
201
  onConnect(backend) {
174
202
  if (this._device) {
175
203
  return;
@@ -185,6 +213,13 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
185
213
  type: 'uniform',
186
214
  },
187
215
  },
216
+ {
217
+ binding: 1,
218
+ visibility: GPUShaderStage.VERTEX,
219
+ buffer: {
220
+ type: 'read-only-storage',
221
+ },
222
+ },
188
223
  ],
189
224
  });
190
225
  this._textureBindGroupLayout = this._device.createBindGroupLayout({
@@ -208,21 +243,20 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
208
243
  this._pipelineLayout = this._device.createPipelineLayout({
209
244
  bindGroupLayouts: [this._uniformBindGroupLayout, this._textureBindGroupLayout],
210
245
  });
246
+ // Single base-texture layout for the custom-material path (group 1).
247
+ this._customBaseTextureLayout = this._device.createBindGroupLayout({
248
+ entries: [
249
+ { binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
250
+ { binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
251
+ ],
252
+ });
211
253
  this._uniformBuffer = this._device.createBuffer({
212
254
  size: projectionByteLength,
213
255
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
214
256
  });
215
- this._uniformBindGroup = this._device.createBindGroup({
216
- layout: this._uniformBindGroupLayout,
217
- entries: [
218
- {
219
- binding: 0,
220
- resource: {
221
- buffer: this._uniformBuffer,
222
- },
223
- },
224
- ],
225
- });
257
+ // The group(0) bind group also binds the shared transform storage buffer,
258
+ // whose identity changes when its capacity grows — so it is built lazily in
259
+ // flush() once the active storage buffer is known.
226
260
  // Static index buffer for the quad. Allocated once at connect; its
227
261
  // contents never change.
228
262
  this._indexBuffer = this._device.createBuffer({
@@ -235,12 +269,21 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
235
269
  this._instanceBuffer?.destroy();
236
270
  this._indexBuffer?.destroy();
237
271
  this._uniformBuffer?.destroy();
272
+ // Custom materials are owned by user code (one SpriteMaterial can be shared
273
+ // across many sprites); their resources are released when the user calls
274
+ // material.destroy(). On disconnect we eagerly release to avoid GPU leaks.
275
+ for (const resources of this._customMaterials.values()) {
276
+ this._releaseCustomResources(resources);
277
+ }
278
+ this._customMaterials.clear();
238
279
  this._pipelines.clear();
239
280
  this._instanceBuffer = null;
240
281
  this._indexBuffer = null;
241
- this._uniformBindGroup = null;
282
+ this._transformBindGroup = null;
283
+ this._transformStorageBuffer = null;
242
284
  this._uniformBuffer = null;
243
285
  this._pipelineLayout = null;
286
+ this._customBaseTextureLayout = null;
244
287
  this._textureBindGroupLayout = null;
245
288
  this._uniformBindGroupLayout = null;
246
289
  this._shaderModule = null;
@@ -251,7 +294,10 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
251
294
  this._instanceFloat32 = new Float32Array(this._instanceData);
252
295
  this._instanceUint32 = new Uint32Array(this._instanceData);
253
296
  this._instanceCount = 0;
297
+ this._maxNodeIndex = 0;
254
298
  this._currentBlendMode = null;
299
+ this._currentMaterial = null;
300
+ this._currentBaseTexture = null;
255
301
  this._resetSlots();
256
302
  }
257
303
  render(sprite) {
@@ -265,15 +311,33 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
265
311
  (texture instanceof Texture && texture.source === null)) {
266
312
  return;
267
313
  }
314
+ const material = sprite.material;
315
+ // The transform lives in the shared storage buffer, keyed by the draw
316
+ // command's stable nodeIndex (already packed at the draw-command boundary).
317
+ // A direct, non-plan `backend.draw(sprite)` has no command — push the
318
+ // sprite's transform into the buffer and use the freshly-allocated slot.
319
+ const command = backend.activeDrawCommand;
320
+ const nodeIndex = command !== null ? command.nodeIndex : backend._pushTransform(sprite);
321
+ if (material === null) {
322
+ this._renderDefault(sprite, texture, backend, nodeIndex);
323
+ }
324
+ else {
325
+ this._renderCustom(sprite, texture, material, backend, nodeIndex);
326
+ }
327
+ }
328
+ /** Default multi-texture path: rotate the base texture through 8 slots. */
329
+ _renderDefault(sprite, texture, backend, nodeIndex) {
268
330
  const blendMode = sprite.blendMode;
269
- // Flush triggers: blend-mode change, instance buffer full at current
270
- // capacity (we'll grow on next render), or texture-slot exhaustion.
331
+ // Flush triggers: blend-mode change, texture-slot exhaustion, or a custom
332
+ // batch still in flight that must drain first.
271
333
  const blendModeChanged = this._currentBlendMode !== null && blendMode !== this._currentBlendMode;
272
334
  const slotExhausted = !this._textureSlots.has(texture) && this._slotCount >= maxBatchTextures;
273
- if (blendModeChanged || slotExhausted) {
335
+ const materialSwitch = this._currentMaterial !== null && this._instanceCount > 0;
336
+ if (blendModeChanged || slotExhausted || materialSwitch) {
274
337
  this.flush();
275
338
  }
276
339
  this._currentBlendMode = blendMode;
340
+ this._currentMaterial = null;
277
341
  backend.setBlendMode(blendMode);
278
342
  // Resolve / assign texture slot.
279
343
  let slot = this._textureSlots.get(texture);
@@ -288,15 +352,38 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
288
352
  // typed-array writes in _packInstance silently fall off the end of a
289
353
  // too-small buffer.
290
354
  this._ensureInstanceCapacity(this._instanceCount + 1);
291
- this._packInstance(sprite, texture, packedSlotFlags);
355
+ this._packInstance(sprite, texture, packedSlotFlags, nodeIndex);
356
+ this._instanceCount++;
357
+ }
358
+ /** Custom-material path: single base texture on group(1), instanced. */
359
+ _renderCustom(sprite, texture, material, backend, nodeIndex) {
360
+ if (material.shader.wgsl === null) {
361
+ throw new Error('SpriteMaterial shader has no `wgsl` source; cannot render through the WebGPU backend.');
362
+ }
363
+ // The material owns its blend mode; the sprite's own blendMode overrides it
364
+ // when set away from the default (Normal).
365
+ const blendMode = sprite.blendMode === BlendModes.Normal ? material.blendMode : sprite.blendMode;
366
+ const blendModeChanged = this._currentBlendMode !== null && blendMode !== this._currentBlendMode;
367
+ const materialChanged = this._currentMaterial !== null && material !== this._currentMaterial;
368
+ const textureChanged = this._currentBaseTexture !== null && texture !== this._currentBaseTexture;
369
+ const modeSwitch = this._currentMaterial === null && this._instanceCount > 0;
370
+ if (blendModeChanged || materialChanged || textureChanged || modeSwitch) {
371
+ this.flush();
372
+ }
373
+ this._currentBlendMode = blendMode;
374
+ this._currentMaterial = material;
375
+ this._currentBaseTexture = texture;
376
+ backend.setBlendMode(blendMode);
377
+ // textureSlot word is unused by custom fragments (base binds to group(1)).
378
+ this._ensureInstanceCapacity(this._instanceCount + 1);
379
+ this._packInstance(sprite, texture, 0, nodeIndex);
292
380
  this._instanceCount++;
293
381
  }
294
382
  flush() {
295
383
  const backend = this._backend;
296
384
  const device = this._device;
297
385
  const uniformBuffer = this._uniformBuffer;
298
- const uniformBindGroup = this._uniformBindGroup;
299
- if (!backend || !device || !uniformBuffer || !uniformBindGroup) {
386
+ if (!backend || !device || !uniformBuffer) {
300
387
  return;
301
388
  }
302
389
  if (this._instanceCount === 0 && !backend.clearRequested) {
@@ -305,34 +392,66 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
305
392
  const viewMatrix = backend.view.getTransform();
306
393
  this._projectionData.set([viewMatrix.a, viewMatrix.c, 0, 0, viewMatrix.b, viewMatrix.d, 0, 0, 0, 0, 1, 0, viewMatrix.x, viewMatrix.y, 0, viewMatrix.z]);
307
394
  device.queue.writeBuffer(uniformBuffer, 0, this._projectionData.buffer, this._projectionData.byteOffset, this._projectionData.byteLength);
308
- const encoder = device.createCommandEncoder();
309
- const pass = encoder.beginRenderPass({
310
- colorAttachments: [backend.createColorAttachment()],
311
- });
312
- backend.stats.renderPasses++;
313
395
  const scissor = backend.getScissorRect();
314
396
  const maskClipsAll = scissor !== null && (scissor.width <= 0 || scissor.height <= 0);
315
- if (scissor !== null && !maskClipsAll) {
316
- pass.setScissorRect(scissor.x, scissor.y, scissor.width, scissor.height);
317
- }
397
+ // The coordinator owns the GPU pass: it opens the encoder + render pass
398
+ // (load/clear resolution, pass count and scissor are applied there) and
399
+ // ends + submits it below.
400
+ const pass = backend._passCoordinator.acquirePass().pass;
318
401
  if (this._instanceCount > 0 && !maskClipsAll && this._instanceBuffer !== null && this._indexBuffer !== null && this._currentBlendMode !== null) {
319
402
  device.queue.writeBuffer(this._instanceBuffer, 0, this._instanceData, 0, this._instanceCount * instanceStrideBytes);
320
- const pipeline = this._getPipeline(this._currentBlendMode, backend.renderTargetFormat);
321
- const textureBindGroup = this._createTextureBindGroup(device, backend);
322
- pass.setPipeline(pipeline);
323
- pass.setBindGroup(0, uniformBindGroup);
324
- pass.setBindGroup(1, textureBindGroup);
325
- pass.setVertexBuffer(0, this._instanceBuffer);
326
- pass.setIndexBuffer(this._indexBuffer, 'uint16');
327
- pass.drawIndexed(indicesPerSprite, this._instanceCount, 0, 0, 0);
403
+ // Resolve the shared transform storage buffer (rows uploaded up to the
404
+ // max nodeIndex referenced by this batch) and bind it alongside the
405
+ // projection UBO on group(0). Both the default and custom programs fetch
406
+ // the world transform from it via nodeIndex.
407
+ const storage = backend.getTransformStorageBuffer(this._maxNodeIndex + 1);
408
+ const transformBindGroup = this._getOrCreateTransformBindGroup(device, uniformBuffer, storage.buffer);
409
+ const material = this._currentMaterial;
410
+ const stencil = backend._passCoordinator.stencilActive;
411
+ if (material === null) {
412
+ const pipeline = this._getPipeline(this._currentBlendMode, backend.renderTargetFormat, stencil);
413
+ const textureBindGroup = this._createTextureBindGroup(device, backend);
414
+ pass.setPipeline(pipeline);
415
+ pass.setBindGroup(0, transformBindGroup);
416
+ pass.setBindGroup(1, textureBindGroup);
417
+ pass.setVertexBuffer(0, this._instanceBuffer);
418
+ pass.setIndexBuffer(this._indexBuffer, 'uint16');
419
+ pass.drawIndexed(indicesPerSprite, this._instanceCount, 0, 0, 0);
420
+ }
421
+ else {
422
+ pass.pushDebugGroup('SpriteMaterial (custom)');
423
+ this._drawCustomBatch(pass, device, backend, material, transformBindGroup, stencil);
424
+ pass.popDebugGroup();
425
+ }
328
426
  backend.stats.batches++;
329
427
  backend.stats.drawCalls++;
330
428
  }
331
- pass.end();
332
- backend.submit(encoder.finish());
429
+ backend._passCoordinator.endPass();
333
430
  this._instanceCount = 0;
431
+ this._maxNodeIndex = 0;
334
432
  this._resetSlots();
335
433
  this._currentBlendMode = null;
434
+ this._currentMaterial = null;
435
+ this._currentBaseTexture = null;
436
+ }
437
+ /**
438
+ * Build (or reuse) the group(0) bind group pairing the fixed projection UBO
439
+ * with the shared transform storage buffer. Cached against the storage buffer
440
+ * identity, which changes only when its capacity grows.
441
+ */
442
+ _getOrCreateTransformBindGroup(device, uniformBuffer, storageBuffer) {
443
+ if (this._transformBindGroup !== null && this._transformStorageBuffer === storageBuffer) {
444
+ return this._transformBindGroup;
445
+ }
446
+ this._transformStorageBuffer = storageBuffer;
447
+ this._transformBindGroup = device.createBindGroup({
448
+ layout: this._uniformBindGroupLayout,
449
+ entries: [
450
+ { binding: 0, resource: { buffer: uniformBuffer } },
451
+ { binding: 1, resource: { buffer: storageBuffer } },
452
+ ],
453
+ });
454
+ return this._transformBindGroup;
336
455
  }
337
456
  destroy() {
338
457
  this.disconnect();
@@ -380,24 +499,18 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
380
499
  }
381
500
  await Promise.all(promises);
382
501
  }
383
- _packInstance(sprite, texture, packedSlotFlags) {
502
+ _packInstance(sprite, texture, packedSlotFlags, nodeIndex) {
384
503
  const offset = this._instanceCount * wordsPerInstance;
385
504
  const f32 = this._instanceFloat32;
386
505
  const u32 = this._instanceUint32;
506
+ // localBounds: left, top, right, bottom (words 0..3, offset 0)
387
507
  const bounds = sprite.getLocalBounds();
388
508
  f32[offset + 0] = bounds.left;
389
509
  f32[offset + 1] = bounds.top;
390
510
  f32[offset + 2] = bounds.right;
391
511
  f32[offset + 3] = bounds.bottom;
392
- const transform = sprite.getGlobalTransform();
393
- f32[offset + 4] = transform.a;
394
- f32[offset + 5] = transform.b;
395
- f32[offset + 6] = transform.x;
396
- f32[offset + 7] = transform.c;
397
- f32[offset + 8] = transform.d;
398
- f32[offset + 9] = transform.y;
399
- // uvBounds: u16x4 normalised, packed into two u32 slots. The CPU
400
- // applies the flipY swap so the shader stays orientation-agnostic.
512
+ // uvBounds: u16x4 normalised, packed into two u32 slots (words 4,5, offset
513
+ // 16). The CPU applies the flipY swap so the shader stays orientation-agnostic.
401
514
  const frame = sprite.textureFrame;
402
515
  const texWidth = texture.width;
403
516
  const texHeight = texture.height;
@@ -408,10 +521,18 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
408
521
  const flipY = texture instanceof Texture && texture.flipY;
409
522
  const vMin = flipY ? vMaxRaw : vMinRaw;
410
523
  const vMax = flipY ? vMinRaw : vMaxRaw;
411
- u32[offset + 10] = uMin | (vMin << 16);
412
- u32[offset + 11] = uMax | (vMax << 16);
413
- u32[offset + 12] = sprite.tint.toRgba();
414
- u32[offset + 13] = packedSlotFlags;
524
+ u32[offset + 4] = uMin | (vMin << 16);
525
+ u32[offset + 5] = uMax | (vMax << 16);
526
+ // color (u8x4 packed) at word 6 (offset 24)
527
+ u32[offset + 6] = sprite.tint.toRgba();
528
+ // packedSlotFlags (u32) at word 7 (offset 28)
529
+ u32[offset + 7] = packedSlotFlags;
530
+ // nodeIndex (u32) at word 8 (offset 32) — row into the shared transform buffer.
531
+ const node = nodeIndex >>> 0;
532
+ u32[offset + 8] = node;
533
+ if (node > this._maxNodeIndex) {
534
+ this._maxNodeIndex = node;
535
+ }
415
536
  }
416
537
  _ensureInstanceCapacity(instanceCount) {
417
538
  if (!this._device || instanceCount <= this._instanceCapacity) {
@@ -481,8 +602,8 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
481
602
  entries,
482
603
  });
483
604
  }
484
- _getPipeline(blendMode, format) {
485
- const pipelineKey = `${blendMode}:${format}`;
605
+ _getPipeline(blendMode, format, stencil) {
606
+ const pipelineKey = `${blendMode}:${format}:${stencil ? 's' : 'n'}`;
486
607
  const existingPipeline = this._pipelines.get(pipelineKey);
487
608
  if (existingPipeline) {
488
609
  return existingPipeline;
@@ -490,15 +611,15 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
490
611
  if (!this._device || !this._shaderModule || !this._pipelineLayout || !this._backend) {
491
612
  throw new Error('Renderer has to be connected first!');
492
613
  }
493
- const pipeline = this._device.createRenderPipeline(this._buildPipelineDescriptor(blendMode, format));
614
+ const pipeline = this._device.createRenderPipeline(this._buildPipelineDescriptor(blendMode, format, stencil));
494
615
  this._pipelines.set(pipelineKey, pipeline);
495
616
  return pipeline;
496
617
  }
497
- _buildPipelineDescriptor(blendMode, format) {
618
+ _buildPipelineDescriptor(blendMode, format, stencil = false) {
498
619
  if (!this._shaderModule || !this._pipelineLayout) {
499
620
  throw new Error('Renderer has to be connected first!');
500
621
  }
501
- return {
622
+ const descriptor = {
502
623
  layout: this._pipelineLayout,
503
624
  vertex: {
504
625
  module: this._shaderModule,
@@ -513,29 +634,24 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
513
634
  offset: 0,
514
635
  format: 'float32x4',
515
636
  },
516
- {
517
- shaderLocation: 1,
518
- offset: 16,
519
- format: 'float32x3',
520
- },
521
- {
522
- shaderLocation: 2,
523
- offset: 28,
524
- format: 'float32x3',
525
- },
526
637
  {
527
638
  shaderLocation: 3,
528
- offset: 40,
639
+ offset: 16,
529
640
  format: 'unorm16x4',
530
641
  },
531
642
  {
532
643
  shaderLocation: 4,
533
- offset: 48,
644
+ offset: 24,
534
645
  format: 'unorm8x4',
535
646
  },
536
647
  {
537
648
  shaderLocation: 5,
538
- offset: 52,
649
+ offset: 28,
650
+ format: 'uint32',
651
+ },
652
+ {
653
+ shaderLocation: 6,
654
+ offset: 32,
539
655
  format: 'uint32',
540
656
  },
541
657
  ],
@@ -557,7 +673,245 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
557
673
  topology: 'triangle-list',
558
674
  },
559
675
  };
676
+ if (stencil) {
677
+ descriptor.depthStencil = stencilContentDepthStencilState();
678
+ }
679
+ return descriptor;
680
+ }
681
+ // ---------------------------------------------------------------------------
682
+ // Custom-material path
683
+ // ---------------------------------------------------------------------------
684
+ _drawCustomBatch(pass, device, backend, material, transformBindGroup, stencil) {
685
+ const resources = this._getOrCreateCustomResources(material, device);
686
+ const baseTexture = this._currentBaseTexture ?? Texture.empty;
687
+ // Re-built every frame so mutations to material.uniforms.X are picked up.
688
+ this._uploadUserUniforms(material, resources, device);
689
+ const pipeline = this._getOrCreateCustomPipeline(resources, this._currentBlendMode, backend.renderTargetFormat, stencil, device);
690
+ pass.setPipeline(pipeline);
691
+ pass.setBindGroup(0, transformBindGroup);
692
+ pass.setBindGroup(1, this._getCustomBaseTextureBindGroup(resources, backend, baseTexture, device));
693
+ pass.setBindGroup(2, this._buildUserBindGroup(material, resources, backend, device));
694
+ pass.setVertexBuffer(0, this._instanceBuffer);
695
+ pass.setIndexBuffer(this._indexBuffer, 'uint16');
696
+ pass.drawIndexed(indicesPerSprite, this._instanceCount, 0, 0, 0);
697
+ }
698
+ _getOrCreateCustomResources(material, device) {
699
+ const existing = this._customMaterials.get(material);
700
+ if (existing !== undefined) {
701
+ return existing;
702
+ }
703
+ const wgsl = material.shader.wgsl;
704
+ if (wgsl === null) {
705
+ throw new Error('SpriteMaterial shader has no `wgsl` source; cannot render through the WebGPU backend.');
706
+ }
707
+ // The engine owns the vertex stage: prepend the canonical sprite vertex
708
+ // module (VertexInput/VertexOutput, group(0) projection + transform storage,
709
+ // group(1) base texture + sampler) to the material's fragment WGSL.
710
+ const shaderModule = device.createShaderModule({ code: `${spriteVertexWgsl}\n${wgsl}` });
711
+ const userLayout = this._buildUserBindGroupLayout(device, material);
712
+ const pipelineLayout = device.createPipelineLayout({
713
+ bindGroupLayouts: [this._uniformBindGroupLayout, this._customBaseTextureLayout, userLayout],
714
+ });
715
+ const resources = {
716
+ shaderModule,
717
+ userLayout,
718
+ pipelineLayout,
719
+ pipelines: new Map(),
720
+ userUniformBuffer: null,
721
+ userUniformBufferCapacity: 0,
722
+ baseTextureBindGroups: new WeakMap(),
723
+ };
724
+ this._customMaterials.set(material, resources);
725
+ material._onDispose(() => {
726
+ const stored = this._customMaterials.get(material);
727
+ if (stored !== undefined) {
728
+ this._releaseCustomResources(stored);
729
+ this._customMaterials.delete(material);
730
+ }
731
+ });
732
+ return resources;
733
+ }
734
+ _getOrCreateCustomPipeline(resources, blendMode, format, stencil, device) {
735
+ const cacheKey = `${blendMode}:${format}:${stencil ? 's' : 'n'}`;
736
+ const existing = resources.pipelines.get(cacheKey);
737
+ if (existing !== undefined) {
738
+ return existing;
739
+ }
740
+ const descriptor = {
741
+ layout: resources.pipelineLayout,
742
+ vertex: {
743
+ module: resources.shaderModule,
744
+ entryPoint: 'vertexMain',
745
+ buffers: [
746
+ {
747
+ arrayStride: instanceStrideBytes,
748
+ stepMode: 'instance',
749
+ attributes: [
750
+ { shaderLocation: 0, offset: 0, format: 'float32x4' },
751
+ { shaderLocation: 3, offset: 16, format: 'unorm16x4' },
752
+ { shaderLocation: 4, offset: 24, format: 'unorm8x4' },
753
+ { shaderLocation: 5, offset: 28, format: 'uint32' },
754
+ { shaderLocation: 6, offset: 32, format: 'uint32' },
755
+ ],
756
+ },
757
+ ],
758
+ },
759
+ fragment: {
760
+ module: resources.shaderModule,
761
+ entryPoint: 'fragmentMain',
762
+ targets: [
763
+ {
764
+ format,
765
+ blend: getWebGpuBlendState(blendMode),
766
+ writeMask: GPUColorWrite.ALL,
767
+ },
768
+ ],
769
+ },
770
+ primitive: {
771
+ topology: 'triangle-list',
772
+ },
773
+ };
774
+ if (stencil) {
775
+ descriptor.depthStencil = stencilContentDepthStencilState();
776
+ }
777
+ const pipeline = device.createRenderPipeline(descriptor);
778
+ resources.pipelines.set(cacheKey, pipeline);
779
+ return pipeline;
780
+ }
781
+ _getCustomBaseTextureBindGroup(resources, backend, texture, device) {
782
+ const existing = resources.baseTextureBindGroups.get(texture);
783
+ if (existing !== undefined) {
784
+ return existing;
785
+ }
786
+ const binding = backend.getTextureBinding(texture);
787
+ const group = device.createBindGroup({
788
+ layout: this._customBaseTextureLayout,
789
+ entries: [
790
+ { binding: 0, resource: binding.view },
791
+ { binding: 1, resource: binding.sampler },
792
+ ],
793
+ });
794
+ resources.baseTextureBindGroups.set(texture, group);
795
+ return group;
796
+ }
797
+ _buildUserBindGroupLayout(device, material) {
798
+ const entries = [];
799
+ // Binding 0 always reserved for the user UBO (even if empty), so the layout
800
+ // is stable across user-uniform mutations.
801
+ entries.push({
802
+ binding: 0,
803
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
804
+ buffer: { type: 'uniform' },
805
+ });
806
+ const textureBindings = collectTextureBindings(material);
807
+ if (textureBindings.length > maxCustomTextureSlots) {
808
+ throw new Error(`SpriteMaterial requested more than ${maxCustomTextureSlots} user texture bindings.`);
809
+ }
810
+ let bindingIndex = 1;
811
+ for (let t = 0; t < textureBindings.length; t++) {
812
+ entries.push({ binding: bindingIndex, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } });
813
+ bindingIndex++;
814
+ entries.push({ binding: bindingIndex, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } });
815
+ bindingIndex++;
816
+ }
817
+ return device.createBindGroupLayout({ entries });
818
+ }
819
+ _uploadUserUniforms(material, resources, device) {
820
+ const scalarValues = collectScalarUniforms(material);
821
+ // Always create a UBO (even if empty) since binding 0 of the user layout is
822
+ // fixed. Min size 16 bytes to satisfy WebGPU's minimum buffer size.
823
+ const slotCount = Math.max(scalarValues.length, 1);
824
+ const bufferBytes = slotCount * 16;
825
+ if (resources.userUniformBuffer === null || resources.userUniformBufferCapacity < bufferBytes) {
826
+ resources.userUniformBuffer?.destroy();
827
+ resources.userUniformBufferCapacity = bufferBytes;
828
+ resources.userUniformBuffer = device.createBuffer({
829
+ size: bufferBytes,
830
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
831
+ });
832
+ }
833
+ const data = new Float32Array(bufferBytes / 4);
834
+ let slot = 0;
835
+ for (const value of scalarValues) {
836
+ const baseFloatIndex = slot * 4;
837
+ if (typeof value === 'number') {
838
+ data[baseFloatIndex] = value;
839
+ }
840
+ else if (value instanceof Float32Array) {
841
+ data.set(value, baseFloatIndex);
842
+ }
843
+ else if (value instanceof Int32Array) {
844
+ for (let i = 0; i < value.length; i++) {
845
+ data[baseFloatIndex + i] = value[i];
846
+ }
847
+ }
848
+ else {
849
+ const arr = value;
850
+ for (let i = 0; i < arr.length; i++) {
851
+ data[baseFloatIndex + i] = arr[i];
852
+ }
853
+ }
854
+ slot++;
855
+ }
856
+ device.queue.writeBuffer(resources.userUniformBuffer, 0, data);
857
+ }
858
+ _buildUserBindGroup(material, resources, backend, device) {
859
+ const entries = [];
860
+ entries.push({ binding: 0, resource: { buffer: resources.userUniformBuffer } });
861
+ let bindingIndex = 1;
862
+ for (const texture of collectTextureBindings(material)) {
863
+ const binding = backend.getTextureBinding(texture);
864
+ entries.push({ binding: bindingIndex, resource: binding.view });
865
+ bindingIndex++;
866
+ entries.push({ binding: bindingIndex, resource: binding.sampler });
867
+ bindingIndex++;
868
+ }
869
+ return device.createBindGroup({ layout: resources.userLayout, entries });
870
+ }
871
+ _releaseCustomResources(resources) {
872
+ resources.userUniformBuffer?.destroy();
873
+ resources.pipelines.clear();
874
+ resources.userUniformBuffer = null;
875
+ resources.userUniformBufferCapacity = 0;
876
+ resources.baseTextureBindGroups = new WeakMap();
877
+ }
878
+ }
879
+ function isTextureUniform(value) {
880
+ return (typeof value === 'object' &&
881
+ value !== null &&
882
+ 'width' in value &&
883
+ 'height' in value &&
884
+ !(value instanceof Float32Array) &&
885
+ !(value instanceof Int32Array) &&
886
+ !Array.isArray(value));
887
+ }
888
+ /** Scalar/vector/matrix uniforms (texture values excluded) in declaration order. */
889
+ function collectScalarUniforms(material) {
890
+ const result = [];
891
+ for (const value of Object.values(material.uniforms)) {
892
+ if (!isTextureUniform(value)) {
893
+ result.push(value);
894
+ }
895
+ }
896
+ return result;
897
+ }
898
+ /**
899
+ * Texture bindings claimed by the material, in a stable order: texture-valued
900
+ * entries of `uniforms` first (declaration order), then the dedicated
901
+ * `textures` map. The WGSL source must declare its `@group(2)` texture/sampler
902
+ * pairs in this same order.
903
+ */
904
+ function collectTextureBindings(material) {
905
+ const result = [];
906
+ for (const value of Object.values(material.uniforms)) {
907
+ if (isTextureUniform(value)) {
908
+ result.push(value);
909
+ }
910
+ }
911
+ for (const texture of Object.values(material.textures)) {
912
+ result.push(texture);
560
913
  }
914
+ return result;
561
915
  }
562
916
 
563
917
  export { WebGpuSpriteRenderer };