@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
@@ -3,7 +3,9 @@ import { Texture } from '../texture/Texture.js';
3
3
  import { BlendModes } from '../types.js';
4
4
  import { AbstractWebGpuRenderer } from './AbstractWebGpuRenderer.js';
5
5
  import { getWebGpuBlendState } from './WebGpuBlendState.js';
6
+ import { stencilContentDepthStencilState } from './WebGpuStencilState.js';
6
7
 
8
+ /* eslint-disable max-lines */
7
9
  /// <reference types="@webgpu/types" />
8
10
  const meshShaderSource = `
9
11
  struct VertexInput {
@@ -47,6 +49,65 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
47
49
  return vec4<f32>(modulated.rgb * modulated.a, modulated.a);
48
50
  }
49
51
  `;
52
+ const instancedMeshShaderSource = `
53
+ struct VertexInput {
54
+ @location(0) position: vec2<f32>,
55
+ @location(1) texcoord: vec2<f32>,
56
+ @location(2) color: vec4<f32>,
57
+ @location(6) nodeIndex: u32,
58
+ };
59
+
60
+ struct VertexOutput {
61
+ @builtin(position) position: vec4<f32>,
62
+ @location(0) texcoord: vec2<f32>,
63
+ @location(1) color: vec4<f32>,
64
+ @location(2) tint: vec4<f32>,
65
+ @location(3) @interpolate(flat) premultiplySample: u32,
66
+ };
67
+
68
+ struct TransformSlot {
69
+ m0: vec4<f32>,
70
+ m1: vec4<f32>,
71
+ m2: vec4<f32>,
72
+ };
73
+
74
+ struct TransformUniforms {
75
+ projection: mat3x3<f32>,
76
+ flags: vec4<f32>,
77
+ };
78
+
79
+ @group(0) @binding(0) var<uniform> uniforms: TransformUniforms;
80
+ @group(0) @binding(1) var<storage, read> transforms: array<TransformSlot>;
81
+
82
+ @group(1) @binding(0) var meshTexture: texture_2d<f32>;
83
+ @group(1) @binding(1) var meshSampler: sampler;
84
+
85
+ @vertex
86
+ fn vertexMain(input: VertexInput) -> VertexOutput {
87
+ let slot = transforms[input.nodeIndex];
88
+ let world = vec3<f32>(
89
+ slot.m0.x * input.position.x + slot.m0.z * input.position.y + slot.m1.x,
90
+ slot.m0.y * input.position.x + slot.m0.w * input.position.y + slot.m1.y,
91
+ 1.0
92
+ );
93
+
94
+ var output: VertexOutput;
95
+ output.position = vec4<f32>((uniforms.projection * world).xy, 0.0, 1.0);
96
+ output.texcoord = input.texcoord;
97
+ output.color = input.color;
98
+ output.tint = slot.m2;
99
+ output.premultiplySample = u32(uniforms.flags.x);
100
+ return output;
101
+ }
102
+
103
+ @fragment
104
+ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
105
+ let sample = textureSample(meshTexture, meshSampler, input.texcoord);
106
+ let resolvedSample = select(sample, vec4(sample.rgb * sample.a, sample.a), input.premultiplySample == 1u);
107
+ let modulated = resolvedSample * input.color * input.tint;
108
+ return vec4<f32>(modulated.rgb * modulated.a, modulated.a);
109
+ }
110
+ `;
50
111
  // Per-vertex layout (20 bytes): pos f32x2 + uv f32x2 + color u8x4-norm.
51
112
  // Default-shader path bakes the (view * globalTransform) into position so the
52
113
  // vertex shader stays branchless and uniform-free except for the per-mesh tint.
@@ -55,29 +116,53 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
55
116
  const vertexStrideBytes = 20;
56
117
  const wordsPerVertex = vertexStrideBytes / 4;
57
118
  const tintByteLength = 32; // vec4 tint + vec4 flags (only flags.x used)
119
+ const transformUniformByteLength = 64; // mat3x3<f32> (48B) + vec4<f32> flags (16B)
58
120
  // Custom-shader uniform layout:
59
121
  // mat3x3<f32> projection — 48 bytes (3 vec3 columns padded to vec4 in WGSL)
60
122
  // mat3x3<f32> translation — 48 bytes
61
123
  // vec4<f32> tint — 16 bytes
62
124
  // Total: 112 bytes; aligned up to 256 for dynamic offset.
63
125
  const customMeshUniformBytes = 112;
126
+ /**
127
+ * Cache key for the default + instanced (static-batch) pipeline maps. The
128
+ * stencil dimension keeps the clip and no-clip variants distinct, mirroring the
129
+ * sprite renderer: a stencil pipeline carries depth/stencil state and is only
130
+ * valid in a pass with the matching attachment, so the two are never
131
+ * interchangeable.
132
+ */
133
+ function meshPipelineCacheKey(blendMode, format, stencil) {
134
+ return `${blendMode}:${format}:${stencil ? 's' : 'n'}`;
135
+ }
64
136
  const meshUniformAlignment = 256;
65
137
  const maxCustomTextureSlots = 7; // user texture uniforms; group 2 binding 1..N
66
138
  class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
67
139
  _combinedTransform = new Matrix();
68
140
  _drawCalls = [];
69
141
  _pipelines = new Map();
142
+ _instancedPipelines = new Map();
143
+ _staticGeometryCache = new Map();
70
144
  _textureBindGroups = new WeakMap();
71
145
  _customShaders = new Map();
72
146
  _device = null;
73
147
  _shaderModule = null;
148
+ _instancedShaderModule = null;
74
149
  _uniformBindGroupLayout = null;
150
+ _instancedTransformBindGroupLayout = null;
75
151
  _textureBindGroupLayout = null;
76
152
  _pipelineLayout = null;
153
+ _instancedPipelineLayout = null;
77
154
  _vertexBuffer = null;
78
155
  _indexBuffer = null;
79
156
  _uniformBuffer = null;
80
157
  _uniformBindGroup = null;
158
+ _instancedUniformBuffer = null;
159
+ _instancedUniformBufferCapacity = 0;
160
+ _instancedUniformScratch = new Float32Array(transformUniformByteLength / Float32Array.BYTES_PER_ELEMENT);
161
+ _instancedNodeIndexBuffer = null;
162
+ _instancedNodeIndexBufferCapacity = 0;
163
+ _instancedNodeIndexData = new Uint32Array(0);
164
+ _instancedTransformBindGroup = null;
165
+ _instancedTransformStorageBuffer = null;
81
166
  _uniformAlignment = 256;
