@codexo/exojs 0.4.0 → 0.6.2

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 (151) hide show
  1. package/CHANGELOG.md +499 -163
  2. package/README.md +156 -141
  3. package/dist/esm/audio/AudioAnalyser.d.ts +0 -1
  4. package/dist/esm/audio/AudioAnalyser.js +0 -2
  5. package/dist/esm/audio/AudioAnalyser.js.map +1 -1
  6. package/dist/esm/core/Application.d.ts +4 -4
  7. package/dist/esm/core/Application.js +19 -19
  8. package/dist/esm/core/Application.js.map +1 -1
  9. package/dist/esm/core/Scene.d.ts +59 -24
  10. package/dist/esm/core/Scene.js +60 -18
  11. package/dist/esm/core/Scene.js.map +1 -1
  12. package/dist/esm/core/SceneManager.js +15 -9
  13. package/dist/esm/core/SceneManager.js.map +1 -1
  14. package/dist/esm/core/SceneNode.d.ts +45 -5
  15. package/dist/esm/core/SceneNode.js +136 -7
  16. package/dist/esm/core/SceneNode.js.map +1 -1
  17. package/dist/esm/index.js +6 -4
  18. package/dist/esm/index.js.map +1 -1
  19. package/dist/esm/math/index.d.ts +0 -1
  20. package/dist/esm/rendering/CallbackRenderPass.d.ts +3 -3
  21. package/dist/esm/rendering/CallbackRenderPass.js +2 -2
  22. package/dist/esm/rendering/CallbackRenderPass.js.map +1 -1
  23. package/dist/esm/rendering/Container.d.ts +10 -11
  24. package/dist/esm/rendering/Container.js +5 -5
  25. package/dist/esm/rendering/Container.js.map +1 -1
  26. package/dist/esm/rendering/Drawable.d.ts +2 -2
  27. package/dist/esm/rendering/Drawable.js +5 -5
  28. package/dist/esm/rendering/Drawable.js.map +1 -1
  29. package/dist/esm/rendering/{SceneRenderRuntime.d.ts → RenderBackend.d.ts} +21 -3
  30. package/dist/esm/rendering/RenderNode.d.ts +41 -5
  31. package/dist/esm/rendering/RenderNode.js +89 -24
  32. package/dist/esm/rendering/RenderNode.js.map +1 -1
  33. package/dist/esm/rendering/RenderPass.d.ts +2 -2
  34. package/dist/esm/rendering/RenderTargetPass.d.ts +3 -3
  35. package/dist/esm/rendering/RenderTargetPass.js +9 -9
  36. package/dist/esm/rendering/RenderTargetPass.js.map +1 -1
  37. package/dist/esm/rendering/Renderer.d.ts +3 -3
  38. package/dist/esm/rendering/RendererRegistry.d.ts +13 -7
  39. package/dist/esm/rendering/RendererRegistry.js +18 -10
  40. package/dist/esm/rendering/RendererRegistry.js.map +1 -1
  41. package/dist/esm/rendering/filters/BlurFilter.d.ts +2 -2
  42. package/dist/esm/rendering/filters/BlurFilter.js +5 -5
  43. package/dist/esm/rendering/filters/BlurFilter.js.map +1 -1
  44. package/dist/esm/rendering/filters/ColorFilter.d.ts +2 -2
  45. package/dist/esm/rendering/filters/ColorFilter.js +3 -3
  46. package/dist/esm/rendering/filters/ColorFilter.js.map +1 -1
  47. package/dist/esm/rendering/filters/Filter.d.ts +2 -2
  48. package/dist/esm/rendering/index.d.ts +9 -6
  49. package/dist/esm/rendering/mesh/Mesh.d.ts +69 -0
  50. package/dist/esm/rendering/mesh/Mesh.js +114 -0
  51. package/dist/esm/rendering/mesh/Mesh.js.map +1 -0
  52. package/dist/esm/rendering/primitives/Graphics.d.ts +3 -3
  53. package/dist/esm/rendering/primitives/Graphics.js.map +1 -1
  54. package/dist/esm/rendering/shader/Shader.d.ts +3 -3
  55. package/dist/esm/rendering/shader/Shader.js +10 -10
  56. package/dist/esm/rendering/text/Text.d.ts +2 -2
  57. package/dist/esm/rendering/text/Text.js +2 -2
  58. package/dist/esm/rendering/text/Text.js.map +1 -1
  59. package/dist/esm/rendering/texture/Sampler.d.ts +0 -3
  60. package/dist/esm/rendering/texture/Sampler.js +5 -7
  61. package/dist/esm/rendering/texture/Sampler.js.map +1 -1
  62. package/dist/esm/rendering/types.d.ts +4 -0
  63. package/dist/esm/rendering/types.js +4 -0
  64. package/dist/esm/rendering/types.js.map +1 -1
  65. package/dist/esm/rendering/video/Video.d.ts +2 -2
  66. package/dist/esm/rendering/video/Video.js +2 -2
  67. package/dist/esm/rendering/video/Video.js.map +1 -1
  68. package/dist/esm/rendering/webgl2/AbstractWebGl2BatchedRenderer.d.ts +2 -2
  69. package/dist/esm/rendering/webgl2/AbstractWebGl2BatchedRenderer.js +35 -11
  70. package/dist/esm/rendering/webgl2/AbstractWebGl2BatchedRenderer.js.map +1 -1
  71. package/dist/esm/rendering/webgl2/AbstractWebGl2Renderer.d.ts +13 -13
  72. package/dist/esm/rendering/webgl2/AbstractWebGl2Renderer.js +20 -20
  73. package/dist/esm/rendering/webgl2/AbstractWebGl2Renderer.js.map +1 -1
  74. package/dist/esm/rendering/webgl2/{WebGl2RenderManager.d.ts → WebGl2Backend.d.ts} +15 -12
  75. package/dist/esm/rendering/webgl2/{WebGl2RenderManager.js → WebGl2Backend.js} +63 -38
  76. package/dist/esm/rendering/webgl2/WebGl2Backend.js.map +1 -0
  77. package/dist/esm/rendering/webgl2/WebGl2MaskCompositor.d.ts +31 -0
  78. package/dist/esm/rendering/webgl2/WebGl2MaskCompositor.js +186 -0
  79. package/dist/esm/rendering/webgl2/WebGl2MaskCompositor.js.map +1 -0
  80. package/dist/esm/rendering/webgl2/WebGl2MeshRenderer.d.ts +27 -0
  81. package/dist/esm/rendering/webgl2/WebGl2MeshRenderer.js +242 -0
  82. package/dist/esm/rendering/webgl2/WebGl2MeshRenderer.js.map +1 -0
  83. package/dist/esm/rendering/webgl2/WebGl2ParticleRenderer.d.ts +38 -7
  84. package/dist/esm/rendering/webgl2/WebGl2ParticleRenderer.js +281 -90
  85. package/dist/esm/rendering/webgl2/WebGl2ParticleRenderer.js.map +1 -1
  86. package/dist/esm/rendering/webgl2/WebGl2PrimitiveRenderer.d.ts +2 -2
  87. package/dist/esm/rendering/webgl2/WebGl2PrimitiveRenderer.js +15 -10
  88. package/dist/esm/rendering/webgl2/WebGl2PrimitiveRenderer.js.map +1 -1
  89. package/dist/esm/rendering/webgl2/WebGl2ShaderMappings.js +12 -0
  90. package/dist/esm/rendering/webgl2/WebGl2ShaderMappings.js.map +1 -1
  91. package/dist/esm/rendering/webgl2/WebGl2ShaderProgram.d.ts +2 -0
  92. package/dist/esm/rendering/webgl2/{WebGl2ShaderRuntime.js → WebGl2ShaderProgram.js} +58 -18
  93. package/dist/esm/rendering/webgl2/WebGl2ShaderProgram.js.map +1 -0
  94. package/dist/esm/rendering/webgl2/WebGl2SpriteRenderer.d.ts +26 -7
  95. package/dist/esm/rendering/webgl2/WebGl2SpriteRenderer.js +260 -62
  96. package/dist/esm/rendering/webgl2/WebGl2SpriteRenderer.js.map +1 -1
  97. package/dist/esm/rendering/webgl2/WebGl2VertexArrayObject.d.ts +24 -1
  98. package/dist/esm/rendering/webgl2/WebGl2VertexArrayObject.js +6 -2
  99. package/dist/esm/rendering/webgl2/WebGl2VertexArrayObject.js.map +1 -1
  100. package/dist/esm/rendering/webgl2/glsl/mask-compose.frag.js +4 -0
  101. package/dist/esm/rendering/webgl2/glsl/mask-compose.frag.js.map +1 -0
  102. package/dist/esm/rendering/webgl2/glsl/mask-compose.vert.js +4 -0
  103. package/dist/esm/rendering/webgl2/glsl/mask-compose.vert.js.map +1 -0
  104. package/dist/esm/rendering/webgl2/glsl/mesh.frag.js +4 -0
  105. package/dist/esm/rendering/webgl2/glsl/mesh.frag.js.map +1 -0
  106. package/dist/esm/rendering/webgl2/glsl/mesh.vert.js +4 -0
  107. package/dist/esm/rendering/webgl2/glsl/mesh.vert.js.map +1 -0
  108. package/dist/esm/rendering/webgl2/glsl/particle.vert.js +1 -1
  109. package/dist/esm/rendering/webgl2/glsl/sprite.frag.js +1 -1
  110. package/dist/esm/rendering/webgl2/glsl/sprite.vert.js +1 -1
  111. package/dist/esm/rendering/webgpu/AbstractWebGpuRenderer.d.ts +9 -9
  112. package/dist/esm/rendering/webgpu/AbstractWebGpuRenderer.js +18 -18
  113. package/dist/esm/rendering/webgpu/AbstractWebGpuRenderer.js.map +1 -1
  114. package/dist/esm/rendering/webgpu/{WebGpuRenderManager.d.ts → WebGpuBackend.d.ts} +17 -14
  115. package/dist/esm/rendering/webgpu/{WebGpuRenderManager.js → WebGpuBackend.js} +77 -40
  116. package/dist/esm/rendering/webgpu/WebGpuBackend.js.map +1 -0
  117. package/dist/esm/rendering/webgpu/WebGpuMaskCompositor.d.ts +37 -0
  118. package/dist/esm/rendering/webgpu/WebGpuMaskCompositor.js +279 -0
  119. package/dist/esm/rendering/webgpu/WebGpuMaskCompositor.js.map +1 -0
  120. package/dist/esm/rendering/webgpu/WebGpuMeshRenderer.d.ts +40 -0
  121. package/dist/esm/rendering/webgpu/WebGpuMeshRenderer.js +439 -0
  122. package/dist/esm/rendering/webgpu/WebGpuMeshRenderer.js.map +1 -0
  123. package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.d.ts +2 -3
  124. package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.js +65 -82
  125. package/dist/esm/rendering/webgpu/WebGpuParticleRenderer.js.map +1 -1
  126. package/dist/esm/rendering/webgpu/WebGpuPrimitiveRenderer.d.ts +2 -3
  127. package/dist/esm/rendering/webgpu/WebGpuPrimitiveRenderer.js +24 -25
  128. package/dist/esm/rendering/webgpu/WebGpuPrimitiveRenderer.js.map +1 -1
  129. package/dist/esm/rendering/webgpu/WebGpuSpriteRenderer.d.ts +28 -13
  130. package/dist/esm/rendering/webgpu/WebGpuSpriteRenderer.js +410 -382
  131. package/dist/esm/rendering/webgpu/WebGpuSpriteRenderer.js.map +1 -1
  132. package/dist/esm/resources/Loader.js +5 -3
  133. package/dist/esm/resources/Loader.js.map +1 -1
  134. package/dist/exo.esm.js +3574 -1696
  135. package/dist/exo.esm.js.map +1 -1
  136. package/package.json +20 -11
  137. package/dist/esm/math/Transformable.d.ts +0 -47
  138. package/dist/esm/math/Transformable.js +0 -140
  139. package/dist/esm/math/Transformable.js.map +0 -1
  140. package/dist/esm/rendering/webgl2/WebGl2RenderManager.js.map +0 -1
  141. package/dist/esm/rendering/webgl2/WebGl2RendererRuntime.d.ts +0 -15
  142. package/dist/esm/rendering/webgl2/WebGl2ShaderRuntime.d.ts +0 -2
  143. package/dist/esm/rendering/webgl2/WebGl2ShaderRuntime.js.map +0 -1
  144. package/dist/esm/rendering/webgpu/WebGpuRenderManager.js.map +0 -1
  145. package/dist/esm/rendering/webgpu/WebGpuRendererRuntime.d.ts +0 -8
  146. package/dist/exo.esm.min.js +0 -2
  147. package/dist/exo.esm.min.js.map +0 -1
  148. package/dist/exo.global.js +0 -17328
  149. package/dist/exo.global.js.map +0 -1
  150. package/dist/exo.global.min.js +0 -2
  151. package/dist/exo.global.min.js.map +0 -1
