@driftengine/core 4.9.0 → 4.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 (231) hide show
  1. package/CHANGELOG.json +133 -11
  2. package/README.md +43 -38
  3. package/dist/index.d.ts +7 -4
  4. package/dist/index.js +5 -4
  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/backend/api.d.ts +10 -3
  10. package/dist/render/backend/select.js +11 -5
  11. package/dist/render/backend/webgl2/boneAnimations.d.ts +29 -0
  12. package/dist/render/backend/webgl2/boneAnimations.js +80 -0
  13. package/dist/render/backend/webgl2/instanced.d.ts +5 -1
  14. package/dist/render/backend/webgl2/instanced.js +13 -1
  15. package/dist/render/backend/webgl2/renderer.d.ts +189 -4
  16. package/dist/render/backend/webgl2/renderer.js +614 -66
  17. package/dist/render/backend/webgl2/skinPaletteTexture.d.ts +5 -3
  18. package/dist/render/backend/webgl2/skinPaletteTexture.js +6 -4
  19. package/dist/render/backend/webgpu/boneAnimations.d.ts +34 -0
  20. package/dist/render/backend/webgpu/boneAnimations.js +76 -0
  21. package/dist/render/backend/webgpu/buffers.d.ts +24 -4
  22. package/dist/render/backend/webgpu/buffers.js +116 -59
  23. package/dist/render/backend/webgpu/clothKernels.d.ts +8 -0
  24. package/dist/render/backend/webgpu/clothKernels.js +76 -0
  25. package/dist/render/backend/webgpu/clothOps.d.ts +39 -0
  26. package/dist/render/backend/webgpu/clothOps.js +78 -0
  27. package/dist/render/backend/webgpu/clothSetLayout.d.ts +25 -0
  28. package/dist/render/backend/webgpu/clothSetLayout.js +84 -0
  29. package/dist/render/backend/webgpu/clothSetPack.d.ts +33 -0
  30. package/dist/render/backend/webgpu/clothSetPack.js +144 -0
  31. package/dist/render/backend/webgpu/compressedUpload.d.ts +0 -1
  32. package/dist/render/backend/webgpu/compressedUpload.js +53 -8
  33. package/dist/render/backend/webgpu/depthPass.d.ts +59 -3
  34. package/dist/render/backend/webgpu/depthPass.js +49 -14
  35. package/dist/render/backend/webgpu/drawCommand.d.ts +20 -4
  36. package/dist/render/backend/webgpu/drawCommand.js +11 -3
  37. package/dist/render/backend/webgpu/flatPass.d.ts +85 -42
  38. package/dist/render/backend/webgpu/flatPass.js +137 -20
  39. package/dist/render/backend/webgpu/gpuClothSet.d.ts +80 -0
  40. package/dist/render/backend/webgpu/gpuClothSet.js +462 -0
  41. package/dist/render/backend/webgpu/instanced.d.ts +14 -1
  42. package/dist/render/backend/webgpu/instanced.js +24 -1
  43. package/dist/render/backend/webgpu/latePass.js +1 -0
  44. package/dist/render/backend/webgpu/materialStore.d.ts +101 -0
  45. package/dist/render/backend/webgpu/materialStore.js +265 -0
  46. package/dist/render/backend/webgpu/motionPass.js +15 -6
  47. package/dist/render/backend/webgpu/pipelineCache.d.ts +28 -1
  48. package/dist/render/backend/webgpu/pipelineCache.js +46 -1
  49. package/dist/render/backend/webgpu/postPass.d.ts +1 -1
  50. package/dist/render/backend/webgpu/renderer.d.ts +200 -35
  51. package/dist/render/backend/webgpu/renderer.js +993 -168
  52. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +9 -2
  53. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +121 -47
  54. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +7 -3
  55. package/dist/render/backend/webgpu/skinPaletteRing.js +13 -7
  56. package/dist/render/backend/webgpu/skinPaletteTexture.d.ts +6 -0
  57. package/dist/render/backend/webgpu/skinPaletteTexture.js +7 -1
  58. package/dist/render/backend/webgpu/staticBundles.d.ts +125 -0
  59. package/dist/render/backend/webgpu/staticBundles.js +242 -0
  60. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +10 -3
  61. package/dist/render/backend/webgpu/surfaceTexturePass.js +20 -2
  62. package/dist/render/backend/webgpu/uniformRing.d.ts +21 -0
  63. package/dist/render/backend/webgpu/uniformRing.js +9 -1
  64. package/dist/render/backend/webgpu/vertexFormats.d.ts +22 -0
  65. package/dist/render/backend/webgpu/vertexFormats.js +97 -0
  66. package/dist/render/bloomChain.d.ts +3 -3
  67. package/dist/render/boneAnimation.d.ts +92 -0
  68. package/dist/render/boneAnimation.js +133 -0
  69. package/dist/render/compressedSource.d.ts +33 -13
  70. package/dist/render/compressedSource.js +70 -25
  71. package/dist/render/glCompressed.js +83 -0
  72. package/dist/render/globalMediumPass.d.ts +2 -0
  73. package/dist/render/globalMediumPass.js +1 -0
  74. package/dist/render/indexWidth.d.ts +17 -0
  75. package/dist/render/indexWidth.js +23 -0
  76. package/dist/render/instances.d.ts +26 -0
  77. package/dist/render/lightBudget.d.ts +10 -0
  78. package/dist/render/lightBudget.js +10 -0
  79. package/dist/render/lightMatrix.d.ts +11 -0
  80. package/dist/render/lightMatrix.js +14 -0
  81. package/dist/render/lightmap.d.ts +1 -1
  82. package/dist/render/mesh.d.ts +14 -0
  83. package/dist/render/mesh.js +44 -14
  84. package/dist/render/pointLightSelection.d.ts +4 -4
  85. package/dist/render/renderQuality.d.ts +30 -0
  86. package/dist/render/renderQuality.js +5 -0
  87. package/dist/render/rendererHarness.d.ts +7 -0
  88. package/dist/render/rendererHarness.js +12 -0
  89. package/dist/render/screenSpaceReflection.d.ts +2 -1
  90. package/dist/render/shader.d.ts +15 -0
  91. package/dist/render/shader.js +41 -0
  92. package/dist/render/shaders/boneAnimation.d.ts +19 -0
  93. package/dist/render/shaders/boneAnimation.js +63 -0
  94. package/dist/render/shaders/depth.d.ts +7 -0
  95. package/dist/render/shaders/depth.js +35 -10
  96. package/dist/render/shaders/flat/directionalShadow.js +84 -10
  97. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  98. package/dist/render/shaders/flat/glassTint.js +3 -0
  99. package/dist/render/shaders/flat/index.d.ts +21 -0
  100. package/dist/render/shaders/flat/index.js +42 -7
  101. package/dist/render/shaders/flat/layered.d.ts +19 -0
  102. package/dist/render/shaders/flat/layered.js +115 -0
  103. package/dist/render/shaders/flat/main.d.ts +1 -1
  104. package/dist/render/shaders/flat/main.js +58 -21
  105. package/dist/render/shaders/flat/mapReads.d.ts +10 -0
  106. package/dist/render/shaders/flat/mapReads.js +38 -0
  107. package/dist/render/shaders/flat/models.js +1 -1
  108. package/dist/render/shaders/flat/movingSun.d.ts +21 -0
  109. package/dist/render/shaders/flat/movingSun.js +46 -0
  110. package/dist/render/shaders/flat/normalMap.d.ts +6 -2
  111. package/dist/render/shaders/flat/normalMap.js +14 -3
  112. package/dist/render/shaders/flat/overlay.d.ts +3 -2
  113. package/dist/render/shaders/flat/overlay.js +42 -20
  114. package/dist/render/shaders/flat/pointShadow.js +2 -2
  115. package/dist/render/shaders/flat/preamble.js +59 -27
  116. package/dist/render/shaders/flat/worldUv.d.ts +22 -0
  117. package/dist/render/shaders/flat/worldUv.js +113 -0
  118. package/dist/render/shaders/generated/depth.wgsl.d.ts +93 -0
  119. package/dist/render/shaders/generated/depth.wgsl.js +93 -0
  120. package/dist/render/shaders/generated/flat.wgsl.d.ts +3616 -3042
  121. package/dist/render/shaders/generated/flat.wgsl.js +3688 -3113
  122. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +8 -3
  123. package/dist/render/shaders/generated/globalMedium.wgsl.js +8 -3
  124. package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +15 -10
  125. package/dist/render/shaders/generated/lightVolume.wgsl.js +16 -11
  126. package/dist/render/shaders/globalMedium.js +9 -1
  127. package/dist/render/shaders/lightVolume.js +9 -1
  128. package/dist/render/shaders/outputTransform.d.ts +7 -0
  129. package/dist/render/shaders/outputTransform.js +28 -14
  130. package/dist/render/shaders/tangentFrame.d.ts +1 -1
  131. package/dist/render/shaders/tangentFrame.js +16 -3
  132. package/dist/render/shaders/vertexChannel.d.ts +7 -0
  133. package/dist/render/shaders/vertexChannel.js +26 -0
  134. package/dist/render/skinnedClothRun.d.ts +28 -0
  135. package/dist/render/skinnedClothRun.js +103 -35
  136. package/dist/render/staticDraws.d.ts +47 -0
  137. package/dist/render/staticDraws.js +43 -0
  138. package/dist/render/surfaceLayers.d.ts +39 -0
  139. package/dist/render/surfaceLayers.js +26 -0
  140. package/dist/render/surfaceOverlay.d.ts +16 -3
  141. package/dist/render/surfaceOverlay.js +11 -6
  142. package/dist/render/surfaceProjection.d.ts +34 -0
  143. package/dist/render/surfaceProjection.js +30 -0
  144. package/dist/render/surfaceTexture.d.ts +68 -7
  145. package/dist/render/surfaceTexture.js +56 -17
  146. package/dist/render/vertexPacking.d.ts +48 -0
  147. package/dist/render/vertexPacking.js +68 -0
  148. package/package.json +6 -6
  149. package/src/index.ts +12 -4
  150. package/src/render/ambientHarmonics.ts +1 -1
  151. package/src/render/areaLights.ts +4 -4
  152. package/src/render/backend/api.ts +13 -3
  153. package/src/render/backend/select.ts +10 -5
  154. package/src/render/backend/webgl2/boneAnimations.ts +105 -0
  155. package/src/render/backend/webgl2/instanced.ts +20 -1
  156. package/src/render/backend/webgl2/renderer.ts +723 -79
  157. package/src/render/backend/webgl2/skinPaletteTexture.ts +6 -4
  158. package/src/render/backend/webgpu/boneAnimations.ts +97 -0
  159. package/src/render/backend/webgpu/buffers.ts +195 -70
  160. package/src/render/backend/webgpu/clothKernels.ts +86 -0
  161. package/src/render/backend/webgpu/clothOps.ts +86 -0
  162. package/src/render/backend/webgpu/clothSetLayout.ts +102 -0
  163. package/src/render/backend/webgpu/clothSetPack.ts +197 -0
  164. package/src/render/backend/webgpu/compressedUpload.ts +59 -9
  165. package/src/render/backend/webgpu/depthPass.ts +51 -10
  166. package/src/render/backend/webgpu/drawCommand.ts +25 -4
  167. package/src/render/backend/webgpu/flatPass.ts +212 -41
  168. package/src/render/backend/webgpu/gpuClothSet.ts +564 -0
  169. package/src/render/backend/webgpu/instanced.ts +32 -1
  170. package/src/render/backend/webgpu/latePass.ts +1 -0
  171. package/src/render/backend/webgpu/materialStore.ts +276 -0
  172. package/src/render/backend/webgpu/motionPass.ts +18 -7
  173. package/src/render/backend/webgpu/pipelineCache.ts +53 -7
  174. package/src/render/backend/webgpu/renderer.ts +1201 -202
  175. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +121 -47
  176. package/src/render/backend/webgpu/skinPaletteRing.ts +13 -7
  177. package/src/render/backend/webgpu/skinPaletteTexture.ts +7 -1
  178. package/src/render/backend/webgpu/staticBundles.ts +364 -0
  179. package/src/render/backend/webgpu/surfaceTexturePass.ts +29 -3
  180. package/src/render/backend/webgpu/uniformRing.ts +10 -1
  181. package/src/render/backend/webgpu/vertexFormats.ts +113 -0
  182. package/src/render/bloomChain.ts +3 -3
  183. package/src/render/boneAnimation.ts +223 -0
  184. package/src/render/compressedSource.ts +121 -29
  185. package/src/render/glCompressed.ts +83 -0
  186. package/src/render/globalMediumPass.ts +7 -0
  187. package/src/render/indexWidth.ts +27 -0
  188. package/src/render/instances.ts +27 -0
  189. package/src/render/lightBudget.ts +11 -0
  190. package/src/render/lightMatrix.ts +15 -0
  191. package/src/render/lightmap.ts +1 -1
  192. package/src/render/mesh.ts +72 -14
  193. package/src/render/pointLightSelection.ts +4 -4
  194. package/src/render/renderQuality.ts +38 -0
  195. package/src/render/rendererHarness.ts +15 -0
  196. package/src/render/screenSpaceReflection.ts +2 -1
  197. package/src/render/shader.ts +59 -0
  198. package/src/render/shaders/boneAnimation.ts +63 -0
  199. package/src/render/shaders/depth.ts +45 -10
  200. package/src/render/shaders/flat/directionalShadow.ts +84 -10
  201. package/src/render/shaders/flat/glassTint.ts +7 -1
  202. package/src/render/shaders/flat/index.ts +63 -7
  203. package/src/render/shaders/flat/layered.ts +115 -0
  204. package/src/render/shaders/flat/main.ts +58 -21
  205. package/src/render/shaders/flat/mapReads.ts +38 -0
  206. package/src/render/shaders/flat/models.ts +1 -1
  207. package/src/render/shaders/flat/movingSun.ts +46 -0
  208. package/src/render/shaders/flat/normalMap.ts +14 -3
  209. package/src/render/shaders/flat/overlay.ts +42 -20
  210. package/src/render/shaders/flat/pointShadow.ts +2 -2
  211. package/src/render/shaders/flat/preamble.ts +59 -27
  212. package/src/render/shaders/flat/worldUv.ts +113 -0
  213. package/src/render/shaders/generated/depth.wgsl.ts +95 -0
  214. package/src/render/shaders/generated/flat.wgsl.ts +3688 -3113
  215. package/src/render/shaders/generated/globalMedium.wgsl.ts +8 -3
  216. package/src/render/shaders/generated/lightVolume.wgsl.ts +16 -11
  217. package/src/render/shaders/globalMedium.ts +9 -1
  218. package/src/render/shaders/lightVolume.ts +9 -1
  219. package/src/render/shaders/outputTransform.ts +30 -14
  220. package/src/render/shaders/tangentFrame.ts +16 -3
  221. package/src/render/shaders/vertexChannel.ts +27 -0
  222. package/src/render/skinnedClothRun.ts +138 -38
  223. package/src/render/staticDraws.ts +95 -0
  224. package/src/render/surfaceLayers.ts +57 -0
  225. package/src/render/surfaceOverlay.ts +24 -7
  226. package/src/render/surfaceProjection.ts +57 -0
  227. package/src/render/surfaceTexture.ts +124 -27
  228. package/src/render/vertexPacking.ts +109 -0
  229. package/dist/render/backend/webgpu/gpuSkinnedCloth.d.ts +0 -74
  230. package/dist/render/backend/webgpu/gpuSkinnedCloth.js +0 -452
  231. package/src/render/backend/webgpu/gpuSkinnedCloth.ts +0 -502
@@ -1,3 +1,5 @@
1
+ import { vertexPackingOf } from '../../vertexPacking.ts';
2
+ import { markPacking, markPackingOfKey, packingKey } from './vertexFormats.ts';
1
3
  import { mat4 } from 'gl-matrix';
2
4
  import type { ProbeBakeOptions } from '../../reflectionProbe.ts';
3
5
  import { probeFaceRange } from '../../reflectionProbe.ts';
@@ -194,15 +196,23 @@ import {
194
196
  packSurfaceOverlay,
195
197
  type SurfaceOverlay,
196
198
  } from '../../surfaceOverlay.ts';
199
+ import { PROJECTION_FLOATS, packSurfaceProjection } from '../../surfaceProjection.ts';
200
+ import { orthographicDepthSpan } from '../../lightMatrix.ts';
201
+ import { LAYER_FLOATS, packSurfaceLayers } from '../../surfaceLayers.ts';
202
+ import type { SurfaceLayers } from '../../surfaceLayers.ts';
197
203
  import {
198
204
  frameReflectionsRefused,
199
205
  resolveFrameReflections,
200
206
  type ResolvedFrameReflections,
201
207
  } from '../../frameReflections.ts';
202
- import { ReflectionSurfacePass } from './reflectionSurfacePass.ts';
208
+ import { REFLECTION_SURFACE_FORMAT, ReflectionSurfacePass } from './reflectionSurfacePass.ts';
203
209
  import { validateClothBinding } from '../../clothBindingData.ts';
204
210
  import type { ClothBindingData } from '../../clothBindingData.ts';
