@driftengine/core 4.8.7 → 4.10.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 (263) hide show
  1. package/CHANGELOG.json +123 -11
  2. package/README.md +43 -38
  3. package/dist/index.d.ts +8 -1
  4. package/dist/index.js +11 -3
  5. package/dist/render/ambientHarmonics.d.ts +1 -1
  6. package/dist/render/ambientHarmonics.js +1 -1
  7. package/dist/render/areaLights.d.ts +4 -4
  8. package/dist/render/areaLights.js +1 -1
  9. package/dist/render/atmosphere.d.ts +13 -0
  10. package/dist/render/atmosphere.js +9 -1
  11. package/dist/render/backend/api.d.ts +19 -4
  12. package/dist/render/backend/createRenderer.d.ts +6 -0
  13. package/dist/render/backend/createRenderer.js +4 -1
  14. package/dist/render/backend/select.js +11 -5
  15. package/dist/render/backend/webgl2/boneAnimations.d.ts +29 -0
  16. package/dist/render/backend/webgl2/boneAnimations.js +80 -0
  17. package/dist/render/backend/webgl2/instanced.d.ts +5 -1
  18. package/dist/render/backend/webgl2/instanced.js +13 -1
  19. package/dist/render/backend/webgl2/renderer.d.ts +180 -4
  20. package/dist/render/backend/webgl2/renderer.js +550 -40
  21. package/dist/render/backend/webgl2/skinPaletteTexture.d.ts +5 -3
  22. package/dist/render/backend/webgl2/skinPaletteTexture.js +6 -4
  23. package/dist/render/backend/webgpu/boneAnimations.d.ts +34 -0
  24. package/dist/render/backend/webgpu/boneAnimations.js +76 -0
  25. package/dist/render/backend/webgpu/buffers.d.ts +19 -4
  26. package/dist/render/backend/webgpu/buffers.js +18 -11
  27. package/dist/render/backend/webgpu/compressedUpload.d.ts +0 -1
  28. package/dist/render/backend/webgpu/compressedUpload.js +53 -8
  29. package/dist/render/backend/webgpu/depthPass.d.ts +59 -3
  30. package/dist/render/backend/webgpu/depthPass.js +49 -14
  31. package/dist/render/backend/webgpu/device.d.ts +7 -1
  32. package/dist/render/backend/webgpu/device.js +31 -3
  33. package/dist/render/backend/webgpu/drawCommand.d.ts +18 -4
  34. package/dist/render/backend/webgpu/drawCommand.js +10 -3
  35. package/dist/render/backend/webgpu/flatPass.d.ts +100 -42
  36. package/dist/render/backend/webgpu/flatPass.js +191 -43
  37. package/dist/render/backend/webgpu/instanced.d.ts +14 -1
  38. package/dist/render/backend/webgpu/instanced.js +24 -1
  39. package/dist/render/backend/webgpu/materialStore.d.ts +101 -0
  40. package/dist/render/backend/webgpu/materialStore.js +265 -0
  41. package/dist/render/backend/webgpu/pipelineCache.d.ts +6 -0
  42. package/dist/render/backend/webgpu/pipelineCache.js +7 -0
  43. package/dist/render/backend/webgpu/postPass.d.ts +51 -9
  44. package/dist/render/backend/webgpu/postPass.js +16 -2
  45. package/dist/render/backend/webgpu/reflectionSurfacePass.d.ts +30 -0
  46. package/dist/render/backend/webgpu/reflectionSurfacePass.js +100 -0
  47. package/dist/render/backend/webgpu/renderer.d.ts +283 -37
  48. package/dist/render/backend/webgpu/renderer.js +1363 -202
  49. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +7 -3
  50. package/dist/render/backend/webgpu/skinPaletteRing.js +13 -7
  51. package/dist/render/backend/webgpu/skinPaletteTexture.d.ts +6 -0
  52. package/dist/render/backend/webgpu/skinPaletteTexture.js +7 -1
  53. package/dist/render/backend/webgpu/staticBundles.d.ts +125 -0
  54. package/dist/render/backend/webgpu/staticBundles.js +241 -0
  55. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +10 -3
  56. package/dist/render/backend/webgpu/surfaceTexturePass.js +20 -2
  57. package/dist/render/backend/webgpu/uniformRing.d.ts +21 -0
  58. package/dist/render/backend/webgpu/uniformRing.js +9 -1
  59. package/dist/render/bloomChain.d.ts +3 -3
  60. package/dist/render/boneAnimation.d.ts +92 -0
  61. package/dist/render/boneAnimation.js +133 -0
  62. package/dist/render/compressedSource.d.ts +33 -13
  63. package/dist/render/compressedSource.js +70 -25
  64. package/dist/render/displayRange.d.ts +31 -0
  65. package/dist/render/displayRange.js +47 -0
  66. package/dist/render/filmicCurve.d.ts +74 -0
  67. package/dist/render/filmicCurve.js +203 -0
  68. package/dist/render/fog.d.ts +4 -1
  69. package/dist/render/fog.js +12 -2
  70. package/dist/render/frame/resources.d.ts +2 -2
  71. package/dist/render/frame/resources.js +6 -0
  72. package/dist/render/frameReflections.d.ts +40 -0
  73. package/dist/render/frameReflections.js +48 -0
  74. package/dist/render/glCompressed.js +83 -0
  75. package/dist/render/instances.d.ts +26 -0
  76. package/dist/render/lightBudget.d.ts +10 -0
  77. package/dist/render/lightBudget.js +10 -0
  78. package/dist/render/lightmap.d.ts +1 -1
  79. package/dist/render/mesh.d.ts +5 -0
  80. package/dist/render/mesh.js +6 -0
  81. package/dist/render/pass.d.ts +49 -0
  82. package/dist/render/pointLightSelection.d.ts +4 -4
  83. package/dist/render/reflectionSurfaceTargets.d.ts +28 -0
  84. package/dist/render/reflectionSurfaceTargets.js +99 -0
  85. package/dist/render/renderQuality.d.ts +39 -8
  86. package/dist/render/renderQuality.js +4 -0
  87. package/dist/render/sceneTarget.d.ts +5 -0
  88. package/dist/render/sceneTarget.js +24 -1
  89. package/dist/render/screenSpaceReflection.d.ts +7 -1
  90. package/dist/render/screenSpaceReflection.js +5 -0
  91. package/dist/render/shaders/arcane.d.ts +1 -1
  92. package/dist/render/shaders/bolt.d.ts +1 -1
  93. package/dist/render/shaders/boneAnimation.d.ts +19 -0
  94. package/dist/render/shaders/boneAnimation.js +63 -0
  95. package/dist/render/shaders/caustics.d.ts +1 -1
  96. package/dist/render/shaders/depth.d.ts +7 -0
  97. package/dist/render/shaders/depth.js +35 -10
  98. package/dist/render/shaders/film.d.ts +1 -1
  99. package/dist/render/shaders/filmLook.js +2 -1
  100. package/dist/render/shaders/fire.d.ts +1 -1
  101. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  102. package/dist/render/shaders/flat/glassTint.js +2 -0
  103. package/dist/render/shaders/flat/index.d.ts +16 -0
  104. package/dist/render/shaders/flat/index.js +33 -5
  105. package/dist/render/shaders/flat/main.d.ts +1 -1
  106. package/dist/render/shaders/flat/main.js +43 -5
  107. package/dist/render/shaders/flat/models.js +1 -1
  108. package/dist/render/shaders/flat/overlay.d.ts +16 -0
  109. package/dist/render/shaders/flat/overlay.js +117 -0
  110. package/dist/render/shaders/flat/pointShadow.js +3 -3
  111. package/dist/render/shaders/flat/preamble.js +38 -24
  112. package/dist/render/shaders/flat/reflectionSurface.d.ts +21 -0
  113. package/dist/render/shaders/flat/reflectionSurface.js +61 -0
  114. package/dist/render/shaders/fog.d.ts +1 -1
  115. package/dist/render/shaders/fog.js +9 -2
  116. package/dist/render/shaders/fringe.d.ts +29 -0
  117. package/dist/render/shaders/fringe.js +55 -0
  118. package/dist/render/shaders/generated/arcane.wgsl.d.ts +1 -1
  119. package/dist/render/shaders/generated/arcane.wgsl.js +1 -1
  120. package/dist/render/shaders/generated/bolt.wgsl.d.ts +1 -1
  121. package/dist/render/shaders/generated/bolt.wgsl.js +1 -1
  122. package/dist/render/shaders/generated/caustics.wgsl.d.ts +1 -1
  123. package/dist/render/shaders/generated/caustics.wgsl.js +1 -1
  124. package/dist/render/shaders/generated/depth.wgsl.d.ts +93 -0
  125. package/dist/render/shaders/generated/depth.wgsl.js +93 -0
  126. package/dist/render/shaders/generated/film.wgsl.d.ts +1 -1
  127. package/dist/render/shaders/generated/film.wgsl.js +1 -1
  128. package/dist/render/shaders/generated/fire.wgsl.d.ts +1 -1
  129. package/dist/render/shaders/generated/fire.wgsl.js +1 -1
  130. package/dist/render/shaders/generated/flat.wgsl.d.ts +3254 -2920
  131. package/dist/render/shaders/generated/flat.wgsl.js +3292 -2957
  132. package/dist/render/shaders/generated/line.wgsl.d.ts +1 -1
  133. package/dist/render/shaders/generated/line.wgsl.js +1 -1
  134. package/dist/render/shaders/generated/particle.wgsl.d.ts +4 -4
  135. package/dist/render/shaders/generated/particle.wgsl.js +4 -4
  136. package/dist/render/shaders/generated/rush.wgsl.d.ts +56 -26
  137. package/dist/render/shaders/generated/rush.wgsl.js +56 -26
  138. package/dist/render/shaders/generated/scatter.wgsl.d.ts +1 -1
  139. package/dist/render/shaders/generated/scatter.wgsl.js +1 -1
  140. package/dist/render/shaders/generated/smoke.wgsl.d.ts +1 -1
  141. package/dist/render/shaders/generated/smoke.wgsl.js +1 -1
  142. package/dist/render/shaders/generated/ssrResolve.wgsl.d.ts +29 -0
  143. package/dist/render/shaders/generated/ssrResolve.wgsl.js +29 -0
  144. package/dist/render/shaders/generated/ssrTrace.wgsl.d.ts +22 -2
  145. package/dist/render/shaders/generated/ssrTrace.wgsl.js +22 -2
  146. package/dist/render/shaders/generated/water.wgsl.d.ts +1 -1
  147. package/dist/render/shaders/generated/water.wgsl.js +1 -1
  148. package/dist/render/shaders/gpudriven/fog.wgsl.d.ts +2 -2
  149. package/dist/render/shaders/gpudriven/fog.wgsl.js +11 -3
  150. package/dist/render/shaders/line.d.ts +1 -1
  151. package/dist/render/shaders/outputTransform.d.ts +7 -0
  152. package/dist/render/shaders/outputTransform.js +28 -14
  153. package/dist/render/shaders/particle.d.ts +3 -3
  154. package/dist/render/shaders/rush.js +54 -11
  155. package/dist/render/shaders/scatter.d.ts +1 -1
  156. package/dist/render/shaders/smoke.d.ts +1 -1
  157. package/dist/render/shaders/ssrResolve.d.ts +17 -0
  158. package/dist/render/shaders/ssrResolve.js +51 -0
  159. package/dist/render/shaders/ssrTrace.js +26 -0
  160. package/dist/render/shaders/vertexChannel.d.ts +7 -0
  161. package/dist/render/shaders/vertexChannel.js +26 -0
  162. package/dist/render/shaders/water.d.ts +1 -1
  163. package/dist/render/ssrPass.d.ts +16 -0
  164. package/dist/render/ssrPass.js +106 -1
  165. package/dist/render/staticDraws.d.ts +47 -0
  166. package/dist/render/staticDraws.js +43 -0
  167. package/dist/render/surfaceOverlay.d.ts +132 -0
  168. package/dist/render/surfaceOverlay.js +85 -0
  169. package/dist/render/surfaceTexture.d.ts +57 -7
  170. package/dist/render/surfaceTexture.js +125 -17
  171. package/dist/render/temporalAa.d.ts +14 -0
  172. package/dist/render/temporalAa.js +20 -0
  173. package/dist/render/vertexDefaults.d.ts +17 -0
  174. package/dist/render/vertexDefaults.js +24 -0
  175. package/package.json +6 -6
  176. package/src/index.ts +24 -2
  177. package/src/render/ambientHarmonics.ts +1 -1
  178. package/src/render/areaLights.ts +4 -4
  179. package/src/render/atmosphere.ts +22 -1
  180. package/src/render/backend/api.ts +22 -3
  181. package/src/render/backend/createRenderer.ts +18 -1
  182. package/src/render/backend/select.ts +10 -5
  183. package/src/render/backend/webgl2/boneAnimations.ts +105 -0
  184. package/src/render/backend/webgl2/instanced.ts +20 -1
  185. package/src/render/backend/webgl2/renderer.ts +672 -37
  186. package/src/render/backend/webgl2/skinPaletteTexture.ts +6 -4
  187. package/src/render/backend/webgpu/boneAnimations.ts +97 -0
  188. package/src/render/backend/webgpu/buffers.ts +42 -16
  189. package/src/render/backend/webgpu/compressedUpload.ts +59 -9
  190. package/src/render/backend/webgpu/depthPass.ts +51 -10
  191. package/src/render/backend/webgpu/device.ts +42 -3
  192. package/src/render/backend/webgpu/drawCommand.ts +22 -4
  193. package/src/render/backend/webgpu/flatPass.ts +250 -40
  194. package/src/render/backend/webgpu/instanced.ts +32 -1
  195. package/src/render/backend/webgpu/materialStore.ts +276 -0
  196. package/src/render/backend/webgpu/pipelineCache.ts +7 -0
  197. package/src/render/backend/webgpu/postPass.ts +18 -0
  198. package/src/render/backend/webgpu/reflectionSurfacePass.ts +114 -0
  199. package/src/render/backend/webgpu/renderer.ts +1655 -230
  200. package/src/render/backend/webgpu/skinPaletteRing.ts +13 -7
  201. package/src/render/backend/webgpu/skinPaletteTexture.ts +7 -1
  202. package/src/render/backend/webgpu/staticBundles.ts +363 -0
  203. package/src/render/backend/webgpu/surfaceTexturePass.ts +29 -3
  204. package/src/render/backend/webgpu/uniformRing.ts +10 -1
  205. package/src/render/bloomChain.ts +3 -3
  206. package/src/render/boneAnimation.ts +223 -0
  207. package/src/render/compressedSource.ts +121 -29
  208. package/src/render/displayRange.ts +61 -0
  209. package/src/render/filmicCurve.ts +229 -0
  210. package/src/render/fog.ts +19 -3
  211. package/src/render/frame/resources.ts +6 -0
  212. package/src/render/frameReflections.ts +72 -0
  213. package/src/render/glCompressed.ts +83 -0
  214. package/src/render/instances.ts +27 -0
  215. package/src/render/lightBudget.ts +11 -0
  216. package/src/render/lightmap.ts +1 -1
  217. package/src/render/mesh.ts +6 -0
  218. package/src/render/pass.ts +53 -1
  219. package/src/render/pointLightSelection.ts +4 -4
  220. package/src/render/reflectionSurfaceTargets.ts +105 -0
  221. package/src/render/renderQuality.ts +44 -8
  222. package/src/render/sceneTarget.ts +30 -2
  223. package/src/render/screenSpaceReflection.ts +7 -1
  224. package/src/render/shaders/boneAnimation.ts +63 -0
  225. package/src/render/shaders/depth.ts +45 -10
  226. package/src/render/shaders/filmLook.ts +2 -1
  227. package/src/render/shaders/flat/glassTint.ts +5 -1
  228. package/src/render/shaders/flat/index.ts +49 -5
  229. package/src/render/shaders/flat/main.ts +43 -5
  230. package/src/render/shaders/flat/models.ts +1 -1
  231. package/src/render/shaders/flat/overlay.ts +117 -0
  232. package/src/render/shaders/flat/pointShadow.ts +3 -3
  233. package/src/render/shaders/flat/preamble.ts +38 -24
  234. package/src/render/shaders/flat/reflectionSurface.ts +61 -0
  235. package/src/render/shaders/fog.ts +9 -2
  236. package/src/render/shaders/fringe.ts +59 -0
  237. package/src/render/shaders/generated/arcane.wgsl.ts +1 -1
  238. package/src/render/shaders/generated/bolt.wgsl.ts +1 -1
  239. package/src/render/shaders/generated/caustics.wgsl.ts +1 -1
  240. package/src/render/shaders/generated/depth.wgsl.ts +95 -0
  241. package/src/render/shaders/generated/film.wgsl.ts +1 -1
  242. package/src/render/shaders/generated/fire.wgsl.ts +1 -1
  243. package/src/render/shaders/generated/flat.wgsl.ts +3292 -2957
  244. package/src/render/shaders/generated/line.wgsl.ts +1 -1
  245. package/src/render/shaders/generated/particle.wgsl.ts +4 -4
  246. package/src/render/shaders/generated/rush.wgsl.ts +56 -26
  247. package/src/render/shaders/generated/scatter.wgsl.ts +1 -1
  248. package/src/render/shaders/generated/smoke.wgsl.ts +1 -1
  249. package/src/render/shaders/generated/ssrResolve.wgsl.ts +30 -0
  250. package/src/render/shaders/generated/ssrTrace.wgsl.ts +22 -2
  251. package/src/render/shaders/generated/water.wgsl.ts +1 -1
  252. package/src/render/shaders/gpudriven/fog.wgsl.ts +11 -3
  253. package/src/render/shaders/outputTransform.ts +30 -14
  254. package/src/render/shaders/rush.ts +54 -11
  255. package/src/render/shaders/ssrResolve.ts +52 -0
  256. package/src/render/shaders/ssrTrace.ts +26 -0
  257. package/src/render/shaders/vertexChannel.ts +27 -0
  258. package/src/render/ssrPass.ts +138 -1
  259. package/src/render/staticDraws.ts +95 -0
  260. package/src/render/surfaceOverlay.ts +195 -0
  261. package/src/render/surfaceTexture.ts +161 -27
  262. package/src/render/temporalAa.ts +20 -0
  263. package/src/render/vertexDefaults.ts +28 -0