@@ -1,137 +1,156 @@
1
1
  import { AbstractWebGpuRenderer } from './AbstractWebGpuRenderer.js';
2
2
  import { Texture } from '../texture/Texture.js';
3
3
  import { RenderTexture } from '../texture/RenderTexture.js';
4
+ import { BlendModes } from '../types.js';
4
5
  import { getWebGpuBlendState } from './WebGpuBlendState.js';
5
6
 
6
7
  /// <reference types="@webgpu/types" />
7
- const spriteShaderSource = `
8
- struct ProjectionUniforms {
9
- matrix: mat4x4<f32>,
10
- };
11
-
12
- @group(0) @binding(0)
13
- var<uniform> projection: ProjectionUniforms;
14
-
15
- @group(1) @binding(0)
16
- var spriteTexture0: texture_2d<f32>;
17
- @group(1) @binding(1)
18
- var spriteTexture1: texture_2d<f32>;
19
- @group(1) @binding(2)
20
- var spriteTexture2: texture_2d<f32>;
21
- @group(1) @binding(3)
22
- var spriteTexture3: texture_2d<f32>;
23
- @group(1) @binding(4)
24
- var spriteTexture4: texture_2d<f32>;
25
- @group(1) @binding(5)
26
- var spriteTexture5: texture_2d<f32>;
27
- @group(1) @binding(6)
28
- var spriteTexture6: texture_2d<f32>;
29
- @group(1) @binding(7)
30
- var spriteTexture7: texture_2d<f32>;
31
-
32
- @group(1) @binding(8)
33
- var spriteSampler0: sampler;
34
- @group(1) @binding(9)
35
- var spriteSampler1: sampler;
36
- @group(1) @binding(10)
37
- var spriteSampler2: sampler;
38
- @group(1) @binding(11)
39
- var spriteSampler3: sampler;
40
- @group(1) @binding(12)
41
- var spriteSampler4: sampler;
42
- @group(1) @binding(13)
43
- var spriteSampler5: sampler;
44
- @group(1) @binding(14)
45
- var spriteSampler6: sampler;
46
- @group(1) @binding(15)
47
- var spriteSampler7: sampler;
48
-
49
- struct VertexInput {
50
- @location(0) position: vec2<f32>,
51
- @location(1) texcoord: vec2<f32>,
52
- @location(2) color: vec4<f32>,
53
- @location(3) premultiplySample: u32,
54
- @location(4) textureSlot: u32,
55
- };
56
-
57
- struct VertexOutput {
58
- @builtin(position) position: vec4<f32>,
59
- @location(0) texcoord: vec2<f32>,
60
- @location(1) color: vec4<f32>,
61
- @location(2) @interpolate(flat) premultiplySample: u32,
62
- @location(3) @interpolate(flat) textureSlot: u32,
63
- };
64
-
65
- @vertex
66
- fn vertexMain(input: VertexInput) -> VertexOutput {
67
- var output: VertexOutput;
68
-
69
- output.position = projection.matrix * vec4<f32>(input.position, 0.0, 1.0);
70
- output.texcoord = input.texcoord;
71
- output.color = vec4(input.color.rgb * input.color.a, input.color.a);
72
- output.premultiplySample = input.premultiplySample;
73
- output.textureSlot = input.textureSlot;
74
-
75
- return output;
76
- }
77
-
78
- fn sampleTexture(slot: u32, uv: vec2<f32>, ddx: vec2<f32>, ddy: vec2<f32>) -> vec4<f32> {
79
- switch slot {
80
- case 0u: {
81
- return textureSampleGrad(spriteTexture0, spriteSampler0, uv, ddx, ddy);
82
- }
83
- case 1u: {
84
- return textureSampleGrad(spriteTexture1, spriteSampler1, uv, ddx, ddy);
85
- }
86
- case 2u: {
87
- return textureSampleGrad(spriteTexture2, spriteSampler2, uv, ddx, ddy);
88
- }
89
- case 3u: {
90
- return textureSampleGrad(spriteTexture3, spriteSampler3, uv, ddx, ddy);
91
- }
92
- case 4u: {
93
- return textureSampleGrad(spriteTexture4, spriteSampler4, uv, ddx, ddy);
94
- }
95
- case 5u: {
96
- return textureSampleGrad(spriteTexture5, spriteSampler5, uv, ddx, ddy);
97
- }
98
- case 6u: {
99
- return textureSampleGrad(spriteTexture6, spriteSampler6, uv, ddx, ddy);
100
- }
101
- default: {
102
- return textureSampleGrad(spriteTexture7, spriteSampler7, uv, ddx, ddy);
103
- }
104
- }
105
- }
106
-
107
- @fragment
108
- fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
109
- // Compute screen-space derivatives in uniform control flow before the
110
- // per-slot switch. WGSL requires textureSample (implicit LOD) to run in
111
- // uniform control flow, which multi-texture batching breaks because the
112
- // slot varies per fragment. textureSampleGrad takes explicit derivatives
113
- // and is valid regardless of control-flow uniformity, while preserving
114
- // mipmap-correct LOD when sprites use mipmapped textures.
115
- let ddx = dpdx(input.texcoord);
116
- let ddy = dpdy(input.texcoord);
117
- let sample = sampleTexture(input.textureSlot, input.texcoord, ddx, ddy);
118
- let resolvedSample = select(sample, vec4(sample.rgb * sample.a, sample.a), input.premultiplySample == 1u);
119
-
120
- return resolvedSample * input.color;
121
- }
8
+ const spriteShaderSource = `
9
+ struct ProjectionUniforms {
10
+ matrix: mat4x4<f32>,
11
+ };
12
+
13
+ @group(0) @binding(0)
14
+ var<uniform> projection: ProjectionUniforms;
15
+
16
+ @group(1) @binding(0)
17
+ var spriteTexture0: texture_2d<f32>;
18
+ @group(1) @binding(1)
19
+ var spriteTexture1: texture_2d<f32>;
20
+ @group(1) @binding(2)
21
+ var spriteTexture2: texture_2d<f32>;
22
+ @group(1) @binding(3)
23
+ var spriteTexture3: texture_2d<f32>;
24
+ @group(1) @binding(4)
25
+ var spriteTexture4: texture_2d<f32>;
26
+ @group(1) @binding(5)
27
+ var spriteTexture5: texture_2d<f32>;
28
+ @group(1) @binding(6)
29
+ var spriteTexture6: texture_2d<f32>;
30
+ @group(1) @binding(7)
31
+ var spriteTexture7: texture_2d<f32>;
32
+
33
+ @group(1) @binding(8)
34
+ var spriteSampler0: sampler;
35
+ @group(1) @binding(9)
36
+ var spriteSampler1: sampler;
37
+ @group(1) @binding(10)
38
+ var spriteSampler2: sampler;
39
+ @group(1) @binding(11)
40
+ var spriteSampler3: sampler;
41
+ @group(1) @binding(12)
42
+ var spriteSampler4: sampler;
43
+ @group(1) @binding(13)
44
+ var spriteSampler5: sampler;
45
+ @group(1) @binding(14)
46
+ var spriteSampler6: sampler;
47
+ @group(1) @binding(15)
48
+ var spriteSampler7: sampler;
49
+
50
+ // Per-instance vertex layout (56 bytes per sprite). The four corners
51
+ // of the quad are derived from @builtin(vertex_index) 0..3 inside the
52
+ // vertex shader — there is no per-vertex stream.
53
+ struct VertexInput {
54
+ @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
+ @location(3) uvBounds: vec4<f32>, // uMin, vMin, uMax, vMax (CPU pre-swaps for flipY)
58
+ @location(4) color: vec4<f32>, // RGBA tint
59
+ @location(5) packedSlotFlags: u32, // bits 0..7 = slot, bit 8 = premultiply
60
+ };
61
+
62
+ struct VertexOutput {
63
+ @builtin(position) position: vec4<f32>,
64
+ @location(0) texcoord: vec2<f32>,
65
+ @location(1) color: vec4<f32>,
66
+ @location(2) @interpolate(flat) premultiplySample: u32,
67
+ @location(3) @interpolate(flat) textureSlot: u32,
68
+ };
69
+
70
+ @vertex
71
+ fn vertexMain(input: VertexInput, @builtin(vertex_index) vid: u32) -> VertexOutput {
72
+ var output: VertexOutput;
73
+
74
+ // vid 0..3 → corners in TL, TR, BR, BL order (matches the static index
75
+ // buffer [0,1,2,0,2,3] used for indexed triangle-list drawing).
76
+ let cornerX = ((vid + 1u) >> 1u) & 1u;
77
+ let cornerY = vid >> 1u;
78
+
79
+ let localX = select(input.localBounds.x, input.localBounds.z, cornerX == 1u);
80
+ let localY = select(input.localBounds.y, input.localBounds.w, cornerY == 1u);
81
+
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;
84
+
85
+ output.position = projection.matrix * vec4<f32>(worldX, worldY, 0.0, 1.0);
86
+
87
+ let u = select(input.uvBounds.x, input.uvBounds.z, cornerX == 1u);
88
+ let v = select(input.uvBounds.y, input.uvBounds.w, cornerY == 1u);
89
+ output.texcoord = vec2<f32>(u, v);
90
+
91
+ output.color = vec4(input.color.rgb * input.color.a, input.color.a);
92
+ output.textureSlot = input.packedSlotFlags & 0xFFu;
93
+ output.premultiplySample = (input.packedSlotFlags >> 8u) & 1u;
94
+
95
+ return output;
96
+ }
97
+
98
+ fn sampleTexture(slot: u32, uv: vec2<f32>, ddx: vec2<f32>, ddy: vec2<f32>) -> vec4<f32> {
99
+ switch slot {
100
+ case 0u: {
101
+ return textureSampleGrad(spriteTexture0, spriteSampler0, uv, ddx, ddy);
102
+ }
103
+ case 1u: {
104
+ return textureSampleGrad(spriteTexture1, spriteSampler1, uv, ddx, ddy);
105
+ }
106
+ case 2u: {
107
+ return textureSampleGrad(spriteTexture2, spriteSampler2, uv, ddx, ddy);
108
+ }
109
+ case 3u: {
110
+ return textureSampleGrad(spriteTexture3, spriteSampler3, uv, ddx, ddy);
111
+ }
112
+ case 4u: {
113
+ return textureSampleGrad(spriteTexture4, spriteSampler4, uv, ddx, ddy);
114
+ }
115
+ case 5u: {
116
+ return textureSampleGrad(spriteTexture5, spriteSampler5, uv, ddx, ddy);
117
+ }
118
+ case 6u: {
119
+ return textureSampleGrad(spriteTexture6, spriteSampler6, uv, ddx, ddy);
120
+ }
121
+ default: {
122
+ return textureSampleGrad(spriteTexture7, spriteSampler7, uv, ddx, ddy);
123
+ }
124
+ }
125
+ }
126
+
127
+ @fragment
128
+ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
129
+ // Compute screen-space derivatives in uniform control flow before the
130
+ // per-slot switch. WGSL requires textureSample (implicit LOD) to run in
131
+ // uniform control flow, which multi-texture batching breaks because the
132
+ // slot varies per fragment. textureSampleGrad takes explicit derivatives
133
+ // and is valid regardless of control-flow uniformity, while preserving
134
+ // mipmap-correct LOD when sprites use mipmapped textures.
135
+ let ddx = dpdx(input.texcoord);
136
+ let ddy = dpdy(input.texcoord);
137
+ let sample = sampleTexture(input.textureSlot, input.texcoord, ddx, ddy);
138
+ let resolvedSample = select(sample, vec4(sample.rgb * sample.a, sample.a), input.premultiplySample == 1u);
139
+
140
+ return resolvedSample * input.color;
141
+ }
122
142
  `;
123
- const vertexStrideBytes = 28;
124
- const spriteVertexCount = 4;
125
- const spriteIndexCount = 6;
143
+ const instanceStrideBytes = 56;
144
+ const wordsPerInstance = instanceStrideBytes / Uint32Array.BYTES_PER_ELEMENT;
126
145
  const projectionByteLength = 64;
127
146
  const initialBatchCapacity = 32;
128
- const wordsPerVertex = vertexStrideBytes / Uint32Array.BYTES_PER_ELEMENT;
129
147
  const maxBatchTextures = 8;
148
+ const indicesPerSprite = 6;
149
+ // Static index buffer: two triangles forming a quad, vertex IDs 0..3 in
150
+ // TL/TR/BR/BL order so the WGSL `cornerX/cornerY` derivation matches.
151
+ const quadIndices = new Uint16Array([0, 1, 2, 0, 2, 3]);
130
152
  class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
131
- _drawCalls = [];
132
- _drawCallCount = 0;
133
153
  _projectionData = new Float32Array(projectionByteLength / Float32Array.BYTES_PER_ELEMENT);
134
- _renderManager = null;
135
154
  _device = null;
136
155
  _shaderModule = null;
137
156
  _uniformBindGroupLayout = null;
@@ -139,71 +158,81 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
139
158
  _pipelineLayout = null;
140
159
  _uniformBuffer = null;
141
160
  _uniformBindGroup = null;
142
- _vertexBuffer = null;
143
161
  _indexBuffer = null;
144
- _vertexCapacity = 0;
145
- _vertexData = new ArrayBuffer(0);
146
- _float32View = new Float32Array(this._vertexData);
147
- _uint32View = new Uint32Array(this._vertexData);
162
+ _instanceBuffer = null;
163
+ _instanceCapacity = 0;
164
+ _instanceData = new ArrayBuffer(0);
165
+ _instanceFloat32 = new Float32Array(this._instanceData);
166
+ _instanceUint32 = new Uint32Array(this._instanceData);
148
167
  _pipelines = new Map();
149
- onConnect(runtime) {
150
- if (!this._renderManager) {
151
- this._renderManager = runtime;
152
- this._device = this._renderManager.device;
153
- this._shaderModule = this._device.createShaderModule({ code: spriteShaderSource });
154
- this._uniformBindGroupLayout = this._device.createBindGroupLayout({
155
- entries: [{
156
- binding: 0,
157
- visibility: GPUShaderStage.VERTEX,
158
- buffer: {
159
- type: 'uniform',
160
- },
161
- }],
162
- });
163
- this._textureBindGroupLayout = this._device.createBindGroupLayout({
164
- entries: [
165
- ...Array.from({ length: maxBatchTextures }, (_, index) => ({
166
- binding: index,
167
- visibility: GPUShaderStage.FRAGMENT,
168
- texture: {
169
- sampleType: 'float',
170
- },
171
- })),
172
- ...Array.from({ length: maxBatchTextures }, (_, index) => ({
173
- binding: maxBatchTextures + index,
174
- visibility: GPUShaderStage.FRAGMENT,
175
- sampler: {
176
- type: 'filtering',
177
- },
178
- })),
179
- ],
180
- });
181
- this._pipelineLayout = this._device.createPipelineLayout({
182
- bindGroupLayouts: [this._uniformBindGroupLayout, this._textureBindGroupLayout],
183
- });
184
- this._uniformBuffer = this._device.createBuffer({
185
- size: projectionByteLength,
186
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
187
- });
188
- this._uniformBindGroup = this._device.createBindGroup({
189
- layout: this._uniformBindGroupLayout,
190
- entries: [{
191
- binding: 0,
192
- resource: {
193
- buffer: this._uniformBuffer,
194
- },
195
- }],
196
- });
197
- this._ensureBatchCapacity(initialBatchCapacity);
168
+ _activeTextures = new Array(maxBatchTextures).fill(null);
169
+ _textureSlots = new Map();
170
+ _slotCount = 0;
171
+ _instanceCount = 0;
172
+ _currentBlendMode = null;
173
+ onConnect(backend) {
174
+ if (this._device) {
175
+ return;
198
176
  }
177
+ this._device = backend.device;
178
+ this._shaderModule = this._device.createShaderModule({ code: spriteShaderSource });
179
+ this._uniformBindGroupLayout = this._device.createBindGroupLayout({
180
+ entries: [{
181
+ binding: 0,
182
+ visibility: GPUShaderStage.VERTEX,
183
+ buffer: {
184
+ type: 'uniform',
185
+ },
186
+ }],
187
+ });
188
+ this._textureBindGroupLayout = this._device.createBindGroupLayout({
189
+ entries: [
190
+ ...Array.from({ length: maxBatchTextures }, (_, index) => ({
191
+ binding: index,
192
+ visibility: GPUShaderStage.FRAGMENT,
193
+ texture: {
194
+ sampleType: 'float',
195
+ },
196
+ })),
197
+ ...Array.from({ length: maxBatchTextures }, (_, index) => ({
198
+ binding: maxBatchTextures + index,
199
+ visibility: GPUShaderStage.FRAGMENT,
200
+ sampler: {
201
+ type: 'filtering',
202
+ },
203
+ })),
204
+ ],
205
+ });
206
+ this._pipelineLayout = this._device.createPipelineLayout({
207
+ bindGroupLayouts: [this._uniformBindGroupLayout, this._textureBindGroupLayout],
208
+ });
209
+ this._uniformBuffer = this._device.createBuffer({
210
+ size: projectionByteLength,
211
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
212
+ });
213
+ this._uniformBindGroup = this._device.createBindGroup({
214
+ layout: this._uniformBindGroupLayout,
215
+ entries: [{
216
+ binding: 0,
217
+ resource: {
218
+ buffer: this._uniformBuffer,
219
+ },
220
+ }],
221
+ });
222
+ // Static index buffer for the quad. Allocated once at connect; its
223
+ // contents never change.
224
+ this._indexBuffer = this._device.createBuffer({
225
+ size: quadIndices.byteLength,
226
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
227
+ });
228
+ this._device.queue.writeBuffer(this._indexBuffer, 0, quadIndices.buffer, quadIndices.byteOffset, quadIndices.byteLength);
199
229
  }
200
230
  onDisconnect() {
201
- this.flush();
202
- this._vertexBuffer?.destroy();
231
+ this._instanceBuffer?.destroy();
203
232
  this._indexBuffer?.destroy();
204
233
  this._uniformBuffer?.destroy();
205
234
  this._pipelines.clear();
206
- this._vertexBuffer = null;
235
+ this._instanceBuffer = null;
207
236
  this._indexBuffer = null;
208
237
  this._uniformBindGroup = null;
209
238
  this._uniformBuffer = null;
@@ -212,94 +241,64 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
212
241
  this._uniformBindGroupLayout = null;
213
242
  this._shaderModule = null;
214
243
  this._device = null;
215
- this._renderManager = null;
216
- this._vertexCapacity = 0;
217
- this._vertexData = new ArrayBuffer(0);
218
- this._float32View = new Float32Array(this._vertexData);
219
- this._uint32View = new Uint32Array(this._vertexData);
220
- this._drawCallCount = 0;
244
+ this._backend = null;
245
+ this._instanceCapacity = 0;
246
+ this._instanceData = new ArrayBuffer(0);
247
+ this._instanceFloat32 = new Float32Array(this._instanceData);
248
+ this._instanceUint32 = new Uint32Array(this._instanceData);
249
+ this._instanceCount = 0;
250
+ this._currentBlendMode = null;
251
+ this._resetSlots();
221
252
  }
222
253
  render(sprite) {
223
- const renderManager = this._renderManager;
254
+ const backend = this._backend;
224
255
  const texture = sprite.texture;
225
- if (renderManager === null
226
- ||
227
- (!(texture instanceof Texture) && !(texture instanceof RenderTexture))
256
+ // Same early-out conditions as the deferred renderer used to apply.
257
+ if (backend === null
258
+ || (!(texture instanceof Texture) && !(texture instanceof RenderTexture))
228
259
  || texture.width === 0
229
260
  || texture.height === 0
230
261
  || (texture instanceof Texture && texture.source === null)) {
231
262
  return;
232
263
  }
233
- renderManager.setBlendMode(sprite.blendMode);
234
- const drawCallIndex = this._drawCallCount++;
235
- const drawCall = this._drawCalls[drawCallIndex];
236
- if (drawCall) {
237
- drawCall.sprite = sprite;
238
- drawCall.texture = texture;
239
- drawCall.color = sprite.tint.toRgba();
240
- drawCall.blendMode = sprite.blendMode;
264
+ const blendMode = sprite.blendMode;
265
+ // Flush triggers: blend-mode change, instance buffer full at current
266
+ // capacity (we'll grow on next render), or texture-slot exhaustion.
267
+ const blendModeChanged = this._currentBlendMode !== null && blendMode !== this._currentBlendMode;
268
+ const slotExhausted = !this._textureSlots.has(texture) && this._slotCount >= maxBatchTextures;
269
+ if (blendModeChanged || slotExhausted) {
270
+ this.flush();
241
271
  }
242
- else {
243
- this._drawCalls.push({
244
- sprite,
245
- texture,
246
- color: sprite.tint.toRgba(),
247
- blendMode: sprite.blendMode,
248
- });
272
+ this._currentBlendMode = blendMode;
273
+ backend.setBlendMode(blendMode);
274
+ // Resolve / assign texture slot.
275
+ let slot = this._textureSlots.get(texture);
276
+ if (slot === undefined) {
277
+ slot = this._slotCount++;
278
+ this._textureSlots.set(texture, slot);
279
+ this._activeTextures[slot] = texture;
249
280
  }
281
+ const premultiplySample = backend.shouldPremultiplyTextureSample(texture) ? 1 : 0;
282
+ const packedSlotFlags = slot | (premultiplySample << 8);
283
+ // Ensure capacity covers the new entry BEFORE packing — otherwise the
284
+ // typed-array writes in _packInstance silently fall off the end of a
285
+ // too-small buffer.
286
+ this._ensureInstanceCapacity(this._instanceCount + 1);
287
+ this._packInstance(sprite, texture, packedSlotFlags);
288
+ this._instanceCount++;
250
289
  }
251
290
  flush() {
252
- const renderManager = this._renderManager;
291
+ const backend = this._backend;
253
292
  const device = this._device;
254
293
  const uniformBuffer = this._uniformBuffer;
255
294
  const uniformBindGroup = this._uniformBindGroup;
256
- const vertexBuffer = this._vertexBuffer;
257
- const indexBuffer = this._indexBuffer;
258
- if (!renderManager || !device || !uniformBuffer || !uniformBindGroup || !vertexBuffer || !indexBuffer) {
295
+ if (!backend || !device || !uniformBuffer || !uniformBindGroup) {
259
296
  return;
260
297
  }
261
- if (this._drawCallCount === 0 && !renderManager.clearRequested) {
298
+ if (this._instanceCount === 0 && !backend.clearRequested) {
262
299
  return;
263
300
  }
264
- // Grow vertex/index buffers up front for the TOTAL sprite count. Two
265
- // reasons this must happen before the render pass begins:
266
- // 1. _ensureBatchCapacity destroys old buffers and creates new ones
267
- // when capacity grows, so running it after setVertexBuffer /
268
- // setIndexBuffer would leave the pass bound to destroyed buffers.
269
- // 2. All batches are packed into the vertex buffer at distinct
270
- // sprite offsets, so the buffer must hold every sprite in the
271
- // flush, not just one batch worth.
272
- if (this._drawCallCount > 0) {
273
- this._ensureBatchCapacity(this._drawCallCount);
274
- }
275
- // Walk the batches once, packing each batch's vertex data into the
276
- // CPU-side buffer at its own sprite-aligned offset. Each batch's
277
- // metadata is recorded for the draw loop below.
278
- //
279
- // This replaces an earlier per-batch queue.writeBuffer(..., offset: 0)
280
- // pattern where every writeBuffer targeted the same GPU offset. All
281
- // writeBuffers in a frame execute before queue.submit(commandBuffer),
282
- // so only the last batch's vertex data survived — which meant any
283
- // flush containing more than one batch rendered every batch using
284
- // the LAST batch's vertices (background vanished, sprites duplicated
285
- // at wrong sizes, etc. whenever blend mode / texture slot / pipeline
286
- // caused a split into multiple batches).
287
- const batchPlan = [];
288
- let packedSpriteCount = 0;
289
- for (let start = 0; start < this._drawCallCount;) {
290
- const batch = this._getBatchRange(start);
291
- const spriteCount = batch.end - batch.start;
292
- this._writeBatchVertexData(batch, packedSpriteCount);
293
- batchPlan.push({
294
- firstSprite: packedSpriteCount,
295
- spriteCount,
296
- blendMode: batch.blendMode,
297
- textures: batch.textures,
298
- });
299
- packedSpriteCount += spriteCount;
300
- start = batch.end;
301
- }
302
- const viewMatrix = renderManager.view.getTransform();
301
+ const viewMatrix = backend.view.getTransform();
303
302
  this._projectionData.set([
304
303
  viewMatrix.a, viewMatrix.c, 0, 0,
305
304
  viewMatrix.b, viewMatrix.d, 0, 0,
@@ -309,157 +308,175 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
309
308
  device.queue.writeBuffer(uniformBuffer, 0, this._projectionData.buffer, this._projectionData.byteOffset, this._projectionData.byteLength);
310
309
  const encoder = device.createCommandEncoder();
311
310
  const pass = encoder.beginRenderPass({
312
- colorAttachments: [renderManager.createColorAttachment()],
311
+ colorAttachments: [backend.createColorAttachment()],
313
312
  });
314
- renderManager.stats.renderPasses++;
315
- const scissor = renderManager.getScissorRect();
313
+ backend.stats.renderPasses++;
314
+ const scissor = backend.getScissorRect();
316
315
  const maskClipsAll = scissor !== null && (scissor.width <= 0 || scissor.height <= 0);
317
316
  if (scissor !== null && !maskClipsAll) {
318
317
  pass.setScissorRect(scissor.x, scissor.y, scissor.width, scissor.height);
319
318
  }
320
- if (this._drawCallCount > 0 && !maskClipsAll) {
321
- // Single upload for the whole packed vertex buffer — every batch
322
- // reads from its own sprite range via drawIndexed's firstIndex.
323
- device.queue.writeBuffer(this._vertexBuffer, 0, this._vertexData, 0, packedSpriteCount * spriteVertexCount * vertexStrideBytes);
319
+ if (this._instanceCount > 0 && !maskClipsAll && this._instanceBuffer !== null && this._indexBuffer !== null && this._currentBlendMode !== null) {
320
+ device.queue.writeBuffer(this._instanceBuffer, 0, this._instanceData, 0, this._instanceCount * instanceStrideBytes);
321
+ const pipeline = this._getPipeline(this._currentBlendMode, backend.renderTargetFormat);
322
+ const textureBindGroup = this._createTextureBindGroup(device, backend);
323
+ pass.setPipeline(pipeline);
324
324
  pass.setBindGroup(0, uniformBindGroup);
325
- pass.setVertexBuffer(0, this._vertexBuffer);
326
- pass.setIndexBuffer(this._indexBuffer, 'uint32');
327
- for (const plan of batchPlan) {
328
- const pipeline = this._getPipeline(plan.blendMode, renderManager.renderTargetFormat);
329
- const textureBindGroup = this._createTextureBindGroup(device, renderManager, plan.textures);
330
- pass.setPipeline(pipeline);
331
- pass.setBindGroup(1, textureBindGroup);
332
- pass.drawIndexed(plan.spriteCount * spriteIndexCount, 1, plan.firstSprite * spriteIndexCount, 0, 0);
333
- renderManager.stats.batches++;
334
- renderManager.stats.drawCalls++;
335
- }
325
+ pass.setBindGroup(1, textureBindGroup);
326
+ pass.setVertexBuffer(0, this._instanceBuffer);
327
+ pass.setIndexBuffer(this._indexBuffer, 'uint16');
328
+ pass.drawIndexed(indicesPerSprite, this._instanceCount, 0, 0, 0);
329
+ backend.stats.batches++;
330
+ backend.stats.drawCalls++;
336
331
  }
337
332
  pass.end();
338
- renderManager.submit(encoder.finish());
339
- this._drawCallCount = 0;
333
+ backend.submit(encoder.finish());
334
+ this._instanceCount = 0;
335
+ this._resetSlots();
336
+ this._currentBlendMode = null;
340
337
  }
341
338
  destroy() {
342
339
  this.disconnect();
343
340
  }
344
- _ensureBatchCapacity(spriteCount) {
345
- if (!this._device || spriteCount <= this._vertexCapacity) {
341
+ /**
342
+ * Pre-create render pipelines for every blend-mode × target-format
343
+ * combination this renderer can produce, asynchronously and in
344
+ * parallel. Called from the render manager's init path so by the time
345
+ * the first frame draws, all pipelines exist in cache.
346
+ *
347
+ * Without prewarm, the first draw of any new (blendMode, format)
348
+ * combination would fall back to the synchronous _getPipeline() path,
349
+ * which blocks while the WebGPU implementation compiles WGSL and
350
+ * sets up the pipeline state object — typically tens of milliseconds.
351
+ */
352
+ async prewarmPipelines(formats) {
353
+ const device = this._device;
354
+ if (!device || !this._shaderModule || !this._pipelineLayout) {
346
355
  return;
347
356
  }
348
- let nextCapacity = Math.max(this._vertexCapacity, initialBatchCapacity);
349
- while (nextCapacity < spriteCount) {
350
- nextCapacity *= 2;
357
+ if (typeof device.createRenderPipelineAsync !== 'function') {
358
+ return;
351
359
  }
352
- const vertexData = new ArrayBuffer(nextCapacity * spriteVertexCount * vertexStrideBytes);
353
- const vertexBuffer = this._device.createBuffer({
354
- size: vertexData.byteLength,
355
- usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
356
- });
357
- const indexData = new Uint32Array(nextCapacity * spriteIndexCount);
358
- const indexBuffer = this._device.createBuffer({
359
- size: indexData.byteLength,
360
- usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
361
- });
362
- for (let spriteIndex = 0; spriteIndex < nextCapacity; spriteIndex++) {
363
- const baseVertex = spriteIndex * spriteVertexCount;
364
- const targetIndex = spriteIndex * spriteIndexCount;
365
- indexData[targetIndex] = baseVertex;
366
- indexData[targetIndex + 1] = baseVertex + 1;
367
- indexData[targetIndex + 2] = baseVertex + 2;
368
- indexData[targetIndex + 3] = baseVertex;
369
- indexData[targetIndex + 4] = baseVertex + 2;
370
- indexData[targetIndex + 5] = baseVertex + 3;
360
+ const blendModes = [
361
+ BlendModes.Normal,
362
+ BlendModes.Additive,
363
+ BlendModes.Subtract,
364
+ BlendModes.Multiply,
365
+ BlendModes.Screen,
366
+ ];
367
+ const promises = [];
368
+ for (const blendMode of blendModes) {
369
+ for (const format of formats) {
370
+ const pipelineKey = `${blendMode}:${format}`;
371
+ if (this._pipelines.has(pipelineKey)) {
372
+ continue;
373
+ }
374
+ const promise = device
375
+ .createRenderPipelineAsync(this._buildPipelineDescriptor(blendMode, format))
376
+ .then((pipeline) => {
377
+ this._pipelines.set(pipelineKey, pipeline);
378
+ });
379
+ promises.push(promise);
380
+ }
371
381
  }
372
- this._device.queue.writeBuffer(indexBuffer, 0, indexData.buffer, indexData.byteOffset, indexData.byteLength);
373
- this._vertexBuffer?.destroy();
374
- this._indexBuffer?.destroy();
375
- this._vertexCapacity = nextCapacity;
376
- this._vertexData = vertexData;
377
- this._float32View = new Float32Array(vertexData);
378
- this._uint32View = new Uint32Array(vertexData);
379
- this._vertexBuffer = vertexBuffer;
380
- this._indexBuffer = indexBuffer;
382
+ await Promise.all(promises);
381
383
  }
382
- _writeBatchVertexData(batch, firstSprite) {
383
- const renderManager = this._renderManager;
384
- if (!renderManager) {
384
+ _packInstance(sprite, texture, packedSlotFlags) {
385
+ const offset = this._instanceCount * wordsPerInstance;
386
+ const f32 = this._instanceFloat32;
387
+ const u32 = this._instanceUint32;
388
+ const bounds = sprite.getLocalBounds();
389
+ f32[offset + 0] = bounds.left;
390
+ f32[offset + 1] = bounds.top;
391
+ f32[offset + 2] = bounds.right;
392
+ f32[offset + 3] = bounds.bottom;
393
+ const transform = sprite.getGlobalTransform();
394
+ f32[offset + 4] = transform.a;
395
+ f32[offset + 5] = transform.b;
396
+ f32[offset + 6] = transform.x;
397
+ f32[offset + 7] = transform.c;
398
+ f32[offset + 8] = transform.d;
399
+ f32[offset + 9] = transform.y;
400
+ // uvBounds: u16x4 normalised, packed into two u32 slots. The CPU
401
+ // applies the flipY swap so the shader stays orientation-agnostic.
402
+ const frame = sprite.textureFrame;
403
+ const texWidth = texture.width;
404
+ const texHeight = texture.height;
405
+ const uMin = ((frame.left / texWidth) * 0xFFFF) & 0xFFFF;
406
+ const uMax = ((frame.right / texWidth) * 0xFFFF) & 0xFFFF;
407
+ const vMinRaw = ((frame.top / texHeight) * 0xFFFF) & 0xFFFF;
408
+ const vMaxRaw = ((frame.bottom / texHeight) * 0xFFFF) & 0xFFFF;
409
+ const flipY = texture instanceof Texture && texture.flipY;
410
+ const vMin = flipY ? vMaxRaw : vMinRaw;
411
+ const vMax = flipY ? vMinRaw : vMaxRaw;
412
+ u32[offset + 10] = uMin | (vMin << 16);
413
+ u32[offset + 11] = uMax | (vMax << 16);
414
+ u32[offset + 12] = sprite.tint.toRgba();
415
+ u32[offset + 13] = packedSlotFlags;
416
+ }
417
+ _ensureInstanceCapacity(instanceCount) {
418
+ if (!this._device || instanceCount <= this._instanceCapacity) {
385
419
  return;
386
420
  }
387
- let vertexOffset = firstSprite * spriteVertexCount * wordsPerVertex;
388
- for (let drawCallIndex = batch.start; drawCallIndex < batch.end; drawCallIndex++) {
389
- const drawCall = this._drawCalls[drawCallIndex];
390
- const textureSlot = batch.textureSlots.get(drawCall.texture) ?? 0;
391
- const premultiplySample = renderManager.shouldPremultiplyTextureSample(drawCall.texture) ? 1 : 0;
392
- const vertices = drawCall.sprite.vertices;
393
- const texCoords = drawCall.sprite.texCoords;
394
- for (let i = 0; i < spriteVertexCount; i++) {
395
- const vertexIndex = i * 2;
396
- const packedTexCoord = texCoords[i];
397
- this._float32View[vertexOffset] = vertices[vertexIndex];
398
- this._float32View[vertexOffset + 1] = vertices[vertexIndex + 1];
399
- this._float32View[vertexOffset + 2] = (packedTexCoord & 0xFFFF) / 65535;
400
- this._float32View[vertexOffset + 3] = ((packedTexCoord >>> 16) & 0xFFFF) / 65535;
401
- this._uint32View[vertexOffset + 4] = drawCall.color;
402
- this._uint32View[vertexOffset + 5] = premultiplySample;
403
- this._uint32View[vertexOffset + 6] = textureSlot;
404
- vertexOffset += wordsPerVertex;
405
- }
421
+ let nextCapacity = Math.max(this._instanceCapacity, initialBatchCapacity);
422
+ while (nextCapacity < instanceCount) {
423
+ nextCapacity *= 2;
406
424
  }
425
+ const oldData = this._instanceData;
426
+ // Preserve any already-packed instances. _instanceCount is bounded by
427
+ // the previous capacity, but oldData may be the initial 0-byte buffer
428
+ // — clamp to its actual byteLength to avoid out-of-range typed-array
429
+ // construction.
430
+ const carryBytes = Math.min(this._instanceCount * instanceStrideBytes, oldData.byteLength);
431
+ const instanceData = new ArrayBuffer(nextCapacity * instanceStrideBytes);
432
+ if (carryBytes > 0) {
433
+ new Uint8Array(instanceData).set(new Uint8Array(oldData, 0, carryBytes));
434
+ }
435
+ const instanceBuffer = this._device.createBuffer({
436
+ size: instanceData.byteLength,
437
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
438
+ });
439
+ this._instanceBuffer?.destroy();
440
+ this._instanceCapacity = nextCapacity;
441
+ this._instanceData = instanceData;
442
+ this._instanceFloat32 = new Float32Array(instanceData);
443
+ this._instanceUint32 = new Uint32Array(instanceData);
444
+ this._instanceBuffer = instanceBuffer;
407
445
  }
408
- _getBatchRange(start) {
409
- const drawCall = this._drawCalls[start];
410
- const textureSlots = new Map();
411
- const textures = new Array();
412
- let end = start + 1;
413
- textureSlots.set(drawCall.texture, 0);
414
- textures.push(drawCall.texture);
415
- while (end < this._drawCallCount) {
416
- const nextDrawCall = this._drawCalls[end];
417
- if (nextDrawCall.blendMode !== drawCall.blendMode) {
418
- break;
419
- }
420
- if (!textureSlots.has(nextDrawCall.texture)) {
421
- if (textures.length >= maxBatchTextures) {
422
- break;
423
- }
424
- textureSlots.set(nextDrawCall.texture, textures.length);
425
- textures.push(nextDrawCall.texture);
446
+ _resetSlots() {
447
+ if (this._slotCount > 0) {
448
+ for (let i = 0; i < this._slotCount; i++) {
449
+ this._activeTextures[i] = null;
426
450
  }
427
- if (textureSlots.size > maxBatchTextures) {
428
- break;
429
- }
430
- end++;
451
+ this._textureSlots.clear();
452
+ this._slotCount = 0;
431
453
  }
432
- return {
433
- start,
434
- end,
435
- spriteCount: end - start,
436
- blendMode: drawCall.blendMode,
437
- textures,
438
- textureSlots,
439
- };
440
454
  }
441
- _createTextureBindGroup(device, renderManager, textures) {
442
- const fallbackTexture = textures[0];
443
- const fallbackBinding = renderManager.getTextureBinding(fallbackTexture);
444
- const entries = [];
455
+ _createTextureBindGroup(device, backend) {
456
+ // Slots beyond the active count get the slot-0 texture as a filler so
457
+ // the bind-group layout always sees N valid texture views and samplers.
458
+ // The fragment shader's switch only ever dispatches to the active slot
459
+ // count, so unsampled fillers cost nothing visually.
460
+ const fallbackTexture = this._activeTextures[0] ?? Texture.empty;
461
+ const fallbackBinding = backend.getTextureBinding(fallbackTexture);
445
462
  const resolvedBindings = new Array(maxBatchTextures);
446
- for (let index = 0; index < maxBatchTextures; index++) {
447
- const texture = textures[index] ?? fallbackTexture;
448
- const textureBinding = texture === fallbackTexture
463
+ for (let i = 0; i < maxBatchTextures; i++) {
464
+ const texture = this._activeTextures[i] ?? fallbackTexture;
465
+ resolvedBindings[i] = texture === fallbackTexture
449
466
  ? fallbackBinding
450
- : renderManager.getTextureBinding(texture);
451
- resolvedBindings[index] = textureBinding;
467
+ : backend.getTextureBinding(texture);
452
468
  }
453
- for (let index = 0; index < maxBatchTextures; index++) {
469
+ const entries = [];
470
+ for (let i = 0; i < maxBatchTextures; i++) {
454
471
  entries.push({
455
- binding: index,
456
- resource: resolvedBindings[index].view,
472
+ binding: i,
473
+ resource: resolvedBindings[i].view,
457
474
  });
458
475
  }
459
- for (let index = 0; index < maxBatchTextures; index++) {
476
+ for (let i = 0; i < maxBatchTextures; i++) {
460
477
  entries.push({
461
- binding: maxBatchTextures + index,
462
- resource: resolvedBindings[index].sampler,
478
+ binding: maxBatchTextures + i,
479
+ resource: resolvedBindings[i].sampler,
463
480
  });
464
481
  }
465
482
  return device.createBindGroup({
@@ -473,35 +490,48 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
473
490
  if (existingPipeline) {
474
491
  return existingPipeline;
475
492
  }
476
- if (!this._device || !this._shaderModule || !this._pipelineLayout || !this._renderManager) {
493
+ if (!this._device || !this._shaderModule || !this._pipelineLayout || !this._backend) {
477
494
  throw new Error('Renderer has to be connected first!');
478
495
  }
479
- const pipeline = this._device.createRenderPipeline({
496
+ const pipeline = this._device.createRenderPipeline(this._buildPipelineDescriptor(blendMode, format));
497
+ this._pipelines.set(pipelineKey, pipeline);
498
+ return pipeline;
499
+ }
500
+ _buildPipelineDescriptor(blendMode, format) {
501
+ if (!this._shaderModule || !this._pipelineLayout) {
502
+ throw new Error('Renderer has to be connected first!');
503
+ }
504
+ return {
480
505
  layout: this._pipelineLayout,
481
506
  vertex: {
482
507
  module: this._shaderModule,
483
508
  entryPoint: 'vertexMain',
484
509
  buffers: [{
485
- arrayStride: vertexStrideBytes,
510
+ arrayStride: instanceStrideBytes,
511
+ stepMode: 'instance',
486
512
  attributes: [{
487
513
  shaderLocation: 0,
488
514
  offset: 0,
489
- format: 'float32x2',
515
+ format: 'float32x4',
490
516
  }, {
491
517
  shaderLocation: 1,
492
- offset: 8,
493
- format: 'float32x2',
518
+ offset: 16,
519
+ format: 'float32x3',
494
520
  }, {
495
521
  shaderLocation: 2,
496
- offset: 16,
497
- format: 'unorm8x4',
522
+ offset: 28,
523
+ format: 'float32x3',
498
524
  }, {
499
525
  shaderLocation: 3,
500
- offset: 20,
501
- format: 'uint32',
526
+ offset: 40,
527
+ format: 'unorm16x4',
502
528
  }, {
503
529
  shaderLocation: 4,
504
- offset: 24,
530
+ offset: 48,
531
+ format: 'unorm8x4',
532
+ }, {
533
+ shaderLocation: 5,
534
+ offset: 52,
505
535
  format: 'uint32',
506
536
  }],
507
537
  }],
@@ -518,9 +548,7 @@ class WebGpuSpriteRenderer extends AbstractWebGpuRenderer {
518
548
  primitive: {
519
549
  topology: 'triangle-list',
520
550
  },
521
- });
522
- this._pipelines.set(pipelineKey, pipeline);
523
- return pipeline;
551
+ };
524
552
  }
525
553
  }
526
554