205
- import type { CompressedTextureFormat, SurfaceSource } from '../../compressedSource.ts';
211
+ import type {
212
+ CompressedTextureFormat,
213
+ CompressedTextureSource,
214
+ SurfaceSource,
215
+ } from '../../compressedSource.ts';
206
216
  import {
207
217
  createSdfTextBindGroup,
208
218
  createSdfTextBindGroupLayout,
@@ -412,16 +422,25 @@ import {
412
422
  SUN_STATIC_LAYER,
413
423
  } from '../../shadowMap.ts';
414
424
  import type { SceneCasterMaterial, ShadowCasterSink, ShadowCasters } from '../../shadowCasters.ts';
415
- import { VERTEX_LAYOUT, createGpuMesh, createGpuMeshIncremental, type GpuMesh } from './buffers.ts';
425
+ import {
426
+ VERTEX_LAYOUT,
427
+ createGpuMesh,
428
+ createGpuMeshIncremental,
429
+ type GpuMesh,
430
+ type StagedRewrite,
431
+ } from './buffers.ts';
416
432
  import type { GpuSurface } from './device.ts';
417
433
  import { SkinPaletteRing } from './skinPaletteRing.ts';
418
434
  import {
419
435
  DEPTH_FORMAT,
436
+ FLAT_ANIMATED_VERT_FIELDS,
420
437
  FLAT_VERT_FIELDS,
421
438
  FLAT_VERT_SIZE,
439
+ FLAT_VIEW_FIELDS,
440
+ FLAT_VIEW_SIZE,
422
441
  createFlatBindGroup,
423
442
  createFlatBindGroupLayout,
424
- flatFragmentBindings,
443
+ flatFragmentScratch,
425
444
  flatVariant,
426
445
  flatVertexBindings,
427
446
  type FlatVariant,
@@ -429,7 +448,15 @@ import {
429
448
  flatPipelineAsync,
430
449
  probeStageCeilings,
431
450
  } from './flatPass.ts';
432
- import type { OitTarget } from './flatPass.ts';
451
+ import type { AnimationViews, FlatFragmentScratch, OitTarget } from './flatPass.ts';
452
+ import { GpuBoneAnimation } from './boneAnimations.ts';
453
+ import { packBoneAnimation } from '../../boneAnimation.ts';
454
+ import type { BoneAnimationClip, BoneAnimationHandle } from '../../boneAnimation.ts';
455
+ import { MaterialStore, hashWords } from './materialStore.ts';
456
+ import { GpuStaticDraws, encodeBundle, takeRecorded } from './staticBundles.ts';
457
+ import type { StaticRecording } from './staticBundles.ts';
458
+ import { captureStaticDraws, replayStaticDraws } from '../../staticDraws.ts';
459
+ import type { StaticDrawsHandle } from '../../staticDraws.ts';
433
460
  import {
434
461
  DEPTH_FRAG_BINDING,
435
462
  DEPTH_FRAG_SIZE,
@@ -437,6 +464,8 @@ import {
437
464
  DEPTH_PREVIOUS_BINDING,
438
465
  DEPTH_CUTOUT_VERT_FIELDS,
439
466
  DEPTH_CUTOUT_VERT_SIZE,
467
+ DEPTH_INSTANCED_ANIMATED_CUTOUT_VERT_FIELDS,
468
+ DEPTH_INSTANCED_ANIMATED_VERT_FIELDS,
440
469
  DEPTH_INSTANCED_CUTOUT_VERT_FIELDS,
441
470
  DEPTH_VERT_FIELDS,
442
471
  DEPTH_VERT_SIZE,
@@ -909,6 +938,14 @@ const MAX_WATER_BODIES_PER_FRAME = 16;
909
938
  */
910
939
  const MAX_BATCHES_PER_FRAME = 8;
911
940
 
941
+ /**
942
+ * Mesh passes a frame may open before the ring grows: the frame's own, a mirror's, a capture's, and
943
+ * a probe face's, which gives its slot back once submitted. A frame past it loses the draws of the
944
+ * passes that found no slot, counted on the `materials` line, and the ring is grown for the next.
945
+ * Each slot is the pass's block, 3,328 to 6,656 bytes by variant, so the ring is 213 to 426 KB.
946
+ */
947
+ const PASSES_PER_FRAME = 64;
948
+
912
949
  /** Buffer usage flags, from the specification. See `buffers.ts` for why they are not read off the global. */
913
950
  const USAGE_UNIFORM_DST = 0x0040 | 0x0008;
914
951
 
@@ -1423,6 +1460,15 @@ interface SunTintTarget {
1423
1460
  readonly mips: readonly [SnapshotMips, SnapshotMips];
1424
1461
  }
1425
1462
 
1463
+ /** A skinned or morphed flat group, and what it was built with, compared by identity. */
1464
+ interface SkinnedTwin {
1465
+ readonly group: GPUBindGroup;
1466
+ readonly palette: GPUTextureView | null;
1467
+ readonly deltas: GPUTextureView | null;
1468
+ readonly cloth: GPUTextureView | null;
1469
+ readonly clothBinding: GPUTextureView | null;
1470
+ }
1471
+
1426
1472
  export class WebGPURenderer implements RendererApi {
1427
1473
  /**
1428
1474
  * Real timestamp queries, where the adapter offers them.
@@ -1490,7 +1536,21 @@ export class WebGPURenderer implements RendererApi {
1490
1536
  * flight at the time, so a consumer that builds more meshes later can wait again.
1491
1537
  */
1492
1538
  async ready(): Promise<void> {
1493
- await Promise.all(this.pipelineTargets.map((cache) => cache.ready()));
1539
+ await Promise.all(this.pipelineCaches().map((cache) => cache.ready()));
1540
+ }
1541
+
1542
+ /** Every distinct cache a lit draw reaches: the frame's, the canvas's, the late pass's, skin's. */
1543
+ private pipelineCaches(): PipelineCache[] {
1544
+ const caches = [...this.pipelineTargets];
1545
+ for (const cache of [this.latePipelines, this.skinPipelines]) {
1546
+ if (cache !== null && !caches.includes(cache)) caches.push(cache);
1547
+ }
1548
+ return caches;
1549
+ }
1550
+
1551
+ /** Whether a lit draw whose pipeline is not built is left out while it compiles. */
1552
+ private skipCompiling(skips: boolean): void {
1553
+ for (const cache of this.pipelineCaches()) cache.skipsCompiling = skips;
1494
1554
  }
1495
1555
 
1496
1556
  private presentedFrameCount = 0;
@@ -1506,6 +1566,8 @@ export class WebGPURenderer implements RendererApi {
1506
1566
  private readonly skinnedBindGroupLayout: GPUBindGroupLayout;
1507
1567
  private readonly morphedBindGroupLayout: GPUBindGroupLayout;
1508
1568
  private readonly bothBindGroupLayout: GPUBindGroupLayout;
1569
+ /** The instanced layout of a batch playing a bone animation. See `animatedGroup`. */
1570
+ private readonly animatedBindGroupLayout: GPUBindGroupLayout;
1509
1571
  /** The morph weights the following draws use, or null for none. */
1510
1572
  private morphWeights: Float32Array | null = null;
1511
1573
  /**
@@ -1518,14 +1580,15 @@ export class WebGPURenderer implements RendererApi {
1518
1580
  * group per character per frame — an allocation in the frame loop, which `AGENTS.md` forbids.
1519
1581
  * Keyed by slot instead, the groups are built once and reused for the life of the renderer.
1520
1582
  */
1521
- private readonly skinnedGroups: (GPUBindGroup | null)[] = [];
1522
- /** The unskinned group each was built beside, for identity-based invalidation. */
1523
- private readonly skinnedGroupsBeside: (GPUBindGroup | null)[] = [];
1524
- private readonly skinnedGroupsPalette: (GPUTextureView | null)[] = [];
1525
- private readonly skinnedGroupsDeltas: (GPUTextureView | null)[] = [];
1526
- /** The cloth views each skinned twin was built with, compared by identity like the palette. */
1527
- private readonly skinnedGroupsCloth: (GPUTextureView | null)[] = [];
1528
- private readonly skinnedGroupsClothBinding: (GPUTextureView | null)[] = [];
1583
+ /*
1584
+ * **And by material within a slot**, keyed by the unskinned group each was built beside. One
1585
+ * group a slot was rebuilt whenever the material beside it changed, so a character drawn in
1586
+ * thirty-odd sections, each its own material, built one a section a frame — about ninety a frame
1587
+ * for two characters, reported by a consumer. A slot's map holds one entry a material its rigs
1588
+ * are drawn in, and is emptied wherever the unskinned groups are, whose identities it is keyed
1589
+ * by.
1590
+ */
1591
+ private readonly skinnedGroups: (Map<GPUBindGroup, SkinnedTwin> | undefined)[] = [];
1529
1592
  /** The cloth the next skinned draws are placed by, or null. See `setCloth`. */
1530
1593
  private cloth: { binding: GpuClothBinding; particles: GpuClothParticles } | null = null;
1531
1594
  private clothStandCache: { texture: GPUTexture; views: ClothViews } | null = null;
@@ -1595,22 +1658,51 @@ export class WebGPURenderer implements RendererApi {
1595
1658
  /** Every distinct cache above, so a mesh's pipelines are built for each. One entry when aliased. */
1596
1659
  private readonly pipelineTargets: readonly PipelineCache[];
1597
1660
  private readonly bindGroupLayout: GPUBindGroupLayout;
1598
- /** One slot per draw, uploaded once a frame. See `UniformRing`. */
1599
- private readonly perDraw: UniformRing;
1600
- /** The lights, fog and camera, settled once per frame by `bindMeshPass`. */
1601
- private readonly perFrame: UniformRing;
1661
+ /** One slot per draw, uploaded once a frame. See `UniformRing`. Swapped while a static list records. */
1662
+ private perDraw: UniformRing;
1663
+ /**
1664
+ * The lights, fog and camera, a slot per mesh pass a frame opens: `bindMeshPass` writes them, and
1665
+ * the first draw after it takes a slot. A ring rather than one buffer because a frame opens more
1666
+ * than one pass — a mirror's, a capture's, a probe face's — and a queue write does not interleave
1667
+ * with the draws recorded between them. See `UniformRing`.
1668
+ */
1669
+ private passRing: UniformRing;
1670
+ /** Each material's block, kept across frames for as long as it does not change. */
1671
+ private readonly materialStore: MaterialStore;
1602
1672
  /**
1603
- * The material block as the CPU holds it, patched by the setters and copied into a slot.
1673
+ * Both blocks as the CPU holds them, the pass's then the material's, patched by name and copied
1674
+ * into a slot by the next draw after something changed them. See `FlatFragmentScratch`.
1604
1675
  *
1605
1676
  * Authoritative between draws: a setter changes one field here and marks the block dirty, and
1606
- * the next `drawMesh` takes a slot and copies the whole thing in. That keeps a setter free of
1607
- * any knowledge of slots, which is what lets it be called anywhere a scene wants.
1677
+ * the next `drawMesh` places it. That keeps a setter free of any knowledge of slots, which is
1678
+ * what lets it be called anywhere a scene wants.
1608
1679
  */
1609
1680
  private readonly perFrameStaging: ArrayBuffer;
1610
1681
  private readonly perFrameFloats: Float32Array;
1611
1682
  private readonly perFrameInts: Int32Array;
1683
+ /** The two halves of the scratch, as the words a slot is written from. */
1684
+ private readonly passInts: Int32Array;
1685
+ private readonly materialInts: Int32Array;
1612
1686
  /** The slot the open material occupies, and whether the next draw must take a new one. */
1613
1687
  private readonly materials = new MaterialChanges();
1688
+ /** The same for the pass's block: reopened by `bindMeshPass` and by nothing a material sets. */
1689
+ private readonly passBlock = new MaterialChanges();
1690
+ /**
1691
+ * The vertex stage's view block — the camera, the light's matrix, the wind — a slot a pass, and a
1692
+ * second, unjittered, for a blended draw landing after the upscale. Reopened with the pass, and by
1693
+ * `setWind` and `beginShadowPass`, which change what it holds.
1694
+ */
1695
+ private viewRing: UniformRing;
1696
+ private readonly viewBlock = new MaterialChanges();
1697
+ private readonly lateViewBlock = new MaterialChanges();
1698
+ /** The view slot the draw being submitted bound, read by the halves it keeps for later passes. */
1699
+ private drawViewOffset = 0;
1700
+ /** Pass slots asked for and refused this frame, and materials refused: what grows the two. */
1701
+ private passesAsked = 0;
1702
+ private passesRefused = 0;
1703
+ private viewsAsked = 0;
1704
+ private viewsRefused = 0;
1705
+ private materialsRefused = 0;
1614
1706
 
1615
1707
  /*
1616
1708
  * The frame graph's recording, and the machinery that replays it.
@@ -1879,8 +1971,8 @@ export class WebGPURenderer implements RendererApi {
1879
1971
  private readonly variant: FlatVariant;
1880
1972
  /** Whether this profile compiled the point-shadow path, and so has sentinels to write. */
1881
1973
  private readonly pointShadowsCompiled: boolean;
1882
- /** What the generator recorded about that permutation: sizes, offsets and bindings. */
1883
- private readonly fragment: ReturnType<typeof flatFragmentBindings>;
1974
+ /** What the generator recorded about that permutation, as the renderer's scratch lays it out. */
1975
+ private readonly fragment: FlatFragmentScratch;
1884
1976
 
1885
1977
  private depth: GPUTexture | null = null;
1886
1978
  private depthView: GPUTextureView | null = null;
@@ -2624,7 +2716,7 @@ export class WebGPURenderer implements RendererApi {
2624
2716
  if (this.quality.frameGraph) recordNode(this.arena, VERB_SCOPE, 0, MIRROR_TARGET, 0, 0);
2625
2717
  this.pass?.end();
2626
2718
  this.flushRings();
2627
- this.surface.device.queue.submit([this.encoder.finish()]);
2719
+ this.submit([this.encoder.finish()]);
2628
2720
  this.encoder = this.surface.device.createCommandEncoder({ label: 'reflection' });
2629
2721
  /*
2630
2722
  * **Opened on demand, like the frame's own.** Nothing has been recorded into the mirror yet,
@@ -2684,7 +2776,8 @@ export class WebGPURenderer implements RendererApi {
2684
2776
  this.windStreakFrags,
2685
2777
  this.scatterVerts,
2686
2778
  this.scatterFrags,
2687
- this.perFrame,
2779
+ this.passRing,
2780
+ this.viewRing,
2688
2781
  this.filmVerts,
2689
2782
  this.filmFrags,
2690
2783
  this.insetUniforms,
@@ -2705,6 +2798,7 @@ export class WebGPURenderer implements RendererApi {
2705
2798
  /* Culls first: a submit that follows may carry a draw reading what they write. */
2706
2799
  this.instanceCulls?.flush();
2707
2800
  for (const ring of this.rings()) ring.flush();
2801
+ this.materialStore.flush();
2708
2802
  }
2709
2803
 
2710
2804
  /** Where every ring stands before work that submits an encoder of its own. */
@@ -2724,6 +2818,8 @@ export class WebGPURenderer implements RendererApi {
2724
2818
  private rewindRings(skinMark: number): void {
2725
2819
  const rings = this.rings();
2726
2820
  for (let i = 0; i < rings.length; i++) (rings[i] as UniformRing).rewind(this.ringMarks[i] ?? 0);
2821
+ /* The open pass may sit in a slot just given back, which the next pass would overwrite. */
2822
+ this.reopenPass();
2727
2823
  this.skinPalettes.rewind(skinMark);
2728
2824
  if (this.skinPaletteSlot >= skinMark) this.skinPaletteSlot = -1;
2729
2825
  }
@@ -2851,6 +2947,7 @@ export class WebGPURenderer implements RendererApi {
2851
2947
  }
2852
2948
  filmCommand.vertexCount = geometry.vertexBuffers.length;
2853
2949
  filmCommand.indexBuffer = geometry.indexBuffer;
2950
+ filmCommand.indexFormat = geometry.indexFormat;
2854
2951
  filmCommand.indexed = true;
2855
2952
  filmCommand.count = geometry.indexCount;
2856
2953
  } else {
@@ -2861,7 +2958,7 @@ export class WebGPURenderer implements RendererApi {
2861
2958
  for (let index = 0; index < geometry.vertexBuffers.length; index++) {
2862
2959
  pass.setVertexBuffer(index, geometry.vertexBuffers[index] as GPUBuffer);
2863
2960
  }
2864
- pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
2961
+ pass.setIndexBuffer(geometry.indexBuffer, geometry.indexFormat);
2865
2962
  pass.drawIndexed(geometry.indexCount);
2866
2963
  }
2867
2964
  }
@@ -2898,6 +2995,11 @@ export class WebGPURenderer implements RendererApi {
2898
2995
  private modelMap: GpuSurfaceTexture | null = null;
2899
2996
  /** The material's shading model, which picks the lit pipeline: null the standard. */
2900
2997
  private materialModel: SurfaceModel | null = null;
2998
+ /** `uWorldUv`, the material's projection packed by `packSurfaceProjection`. */
2999
+ private readonly projection = new Float32Array(PROJECTION_FLOATS);
3000
+ /** `uLayers`, the material's layers packed by `packSurfaceLayers`. */
3001
+ private readonly layerFloats = new Float32Array(LAYER_FLOATS);
3002
+ private warnedLayersModelMap = false;
2901
3003
  /** Its numbers, `uModelParams`, packed by `packModel`. */
2902
3004
  private readonly modelParams = new Float32Array(MODEL_PARAM_FLOATS);
2903
3005
  /**
@@ -2926,7 +3028,7 @@ export class WebGPURenderer implements RendererApi {
2926
3028
  * ones that move something *every* group holds: the shadow cubemaps, the probe fence, the cookie
2927
3029
  * atlas and the IES rows. None of those is a frame path.
2928
3030
  */
2929
- private readonly flatBindGroups = new Map<
3031
+ private flatBindGroups = new Map<
2930
3032
  GpuSurfaceTexture | null,
2931
3033
  Map<
2932
3034
  GpuSurfaceTexture | null,
@@ -3097,7 +3199,7 @@ export class WebGPURenderer implements RendererApi {
3097
3199
  pass.end();
3098
3200
  this.pass = null;
3099
3201
  this.flushRings();
3100
- device.queue.submit([encoder.finish()]);
3202
+ this.submit([encoder.finish()]);
3101
3203
  this.rewindRings(skinMark);
3102
3204
  this.encoderEpoch = frameEpoch;
3103
3205
  this.encoder = savedEncoder;
@@ -3105,6 +3207,7 @@ export class WebGPURenderer implements RendererApi {
3105
3207
  this.perFrameInts[this.materialField('uOutputTransform')] = heldGrade;
3106
3208
  this.perFrameFloats[this.materialField('uOutputExposure')] = heldExposure;
3107
3209
  this.materials.dirty();
3210
+ this.reopenPass();
3108
3211
  this.capturePassActive = false;
3109
3212
  }
3110
3213
  return true;
@@ -3119,11 +3222,15 @@ export class WebGPURenderer implements RendererApi {
3119
3222
  */
3120
3223
  private readonly retiredTextures: GPUTexture[] = [];
3121
3224
 
3122
- updateSurfaceTexture(texture: GpuSurfaceTexture, source: TexImageSource): void {
3225
+ updateSurfaceTexture(
3226
+ texture: GpuSurfaceTexture,
3227
+ source: TexImageSource | CompressedTextureSource,
3228
+ ): void {
3123
3229
  if (this.surface.lost) return;
3124
- const replaced = texture.update(source);
3230
+ const replaced = texture.update(source, this.compressedFormats);
3125
3231
  if (replaced === null) return;
3126
- /* A new size is a new view, so every group holding the old one is dropped — see below. */
3232
+ /* A new size or format is a new view, so every group holding the old one is dropped — see
3233
+ below. */
3127
3234
  this.retiredTextures.push(replaced);
3128
3235
  this.forgetBindingsOf(texture);
3129
3236
  }
@@ -3175,7 +3282,9 @@ export class WebGPURenderer implements RendererApi {
3175
3282
  /* Same reasoning, for whichever SDF labels were bound against this atlas. */
3176
3283
  this.sdfTextBindGroups.delete(texture);
3177
3284
  this.flatBindGroups.clear();
3285
+ this.flatGroupEpoch += 1;
3178
3286
  this.overlayGroups.clear();
3287
+ this.skinnedGroups.length = 0;
3179
3288
  /*
3180
3289
  * The blank one is built with the albedo held aside, because `buildFlatBindGroup` reads
3181
3290
  * `this.albedo` for that slot — leaving it in place would put a real colour map into the
@@ -3365,11 +3474,21 @@ export class WebGPURenderer implements RendererApi {
3365
3474
  const normal = (material?.normal ?? null) as GpuSurfaceTexture | null;
3366
3475
  const orm = (material?.orm ?? null) as GpuSurfaceTexture | null;
3367
3476
  const emissiveMap = (material?.emissive ?? null) as GpuSurfaceTexture | null;
3368
- const modelMap = (material?.modelMap ?? null) as GpuSurfaceTexture | null;
3477
+ const ownModelMap = (material?.modelMap ?? null) as GpuSurfaceTexture | null;
3478
+ /* A layered material's mask goes where a model's map would: see `SurfaceMaterial.layers`. */
3479
+ const layers = this.layersOf(material);
3480
+ const modelMap = ownModelMap ?? ((layers?.mask ?? null) as GpuSurfaceTexture | null);
3369
3481
  this.materialModel = material?.model ?? null;
3482
+ const reflectivity = material?.reflectivity;
3483
+ const environmentGain = material?.environmentGain;
3484
+ this.materialReflectivity =
3485
+ reflectivity === undefined ? null : Math.min(1, Math.max(0, reflectivity));
3486
+ this.materialEnvironmentGain =
3487
+ environmentGain === undefined ? null : Math.max(0, environmentGain);
3488
+ this.writeEnvironmentDials();
3370
3489
  /* What the blur spreads this skin by: the last material to name a profile says. */
3371
3490
  const model = material?.model;
3372
- warnUnpagedLightmap(model, modelMap);
3491
+ warnUnpagedLightmap(model, ownModelMap);
3373
3492
  if (model?.kind === 'skin' && this.skinScatter !== null) {
3374
3493
  this.skinScatter.setProfile(
3375
3494
  model.profile,
@@ -3395,7 +3514,8 @@ export class WebGPURenderer implements RendererApi {
3395
3514
  const cut = this.materialField('uCutout');
3396
3515
  f[cut] = cutoff;
3397
3516
  f[cut + 1] = CUTOUT_RESOLVE_CODE[this.cutoutResolveStaged];
3398
- f[cut + 2] = this.cutoutFrame;
3517
+ /* z unused: the dither's frame is the pass's, in `uHighlightMin.w`. See `uCutout`. */
3518
+ f[cut + 2] = 0;
3399
3519
  /* The material's lighting channels, which the lamps' are compared with. See `uCutout`. */
3400
3520
  f[cut + 3] = lightChannelsOf(material?.lightChannels);
3401
3521
  i[flags + 3] = material?.doubleSided === true ? 1 : 0;
@@ -3427,12 +3547,23 @@ export class WebGPURenderer implements RendererApi {
3427
3547
  /* The model's numbers and whether its map is bound. See `packModel`. */
3428
3548
  packModel(
3429
3549
  material?.model ?? null,
3430
- modelMap !== null,
3550
+ ownModelMap !== null,
3431
3551
  this.modelParams,
3432
3552
  material?.physicalSpecular === true,
3433
3553
  );
3434
3554
  /* The first physical highlight asked for is when the lit pipelines start carrying one. */
3435
3555
  if (material?.physicalSpecular === true) this.enableLit('PHYSICAL_SPECULAR');
3556
+ /* Maps placed by the world: the first material that asks is when lit pipelines carry it. */
3557
+ packSurfaceProjection(material?.projection, this.projection);
3558
+ f.set(this.projection, this.materialField('uWorldUv'));
3559
+ /* How much light from behind it lets through, in the glass tint's spare lane. */
3560
+ f[this.materialField('uGlassTint') + 3] =
3561
+ Math.min(Math.max(material?.diffuseTransmission ?? 0, 0), 1) || 0;
3562
+ if ((this.projection[0] as number) > 0) this.enableLit('WORLD_UVS');
3563
+ /* And its layers, the first time a material blends any: layered.ts. */
3564
+ packSurfaceLayers(layers, this.layerFloats);
3565
+ f.set(this.layerFloats, this.materialField('uLayers'));
3566
+ if (layers !== null) this.enableLit('LAYERED');
3436
3567
  f.set(this.modelParams, this.materialField('uModelParams'));
3437
3568
  });
3438
3569
  /*
@@ -3585,15 +3716,31 @@ export class WebGPURenderer implements RendererApi {
3585
3716
 
3586
3717
  /** How reflective the surfaces drawn next are. Clamped, as `renderer.ts` clamps it. */
3587
3718
  setSurfaceReflectivity(amount: number): void {
3588
- this.material((f) => {
3589
- f[this.materialField('uEnvironmentDials') + 1] = Math.min(1, Math.max(0, amount));
3590
- });
3719
+ this.passReflectivity = Math.min(1, Math.max(0, amount));
3720
+ this.writeEnvironmentDials();
3591
3721
  }
3592
3722
 
3593
3723
  /** How bright the environment the surfaces drawn next reflect is. See `renderer.ts`. */
3594
3724
  setEnvironmentGain(gain: number): void {
3725
+ this.passEnvironmentGain = Math.max(0, gain);
3726
+ this.writeEnvironmentDials();
3727
+ }
3728
+
3729
+ /**
3730
+ * The reflectivity and environment gain the setters gave the pass, and the ones the material
3731
+ * set now states, or null where it states none (`SurfaceMaterial.reflectivity`). A draw wears the
3732
+ * material's where it has one and the pass's otherwise, the rule `renderer.ts` keeps too.
3733
+ */
3734
+ private passReflectivity = 0;
3735
+ private passEnvironmentGain = 1;
3736
+ private materialReflectivity: number | null = null;
3737
+ private materialEnvironmentGain: number | null = null;
3738
+
3739
+ private writeEnvironmentDials(): void {
3595
3740
  this.material((f) => {
3596
- f[this.materialField('uEnvironmentDials') + 2] = Math.max(0, gain);
3741
+ const dials = this.materialField('uEnvironmentDials');
3742
+ f[dials + 1] = this.materialReflectivity ?? this.passReflectivity;
3743
+ f[dials + 2] = this.materialEnvironmentGain ?? this.passEnvironmentGain;
3597
3744
  });
3598
3745
  }
3599
3746
 
@@ -3624,6 +3771,14 @@ export class WebGPURenderer implements RendererApi {
3624
3771
  this.materials.dirty();
3625
3772
  }
3626
3773
 
3774
+ /** Whether the draws that follow spend their own opacity on that screen door. See `renderer.ts`. */
3775
+ setDitherOpacity(on: boolean): void {
3776
+ if (this.surface.lost) return;
3777
+ /* The third lane of `uWriteMode`, beside the fade. */
3778
+ this.perFrameFloats[this.materialField('uWriteMode') + 2] = on ? 1 : 0;
3779
+ this.materials.dirty();
3780
+ }
3781
+
3627
3782
  /**
3628
3783
  * How strong the microscopic relief on the surfaces drawn next is, and how coarse.
3629
3784
  *
@@ -4112,7 +4267,7 @@ export class WebGPURenderer implements RendererApi {
4112
4267
  }
4113
4268
  this.pass?.end();
4114
4269
  this.flushRings();
4115
- this.surface.device.queue.submit([this.encoder.finish()]);
4270
+ this.submit([this.encoder.finish()]);
4116
4271
  this.encoder = this.surface.device.createCommandEncoder({ label: 'frame.resumed' });
4117
4272
  this.reflectionPassActive = false;
4118
4273
  this.reflectionReady = true;
@@ -5121,6 +5276,11 @@ export class WebGPURenderer implements RendererApi {
5121
5276
  at('uLightViewProj'),
5122
5277
  env === undefined ? NO_LIGHT_MATRIX : (env.lightViewProj as Float32Array),
5123
5278
  );
5279
+ ring.writeFloats(
5280
+ fragmentSlot,
5281
+ at('uMovingLightViewProj'),
5282
+ env === undefined ? NO_LIGHT_MATRIX : (this.movingLightFor(env) as Float32Array),
5283
+ );
5124
5284
  ring.writeFloat(fragmentSlot, at('uShadowMapSize'), this.quality.directionalShadowMapSize);
5125
5285
  /* Sampled only where a pass filled it — see `peelFilled` for why existing is not enough. */
5126
5286
  ring.writeInt(fragmentSlot, at('uPeeledShadowEnabled'), this.peelFilled ? 1 : 0);
@@ -5160,6 +5320,7 @@ export class WebGPURenderer implements RendererApi {
5160
5320
  }
5161
5321
  volumeCommand.vertexCount = mesh.vertexBuffers.length;
5162
5322
  volumeCommand.indexBuffer = mesh.indexBuffer;
5323
+ volumeCommand.indexFormat = mesh.indexFormat;
5163
5324
  volumeCommand.indexed = true;
5164
5325
  volumeCommand.count = mesh.indexCount;
5165
5326
  } else {
@@ -5168,7 +5329,7 @@ export class WebGPURenderer implements RendererApi {
5168
5329
  for (let index = 0; index < mesh.vertexBuffers.length; index++) {
5169
5330
  pass.setVertexBuffer(index, mesh.vertexBuffers[index] as GPUBuffer);
5170
5331
  }
5171
- pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
5332
+ pass.setIndexBuffer(mesh.indexBuffer, mesh.indexFormat);
5172
5333
  pass.drawIndexed(mesh.indexCount);
5173
5334
  }
5174
5335
  }
@@ -5795,6 +5956,7 @@ export class WebGPURenderer implements RendererApi {
5795
5956
  */
5796
5957
  setWind(windX: number, windZ: number, windGust: number, timeSeconds: number): void {
5797
5958
  resolveScatterDeform(windX, windZ, windGust, timeSeconds, null, this.frameWind);
5959
+ this.reopenViews();
5798
5960
  }
5799
5961
 
5800
5962
  /**
@@ -5814,22 +5976,6 @@ export class WebGPURenderer implements RendererApi {
5814
5976
  this.shadowDraws.writeFloats(slot, DEPTH_VERT_FIELDS.uWindSpatialPhase.offset, d.spatialPhase);
5815
5977
  }
5816
5978
 
5817
- /**
5818
- * The five wind fields of one per-draw slot.
5819
- *
5820
- * **Never called for an instanced draw.** That variant declares none of these, and
5821
- * `flatPass.ts` records what writing a field a variant does not declare costs: the block is
5822
- * shorter, so the write lands on whatever occupies that offset instead.
5823
- */
5824
- private writeWind(slot: number): void {
5825
- const d = this.frameWind;
5826
- this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uWindDirection.offset, d.direction);
5827
- this.perDraw.writeFloat(slot, FLAT_VERT_FIELDS.uWindSpeed.offset, d.bend);
5828
- this.perDraw.writeFloat(slot, FLAT_VERT_FIELDS.uWindGust.offset, d.gust);
5829
- this.perDraw.writeFloat(slot, FLAT_VERT_FIELDS.uWindTime.offset, d.time);
5830
- this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uWindSpatialPhase.offset, d.spatialPhase);
5831
- }
5832
-
5833
5979
  /** Refilled per frame rather than allocated; see `resolvePointLights`. */
5834
5980
  private readonly lights: ResolvedPointLights = {
5835
5981
  count: 0,
@@ -5984,6 +6130,9 @@ export class WebGPURenderer implements RendererApi {
5984
6130
  * at submit, and a texture's view is new when an update changes its size.
5985
6131
  */
5986
6132
  private readonly shadowCutoutLayout: GPUBindGroupLayout;
6133
+ /** A crowd's casters: the plain and cutout layouts with a bone animation's three textures. */
6134
+ private readonly shadowAnimatedLayout: GPUBindGroupLayout;
6135
+ private readonly shadowAnimatedCutoutLayout: GPUBindGroupLayout;
5987
6136
  private readonly cutoutGroups = new Map<GpuSurfaceTexture, GPUBindGroup>();
5988
6137
  private readonly cutoutPeelGroups = new Map<GpuSurfaceTexture, GPUBindGroup>();
5989
6138
  private shadowPeelPassUniforms: GPUBuffer | null = null;
@@ -6021,6 +6170,22 @@ export class WebGPURenderer implements RendererApi {
6021
6170
  * the texel-for-texel comparison that settled it.
6022
6171
  */
6023
6172
  private readonly correctedLightViewProj = new Float32Array(16);
6173
+ /**
6174
+ * What a receiver projects through to read the sun's static layers: the environment's
6175
+ * `lightViewProj`, taken at `bindMeshPass` as WebGL2 takes it there.
6176
+ *
6177
+ * **It was the last shadow pass's matrix**, which is the environment's wherever both layers are
6178
+ * drawn with one and the moving layer's wherever they are not: the static layer was then read
6179
+ * through the moving layer's tight square, and a distant pillar lost its shadow on WebGPU and kept
6180
+ * it on WebGL2, measured on demo/dev/sunLayers.html?moving=tight.
6181
+ */
6182
+ private readonly receiverLightViewProj = new Float32Array(16);
6183
+ /**
6184
+ * The matrix the sun's dynamic shadow pass was last drawn with, uncorrected as the lit stage
6185
+ * projects with, and whether one has been. See `movingLightFor`.
6186
+ */
6187
+ private readonly movingLightViewProj = new Float32Array(16);
6188
+ private movingLightKnown = false;
6024
6189
  /**
6025
6190
  * The same matrix *uncorrected*, which is what the flat shader must project with.
6026
6191
  *
@@ -6151,6 +6316,48 @@ export class WebGPURenderer implements RendererApi {
6151
6316
  this.enableLit('GLASS_SHADOWS');
6152
6317
  }
6153
6318
 
6319
+ /**
6320
+ * A material's layers where they can be drawn: none asked, or a model's map in the mask's place,
6321
+ * said once, leave one layer.
6322
+ */
6323
+ private layersOf(
6324
+ material: SurfaceMaterial<GpuSurfaceTexture> | null,
6325
+ ): SurfaceLayers<GpuSurfaceTexture> | null {
6326
+ const layers = material?.layers ?? null;
6327
+ if (layers === null || layers.repeats.length === 0) return null;
6328
+ if ((material?.modelMap ?? null) === null) return layers;
6329
+ if (!this.warnedLayersModelMap) {
6330
+ this.warnedLayersModelMap = true;
6331
+ console.warn(
6332
+ "WebGPU: a material's layers are drawn as one: its mask is read where a model's map goes, " +
6333
+ 'and this material carries a `modelMap` too.',
6334
+ );
6335
+ }
6336
+ return null;
6337
+ }
6338
+
6339
+ /**
6340
+ * What the sun's moving layer is read through: the matrix its shadow pass was last drawn with,
6341
+ * where the lit pipelines carry one (`MOVING_SUN`); the environment's otherwise, which every caller
6342
+ * drawing both layers with one matrix hands it anyway.
6343
+ */
6344
+ private movingLightFor(env: Environment): ReadonlyMat4 {
6345
+ return this.movingLightKnown && this.pipelines.litSwitches.MOVING_SUN
6346
+ ? this.movingLightViewProj
6347
+ : env.lightViewProj;
6348
+ }
6349
+
6350
+ /** The moving layer's own matrix on, the first time a frame draws that layer with one. */
6351
+ private enableMovingSun(env: Environment): void {
6352
+ if (this.pipelines.litSwitches.MOVING_SUN || !this.movingLightKnown) return;
6353
+ for (let k = 0; k < 16; k++) {
6354
+ if (this.movingLightViewProj[k] !== env.lightViewProj[k]) {
6355
+ this.enableLit('MOVING_SUN');
6356
+ return;
6357
+ }
6358
+ }
6359
+ }
6360
+
6154
6361
  /** One of the lit stage's switches on, in every cache a lit pipeline can come from. */
6155
6362
  private enableLit(feature: LitSwitch): void {
6156
6363
  if (this.pipelines.litSwitches[feature]) return;
@@ -6216,7 +6423,7 @@ export class WebGPURenderer implements RendererApi {
6216
6423
  for (let index = 0; index < geometry.vertexBuffers.length; index++) {
6217
6424
  pass.setVertexBuffer(index, geometry.vertexBuffers[index] as GPUBuffer);
6218
6425
  }
6219
- pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
6426
+ pass.setIndexBuffer(geometry.indexBuffer, geometry.indexFormat);
6220
6427
  pass.drawIndexed(geometry.indexCount);
6221
6428
  },
6222
6429
  /**
@@ -6239,8 +6446,18 @@ export class WebGPURenderer implements RendererApi {
6239
6446
  const count = Math.min(data.count, gpuBatch.capacity);
6240
6447
  if (count === 0) return;
6241
6448
  const cutout = cutoutOf(material);
6449
+ /* A crowd casts where its clip has moved it, through the animated variant. */
6450
+ const animation = gpuBatch.animation === null ? null : this.animationViews(gpuBatch);
6451
+ if (gpuBatch.animation !== null && animation === null) return;
6452
+ const cutoutTexture =
6453
+ cutout === null ? null : (cutout.albedo as unknown as GpuSurfaceTexture);
6242
6454
  const cutoutGroup =
6243
- cutout === null ? null : this.cutoutGroup(cutout.albedo as unknown as GpuSurfaceTexture);
6455
+ animation !== null
6456
+ ? this.animatedDepthGroup(gpuBatch, animation, cutoutTexture)
6457
+ : cutoutTexture === null
6458
+ ? null
6459
+ : this.cutoutGroup(cutoutTexture);
6460
+ if (animation !== null && cutoutGroup === null) return;
6244
6461
 
6245
6462
  const slot = this.shadowDraws.allocate();
6246
6463
  if (slot === null) return;
@@ -6251,23 +6468,43 @@ export class WebGPURenderer implements RendererApi {
6251
6468
  DEPTH_VERT_FIELDS.uLightViewProj.offset,
6252
6469
  this.correctedLightViewProj,
6253
6470
  );
6254
- if (cutout !== null && cutoutGroup !== null) {
6471
+ const cuts = cutoutTexture !== null && cutoutGroup !== null;
6472
+ if (cutout !== null && cuts) {
6255
6473
  this.writeCutout(slot, cutout, DEPTH_INSTANCED_CUTOUT_VERT_FIELDS);
6256
6474
  }
6475
+ const clip = gpuBatch.animation;
6476
+ if (animation !== null && clip !== null) {
6477
+ const fields = cuts
6478
+ ? DEPTH_INSTANCED_ANIMATED_CUTOUT_VERT_FIELDS
6479
+ : DEPTH_INSTANCED_ANIMATED_VERT_FIELDS;
6480
+ this.shadowDraws.writeFloat(slot, fields.uSceneTime.offset, this.animationTime);
6481
+ this.shadowDraws.writeFloat(slot, fields.uBoneClip.offset, clip.framesPerSecond);
6482
+ this.shadowDraws.writeFloat(slot, fields.uBoneClip.offset + 4, clip.boneScale);
6483
+ }
6257
6484
 
6258
- const cuts = cutoutGroup !== null;
6259
6485
  const cull = material?.doubleSided === true ? 'none' : this.depthCullMode;
6486
+ const animated = animation !== null;
6260
6487
  pass.setPipeline(
6261
6488
  depthPipeline(
6262
6489
  this.pipelines,
6263
6490
  this.surface.device,
6264
- cuts ? this.shadowCutoutLayout : this.shadowLayout,
6265
- `${cuts ? 'depth-instanced-cutout' : 'depth-instanced'}|${cull}|${geometry.key ?? ''}`,
6491
+ animated
6492
+ ? cuts
6493
+ ? this.shadowAnimatedCutoutLayout
6494
+ : this.shadowAnimatedLayout
6495
+ : cuts
6496
+ ? this.shadowCutoutLayout
6497
+ : this.shadowLayout,
6498
+ `${cuts ? 'depth-instanced-cutout' : 'depth-instanced'}${animated ? '-animated' : ''}` +
6499
+ `|${cull}|${geometry.key ?? ''}`,
6266
6500
  this.presentOf(geometry),
6267
6501
  cull,
6268
6502
  false,
6269
6503
  true,
6270
6504
  cuts,
6505
+ false,
6506
+ false,
6507
+ animated,
6271
6508
  ),
6272
6509
  );
6273
6510
  pass.setBindGroup(0, cutoutGroup ?? this.shadowGroup(), [slot]);
@@ -6276,7 +6513,7 @@ export class WebGPURenderer implements RendererApi {
6276
6513
  pass.setVertexBuffer(index, geometry.vertexBuffers[index] as GPUBuffer);
6277
6514
  }
6278
6515
  pass.setVertexBuffer(index, gpuBatch.buffer);
6279
- pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
6516
+ pass.setIndexBuffer(geometry.indexBuffer, geometry.indexFormat);
6280
6517
  pass.drawIndexed(geometry.indexCount, count);
6281
6518
  },
6282
6519
  skinnedMesh: (mesh, model, palette, material, cloth) => {
@@ -6338,7 +6575,7 @@ export class WebGPURenderer implements RendererApi {
6338
6575
  for (let index = 0; index < geometry.vertexBuffers.length; index++) {
6339
6576
  pass.setVertexBuffer(index, geometry.vertexBuffers[index] as GPUBuffer);
6340
6577
  }
6341
- pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
6578
+ pass.setIndexBuffer(geometry.indexBuffer, geometry.indexFormat);
6342
6579
  pass.drawIndexed(geometry.indexCount);
6343
6580
  },
6344
6581
  /**
@@ -6761,6 +6998,8 @@ export class WebGPURenderer implements RendererApi {
6761
6998
  this.overlayPipelines === this.pipelines
6762
6999
  ? [this.pipelines]
6763
7000
  : [this.pipelines, this.overlayPipelines];
7001
+ /* A lit draw first needed inside a frame waits for its compile or is left out while it runs. */
7002
+ this.skipCompiling(quality.pipelineCompile === 'skip');
6764
7003
  this.variant = flatVariant({
6765
7004
  directionalShadows: quality.directionalShadows,
6766
7005
  /* No `bakeReflectionProbe` here; see the field. */
@@ -6781,7 +7020,7 @@ export class WebGPURenderer implements RendererApi {
6781
7020
  pointShadows: quality.pointShadows,
6782
7021
  });
6783
7022
  this.pointShadowsCompiled = quality.pointShadows;
6784
- this.fragment = flatFragmentBindings(this.variant);
7023
+ this.fragment = flatFragmentScratch(this.variant);
6785
7024
  this.bindGroupLayout = createFlatBindGroupLayout(device, this.variant);
6786
7025
  /*
6787
7026
  * The skinned twin, built eagerly because it is a descriptor rather than a compile — WebGPU
@@ -6791,6 +7030,14 @@ export class WebGPURenderer implements RendererApi {
6791
7030
  this.skinnedBindGroupLayout = createFlatBindGroupLayout(device, this.variant, true);
6792
7031
  this.morphedBindGroupLayout = createFlatBindGroupLayout(device, this.variant, false, true);
6793
7032
  this.bothBindGroupLayout = createFlatBindGroupLayout(device, this.variant, true, true);
7033
+ /* And the instanced twin that plays a bone animation: its clip's and clocks' textures. */
7034
+ this.animatedBindGroupLayout = createFlatBindGroupLayout(
7035
+ device,
7036
+ this.variant,
7037
+ false,
7038
+ false,
7039
+ true,
7040
+ );
6794
7041
  this.perDraw = new UniformRing(
6795
7042
  device,
6796
7043
  FLAT_VERT_SIZE,
@@ -6799,30 +7046,48 @@ export class WebGPURenderer implements RendererApi {
6799
7046
  'flat.vertRing',
6800
7047
  this.drawBudget,
6801
7048
  );
6802
- this.perFrameStaging = new ArrayBuffer(this.fragment.uniformSize);
7049
+ const { passSize, materialSize } = this.fragment;
7050
+ this.perFrameStaging = new ArrayBuffer(passSize + materialSize);
6803
7051
  this.perFrameFloats = new Float32Array(this.perFrameStaging);
6804
7052
  this.perFrameInts = new Int32Array(this.perFrameStaging);
7053
+ this.passInts = new Int32Array(this.perFrameStaging, 0, passSize / 4);
7054
+ this.materialInts = new Int32Array(this.perFrameStaging, passSize, materialSize / 4);
6805
7055
  /*
6806
- * **A ring, because the block holds material state and a material changes between draws.**
7056
+ * **Two blocks, because they change at two rates.** `setSurfaceTexture`, `setSurfaceGrain` and
7057
+ * their siblings are pass state on both backends — a material covers many draws — and on WebGL2
7058
+ * each is a `uniform1f` that lands immediately. Here they live in a block, and
7059
+ * `queue.writeBuffer` does not interleave with recorded commands, so one buffer rewritten between
7060
+ * two draws gives *both* the second value: every surface would wear the last material set, which
7061
+ * is bug 7's shape at pass scale. So each draw binds a slot.
6807
7062
  *
6808
- * `setSurfaceTexture`, `setSurfaceGrain` and their siblings are pass state on both backends
6809
- * — a material covers many draws, so the alternative is rebinding the same values twenty
6810
- * times to say the same thing. On WebGL2 each is a `uniform1f` that lands immediately. Here
6811
- * they live in this block, and `queue.writeBuffer` does not interleave with recorded
6812
- * commands, so one buffer rewritten between two draws gives *both* the second value. Every
6813
- * surface in a scene would wear the last material set, which is bug 7's shape at pass scale.
6814
- *
6815
- * A slot per *material change* rather than per draw: `drawMesh` reuses the open slot until
6816
- * something dirties it, so a scene drawing a hundred meshes of one material spends one.
7063
+ * It was one block until 4.10.0, lights and all, copied whole into a ring slot per material
7064
+ * change and uploaded every frame: a stage of four hundred materials sent 4.8 MB a frame to say
7065
+ * what it had said the frame before. Now the pass's block takes a ring slot per pass, and a
7066
+ * material's block a slot of the store, where an unchanged material stays from frame to frame.
6817
7067
  */
6818
- this.perFrame = new UniformRing(
7068
+ this.passRing = new UniformRing(
6819
7069
  device,
6820
- this.fragment.uniformSize,
7070
+ passSize,
7071
+ PASSES_PER_FRAME,
7072
+ USAGE_UNIFORM_DST,
7073
+ 'flat.passRing',
7074
+ );
7075
+ this.materialStore = new MaterialStore(
7076
+ device,
7077
+ materialSize,
6821
7078
  this.quality.materialChangesPerFrame,
6822
7079
  USAGE_UNIFORM_DST,
6823
- 'flat.fragRing',
7080
+ 'flat.materials',
6824
7081
  this.materialBudget,
6825
7082
  );
7083
+ /* Two a pass at most: the jittered view and the unjittered one. */
7084
+ this.viewRing = new UniformRing(
7085
+ device,
7086
+ FLAT_VIEW_SIZE,
7087
+ 2 * PASSES_PER_FRAME,
7088
+ USAGE_UNIFORM_DST,
7089
+ 'flat.viewRing',
7090
+ );
6826
7091
 
6827
7092
  /*
6828
7093
  * A one-pixel stand-in for the albedo binding. The albedo's flag is zero for untextured
@@ -6971,7 +7236,7 @@ export class WebGPURenderer implements RendererApi {
6971
7236
  })
6972
7237
  .end();
6973
7238
  }
6974
- device.queue.submit([clear.finish()]);
7239
+ this.submit([clear.finish()]);
6975
7240
  /*
6976
7241
  * `nearest` on a depth texture sampled as an ordinary float, matching what
6977
7242
  * `shadowMap.ts` asks WebGL2 for. A filtering sampler cannot be used on a depth format
@@ -7584,6 +7849,8 @@ export class WebGPURenderer implements RendererApi {
7584
7849
  this.shadowLayout = createDepthBindGroupLayout(device);
7585
7850
  this.shadowSkinnedLayout = createDepthBindGroupLayout(device, true);
7586
7851
  this.shadowCutoutLayout = createDepthBindGroupLayout(device, false, true);
7852
+ this.shadowAnimatedLayout = createDepthBindGroupLayout(device, false, false, true);
7853
+ this.shadowAnimatedCutoutLayout = createDepthBindGroupLayout(device, false, true, true);
7587
7854
  this.shadowDraws = new UniformRing(
7588
7855
  device,
7589
7856
  /* The cutout block is the plain one and two fields more, and a slot must hold either. */
@@ -7985,6 +8252,8 @@ export class WebGPURenderer implements RendererApi {
7985
8252
  present['layers'] = key.includes(':layers');
7986
8253
  /* Nor is a dynamic mesh's split, which moves positions and normals into buffers of their own. */
7987
8254
  present['dynamic'] = key.includes(':dynamic');
8255
+ /* Nor how each attribute travels, which moves every field after a packed one. */
8256
+ markPackingOfKey(present, key);
7988
8257
  return present;
7989
8258
  }
7990
8259
 
@@ -8095,9 +8364,18 @@ export class WebGPURenderer implements RendererApi {
8095
8364
  this.lineVerts.reset();
8096
8365
  this.lineFrags.reset();
8097
8366
  this.skyUniforms.reset();
8098
- /* The material ring, and the open slot with it: last frame's slots are gone. */
8099
- this.perFrame.reset();
8367
+ /* The pass ring, and the open slots with it: last frame's pass slots are gone. Its materials
8368
+ stay in the store, which a draw finds them in again. */
8369
+ this.passRing.reset();
8370
+ this.viewRing.reset();
8371
+ this.materialStore.beginFrame();
8372
+ this.passesAsked = 0;
8373
+ this.passesRefused = 0;
8374
+ this.viewsAsked = 0;
8375
+ this.viewsRefused = 0;
8376
+ this.materialsRefused = 0;
8100
8377
  this.materials.dirty();
8378
+ this.reopenPass();
8101
8379
  /* Spent by `runMotion` at the end of the last frame; a frame states its movers afresh. */
8102
8380
  this.motionPass?.reset();
8103
8381
  this.latePass?.reset();
@@ -8285,6 +8563,13 @@ export class WebGPURenderer implements RendererApi {
8285
8563
  present['dynamic'] = true;
8286
8564
  key += ':dynamic';
8287
8565
  }
8566
+ /*
8567
+ * And how each attribute travels (`vertexPacking.ts`): a field read as sixteen bits rather than
8568
+ * thirty-two, or not interleaved at all, moves every field after it, so the key says so.
8569
+ */
8570
+ const packing = vertexPackingOf(data, dynamic);
8571
+ markPacking(present, packing);
8572
+ key += packingKey(packing);
8288
8573
  const fullKey = `${this.variant}|${key}`;
8289
8574
  /*
8290
8575
  * **For every target this mesh can land on, which is two when the overlay is its own.**
@@ -8432,6 +8717,40 @@ export class WebGPURenderer implements RendererApi {
8432
8717
  };
8433
8718
  }
8434
8719
 
8720
+ /**
8721
+ * The copies this frame's mesh rewrites owe (`StagedRewrite`), recorded since the last submit and
8722
+ * sent at the head of the next one: one encoder however many meshes are rewritten. Null while
8723
+ * nothing is owed.
8724
+ */
8725
+ private pendingRewrites: GPUCommandEncoder | null = null;
8726
+ /** One record handed to every staged rewrite, refilled each call, so a rewrite allocates nothing. */
8727
+ private readonly stagedRewrite: StagedRewrite = {
8728
+ encoder: null as unknown as GPUCommandEncoder,
8729
+ staging: null as unknown as GPUBuffer,
8730
+ previous: null,
8731
+ };
8732
+
8733
+ private rewriteCopies(): GPUCommandEncoder {
8734
+ this.pendingRewrites ??= this.surface.device.createCommandEncoder({
8735
+ label: 'mesh.rewriteCopies',
8736
+ });
8737
+ return this.pendingRewrites;
8738
+ }
8739
+
8740
+ /**
8741
+ * Submit `buffers`, led by any copies the frame's mesh rewrites owe. **Every submit of this
8742
+ * renderer goes through here**, because any of them may draw a rewritten mesh — the frame's own,
8743
+ * a mirror's, a shadow's, a probe's — and the copies must be ahead of the first that does.
8744
+ */
8745
+ private submit(buffers: GPUCommandBuffer[]): void {
8746
+ const rewrites = this.pendingRewrites;
8747
+ if (rewrites !== null) {
8748
+ this.pendingRewrites = null;
8749
+ buffers.unshift(rewrites.finish());
8750
+ }
8751
+ this.surface.device.queue.submit(buffers);
8752
+ }
8753
+
8435
8754
  /**
8436
8755
  * Rewrite a mesh's positions, and its normals where the caller has them.
8437
8756
  *
@@ -8454,20 +8773,35 @@ export class WebGPURenderer implements RendererApi {
8454
8773
  * `changeFrames.ts`.
8455
8774
  */
8456
8775
  const motion = mesh.motion;
8457
- let previous: GPUBuffer | null = null;
8776
+ let staged: StagedRewrite | null = null;
8458
8777
  if (motion !== null && this.reconstructionWanted) {
8459
8778
  const frame = frameOfChange(this.moverFrame, this.inFrame);
8779
+ const size = mesh.vertexBuffers[0]?.size ?? 0;
8780
+ let previous: GPUBuffer | null = null;
8460
8781
  if (keepsPrevious(motion.changed, frame)) {
8461
8782
  motion.previous ??= this.surface.device.createBuffer({
8462
8783
  label: 'recon.dynamicPrevious',
8463
- size: mesh.vertexBuffers[0]?.size ?? 0,
8784
+ size,
8464
8785
  usage: 0x0020 | 0x0008, // VERTEX | COPY_DST
8465
8786
  });
8466
8787
  previous = motion.previous;
8467
8788
  }
8468
8789
  motion.changed = frame;
8790
+ /*
8791
+ * **Staged, and copied in behind the copy aside, in an encoder the frame's next submit leads
8792
+ * with**: no submit of the rewrite's own. See `StagedRewrite` and `submit`.
8793
+ */
8794
+ motion.staging ??= this.surface.device.createBuffer({
8795
+ label: 'mesh.stagedPositions',
8796
+ size,
8797
+ usage: 0x0004 | 0x0008, // COPY_SRC | COPY_DST
8798
+ });
8799
+ staged = this.stagedRewrite;
8800
+ staged.encoder = this.rewriteCopies();
8801
+ staged.staging = motion.staging;
8802
+ staged.previous = previous;
8469
8803
  }
8470
- mesh.update(this.surface.device, positions, normals, previous);
8804
+ mesh.update(this.surface.device, positions, normals, staged);
8471
8805
  }
8472
8806
 
8473
8807
  /**
@@ -8480,6 +8814,8 @@ export class WebGPURenderer implements RendererApi {
8480
8814
  */
8481
8815
  bindMeshPass(camera: Camera, env: Environment): void {
8482
8816
  if (this.surface.lost) return;
8817
+ /* The matrix every receiver projects through: the environment's, as WebGL2's. See the field. */
8818
+ this.receiverLightViewProj.set(env.lightViewProj as Float32Array);
8483
8819
 
8484
8820
  /* Rectangles past the fixed arm's are the froxel table's, and without it nobody shades them. */
8485
8821
  if (
@@ -8598,6 +8934,12 @@ export class WebGPURenderer implements RendererApi {
8598
8934
  this.surfaceFog = FOG_RECEDE;
8599
8935
  /* Every material switch off — albedo, ORM map, emissive map, two-sided — as WebGL2 resets them. */
8600
8936
  i.fill(0, at('uMaterialFlags'), at('uMaterialFlags') + 4);
8937
+ /* And no projection: maps follow the mesh's coordinates until a material asks otherwise. */
8938
+ f.fill(0, at('uWorldUv'), at('uWorldUv') + 4);
8939
+ /* Nor light through from behind. See `SurfaceMaterial.diffuseTransmission`. */
8940
+ f[at('uGlassTint') + 3] = 0;
8941
+ /* Nor layers: one layer until a material asks for more. */
8942
+ f.fill(0, at('uLayers'), at('uLayers') + LAYER_FLOATS);
8601
8943
  f[at('uCutout')] = 0;
8602
8944
  f[at('uCutout') + 1] = 0;
8603
8945
  this.cutoutResolveStaged = 'hard';
@@ -8671,10 +9013,16 @@ export class WebGPURenderer implements RendererApi {
8671
9013
  * number, so this is a copy and is marked as one; the two must move together.
8672
9014
  */
8673
9015
  f[at('uGrain')] = 1;
9016
+ /* No reflection and an environment at its own brightness, from the pass and from no material. */
9017
+ this.passReflectivity = 0;
9018
+ this.passEnvironmentGain = 1;
9019
+ this.materialReflectivity = null;
9020
+ this.materialEnvironmentGain = null;
8674
9021
  f[at('uEnvironmentDials') + 1] = 0;
8675
9022
  /* No weighting and no dither until a pass or a crossfade asks. See `setDitherFade`. */
8676
9023
  f[at('uWriteMode')] = 0;
8677
9024
  f[at('uWriteMode') + 1] = 0;
9025
+ f[at('uWriteMode') + 2] = 0;
8678
9026
  /* 1, not 0: a multiplier's identity is one. See `setEnvironmentGain`. */
8679
9027
  f[at('uEnvironmentDials') + 2] = 1;
8680
9028
  /* No overlay until a draw sets one. See `setSurfaceOverlay`. */
@@ -8785,6 +9133,8 @@ export class WebGPURenderer implements RendererApi {
8785
9133
  */
8786
9134
  if (this.quality.nightEmissive) f[at('uNightEmissive')] = env.nightEmissive ?? 0;
8787
9135
  f.set(env.highlightMin, at('uHighlightMin'));
9136
+ /* The frame the cutout dither moves with, in the near corner's w. See `uHighlightMin`. */
9137
+ f[at('uHighlightMin') + 3] = this.cutoutFrame;
8788
9138
  f.set(env.highlightMax, at('uHighlightMax'));
8789
9139
  /* The gain in the far corner's w. See `uHighlightMax`. */
8790
9140
  f[at('uHighlightMax') + 3] = env.highlightGain;
@@ -8966,6 +9316,11 @@ export class WebGPURenderer implements RendererApi {
8966
9316
  f[at('uShadowStrength')] = env.shadowStrength;
8967
9317
  f[at('uShadowMapSize')] = this.quality.directionalShadowMapSize;
8968
9318
  f[at('uShadowDepthSpan')] = env.shadowDepthSpan;
9319
+ /* The moving layer's own matrix and span, where the lit stage reads one: `movingLightFor`. */
9320
+ this.enableMovingSun(env);
9321
+ const moving = this.movingLightFor(env);
9322
+ f.set(moving as Float32Array, at('uMovingLightViewProj'));
9323
+ f[at('uMovingShadowDepthSpan')] = orthographicDepthSpan(moving);
8969
9324
  f[at('uShadowMaxDistance')] = this.quality.directionalShadowMaxDistance;
8970
9325
  f[at('uShadowMaxSlope')] = this.quality.directionalShadowMaxSlope;
8971
9326
  /* Sampled only where a pass filled it — see `peelFilled` for why existing is not enough. */
@@ -9057,6 +9412,7 @@ export class WebGPURenderer implements RendererApi {
9057
9412
  * same terms here, which is why a pass cannot inherit a material from the one before it.
9058
9413
  */
9059
9414
  this.materials.dirty();
9415
+ this.reopenPass();
9060
9416
  /*
9061
9417
  * A pass starts with no material, exactly as `renderer.ts` resets these terms — **every map,
9062
9418
  * not the albedo alone**. `setMaterial` skips the lookup when the maps it is given are the ones
@@ -9081,31 +9437,114 @@ export class WebGPURenderer implements RendererApi {
9081
9437
  }
9082
9438
 
9083
9439
  /**
9084
- * The slot the next draw's material lives in, taking a new one when something dirtied it.
9440
+ * The slot the next draw's material lives in, placing it when something dirtied it, with the
9441
+ * pass's slot taken first where `bindMeshPass` reopened it: `passOffset` is that one's offset.
9085
9442
  *
9086
- * **A copy per material change, not per draw.** A scene drawing a hundred meshes of one
9087
- * material spends one slot and one copy; a scene switching material every draw spends one
9088
- * each, which is what it asked for.
9443
+ * **A placement per material change, not per draw.** A scene drawing a hundred meshes of one
9444
+ * material places it once; a scene switching material every draw places one each, and a material
9445
+ * the store already holds — the same numbers as a draw before it, this frame or an earlier one —
9446
+ * is bound where it is and uploads nothing.
9089
9447
  */
9090
9448
  private materialSlotForDraw(): number | null {
9449
+ if (!this.passBlock.open) {
9450
+ this.passesAsked += 1;
9451
+ const pass = this.passRing.allocate();
9452
+ if (pass === null) {
9453
+ this.passesRefused += 1;
9454
+ this.materialBudget.ask();
9455
+ this.materialBudget.drop();
9456
+ if (!this.warnedPassesFull) {
9457
+ this.warnedPassesFull = true;
9458
+ console.warn(
9459
+ `WebGPU: more than ${this.passRing.slots} mesh passes drew in one frame; the draws of the rest are skipped this frame and the ring grows for the next.`,
9460
+ );
9461
+ }
9462
+ return null;
9463
+ }
9464
+ this.passRing.writeBlock(pass, this.passInts);
9465
+ this.passBlock.slot = pass;
9466
+ }
9091
9467
  if (this.materials.open) return this.materials.slot;
9092
- const slot = this.perFrame.allocate();
9468
+ const slot = this.materialStore.place(this.materialInts);
9093
9469
  if (slot === null) {
9094
- if (!this.warnedMaterialsFull) {
9095
- this.warnedMaterialsFull = true;
9470
+ this.materialsRefused += 1;
9471
+ this.warnMaterialsFull();
9472
+ return null;
9473
+ }
9474
+ this.materials.slot = slot;
9475
+ return slot;
9476
+ }
9477
+
9478
+ /** The pass slot `materialSlotForDraw` left open, read beside the material's by every lit draw. */
9479
+ private get passOffset(): number {
9480
+ return this.passBlock.slot;
9481
+ }
9482
+
9483
+ /** A new pass: its block, and both its views, are taken afresh by the next draw. */
9484
+ private reopenPass(): void {
9485
+ this.passBlock.dirty();
9486
+ this.reopenViews();
9487
+ }
9488
+
9489
+ /** What a view holds changed — the wind, the light's matrix — so the next draw takes a new one. */
9490
+ private reopenViews(): void {
9491
+ this.viewBlock.dirty();
9492
+ this.lateViewBlock.dirty();
9493
+ }
9494
+
9495
+ /**
9496
+ * The view slot a lit draw reads its camera from: the pass's, or its unjittered twin for a blended
9497
+ * draw landing after the upscale. Taken by the first draw that needs it after the pass, the wind or
9498
+ * the light's matrix changed, and shared by every draw until then. Null when the ring is full, which
9499
+ * skips the draw and grows the ring for the next frame, as the pass ring does.
9500
+ */
9501
+ private viewSlotFor(late: boolean, viewProj: Float32Array): number | null {
9502
+ const block = late ? this.lateViewBlock : this.viewBlock;
9503
+ if (block.open) return block.slot;
9504
+ this.viewsAsked += 1;
9505
+ const slot = this.viewRing.allocate();
9506
+ if (slot === null) {
9507
+ this.viewsRefused += 1;
9508
+ this.materialBudget.ask();
9509
+ this.materialBudget.drop();
9510
+ if (!this.warnedPassesFull) {
9511
+ this.warnedPassesFull = true;
9096
9512
  console.warn(
9097
- `WebGPU: more than ${this.quality.materialChangesPerFrame} material changes in a frame; the draws asking for the rest are skipped. renderer.frameBudget names the line and the count.`,
9513
+ `WebGPU: more than ${this.viewRing.slots} views drew in one frame; the draws of the rest are skipped this frame and the ring grows for the next.`,
9098
9514
  );
9099
9515
  }
9100
9516
  return null;
9101
9517
  }
9102
- this.perFrame.writeBlock(slot, this.perFrameInts);
9103
- this.materials.slot = slot;
9518
+ this.writeView(this.viewRing, slot, viewProj);
9519
+ block.slot = slot;
9104
9520
  return slot;
9105
9521
  }
9106
9522
 
9523
+ /** A view block: the camera, the light's matrix and the frame's wind, into `ring` at `slot`. */
9524
+ private writeView(ring: UniformRing, slot: number, viewProj: Float32Array): void {
9525
+ ring.writeFloats(slot, FLAT_VIEW_FIELDS.uViewProj.offset, viewProj);
9526
+ ring.writeFloats(slot, FLAT_VIEW_FIELDS.uLightViewProj.offset, this.receiverLightViewProj);
9527
+ /* The scene's clock, which a bone animation's instances read their moment from. */
9528
+ ring.writeFloat(slot, FLAT_VIEW_FIELDS.uSceneTime.offset, this.animationTime);
9529
+ const d = this.frameWind;
9530
+ ring.writeFloats(slot, FLAT_VIEW_FIELDS.uWindDirection.offset, d.direction);
9531
+ ring.writeFloat(slot, FLAT_VIEW_FIELDS.uWindSpeed.offset, d.bend);
9532
+ ring.writeFloat(slot, FLAT_VIEW_FIELDS.uWindGust.offset, d.gust);
9533
+ ring.writeFloat(slot, FLAT_VIEW_FIELDS.uWindTime.offset, d.time);
9534
+ ring.writeFloats(slot, FLAT_VIEW_FIELDS.uWindSpatialPhase.offset, d.spatialPhase);
9535
+ }
9536
+
9537
+ private warnMaterialsFull(): void {
9538
+ if (this.warnedMaterialsFull) return;
9539
+ this.warnedMaterialsFull = true;
9540
+ console.warn(
9541
+ `WebGPU: more than ${this.quality.materialChangesPerFrame} distinct materials in a frame; the draws asking for the rest are skipped. renderer.frameBudget names the line and the count.`,
9542
+ );
9543
+ }
9544
+
9107
9545
  /** Said once rather than every frame, for the reason `warnedFull` gives. */
9108
9546
  private warnedMaterialsFull = false;
9547
+ private warnedPassesFull = false;
9109
9548
  private warnedSkyFull = false;
9110
9549
  private warnedParticlesTwice = false;
9111
9550
  /**
@@ -9146,7 +9585,14 @@ export class WebGPURenderer implements RendererApi {
9146
9585
  * What this saves is the GPU's share, which for a heavy mesh is most of the cost and for a
9147
9586
  * trivial one is very little.
9148
9587
  */
9149
- if (this.quality.cullDraws && !this.visible(mesh.bounds, model as ReadonlyMat4)) return;
9588
+ /* A static list draws every entry in every view, since a bundle cannot leave one out. */
9589
+ if (
9590
+ this.quality.cullDraws &&
9591
+ this.staticRecording === null &&
9592
+ !this.visible(mesh.bounds, model as ReadonlyMat4)
9593
+ ) {
9594
+ return;
9595
+ }
9150
9596
  /*
9151
9597
  * And the other half of the same question, where the profile asked for one. Gated on
9152
9598
  * `cullDraws` alongside the frustum test, because both are the renderer doing a consumer's
@@ -9337,21 +9783,16 @@ export class WebGPURenderer implements RendererApi {
9337
9783
  `TranslucentMeshOptions.reconstructed`. */
9338
9784
  const lands = blend && options.reconstructed !== true && this.drawsLate();
9339
9785
  const late = lands || (blend && this.oitReplaying && this.reconstructing);
9340
- this.perDraw.writeFloats(
9341
- slot,
9342
- FLAT_VERT_FIELDS.uViewProj.offset,
9343
- late ? this.correctedViewProj : this.viewProj,
9344
- );
9786
+ /* The camera, the light's matrix and the wind are the pass's, in its view block. */
9787
+ const view = this.viewSlotFor(late, late ? this.correctedViewProj : this.viewProj);
9788
+ if (view === null) return;
9789
+ this.drawViewOffset = view;
9345
9790
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uModel.offset, model);
9346
9791
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uTint.offset, tint ?? WHITE);
9347
- /* The frame's wind, for a mesh carrying a sway lane. Written on this path and not the
9348
- instanced one, which declares none of these fields. */
9349
- this.writeWind(slot);
9350
9792
  /* The material's repeats, not a constant: geometry authored in metres is textured at
9351
9793
  whatever density the material asks for without rebuilding it. */
9352
9794
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvScale.offset, this.uvScale);
9353
9795
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvOffset.offset, this.uvOffset);
9354
- this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uLightViewProj.offset, this.lightViewProj);
9355
9796
  /*
9356
9797
  * Whether this mesh's tangent frame is real. WebGPU has no disabled attribute, so location 10
9357
9798
  * is always backed — by the caller's data or by the constant buffer — which makes the binding
@@ -9489,8 +9930,8 @@ export class WebGPURenderer implements RendererApi {
9489
9930
  * thrown error in the middle of one.
9490
9931
  */
9491
9932
  const pipelines = blend ? this.blendedPipelines(lands) : this.targetPipelines();
9492
- let pipeline = pipelines.peek(key);
9493
- if (pipeline === undefined) {
9933
+ let pipeline: GPURenderPipeline | null = pipelines.peek(key) ?? null;
9934
+ if (pipeline === null) {
9494
9935
  const present = this.meshPresent.get(base);
9495
9936
  if (present === undefined) {
9496
9937
  throw new Error(`WebGPU: no pipeline for ${key}; createMesh builds it`);
@@ -9517,6 +9958,14 @@ export class WebGPURenderer implements RendererApi {
9517
9958
  splits ? 'scene' : 'whole',
9518
9959
  );
9519
9960
  }
9961
+ /* A split skin draws its three halves or none of them. */
9962
+ if (
9963
+ pipeline !== null &&
9964
+ splits &&
9965
+ !this.skinHalvesReady(key, base, skinned, morphed, layer, eight, clothed)
9966
+ ) {
9967
+ pipeline = null;
9968
+ }
9520
9969
 
9521
9970
  /*
9522
9971
  * Guards the draw call only, not the restore below.
@@ -9534,8 +9983,9 @@ export class WebGPURenderer implements RendererApi {
9534
9983
  * close to the 256-slot budget before the gateway, the play controls, the waveform, the
9535
9984
  * label handles and the sparkles are even counted.
9536
9985
  */
9537
- const material = this.materialSlotForDraw();
9538
- if (material !== null) {
9986
+ /* A draw whose pipeline is still compiling is left out, its state restored all the same. */
9987
+ const material = pipeline === null ? null : this.materialSlotForDraw();
9988
+ if (pipeline !== null && material !== null) {
9539
9989
  /*
9540
9990
  * Record rather than issue. Every per-draw value is already in `slot` and `material` —
9541
9991
  * the uniform rings are the state arena this design would otherwise have had to build —
@@ -9548,24 +9998,32 @@ export class WebGPURenderer implements RendererApi {
9548
9998
  command.pipeline = pipeline;
9549
9999
  command.bindGroup = this.flatBindGroupFor(skinned, morphed, deltas);
9550
10000
  command.offsetA = slot;
9551
- command.offsetB = material;
9552
- command.offsetCount = 2;
10001
+ command.offsetB = this.passOffset;
10002
+ command.offsetC = material;
10003
+ command.offsetD = this.drawViewOffset;
10004
+ command.offsetCount = 4;
9553
10005
  for (let index = 0; index < mesh.vertexBuffers.length; index++) {
9554
10006
  command.vertexBuffers[index] = mesh.vertexBuffers[index] as GPUBuffer;
9555
10007
  }
9556
10008
  command.vertexCount = mesh.vertexBuffers.length;
9557
10009
  command.indexBuffer = mesh.indexBuffer;
10010
+ command.indexFormat = mesh.indexFormat;
9558
10011
  command.indexed = true;
9559
10012
  command.count = mesh.indexCount;
9560
10013
  } else {
9561
10014
  const pass = this.openPass();
9562
10015
  if (pass === null) return;
9563
10016
  pass.setPipeline(pipeline);
9564
- pass.setBindGroup(0, this.flatBindGroupFor(skinned, morphed, deltas), [slot, material]);
10017
+ pass.setBindGroup(0, this.flatBindGroupFor(skinned, morphed, deltas), [
10018
+ slot,
10019
+ this.passOffset,
10020
+ material,
10021
+ this.drawViewOffset,
10022
+ ]);
9565
10023
  for (let index = 0; index < mesh.vertexBuffers.length; index++) {
9566
10024
  pass.setVertexBuffer(index, mesh.vertexBuffers[index] as GPUBuffer);
9567
10025
  }
9568
- pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
10026
+ pass.setIndexBuffer(mesh.indexBuffer, mesh.indexFormat);
9569
10027
  pass.drawIndexed(mesh.indexCount);
9570
10028
  }
9571
10029
  if (splits) {
@@ -9677,16 +10135,127 @@ export class WebGPURenderer implements RendererApi {
9677
10135
  'instanced pipeline.',
9678
10136
  );
9679
10137
  }
10138
+ const animation = (options.animation ?? null) as GpuBoneAnimation | null;
10139
+ /* The bone a vertex follows is its second coordinates' u, which a mesh carries in its grain
10140
+ lane; a mesh with none has nothing to name a bone by. See `boneAnimation.ts`. */
10141
+ if (animation !== null && !this.presentOf(mesh as GpuMesh & { key?: string })['grain']) {
10142
+ throw new Error(
10143
+ 'WebGPU: a bone animation reads each vertex’s bone from the mesh’s second coordinates ' +
10144
+ '(`MeshData.lightmapUvs`), and this mesh has none.',
10145
+ );
10146
+ }
9680
10147
  const batch = new GpuInstancedBatch(
9681
10148
  this.surface.device,
9682
10149
  mesh,
9683
10150
  capacity,
9684
10151
  `flat.instances:${(mesh as GpuMesh & { key?: string }).key ?? 'mesh'}`,
9685
10152
  options.cull ?? false,
10153
+ animation,
9686
10154
  );
9687
10155
  return batch;
9688
10156
  }
9689
10157
 
10158
+ /** See `Renderer.setAnimationTime`. Every view taken after it reads the new clock. */
10159
+ setAnimationTime(seconds: number): void {
10160
+ if (!Number.isFinite(seconds) || seconds === this.animationTime) return;
10161
+ this.animationTime = seconds;
10162
+ this.reopenViews();
10163
+ }
10164
+
10165
+ /** The clock `setAnimationTime` set, 0 until it is. */
10166
+ private animationTime = 0;
10167
+
10168
+ /** See `Renderer.createBoneAnimation`. */
10169
+ createBoneAnimation(clip: BoneAnimationClip): BoneAnimationHandle {
10170
+ return new GpuBoneAnimation(this.surface.device, packBoneAnimation(clip));
10171
+ }
10172
+
10173
+ /** See `Renderer.disposeBoneAnimation`. A batch still playing it draws nothing after. */
10174
+ disposeBoneAnimation(animation: BoneAnimationHandle): void {
10175
+ const clip = animation as GpuBoneAnimation;
10176
+ if (!clip.disposed) clip.dispose();
10177
+ }
10178
+
10179
+ /** An animated batch's clip and clocks, as its groups bind them. */
10180
+ private animationViews(batch: GpuInstancedBatch): AnimationViews | null {
10181
+ const animation = batch.animation;
10182
+ const clocks = batch.clocks;
10183
+ if (animation === null || clocks === null || animation.disposed) return null;
10184
+ return { places: animation.placesView, turns: animation.turnsView, clocks: clocks.view };
10185
+ }
10186
+
10187
+ /** An animated batch's groups, dropped wherever the renderer drops its own. */
10188
+ private freshAnimatedGroups(batch: GpuInstancedBatch): void {
10189
+ if (batch.groupEpoch === this.flatGroupEpoch) return;
10190
+ batch.litGroups.clear();
10191
+ batch.depthGroups.clear();
10192
+ batch.depthPeelGroups.clear();
10193
+ batch.groupEpoch = this.flatGroupEpoch;
10194
+ }
10195
+
10196
+ /**
10197
+ * The lit group an animated batch draws with: the material's group, or its overlay twin, again
10198
+ * with the clip's and the clocks' textures — kept on the batch by the group it was built beside,
10199
+ * as a skinned twin is kept by its palette slot.
10200
+ */
10201
+ private animatedGroup(batch: GpuInstancedBatch, views: AnimationViews): GPUBindGroup {
10202
+ this.freshAnimatedGroups(batch);
10203
+ const base = this.overlayGroup();
10204
+ let group = batch.litGroups.get(base);
10205
+ if (group === undefined) {
10206
+ this.overlayBinding = base !== this.bindGroup;
10207
+ group = createFlatBindGroup(
10208
+ this.surface.device,
10209
+ this.animatedBindGroupLayout,
10210
+ this.variant,
10211
+ this.perDraw.buffer,
10212
+ this.passRing.buffer,
10213
+ this.materialStore.buffer,
10214
+ this.viewRing.buffer,
10215
+ this.flatTextures,
10216
+ null,
10217
+ null,
10218
+ null,
10219
+ views,
10220
+ );
10221
+ this.overlayBinding = false;
10222
+ this.bindGroupBudget.ask();
10223
+ batch.litGroups.set(base, group);
10224
+ }
10225
+ return group;
10226
+ }
10227
+
10228
+ /** An animated caster's shadow group, by what it cuts with and whether this is the peel. */
10229
+ private animatedDepthGroup(
10230
+ batch: GpuInstancedBatch,
10231
+ views: AnimationViews,
10232
+ cutout: GpuSurfaceTexture | null,
10233
+ ): GPUBindGroup | null {
10234
+ this.freshAnimatedGroups(batch);
10235
+ const peel = this.shadowLayerIsPeel && this.shadowPeelPassUniforms !== null;
10236
+ const groups = peel ? batch.depthPeelGroups : batch.depthGroups;
10237
+ const held = groups.get(cutout);
10238
+ if (held !== undefined) return held;
10239
+ const passUniforms = peel ? this.shadowPeelPassUniforms : this.shadowPassUniforms;
10240
+ const previousView = peel ? this.sunStaticArrayView : this.shadowBlankView;
10241
+ const previousSampler = peel ? this.shadowPeelSampler : this.shadowBlankSampler;
10242
+ if (passUniforms === null || previousView === null || previousSampler === null) return null;
10243
+ const group = createDepthBindGroup(
10244
+ this.surface.device,
10245
+ cutout === null ? this.shadowAnimatedLayout : this.shadowAnimatedCutoutLayout,
10246
+ this.shadowDraws.buffer,
10247
+ passUniforms,
10248
+ previousView,
10249
+ previousSampler,
10250
+ null,
10251
+ cutout === null ? null : { view: cutout.view, sampler: cutout.sampler },
10252
+ null,
10253
+ views,
10254
+ );
10255
+ groups.set(cutout, group);
10256
+ return group;
10257
+ }
10258
+
9690
10259
  /** The device instance culls, built on the first culling draw. See `InstanceCullPass`. */
9691
10260
  private instanceCulls: InstanceCullPass | null = null;
9692
10261
  /** Counts frames, so a batch knows when its cull slots were last taken. */
@@ -9720,7 +10289,12 @@ export class WebGPURenderer implements RendererApi {
9720
10289
  this.submitInstanced(batch, data, 1, false, {});
9721
10290
  /* A batch that moved this frame pairs each slot with last frame's; the batch is the identity. */
9722
10291
  const previous = batch.previousBuffer;
9723
- if (previous !== null && batch.changed === this.moverFrame && this.tracksMotionNow()) {
10292
+ if (
10293
+ previous !== null &&
10294
+ this.staticRecording === null &&
10295
+ batch.changed === this.moverFrame &&
10296
+ this.tracksMotionNow()
10297
+ ) {
9724
10298
  const paired = pairedInstances(batch.previousCount, Math.min(data.count, batch.capacity));
9725
10299
  if (paired > 0) this.motionPass?.recordInstanced(batch.mesh, batch.buffer, previous, paired);
9726
10300
  }
@@ -9767,6 +10341,8 @@ export class WebGPURenderer implements RendererApi {
9767
10341
  this.perFrameFloats[tintAt] = refractTint?.[0] ?? 1;
9768
10342
  this.perFrameFloats[tintAt + 1] = refractTint?.[1] ?? 1;
9769
10343
  this.perFrameFloats[tintAt + 2] = refractTint?.[2] ?? 1;
10344
+ /* w: this framebuffer's first row is the top, so up the screen is down the rows. */
10345
+ this.perFrameFloats[tintAt + 3] = -1;
9770
10346
  const glassTintAt = this.materialField('uGlassTint');
9771
10347
  this.perFrameFloats[glassTintAt] = glass.tint[0];
9772
10348
  this.perFrameFloats[glassTintAt + 1] = glass.tint[1];
@@ -9842,10 +10418,16 @@ export class WebGPURenderer implements RendererApi {
9842
10418
  /* A culling batch's camera draw: the whole batch against the view and the occluders on the CPU,
9843
10419
  then instance by instance on the device into an indirect draw. Blended draws keep every
9844
10420
  instance and their order. See `InstancedOptions.cull`. */
9845
- const culling = !blend && batch.cull;
10421
+ const culling = !blend && batch.cull && this.staticRecording === null;
9846
10422
  if (culling && !batchBoxVisible(batch.box, this.frustum, this.occlusion)) return;
10423
+ /* A batch playing a bone animation reads its clocks by instance index, which a compaction
10424
+ would reorder: it is culled whole and never instance by instance. */
10425
+ const animation = batch.animation === null ? null : this.animationViews(batch);
10426
+ if (batch.animation !== null && animation === null) return;
9847
10427
  const cut =
9848
- culling && cullsInstances(count, mesh.indexCount) ? this.takeCullSlot(batch, count) : null;
10428
+ culling && animation === null && cullsInstances(count, mesh.indexCount)
10429
+ ? this.takeCullSlot(batch, count)
10430
+ : null;
9849
10431
  const instanceBuffer = cut === null ? batch.buffer : cut.instances;
9850
10432
 
9851
10433
  const slot = this.perDraw.allocate();
@@ -9861,15 +10443,22 @@ export class WebGPURenderer implements RendererApi {
9861
10443
 
9862
10444
  /* Late unless it moves with what it lies on. See `TranslucentMeshOptions.reconstructed`. */
9863
10445
  const lands = blend && options.reconstructed !== true && this.drawsLate();
9864
- this.perDraw.writeFloats(
9865
- slot,
9866
- FLAT_VERT_FIELDS.uViewProj.offset,
9867
- lands ? this.correctedViewProj : this.viewProj,
9868
- );
10446
+ const view = this.viewSlotFor(lands, lands ? this.correctedViewProj : this.viewProj);
10447
+ if (view === null) return;
10448
+ this.drawViewOffset = view;
9869
10449
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvScale.offset, this.uvScale);
9870
10450
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvOffset.offset, this.uvOffset);
9871
- this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uLightViewProj.offset, this.lightViewProj);
9872
10451
  this.perDraw.writeInt(slot, FLAT_VERT_FIELDS.uHasTangents.offset, mesh.hasTangents ? 1 : 0);
10452
+ /* The clip's speed and bone scale, read by the animated variant's own field. */
10453
+ const clip = batch.animation;
10454
+ if (animation !== null && clip !== null) {
10455
+ this.perDraw.writeFloat(
10456
+ slot,
10457
+ FLAT_ANIMATED_VERT_FIELDS.uBoneClip.offset,
10458
+ clip.framesPerSecond,
10459
+ );
10460
+ this.perDraw.writeFloat(slot, FLAT_ANIMATED_VERT_FIELDS.uBoneClip.offset + 4, clip.boneScale);
10461
+ }
9873
10462
 
9874
10463
  const dimmed = opacity < 1;
9875
10464
  if (dimmed) {
@@ -9907,14 +10496,14 @@ export class WebGPURenderer implements RendererApi {
9907
10496
  plain one would hand a draw the wrong layout. */
9908
10497
  const adds = blend && options.additive === true;
9909
10498
  const key =
9910
- `${base}|inst${blend ? (adds ? '|add' : '|blend') : ''}` +
10499
+ `${base}|inst${animation === null ? '' : '|anim'}${blend ? (adds ? '|add' : '|blend') : ''}` +
9911
10500
  `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
9912
10501
  `${this.materialDoubleSided ? '|2s' : ''}${coverage ? '|a2c' : ''}` +
9913
10502
  `${modelKind === null ? '' : `|m:${modelKind}`}`;
9914
10503
 
9915
10504
  const pipelines = blend ? this.blendedPipelines(lands) : this.targetPipelines();
9916
- let pipeline = pipelines.peek(key);
9917
- if (pipeline === undefined) {
10505
+ let pipeline: GPURenderPipeline | null = pipelines.peek(key) ?? null;
10506
+ if (pipeline === null) {
9918
10507
  const present = this.meshPresent.get(base);
9919
10508
  if (present === undefined) {
9920
10509
  throw new Error(`WebGPU: no mesh attributes recorded for ${base}; createMesh records them`);
@@ -9922,7 +10511,7 @@ export class WebGPURenderer implements RendererApi {
9922
10511
  pipeline = flatPipeline(
9923
10512
  pipelines,
9924
10513
  this.surface.device,
9925
- this.flatLayoutFor(false, false),
10514
+ animation === null ? this.flatLayoutFor(false, false) : this.animatedBindGroupLayout,
9926
10515
  this.variant,
9927
10516
  key,
9928
10517
  present,
@@ -9938,20 +10527,29 @@ export class WebGPURenderer implements RendererApi {
9938
10527
  false,
9939
10528
  false,
9940
10529
  modelKind,
10530
+ 'whole',
10531
+ false,
10532
+ animation !== null,
9941
10533
  );
9942
10534
  }
9943
10535
 
9944
- const material = this.materialSlotForDraw();
9945
- if (material !== null) {
10536
+ /* A batch whose pipeline is still compiling is left out, as a mesh is. */
10537
+ const material = pipeline === null ? null : this.materialSlotForDraw();
10538
+ if (pipeline !== null && material !== null) {
9946
10539
  const command = blend
9947
10540
  ? this.recordBlended(0, lands)
9948
10541
  : this.recordDraw(0, this.currentTarget());
9949
10542
  if (command !== null) {
9950
10543
  command.pipeline = pipeline;
9951
- command.bindGroup = this.flatBindGroupFor(false, false, null);
10544
+ command.bindGroup =
10545
+ animation === null
10546
+ ? this.flatBindGroupFor(false, false, null)
10547
+ : this.animatedGroup(batch, animation);
9952
10548
  command.offsetA = slot;
9953
- command.offsetB = material;
9954
- command.offsetCount = 2;
10549
+ command.offsetB = this.passOffset;
10550
+ command.offsetC = material;
10551
+ command.offsetD = this.drawViewOffset;
10552
+ command.offsetCount = 4;
9955
10553
  let index = 0;
9956
10554
  for (; index < mesh.vertexBuffers.length; index++) {
9957
10555
  command.vertexBuffers[index] = mesh.vertexBuffers[index] as GPUBuffer;
@@ -9959,6 +10557,7 @@ export class WebGPURenderer implements RendererApi {
9959
10557
  command.vertexBuffers[index] = instanceBuffer;
9960
10558
  command.vertexCount = mesh.vertexBuffers.length + 1;
9961
10559
  command.indexBuffer = mesh.indexBuffer;
10560
+ command.indexFormat = mesh.indexFormat;
9962
10561
  command.indexed = true;
9963
10562
  command.count = mesh.indexCount;
9964
10563
  command.instances = count;
@@ -9967,13 +10566,19 @@ export class WebGPURenderer implements RendererApi {
9967
10566
  const pass = this.openPass();
9968
10567
  if (pass === null) return;
9969
10568
  pass.setPipeline(pipeline);
9970
- pass.setBindGroup(0, this.flatBindGroupFor(false, false, null), [slot, material]);
10569
+ pass.setBindGroup(
10570
+ 0,
10571
+ animation === null
10572
+ ? this.flatBindGroupFor(false, false, null)
10573
+ : this.animatedGroup(batch, animation),
10574
+ [slot, this.passOffset, material, this.drawViewOffset],
10575
+ );
9971
10576
  let index = 0;
9972
10577
  for (; index < mesh.vertexBuffers.length; index++) {
9973
10578
  pass.setVertexBuffer(index, mesh.vertexBuffers[index] as GPUBuffer);
9974
10579
  }
9975
10580
  pass.setVertexBuffer(index, instanceBuffer);
9976
- pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
10581
+ pass.setIndexBuffer(mesh.indexBuffer, mesh.indexFormat);
9977
10582
  if (cut === null) pass.drawIndexed(mesh.indexCount, count);
9978
10583
  else pass.drawIndexedIndirect(cut.args, 0);
9979
10584
  }
@@ -9995,6 +10600,7 @@ export class WebGPURenderer implements RendererApi {
9995
10600
  instanceBuffer,
9996
10601
  count,
9997
10602
  cut === null ? null : cut.args,
10603
+ animation === null ? null : batch,
9998
10604
  );
9999
10605
  }
10000
10606
  }
@@ -10207,6 +10813,12 @@ export class WebGPURenderer implements RendererApi {
10207
10813
  if (view === null) return false;
10208
10814
 
10209
10815
  this.lightViewProj.set(lightViewProj as Float32Array);
10816
+ /* The moving layer is read through the matrix it is drawn with: see `movingLightFor`. */
10817
+ if (dynamic) {
10818
+ this.movingLightViewProj.set(lightViewProj as Float32Array);
10819
+ this.movingLightKnown = true;
10820
+ }
10821
+ this.reopenViews();
10210
10822
  mat4.multiply(this.correctedLightViewProj, SHADOW_CLIP_CORRECTION, lightViewProj);
10211
10823
  this.shadowLayerIsPeel = peel;
10212
10824
  this.shadowLayerIsDynamic = dynamic;
@@ -10319,6 +10931,267 @@ export class WebGPURenderer implements RendererApi {
10319
10931
  casters(this.sceneCasterSink);
10320
10932
  }
10321
10933
 
10934
+ /* -- Static draws ---------------------------------------------------------------- */
10935
+
10936
+ /** See `Renderer.createStaticDraws`. Recorded on its first draw, into each view it is drawn in. */
10937
+ createStaticDraws(casters: ShadowCasters): StaticDrawsHandle {
10938
+ return new GpuStaticDraws(
10939
+ this.surface.device,
10940
+ captureStaticDraws(casters),
10941
+ this.fragment.passSize,
10942
+ FLAT_VIEW_SIZE,
10943
+ FLAT_VERT_SIZE,
10944
+ );
10945
+ }
10946
+
10947
+ /**
10948
+ * See `Renderer.drawStaticDraws`. One bundle executed, after a pass block and a view block are
10949
+ * written for this view; the list is recorded first where it has no recording that is current for
10950
+ * this target and starting state. See `staticBundles.ts`.
10951
+ */
10952
+ drawStaticDraws(draws: StaticDrawsHandle): void {
10953
+ if (this.surface.lost) return;
10954
+ const list = draws as GpuStaticDraws;
10955
+ if (list.disposed) {
10956
+ this.warnStaticDisposed();
10957
+ return;
10958
+ }
10959
+ if (!this.canDraw() || this.viewProj === null) return;
10960
+ /* A list's draws are rigid and wear no overlay: put back to none, as `bindMeshPass` does. */
10961
+ this.setSkinPalette(null);
10962
+ this.setMorphWeights(null);
10963
+ this.setCloth(null);
10964
+ this.setSurfaceOverlay(null);
10965
+ this.setMaterial(null);
10966
+ this.sceneCasterMaterial = undefined;
10967
+ /* A probe face gives its slots back after every face; a list replays there draw by draw. */
10968
+ if (this.probePassActive) {
10969
+ this.replayStaticImmediately(list);
10970
+ return;
10971
+ }
10972
+ /* The copy first: taking one may grow the list's rings, which no recording before can bind. */
10973
+ const copy = list.takeCopy(this.moverFrame);
10974
+ const current = this.staticRecordingFor(list);
10975
+ if (current === null || current.immediate) {
10976
+ this.replayStaticImmediately(list);
10977
+ return;
10978
+ }
10979
+ current.usedIn = this.moverFrame;
10980
+ list.writePass(copy, this.passInts);
10981
+ this.writeView(list.views, list.viewOffset(copy), this.viewProj);
10982
+ list.uploadCopy(copy);
10983
+
10984
+ const cache = current.cache;
10985
+ let frame = current.frameBundles[copy];
10986
+ if (frame === undefined || frame === null) {
10987
+ frame = encodeBundle(
10988
+ this.surface.device,
10989
+ {
10990
+ colorFormats: [cache.format],
10991
+ depthStencilFormat: DEPTH_FORMAT,
10992
+ sampleCount: cache.sampleCount,
10993
+ depthReadOnly: false,
10994
+ },
10995
+ current.frame,
10996
+ list.passOffset(copy),
10997
+ list.viewOffset(copy),
10998
+ 'static.frame',
10999
+ );
11000
+ current.frameBundles[copy] = frame;
11001
+ }
11002
+ const command = this.recordDraw(0, this.currentTarget());
11003
+ if (command !== null) {
11004
+ command.bundles = frame;
11005
+ command.pipeline = null;
11006
+ command.bindGroup = null;
11007
+ } else {
11008
+ this.openPass()?.executeBundles(frame);
11009
+ }
11010
+ const surface = this.reflectionSurface;
11011
+ if (current.surfaceKept && surface !== null && current.surface.taken > 0) {
11012
+ let halves = current.surfaceBundles[copy];
11013
+ if (halves === undefined || halves === null) {
11014
+ halves = encodeBundle(
11015
+ this.surface.device,
11016
+ {
11017
+ colorFormats: [null, REFLECTION_SURFACE_FORMAT, REFLECTION_SURFACE_FORMAT],
11018
+ depthStencilFormat: DEPTH_FORMAT,
11019
+ sampleCount: 1,
11020
+ depthReadOnly: true,
11021
+ },
11022
+ current.surface,
11023
+ list.passOffset(copy),
11024
+ list.viewOffset(copy),
11025
+ 'static.surface',
11026
+ );
11027
+ current.surfaceBundles[copy] = halves;
11028
+ }
11029
+ const kept = surface.take();
11030
+ kept.bundles = halves;
11031
+ }
11032
+ /* Counted as the draws would have been, so a frame reports the same on both backends. */
11033
+ for (let draw = 0; draw < current.frame.taken; draw++) this.drawBudget.ask();
11034
+ if (!this.staticJustRecorded) {
11035
+ for (let ask = 0; ask < current.materialAsks; ask++) this.materialBudget.ask();
11036
+ }
11037
+ this.staticJustRecorded = false;
11038
+ }
11039
+
11040
+ /** A list drawn the way `drawSceneCasters` draws, a draw at a time, for where a bundle cannot be. */
11041
+ private replayStaticImmediately(list: GpuStaticDraws): void {
11042
+ this.staticJustRecorded = false;
11043
+ replayStaticDraws(list, this.sceneCasterSink);
11044
+ this.setMaterial(null);
11045
+ this.sceneCasterMaterial = undefined;
11046
+ }
11047
+
11048
+ /** See `Renderer.disposeStaticDraws`. Its pins let go; its meshes and materials are the caller's. */
11049
+ disposeStaticDraws(draws: StaticDrawsHandle): void {
11050
+ const list = draws as GpuStaticDraws;
11051
+ if (list.disposed) return;
11052
+ list.dispose(this.unpinFor(list));
11053
+ }
11054
+
11055
+ /** The recording to replay `list` from here, made now if none is current; null if it cannot be. */
11056
+ private staticRecordingFor(list: GpuStaticDraws): StaticRecording | null {
11057
+ /* Geometry still arriving is not drawn yet, so a recording now would leave it out for good. */
11058
+ for (const entry of list.entries) {
11059
+ const mesh = entry.kind === 'mesh' ? entry.mesh : (entry.batch as GpuInstancedBatch).mesh;
11060
+ if (!mesh.complete) return null;
11061
+ }
11062
+ const cache = this.targetPipelines();
11063
+ const start = this.materialInts;
11064
+ const startHash = hashWords(start, start.length);
11065
+ const surfaceKept = this.reflectionSurfaceKeeps();
11066
+ let recording = list.find(cache, start, startHash, this.cutoutResolveStaged, surfaceKept);
11067
+ if (
11068
+ recording !== null &&
11069
+ recording.cacheVersion === cache.version &&
11070
+ recording.groupEpoch === this.flatGroupEpoch &&
11071
+ recording.storeEpoch === this.materialStore.epoch &&
11072
+ recording.ringEpoch === list.ringEpoch
11073
+ ) {
11074
+ return recording;
11075
+ }
11076
+ const unpin = this.unpinFor(list);
11077
+ if (recording === null) {
11078
+ recording = list.open(cache, start, startHash, this.cutoutResolveStaged, surfaceKept, unpin);
11079
+ } else {
11080
+ list.clear(recording, recording.storeEpoch === this.materialStore.epoch ? unpin : () => {});
11081
+ }
11082
+ this.recordStatic(list, recording);
11083
+ return recording;
11084
+ }
11085
+
11086
+ /** How a list's pins are let go: through the store, unless it has forgotten them since. */
11087
+ private unpinFor(list: GpuStaticDraws): (offset: number) => void {
11088
+ const epoch = this.materialStore.epoch;
11089
+ return (offset) => {
11090
+ if (this.materialStore.epoch === epoch && list.pinEpoch === epoch) {
11091
+ this.materialStore.unpin(offset);
11092
+ }
11093
+ };
11094
+ }
11095
+
11096
+ /**
11097
+ * Record `list` into `recording`: its entries through the scene-caster path, with the list's
11098
+ * rings and group cache swapped in for the frame's and the pass and view slots held at the list's
11099
+ * first copy, so every command the path records binds the list's buffers and offsets the bundle
11100
+ * can rewrite per copy. Everything swapped is put back, whatever the path throws.
11101
+ */
11102
+ private recordStatic(list: GpuStaticDraws, recording: StaticRecording): void {
11103
+ if (list.groupEpoch !== this.flatGroupEpoch) {
11104
+ list.groups = new Map();
11105
+ list.blank = null;
11106
+ list.groupEpoch = this.flatGroupEpoch;
11107
+ }
11108
+ list.drawRing.reset();
11109
+ const perDraw = this.perDraw;
11110
+ const passRing = this.passRing;
11111
+ const viewRing = this.viewRing;
11112
+ const groups = this.flatBindGroups;
11113
+ const blank = this.blankAlbedoBindGroup;
11114
+ const bindGroup = this.bindGroup;
11115
+ const passSlot = this.passBlock.slot;
11116
+ const viewSlot = this.viewBlock.slot;
11117
+ const lateSlot = this.lateViewBlock.slot;
11118
+ const asked = this.materialBudget.used;
11119
+ this.perDraw = list.drawRing;
11120
+ this.passRing = list.passes;
11121
+ this.viewRing = list.views;
11122
+ this.flatBindGroups = list.groups as typeof groups;
11123
+ /* The blank group over the list's rings, built as the frame's is: with no maps held. */
11124
+ list.blank ??= this.buildFlatBindGroup();
11125
+ this.blankAlbedoBindGroup = list.blank;
11126
+ this.bindGroup = this.flatGroupForMaps(null, null, null, null, null);
11127
+ this.passBlock.slot = list.passOffset(0);
11128
+ this.viewBlock.slot = list.viewOffset(0);
11129
+ this.lateViewBlock.slot = list.viewOffset(0);
11130
+ this.materials.dirty();
11131
+ this.staticRecording = recording;
11132
+ /* A recorded list keeps what it recorded, so every draw of it waits for its pipeline. */
11133
+ this.skipCompiling(false);
11134
+ try {
11135
+ this.sceneCasterMaterial = undefined;
11136
+ replayStaticDraws(list, this.sceneCasterSink);
11137
+ } finally {
11138
+ this.skipCompiling(this.quality.pipelineCompile === 'skip');
11139
+ this.staticRecording = null;
11140
+ this.perDraw = perDraw;
11141
+ this.passRing = passRing;
11142
+ this.viewRing = viewRing;
11143
+ this.flatBindGroups = groups;
11144
+ this.blankAlbedoBindGroup = blank;
11145
+ this.bindGroup = bindGroup;
11146
+ this.passBlock.slot = passSlot;
11147
+ this.viewBlock.slot = viewSlot;
11148
+ this.lateViewBlock.slot = lateSlot;
11149
+ this.materials.dirty();
11150
+ this.sceneCasterMaterial = undefined;
11151
+ this.setMaterial(null);
11152
+ }
11153
+ recording.materialAsks = this.materialBudget.used - asked;
11154
+ const store = this.materialStore;
11155
+ recording.cacheVersion = recording.cache.version;
11156
+ recording.groupEpoch = this.flatGroupEpoch;
11157
+ recording.storeEpoch = store.epoch;
11158
+ recording.ringEpoch = list.ringEpoch;
11159
+ /* A skin split into halves is replayed draw by draw, so nothing recorded is kept or pinned. */
11160
+ if (recording.immediate) {
11161
+ resetPool(recording.frame);
11162
+ resetPool(recording.surface);
11163
+ return;
11164
+ }
11165
+ /* Every material the commands bind, pinned once, so nothing displaces what a bundle bakes. */
11166
+ for (const pool of [recording.frame, recording.surface]) {
11167
+ for (let i = 0; i < pool.taken; i++) {
11168
+ const offset = (pool.commands[i] as DrawCommand).offsetC;
11169
+ if (!recording.pins.includes(offset)) {
11170
+ store.pin(offset);
11171
+ recording.pins.push(offset);
11172
+ }
11173
+ }
11174
+ }
11175
+ list.pinEpoch = store.epoch;
11176
+ list.uploadDraws();
11177
+ this.staticJustRecorded = true;
11178
+ }
11179
+
11180
+ /** The recording under way, which `recordDraw` and the side passes take their commands into. */
11181
+ private staticRecording: StaticRecording | null = null;
11182
+ /** Whether the replay about to count its material changes recorded them, and so counted them. */
11183
+ private staticJustRecorded = false;
11184
+
11185
+ /** A disposed list drawn, said once: the frame loop does not throw. */
11186
+ private warnStaticDisposed(): void {
11187
+ if (this.warnedStaticDisposed) return;
11188
+ this.warnedStaticDisposed = true;
11189
+ console.warn('drawStaticDraws: this list was disposed, and draws nothing.');
11190
+ }
11191
+ private warnedStaticDisposed = false;
11192
+ /** Bumped wherever the lit groups are dropped, which a static list's own groups follow. */
11193
+ private flatGroupEpoch = 0;
11194
+
10322
11195
  drawShadowCasters(casters: ShadowCasters): void {
10323
11196
  if (this.surface.lost || this.shadowPass === null) return;
10324
11197
  casters(this.casterSink);
@@ -10569,7 +11442,7 @@ export class WebGPURenderer implements RendererApi {
10569
11442
  for (let index = 0; index < geometry.vertexBuffers.length; index++) {
10570
11443
  pass.setVertexBuffer(index, geometry.vertexBuffers[index] as GPUBuffer);
10571
11444
  }
10572
- pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
11445
+ pass.setIndexBuffer(geometry.indexBuffer, geometry.indexFormat);
10573
11446
  pass.drawIndexed(geometry.indexCount);
10574
11447
  },
10575
11448
  instanced: (batch, data, material, glass) => {
@@ -10614,7 +11487,7 @@ export class WebGPURenderer implements RendererApi {
10614
11487
  pass.setVertexBuffer(index, geometry.vertexBuffers[index] as GPUBuffer);
10615
11488
  }
10616
11489
  pass.setVertexBuffer(index, gpuBatch.buffer);
10617
- pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
11490
+ pass.setIndexBuffer(geometry.indexBuffer, geometry.indexFormat);
10618
11491
  pass.drawIndexed(geometry.indexCount, count);
10619
11492
  },
10620
11493
  skinnedMesh: (mesh, model, palette, _material, glass) => {
@@ -10650,7 +11523,7 @@ export class WebGPURenderer implements RendererApi {
10650
11523
  for (let index = 0; index < geometry.vertexBuffers.length; index++) {
10651
11524
  pass.setVertexBuffer(index, geometry.vertexBuffers[index] as GPUBuffer);
10652
11525
  }
10653
- pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
11526
+ pass.setIndexBuffer(geometry.indexBuffer, geometry.indexFormat);
10654
11527
  pass.drawIndexed(geometry.indexCount);
10655
11528
  },
10656
11529
  };
@@ -10664,7 +11537,7 @@ export class WebGPURenderer implements RendererApi {
10664
11537
  }
10665
11538
  this.shadowDraws.flush();
10666
11539
  this.scatterDepthDraws.flush();
10667
- this.surface.device.queue.submit([this.shadowEncoder.finish()]);
11540
+ this.submit([this.shadowEncoder.finish()]);
10668
11541
  /* A growth's old texture, copied out of by what was just submitted and needed no longer. */
10669
11542
  this.retiredSunTexture?.destroy();
10670
11543
  this.retiredSunTexture = null;
@@ -10954,7 +11827,7 @@ export class WebGPURenderer implements RendererApi {
10954
11827
  * its own slot.
10955
11828
  */
10956
11829
  this.pointShadowArray?.flush();
10957
- this.surface.device.queue.submit([this.shadowEncoder.finish()]);
11830
+ this.submit([this.shadowEncoder.finish()]);
10958
11831
  this.shadowEncoder = null;
10959
11832
  this.shadowPass = null;
10960
11833
  this.gpuTimer.end();
@@ -11161,7 +12034,9 @@ export class WebGPURenderer implements RendererApi {
11161
12034
  this.bindGroupLayout,
11162
12035
  this.variant,
11163
12036
  this.perDraw.buffer,
11164
- this.perFrame.buffer,
12037
+ this.passRing.buffer,
12038
+ this.materialStore.buffer,
12039
+ this.viewRing.buffer,
11165
12040
  this.flatTextures,
11166
12041
  );
11167
12042
  }
@@ -11206,11 +12081,11 @@ export class WebGPURenderer implements RendererApi {
11206
12081
  /**
11207
12082
  * The bind group a draw uses, skinned or not.
11208
12083
  *
11209
- * **The skinned twin is cached against the two things that can invalidate it**, rather than
11210
- * against a flag somebody has to remember to clear at six call sites: the unskinned group it was
11211
- * built beside — which is rebuilt whenever the material or a shadow slot moves — and the palette
11212
- * view, which changes only when the texture is reallocated. Comparing object identity means a
11213
- * missed invalidation is not possible rather than merely unlikely, and it is why
12084
+ * **The skinned twin is kept by palette slot and by the unskinned group it was built beside**,
12085
+ * which is one a material, and compared against the views it holds — the palette's, which
12086
+ * changes only when the slot's texture grows, the morph deltas and the cloth's — rather than
12087
+ * against a flag somebody has to remember to clear at six call sites. Comparing object identity
12088
+ * means a missed invalidation is not possible rather than merely unlikely, and it is why
11214
12089
  * `SkinPaletteTexture.view()` caches: a fresh view every call never compares equal and would
11215
12090
  * rebuild this every draw.
11216
12091
  */
@@ -11227,35 +12102,45 @@ export class WebGPURenderer implements RendererApi {
11227
12102
  const at = skinned ? this.skinPaletteSlot + 1 : 0;
11228
12103
  /* A skinned group carries the cloth's views, or the stand-in's: see `clothViews`. */
11229
12104
  const cloth = skinned ? this.clothViews() : null;
12105
+ let byMaterial = this.skinnedGroups[at];
12106
+ if (byMaterial === undefined) {
12107
+ byMaterial = new Map();
12108
+ this.skinnedGroups[at] = byMaterial;
12109
+ }
12110
+ let twin = byMaterial.get(base);
11230
12111
  if (
11231
- (this.skinnedGroups[at] ?? null) === null ||
11232
- this.skinnedGroupsBeside[at] !== base ||
11233
- this.skinnedGroupsPalette[at] !== palette ||
11234
- this.skinnedGroupsDeltas[at] !== deltas ||
11235
- this.skinnedGroupsCloth[at] !== (cloth?.particles ?? null) ||
11236
- this.skinnedGroupsClothBinding[at] !== (cloth?.binding ?? null)
12112
+ twin === undefined ||
12113
+ twin.palette !== palette ||
12114
+ twin.deltas !== deltas ||
12115
+ twin.cloth !== (cloth?.particles ?? null) ||
12116
+ twin.clothBinding !== (cloth?.binding ?? null)
11237
12117
  ) {
11238
12118
  /* Built from the live material, with the overlay's atlas where `base` is its twin. */
11239
12119
  this.overlayBinding = base !== this.bindGroup;
11240
- this.skinnedGroups[at] = createFlatBindGroup(
12120
+ const group = createFlatBindGroup(
11241
12121
  this.surface.device,
11242
12122
  this.flatLayoutFor(skinned, morphed),
11243
12123
  this.variant,
11244
12124
  this.perDraw.buffer,
11245
- this.perFrame.buffer,
12125
+ this.passRing.buffer,
12126
+ this.materialStore.buffer,
12127
+ this.viewRing.buffer,
11246
12128
  this.flatTextures,
11247
12129
  palette,
11248
12130
  deltas,
11249
12131
  cloth,
11250
12132
  );
11251
12133
  this.overlayBinding = false;
11252
- this.skinnedGroupsBeside[at] = base;
11253
- this.skinnedGroupsPalette[at] = palette;
11254
- this.skinnedGroupsDeltas[at] = deltas;
11255
- this.skinnedGroupsCloth[at] = cloth?.particles ?? null;
11256
- this.skinnedGroupsClothBinding[at] = cloth?.binding ?? null;
12134
+ twin = {
12135
+ group,
12136
+ palette,
12137
+ deltas,
12138
+ cloth: cloth?.particles ?? null,
12139
+ clothBinding: cloth?.binding ?? null,
12140
+ };
12141
+ byMaterial.set(base, twin);
11257
12142
  }
11258
- return this.skinnedGroups[at] as GPUBindGroup;
12143
+ return twin.group;
11259
12144
  }
11260
12145
 
11261
12146
  /** The layout matching a pair of vertex flags. A pipeline and its bind group must agree. */
@@ -11293,8 +12178,14 @@ export class WebGPURenderer implements RendererApi {
11293
12178
  if (this.drawBudget.dropped > 0) {
11294
12179
  grew = this.perDraw.growTo(roomFor(this.drawBudget.used)) || grew;
11295
12180
  }
11296
- if (this.materialBudget.dropped > 0) {
11297
- grew = this.perFrame.growTo(roomFor(this.materialBudget.used)) || grew;
12181
+ if (this.materialsRefused > 0) {
12182
+ grew = this.materialStore.growTo(roomFor(this.materialBudget.used)) || grew;
12183
+ }
12184
+ if (this.passesRefused > 0) {
12185
+ grew = this.passRing.growTo(roomFor(this.passesAsked)) || grew;
12186
+ }
12187
+ if (this.viewsRefused > 0) {
12188
+ grew = this.viewRing.growTo(roomFor(this.viewsAsked)) || grew;
11298
12189
  }
11299
12190
  if (grew) this.rebuildFlatGroupsForNewRings();
11300
12191
  }
@@ -11315,9 +12206,10 @@ export class WebGPURenderer implements RendererApi {
11315
12206
  */
11316
12207
  private rebuildFlatGroupsForNewRings(): void {
11317
12208
  this.flatBindGroups.clear();
12209
+ this.flatGroupEpoch += 1;
11318
12210
  this.overlayGroups.clear();
11319
- this.flatEnvironment = this.flatTextures('uEnvironment').view;
11320
12211
  this.skinnedGroups.length = 0;
12212
+ this.flatEnvironment = this.flatTextures('uEnvironment').view;
11321
12213
 
11322
12214
  const albedo = this.albedo;
11323
12215
  const normalMap = this.normalMap;
@@ -11345,6 +12237,7 @@ export class WebGPURenderer implements RendererApi {
11345
12237
  );
11346
12238
  /* The open material lives in a slot of a buffer that no longer exists. */
11347
12239
  this.materials.dirty();
12240
+ this.reopenPass();
11348
12241
  }
11349
12242
 
11350
12243
  /**
@@ -11379,7 +12272,9 @@ export class WebGPURenderer implements RendererApi {
11379
12272
  */
11380
12273
  private rebuildFlatBindGroup(): void {
11381
12274
  this.flatBindGroups.clear();
12275
+ this.flatGroupEpoch += 1;
11382
12276
  this.overlayGroups.clear();
12277
+ this.skinnedGroups.length = 0;
11383
12278
  this.flatEnvironment = this.flatTextures('uEnvironment').view;
11384
12279
  if (
11385
12280
  this.albedo === null &&
@@ -13127,6 +14022,10 @@ export class WebGPURenderer implements RendererApi {
13127
14022
  /* Uncorrected, for the reason the shaft's own copy of this line carries: the shader does its
13128
14023
  own `* 0.5 + 0.5`, and correcting the matrix as well doubles it. */
13129
14024
  f.set(env?.lightViewProj ?? this.mediumIdentity, at('uLightViewProj'));
14025
+ f.set(
14026
+ (env === null ? this.mediumIdentity : this.movingLightFor(env)) as Float32Array,
14027
+ at('uMovingLightViewProj'),
14028
+ );
13130
14029
  ints[at('uPeeledShadowEnabled')] = this.peelView === null || !this.peelFilled ? 0 : 1;
13131
14030
  device.queue.writeBuffer(this.mediumUniforms, 0, this.mediumStaging);
13132
14031
 
@@ -13618,17 +14517,20 @@ export class WebGPURenderer implements RendererApi {
13618
14517
  instanceBuffer: GPUBuffer | null,
13619
14518
  instances: number,
13620
14519
  indirect: GPUBuffer | null,
14520
+ /** A batch playing a bone animation, whose surface half moves as its draw does. */
14521
+ animated: GpuInstancedBatch | null = null,
13621
14522
  ): void {
13622
14523
  const surface = this.reflectionSurface;
13623
14524
  const present = this.meshPresent.get(base);
13624
14525
  if (surface === null || present === undefined) return;
14526
+ const animation = animated === null ? null : this.animationViews(animated);
13625
14527
  const key = `${frameKey}|rs`;
13626
14528
  const pipeline =
13627
14529
  this.pipelines.peek(key) ??
13628
14530
  flatPipeline(
13629
14531
  this.pipelines,
13630
14532
  this.surface.device,
13631
- this.flatLayoutFor(skinned, morphed),
14533
+ animation === null ? this.flatLayoutFor(skinned, morphed) : this.animatedBindGroupLayout,
13632
14534
  this.variant,
13633
14535
  key,
13634
14536
  present,
@@ -13646,13 +14548,23 @@ export class WebGPURenderer implements RendererApi {
13646
14548
  modelKind,
13647
14549
  'whole',
13648
14550
  true,
14551
+ animation !== null,
13649
14552
  );
13650
- const command = surface.take();
14553
+ /* Still compiling where draws skip: the probe stands in for this surface until it lands. */
14554
+ if (pipeline === null) return;
14555
+ /* A static list's surface halves go into its own pool, beside its draws. */
14556
+ const command =
14557
+ this.staticRecording === null ? surface.take() : takeRecorded(this.staticRecording.surface);
13651
14558
  command.pipeline = pipeline;
13652
- command.bindGroup = this.flatBindGroupFor(skinned, morphed, deltas);
14559
+ command.bindGroup =
14560
+ animated === null || animation === null
14561
+ ? this.flatBindGroupFor(skinned, morphed, deltas)
14562
+ : this.animatedGroup(animated, animation);
13653
14563
  command.offsetA = slot;
13654
- command.offsetB = material;
13655
- command.offsetCount = 2;
14564
+ command.offsetB = this.passOffset;
14565
+ command.offsetC = material;
14566
+ command.offsetD = this.drawViewOffset;
14567
+ command.offsetCount = 4;
13656
14568
  let index = 0;
13657
14569
  for (; index < mesh.vertexBuffers.length; index++) {
13658
14570
  command.vertexBuffers[index] = mesh.vertexBuffers[index] as GPUBuffer;
@@ -13660,6 +14572,7 @@ export class WebGPURenderer implements RendererApi {
13660
14572
  if (instanceBuffer !== null) command.vertexBuffers[index++] = instanceBuffer;
13661
14573
  command.vertexCount = index;
13662
14574
  command.indexBuffer = mesh.indexBuffer;
14575
+ command.indexFormat = mesh.indexFormat;
13663
14576
  command.indexed = true;
13664
14577
  command.count = mesh.indexCount;
13665
14578
  command.instances = instances;
@@ -13672,6 +14585,78 @@ export class WebGPURenderer implements RendererApi {
13672
14585
  * light alone and its colour alone into the blur's targets and test for the depth its frame half
13673
14586
  * wrote.
13674
14587
  */
14588
+ /**
14589
+ * Whether a split skin draw's two kept halves have pipelines, starting the compile of either that
14590
+ * has not: a skin drawn without them would lose its diffuse light for the frames they take, so a
14591
+ * split skin draws all three halves or none. See `PipelineCache.lit`.
14592
+ */
14593
+ private skinHalvesReady(
14594
+ frameKey: string,
14595
+ base: string,
14596
+ skinned: boolean,
14597
+ morphed: boolean,
14598
+ layer: number,
14599
+ eight: boolean,
14600
+ clothed: boolean,
14601
+ ): boolean {
14602
+ let ready = true;
14603
+ for (const half of SKIN_KEPT_HALVES) {
14604
+ const pipeline = this.skinHalfPipeline(
14605
+ half,
14606
+ frameKey,
14607
+ base,
14608
+ skinned,
14609
+ morphed,
14610
+ layer,
14611
+ eight,
14612
+ clothed,
14613
+ );
14614
+ if (pipeline === null) ready = false;
14615
+ }
14616
+ return ready;
14617
+ }
14618
+
14619
+ /** One kept half's pipeline, or none where it is still compiling: see `PipelineCache.lit`. */
14620
+ private skinHalfPipeline(
14621
+ half: (typeof SKIN_KEPT_HALVES)[number],
14622
+ frameKey: string,
14623
+ base: string,
14624
+ skinned: boolean,
14625
+ morphed: boolean,
14626
+ layer: number,
14627
+ eight: boolean,
14628
+ clothed: boolean,
14629
+ ): GPURenderPipeline | null {
14630
+ const pipelines = this.skinPipelines;
14631
+ const present = this.meshPresent.get(base);
14632
+ if (pipelines === null || present === undefined) return null;
14633
+ const key = `${frameKey}|${half === 'diffuse' ? 'sd' : 'sa'}`;
14634
+ return (
14635
+ pipelines.peek(key) ??
14636
+ flatPipeline(
14637
+ pipelines,
14638
+ this.surface.device,
14639
+ this.flatLayoutFor(skinned, morphed),
14640
+ this.variant,
14641
+ key,
14642
+ present,
14643
+ false,
14644
+ skinned,
14645
+ morphed,
14646
+ true,
14647
+ layer,
14648
+ false,
14649
+ 'none',
14650
+ this.materialDoubleSided,
14651
+ false,
14652
+ eight,
14653
+ clothed,
14654
+ 'skin',
14655
+ half,
14656
+ )
14657
+ );
14658
+ }
14659
+
13675
14660
  private keepSkinDiffuse(
13676
14661
  mesh: GpuMesh,
13677
14662
  frameKey: string,
@@ -13685,6 +14670,11 @@ export class WebGPURenderer implements RendererApi {
13685
14670
  slot: number,
13686
14671
  material: number,
13687
14672
  ): void {
14673
+ /* A skin's halves are not bundled: a static list holding one replays draw by draw. */
14674
+ if (this.staticRecording !== null) {
14675
+ this.staticRecording.immediate = true;
14676
+ return;
14677
+ }
13688
14678
  const skin = this.skinScatter;
13689
14679
  const pipelines = this.skinPipelines;
13690
14680
  const present = this.meshPresent.get(base);
@@ -13692,41 +14682,32 @@ export class WebGPURenderer implements RendererApi {
13692
14682
  const bindGroup = this.flatBindGroupFor(skinned, morphed, deltas);
13693
14683
  /* The light alone, which the blur spreads, then the colour alone, which it is multiplied by. */
13694
14684
  for (const half of SKIN_KEPT_HALVES) {
13695
- const key = `${frameKey}|${half === 'diffuse' ? 'sd' : 'sa'}`;
13696
- const pipeline =
13697
- pipelines.peek(key) ??
13698
- flatPipeline(
13699
- pipelines,
13700
- this.surface.device,
13701
- this.flatLayoutFor(skinned, morphed),
13702
- this.variant,
13703
- key,
13704
- present,
13705
- false,
13706
- skinned,
13707
- morphed,
13708
- true,
13709
- layer,
13710
- false,
13711
- 'none',
13712
- this.materialDoubleSided,
13713
- false,
13714
- eight,
13715
- clothed,
13716
- 'skin',
13717
- half,
13718
- );
14685
+ const pipeline = this.skinHalfPipeline(
14686
+ half,
14687
+ frameKey,
14688
+ base,
14689
+ skinned,
14690
+ morphed,
14691
+ layer,
14692
+ eight,
14693
+ clothed,
14694
+ );
14695
+ /* Both landed before the draw was let through: see `skinHalvesReady`. */
14696
+ if (pipeline === null) return;
13719
14697
  const command = half === 'diffuse' ? skin.take() : skin.takeAlbedo();
13720
14698
  command.pipeline = pipeline;
13721
14699
  command.bindGroup = bindGroup;
13722
14700
  command.offsetA = slot;
13723
- command.offsetB = material;
13724
- command.offsetCount = 2;
14701
+ command.offsetB = this.passOffset;
14702
+ command.offsetC = material;
14703
+ command.offsetD = this.drawViewOffset;
14704
+ command.offsetCount = 4;
13725
14705
  for (let index = 0; index < mesh.vertexBuffers.length; index++) {
13726
14706
  command.vertexBuffers[index] = mesh.vertexBuffers[index] as GPUBuffer;
13727
14707
  }
13728
14708
  command.vertexCount = mesh.vertexBuffers.length;
13729
14709
  command.indexBuffer = mesh.indexBuffer;
14710
+ command.indexFormat = mesh.indexFormat;
13730
14711
  command.indexed = true;
13731
14712
  command.count = mesh.indexCount;
13732
14713
  command.instances = 1;
@@ -14167,6 +15148,8 @@ export class WebGPURenderer implements RendererApi {
14167
15148
  * site reads as one `if` either way.
14168
15149
  */
14169
15150
  private recordDraw(reads: number, writes: number): DrawCommand | null {
15151
+ /* A static list recording takes its draws into its own pool, never the frame graph's. */
15152
+ if (this.staticRecording !== null) return takeRecorded(this.staticRecording.frame);
14170
15153
  /* The order-independent replay owns its pass and issues directly into it; a recorded command
14171
15154
  would be replayed into the frame's passes instead, which is not where these belong. */
14172
15155
  if (this.oitEncoder !== null) return null;
@@ -14178,6 +15161,7 @@ export class WebGPURenderer implements RendererApi {
14178
15161
  command.offsetCount = 0;
14179
15162
  command.vertexCount = 0;
14180
15163
  command.indexBuffer = null;
15164
+ command.indexFormat = 'uint32';
14181
15165
  command.indexed = false;
14182
15166
  command.instances = 1;
14183
15167
  command.indirect = null;
@@ -14209,9 +15193,23 @@ export class WebGPURenderer implements RendererApi {
14209
15193
 
14210
15194
  /** One recorded draw, issued into an open pass. Shared by the graph's replay and `runLate`. */
14211
15195
  private issueCommand(pass: GPURenderPassEncoder, command: DrawCommand): void {
15196
+ /* A static list, recorded once: one call, and the pass's bindings are its to set again. */
15197
+ if (command.bundles !== null) {
15198
+ pass.executeBundles(command.bundles);
15199
+ return;
15200
+ }
14212
15201
  if (command.pipeline === null || command.bindGroup === null) return;
14213
15202
  pass.setPipeline(command.pipeline);
14214
- if (command.offsetCount === 2) {
15203
+ if (command.offsetCount === 4) {
15204
+ pass.setBindGroup(0, command.bindGroup, [
15205
+ command.offsetA,
15206
+ command.offsetB,
15207
+ command.offsetC,
15208
+ command.offsetD,
15209
+ ]);
15210
+ } else if (command.offsetCount === 3) {
15211
+ pass.setBindGroup(0, command.bindGroup, [command.offsetA, command.offsetB, command.offsetC]);
15212
+ } else if (command.offsetCount === 2) {
14215
15213
  pass.setBindGroup(0, command.bindGroup, [command.offsetA, command.offsetB]);
14216
15214
  } else if (command.offsetCount === 1) {
14217
15215
  pass.setBindGroup(0, command.bindGroup, [command.offsetA]);
@@ -14223,7 +15221,7 @@ export class WebGPURenderer implements RendererApi {
14223
15221
  if (buffer != null) pass.setVertexBuffer(i, buffer);
14224
15222
  }
14225
15223
  if (command.indexed && command.indexBuffer !== null) {
14226
- pass.setIndexBuffer(command.indexBuffer, 'uint32');
15224
+ pass.setIndexBuffer(command.indexBuffer, command.indexFormat);
14227
15225
  if (command.indirect !== null) pass.drawIndexedIndirect(command.indirect, 0);
14228
15226
  else pass.drawIndexed(command.count, command.instances);
14229
15227
  } else {
@@ -14473,7 +15471,7 @@ export class WebGPURenderer implements RendererApi {
14473
15471
  this.flushRings();
14474
15472
  this.pass?.end();
14475
15473
  this.pass = null;
14476
- if (!this.surface.lost) this.surface.device.queue.submit([this.encoder.finish()]);
15474
+ if (!this.surface.lost) this.submit([this.encoder.finish()]);
14477
15475
  this.encoder = null;
14478
15476
  }
14479
15477
 
@@ -14533,7 +15531,7 @@ export class WebGPURenderer implements RendererApi {
14533
15531
  * there would have nothing to ride on. See `GpuTimestamps.resolve`.
14534
15532
  */
14535
15533
  this.gpuTimer.resolve(this.encoder);
14536
- this.surface.device.queue.submit([this.encoder.finish()]);
15534
+ this.submit([this.encoder.finish()]);
14537
15535
  this.encoder = null;
14538
15536
  this.framePresented = true;
14539
15537
  /* Latched rather than assigned: `framePresented` goes back to false every `beginFrame`. */
@@ -15275,7 +16273,7 @@ export class WebGPURenderer implements RendererApi {
15275
16273
  definition.dispatch(this.computeContext as ComputeContext);
15276
16274
  pass.end();
15277
16275
  this.flushRings();
15278
- device.queue.submit([encoder.finish()]);
16276
+ this.submit([encoder.finish()]);
15279
16277
  }
15280
16278
 
15281
16279
  /** Let go of a definition. A handle kept past this dispatches nothing; see `ComputeHandle`. */
@@ -16434,7 +17432,7 @@ export class WebGPURenderer implements RendererApi {
16434
17432
  .end();
16435
17433
  }
16436
17434
  }
16437
- device.queue.submit([clear.finish()]);
17435
+ this.submit([clear.finish()]);
16438
17436
  }
16439
17437
 
16440
17438
  /**
@@ -16665,7 +17663,7 @@ export class WebGPURenderer implements RendererApi {
16665
17663
  const whole = endFace === PROBE_FACES.length;
16666
17664
  if (whole) this.buildProbeChain(encoder, probe, this.probeSweeps?.writeLayer(layer) ?? layer);
16667
17665
  this.flushRings();
16668
- device.queue.submit([encoder.finish()]);
17666
+ this.submit([encoder.finish()]);
16669
17667
  this.rewindRings(skinMark);
16670
17668
  this.encoderEpoch = frameEpoch;
16671
17669
 
@@ -16675,6 +17673,7 @@ export class WebGPURenderer implements RendererApi {
16675
17673
  this.perFrameInts[this.materialField('uOutputTransform')] = heldGrade;
16676
17674
  this.perFrameFloats[this.materialField('uOutputExposure')] = heldExposure;
16677
17675
  this.materials.dirty();
17676
+ this.reopenPass();
16678
17677
  this.probePassActive = false;
16679
17678
  this.probeBounce = false;
16680
17679
  this.rebindEnvironment();
@@ -16732,7 +17731,7 @@ export class WebGPURenderer implements RendererApi {
16732
17731
  this.uploadProbeFaces(probe, image);
16733
17732
  const encoder = device.createCommandEncoder({ label: 'probe.environmentImage' });
16734
17733
  this.buildProbeChain(encoder, probe, 0);
16735
- device.queue.submit([encoder.finish()]);
17734
+ this.submit([encoder.finish()]);
16736
17735
 
16737
17736
  this.markProbeFilled(0);
16738
17737
  this.probeAmbient = options?.irradiance ?? true;
@@ -16770,7 +17769,7 @@ export class WebGPURenderer implements RendererApi {
16770
17769
  const { device } = this.surface;
16771
17770
  const encoder = device.createCommandEncoder({ label: 'probe.layerImage' });
16772
17771
  this.buildProbeChain(encoder, probe, this.probeSweeps?.writeLayer(layer) ?? layer);
16773
- device.queue.submit([encoder.finish()]);
17772
+ this.submit([encoder.finish()]);
16774
17773
  this.markProbeFilled(layer);
16775
17774
  this.probeAmbient = options?.irradiance ?? true;
16776
17775
  this.rebuildFlatBindGroup();