82
167
  _vertexBufferCapacity = 0;
83
168
  _indexBufferCapacity = 0;
@@ -92,17 +177,21 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
92
177
  if (backend === null) {
93
178
  throw new Error('WebGpuMeshRenderer is not connected to a backend.');
94
179
  }
95
- const customShader = mesh.shader;
96
- if (customShader !== null && customShader.wgsl === null) {
97
- throw new Error('MeshShader has no `wgsl` source; cannot render through the WebGPU backend.');
180
+ const customShader = mesh.material;
181
+ if (customShader !== null && customShader.shader.wgsl === null) {
182
+ throw new Error('Mesh material shader has no `wgsl` source; cannot render through the WebGPU backend.');
98
183
  }
99
184
  const vertexCount = mesh.vertexCount;
100
185
  if (vertexCount === 0) {
101
186
  return;
102
187
  }
103
- const blendMode = mesh.blendMode;
188
+ // The material owns its blend mode; the mesh's own blendMode overrides it
189
+ // when set away from the default (Normal). Default-path meshes keep their
190
+ // own blendMode verbatim.
191
+ const blendMode = customShader !== null && mesh.blendMode === BlendModes.Normal ? customShader.blendMode : mesh.blendMode;
104
192
  backend.setBlendMode(blendMode);
105
193
  const meshTexture = mesh.texture ?? Texture.white;
194
+ const command = backend.activeDrawCommand;
106
195
  // backend.shouldPremultiplyTextureSample expects RenderTexture-or-Texture.
107
196
  // Both branches are valid here. Premultiply flag is ignored by custom
108
197
  // shaders (they handle premultiplication themselves), but we still record
@@ -123,6 +212,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
123
212
  const drawCall = {
124
213
  mesh,
125
214
  customShader,
215
+ command,
126
216
  blendMode,
127
217
  texture: meshTexture,
128
218
  premultiplySample,
@@ -151,13 +241,8 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
151
241
  // Honor a pending clear with an empty pass so createColorAttachment
152
242
  // consumes the clear-state once.
153
243
  if (backend.clearRequested) {
154
- const encoder = device.createCommandEncoder();
155
- const pass = encoder.beginRenderPass({
156
- colorAttachments: [backend.createColorAttachment()],
157
- });
158
- backend.stats.renderPasses++;
159
- pass.end();
160
- backend.submit(encoder.finish());
244
+ backend._passCoordinator.acquirePass();
245
+ backend._passCoordinator.endPass();
161
246
  }
162
247
  this._resetFrame();
163
248
  return;
@@ -166,7 +251,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
166
251
  // buffers, so default offsets are independent of custom offsets).
167
252
  let defaultVertices = 0;
168
253
  let defaultIndices = 0;
169
- const customVertexCursors = new Map(); // running vertex count per shader
254
+ const customVertexCursors = new Map(); // running vertex count per material
170
255
  const customIndexCursors = new Map();
171
256
  for (let i = 0; i < this._drawCallCount; i++) {
172
257
  const dc = this._drawCalls[i];
@@ -192,6 +277,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
192
277
  // each custom-shader resource manages its own.
193
278
  const defaultDrawCalls = this._drawCallCount - this._totalCustomDraws();
194
279
  this._ensureUniformCapacity(defaultDrawCalls);
280
+ this._ensureInstancedUniformCapacity(this._drawCallCount);
195
281
  // Phase 3: pack default-path vertex/index/uniform data.
196
282
  const defaultUniformBytes = defaultDrawCalls * this._uniformAlignment;
197
283
  const defaultUniformData = defaultUniformBytes > 0 ? new ArrayBuffer(defaultUniformBytes) : null;
@@ -211,13 +297,17 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
211
297
  this._packedIndexData[start + j] = j;
212
298
  }
213
299
  }
214
- // Pack tint+flags for default path.
300
+ // Pack tint+flags for default path. Color RGB channels are 0..255; the
301
+ // shader multiplies the sampled texel by this tint, so normalize to
302
+ // 0..1 (matching TransformBuffer and the WebGL2 mesh shader). Leaving
303
+ // them at 0..255 scales every non-zero texel channel past 1.0, which
304
+ // clamps intermediate colors (gradients, photos) to full saturation.
215
305
  if (defaultUniformF32 !== null) {
216
306
  const offsetWords = (defaultUniformIndex * this._uniformAlignment) / Float32Array.BYTES_PER_ELEMENT;
217
307
  const tint = dc.mesh.tint;
218
- defaultUniformF32[offsetWords + 0] = tint.r;
219
- defaultUniformF32[offsetWords + 1] = tint.g;
220
- defaultUniformF32[offsetWords + 2] = tint.b;
308
+ defaultUniformF32[offsetWords + 0] = tint.r / 255;
309
+ defaultUniformF32[offsetWords + 1] = tint.g / 255;
310
+ defaultUniformF32[offsetWords + 2] = tint.b / 255;
221
311
  defaultUniformF32[offsetWords + 3] = tint.a;
222
312
  defaultUniformF32[offsetWords + 4] = dc.premultiplySample ? 1 : 0;
223
313
  defaultUniformF32[offsetWords + 5] = 0;
@@ -227,8 +317,8 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
227
317
  defaultUniformIndex++;
228
318
  }
229
319
  }
230
- // Phase 3b: pack custom-path vertex/index/uniform data per shader.
231
- for (const [shader, resources] of this._customShaders) {
320
+ // Phase 3b: pack custom-path vertex/index/uniform data per material.
321
+ for (const [material, resources] of this._customShaders) {
232
322
  if (resources.drawCount === 0) {
233
323
  continue;
234
324
  }
@@ -239,7 +329,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
239
329
  let drawCursor = 0;
240
330
  for (let i = 0; i < this._drawCallCount; i++) {
241
331
  const dc = this._drawCalls[i];
242
- if (dc.customShader !== shader)
332
+ if (dc.customShader !== material)
243
333
  continue;
244
334
  this._writeMeshVerticesIntoBuffer(dc.mesh, vWritten, resources.vertexFloatView, resources.vertexUintView);
245
335
  if (dc.mesh.indices !== null) {
@@ -251,50 +341,85 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
251
341
  }
252
342
  }
253
343
  // Write mesh-uniform slot (proj/trans/tint) with dynamic offset.
254
- this._writeCustomMeshUniform(shader, resources, drawCursor, dc.mesh, backend);
344
+ this._writeCustomMeshUniform(material, resources, drawCursor, dc.mesh, backend);
255
345
  vWritten += dc.vertexCount;
256
346
  iWritten += dc.indexCount;
257
347
  drawCursor++;
258
348
  }
259
349
  device.queue.writeBuffer(resources.vertexBuffer, 0, resources.vertexData, 0, resources.totalVertices * vertexStrideBytes);
260
- device.queue.writeBuffer(resources.indexBuffer, 0, resources.indexData.buffer, resources.indexData.byteOffset, resources.totalIndices * Uint16Array.BYTES_PER_ELEMENT);
261
- // Build/refresh user uniform UBO from shader.uniforms (re-built every
262
- // frame so mutations to shader.uniforms.X are picked up).
263
- this._uploadUserUniforms(shader, resources);
350
+ device.queue.writeBuffer(resources.indexBuffer, 0, resources.indexData.buffer, resources.indexData.byteOffset, (resources.totalIndices * Uint16Array.BYTES_PER_ELEMENT + 3) & -4);
351
+ // Build/refresh user uniform UBO from the material (re-built every frame
352
+ // so mutations to material.uniforms.X are picked up).
353
+ this._uploadUserUniforms(material, resources);
264
354
  }