@@ -22,16 +22,18 @@ export class SkinPaletteTexture {
22
22
  /**
23
23
  * Upload a palette, reallocating only when the joint count changes.
24
24
  *
25
- * A rig's joint count is fixed for its life, so the reallocation path runs once per skeleton and
26
- * the steady state is one `texSubImage2D`. Reallocating per frame would be a GPU allocation in
27
- * the frame loop, which is the same class of mistake as allocating on the CPU there.
25
+ * **The texture grows and never shrinks**, so the steady state is one `texSubImage2D` whatever
26
+ * mix of rigs a frame draws. It was rebuilt whenever the joint count changed, which with one
27
+ * texture for every skinned draw meant a character and a prop drawn in turn reallocated it at
28
+ * every draw: a GPU allocation in the frame loop. The shader reads joints by index, so a texture
29
+ * wider than the rig is read exactly as one that fits.
28
30
  */
29
31
  update(gl: WebGL2RenderingContext, palette: Float32Array): void {
30
32
  validateSkinPalette(palette);
31
33
  const joints = palette.length / 16;
32
34
  const width = paletteTextureWidth(joints);
33
35
 
34
- if (this.texture === null || this.joints !== joints) {
36
+ if (this.texture === null || joints > this.joints) {
35
37
  if (this.texture !== null) gl.deleteTexture(this.texture);
36
38
  const created = gl.createTexture();
37
39
  if (created === null) throw new Error('skin palette: failed to create the palette texture');
@@ -0,0 +1,97 @@
1
+ /**
2
+ * A bone animation's textures on this backend, and a batch's clocks: what
3
+ * `shaders/boneAnimation.ts` reads by index, made once and never filtered.
4
+ *
5
+ * **`rgba32float` places and turns, and `rg32float` clocks**, all read with `textureLoad` and bound
6
+ * `unfilterable-float`, as the joint palette is: a turn in half floats moves a crowd's hands by a
7
+ * thousandth of their reach, a jitter at the frame's rate, and the bytes are a clip's, once.
8
+ */
9
+ import {
10
+ CLOCK_TEXTURE_WIDTH,
11
+ type BoneAnimationHandle,
12
+ type BoneAnimationTexels,
13
+ } from '../../boneAnimation.ts';
14
+
15
+ const USAGE_TEXTURE = 0x4 | 0x2; // TEXTURE_BINDING | COPY_DST
16
+
17
+ /** A clip on the device, from `createBoneAnimation`. */
18
+ export class GpuBoneAnimation implements BoneAnimationHandle {
19
+ readonly bones: number;
20
+ readonly frames: number;
21
+ readonly framesPerSecond: number;
22
+ readonly boneScale: number;
23
+ readonly placesView: GPUTextureView;
24
+ readonly turnsView: GPUTextureView;
25
+ disposed = false;
26
+ private readonly places: GPUTexture;
27
+ private readonly turns: GPUTexture;
28
+
29
+ constructor(device: GPUDevice, texels: BoneAnimationTexels) {
30
+ this.bones = texels.bones;
31
+ this.frames = texels.frames;
32
+ this.framesPerSecond = texels.framesPerSecond;
33
+ this.boneScale = texels.boneScale;
34
+ const make = (label: string, data: Float32Array): GPUTexture => {
35
+ const texture = device.createTexture({
36
+ label,
37
+ size: [texels.bones, texels.frames],
38
+ format: 'rgba32float',
39
+ usage: USAGE_TEXTURE,
40
+ });
41
+ device.queue.writeTexture(
42
+ { texture },
43
+ data,
44
+ { bytesPerRow: texels.bones * 16, rowsPerImage: texels.frames },
45
+ [texels.bones, texels.frames],
46
+ );
47
+ return texture;
48
+ };
49
+ this.places = make('bones.places', texels.places);
50
+ this.turns = make('bones.turns', texels.turns);
51
+ this.placesView = this.places.createView();
52
+ this.turnsView = this.turns.createView();
53
+ }
54
+
55
+ dispose(): void {
56
+ this.disposed = true;
57
+ this.places.destroy();
58
+ this.turns.destroy();
59
+ }
60
+ }
61
+
62
+ /** A batch's clocks: two floats an instance, wrapped onto rows `CLOCK_TEXTURE_WIDTH` wide. */
63
+ export class GpuInstanceClocks {
64
+ readonly view: GPUTextureView;
65
+ readonly staging: Float32Array;
66
+ private readonly texture: GPUTexture;
67
+ private readonly width: number;
68
+
69
+ constructor(device: GPUDevice, capacity: number, label: string) {
70
+ this.width = Math.max(1, Math.min(capacity, CLOCK_TEXTURE_WIDTH));
71
+ const rows = Math.max(1, Math.ceil(capacity / this.width));
72
+ this.staging = new Float32Array(this.width * rows * 2);
73
+ this.texture = device.createTexture({
74
+ label,
75
+ size: [this.width, rows],
76
+ format: 'rg32float',
77
+ usage: USAGE_TEXTURE,
78
+ });
79
+ this.view = this.texture.createView();
80
+ }
81
+
82
+ /** The rows that hold the first `count` instances, from `staging`. */
83
+ upload(queue: GPUQueue, count: number): void {
84
+ if (count <= 0) return;
85
+ const rows = Math.ceil(count / this.width);
86
+ queue.writeTexture(
87
+ { texture: this.texture },
88
+ this.staging,
89
+ { bytesPerRow: this.width * 8, rowsPerImage: rows },
90
+ [this.width, rows],
91
+ );
92
+ }
93
+
94
+ dispose(): void {
95
+ this.texture.destroy();
96
+ }
97
+ }
@@ -258,18 +258,37 @@ export interface GpuMesh {
258
258
  device: GPUDevice,
259
259
  positions: Float32Array,
260
260
  normals?: Float32Array,
261
- previous?: GPUBuffer | null,
261
+ staged?: StagedRewrite | null,
262
262
  ) => void)
263
263
  | null;
264
264
  /**
265
- * A dynamic mesh's motion state, which the renderer owns: a buffer for last frame's positions,
266
- * made the first time a reconstructing renderer rewrites the mesh, and the frame of the last
267
- * rewrite. Null on a mesh created without `{ dynamic: true }`, which never moves a vertex.
265
+ * A dynamic mesh's motion state, which the renderer owns: a buffer for last frame's positions and
266
+ * one the new positions are staged in, each made the first time a reconstructing renderer
267
+ * rewrites the mesh, and the frame of the last rewrite. Null on a mesh created without
268
+ * `{ dynamic: true }`, which never moves a vertex.
268
269
  */
269
- readonly motion: { previous: GPUBuffer | null; changed: number } | null;
270
+ readonly motion: {
271
+ previous: GPUBuffer | null;
272
+ staging: GPUBuffer | null;
273
+ changed: number;
274
+ } | null;
270
275
  dispose(): void;
271
276
  }
272
277
 
278
+ /**
279
+ * A rewrite a reconstructing renderer stages rather than writes, so last frame's positions can be
280
+ * kept with no submit of the rewrite's own: the new positions are written into `staging`, and two
281
+ * copies are recorded into `encoder` — the live positions into `previous` first, where the frame's
282
+ * first rewrite keeps them, then the staged ones in. The renderer submits that encoder at the head
283
+ * of the frame's next submit, which is what keeps both copies ahead of every draw that reads the
284
+ * mesh. A `writeBuffer` straight to the live positions would land before the copy aside could run.
285
+ */
286
+ export interface StagedRewrite {
287
+ encoder: GPUCommandEncoder;
288
+ staging: GPUBuffer;
289
+ previous: GPUBuffer | null;
290
+ }
291
+
273
292
  /** A mesh on its way to the device, and the iterator that gets it there. */
274
293
  export interface IncrementalGpuMesh {
275
294
  /**
@@ -658,7 +677,10 @@ function gpuMeshOf(parts: {
658
677
  readonly morph: MorphTexture | null;
659
678
  readonly update: GpuMesh['update'];
660
679
  }): GpuMesh {
661
- const motion = parts.update === null ? null : { previous: null as GPUBuffer | null, changed: -1 };
680
+ const motion =
681
+ parts.update === null
682
+ ? null
683
+ : { previous: null as GPUBuffer | null, staging: null as GPUBuffer | null, changed: -1 };
662
684
  const { vertices, constants, streams, indexBuffer, progress } = parts;
663
685
  return {
664
686
  vertexBuffers:
@@ -684,6 +706,7 @@ function gpuMeshOf(parts: {
684
706
  motion,
685
707
  dispose(): void {
686
708
  motion?.previous?.destroy();
709
+ motion?.staging?.destroy();
687
710
  streams?.positions.destroy();
688
711
  streams?.normals.destroy();
689
712
  vertices.destroy();
@@ -700,9 +723,9 @@ function isStreamed(attribute: VertexAttribute): boolean {
700
723
 
701
724
  /**
702
725
  * A dynamic mesh's `update`: the caller's positions, and its normals where it has them, each
703
- * written to its own buffer as it came. Last frame's positions, where a reconstruction wants them,
704
- * are copied on the device before the write — a copy submitted ahead of a `writeBuffer` runs ahead
705
- * of it on the queue — so no CPU copy of anything is kept.
726
+ * written to its own buffer as it came — or, where a reconstruction keeps last frame's positions,
727
+ * the positions staged and copied in on the device behind the copy aside (`StagedRewrite`), so no
728
+ * CPU copy of anything is kept and the rewrite submits nothing of its own.
706
729
  */
707
730
  function streamUpdate(
708
731
  positionsBuffer: GPUBuffer,
@@ -715,7 +738,7 @@ function streamUpdate(
715
738
  target: GPUDevice,
716
739
  positions: Float32Array,
717
740
  normals?: Float32Array,
718
- previous: GPUBuffer | null = null,
741
+ staged: StagedRewrite | null = null,
719
742
  ): void => {
720
743
  for (const [name, values] of [
721
744
  ['update', positions],
@@ -728,15 +751,18 @@ function streamUpdate(
728
751
  'every other attribute are sized against it.',
729
752
  );
730
753
  }
731
- if (previous !== null) {
732
- const encoder = target.createCommandEncoder({ label: 'mesh.previousPositions' });
733
- encoder.copyBufferToBuffer(positionsBuffer, 0, previous, 0, streamBytes);
734
- target.queue.submit([encoder.finish()]);
735
- }
736
754
  /* Everything that decides whether this is on screen starts from the bounds, so a cloth
737
755
  that blew sideways out of its original box would be culled while still visible. */
738
756
  boundsOfPositions(positions, bounds);
739
- target.queue.writeBuffer(positionsBuffer, 0, positions);
757
+ if (staged === null) {
758
+ target.queue.writeBuffer(positionsBuffer, 0, positions);
759
+ } else {
760
+ target.queue.writeBuffer(staged.staging, 0, positions);
761
+ if (staged.previous !== null) {
762
+ staged.encoder.copyBufferToBuffer(positionsBuffer, 0, staged.previous, 0, streamBytes);
763
+ }
764
+ staged.encoder.copyBufferToBuffer(staged.staging, 0, positionsBuffer, 0, streamBytes);
765
+ }
740
766
  if (normals !== undefined) target.queue.writeBuffer(normalsBuffer, 0, normals);
741
767
  };
742
768
  }
@@ -12,7 +12,8 @@
12
12
  * the level nominally has.
13
13
  */
14
14
  import type { CompressedTextureFormat, CompressedTextureSource } from '../../compressedSource.ts';
15
- import { BLOCK_BYTES } from '../../compressedSource.ts';
15
+ import { BLOCK_SHAPES, blockFamily } from '../../compressedSource.ts';
16
+ import type { BlockFormat } from '../../compressedSource.ts';
16
17
 
17
18
  const GPU_FORMAT: Readonly<Record<CompressedTextureFormat, GPUTextureFormat>> = {
18
19
  bc1: 'bc1-rgba-unorm',
@@ -25,13 +26,61 @@ const GPU_FORMAT: Readonly<Record<CompressedTextureFormat, GPUTextureFormat>> =
25
26
  bc5: 'bc5-rg-unorm',
26
27
  bc7: 'bc7-rgba-unorm',
27
28
  'bc7-srgb': 'bc7-rgba-unorm-srgb',
29
+ 'etc2-rgb8': 'etc2-rgb8unorm',
30
+ 'etc2-rgb8-srgb': 'etc2-rgb8unorm-srgb',
31
+ 'etc2-rgb8a1': 'etc2-rgb8a1unorm',
32
+ 'etc2-rgb8a1-srgb': 'etc2-rgb8a1unorm-srgb',
33
+ 'etc2-rgba8': 'etc2-rgba8unorm',
34
+ 'etc2-rgba8-srgb': 'etc2-rgba8unorm-srgb',
35
+ 'eac-r11': 'eac-r11unorm',
36
+ 'eac-rg11': 'eac-rg11unorm',
37
+ 'astc-4x4': 'astc-4x4-unorm',
38
+ 'astc-4x4-srgb': 'astc-4x4-unorm-srgb',
39
+ 'astc-5x4': 'astc-5x4-unorm',
40
+ 'astc-5x4-srgb': 'astc-5x4-unorm-srgb',
41
+ 'astc-5x5': 'astc-5x5-unorm',
42
+ 'astc-5x5-srgb': 'astc-5x5-unorm-srgb',
43
+ 'astc-6x5': 'astc-6x5-unorm',
44
+ 'astc-6x5-srgb': 'astc-6x5-unorm-srgb',
45
+ 'astc-6x6': 'astc-6x6-unorm',
46
+ 'astc-6x6-srgb': 'astc-6x6-unorm-srgb',
47
+ 'astc-8x5': 'astc-8x5-unorm',
48
+ 'astc-8x5-srgb': 'astc-8x5-unorm-srgb',
49
+ 'astc-8x6': 'astc-8x6-unorm',
50
+ 'astc-8x6-srgb': 'astc-8x6-unorm-srgb',
51
+ 'astc-8x8': 'astc-8x8-unorm',
52
+ 'astc-8x8-srgb': 'astc-8x8-unorm-srgb',
53
+ 'astc-10x5': 'astc-10x5-unorm',
54
+ 'astc-10x5-srgb': 'astc-10x5-unorm-srgb',
55
+ 'astc-10x6': 'astc-10x6-unorm',
56
+ 'astc-10x6-srgb': 'astc-10x6-unorm-srgb',
57
+ 'astc-10x8': 'astc-10x8-unorm',
58
+ 'astc-10x8-srgb': 'astc-10x8-unorm-srgb',
59
+ 'astc-10x10': 'astc-10x10-unorm',
60
+ 'astc-10x10-srgb': 'astc-10x10-unorm-srgb',
61
+ 'astc-12x10': 'astc-12x10-unorm',
62
+ 'astc-12x10-srgb': 'astc-12x10-unorm-srgb',
63
+ 'astc-12x12': 'astc-12x12-unorm',
64
+ 'astc-12x12-srgb': 'astc-12x12-unorm-srgb',
28
65
  };
29
66
 
30
67
  /** Every BC format, where the device carries the feature; none where it does not. */
68
+ /** The feature that grants each family, which `select.ts` asks for wherever the adapter offers it. */
69
+ const FEATURE: Readonly<Record<'bc' | 'etc2' | 'astc', string>> = {
70
+ bc: 'texture-compression-bc',
71
+ etc2: 'texture-compression-etc2',
72
+ astc: 'texture-compression-astc',
73
+ };
74
+
31
75
  export function gpuCompressedFormats(device: GPUDevice): CompressedTextureFormat[] {
32
- return device.features.has('texture-compression-bc')
33
- ? (Object.keys(GPU_FORMAT) as CompressedTextureFormat[])
34
- : [];
76
+ return (Object.keys(GPU_FORMAT) as CompressedTextureFormat[]).filter((name) =>
77
+ device.features.has(FEATURE[blockFamily(baseOf(name))]),
78
+ );
79
+ }
80
+
81
+ /** The format a name is of, without its colour space. */
82
+ function baseOf(name: CompressedTextureFormat): BlockFormat {
83
+ return (name.endsWith('-srgb') ? name.slice(0, -'-srgb'.length) : name) as BlockFormat;
35
84
  }
36
85
 
37
86
  /** A texture holding `layers` as `name`, `levels` levels of each, written as they are. */
@@ -43,21 +92,22 @@ export function createCompressedTexture(
43
92
  ): GPUTexture {
44
93
  const { format, width, height } = layers[0] as CompressedTextureSource;
45
94
  const texture = device.createTexture({
46
- label: 'surface.texture.bc',
95
+ label: `surface.texture.${format}`,
47
96
  size: [width, height, layers.length],
48
97
  format: GPU_FORMAT[name],
49
98
  mipLevelCount: levels,
50
99
  usage: 0x2 | 0x4, // COPY_DST | TEXTURE_BINDING
51
100
  });
101
+ const block = BLOCK_SHAPES[format];
52
102
  for (let level = 0; level < levels; level++) {
53
- const across = Math.ceil(Math.max(1, width >> level) / 4);
54
- const down = Math.ceil(Math.max(1, height >> level) / 4);
103
+ const across = Math.ceil(Math.max(1, width >> level) / block.width);
104
+ const down = Math.ceil(Math.max(1, height >> level) / block.height);
55
105
  for (let layer = 0; layer < layers.length; layer++) {
56
106
  device.queue.writeTexture(
57
107
  { texture, mipLevel: level, origin: [0, 0, layer] },
58
108
  (layers[layer] as CompressedTextureSource).levels[level] as Uint8Array<ArrayBuffer>,
59
- { bytesPerRow: across * BLOCK_BYTES[format], rowsPerImage: down },
60
- { width: across * 4, height: down * 4, depthOrArrayLayers: 1 },
109
+ { bytesPerRow: across * block.bytes, rowsPerImage: down },
110
+ { width: across * block.width, height: down * block.height, depthOrArrayLayers: 1 },
61
111
  );
62
112
  }
63
113
  }
@@ -3,6 +3,8 @@ import {
3
3
  DEPTH_CUTOUT_FRAG_WGSL,
4
4
  DEPTH_CUTOUT_VERT_WGSL,
5
5
  DEPTH_FRAG_WGSL,
6
+ DEPTH_INSTANCED_ANIMATED_CUTOUT_VERT_WGSL,
7
+ DEPTH_INSTANCED_ANIMATED_VERT_WGSL,
6
8
  DEPTH_INSTANCED_CUTOUT_VERT_WGSL,
7
9
  DEPTH_INSTANCED_VERT_WGSL,
8
10
  DEPTH_SKINNED_VERT_WGSL,
@@ -13,6 +15,7 @@ import { vertexConstants } from './flatPass.ts';
13
15
  import type { PipelineCache } from './pipelineCache.ts';
14
16
  import { shaderModule } from './shaderModules.ts';
15
17
  import type { ClothViews } from './clothTextures.ts';
18
+ import type { AnimationViews } from './flatPass.ts';
16
19
 
17
20
  /**
18
21
  * The shadow pass: the world drawn from the light, into a depth map.
@@ -52,6 +55,17 @@ const CLOTH_TEXTURES = [
52
55
  DEPTH_BINDINGS.DEPTH_SKINNED_VERT.textures.uClothRest,
53
56
  ] as const;
54
57
 
58
+ /** A bone animation's three textures, at the bindings both animated depth variants declare them. */
59
+ const ANIMATION_TEXTURES = [
60
+ DEPTH_BINDINGS.DEPTH_INSTANCED_ANIMATED_VERT.textures.uBonePlaces,
61
+ DEPTH_BINDINGS.DEPTH_INSTANCED_ANIMATED_VERT.textures.uBoneTurns,
62
+ DEPTH_BINDINGS.DEPTH_INSTANCED_ANIMATED_VERT.textures.uInstanceClocks,
63
+ ] as const;
64
+ export const DEPTH_INSTANCED_ANIMATED_VERT_FIELDS =
65
+ DEPTH_BINDINGS.DEPTH_INSTANCED_ANIMATED_VERT.fields;
66
+ export const DEPTH_INSTANCED_ANIMATED_CUTOUT_VERT_FIELDS =
67
+ DEPTH_BINDINGS.DEPTH_INSTANCED_ANIMATED_CUTOUT_VERT.fields;
68
+
55
69
  const VISIBILITY_VERTEX = 0x1;
56
70
  const VISIBILITY_FRAGMENT = 0x2;
57
71
 
@@ -82,10 +96,19 @@ export function createDepthBindGroupLayout(
82
96
  * no cutout caster pays nothing.
83
97
  */
84
98
  cutout = false,
99
+ /** An instanced caster playing a bone animation: its three vertex textures. */
100
+ animated = false,
85
101
  ): GPUBindGroupLayout {
86
102
  return device.createBindGroupLayout({
87
- label: skinned ? 'depth.layout.skinned' : cutout ? 'depth.layout.cutout' : 'depth.layout',
103
+ label: `${skinned ? 'depth.layout.skinned' : cutout ? 'depth.layout.cutout' : 'depth.layout'}${animated ? '.animated' : ''}`,
88
104
  entries: [
105
+ ...(animated
106
+ ? ANIMATION_TEXTURES.map((binding) => ({
107
+ binding: binding.texture,
108
+ visibility: VISIBILITY_VERTEX,
109
+ texture: { sampleType: 'unfilterable-float' as GPUTextureSampleType },
110
+ }))
111
+ : []),
89
112
  /* The texture and no sampler: the shader reads the palette with `textureLoad`, an integer
90
113
  coordinate with nothing to filter, which is what `flatPass.ts` binds for the same
91
114
  palette. naga declares a sampler for it and nothing ever reads one. */
@@ -174,6 +197,8 @@ export function createDepthBindGroup(
174
197
  cutout: { readonly view: GPUTextureView; readonly sampler: GPUSampler } | null = null,
175
198
  /** A skinned group's cloth views: the caster's, or the stand-in where it has none. */
176
199
  cloth: ClothViews | null = null,
200
+ /** An animated caster's clip and clocks. See `AnimationViews`. */
201
+ animation: AnimationViews | null = null,
177
202
  ): GPUBindGroup {
178
203
  return device.createBindGroup({
179
204
  label:
@@ -208,6 +233,13 @@ export function createDepthBindGroup(
208
233
  { binding: CUTOUT_MAP.sampler, resource: cutout.sampler },
209
234
  ]
210
235
  : []),
236
+ ...(animation !== null
237
+ ? [
238
+ { binding: ANIMATION_TEXTURES[0].texture, resource: animation.places },
239
+ { binding: ANIMATION_TEXTURES[1].texture, resource: animation.turns },
240
+ { binding: ANIMATION_TEXTURES[2].texture, resource: animation.clocks },
241
+ ]
242
+ : []),
211
243
  ],
212
244
  });
213
245
  }
@@ -256,6 +288,8 @@ export function depthPipeline(
256
288
  skinEight = false,
257
289
  /** Whether a skinned caster is placed by a cloth binding, as its visible draw is. The key carries it. */
258
290
  cloth = false,
291
+ /** An instanced caster playing a bone animation. The key must carry it. */
292
+ animated = false,
259
293
  ): GPURenderPipeline {
260
294
  return cache.get(key, () => ({
261
295
  label: key,
@@ -263,15 +297,22 @@ export function depthPipeline(
263
297
  vertex: {
264
298
  module: shaderModule(
265
299
  device,
266
- cutout
267
- ? instanced
268
- ? { label: 'depth.instanced.cutout.vert', code: DEPTH_INSTANCED_CUTOUT_VERT_WGSL }
269
- : { label: 'depth.cutout.vert', code: DEPTH_CUTOUT_VERT_WGSL }
270
- : instanced
271
- ? { label: 'depth.instanced.vert', code: DEPTH_INSTANCED_VERT_WGSL }
272
- : skinned
273
- ? { label: 'depth.skinned.vert', code: DEPTH_SKINNED_VERT_WGSL }
274
- : { label: 'depth.vert', code: DEPTH_VERT_WGSL },
300
+ animated
301
+ ? cutout
302
+ ? {
303
+ label: 'depth.instanced.animated.cutout.vert',
304
+ code: DEPTH_INSTANCED_ANIMATED_CUTOUT_VERT_WGSL,
305
+ }
306
+ : { label: 'depth.instanced.animated.vert', code: DEPTH_INSTANCED_ANIMATED_VERT_WGSL }
307
+ : cutout
308
+ ? instanced
309
+ ? { label: 'depth.instanced.cutout.vert', code: DEPTH_INSTANCED_CUTOUT_VERT_WGSL }
310
+ : { label: 'depth.cutout.vert', code: DEPTH_CUTOUT_VERT_WGSL }
311
+ : instanced
312
+ ? { label: 'depth.instanced.vert', code: DEPTH_INSTANCED_VERT_WGSL }
313
+ : skinned
314
+ ? { label: 'depth.skinned.vert', code: DEPTH_SKINNED_VERT_WGSL }
315
+ : { label: 'depth.vert', code: DEPTH_VERT_WGSL },
275
316
  ),
276
317
  entryPoint: 'main',
277
318
  buffers: vertexBufferLayouts(present, instanced),
@@ -7,6 +7,14 @@
7
7
  * reset the page did not cause, and a frame is wrong because of something the frame did.
8
8
  */
9
9
 
10
+ import type { DisplayRange, DisplayRangeChoice } from '../../displayRange.ts';
11
+
12
+ /** A canvas nobody asked to extend. */
13
+ const STANDARD_RANGE: DisplayRangeChoice = {
14
+ range: 'standard',
15
+ reason: 'highDynamicRange was not asked for',
16
+ };
17
+
10
18
  /** What the renderer needs from the canvas it draws into, with the API's shape absorbed. */
11
19
  export interface GpuSurface {
12
20
  readonly device: GPUDevice;
@@ -22,6 +30,9 @@ export interface GpuSurface {
22
30
  readonly canvas: HTMLCanvasElement;
23
31
  /** The swap chain's format, which every render pipeline has to be built against. */
24
32
  readonly format: GPUTextureFormat;
33
+ /** The range the canvas was configured in, and why. See `displayRange.ts`. */
34
+ readonly displayRange: DisplayRange;
35
+ readonly displayRangeReason: string;
25
36
  /** Whether the device has gone. Read per frame; never throws. */
26
37
  readonly lost: boolean;
27
38
  /** Called when the device is lost. Fires immediately if it already has been. */
@@ -51,13 +62,27 @@ export interface GpuSurface {
51
62
  * consumer its GPU had failed every time it unmounted a canvas on purpose, so a disposed
52
63
  * surface stays silent.
53
64
  */
54
- export function createGpuSurface(canvas: HTMLCanvasElement, device: GPUDevice): GpuSurface {
65
+ export function createGpuSurface(
66
+ canvas: HTMLCanvasElement,
67
+ device: GPUDevice,
68
+ /** The range the profile and the display allow (`chooseDisplayRange`); standard unless given. */
69
+ wanted: DisplayRangeChoice = STANDARD_RANGE,
70
+ ): GpuSurface {
55
71
  const context = canvas.getContext('webgpu') as GPUCanvasContext | null;
56
72
  if (context === null) {
57
73
  throw new Error('createGpuSurface: the canvas would not give a webgpu context');
58
74
  }
59
75
 
60
- const format = navigator.gpu.getPreferredCanvasFormat();
76
+ /*
77
+ * **A high range is half floats with extended tone mapping**: a value of 1 is the display's
78
+ * paper white, and what lies above it is shown up to the display's peak instead of clipped. The
79
+ * colour space stays sRGB, so the composite's encoding is the one it always wrote, carried past 1.
80
+ */
81
+ const extended = wanted.range === 'high';
82
+ const format: GPUTextureFormat = extended
83
+ ? 'rgba16float'
84
+ : navigator.gpu.getPreferredCanvasFormat();
85
+ let range: DisplayRangeChoice = wanted;
61
86
  const listeners: (() => void)[] = [];
62
87
  let lost = false;
63
88
  let disposed = false;
@@ -115,16 +140,30 @@ export function createGpuSurface(canvas: HTMLCanvasElement, device: GPUDevice):
115
140
  * has no use for a transparent canvas, and the opaque path is the one a compositor can
116
141
  * take without a blend.
117
142
  */
118
- context.configure({ device, format, alphaMode: 'opaque' });
143
+ context.configure(
144
+ extended
145
+ ? { device, format, alphaMode: 'opaque', toneMapping: { mode: 'extended' } }
146
+ : { device, format, alphaMode: 'opaque' },
147
+ );
119
148
  };
120
149
 
121
150
  configure(canvas.width, canvas.height);
151
+ /*
152
+ * **Read back, because a browser that does not know the option ignores it** and draws the
153
+ * standard range from an extended-range frame, clipped at paper white, which is the frame a game
154
+ * would grade for the standard range if it knew. So it is told.
155
+ */
156
+ if (extended && context.getConfiguration?.()?.toneMapping?.mode !== 'extended') {
157
+ range = { range: 'standard', reason: 'this browser has no extended-range canvas' };
158
+ }
122
159
 
123
160
  return {
124
161
  device,
125
162
  context,
126
163
  canvas,
127
164
  format,
165
+ displayRange: range.range,
166
+ displayRangeReason: range.reason,
128
167
  get lost() {
129
168
  return lost;
130
169
  },
@@ -2,7 +2,7 @@
2
2
  * One recorded draw, in the only shape this backend ever issues.
3
3
  *
4
4
  * Sampled across three unrelated verbs — panels, particles, wind streaks — the sequence is the
5
- * same every time: a pipeline, a bind group with up to two dynamic offsets, some vertex
5
+ * same every time: a pipeline, a bind group with up to four dynamic offsets, some vertex
6
6
  * buffers, perhaps an index buffer, then a draw. Nothing in it is specific to what is being
7
7
  * drawn except the values.
8
8
  *
@@ -21,9 +21,14 @@ const VERTEX_BUFFER_SLOTS = 8;
21
21
  export interface DrawCommand {
22
22
  pipeline: GPURenderPipeline | null;
23
23
  bindGroup: GPUBindGroup | null;
24
- /** Dynamic offsets, held as two numbers rather than an array so a reset stays free. */
24
+ /**
25
+ * Dynamic offsets, held as numbers rather than an array so a reset stays free. Four only for a
26
+ * lit mesh: its draw's slot, its pass's, its material's and its view's, in binding order.
27
+ */
25
28
  offsetA: number;
26
29
  offsetB: number;
30
+ offsetC: number;
31
+ offsetD: number;
27
32
  offsetCount: number;
28
33
  /** Fixed length; `vertexCount` says how many slots are live. */
29
34
  readonly vertexBuffers: (GPUBuffer | null)[];
@@ -37,6 +42,12 @@ export interface DrawCommand {
37
42
  * cull's survivors, counted on the device. Null for every other draw.
38
43
  */
39
44
  indirect: GPUBuffer | null;
45
+ /**
46
+ * Render bundles to execute instead of everything above: a static list recorded once, replayed
47
+ * as one command. Held as the array `executeBundles` takes, kept by the list, so issuing it
48
+ * allocates nothing. Null for every other command, and put back to null by every take.
49
+ */
50
+ bundles: readonly GPURenderBundle[] | null;
40
51
  }
41
52
 
42
53
  export interface CommandPool {
@@ -52,6 +63,8 @@ function emptyCommand(): DrawCommand {
52
63
  bindGroup: null,
53
64
  offsetA: 0,
54
65
  offsetB: 0,
66
+ offsetC: 0,
67
+ offsetD: 0,
55
68
  offsetCount: 0,
56
69
  vertexBuffers: new Array<GPUBuffer | null>(VERTEX_BUFFER_SLOTS).fill(null),
57
70
  vertexCount: 0,
@@ -60,6 +73,7 @@ function emptyCommand(): DrawCommand {
60
73
  count: 0,
61
74
  instances: 1,
62
75
  indirect: null,
76
+ bundles: null,
63
77
  };
64
78
  }
65
79
 
@@ -72,14 +86,18 @@ export function createCommandPool(capacity: number): CommandPool {
72
86
  /**
73
87
  * Hand out the next command, growing if a frame overruns.
74
88
  *
75
- * The entry is **not cleared**. Every caller fills every field it uses, and clearing here would
76
- * be a write per field per draw for no reader — the same reasoning the node arena uses.
89
+ * The entry is **not cleared**, but for `bundles`. Every caller fills every field it uses, and
90
+ * clearing here would be a write per field per draw for no reader — the same reasoning the node
91
+ * arena uses. `bundles` is the exception because almost no caller knows it exists: a pooled command
92
+ * that once carried a static list's bundles and was then taken for an ordinary draw would replay the
93
+ * list instead, so the one field nobody fills is the one field put back here.
77
94
  */
78
95
  export function takeCommand(pool: CommandPool): number {
79
96
  const at = pool.taken;
80
97
  while (at >= pool.commands.length) pool.commands.push(emptyCommand());
81
98
  pool.taken = at + 1;
82
99
  if (pool.taken > pool.highWater) pool.highWater = pool.taken;
100
+ (pool.commands[at] as DrawCommand).bundles = null;
83
101
  return at;
84
102
  }
85
103