265
355
  // Phase 4: single writeBuffer per resource for the default path.
266
356
  if (defaultVertices > 0) {
267
357
  device.queue.writeBuffer(this._vertexBuffer, 0, this._vertexData, 0, defaultVertices * vertexStrideBytes);
268
- device.queue.writeBuffer(this._indexBuffer, 0, this._packedIndexData.buffer, this._packedIndexData.byteOffset, defaultIndices * Uint16Array.BYTES_PER_ELEMENT);
358
+ device.queue.writeBuffer(this._indexBuffer, 0, this._packedIndexData.buffer, this._packedIndexData.byteOffset, (defaultIndices * Uint16Array.BYTES_PER_ELEMENT + 3) & -4);
269
359
  }
270
360
  if (defaultUniformData !== null) {
271
361
  device.queue.writeBuffer(this._uniformBuffer, 0, defaultUniformData, 0, defaultUniformBytes);
272
362
  }
273
363
  // Phase 5: single render pass with one drawIndexed per mesh, switching
274
- // pipeline+bind groups between default and custom paths as needed.
275
- const encoder = device.createCommandEncoder({ label: 'WebGpuMeshRenderer' });
276
- const pass = encoder.beginRenderPass({
277
- colorAttachments: [backend.createColorAttachment()],
278
- label: 'WebGpuMeshRenderer pass',
279
- });
280
- backend.stats.renderPasses++;
281
- if (scissor !== null) {
282
- pass.setScissorRect(scissor.x, scissor.y, scissor.width, scissor.height);
283
- }
364
+ // pipeline+bind groups between default and custom paths as needed. The
365
+ // coordinator owns the GPU pass (load/clear resolution, pass count and
366
+ // scissor are applied there) and ends + submits it below.
367
+ const pass = backend._passCoordinator.acquirePass().pass;
284
368
  const renderTargetFormat = backend.renderTargetFormat;
369
+ // A clip scope flushes the active renderer on push/pop, so every draw call
370
+ // in this batch shares one stencil state — read it once. While active, the
371
+ // coordinator's pass carries a depth/stencil attachment, so the default,
372
+ // static-batch, and custom-material pipelines must all select their
373
+ // stencil-enabled variants to match it.
374
+ const stencil = backend._passCoordinator.stencilActive;
285
375
  let lastShader = null;
286
376
  let lastBlendMode = null;
287
377
  let lastFormat = null;
288
378
  let lastTexture = null;
289
379
  let defaultDrawCursor = 0;
380
+ let instancedDrawCursor = 0;
290
381
  const customDrawCursors = new Map();
291
382
  for (let i = 0; i < this._drawCallCount; i++) {
292
383
  const dc = this._drawCalls[i];
293
384
  if (dc.customShader === null) {
385
+ const batchLength = this._getStaticBatchLength(i);
386
+ if (batchLength >= 2) {
387
+ const needsPipeline = lastShader !== 'instanced' || dc.blendMode !== lastBlendMode || renderTargetFormat !== lastFormat;
388
+ if (needsPipeline) {
389
+ pass.setPipeline(this._getInstancedPipeline({ blendMode: dc.blendMode, format: renderTargetFormat, stencil }));
390
+ lastShader = 'instanced';
391
+ lastBlendMode = dc.blendMode;
392
+ lastFormat = renderTargetFormat;
393
+ lastTexture = null;
394
+ }
395
+ const maxNodeIndex = this._uploadInstancedNodeIndices(i, batchLength);
396
+ const storage = backend.getTransformStorageBuffer(maxNodeIndex + 1);
397
+ this._writeInstancedUniformSlot(instancedDrawCursor, backend, dc.premultiplySample);
398
+ pass.setBindGroup(0, this._getOrCreateInstancedTransformBindGroup(storage.buffer), [instancedDrawCursor * this._uniformAlignment]);
399
+ if (dc.texture !== lastTexture) {
400
+ lastTexture = dc.texture;
401
+ pass.setBindGroup(1, this._getTextureBindGroup(backend, dc.texture));
402
+ }
403
+ const staticGeometry = this._getOrCreateStaticGeometryEntry(dc.mesh);
404
+ const instanceNodeIndexBuffer = this._instancedNodeIndexBuffer;
405
+ if (instanceNodeIndexBuffer === null) {
406
+ throw new Error('Instanced node-index buffer must be initialized before drawing.');
407
+ }
408
+ pass.setVertexBuffer(0, staticGeometry.vertexBuffer);
409
+ pass.setVertexBuffer(1, instanceNodeIndexBuffer);
410
+ pass.setIndexBuffer(staticGeometry.indexBuffer, 'uint16');
411
+ pass.drawIndexed(staticGeometry.indexCount, batchLength);
412
+ backend.stats.batches++;
413
+ backend.stats.drawCalls++;
414
+ defaultDrawCursor += batchLength;
415
+ instancedDrawCursor++;
416
+ i += batchLength - 1;
417
+ continue;
418
+ }
294
419
  // ----- Default path -----
295
420
  const needsPipeline = lastShader !== 'default' || dc.blendMode !== lastBlendMode || renderTargetFormat !== lastFormat;
296
421
  if (needsPipeline) {
297
- pass.setPipeline(this._getPipeline({ blendMode: dc.blendMode, format: renderTargetFormat }));
422
+ pass.setPipeline(this._getPipeline({ blendMode: dc.blendMode, format: renderTargetFormat, stencil }));
298
423
  lastShader = 'default';
299
424
  lastBlendMode = dc.blendMode;
300
425
  lastFormat = renderTargetFormat;
@@ -319,9 +444,9 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
319
444
  // (Spector.js, Chrome DevTools' WebGPU panel) show meaningful
320
445
  // labels for the otherwise-anonymous mesh draws inside the
321
446
  // batched render pass.
322
- pass.pushDebugGroup('MeshShader (custom)');
447
+ pass.pushDebugGroup('MeshMaterial (custom)');
323
448
  if (needsPipeline) {
324
- pass.setPipeline(this._getOrCreateCustomPipeline(resources, dc.blendMode, renderTargetFormat));
449
+ pass.setPipeline(this._getOrCreateCustomPipeline(resources, dc.blendMode, renderTargetFormat, stencil));
325
450
  lastShader = dc.customShader;
326
451
  lastBlendMode = dc.blendMode;
327
452
  lastFormat = renderTargetFormat;
@@ -344,8 +469,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
344
469
  backend.stats.batches++;
345
470
  backend.stats.drawCalls++;
346
471
  }
347
- pass.end();
348
- backend.submit(encoder.finish());
472
+ backend._passCoordinator.endPass();
349
473
  this._resetFrame();
350
474
  }
351
475
  destroy() {
@@ -372,12 +496,20 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
372
496
  const promises = [];
373
497
  for (const blendMode of blendModes) {
374
498
  for (const format of formats) {
375
- const key = `${blendMode}:${format}`;
499
+ // Prewarm only the no-clip variants; the stencil pipelines are created
500
+ // lazily on the first clipped draw (a rare path not worth the upfront
501
+ // compile cost for every blend-mode × format combination).
502
+ const key = meshPipelineCacheKey(blendMode, format, false);
376
503
  if (this._pipelines.has(key))
377
504
  continue;
378
505
  promises.push(device.createRenderPipelineAsync(this._buildPipelineDescriptor(blendMode, format)).then(pipeline => {
379
506
  this._pipelines.set(key, pipeline);
380
507
  }));
508
+ if (!this._instancedPipelines.has(key)) {
509
+ promises.push(device.createRenderPipelineAsync(this._buildInstancedPipelineDescriptor(blendMode, format)).then(pipeline => {
510
+ this._instancedPipelines.set(key, pipeline);
511
+ }));
512
+ }
381
513
  }
382
514
  }
383
515
  await Promise.all(promises);
@@ -388,6 +520,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
388
520
  }
389
521
  this._device = backend.device;
390
522
  this._shaderModule = this._device.createShaderModule({ code: meshShaderSource });
523
+ this._instancedShaderModule = this._device.createShaderModule({ code: instancedMeshShaderSource });
391
524
  this._uniformBindGroupLayout = this._device.createBindGroupLayout({
392
525
  entries: [
393
526
  {
@@ -414,27 +547,59 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
414
547
  this._pipelineLayout = this._device.createPipelineLayout({
415
548
  bindGroupLayouts: [this._uniformBindGroupLayout, this._textureBindGroupLayout],
416
549
  });
550
+ this._instancedTransformBindGroupLayout = this._device.createBindGroupLayout({
551
+ entries: [
552
+ {
553
+ binding: 0,
554
+ visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
555
+ buffer: { type: 'uniform', hasDynamicOffset: true },
556
+ },
557
+ {
558
+ binding: 1,
559
+ visibility: GPUShaderStage.VERTEX,
560
+ buffer: { type: 'read-only-storage' },
561
+ },
562
+ ],
563
+ });
564
+ this._instancedPipelineLayout = this._device.createPipelineLayout({
565
+ bindGroupLayouts: [this._instancedTransformBindGroupLayout, this._textureBindGroupLayout],
566
+ });
417
567
  }
418
568
  onDisconnect() {
419
569
  this.flush();
420
570
  this._vertexBuffer?.destroy();
421
571
  this._indexBuffer?.destroy();
422
572
  this._uniformBuffer?.destroy();
573
+ this._instancedUniformBuffer?.destroy();
574
+ this._instancedNodeIndexBuffer?.destroy();
423
575
  this._pipelines.clear();
576
+ this._instancedPipelines.clear();
424
577
  this._textureBindGroups = new WeakMap();
578
+ for (const entry of this._staticGeometryCache.values()) {
579
+ entry.vertexBuffer.destroy();
580
+ entry.indexBuffer.destroy();
581
+ }
582
+ this._staticGeometryCache.clear();
425
583
  this._vertexBuffer = null;
426
584
  this._indexBuffer = null;
427
585
  this._uniformBuffer = null;
428
586
  this._uniformBindGroup = null;
587
+ this._instancedUniformBuffer = null;
588
+ this._instancedNodeIndexBuffer = null;
589
+ this._instancedTransformBindGroup = null;
590
+ this._instancedTransformStorageBuffer = null;
429
591
  this._pipelineLayout = null;
592
+ this._instancedPipelineLayout = null;
430
593
  this._textureBindGroupLayout = null;
431
594
  this._uniformBindGroupLayout = null;
595
+ this._instancedTransformBindGroupLayout = null;
432
596
  this._shaderModule = null;
433
- // Custom shaders are owned by user code (one MeshShader can be shared
597
+ this._instancedShaderModule = null;
598
+ // Custom materials are owned by user code (one MeshMaterial can be shared
434
599
  // across multiple Mesh instances). Their resources are released when the
435
- // user calls shader.destroy(), which fires our _onDispose callback. On
600
+ // user calls material.destroy(), which fires our _onDispose callback. On
436
601
  // backend disconnect we eagerly release everything to avoid GPU leaks
437
- // even if the user keeps the shader reference around.
602
+ // even if the user keeps the material reference around.
438
603
  for (const resources of this._customShaders.values()) {
439
604
  this._releaseCustomShaderResources(resources);
440
605
  }
@@ -445,6 +610,9 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
445
610
  this._vertexBufferCapacity = 0;
446
611
  this._indexBufferCapacity = 0;
447
612
  this._uniformBufferCapacity = 0;
613
+ this._instancedUniformBufferCapacity = 0;
614
+ this._instancedNodeIndexBufferCapacity = 0;
615
+ this._instancedNodeIndexData = new Uint32Array(0);
448
616
  }
449
617
  // ---------------------------------------------------------------------------
450
618
  // Default-path helpers
@@ -490,16 +658,16 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
490
658
  }
491
659
  }
492
660
  _getPipeline(key) {
493
- const cacheKey = `${key.blendMode}:${key.format}`;
661
+ const cacheKey = meshPipelineCacheKey(key.blendMode, key.format, key.stencil);
494
662
  let pipeline = this._pipelines.get(cacheKey);
495
663
  if (!pipeline) {
496
- pipeline = this._device.createRenderPipeline(this._buildPipelineDescriptor(key.blendMode, key.format));
664
+ pipeline = this._device.createRenderPipeline(this._buildPipelineDescriptor(key.blendMode, key.format, key.stencil));
497
665
  this._pipelines.set(cacheKey, pipeline);
498
666
  }
499
667
  return pipeline;
500
668
  }
501
- _buildPipelineDescriptor(blendMode, format) {
502
- return {
669
+ _buildPipelineDescriptor(blendMode, format, stencil = false) {
670
+ const descriptor = {
503
671
  layout: this._pipelineLayout,
504
672
  vertex: {
505
673
  module: this._shaderModule,
@@ -532,6 +700,10 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
532
700
  cullMode: 'none',
533
701
  },
534
702
  };
703
+ if (stencil) {
704
+ descriptor.depthStencil = stencilContentDepthStencilState();
705
+ }
706
+ return descriptor;
535
707
  }
536
708
  _getTextureBindGroup(backend, texture) {
537
709
  let group = this._textureBindGroups.get(texture);
@@ -548,6 +720,227 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
548
720
  }
549
721
  return group;
550
722
  }
723
+ _getStaticBatchLength(startIndex) {
724
+ const first = this._drawCalls[startIndex];
725
+ if (!this._isStaticBatchCandidate(first)) {
726
+ return 1;
727
+ }
728
+ let length = 1;
729
+ for (let i = startIndex + 1; i < this._drawCallCount; i++) {
730
+ const next = this._drawCalls[i];
731
+ if (!this._isSameStaticBatch(first, next)) {
732
+ break;
733
+ }
734
+ length++;
735
+ }
736
+ return length;
737
+ }
738
+ _isStaticBatchCandidate(drawCall) {
739
+ const command = drawCall.command;
740
+ return drawCall.customShader === null && command?.groupIndex !== undefined && drawCall.mesh.geometry?.usage === 'static';
741
+ }
742
+ _isSameStaticBatch(left, right) {
743
+ if (!this._isStaticBatchCandidate(left) || !this._isStaticBatchCandidate(right)) {
744
+ return false;
745
+ }
746
+ return (left.command.groupIndex === right.command.groupIndex &&
747
+ left.mesh.geometry === right.mesh.geometry &&
748
+ left.texture === right.texture &&
749
+ left.blendMode === right.blendMode &&
750
+ left.command.material.pipelineKey === right.command.material.pipelineKey &&
751
+ left.command.material.bindKey === right.command.material.bindKey);
752
+ }
753
+ _uploadInstancedNodeIndices(startIndex, batchLength) {
754
+ this._ensureInstancedNodeIndexCapacity(batchLength);
755
+ let maxNodeIndex = 0;
756
+ for (let i = 0; i < batchLength; i++) {
757
+ const nodeIndex = this._drawCalls[startIndex + i].command.nodeIndex >>> 0;
758
+ this._instancedNodeIndexData[i] = nodeIndex;
759
+ if (nodeIndex > maxNodeIndex) {
760
+ maxNodeIndex = nodeIndex;
761
+ }
762
+ }
763
+ this._device.queue.writeBuffer(this._instancedNodeIndexBuffer, 0, this._instancedNodeIndexData.buffer, this._instancedNodeIndexData.byteOffset, batchLength * Uint32Array.BYTES_PER_ELEMENT);
764
+ return maxNodeIndex;
765
+ }
766
+ _ensureInstancedNodeIndexCapacity(instanceCount) {
767
+ const requiredBytes = instanceCount * Uint32Array.BYTES_PER_ELEMENT;
768
+ if (this._instancedNodeIndexData.length < instanceCount) {
769
+ this._instancedNodeIndexData = new Uint32Array(Math.max(instanceCount, this._instancedNodeIndexData.length * 2 || 1));
770
+ }
771
+ if (requiredBytes > this._instancedNodeIndexBufferCapacity) {
772
+ this._instancedNodeIndexBuffer?.destroy();
773
+ this._instancedNodeIndexBufferCapacity = Math.max(requiredBytes, this._instancedNodeIndexBufferCapacity * 2 || Uint32Array.BYTES_PER_ELEMENT);
774
+ this._instancedNodeIndexBuffer = this._device.createBuffer({
775
+ size: this._instancedNodeIndexBufferCapacity,
776
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
777
+ });
778
+ }
779
+ }
780
+ _ensureInstancedUniformCapacity(drawCallCount) {
781
+ if (drawCallCount === 0) {
782
+ return;
783
+ }
784
+ const requiredBytes = drawCallCount * this._uniformAlignment;
785
+ if (requiredBytes > this._instancedUniformBufferCapacity) {
786
+ this._instancedUniformBuffer?.destroy();
787
+ this._instancedUniformBufferCapacity = Math.max(requiredBytes, this._instancedUniformBufferCapacity * 2 || this._uniformAlignment);
788
+ this._instancedUniformBuffer = this._device.createBuffer({
789
+ size: this._instancedUniformBufferCapacity,
790
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
791
+ });
792
+ this._instancedTransformBindGroup = null;
793
+ this._instancedTransformStorageBuffer = null;
794
+ }
795
+ }
796
+ _writeInstancedUniformSlot(slot, backend, premultiplySample) {
797
+ const data = this._instancedUniformScratch;
798
+ const projection = backend.view.getTransform();
799
+ data.fill(0);
800
+ data[0] = projection.a;
801
+ data[1] = projection.b;
802
+ data[4] = projection.c;
803
+ data[5] = projection.d;
804
+ data[8] = projection.x;
805
+ data[9] = projection.y;
806
+ data[10] = 1;
807
+ data[12] = premultiplySample ? 1 : 0;
808
+ this._device.queue.writeBuffer(this._instancedUniformBuffer, slot * this._uniformAlignment, data.buffer, data.byteOffset, transformUniformByteLength);
809
+ }
810
+ _getOrCreateInstancedTransformBindGroup(storageBuffer) {
811
+ if (this._instancedTransformBindGroup !== null && this._instancedTransformStorageBuffer === storageBuffer) {
812
+ return this._instancedTransformBindGroup;
813
+ }
814
+ this._instancedTransformStorageBuffer = storageBuffer;
815
+ this._instancedTransformBindGroup = this._device.createBindGroup({
816
+ layout: this._instancedTransformBindGroupLayout,
817
+ entries: [
818
+ {
819
+ binding: 0,
820
+ resource: {
821
+ buffer: this._instancedUniformBuffer,
822
+ size: transformUniformByteLength,
823
+ },
824
+ },
825
+ {
826
+ binding: 1,
827
+ resource: {
828
+ buffer: storageBuffer,
829
+ },
830
+ },
831
+ ],
832
+ });
833
+ return this._instancedTransformBindGroup;
834
+ }
835
+ _getInstancedPipeline(key) {
836
+ const cacheKey = meshPipelineCacheKey(key.blendMode, key.format, key.stencil);
837
+ let pipeline = this._instancedPipelines.get(cacheKey);
838
+ if (!pipeline) {
839
+ pipeline = this._device.createRenderPipeline(this._buildInstancedPipelineDescriptor(key.blendMode, key.format, key.stencil));
840
+ this._instancedPipelines.set(cacheKey, pipeline);
841
+ }
842
+ return pipeline;
843
+ }
844
+ _buildInstancedPipelineDescriptor(blendMode, format, stencil = false) {
845
+ const descriptor = {
846
+ layout: this._instancedPipelineLayout,
847
+ vertex: {
848
+ module: this._instancedShaderModule,
849
+ entryPoint: 'vertexMain',
850
+ buffers: [
851
+ {
852
+ arrayStride: vertexStrideBytes,
853
+ stepMode: 'vertex',
854
+ attributes: [
855
+ { shaderLocation: 0, offset: 0, format: 'float32x2' },
856
+ { shaderLocation: 1, offset: 8, format: 'float32x2' },
857
+ { shaderLocation: 2, offset: 16, format: 'unorm8x4' },
858
+ ],
859
+ },
860
+ {
861
+ arrayStride: Uint32Array.BYTES_PER_ELEMENT,
862
+ stepMode: 'instance',
863
+ attributes: [{ shaderLocation: 6, offset: 0, format: 'uint32' }],
864
+ },
865
+ ],
866
+ },
867
+ fragment: {
868
+ module: this._instancedShaderModule,
869
+ entryPoint: 'fragmentMain',
870
+ targets: [
871
+ {
872
+ format,
873
+ blend: getWebGpuBlendState(blendMode),
874
+ writeMask: GPUColorWrite.ALL,
875
+ },
876
+ ],
877
+ },
878
+ primitive: {
879
+ topology: 'triangle-list',
880
+ cullMode: 'none',
881
+ },
882
+ };
883
+ if (stencil) {
884
+ descriptor.depthStencil = stencilContentDepthStencilState();
885
+ }
886
+ return descriptor;
887
+ }
888
+ _getOrCreateStaticGeometryEntry(mesh) {
889
+ const geometry = mesh.geometry;
890
+ if (geometry?.usage !== 'static') {
891
+ throw new Error('Static mesh batching requires Geometry with usage="static".');
892
+ }
893
+ const existing = this._staticGeometryCache.get(geometry);
894
+ if (existing !== undefined) {
895
+ return existing;
896
+ }
897
+ const vertexData = new ArrayBuffer(mesh.vertexCount * vertexStrideBytes);
898
+ const vertexFloatView = new Float32Array(vertexData);
899
+ const vertexUintView = new Uint32Array(vertexData);
900
+ this._writeMeshVerticesIntoBuffer(mesh, 0, vertexFloatView, vertexUintView);
901
+ // Allocate one extra element when indexCount is odd so the GPU buffer and
902
+ // writeBuffer byte count can be rounded up to 4 without a buffer overread.
903
+ const indexData = new Uint16Array(mesh.indexCount + (mesh.indexCount & 1));
904
+ if (mesh.indices !== null) {
905
+ indexData.set(mesh.indices, 0);
906
+ }
907
+ else {
908
+ for (let i = 0; i < mesh.indexCount; i++) {
909
+ indexData[i] = i;
910
+ }
911
+ }
912
+ const indexByteLen = mesh.indexCount * Uint16Array.BYTES_PER_ELEMENT;
913
+ const alignedIndexByteLen = (indexByteLen + 3) & -4;
914
+ const vertexBuffer = this._device.createBuffer({
915
+ size: vertexData.byteLength,
916
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
917
+ });
918
+ const indexBuffer = this._device.createBuffer({
919
+ size: alignedIndexByteLen,
920
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
921
+ });
922
+ this._device.queue.writeBuffer(vertexBuffer, 0, vertexData, 0, vertexData.byteLength);
923
+ this._device.queue.writeBuffer(indexBuffer, 0, indexData.buffer, indexData.byteOffset, alignedIndexByteLen);
924
+ const disposeListener = () => {
925
+ const entry = this._staticGeometryCache.get(geometry);
926
+ if (entry === undefined) {
927
+ return;
928
+ }
929
+ entry.vertexBuffer.destroy();
930
+ entry.indexBuffer.destroy();
931
+ this._staticGeometryCache.delete(geometry);
932
+ };
933
+ geometry._onDispose(disposeListener);
934
+ const created = {
935
+ geometry,
936
+ vertexBuffer,
937
+ indexBuffer,
938
+ indexCount: mesh.indexCount,
939
+ disposeListener,
940
+ };
941
+ this._staticGeometryCache.set(geometry, created);
942
+ return created;
943
+ }
551
944
  _ensureVertexCapacity(vertexCount) {
552
945
  const requiredBytes = vertexCount * vertexStrideBytes;
553
946
  if (requiredBytes > this._vertexData.byteLength) {
@@ -566,13 +959,16 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
566
959
  }
567
960
  }
568
961
  _ensureIndexCapacity(indexCount) {
569
- const requiredBytes = indexCount * Uint16Array.BYTES_PER_ELEMENT;
570
- if (this._packedIndexData.length < indexCount) {
571
- this._packedIndexData = new Uint16Array(Math.max(indexCount, this._packedIndexData.length === 0 ? 1 : this._packedIndexData.length * 2));
962
+ // GPUQueue.writeBuffer requires the byte count to be a multiple of 4.
963
+ // Round up: odd Uint16 counts (e.g. a 3-index triangle) would otherwise
964
+ // produce 6-byte writes which the WebGPU validation layer rejects.
965
+ const requiredBytes = (indexCount * Uint16Array.BYTES_PER_ELEMENT + 3) & -4;
966
+ if (this._packedIndexData.length * Uint16Array.BYTES_PER_ELEMENT < requiredBytes) {
967
+ this._packedIndexData = new Uint16Array(Math.max(requiredBytes / Uint16Array.BYTES_PER_ELEMENT, this._packedIndexData.length === 0 ? 2 : this._packedIndexData.length * 2));
572
968
  }
573
969
  if (requiredBytes > this._indexBufferCapacity) {
574
970
  this._indexBuffer?.destroy();
575
- this._indexBufferCapacity = Math.max(requiredBytes, this._indexBufferCapacity === 0 ? Uint16Array.BYTES_PER_ELEMENT : this._indexBufferCapacity * 2);
971
+ this._indexBufferCapacity = Math.max(requiredBytes, this._indexBufferCapacity === 0 ? 4 : this._indexBufferCapacity * 2);
576
972
  this._indexBuffer = this._device.createBuffer({
577
973
  size: this._indexBufferCapacity,
578
974
  usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
@@ -620,19 +1016,19 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
620
1016
  resources.totalIndices = 0;
621
1017
  }
622
1018
  }
623
- _getOrCreateCustomShaderResources(shader) {
624
- let resources = this._customShaders.get(shader);
1019
+ _getOrCreateCustomShaderResources(material) {
1020
+ let resources = this._customShaders.get(material);
625
1021
  if (resources !== undefined) {
626
1022
  return resources;
627
1023
  }
628
1024
  if (this._device === null) {
629
1025
  throw new Error('WebGpuMeshRenderer is not connected to a backend.');
630
1026
  }
631
- if (shader.wgsl === null) {
632
- throw new Error('MeshShader has no `wgsl` source; cannot render through the WebGPU backend.');
1027
+ if (material.shader.wgsl === null) {
1028
+ throw new Error('Mesh material shader has no `wgsl` source; cannot render through the WebGPU backend.');
633
1029
  }
634
1030
  const device = this._device;
635
- const shaderModule = device.createShaderModule({ code: shader.wgsl });
1031
+ const shaderModule = device.createShaderModule({ code: material.shader.wgsl });
636
1032
  const meshUniformLayout = device.createBindGroupLayout({
637
1033
  entries: [
638
1034
  {
@@ -648,7 +1044,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
648
1044
  { binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
649
1045
  ],
650
1046
  });
651
- const userLayout = this._buildUserBindGroupLayout(device, shader);
1047
+ const userLayout = this._buildUserBindGroupLayout(device, material);
652
1048
  const pipelineLayout = device.createPipelineLayout({
653
1049
  bindGroupLayouts: [meshUniformLayout, meshTextureLayout, userLayout],
654
1050
  });
@@ -687,13 +1083,13 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
687
1083
  totalVertices: 0,
688
1084
  totalIndices: 0,
689
1085
  };
690
- this._customShaders.set(shader, resources);
691
- // When the user calls shader.destroy(), evict and release.
692
- shader._onDispose(() => {
693
- const r = this._customShaders.get(shader);
1086
+ this._customShaders.set(material, resources);
1087
+ // When the user calls material.destroy(), evict and release.
1088
+ material._onDispose(() => {
1089
+ const r = this._customShaders.get(material);
694
1090
  if (r !== undefined) {
695
1091
  this._releaseCustomShaderResources(r);
696
- this._customShaders.delete(shader);
1092
+ this._customShaders.delete(material);
697
1093
  }
698
1094
  });
699
1095
  return resources;
@@ -716,14 +1112,14 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
716
1112
  usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
717
1113
  });
718
1114
  }
719
- // Index buffer
720
- const indexBytes = resources.totalIndices * Uint16Array.BYTES_PER_ELEMENT;
721
- if (resources.indexData.length < resources.totalIndices) {
722
- resources.indexData = new Uint16Array(Math.max(resources.totalIndices, resources.indexData.length * 2));
1115
+ // Index buffer — capacity must be 4-byte aligned for GPUQueue.writeBuffer.
1116
+ const indexBytes = (resources.totalIndices * Uint16Array.BYTES_PER_ELEMENT + 3) & -4;
1117
+ if (resources.indexData.length * Uint16Array.BYTES_PER_ELEMENT < indexBytes) {
1118
+ resources.indexData = new Uint16Array(Math.max(indexBytes / Uint16Array.BYTES_PER_ELEMENT, resources.indexData.length * 2));
723
1119
  }
724
1120
  if (indexBytes > resources.indexBufferCapacity) {
725
1121
  resources.indexBuffer?.destroy();
726
- resources.indexBufferCapacity = Math.max(indexBytes, resources.indexBufferCapacity * 2 || Uint16Array.BYTES_PER_ELEMENT);
1122
+ resources.indexBufferCapacity = Math.max(indexBytes, resources.indexBufferCapacity * 2 || 4);
727
1123
  resources.indexBuffer = device.createBuffer({
728
1124
  size: resources.indexBufferCapacity,
729
1125
  usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
@@ -764,7 +1160,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
764
1160
  uintView[targetIndex + 4] = colors !== null ? colors[i] : 0xffffffff;
765
1161
  }
766
1162
  }
767
- _writeCustomMeshUniform(_shader, resources, drawCursor, mesh, backend) {
1163
+ _writeCustomMeshUniform(_material, resources, drawCursor, mesh, backend) {
768
1164
  // Layout: mat3x3 projection (48B) + mat3x3 translation (48B) + vec4 tint (16B) = 112B.
769
1165
  // WGSL mat3x3 stores 3 vec3 columns padded to vec4 alignment.
770
1166
  const slotBytes = meshUniformAlignment;
@@ -809,19 +1205,24 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
809
1205
  data[off + 10] = 1;
810
1206
  data[off + 11] = 0;
811
1207
  off += 12;
812
- // tint (vec4)
1208
+ // tint (vec4). RGB are 0..255; normalize to 0..1 for the shader multiply
1209
+ // (u_mesh.tint is documented as 0..1, matching the default path above).
813
1210
  const tint = mesh.tint;
814
- data[off + 0] = tint.r;
815
- data[off + 1] = tint.g;
816
- data[off + 2] = tint.b;
1211
+ data[off + 0] = tint.r / 255;
1212
+ data[off + 1] = tint.g / 255;
1213
+ data[off + 2] = tint.b / 255;
817
1214
  data[off + 3] = tint.a;
818
1215
  this._device.queue.writeBuffer(resources.meshUniformBuffer, drawCursor * slotBytes, data);
819
1216
  }
820
- _getOrCreateCustomPipeline(resources, blendMode, format) {
821
- const cacheKey = `${blendMode}:${format}`;
1217
+ _getOrCreateCustomPipeline(resources, blendMode, format, stencil) {
1218
+ // The stencil dimension keeps the clip and no-clip variants distinct,
1219
+ // mirroring the default and static-batch caches: a stencil pipeline carries
1220
+ // depth/stencil state and is only valid in a pass with the matching
1221
+ // attachment, so the two are never interchangeable.
1222
+ const cacheKey = `${blendMode}:${format}:${stencil ? 's' : 'n'}`;
822
1223
  let pipeline = resources.pipelines.get(cacheKey);
823
1224
  if (pipeline === undefined) {
824
- pipeline = this._device.createRenderPipeline({
1225
+ const descriptor = {
825
1226
  layout: resources.pipelineLayout,
826
1227
  vertex: {
827
1228
  module: resources.shaderModule,
@@ -853,7 +1254,14 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
853
1254
  topology: 'triangle-list',
854
1255
  cullMode: 'none',
855
1256
  },
856
- });
1257
+ };
1258
+ // While a geometric clip is active the coordinator's pass carries a
1259
+ // depth/stencil attachment; the content pipeline must test stencil ==
1260
+ // reference and leave depth/stencil otherwise inert to match it.
1261
+ if (stencil) {
1262
+ descriptor.depthStencil = stencilContentDepthStencilState();
1263
+ }
1264
+ pipeline = this._device.createRenderPipeline(descriptor);
857
1265
  resources.pipelines.set(cacheKey, pipeline);
858
1266
  }
859
1267
  return pipeline;
@@ -873,10 +1281,8 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
873
1281
  }
874
1282
  return group;
875
1283
  }
876
- _buildUserBindGroupLayout(device, shader) {
1284
+ _buildUserBindGroupLayout(device, material) {
877
1285
  const entries = [];
878
- const userUniforms = shader.uniforms;
879
- Object.values(userUniforms).some(v => !isTextureUniform(v));
880
1286
  // Binding 0 always reserved for the user UBO (even if empty), so the
881
1287
  // bind-group layout is stable across user-uniform mutations.
882
1288
  entries.push({
@@ -884,15 +1290,12 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
884
1290
  visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
885
1291
  buffer: { type: 'uniform' },
886
1292
  });
1293
+ const textureBindings = collectTextureBindings(material);
1294
+ if (textureBindings.length > maxCustomTextureSlots) {
1295
+ throw new Error(`Mesh material requested more than ${maxCustomTextureSlots} user texture bindings.`);
1296
+ }
887
1297
  let bindingIndex = 1;
888
- let textureCount = 0;
889
- for (const value of Object.values(userUniforms)) {
890
- if (!isTextureUniform(value)) {
891
- continue;
892
- }
893
- if (textureCount >= maxCustomTextureSlots) {
894
- throw new Error(`MeshShader requested more than ${maxCustomTextureSlots} user texture uniforms.`);
895
- }
1298
+ for (let t = 0; t < textureBindings.length; t++) {
896
1299
  entries.push({
897
1300
  binding: bindingIndex,
898
1301
  visibility: GPUShaderStage.FRAGMENT,
@@ -905,14 +1308,12 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
905
1308
  sampler: { type: 'filtering' },
906
1309
  });
907
1310
  bindingIndex++;
908
- textureCount++;
909
1311
  }
910
1312
  return device.createBindGroupLayout({ entries });
911
1313
  }
912
- _uploadUserUniforms(_shader, resources) {
1314
+ _uploadUserUniforms(material, resources) {
913
1315
  const device = this._device;
914
- const uniforms = _shader.uniforms;
915
- const scalarValues = Object.values(uniforms).filter(v => !isTextureUniform(v));
1316
+ const scalarValues = collectScalarUniforms(material);
916
1317
  // Always create a UBO (even if empty) since binding 0 of the user layout
917
1318
  // is fixed. Min size 16 bytes to satisfy WebGPU's minimum buffer size.
918
1319
  const slotCount = Math.max(scalarValues.length, 1);
@@ -950,16 +1351,13 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
950
1351
  }
951
1352
  device.queue.writeBuffer(resources.userUniformBuffer, 0, data);
952
1353
  }
953
- _buildUserBindGroup(backend, shader, resources) {
1354
+ _buildUserBindGroup(backend, material, resources) {
954
1355
  const device = this._device;
955
1356
  const entries = [];
956
1357
  entries.push({ binding: 0, resource: { buffer: resources.userUniformBuffer } });
957
1358
  let bindingIndex = 1;
958
- for (const value of Object.values(shader.uniforms)) {
959
- if (!isTextureUniform(value)) {
960
- continue;
961
- }
962
- const binding = backend.getTextureBinding(value);
1359
+ for (const texture of collectTextureBindings(material)) {
1360
+ const binding = backend.getTextureBinding(texture);
963
1361
  entries.push({ binding: bindingIndex, resource: binding.view });
964
1362
  bindingIndex++;
965
1363
  entries.push({ binding: bindingIndex, resource: binding.sampler });
@@ -997,6 +1395,34 @@ function isTextureUniform(value) {
997
1395
  !(value instanceof Int32Array) &&
998
1396
  !Array.isArray(value));
999
1397
  }
1398
+ /** Scalar/vector/matrix uniforms (texture values excluded) in declaration order. */
1399
+ function collectScalarUniforms(material) {
1400
+ const result = [];
1401
+ for (const value of Object.values(material.uniforms)) {
1402
+ if (!isTextureUniform(value)) {
1403
+ result.push(value);
1404
+ }
1405
+ }
1406
+ return result;
1407
+ }
1408
+ /**
1409
+ * Texture bindings claimed by the material, in a stable order: texture-valued
1410
+ * entries of `uniforms` first (declaration order), then the dedicated
1411
+ * `textures` map (declaration order). The WGSL source must declare its
1412
+ * `@group(2)` texture/sampler pairs in this same order.
1413
+ */
1414
+ function collectTextureBindings(material) {
1415
+ const result = [];
1416
+ for (const value of Object.values(material.uniforms)) {
1417
+ if (isTextureUniform(value)) {
1418
+ result.push(value);
1419
+ }
1420
+ }
1421
+ for (const texture of Object.values(material.textures)) {
1422
+ result.push(texture);
1423
+ }
1424
+ return result;
1425
+ }
1000
1426
 
1001
1427
  export { WebGpuMeshRenderer };
1002
1428
  //# sourceMappingURL=WebGpuMeshRenderer.js.map