@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.js';
2
+ import { markPacking, markPackingOfKey, packingKey } from './vertexFormats.js';
1
3
  import { mat4 } from 'gl-matrix';
2
4
  import { probeFaceRange } from '../../reflectionProbe.js';
3
5
  import { TEMPORAL_FLICKER, TEMPORAL_HISTORY_BLEND, TemporalHistory, flickerRule, jitterOffset, jitterProjection, } from '../../temporalAa.js';
@@ -60,8 +62,11 @@ import { gpuCompressedFormats } from './compressedUpload.js';
60
62
  import { GpuClothBinding, GpuClothParticles, clothStandIn } from './clothTextures.js';
61
63
  import { MODEL_PARAM_FLOATS, eyeAxisInWorld, packModel, skinProfileDistance, } from '../../surfaceModel.js';
62
64
  import { OVERLAY_FLOATS, overlayLays, packSurfaceOverlay, } from '../../surfaceOverlay.js';
65
+ import { PROJECTION_FLOATS, packSurfaceProjection } from '../../surfaceProjection.js';
66
+ import { orthographicDepthSpan } from '../../lightMatrix.js';
67
+ import { LAYER_FLOATS, packSurfaceLayers } from '../../surfaceLayers.js';
63
68
  import { frameReflectionsRefused, resolveFrameReflections, } from '../../frameReflections.js';
64
- import { ReflectionSurfacePass } from './reflectionSurfacePass.js';
69
+ import { REFLECTION_SURFACE_FORMAT, ReflectionSurfacePass } from './reflectionSurfacePass.js';
65
70
  import { validateClothBinding } from '../../clothBindingData.js';
66
71
  import { createSdfTextBindGroup, createSdfTextBindGroupLayout, GpuSdfText, sdfTextPipeline, SDF_TEXT_FRAG_FIELDS, SDF_TEXT_FRAG_SIZE, SDF_TEXT_VERT_FIELDS, SDF_TEXT_VERT_SIZE, } from './sdfTextPass.js';
67
72
  import { createFilmBindGroup, createFilmBindGroupLayout, filmPipeline, FILM_FRAG_FIELDS, FILM_FRAG_SIZE, FILM_VERT_FIELDS, FILM_VERT_SIZE, } from './filmPass.js';
@@ -97,10 +102,15 @@ import { VIEW_MODEL_DEPTH_SHARE, viewModelDepthRange } from '../../viewModel.js'
97
102
  import { GlassCasterList } from '../../glassCasters.js';
98
103
  import { GpuGlassTint } from './glassTintPass.js';
99
104
  import { SUN_DYNAMIC_LAYER, SUN_GLASS_MOVING_LAYER, SUN_GLASS_STATIC_LAYER, SUN_PEELED_LAYER, SUN_STATIC_LAYER, } from '../../shadowMap.js';
100
- import { VERTEX_LAYOUT, createGpuMesh, createGpuMeshIncremental } from './buffers.js';
105
+ import { VERTEX_LAYOUT, createGpuMesh, createGpuMeshIncremental, } from './buffers.js';
101
106
  import { SkinPaletteRing } from './skinPaletteRing.js';
102
- import { DEPTH_FORMAT, FLAT_VERT_FIELDS, FLAT_VERT_SIZE, createFlatBindGroup, createFlatBindGroupLayout, flatFragmentBindings, flatVariant, flatVertexBindings, flatPipeline, flatPipelineAsync, probeStageCeilings, } from './flatPass.js';
103
- import { DEPTH_FRAG_BINDING, DEPTH_FRAG_SIZE, DEPTH_FRAG_WGSL, DEPTH_PREVIOUS_BINDING, DEPTH_CUTOUT_VERT_FIELDS, DEPTH_CUTOUT_VERT_SIZE, DEPTH_INSTANCED_CUTOUT_VERT_FIELDS, DEPTH_VERT_FIELDS, DEPTH_VERT_SIZE, SHADOW_FORMAT, createDepthBindGroup, createDepthBindGroupLayout, depthPipeline, } from './depthPass.js';
107
+ import { DEPTH_FORMAT, FLAT_ANIMATED_VERT_FIELDS, FLAT_VERT_FIELDS, FLAT_VERT_SIZE, FLAT_VIEW_FIELDS, FLAT_VIEW_SIZE, createFlatBindGroup, createFlatBindGroupLayout, flatFragmentScratch, flatVariant, flatVertexBindings, flatPipeline, flatPipelineAsync, probeStageCeilings, } from './flatPass.js';
108
+ import { GpuBoneAnimation } from './boneAnimations.js';
109
+ import { packBoneAnimation } from '../../boneAnimation.js';
110
+ import { MaterialStore, hashWords } from './materialStore.js';
111
+ import { GpuStaticDraws, encodeBundle, takeRecorded } from './staticBundles.js';
112
+ import { captureStaticDraws, replayStaticDraws } from '../../staticDraws.js';
113
+ import { DEPTH_FRAG_BINDING, DEPTH_FRAG_SIZE, DEPTH_FRAG_WGSL, DEPTH_PREVIOUS_BINDING, DEPTH_CUTOUT_VERT_FIELDS, DEPTH_CUTOUT_VERT_SIZE, DEPTH_INSTANCED_ANIMATED_CUTOUT_VERT_FIELDS, DEPTH_INSTANCED_ANIMATED_VERT_FIELDS, DEPTH_INSTANCED_CUTOUT_VERT_FIELDS, DEPTH_VERT_FIELDS, DEPTH_VERT_SIZE, SHADOW_FORMAT, createDepthBindGroup, createDepthBindGroupLayout, depthPipeline, } from './depthPass.js';
104
114
  import { PipelineCache } from './pipelineCache.js';
105
115
  import { SKIN_TARGET_FORMAT, SkinScatterPass } from './skinScatterPass.js';
106
116
  import { SKY_FIELDS, SKY_UNIFORM_SIZE, createSkyBindGroup, createSkyBindGroupLayout, skyPipeline, } from './skyPass.js';
@@ -359,6 +369,13 @@ const MAX_WATER_BODIES_PER_FRAME = 16;
359
369
  * numbers, because that is precisely the defect the ring exists to end.
360
370
  */
361
371
  const MAX_BATCHES_PER_FRAME = 8;
372
+ /**
373
+ * Mesh passes a frame may open before the ring grows: the frame's own, a mirror's, a capture's, and
374
+ * a probe face's, which gives its slot back once submitted. A frame past it loses the draws of the
375
+ * passes that found no slot, counted on the `materials` line, and the ring is grown for the next.
376
+ * Each slot is the pass's block, 3,328 to 6,656 bytes by variant, so the ring is 213 to 426 KB.
377
+ */
378
+ const PASSES_PER_FRAME = 64;
362
379
  /** Buffer usage flags, from the specification. See `buffers.ts` for why they are not read off the global. */
363
380
  const USAGE_UNIFORM_DST = 0x0040 | 0x0008;
364
381
  /**
@@ -845,7 +862,21 @@ export class WebGPURenderer {
845
862
  * flight at the time, so a consumer that builds more meshes later can wait again.
846
863
  */
847
864
  async ready() {
848
- await Promise.all(this.pipelineTargets.map((cache) => cache.ready()));
865
+ await Promise.all(this.pipelineCaches().map((cache) => cache.ready()));
866
+ }
867
+ /** Every distinct cache a lit draw reaches: the frame's, the canvas's, the late pass's, skin's. */
868
+ pipelineCaches() {
869
+ const caches = [...this.pipelineTargets];
870
+ for (const cache of [this.latePipelines, this.skinPipelines]) {
871
+ if (cache !== null && !caches.includes(cache))
872
+ caches.push(cache);
873
+ }
874
+ return caches;
875
+ }
876
+ /** Whether a lit draw whose pipeline is not built is left out while it compiles. */
877
+ skipCompiling(skips) {
878
+ for (const cache of this.pipelineCaches())
879
+ cache.skipsCompiling = skips;
849
880
  }
850
881
  presentedFrameCount = 0;
851
882
  /**
@@ -859,6 +890,8 @@ export class WebGPURenderer {
859
890
  skinnedBindGroupLayout;
860
891
  morphedBindGroupLayout;
861
892
  bothBindGroupLayout;
893
+ /** The instanced layout of a batch playing a bone animation. See `animatedGroup`. */
894
+ animatedBindGroupLayout;
862
895
  /** The morph weights the following draws use, or null for none. */
863
896
  morphWeights = null;
864
897
  /**
@@ -871,14 +904,15 @@ export class WebGPURenderer {
871
904
  * group per character per frame — an allocation in the frame loop, which `AGENTS.md` forbids.
872
905
  * Keyed by slot instead, the groups are built once and reused for the life of the renderer.
873
906
  */
907
+ /*
908
+ * **And by material within a slot**, keyed by the unskinned group each was built beside. One
909
+ * group a slot was rebuilt whenever the material beside it changed, so a character drawn in
910
+ * thirty-odd sections, each its own material, built one a section a frame — about ninety a frame
911
+ * for two characters, reported by a consumer. A slot's map holds one entry a material its rigs
912
+ * are drawn in, and is emptied wherever the unskinned groups are, whose identities it is keyed
913
+ * by.
914
+ */
874
915
  skinnedGroups = [];
875
- /** The unskinned group each was built beside, for identity-based invalidation. */
876
- skinnedGroupsBeside = [];
877
- skinnedGroupsPalette = [];
878
- skinnedGroupsDeltas = [];
879
- /** The cloth views each skinned twin was built with, compared by identity like the palette. */
880
- skinnedGroupsCloth = [];
881
- skinnedGroupsClothBinding = [];
882
916
  /** The cloth the next skinned draws are placed by, or null. See `setCloth`. */
883
917
  cloth = null;
884
918
  clothStandCache = null;
@@ -946,22 +980,51 @@ export class WebGPURenderer {
946
980
  /** Every distinct cache above, so a mesh's pipelines are built for each. One entry when aliased. */
947
981
  pipelineTargets;
948
982
  bindGroupLayout;
949
- /** One slot per draw, uploaded once a frame. See `UniformRing`. */
983
+ /** One slot per draw, uploaded once a frame. See `UniformRing`. Swapped while a static list records. */
950
984
  perDraw;
951
- /** The lights, fog and camera, settled once per frame by `bindMeshPass`. */
952
- perFrame;
953
985
  /**
954
- * The material block as the CPU holds it, patched by the setters and copied into a slot.
986
+ * The lights, fog and camera, a slot per mesh pass a frame opens: `bindMeshPass` writes them, and
987
+ * the first draw after it takes a slot. A ring rather than one buffer because a frame opens more
988
+ * than one pass — a mirror's, a capture's, a probe face's — and a queue write does not interleave
989
+ * with the draws recorded between them. See `UniformRing`.
990
+ */
991
+ passRing;
992
+ /** Each material's block, kept across frames for as long as it does not change. */
993
+ materialStore;
994
+ /**
995
+ * Both blocks as the CPU holds them, the pass's then the material's, patched by name and copied
996
+ * into a slot by the next draw after something changed them. See `FlatFragmentScratch`.
955
997
  *
956
998
  * Authoritative between draws: a setter changes one field here and marks the block dirty, and
957
- * the next `drawMesh` takes a slot and copies the whole thing in. That keeps a setter free of
958
- * any knowledge of slots, which is what lets it be called anywhere a scene wants.
999
+ * the next `drawMesh` places it. That keeps a setter free of any knowledge of slots, which is
1000
+ * what lets it be called anywhere a scene wants.
959
1001
  */
960
1002
  perFrameStaging;
961
1003
  perFrameFloats;
962
1004
  perFrameInts;
1005
+ /** The two halves of the scratch, as the words a slot is written from. */
1006
+ passInts;
1007
+ materialInts;
963
1008
  /** The slot the open material occupies, and whether the next draw must take a new one. */
964
1009
  materials = new MaterialChanges();
1010
+ /** The same for the pass's block: reopened by `bindMeshPass` and by nothing a material sets. */
1011
+ passBlock = new MaterialChanges();
1012
+ /**
1013
+ * The vertex stage's view block — the camera, the light's matrix, the wind — a slot a pass, and a
1014
+ * second, unjittered, for a blended draw landing after the upscale. Reopened with the pass, and by
1015
+ * `setWind` and `beginShadowPass`, which change what it holds.
1016
+ */
1017
+ viewRing;
1018
+ viewBlock = new MaterialChanges();
1019
+ lateViewBlock = new MaterialChanges();
1020
+ /** The view slot the draw being submitted bound, read by the halves it keeps for later passes. */
1021
+ drawViewOffset = 0;
1022
+ /** Pass slots asked for and refused this frame, and materials refused: what grows the two. */
1023
+ passesAsked = 0;
1024
+ passesRefused = 0;
1025
+ viewsAsked = 0;
1026
+ viewsRefused = 0;
1027
+ materialsRefused = 0;
965
1028
  /*
966
1029
  * The frame graph's recording, and the machinery that replays it.
967
1030
  *
@@ -1214,7 +1277,7 @@ export class WebGPURenderer {
1214
1277
  variant;
1215
1278
  /** Whether this profile compiled the point-shadow path, and so has sentinels to write. */
1216
1279
  pointShadowsCompiled;
1217
- /** What the generator recorded about that permutation: sizes, offsets and bindings. */
1280
+ /** What the generator recorded about that permutation, as the renderer's scratch lays it out. */
1218
1281
  fragment;
1219
1282
  depth = null;
1220
1283
  depthView = null;
@@ -1934,7 +1997,7 @@ export class WebGPURenderer {
1934
1997
  recordNode(this.arena, VERB_SCOPE, 0, MIRROR_TARGET, 0, 0);
1935
1998
  this.pass?.end();
1936
1999
  this.flushRings();
1937
- this.surface.device.queue.submit([this.encoder.finish()]);
2000
+ this.submit([this.encoder.finish()]);
1938
2001
  this.encoder = this.surface.device.createCommandEncoder({ label: 'reflection' });
1939
2002
  /*
1940
2003
  * **Opened on demand, like the frame's own.** Nothing has been recorded into the mirror yet,
@@ -1992,7 +2055,8 @@ export class WebGPURenderer {
1992
2055
  this.windStreakFrags,
1993
2056
  this.scatterVerts,
1994
2057
  this.scatterFrags,
1995
- this.perFrame,
2058
+ this.passRing,
2059
+ this.viewRing,
1996
2060
  this.filmVerts,
1997
2061
  this.filmFrags,
1998
2062
  this.insetUniforms,
@@ -2013,6 +2077,7 @@ export class WebGPURenderer {
2013
2077
  this.instanceCulls?.flush();
2014
2078
  for (const ring of this.rings())
2015
2079
  ring.flush();
2080
+ this.materialStore.flush();
2016
2081
  }
2017
2082
  /** Where every ring stands before work that submits an encoder of its own. */
2018
2083
  ringMarks = [];
@@ -2032,6 +2097,8 @@ export class WebGPURenderer {
2032
2097
  const rings = this.rings();
2033
2098
  for (let i = 0; i < rings.length; i++)
2034
2099
  rings[i].rewind(this.ringMarks[i] ?? 0);
2100
+ /* The open pass may sit in a slot just given back, which the next pass would overwrite. */
2101
+ this.reopenPass();
2035
2102
  this.skinPalettes.rewind(skinMark);
2036
2103
  if (this.skinPaletteSlot >= skinMark)
2037
2104
  this.skinPaletteSlot = -1;
@@ -2129,6 +2196,7 @@ export class WebGPURenderer {
2129
2196
  }
2130
2197
  filmCommand.vertexCount = geometry.vertexBuffers.length;
2131
2198
  filmCommand.indexBuffer = geometry.indexBuffer;
2199
+ filmCommand.indexFormat = geometry.indexFormat;
2132
2200
  filmCommand.indexed = true;
2133
2201
  filmCommand.count = geometry.indexCount;
2134
2202
  }
@@ -2141,7 +2209,7 @@ export class WebGPURenderer {
2141
2209
  for (let index = 0; index < geometry.vertexBuffers.length; index++) {
2142
2210
  pass.setVertexBuffer(index, geometry.vertexBuffers[index]);
2143
2211
  }
2144
- pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
2212
+ pass.setIndexBuffer(geometry.indexBuffer, geometry.indexFormat);
2145
2213
  pass.drawIndexed(geometry.indexCount);
2146
2214
  }
2147
2215
  }
@@ -2175,6 +2243,11 @@ export class WebGPURenderer {
2175
2243
  modelMap = null;
2176
2244
  /** The material's shading model, which picks the lit pipeline: null the standard. */
2177
2245
  materialModel = null;
2246
+ /** `uWorldUv`, the material's projection packed by `packSurfaceProjection`. */
2247
+ projection = new Float32Array(PROJECTION_FLOATS);
2248
+ /** `uLayers`, the material's layers packed by `packSurfaceLayers`. */
2249
+ layerFloats = new Float32Array(LAYER_FLOATS);
2250
+ warnedLayersModelMap = false;
2178
2251
  /** Its numbers, `uModelParams`, packed by `packModel`. */
2179
2252
  modelParams = new Float32Array(MODEL_PARAM_FLOATS);
2180
2253
  /**
@@ -2329,7 +2402,7 @@ export class WebGPURenderer {
2329
2402
  pass.end();
2330
2403
  this.pass = null;
2331
2404
  this.flushRings();
2332
- device.queue.submit([encoder.finish()]);
2405
+ this.submit([encoder.finish()]);
2333
2406
  this.rewindRings(skinMark);
2334
2407
  this.encoderEpoch = frameEpoch;
2335
2408
  this.encoder = savedEncoder;
@@ -2337,6 +2410,7 @@ export class WebGPURenderer {
2337
2410
  this.perFrameInts[this.materialField('uOutputTransform')] = heldGrade;
2338
2411
  this.perFrameFloats[this.materialField('uOutputExposure')] = heldExposure;
2339
2412
  this.materials.dirty();
2413
+ this.reopenPass();
2340
2414
  this.capturePassActive = false;
2341
2415
  }
2342
2416
  return true;
@@ -2352,10 +2426,11 @@ export class WebGPURenderer {
2352
2426
  updateSurfaceTexture(texture, source) {
2353
2427
  if (this.surface.lost)
2354
2428
  return;
2355
- const replaced = texture.update(source);
2429
+ const replaced = texture.update(source, this.compressedFormats);
2356
2430
  if (replaced === null)
2357
2431
  return;
2358
- /* A new size is a new view, so every group holding the old one is dropped — see below. */
2432
+ /* A new size or format is a new view, so every group holding the old one is dropped — see
2433
+ below. */
2359
2434
  this.retiredTextures.push(replaced);
2360
2435
  this.forgetBindingsOf(texture);
2361
2436
  }
@@ -2411,7 +2486,9 @@ export class WebGPURenderer {
2411
2486
  /* Same reasoning, for whichever SDF labels were bound against this atlas. */
2412
2487
  this.sdfTextBindGroups.delete(texture);
2413
2488
  this.flatBindGroups.clear();
2489
+ this.flatGroupEpoch += 1;
2414
2490
  this.overlayGroups.clear();
2491
+ this.skinnedGroups.length = 0;
2415
2492
  /*
2416
2493
  * The blank one is built with the albedo held aside, because `buildFlatBindGroup` reads
2417
2494
  * `this.albedo` for that slot — leaving it in place would put a real colour map into the
@@ -2571,11 +2648,21 @@ export class WebGPURenderer {
2571
2648
  const normal = (material?.normal ?? null);
2572
2649
  const orm = (material?.orm ?? null);
2573
2650
  const emissiveMap = (material?.emissive ?? null);
2574
- const modelMap = (material?.modelMap ?? null);
2651
+ const ownModelMap = (material?.modelMap ?? null);
2652
+ /* A layered material's mask goes where a model's map would: see `SurfaceMaterial.layers`. */
2653
+ const layers = this.layersOf(material);
2654
+ const modelMap = ownModelMap ?? (layers?.mask ?? null);
2575
2655
  this.materialModel = material?.model ?? null;
2656
+ const reflectivity = material?.reflectivity;
2657
+ const environmentGain = material?.environmentGain;
2658
+ this.materialReflectivity =
2659
+ reflectivity === undefined ? null : Math.min(1, Math.max(0, reflectivity));
2660
+ this.materialEnvironmentGain =
2661
+ environmentGain === undefined ? null : Math.max(0, environmentGain);
2662
+ this.writeEnvironmentDials();
2576
2663
  /* What the blur spreads this skin by: the last material to name a profile says. */
2577
2664
  const model = material?.model;
2578
- warnUnpagedLightmap(model, modelMap);
2665
+ warnUnpagedLightmap(model, ownModelMap);
2579
2666
  if (model?.kind === 'skin' && this.skinScatter !== null) {
2580
2667
  this.skinScatter.setProfile(model.profile, skinProfileDistance(model, 0), skinProfileDistance(model, 1), skinProfileDistance(model, 2));
2581
2668
  }
@@ -2592,7 +2679,8 @@ export class WebGPURenderer {
2592
2679
  const cut = this.materialField('uCutout');
2593
2680
  f[cut] = cutoff;
2594
2681
  f[cut + 1] = CUTOUT_RESOLVE_CODE[this.cutoutResolveStaged];
2595
- f[cut + 2] = this.cutoutFrame;
2682
+ /* z unused: the dither's frame is the pass's, in `uHighlightMin.w`. See `uCutout`. */
2683
+ f[cut + 2] = 0;
2596
2684
  /* The material's lighting channels, which the lamps' are compared with. See `uCutout`. */
2597
2685
  f[cut + 3] = lightChannelsOf(material?.lightChannels);
2598
2686
  i[flags + 3] = material?.doubleSided === true ? 1 : 0;
@@ -2622,10 +2710,23 @@ export class WebGPURenderer {
2622
2710
  f[emissive + 1] = asked?.[1] ?? 1;
2623
2711
  f[emissive + 2] = asked?.[2] ?? 1;
2624
2712
  /* The model's numbers and whether its map is bound. See `packModel`. */
2625
- packModel(material?.model ?? null, modelMap !== null, this.modelParams, material?.physicalSpecular === true);
2713
+ packModel(material?.model ?? null, ownModelMap !== null, this.modelParams, material?.physicalSpecular === true);
2626
2714
  /* The first physical highlight asked for is when the lit pipelines start carrying one. */
2627
2715
  if (material?.physicalSpecular === true)
2628
2716
  this.enableLit('PHYSICAL_SPECULAR');
2717
+ /* Maps placed by the world: the first material that asks is when lit pipelines carry it. */
2718
+ packSurfaceProjection(material?.projection, this.projection);
2719
+ f.set(this.projection, this.materialField('uWorldUv'));
2720
+ /* How much light from behind it lets through, in the glass tint's spare lane. */
2721
+ f[this.materialField('uGlassTint') + 3] =
2722
+ Math.min(Math.max(material?.diffuseTransmission ?? 0, 0), 1) || 0;
2723
+ if (this.projection[0] > 0)
2724
+ this.enableLit('WORLD_UVS');
2725
+ /* And its layers, the first time a material blends any: layered.ts. */
2726
+ packSurfaceLayers(layers, this.layerFloats);
2727
+ f.set(this.layerFloats, this.materialField('uLayers'));
2728
+ if (layers !== null)
2729
+ this.enableLit('LAYERED');
2629
2730
  f.set(this.modelParams, this.materialField('uModelParams'));
2630
2731
  });
2631
2732
  /*
@@ -2764,14 +2865,28 @@ export class WebGPURenderer {
2764
2865
  }
2765
2866
  /** How reflective the surfaces drawn next are. Clamped, as `renderer.ts` clamps it. */
2766
2867
  setSurfaceReflectivity(amount) {
2767
- this.material((f) => {
2768
- f[this.materialField('uEnvironmentDials') + 1] = Math.min(1, Math.max(0, amount));
2769
- });
2868
+ this.passReflectivity = Math.min(1, Math.max(0, amount));
2869
+ this.writeEnvironmentDials();
2770
2870
  }
2771
2871
  /** How bright the environment the surfaces drawn next reflect is. See `renderer.ts`. */
2772
2872
  setEnvironmentGain(gain) {
2873
+ this.passEnvironmentGain = Math.max(0, gain);
2874
+ this.writeEnvironmentDials();
2875
+ }
2876
+ /**
2877
+ * The reflectivity and environment gain the setters gave the pass, and the ones the material
2878
+ * set now states, or null where it states none (`SurfaceMaterial.reflectivity`). A draw wears the
2879
+ * material's where it has one and the pass's otherwise, the rule `renderer.ts` keeps too.
2880
+ */
2881
+ passReflectivity = 0;
2882
+ passEnvironmentGain = 1;
2883
+ materialReflectivity = null;
2884
+ materialEnvironmentGain = null;
2885
+ writeEnvironmentDials() {
2773
2886
  this.material((f) => {
2774
- f[this.materialField('uEnvironmentDials') + 2] = Math.max(0, gain);
2887
+ const dials = this.materialField('uEnvironmentDials');
2888
+ f[dials + 1] = this.materialReflectivity ?? this.passReflectivity;
2889
+ f[dials + 2] = this.materialEnvironmentGain ?? this.passEnvironmentGain;
2775
2890
  });
2776
2891
  }
2777
2892
  setSurfaceGrain(amount) {
@@ -2799,6 +2914,14 @@ export class WebGPURenderer {
2799
2914
  this.perFrameFloats[this.materialField('uWriteMode') + 1] = Math.min(1, Math.max(-1, amount));
2800
2915
  this.materials.dirty();
2801
2916
  }
2917
+ /** Whether the draws that follow spend their own opacity on that screen door. See `renderer.ts`. */
2918
+ setDitherOpacity(on) {
2919
+ if (this.surface.lost)
2920
+ return;
2921
+ /* The third lane of `uWriteMode`, beside the fade. */
2922
+ this.perFrameFloats[this.materialField('uWriteMode') + 2] = on ? 1 : 0;
2923
+ this.materials.dirty();
2924
+ }
2802
2925
  /**
2803
2926
  * How strong the microscopic relief on the surfaces drawn next is, and how coarse.
2804
2927
  *
@@ -3242,7 +3365,7 @@ export class WebGPURenderer {
3242
3365
  }
3243
3366
  this.pass?.end();
3244
3367
  this.flushRings();
3245
- this.surface.device.queue.submit([this.encoder.finish()]);
3368
+ this.submit([this.encoder.finish()]);
3246
3369
  this.encoder = this.surface.device.createCommandEncoder({ label: 'frame.resumed' });
3247
3370
  this.reflectionPassActive = false;
3248
3371
  this.reflectionReady = true;
@@ -4080,6 +4203,7 @@ export class WebGPURenderer {
4080
4203
  const sunShadow = env === undefined ? 0 : clamp(options.sunShadow ?? 0, 0, 1);
4081
4204
  ring.writeFloat(fragmentSlot, at('uSunShadow'), sunShadow);
4082
4205
  ring.writeFloats(fragmentSlot, at('uLightViewProj'), env === undefined ? NO_LIGHT_MATRIX : env.lightViewProj);
4206
+ ring.writeFloats(fragmentSlot, at('uMovingLightViewProj'), env === undefined ? NO_LIGHT_MATRIX : this.movingLightFor(env));
4083
4207
  ring.writeFloat(fragmentSlot, at('uShadowMapSize'), this.quality.directionalShadowMapSize);
4084
4208
  /* Sampled only where a pass filled it — see `peelFilled` for why existing is not enough. */
4085
4209
  ring.writeInt(fragmentSlot, at('uPeeledShadowEnabled'), this.peelFilled ? 1 : 0);
@@ -4108,6 +4232,7 @@ export class WebGPURenderer {
4108
4232
  }
4109
4233
  volumeCommand.vertexCount = mesh.vertexBuffers.length;
4110
4234
  volumeCommand.indexBuffer = mesh.indexBuffer;
4235
+ volumeCommand.indexFormat = mesh.indexFormat;
4111
4236
  volumeCommand.indexed = true;
4112
4237
  volumeCommand.count = mesh.indexCount;
4113
4238
  }
@@ -4117,7 +4242,7 @@ export class WebGPURenderer {
4117
4242
  for (let index = 0; index < mesh.vertexBuffers.length; index++) {
4118
4243
  pass.setVertexBuffer(index, mesh.vertexBuffers[index]);
4119
4244
  }
4120
- pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
4245
+ pass.setIndexBuffer(mesh.indexBuffer, mesh.indexFormat);
4121
4246
  pass.drawIndexed(mesh.indexCount);
4122
4247
  }
4123
4248
  }
@@ -4626,6 +4751,7 @@ export class WebGPURenderer {
4626
4751
  */
4627
4752
  setWind(windX, windZ, windGust, timeSeconds) {
4628
4753
  resolveScatterDeform(windX, windZ, windGust, timeSeconds, null, this.frameWind);
4754
+ this.reopenViews();
4629
4755
  }
4630
4756
  /**
4631
4757
  * The five wind fields of one shadow-draw slot.
@@ -4643,21 +4769,6 @@ export class WebGPURenderer {
4643
4769
  this.shadowDraws.writeFloat(slot, DEPTH_VERT_FIELDS.uWindTime.offset, d.time);
4644
4770
  this.shadowDraws.writeFloats(slot, DEPTH_VERT_FIELDS.uWindSpatialPhase.offset, d.spatialPhase);
4645
4771
  }
4646
- /**
4647
- * The five wind fields of one per-draw slot.
4648
- *
4649
- * **Never called for an instanced draw.** That variant declares none of these, and
4650
- * `flatPass.ts` records what writing a field a variant does not declare costs: the block is
4651
- * shorter, so the write lands on whatever occupies that offset instead.
4652
- */
4653
- writeWind(slot) {
4654
- const d = this.frameWind;
4655
- this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uWindDirection.offset, d.direction);
4656
- this.perDraw.writeFloat(slot, FLAT_VERT_FIELDS.uWindSpeed.offset, d.bend);
4657
- this.perDraw.writeFloat(slot, FLAT_VERT_FIELDS.uWindGust.offset, d.gust);
4658
- this.perDraw.writeFloat(slot, FLAT_VERT_FIELDS.uWindTime.offset, d.time);
4659
- this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uWindSpatialPhase.offset, d.spatialPhase);
4660
- }
4661
4772
  /** Refilled per frame rather than allocated; see `resolvePointLights`. */
4662
4773
  lights = {
4663
4774
  count: 0,
@@ -4807,6 +4918,9 @@ export class WebGPURenderer {
4807
4918
  * at submit, and a texture's view is new when an update changes its size.
4808
4919
  */
4809
4920
  shadowCutoutLayout;
4921
+ /** A crowd's casters: the plain and cutout layouts with a bone animation's three textures. */
4922
+ shadowAnimatedLayout;
4923
+ shadowAnimatedCutoutLayout;
4810
4924
  cutoutGroups = new Map();
4811
4925
  cutoutPeelGroups = new Map();
4812
4926
  shadowPeelPassUniforms = null;
@@ -4844,6 +4958,22 @@ export class WebGPURenderer {
4844
4958
  * the texel-for-texel comparison that settled it.
4845
4959
  */
4846
4960
  correctedLightViewProj = new Float32Array(16);
4961
+ /**
4962
+ * What a receiver projects through to read the sun's static layers: the environment's
4963
+ * `lightViewProj`, taken at `bindMeshPass` as WebGL2 takes it there.
4964
+ *
4965
+ * **It was the last shadow pass's matrix**, which is the environment's wherever both layers are
4966
+ * drawn with one and the moving layer's wherever they are not: the static layer was then read
4967
+ * through the moving layer's tight square, and a distant pillar lost its shadow on WebGPU and kept
4968
+ * it on WebGL2, measured on demo/dev/sunLayers.html?moving=tight.
4969
+ */
4970
+ receiverLightViewProj = new Float32Array(16);
4971
+ /**
4972
+ * The matrix the sun's dynamic shadow pass was last drawn with, uncorrected as the lit stage
4973
+ * projects with, and whether one has been. See `movingLightFor`.
4974
+ */
4975
+ movingLightViewProj = new Float32Array(16);
4976
+ movingLightKnown = false;
4847
4977
  /**
4848
4978
  * The same matrix *uncorrected*, which is what the flat shader must project with.
4849
4979
  *
@@ -4940,6 +5070,44 @@ export class WebGPURenderer {
4940
5070
  this.glassSeen = true;
4941
5071
  this.enableLit('GLASS_SHADOWS');
4942
5072
  }
5073
+ /**
5074
+ * A material's layers where they can be drawn: none asked, or a model's map in the mask's place,
5075
+ * said once, leave one layer.
5076
+ */
5077
+ layersOf(material) {
5078
+ const layers = material?.layers ?? null;
5079
+ if (layers === null || layers.repeats.length === 0)
5080
+ return null;
5081
+ if ((material?.modelMap ?? null) === null)
5082
+ return layers;
5083
+ if (!this.warnedLayersModelMap) {
5084
+ this.warnedLayersModelMap = true;
5085
+ console.warn("WebGPU: a material's layers are drawn as one: its mask is read where a model's map goes, " +
5086
+ 'and this material carries a `modelMap` too.');
5087
+ }
5088
+ return null;
5089
+ }
5090
+ /**
5091
+ * What the sun's moving layer is read through: the matrix its shadow pass was last drawn with,
5092
+ * where the lit pipelines carry one (`MOVING_SUN`); the environment's otherwise, which every caller
5093
+ * drawing both layers with one matrix hands it anyway.
5094
+ */
5095
+ movingLightFor(env) {
5096
+ return this.movingLightKnown && this.pipelines.litSwitches.MOVING_SUN
5097
+ ? this.movingLightViewProj
5098
+ : env.lightViewProj;
5099
+ }
5100
+ /** The moving layer's own matrix on, the first time a frame draws that layer with one. */
5101
+ enableMovingSun(env) {
5102
+ if (this.pipelines.litSwitches.MOVING_SUN || !this.movingLightKnown)
5103
+ return;
5104
+ for (let k = 0; k < 16; k++) {
5105
+ if (this.movingLightViewProj[k] !== env.lightViewProj[k]) {
5106
+ this.enableLit('MOVING_SUN');
5107
+ return;
5108
+ }
5109
+ }
5110
+ }
4943
5111
  /** One of the lit stage's switches on, in every cache a lit pipeline can come from. */
4944
5112
  enableLit(feature) {
4945
5113
  if (this.pipelines.litSwitches[feature])
@@ -4993,7 +5161,7 @@ export class WebGPURenderer {
4993
5161
  for (let index = 0; index < geometry.vertexBuffers.length; index++) {
4994
5162
  pass.setVertexBuffer(index, geometry.vertexBuffers[index]);
4995
5163
  }
4996
- pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
5164
+ pass.setIndexBuffer(geometry.indexBuffer, geometry.indexFormat);
4997
5165
  pass.drawIndexed(geometry.indexCount);
4998
5166
  },
4999
5167
  /**
@@ -5019,26 +5187,54 @@ export class WebGPURenderer {
5019
5187
  if (count === 0)
5020
5188
  return;
5021
5189
  const cutout = cutoutOf(material);
5022
- const cutoutGroup = cutout === null ? null : this.cutoutGroup(cutout.albedo);
5190
+ /* A crowd casts where its clip has moved it, through the animated variant. */
5191
+ const animation = gpuBatch.animation === null ? null : this.animationViews(gpuBatch);
5192
+ if (gpuBatch.animation !== null && animation === null)
5193
+ return;
5194
+ const cutoutTexture = cutout === null ? null : cutout.albedo;
5195
+ const cutoutGroup = animation !== null
5196
+ ? this.animatedDepthGroup(gpuBatch, animation, cutoutTexture)
5197
+ : cutoutTexture === null
5198
+ ? null
5199
+ : this.cutoutGroup(cutoutTexture);
5200
+ if (animation !== null && cutoutGroup === null)
5201
+ return;
5023
5202
  const slot = this.shadowDraws.allocate();
5024
5203
  if (slot === null)
5025
5204
  return;
5026
5205
  /* Only the light matrix: the instanced depth variant has no uModel, its placement being
5027
5206
  the four attribute columns the batch's buffer supplies. */
5028
5207
  this.shadowDraws.writeFloats(slot, DEPTH_VERT_FIELDS.uLightViewProj.offset, this.correctedLightViewProj);
5029
- if (cutout !== null && cutoutGroup !== null) {
5208
+ const cuts = cutoutTexture !== null && cutoutGroup !== null;
5209
+ if (cutout !== null && cuts) {
5030
5210
  this.writeCutout(slot, cutout, DEPTH_INSTANCED_CUTOUT_VERT_FIELDS);
5031
5211
  }
5032
- const cuts = cutoutGroup !== null;
5212
+ const clip = gpuBatch.animation;
5213
+ if (animation !== null && clip !== null) {
5214
+ const fields = cuts
5215
+ ? DEPTH_INSTANCED_ANIMATED_CUTOUT_VERT_FIELDS
5216
+ : DEPTH_INSTANCED_ANIMATED_VERT_FIELDS;
5217
+ this.shadowDraws.writeFloat(slot, fields.uSceneTime.offset, this.animationTime);
5218
+ this.shadowDraws.writeFloat(slot, fields.uBoneClip.offset, clip.framesPerSecond);
5219
+ this.shadowDraws.writeFloat(slot, fields.uBoneClip.offset + 4, clip.boneScale);
5220
+ }
5033
5221
  const cull = material?.doubleSided === true ? 'none' : this.depthCullMode;
5034
- pass.setPipeline(depthPipeline(this.pipelines, this.surface.device, cuts ? this.shadowCutoutLayout : this.shadowLayout, `${cuts ? 'depth-instanced-cutout' : 'depth-instanced'}|${cull}|${geometry.key ?? ''}`, this.presentOf(geometry), cull, false, true, cuts));
5222
+ const animated = animation !== null;
5223
+ pass.setPipeline(depthPipeline(this.pipelines, this.surface.device, animated
5224
+ ? cuts
5225
+ ? this.shadowAnimatedCutoutLayout
5226
+ : this.shadowAnimatedLayout
5227
+ : cuts
5228
+ ? this.shadowCutoutLayout
5229
+ : this.shadowLayout, `${cuts ? 'depth-instanced-cutout' : 'depth-instanced'}${animated ? '-animated' : ''}` +
5230
+ `|${cull}|${geometry.key ?? ''}`, this.presentOf(geometry), cull, false, true, cuts, false, false, animated));
5035
5231
  pass.setBindGroup(0, cutoutGroup ?? this.shadowGroup(), [slot]);
5036
5232
  let index = 0;
5037
5233
  for (; index < geometry.vertexBuffers.length; index++) {
5038
5234
  pass.setVertexBuffer(index, geometry.vertexBuffers[index]);
5039
5235
  }
5040
5236
  pass.setVertexBuffer(index, gpuBatch.buffer);
5041
- pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
5237
+ pass.setIndexBuffer(geometry.indexBuffer, geometry.indexFormat);
5042
5238
  pass.drawIndexed(geometry.indexCount, count);
5043
5239
  },
5044
5240
  skinnedMesh: (mesh, model, palette, material, cloth) => {
@@ -5081,7 +5277,7 @@ export class WebGPURenderer {
5081
5277
  for (let index = 0; index < geometry.vertexBuffers.length; index++) {
5082
5278
  pass.setVertexBuffer(index, geometry.vertexBuffers[index]);
5083
5279
  }
5084
- pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
5280
+ pass.setIndexBuffer(geometry.indexBuffer, geometry.indexFormat);
5085
5281
  pass.drawIndexed(geometry.indexCount);
5086
5282
  },
5087
5283
  /**
@@ -5452,6 +5648,8 @@ export class WebGPURenderer {
5452
5648
  this.overlayPipelines === this.pipelines
5453
5649
  ? [this.pipelines]
5454
5650
  : [this.pipelines, this.overlayPipelines];
5651
+ /* A lit draw first needed inside a frame waits for its compile or is left out while it runs. */
5652
+ this.skipCompiling(quality.pipelineCompile === 'skip');
5455
5653
  this.variant = flatVariant({
5456
5654
  directionalShadows: quality.directionalShadows,
5457
5655
  /* No `bakeReflectionProbe` here; see the field. */
@@ -5472,7 +5670,7 @@ export class WebGPURenderer {
5472
5670
  pointShadows: quality.pointShadows,
5473
5671
  });
5474
5672
  this.pointShadowsCompiled = quality.pointShadows;
5475
- this.fragment = flatFragmentBindings(this.variant);
5673
+ this.fragment = flatFragmentScratch(this.variant);
5476
5674
  this.bindGroupLayout = createFlatBindGroupLayout(device, this.variant);
5477
5675
  /*
5478
5676
  * The skinned twin, built eagerly because it is a descriptor rather than a compile — WebGPU
@@ -5482,24 +5680,32 @@ export class WebGPURenderer {
5482
5680
  this.skinnedBindGroupLayout = createFlatBindGroupLayout(device, this.variant, true);
5483
5681
  this.morphedBindGroupLayout = createFlatBindGroupLayout(device, this.variant, false, true);
5484
5682
  this.bothBindGroupLayout = createFlatBindGroupLayout(device, this.variant, true, true);
5683
+ /* And the instanced twin that plays a bone animation: its clip's and clocks' textures. */
5684
+ this.animatedBindGroupLayout = createFlatBindGroupLayout(device, this.variant, false, false, true);
5485
5685
  this.perDraw = new UniformRing(device, FLAT_VERT_SIZE, quality.drawsPerFrame, USAGE_UNIFORM_DST, 'flat.vertRing', this.drawBudget);
5486
- this.perFrameStaging = new ArrayBuffer(this.fragment.uniformSize);
5686
+ const { passSize, materialSize } = this.fragment;
5687
+ this.perFrameStaging = new ArrayBuffer(passSize + materialSize);
5487
5688
  this.perFrameFloats = new Float32Array(this.perFrameStaging);
5488
5689
  this.perFrameInts = new Int32Array(this.perFrameStaging);
5489
- /*
5490
- * **A ring, because the block holds material state and a material changes between draws.**
5491
- *
5492
- * `setSurfaceTexture`, `setSurfaceGrain` and their siblings are pass state on both backends
5493
- * — a material covers many draws, so the alternative is rebinding the same values twenty
5494
- * times to say the same thing. On WebGL2 each is a `uniform1f` that lands immediately. Here
5495
- * they live in this block, and `queue.writeBuffer` does not interleave with recorded
5496
- * commands, so one buffer rewritten between two draws gives *both* the second value. Every
5497
- * surface in a scene would wear the last material set, which is bug 7's shape at pass scale.
5498
- *
5499
- * A slot per *material change* rather than per draw: `drawMesh` reuses the open slot until
5500
- * something dirties it, so a scene drawing a hundred meshes of one material spends one.
5501
- */
5502
- this.perFrame = new UniformRing(device, this.fragment.uniformSize, this.quality.materialChangesPerFrame, USAGE_UNIFORM_DST, 'flat.fragRing', this.materialBudget);
5690
+ this.passInts = new Int32Array(this.perFrameStaging, 0, passSize / 4);
5691
+ this.materialInts = new Int32Array(this.perFrameStaging, passSize, materialSize / 4);
5692
+ /*
5693
+ * **Two blocks, because they change at two rates.** `setSurfaceTexture`, `setSurfaceGrain` and
5694
+ * their siblings are pass state on both backends — a material covers many draws — and on WebGL2
5695
+ * each is a `uniform1f` that lands immediately. Here they live in a block, and
5696
+ * `queue.writeBuffer` does not interleave with recorded commands, so one buffer rewritten between
5697
+ * two draws gives *both* the second value: every surface would wear the last material set, which
5698
+ * is bug 7's shape at pass scale. So each draw binds a slot.
5699
+ *
5700
+ * It was one block until 4.10.0, lights and all, copied whole into a ring slot per material
5701
+ * change and uploaded every frame: a stage of four hundred materials sent 4.8 MB a frame to say
5702
+ * what it had said the frame before. Now the pass's block takes a ring slot per pass, and a
5703
+ * material's block a slot of the store, where an unchanged material stays from frame to frame.
5704
+ */
5705
+ this.passRing = new UniformRing(device, passSize, PASSES_PER_FRAME, USAGE_UNIFORM_DST, 'flat.passRing');
5706
+ this.materialStore = new MaterialStore(device, materialSize, this.quality.materialChangesPerFrame, USAGE_UNIFORM_DST, 'flat.materials', this.materialBudget);
5707
+ /* Two a pass at most: the jittered view and the unjittered one. */
5708
+ this.viewRing = new UniformRing(device, FLAT_VIEW_SIZE, 2 * PASSES_PER_FRAME, USAGE_UNIFORM_DST, 'flat.viewRing');
5503
5709
  /*
5504
5710
  * A one-pixel stand-in for the albedo binding. The albedo's flag is zero for untextured
5505
5711
  * geometry, which is nearly all of it, so the shader never samples this — but WebGPU
@@ -5632,7 +5838,7 @@ export class WebGPURenderer {
5632
5838
  })
5633
5839
  .end();
5634
5840
  }
5635
- device.queue.submit([clear.finish()]);
5841
+ this.submit([clear.finish()]);
5636
5842
  /*
5637
5843
  * `nearest` on a depth texture sampled as an ordinary float, matching what
5638
5844
  * `shadowMap.ts` asks WebGL2 for. A filtering sampler cannot be used on a depth format
@@ -6120,6 +6326,8 @@ export class WebGPURenderer {
6120
6326
  this.shadowLayout = createDepthBindGroupLayout(device);
6121
6327
  this.shadowSkinnedLayout = createDepthBindGroupLayout(device, true);
6122
6328
  this.shadowCutoutLayout = createDepthBindGroupLayout(device, false, true);
6329
+ this.shadowAnimatedLayout = createDepthBindGroupLayout(device, false, false, true);
6330
+ this.shadowAnimatedCutoutLayout = createDepthBindGroupLayout(device, false, true, true);
6123
6331
  this.shadowDraws = new UniformRing(device,
6124
6332
  /* The cutout block is the plain one and two fields more, and a slot must hold either. */
6125
6333
  Math.max(DEPTH_VERT_SIZE, DEPTH_CUTOUT_VERT_SIZE), quality.drawsPerFrame, USAGE_UNIFORM_DST, 'shadow.drawRing', this.shadowDrawBudget);
@@ -6309,6 +6517,8 @@ export class WebGPURenderer {
6309
6517
  present['layers'] = key.includes(':layers');
6310
6518
  /* Nor is a dynamic mesh's split, which moves positions and normals into buffers of their own. */
6311
6519
  present['dynamic'] = key.includes(':dynamic');
6520
+ /* Nor how each attribute travels, which moves every field after a packed one. */
6521
+ markPackingOfKey(present, key);
6312
6522
  return present;
6313
6523
  }
6314
6524
  /**
@@ -6418,9 +6628,18 @@ export class WebGPURenderer {
6418
6628
  this.lineVerts.reset();
6419
6629
  this.lineFrags.reset();
6420
6630
  this.skyUniforms.reset();
6421
- /* The material ring, and the open slot with it: last frame's slots are gone. */
6422
- this.perFrame.reset();
6631
+ /* The pass ring, and the open slots with it: last frame's pass slots are gone. Its materials
6632
+ stay in the store, which a draw finds them in again. */
6633
+ this.passRing.reset();
6634
+ this.viewRing.reset();
6635
+ this.materialStore.beginFrame();
6636
+ this.passesAsked = 0;
6637
+ this.passesRefused = 0;
6638
+ this.viewsAsked = 0;
6639
+ this.viewsRefused = 0;
6640
+ this.materialsRefused = 0;
6423
6641
  this.materials.dirty();
6642
+ this.reopenPass();
6424
6643
  /* Spent by `runMotion` at the end of the last frame; a frame states its movers afresh. */
6425
6644
  this.motionPass?.reset();
6426
6645
  this.latePass?.reset();
@@ -6603,6 +6822,13 @@ export class WebGPURenderer {
6603
6822
  present['dynamic'] = true;
6604
6823
  key += ':dynamic';
6605
6824
  }
6825
+ /*
6826
+ * And how each attribute travels (`vertexPacking.ts`): a field read as sixteen bits rather than
6827
+ * thirty-two, or not interleaved at all, moves every field after it, so the key says so.
6828
+ */
6829
+ const packing = vertexPackingOf(data, dynamic);
6830
+ markPacking(present, packing);
6831
+ key += packingKey(packing);
6606
6832
  const fullKey = `${this.variant}|${key}`;
6607
6833
  /*
6608
6834
  * **For every target this mesh can land on, which is two when the overlay is its own.**
@@ -6718,6 +6944,37 @@ export class WebGPURenderer {
6718
6944
  },
6719
6945
  };
6720
6946
  }
6947
+ /**
6948
+ * The copies this frame's mesh rewrites owe (`StagedRewrite`), recorded since the last submit and
6949
+ * sent at the head of the next one: one encoder however many meshes are rewritten. Null while
6950
+ * nothing is owed.
6951
+ */
6952
+ pendingRewrites = null;
6953
+ /** One record handed to every staged rewrite, refilled each call, so a rewrite allocates nothing. */
6954
+ stagedRewrite = {
6955
+ encoder: null,
6956
+ staging: null,
6957
+ previous: null,
6958
+ };
6959
+ rewriteCopies() {
6960
+ this.pendingRewrites ??= this.surface.device.createCommandEncoder({
6961
+ label: 'mesh.rewriteCopies',
6962
+ });
6963
+ return this.pendingRewrites;
6964
+ }
6965
+ /**
6966
+ * Submit `buffers`, led by any copies the frame's mesh rewrites owe. **Every submit of this
6967
+ * renderer goes through here**, because any of them may draw a rewritten mesh — the frame's own,
6968
+ * a mirror's, a shadow's, a probe's — and the copies must be ahead of the first that does.
6969
+ */
6970
+ submit(buffers) {
6971
+ const rewrites = this.pendingRewrites;
6972
+ if (rewrites !== null) {
6973
+ this.pendingRewrites = null;
6974
+ buffers.unshift(rewrites.finish());
6975
+ }
6976
+ this.surface.device.queue.submit(buffers);
6977
+ }
6721
6978
  /**
6722
6979
  * Rewrite a mesh's positions, and its normals where the caller has them.
6723
6980
  *
@@ -6738,20 +6995,35 @@ export class WebGPURenderer {
6738
6995
  * `changeFrames.ts`.
6739
6996
  */
6740
6997
  const motion = mesh.motion;
6741
- let previous = null;
6998
+ let staged = null;
6742
6999
  if (motion !== null && this.reconstructionWanted) {
6743
7000
  const frame = frameOfChange(this.moverFrame, this.inFrame);
7001
+ const size = mesh.vertexBuffers[0]?.size ?? 0;
7002
+ let previous = null;
6744
7003
  if (keepsPrevious(motion.changed, frame)) {
6745
7004
  motion.previous ??= this.surface.device.createBuffer({
6746
7005
  label: 'recon.dynamicPrevious',
6747
- size: mesh.vertexBuffers[0]?.size ?? 0,
7006
+ size,
6748
7007
  usage: 0x0020 | 0x0008, // VERTEX | COPY_DST
6749
7008
  });
6750
7009
  previous = motion.previous;
6751
7010
  }
6752
7011
  motion.changed = frame;
7012
+ /*
7013
+ * **Staged, and copied in behind the copy aside, in an encoder the frame's next submit leads
7014
+ * with**: no submit of the rewrite's own. See `StagedRewrite` and `submit`.
7015
+ */
7016
+ motion.staging ??= this.surface.device.createBuffer({
7017
+ label: 'mesh.stagedPositions',
7018
+ size,
7019
+ usage: 0x0004 | 0x0008, // COPY_SRC | COPY_DST
7020
+ });
7021
+ staged = this.stagedRewrite;
7022
+ staged.encoder = this.rewriteCopies();
7023
+ staged.staging = motion.staging;
7024
+ staged.previous = previous;
6753
7025
  }
6754
- mesh.update(this.surface.device, positions, normals, previous);
7026
+ mesh.update(this.surface.device, positions, normals, staged);
6755
7027
  }
6756
7028
  /**
6757
7029
  * Settle everything the frame shares: the camera, the lights and the fog.
@@ -6764,6 +7036,8 @@ export class WebGPURenderer {
6764
7036
  bindMeshPass(camera, env) {
6765
7037
  if (this.surface.lost)
6766
7038
  return;
7039
+ /* The matrix every receiver projects through: the environment's, as WebGL2's. See the field. */
7040
+ this.receiverLightViewProj.set(env.lightViewProj);
6767
7041
  /* Rectangles past the fixed arm's are the froxel table's, and without it nobody shades them. */
6768
7042
  if (!this.warnedAreaOverflow &&
6769
7043
  !this.quality.clusteredLights &&
@@ -6878,6 +7152,12 @@ export class WebGPURenderer {
6878
7152
  this.surfaceFog = FOG_RECEDE;
6879
7153
  /* Every material switch off — albedo, ORM map, emissive map, two-sided — as WebGL2 resets them. */
6880
7154
  i.fill(0, at('uMaterialFlags'), at('uMaterialFlags') + 4);
7155
+ /* And no projection: maps follow the mesh's coordinates until a material asks otherwise. */
7156
+ f.fill(0, at('uWorldUv'), at('uWorldUv') + 4);
7157
+ /* Nor light through from behind. See `SurfaceMaterial.diffuseTransmission`. */
7158
+ f[at('uGlassTint') + 3] = 0;
7159
+ /* Nor layers: one layer until a material asks for more. */
7160
+ f.fill(0, at('uLayers'), at('uLayers') + LAYER_FLOATS);
6881
7161
  f[at('uCutout')] = 0;
6882
7162
  f[at('uCutout') + 1] = 0;
6883
7163
  this.cutoutResolveStaged = 'hard';
@@ -6941,10 +7221,16 @@ export class WebGPURenderer {
6941
7221
  * number, so this is a copy and is marked as one; the two must move together.
6942
7222
  */
6943
7223
  f[at('uGrain')] = 1;
7224
+ /* No reflection and an environment at its own brightness, from the pass and from no material. */
7225
+ this.passReflectivity = 0;
7226
+ this.passEnvironmentGain = 1;
7227
+ this.materialReflectivity = null;
7228
+ this.materialEnvironmentGain = null;
6944
7229
  f[at('uEnvironmentDials') + 1] = 0;
6945
7230
  /* No weighting and no dither until a pass or a crossfade asks. See `setDitherFade`. */
6946
7231
  f[at('uWriteMode')] = 0;
6947
7232
  f[at('uWriteMode') + 1] = 0;
7233
+ f[at('uWriteMode') + 2] = 0;
6948
7234
  /* 1, not 0: a multiplier's identity is one. See `setEnvironmentGain`. */
6949
7235
  f[at('uEnvironmentDials') + 2] = 1;
6950
7236
  /* No overlay until a draw sets one. See `setSurfaceOverlay`. */
@@ -7051,6 +7337,8 @@ export class WebGPURenderer {
7051
7337
  if (this.quality.nightEmissive)
7052
7338
  f[at('uNightEmissive')] = env.nightEmissive ?? 0;
7053
7339
  f.set(env.highlightMin, at('uHighlightMin'));
7340
+ /* The frame the cutout dither moves with, in the near corner's w. See `uHighlightMin`. */
7341
+ f[at('uHighlightMin') + 3] = this.cutoutFrame;
7054
7342
  f.set(env.highlightMax, at('uHighlightMax'));
7055
7343
  /* The gain in the far corner's w. See `uHighlightMax`. */
7056
7344
  f[at('uHighlightMax') + 3] = env.highlightGain;
@@ -7223,6 +7511,11 @@ export class WebGPURenderer {
7223
7511
  f[at('uShadowStrength')] = env.shadowStrength;
7224
7512
  f[at('uShadowMapSize')] = this.quality.directionalShadowMapSize;
7225
7513
  f[at('uShadowDepthSpan')] = env.shadowDepthSpan;
7514
+ /* The moving layer's own matrix and span, where the lit stage reads one: `movingLightFor`. */
7515
+ this.enableMovingSun(env);
7516
+ const moving = this.movingLightFor(env);
7517
+ f.set(moving, at('uMovingLightViewProj'));
7518
+ f[at('uMovingShadowDepthSpan')] = orthographicDepthSpan(moving);
7226
7519
  f[at('uShadowMaxDistance')] = this.quality.directionalShadowMaxDistance;
7227
7520
  f[at('uShadowMaxSlope')] = this.quality.directionalShadowMaxSlope;
7228
7521
  /* Sampled only where a pass filled it — see `peelFilled` for why existing is not enough. */
@@ -7303,6 +7596,7 @@ export class WebGPURenderer {
7303
7596
  * same terms here, which is why a pass cannot inherit a material from the one before it.
7304
7597
  */
7305
7598
  this.materials.dirty();
7599
+ this.reopenPass();
7306
7600
  /*
7307
7601
  * A pass starts with no material, exactly as `renderer.ts` resets these terms — **every map,
7308
7602
  * not the albedo alone**. `setMaterial` skips the lookup when the maps it is given are the ones
@@ -7326,29 +7620,104 @@ export class WebGPURenderer {
7326
7620
  this.uvOffset[1] = 0;
7327
7621
  }
7328
7622
  /**
7329
- * The slot the next draw's material lives in, taking a new one when something dirtied it.
7623
+ * The slot the next draw's material lives in, placing it when something dirtied it, with the
7624
+ * pass's slot taken first where `bindMeshPass` reopened it: `passOffset` is that one's offset.
7330
7625
  *
7331
- * **A copy per material change, not per draw.** A scene drawing a hundred meshes of one
7332
- * material spends one slot and one copy; a scene switching material every draw spends one
7333
- * each, which is what it asked for.
7626
+ * **A placement per material change, not per draw.** A scene drawing a hundred meshes of one
7627
+ * material places it once; a scene switching material every draw places one each, and a material
7628
+ * the store already holds — the same numbers as a draw before it, this frame or an earlier one —
7629
+ * is bound where it is and uploads nothing.
7334
7630
  */
7335
7631
  materialSlotForDraw() {
7632
+ if (!this.passBlock.open) {
7633
+ this.passesAsked += 1;
7634
+ const pass = this.passRing.allocate();
7635
+ if (pass === null) {
7636
+ this.passesRefused += 1;
7637
+ this.materialBudget.ask();
7638
+ this.materialBudget.drop();
7639
+ if (!this.warnedPassesFull) {
7640
+ this.warnedPassesFull = true;
7641
+ console.warn(`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.`);
7642
+ }
7643
+ return null;
7644
+ }
7645
+ this.passRing.writeBlock(pass, this.passInts);
7646
+ this.passBlock.slot = pass;
7647
+ }
7336
7648
  if (this.materials.open)
7337
7649
  return this.materials.slot;
7338
- const slot = this.perFrame.allocate();
7650
+ const slot = this.materialStore.place(this.materialInts);
7339
7651
  if (slot === null) {
7340
- if (!this.warnedMaterialsFull) {
7341
- this.warnedMaterialsFull = true;
7342
- console.warn(`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.`);
7343
- }
7652
+ this.materialsRefused += 1;
7653
+ this.warnMaterialsFull();
7344
7654
  return null;
7345
7655
  }
7346
- this.perFrame.writeBlock(slot, this.perFrameInts);
7347
7656
  this.materials.slot = slot;
7348
7657
  return slot;
7349
7658
  }
7659
+ /** The pass slot `materialSlotForDraw` left open, read beside the material's by every lit draw. */
7660
+ get passOffset() {
7661
+ return this.passBlock.slot;
7662
+ }
7663
+ /** A new pass: its block, and both its views, are taken afresh by the next draw. */
7664
+ reopenPass() {
7665
+ this.passBlock.dirty();
7666
+ this.reopenViews();
7667
+ }
7668
+ /** What a view holds changed — the wind, the light's matrix — so the next draw takes a new one. */
7669
+ reopenViews() {
7670
+ this.viewBlock.dirty();
7671
+ this.lateViewBlock.dirty();
7672
+ }
7673
+ /**
7674
+ * The view slot a lit draw reads its camera from: the pass's, or its unjittered twin for a blended
7675
+ * draw landing after the upscale. Taken by the first draw that needs it after the pass, the wind or
7676
+ * the light's matrix changed, and shared by every draw until then. Null when the ring is full, which
7677
+ * skips the draw and grows the ring for the next frame, as the pass ring does.
7678
+ */
7679
+ viewSlotFor(late, viewProj) {
7680
+ const block = late ? this.lateViewBlock : this.viewBlock;
7681
+ if (block.open)
7682
+ return block.slot;
7683
+ this.viewsAsked += 1;
7684
+ const slot = this.viewRing.allocate();
7685
+ if (slot === null) {
7686
+ this.viewsRefused += 1;
7687
+ this.materialBudget.ask();
7688
+ this.materialBudget.drop();
7689
+ if (!this.warnedPassesFull) {
7690
+ this.warnedPassesFull = true;
7691
+ console.warn(`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.`);
7692
+ }
7693
+ return null;
7694
+ }
7695
+ this.writeView(this.viewRing, slot, viewProj);
7696
+ block.slot = slot;
7697
+ return slot;
7698
+ }
7699
+ /** A view block: the camera, the light's matrix and the frame's wind, into `ring` at `slot`. */
7700
+ writeView(ring, slot, viewProj) {
7701
+ ring.writeFloats(slot, FLAT_VIEW_FIELDS.uViewProj.offset, viewProj);
7702
+ ring.writeFloats(slot, FLAT_VIEW_FIELDS.uLightViewProj.offset, this.receiverLightViewProj);
7703
+ /* The scene's clock, which a bone animation's instances read their moment from. */
7704
+ ring.writeFloat(slot, FLAT_VIEW_FIELDS.uSceneTime.offset, this.animationTime);
7705
+ const d = this.frameWind;
7706
+ ring.writeFloats(slot, FLAT_VIEW_FIELDS.uWindDirection.offset, d.direction);
7707
+ ring.writeFloat(slot, FLAT_VIEW_FIELDS.uWindSpeed.offset, d.bend);
7708
+ ring.writeFloat(slot, FLAT_VIEW_FIELDS.uWindGust.offset, d.gust);
7709
+ ring.writeFloat(slot, FLAT_VIEW_FIELDS.uWindTime.offset, d.time);
7710
+ ring.writeFloats(slot, FLAT_VIEW_FIELDS.uWindSpatialPhase.offset, d.spatialPhase);
7711
+ }
7712
+ warnMaterialsFull() {
7713
+ if (this.warnedMaterialsFull)
7714
+ return;
7715
+ this.warnedMaterialsFull = true;
7716
+ console.warn(`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.`);
7717
+ }
7350
7718
  /** Said once rather than every frame, for the reason `warnedFull` gives. */
7351
7719
  warnedMaterialsFull = false;
7720
+ warnedPassesFull = false;
7352
7721
  warnedSkyFull = false;
7353
7722
  warnedParticlesTwice = false;
7354
7723
  /**
@@ -7383,8 +7752,12 @@ export class WebGPURenderer {
7383
7752
  * What this saves is the GPU's share, which for a heavy mesh is most of the cost and for a
7384
7753
  * trivial one is very little.
7385
7754
  */
7386
- if (this.quality.cullDraws && !this.visible(mesh.bounds, model))
7755
+ /* A static list draws every entry in every view, since a bundle cannot leave one out. */
7756
+ if (this.quality.cullDraws &&
7757
+ this.staticRecording === null &&
7758
+ !this.visible(mesh.bounds, model)) {
7387
7759
  return;
7760
+ }
7388
7761
  /*
7389
7762
  * And the other half of the same question, where the profile asked for one. Gated on
7390
7763
  * `cullDraws` alongside the frustum test, because both are the renderer doing a consumer's
@@ -7549,17 +7922,17 @@ export class WebGPURenderer {
7549
7922
  `TranslucentMeshOptions.reconstructed`. */
7550
7923
  const lands = blend && options.reconstructed !== true && this.drawsLate();
7551
7924
  const late = lands || (blend && this.oitReplaying && this.reconstructing);
7552
- this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uViewProj.offset, late ? this.correctedViewProj : this.viewProj);
7925
+ /* The camera, the light's matrix and the wind are the pass's, in its view block. */
7926
+ const view = this.viewSlotFor(late, late ? this.correctedViewProj : this.viewProj);
7927
+ if (view === null)
7928
+ return;
7929
+ this.drawViewOffset = view;
7553
7930
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uModel.offset, model);
7554
7931
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uTint.offset, tint ?? WHITE);
7555
- /* The frame's wind, for a mesh carrying a sway lane. Written on this path and not the
7556
- instanced one, which declares none of these fields. */
7557
- this.writeWind(slot);
7558
7932
  /* The material's repeats, not a constant: geometry authored in metres is textured at
7559
7933
  whatever density the material asks for without rebuilding it. */
7560
7934
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvScale.offset, this.uvScale);
7561
7935
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvOffset.offset, this.uvOffset);
7562
- this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uLightViewProj.offset, this.lightViewProj);
7563
7936
  /*
7564
7937
  * Whether this mesh's tangent frame is real. WebGPU has no disabled attribute, so location 10
7565
7938
  * is always backed — by the caller's data or by the constant buffer — which makes the binding
@@ -7696,14 +8069,20 @@ export class WebGPURenderer {
7696
8069
  * thrown error in the middle of one.
7697
8070
  */
7698
8071
  const pipelines = blend ? this.blendedPipelines(lands) : this.targetPipelines();
7699
- let pipeline = pipelines.peek(key);
7700
- if (pipeline === undefined) {
8072
+ let pipeline = pipelines.peek(key) ?? null;
8073
+ if (pipeline === null) {
7701
8074
  const present = this.meshPresent.get(base);
7702
8075
  if (present === undefined) {
7703
8076
  throw new Error(`WebGPU: no pipeline for ${key}; createMesh builds it`);
7704
8077
  }
7705
8078
  pipeline = flatPipeline(pipelines, this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, key, present, adds ? 'additive' : blend, skinned, morphed, depthWrite, layer, false, this.oitMode, this.materialDoubleSided, coverage, eight, clothed, modelKind, splits ? 'scene' : 'whole');
7706
8079
  }
8080
+ /* A split skin draws its three halves or none of them. */
8081
+ if (pipeline !== null &&
8082
+ splits &&
8083
+ !this.skinHalvesReady(key, base, skinned, morphed, layer, eight, clothed)) {
8084
+ pipeline = null;
8085
+ }
7707
8086
  /*
7708
8087
  * Guards the draw call only, not the restore below.
7709
8088
  *
@@ -7720,8 +8099,9 @@ export class WebGPURenderer {
7720
8099
  * close to the 256-slot budget before the gateway, the play controls, the waveform, the
7721
8100
  * label handles and the sparkles are even counted.
7722
8101
  */
7723
- const material = this.materialSlotForDraw();
7724
- if (material !== null) {
8102
+ /* A draw whose pipeline is still compiling is left out, its state restored all the same. */
8103
+ const material = pipeline === null ? null : this.materialSlotForDraw();
8104
+ if (pipeline !== null && material !== null) {
7725
8105
  /*
7726
8106
  * Record rather than issue. Every per-draw value is already in `slot` and `material` —
7727
8107
  * the uniform rings are the state arena this design would otherwise have had to build —
@@ -7734,13 +8114,16 @@ export class WebGPURenderer {
7734
8114
  command.pipeline = pipeline;
7735
8115
  command.bindGroup = this.flatBindGroupFor(skinned, morphed, deltas);
7736
8116
  command.offsetA = slot;
7737
- command.offsetB = material;
7738
- command.offsetCount = 2;
8117
+ command.offsetB = this.passOffset;
8118
+ command.offsetC = material;
8119
+ command.offsetD = this.drawViewOffset;
8120
+ command.offsetCount = 4;
7739
8121
  for (let index = 0; index < mesh.vertexBuffers.length; index++) {
7740
8122
  command.vertexBuffers[index] = mesh.vertexBuffers[index];
7741
8123
  }
7742
8124
  command.vertexCount = mesh.vertexBuffers.length;
7743
8125
  command.indexBuffer = mesh.indexBuffer;
8126
+ command.indexFormat = mesh.indexFormat;
7744
8127
  command.indexed = true;
7745
8128
  command.count = mesh.indexCount;
7746
8129
  }
@@ -7749,11 +8132,16 @@ export class WebGPURenderer {
7749
8132
  if (pass === null)
7750
8133
  return;
7751
8134
  pass.setPipeline(pipeline);
7752
- pass.setBindGroup(0, this.flatBindGroupFor(skinned, morphed, deltas), [slot, material]);
8135
+ pass.setBindGroup(0, this.flatBindGroupFor(skinned, morphed, deltas), [
8136
+ slot,
8137
+ this.passOffset,
8138
+ material,
8139
+ this.drawViewOffset,
8140
+ ]);
7753
8141
  for (let index = 0; index < mesh.vertexBuffers.length; index++) {
7754
8142
  pass.setVertexBuffer(index, mesh.vertexBuffers[index]);
7755
8143
  }
7756
- pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
8144
+ pass.setIndexBuffer(mesh.indexBuffer, mesh.indexFormat);
7757
8145
  pass.drawIndexed(mesh.indexCount);
7758
8146
  }
7759
8147
  if (splits) {
@@ -7829,9 +8217,87 @@ export class WebGPURenderer {
7829
8217
  'an ordinary mesh, or drop the channel — the sway, sky and alpha lanes cannot reach an ' +
7830
8218
  'instanced pipeline.');
7831
8219
  }
7832
- const batch = new GpuInstancedBatch(this.surface.device, mesh, capacity, `flat.instances:${mesh.key ?? 'mesh'}`, options.cull ?? false);
8220
+ const animation = (options.animation ?? null);
8221
+ /* The bone a vertex follows is its second coordinates' u, which a mesh carries in its grain
8222
+ lane; a mesh with none has nothing to name a bone by. See `boneAnimation.ts`. */
8223
+ if (animation !== null && !this.presentOf(mesh)['grain']) {
8224
+ throw new Error('WebGPU: a bone animation reads each vertex’s bone from the mesh’s second coordinates ' +
8225
+ '(`MeshData.lightmapUvs`), and this mesh has none.');
8226
+ }
8227
+ const batch = new GpuInstancedBatch(this.surface.device, mesh, capacity, `flat.instances:${mesh.key ?? 'mesh'}`, options.cull ?? false, animation);
7833
8228
  return batch;
7834
8229
  }
8230
+ /** See `Renderer.setAnimationTime`. Every view taken after it reads the new clock. */
8231
+ setAnimationTime(seconds) {
8232
+ if (!Number.isFinite(seconds) || seconds === this.animationTime)
8233
+ return;
8234
+ this.animationTime = seconds;
8235
+ this.reopenViews();
8236
+ }
8237
+ /** The clock `setAnimationTime` set, 0 until it is. */
8238
+ animationTime = 0;
8239
+ /** See `Renderer.createBoneAnimation`. */
8240
+ createBoneAnimation(clip) {
8241
+ return new GpuBoneAnimation(this.surface.device, packBoneAnimation(clip));
8242
+ }
8243
+ /** See `Renderer.disposeBoneAnimation`. A batch still playing it draws nothing after. */
8244
+ disposeBoneAnimation(animation) {
8245
+ const clip = animation;
8246
+ if (!clip.disposed)
8247
+ clip.dispose();
8248
+ }
8249
+ /** An animated batch's clip and clocks, as its groups bind them. */
8250
+ animationViews(batch) {
8251
+ const animation = batch.animation;
8252
+ const clocks = batch.clocks;
8253
+ if (animation === null || clocks === null || animation.disposed)
8254
+ return null;
8255
+ return { places: animation.placesView, turns: animation.turnsView, clocks: clocks.view };
8256
+ }
8257
+ /** An animated batch's groups, dropped wherever the renderer drops its own. */
8258
+ freshAnimatedGroups(batch) {
8259
+ if (batch.groupEpoch === this.flatGroupEpoch)
8260
+ return;
8261
+ batch.litGroups.clear();
8262
+ batch.depthGroups.clear();
8263
+ batch.depthPeelGroups.clear();
8264
+ batch.groupEpoch = this.flatGroupEpoch;
8265
+ }
8266
+ /**
8267
+ * The lit group an animated batch draws with: the material's group, or its overlay twin, again
8268
+ * with the clip's and the clocks' textures — kept on the batch by the group it was built beside,
8269
+ * as a skinned twin is kept by its palette slot.
8270
+ */
8271
+ animatedGroup(batch, views) {
8272
+ this.freshAnimatedGroups(batch);
8273
+ const base = this.overlayGroup();
8274
+ let group = batch.litGroups.get(base);
8275
+ if (group === undefined) {
8276
+ this.overlayBinding = base !== this.bindGroup;
8277
+ group = createFlatBindGroup(this.surface.device, this.animatedBindGroupLayout, this.variant, this.perDraw.buffer, this.passRing.buffer, this.materialStore.buffer, this.viewRing.buffer, this.flatTextures, null, null, null, views);
8278
+ this.overlayBinding = false;
8279
+ this.bindGroupBudget.ask();
8280
+ batch.litGroups.set(base, group);
8281
+ }
8282
+ return group;
8283
+ }
8284
+ /** An animated caster's shadow group, by what it cuts with and whether this is the peel. */
8285
+ animatedDepthGroup(batch, views, cutout) {
8286
+ this.freshAnimatedGroups(batch);
8287
+ const peel = this.shadowLayerIsPeel && this.shadowPeelPassUniforms !== null;
8288
+ const groups = peel ? batch.depthPeelGroups : batch.depthGroups;
8289
+ const held = groups.get(cutout);
8290
+ if (held !== undefined)
8291
+ return held;
8292
+ const passUniforms = peel ? this.shadowPeelPassUniforms : this.shadowPassUniforms;
8293
+ const previousView = peel ? this.sunStaticArrayView : this.shadowBlankView;
8294
+ const previousSampler = peel ? this.shadowPeelSampler : this.shadowBlankSampler;
8295
+ if (passUniforms === null || previousView === null || previousSampler === null)
8296
+ return null;
8297
+ const group = createDepthBindGroup(this.surface.device, cutout === null ? this.shadowAnimatedLayout : this.shadowAnimatedCutoutLayout, this.shadowDraws.buffer, passUniforms, previousView, previousSampler, null, cutout === null ? null : { view: cutout.view, sampler: cutout.sampler }, null, views);
8298
+ groups.set(cutout, group);
8299
+ return group;
8300
+ }
7835
8301
  /** The device instance culls, built on the first culling draw. See `InstanceCullPass`. */
7836
8302
  instanceCulls = null;
7837
8303
  /** Counts frames, so a batch knows when its cull slots were last taken. */
@@ -7864,7 +8330,10 @@ export class WebGPURenderer {
7864
8330
  this.submitInstanced(batch, data, 1, false, {});
7865
8331
  /* A batch that moved this frame pairs each slot with last frame's; the batch is the identity. */
7866
8332
  const previous = batch.previousBuffer;
7867
- if (previous !== null && batch.changed === this.moverFrame && this.tracksMotionNow()) {
8333
+ if (previous !== null &&
8334
+ this.staticRecording === null &&
8335
+ batch.changed === this.moverFrame &&
8336
+ this.tracksMotionNow()) {
7868
8337
  const paired = pairedInstances(batch.previousCount, Math.min(data.count, batch.capacity));
7869
8338
  if (paired > 0)
7870
8339
  this.motionPass?.recordInstanced(batch.mesh, batch.buffer, previous, paired);
@@ -7905,6 +8374,8 @@ export class WebGPURenderer {
7905
8374
  this.perFrameFloats[tintAt] = refractTint?.[0] ?? 1;
7906
8375
  this.perFrameFloats[tintAt + 1] = refractTint?.[1] ?? 1;
7907
8376
  this.perFrameFloats[tintAt + 2] = refractTint?.[2] ?? 1;
8377
+ /* w: this framebuffer's first row is the top, so up the screen is down the rows. */
8378
+ this.perFrameFloats[tintAt + 3] = -1;
7908
8379
  const glassTintAt = this.materialField('uGlassTint');
7909
8380
  this.perFrameFloats[glassTintAt] = glass.tint[0];
7910
8381
  this.perFrameFloats[glassTintAt + 1] = glass.tint[1];
@@ -7969,10 +8440,17 @@ export class WebGPURenderer {
7969
8440
  /* A culling batch's camera draw: the whole batch against the view and the occluders on the CPU,
7970
8441
  then instance by instance on the device into an indirect draw. Blended draws keep every
7971
8442
  instance and their order. See `InstancedOptions.cull`. */
7972
- const culling = !blend && batch.cull;
8443
+ const culling = !blend && batch.cull && this.staticRecording === null;
7973
8444
  if (culling && !batchBoxVisible(batch.box, this.frustum, this.occlusion))
7974
8445
  return;
7975
- const cut = culling && cullsInstances(count, mesh.indexCount) ? this.takeCullSlot(batch, count) : null;
8446
+ /* A batch playing a bone animation reads its clocks by instance index, which a compaction
8447
+ would reorder: it is culled whole and never instance by instance. */
8448
+ const animation = batch.animation === null ? null : this.animationViews(batch);
8449
+ if (batch.animation !== null && animation === null)
8450
+ return;
8451
+ const cut = culling && animation === null && cullsInstances(count, mesh.indexCount)
8452
+ ? this.takeCullSlot(batch, count)
8453
+ : null;
7976
8454
  const instanceBuffer = cut === null ? batch.buffer : cut.instances;
7977
8455
  const slot = this.perDraw.allocate();
7978
8456
  if (slot === null) {
@@ -7984,11 +8462,19 @@ export class WebGPURenderer {
7984
8462
  }
7985
8463
  /* Late unless it moves with what it lies on. See `TranslucentMeshOptions.reconstructed`. */
7986
8464
  const lands = blend && options.reconstructed !== true && this.drawsLate();
7987
- this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uViewProj.offset, lands ? this.correctedViewProj : this.viewProj);
8465
+ const view = this.viewSlotFor(lands, lands ? this.correctedViewProj : this.viewProj);
8466
+ if (view === null)
8467
+ return;
8468
+ this.drawViewOffset = view;
7988
8469
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvScale.offset, this.uvScale);
7989
8470
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvOffset.offset, this.uvOffset);
7990
- this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uLightViewProj.offset, this.lightViewProj);
7991
8471
  this.perDraw.writeInt(slot, FLAT_VERT_FIELDS.uHasTangents.offset, mesh.hasTangents ? 1 : 0);
8472
+ /* The clip's speed and bone scale, read by the animated variant's own field. */
8473
+ const clip = batch.animation;
8474
+ if (animation !== null && clip !== null) {
8475
+ this.perDraw.writeFloat(slot, FLAT_ANIMATED_VERT_FIELDS.uBoneClip.offset, clip.framesPerSecond);
8476
+ this.perDraw.writeFloat(slot, FLAT_ANIMATED_VERT_FIELDS.uBoneClip.offset + 4, clip.boneScale);
8477
+ }
7992
8478
  const dimmed = opacity < 1;
7993
8479
  if (dimmed) {
7994
8480
  this.perFrameFloats[this.materialField('uOpacity')] = opacity;
@@ -8025,30 +8511,36 @@ export class WebGPURenderer {
8025
8511
  a third vertex buffer and a different vertex module, and sharing a cache entry with the
8026
8512
  plain one would hand a draw the wrong layout. */
8027
8513
  const adds = blend && options.additive === true;
8028
- const key = `${base}|inst${blend ? (adds ? '|add' : '|blend') : ''}` +
8514
+ const key = `${base}|inst${animation === null ? '' : '|anim'}${blend ? (adds ? '|add' : '|blend') : ''}` +
8029
8515
  `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
8030
8516
  `${this.materialDoubleSided ? '|2s' : ''}${coverage ? '|a2c' : ''}` +
8031
8517
  `${modelKind === null ? '' : `|m:${modelKind}`}`;
8032
8518
  const pipelines = blend ? this.blendedPipelines(lands) : this.targetPipelines();
8033
- let pipeline = pipelines.peek(key);
8034
- if (pipeline === undefined) {
8519
+ let pipeline = pipelines.peek(key) ?? null;
8520
+ if (pipeline === null) {
8035
8521
  const present = this.meshPresent.get(base);
8036
8522
  if (present === undefined) {
8037
8523
  throw new Error(`WebGPU: no mesh attributes recorded for ${base}; createMesh records them`);
8038
8524
  }
8039
- pipeline = flatPipeline(pipelines, this.surface.device, this.flatLayoutFor(false, false), this.variant, key, present, adds ? 'additive' : blend, false, false, depthWrite, layer, true, 'none', this.materialDoubleSided, coverage, false, false, modelKind);
8525
+ pipeline = flatPipeline(pipelines, this.surface.device, animation === null ? this.flatLayoutFor(false, false) : this.animatedBindGroupLayout, this.variant, key, present, adds ? 'additive' : blend, false, false, depthWrite, layer, true, 'none', this.materialDoubleSided, coverage, false, false, modelKind, 'whole', false, animation !== null);
8040
8526
  }
8041
- const material = this.materialSlotForDraw();
8042
- if (material !== null) {
8527
+ /* A batch whose pipeline is still compiling is left out, as a mesh is. */
8528
+ const material = pipeline === null ? null : this.materialSlotForDraw();
8529
+ if (pipeline !== null && material !== null) {
8043
8530
  const command = blend
8044
8531
  ? this.recordBlended(0, lands)
8045
8532
  : this.recordDraw(0, this.currentTarget());
8046
8533
  if (command !== null) {
8047
8534
  command.pipeline = pipeline;
8048
- command.bindGroup = this.flatBindGroupFor(false, false, null);
8535
+ command.bindGroup =
8536
+ animation === null
8537
+ ? this.flatBindGroupFor(false, false, null)
8538
+ : this.animatedGroup(batch, animation);
8049
8539
  command.offsetA = slot;
8050
- command.offsetB = material;
8051
- command.offsetCount = 2;
8540
+ command.offsetB = this.passOffset;
8541
+ command.offsetC = material;
8542
+ command.offsetD = this.drawViewOffset;
8543
+ command.offsetCount = 4;
8052
8544
  let index = 0;
8053
8545
  for (; index < mesh.vertexBuffers.length; index++) {
8054
8546
  command.vertexBuffers[index] = mesh.vertexBuffers[index];
@@ -8056,6 +8548,7 @@ export class WebGPURenderer {
8056
8548
  command.vertexBuffers[index] = instanceBuffer;
8057
8549
  command.vertexCount = mesh.vertexBuffers.length + 1;
8058
8550
  command.indexBuffer = mesh.indexBuffer;
8551
+ command.indexFormat = mesh.indexFormat;
8059
8552
  command.indexed = true;
8060
8553
  command.count = mesh.indexCount;
8061
8554
  command.instances = count;
@@ -8066,13 +8559,15 @@ export class WebGPURenderer {
8066
8559
  if (pass === null)
8067
8560
  return;
8068
8561
  pass.setPipeline(pipeline);
8069
- pass.setBindGroup(0, this.flatBindGroupFor(false, false, null), [slot, material]);
8562
+ pass.setBindGroup(0, animation === null
8563
+ ? this.flatBindGroupFor(false, false, null)
8564
+ : this.animatedGroup(batch, animation), [slot, this.passOffset, material, this.drawViewOffset]);
8070
8565
  let index = 0;
8071
8566
  for (; index < mesh.vertexBuffers.length; index++) {
8072
8567
  pass.setVertexBuffer(index, mesh.vertexBuffers[index]);
8073
8568
  }
8074
8569
  pass.setVertexBuffer(index, instanceBuffer);
8075
- pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
8570
+ pass.setIndexBuffer(mesh.indexBuffer, mesh.indexFormat);
8076
8571
  if (cut === null)
8077
8572
  pass.drawIndexed(mesh.indexCount, count);
8078
8573
  else
@@ -8080,7 +8575,7 @@ export class WebGPURenderer {
8080
8575
  }
8081
8576
  /* And its surface half, for the frame's materials' reflections, with the same survivors. */
8082
8577
  if (!blend && lit && depthWrite && this.reflectionSurfaceKeeps()) {
8083
- this.keepReflectionSurface(mesh, key, base, false, false, null, layer, false, false, modelKind, slot, material, instanceBuffer, count, cut === null ? null : cut.args);
8578
+ this.keepReflectionSurface(mesh, key, base, false, false, null, layer, false, false, modelKind, slot, material, instanceBuffer, count, cut === null ? null : cut.args, animation === null ? null : batch);
8084
8579
  }
8085
8580
  }
8086
8581
  /* Back to the defaults, unconditionally and for the reason `drawMesh` gives at length: this
@@ -8273,6 +8768,12 @@ export class WebGPURenderer {
8273
8768
  if (view === null)
8274
8769
  return false;
8275
8770
  this.lightViewProj.set(lightViewProj);
8771
+ /* The moving layer is read through the matrix it is drawn with: see `movingLightFor`. */
8772
+ if (dynamic) {
8773
+ this.movingLightViewProj.set(lightViewProj);
8774
+ this.movingLightKnown = true;
8775
+ }
8776
+ this.reopenViews();
8276
8777
  mat4.multiply(this.correctedLightViewProj, SHADOW_CLIP_CORRECTION, lightViewProj);
8277
8778
  this.shadowLayerIsPeel = peel;
8278
8779
  this.shadowLayerIsDynamic = dynamic;
@@ -8380,6 +8881,244 @@ export class WebGPURenderer {
8380
8881
  this.sceneCasterMaterial = undefined;
8381
8882
  casters(this.sceneCasterSink);
8382
8883
  }
8884
+ /* -- Static draws ---------------------------------------------------------------- */
8885
+ /** See `Renderer.createStaticDraws`. Recorded on its first draw, into each view it is drawn in. */
8886
+ createStaticDraws(casters) {
8887
+ return new GpuStaticDraws(this.surface.device, captureStaticDraws(casters), this.fragment.passSize, FLAT_VIEW_SIZE, FLAT_VERT_SIZE);
8888
+ }
8889
+ /**
8890
+ * See `Renderer.drawStaticDraws`. One bundle executed, after a pass block and a view block are
8891
+ * written for this view; the list is recorded first where it has no recording that is current for
8892
+ * this target and starting state. See `staticBundles.ts`.
8893
+ */
8894
+ drawStaticDraws(draws) {
8895
+ if (this.surface.lost)
8896
+ return;
8897
+ const list = draws;
8898
+ if (list.disposed) {
8899
+ this.warnStaticDisposed();
8900
+ return;
8901
+ }
8902
+ if (!this.canDraw() || this.viewProj === null)
8903
+ return;
8904
+ /* A list's draws are rigid and wear no overlay: put back to none, as `bindMeshPass` does. */
8905
+ this.setSkinPalette(null);
8906
+ this.setMorphWeights(null);
8907
+ this.setCloth(null);
8908
+ this.setSurfaceOverlay(null);
8909
+ this.setMaterial(null);
8910
+ this.sceneCasterMaterial = undefined;
8911
+ /* A probe face gives its slots back after every face; a list replays there draw by draw. */
8912
+ if (this.probePassActive) {
8913
+ this.replayStaticImmediately(list);
8914
+ return;
8915
+ }
8916
+ /* The copy first: taking one may grow the list's rings, which no recording before can bind. */
8917
+ const copy = list.takeCopy(this.moverFrame);
8918
+ const current = this.staticRecordingFor(list);
8919
+ if (current === null || current.immediate) {
8920
+ this.replayStaticImmediately(list);
8921
+ return;
8922
+ }
8923
+ current.usedIn = this.moverFrame;
8924
+ list.writePass(copy, this.passInts);
8925
+ this.writeView(list.views, list.viewOffset(copy), this.viewProj);
8926
+ list.uploadCopy(copy);
8927
+ const cache = current.cache;
8928
+ let frame = current.frameBundles[copy];
8929
+ if (frame === undefined || frame === null) {
8930
+ frame = encodeBundle(this.surface.device, {
8931
+ colorFormats: [cache.format],
8932
+ depthStencilFormat: DEPTH_FORMAT,
8933
+ sampleCount: cache.sampleCount,
8934
+ depthReadOnly: false,
8935
+ }, current.frame, list.passOffset(copy), list.viewOffset(copy), 'static.frame');
8936
+ current.frameBundles[copy] = frame;
8937
+ }
8938
+ const command = this.recordDraw(0, this.currentTarget());
8939
+ if (command !== null) {
8940
+ command.bundles = frame;
8941
+ command.pipeline = null;
8942
+ command.bindGroup = null;
8943
+ }
8944
+ else {
8945
+ this.openPass()?.executeBundles(frame);
8946
+ }
8947
+ const surface = this.reflectionSurface;
8948
+ if (current.surfaceKept && surface !== null && current.surface.taken > 0) {
8949
+ let halves = current.surfaceBundles[copy];
8950
+ if (halves === undefined || halves === null) {
8951
+ halves = encodeBundle(this.surface.device, {
8952
+ colorFormats: [null, REFLECTION_SURFACE_FORMAT, REFLECTION_SURFACE_FORMAT],
8953
+ depthStencilFormat: DEPTH_FORMAT,
8954
+ sampleCount: 1,
8955
+ depthReadOnly: true,
8956
+ }, current.surface, list.passOffset(copy), list.viewOffset(copy), 'static.surface');
8957
+ current.surfaceBundles[copy] = halves;
8958
+ }
8959
+ const kept = surface.take();
8960
+ kept.bundles = halves;
8961
+ }
8962
+ /* Counted as the draws would have been, so a frame reports the same on both backends. */
8963
+ for (let draw = 0; draw < current.frame.taken; draw++)
8964
+ this.drawBudget.ask();
8965
+ if (!this.staticJustRecorded) {
8966
+ for (let ask = 0; ask < current.materialAsks; ask++)
8967
+ this.materialBudget.ask();
8968
+ }
8969
+ this.staticJustRecorded = false;
8970
+ }
8971
+ /** A list drawn the way `drawSceneCasters` draws, a draw at a time, for where a bundle cannot be. */
8972
+ replayStaticImmediately(list) {
8973
+ this.staticJustRecorded = false;
8974
+ replayStaticDraws(list, this.sceneCasterSink);
8975
+ this.setMaterial(null);
8976
+ this.sceneCasterMaterial = undefined;
8977
+ }
8978
+ /** See `Renderer.disposeStaticDraws`. Its pins let go; its meshes and materials are the caller's. */
8979
+ disposeStaticDraws(draws) {
8980
+ const list = draws;
8981
+ if (list.disposed)
8982
+ return;
8983
+ list.dispose(this.unpinFor(list));
8984
+ }
8985
+ /** The recording to replay `list` from here, made now if none is current; null if it cannot be. */
8986
+ staticRecordingFor(list) {
8987
+ /* Geometry still arriving is not drawn yet, so a recording now would leave it out for good. */
8988
+ for (const entry of list.entries) {
8989
+ const mesh = entry.kind === 'mesh' ? entry.mesh : entry.batch.mesh;
8990
+ if (!mesh.complete)
8991
+ return null;
8992
+ }
8993
+ const cache = this.targetPipelines();
8994
+ const start = this.materialInts;
8995
+ const startHash = hashWords(start, start.length);
8996
+ const surfaceKept = this.reflectionSurfaceKeeps();
8997
+ let recording = list.find(cache, start, startHash, this.cutoutResolveStaged, surfaceKept);
8998
+ if (recording !== null &&
8999
+ recording.cacheVersion === cache.version &&
9000
+ recording.groupEpoch === this.flatGroupEpoch &&
9001
+ recording.storeEpoch === this.materialStore.epoch &&
9002
+ recording.ringEpoch === list.ringEpoch) {
9003
+ return recording;
9004
+ }
9005
+ const unpin = this.unpinFor(list);
9006
+ if (recording === null) {
9007
+ recording = list.open(cache, start, startHash, this.cutoutResolveStaged, surfaceKept, unpin);
9008
+ }
9009
+ else {
9010
+ list.clear(recording, recording.storeEpoch === this.materialStore.epoch ? unpin : () => { });
9011
+ }
9012
+ this.recordStatic(list, recording);
9013
+ return recording;
9014
+ }
9015
+ /** How a list's pins are let go: through the store, unless it has forgotten them since. */
9016
+ unpinFor(list) {
9017
+ const epoch = this.materialStore.epoch;
9018
+ return (offset) => {
9019
+ if (this.materialStore.epoch === epoch && list.pinEpoch === epoch) {
9020
+ this.materialStore.unpin(offset);
9021
+ }
9022
+ };
9023
+ }
9024
+ /**
9025
+ * Record `list` into `recording`: its entries through the scene-caster path, with the list's
9026
+ * rings and group cache swapped in for the frame's and the pass and view slots held at the list's
9027
+ * first copy, so every command the path records binds the list's buffers and offsets the bundle
9028
+ * can rewrite per copy. Everything swapped is put back, whatever the path throws.
9029
+ */
9030
+ recordStatic(list, recording) {
9031
+ if (list.groupEpoch !== this.flatGroupEpoch) {
9032
+ list.groups = new Map();
9033
+ list.blank = null;
9034
+ list.groupEpoch = this.flatGroupEpoch;
9035
+ }
9036
+ list.drawRing.reset();
9037
+ const perDraw = this.perDraw;
9038
+ const passRing = this.passRing;
9039
+ const viewRing = this.viewRing;
9040
+ const groups = this.flatBindGroups;
9041
+ const blank = this.blankAlbedoBindGroup;
9042
+ const bindGroup = this.bindGroup;
9043
+ const passSlot = this.passBlock.slot;
9044
+ const viewSlot = this.viewBlock.slot;
9045
+ const lateSlot = this.lateViewBlock.slot;
9046
+ const asked = this.materialBudget.used;
9047
+ this.perDraw = list.drawRing;
9048
+ this.passRing = list.passes;
9049
+ this.viewRing = list.views;
9050
+ this.flatBindGroups = list.groups;
9051
+ /* The blank group over the list's rings, built as the frame's is: with no maps held. */
9052
+ list.blank ??= this.buildFlatBindGroup();
9053
+ this.blankAlbedoBindGroup = list.blank;
9054
+ this.bindGroup = this.flatGroupForMaps(null, null, null, null, null);
9055
+ this.passBlock.slot = list.passOffset(0);
9056
+ this.viewBlock.slot = list.viewOffset(0);
9057
+ this.lateViewBlock.slot = list.viewOffset(0);
9058
+ this.materials.dirty();
9059
+ this.staticRecording = recording;
9060
+ /* A recorded list keeps what it recorded, so every draw of it waits for its pipeline. */
9061
+ this.skipCompiling(false);
9062
+ try {
9063
+ this.sceneCasterMaterial = undefined;
9064
+ replayStaticDraws(list, this.sceneCasterSink);
9065
+ }
9066
+ finally {
9067
+ this.skipCompiling(this.quality.pipelineCompile === 'skip');
9068
+ this.staticRecording = null;
9069
+ this.perDraw = perDraw;
9070
+ this.passRing = passRing;
9071
+ this.viewRing = viewRing;
9072
+ this.flatBindGroups = groups;
9073
+ this.blankAlbedoBindGroup = blank;
9074
+ this.bindGroup = bindGroup;
9075
+ this.passBlock.slot = passSlot;
9076
+ this.viewBlock.slot = viewSlot;
9077
+ this.lateViewBlock.slot = lateSlot;
9078
+ this.materials.dirty();
9079
+ this.sceneCasterMaterial = undefined;
9080
+ this.setMaterial(null);
9081
+ }
9082
+ recording.materialAsks = this.materialBudget.used - asked;
9083
+ const store = this.materialStore;
9084
+ recording.cacheVersion = recording.cache.version;
9085
+ recording.groupEpoch = this.flatGroupEpoch;
9086
+ recording.storeEpoch = store.epoch;
9087
+ recording.ringEpoch = list.ringEpoch;
9088
+ /* A skin split into halves is replayed draw by draw, so nothing recorded is kept or pinned. */
9089
+ if (recording.immediate) {
9090
+ resetPool(recording.frame);
9091
+ resetPool(recording.surface);
9092
+ return;
9093
+ }
9094
+ /* Every material the commands bind, pinned once, so nothing displaces what a bundle bakes. */
9095
+ for (const pool of [recording.frame, recording.surface]) {
9096
+ for (let i = 0; i < pool.taken; i++) {
9097
+ const offset = pool.commands[i].offsetC;
9098
+ if (!recording.pins.includes(offset)) {
9099
+ store.pin(offset);
9100
+ recording.pins.push(offset);
9101
+ }
9102
+ }
9103
+ }
9104
+ list.pinEpoch = store.epoch;
9105
+ list.uploadDraws();
9106
+ this.staticJustRecorded = true;
9107
+ }
9108
+ /** The recording under way, which `recordDraw` and the side passes take their commands into. */
9109
+ staticRecording = null;
9110
+ /** Whether the replay about to count its material changes recorded them, and so counted them. */
9111
+ staticJustRecorded = false;
9112
+ /** A disposed list drawn, said once: the frame loop does not throw. */
9113
+ warnStaticDisposed() {
9114
+ if (this.warnedStaticDisposed)
9115
+ return;
9116
+ this.warnedStaticDisposed = true;
9117
+ console.warn('drawStaticDraws: this list was disposed, and draws nothing.');
9118
+ }
9119
+ warnedStaticDisposed = false;
9120
+ /** Bumped wherever the lit groups are dropped, which a static list's own groups follow. */
9121
+ flatGroupEpoch = 0;
8383
9122
  drawShadowCasters(casters) {
8384
9123
  if (this.surface.lost || this.shadowPass === null)
8385
9124
  return;
@@ -8595,7 +9334,7 @@ export class WebGPURenderer {
8595
9334
  for (let index = 0; index < geometry.vertexBuffers.length; index++) {
8596
9335
  pass.setVertexBuffer(index, geometry.vertexBuffers[index]);
8597
9336
  }
8598
- pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
9337
+ pass.setIndexBuffer(geometry.indexBuffer, geometry.indexFormat);
8599
9338
  pass.drawIndexed(geometry.indexCount);
8600
9339
  },
8601
9340
  instanced: (batch, data, material, glass) => {
@@ -8629,7 +9368,7 @@ export class WebGPURenderer {
8629
9368
  pass.setVertexBuffer(index, geometry.vertexBuffers[index]);
8630
9369
  }
8631
9370
  pass.setVertexBuffer(index, gpuBatch.buffer);
8632
- pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
9371
+ pass.setIndexBuffer(geometry.indexBuffer, geometry.indexFormat);
8633
9372
  pass.drawIndexed(geometry.indexCount, count);
8634
9373
  },
8635
9374
  skinnedMesh: (mesh, model, palette, _material, glass) => {
@@ -8657,7 +9396,7 @@ export class WebGPURenderer {
8657
9396
  for (let index = 0; index < geometry.vertexBuffers.length; index++) {
8658
9397
  pass.setVertexBuffer(index, geometry.vertexBuffers[index]);
8659
9398
  }
8660
- pass.setIndexBuffer(geometry.indexBuffer, 'uint32');
9399
+ pass.setIndexBuffer(geometry.indexBuffer, geometry.indexFormat);
8661
9400
  pass.drawIndexed(geometry.indexCount);
8662
9401
  },
8663
9402
  };
@@ -8671,7 +9410,7 @@ export class WebGPURenderer {
8671
9410
  }
8672
9411
  this.shadowDraws.flush();
8673
9412
  this.scatterDepthDraws.flush();
8674
- this.surface.device.queue.submit([this.shadowEncoder.finish()]);
9413
+ this.submit([this.shadowEncoder.finish()]);
8675
9414
  /* A growth's old texture, copied out of by what was just submitted and needed no longer. */
8676
9415
  this.retiredSunTexture?.destroy();
8677
9416
  this.retiredSunTexture = null;
@@ -8905,7 +9644,7 @@ export class WebGPURenderer {
8905
9644
  * its own slot.
8906
9645
  */
8907
9646
  this.pointShadowArray?.flush();
8908
- this.surface.device.queue.submit([this.shadowEncoder.finish()]);
9647
+ this.submit([this.shadowEncoder.finish()]);
8909
9648
  this.shadowEncoder = null;
8910
9649
  this.shadowPass = null;
8911
9650
  this.gpuTimer.end();
@@ -9072,7 +9811,7 @@ export class WebGPURenderer {
9072
9811
  tintTarget.resolve(encoder, layer, face, offered);
9073
9812
  }
9074
9813
  buildFlatBindGroup() {
9075
- return createFlatBindGroup(this.surface.device, this.bindGroupLayout, this.variant, this.perDraw.buffer, this.perFrame.buffer, this.flatTextures);
9814
+ return createFlatBindGroup(this.surface.device, this.bindGroupLayout, this.variant, this.perDraw.buffer, this.passRing.buffer, this.materialStore.buffer, this.viewRing.buffer, this.flatTextures);
9076
9815
  }
9077
9816
  /**
9078
9817
  * The open material's flat group, or its twin with the surface overlay's atlas in the refraction
@@ -9112,11 +9851,11 @@ export class WebGPURenderer {
9112
9851
  /**
9113
9852
  * The bind group a draw uses, skinned or not.
9114
9853
  *
9115
- * **The skinned twin is cached against the two things that can invalidate it**, rather than
9116
- * against a flag somebody has to remember to clear at six call sites: the unskinned group it was
9117
- * built beside — which is rebuilt whenever the material or a shadow slot moves — and the palette
9118
- * view, which changes only when the texture is reallocated. Comparing object identity means a
9119
- * missed invalidation is not possible rather than merely unlikely, and it is why
9854
+ * **The skinned twin is kept by palette slot and by the unskinned group it was built beside**,
9855
+ * which is one a material, and compared against the views it holds — the palette's, which
9856
+ * changes only when the slot's texture grows, the morph deltas and the cloth's — rather than
9857
+ * against a flag somebody has to remember to clear at six call sites. Comparing object identity
9858
+ * means a missed invalidation is not possible rather than merely unlikely, and it is why
9120
9859
  * `SkinPaletteTexture.view()` caches: a fresh view every call never compares equal and would
9121
9860
  * rebuild this every draw.
9122
9861
  */
@@ -9130,23 +9869,31 @@ export class WebGPURenderer {
9130
9869
  const at = skinned ? this.skinPaletteSlot + 1 : 0;
9131
9870
  /* A skinned group carries the cloth's views, or the stand-in's: see `clothViews`. */
9132
9871
  const cloth = skinned ? this.clothViews() : null;
9133
- if ((this.skinnedGroups[at] ?? null) === null ||
9134
- this.skinnedGroupsBeside[at] !== base ||
9135
- this.skinnedGroupsPalette[at] !== palette ||
9136
- this.skinnedGroupsDeltas[at] !== deltas ||
9137
- this.skinnedGroupsCloth[at] !== (cloth?.particles ?? null) ||
9138
- this.skinnedGroupsClothBinding[at] !== (cloth?.binding ?? null)) {
9872
+ let byMaterial = this.skinnedGroups[at];
9873
+ if (byMaterial === undefined) {
9874
+ byMaterial = new Map();
9875
+ this.skinnedGroups[at] = byMaterial;
9876
+ }
9877
+ let twin = byMaterial.get(base);
9878
+ if (twin === undefined ||
9879
+ twin.palette !== palette ||
9880
+ twin.deltas !== deltas ||
9881
+ twin.cloth !== (cloth?.particles ?? null) ||
9882
+ twin.clothBinding !== (cloth?.binding ?? null)) {
9139
9883
  /* Built from the live material, with the overlay's atlas where `base` is its twin. */
9140
9884
  this.overlayBinding = base !== this.bindGroup;
9141
- this.skinnedGroups[at] = createFlatBindGroup(this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, this.perDraw.buffer, this.perFrame.buffer, this.flatTextures, palette, deltas, cloth);
9885
+ const group = createFlatBindGroup(this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, this.perDraw.buffer, this.passRing.buffer, this.materialStore.buffer, this.viewRing.buffer, this.flatTextures, palette, deltas, cloth);
9142
9886
  this.overlayBinding = false;
9143
- this.skinnedGroupsBeside[at] = base;
9144
- this.skinnedGroupsPalette[at] = palette;
9145
- this.skinnedGroupsDeltas[at] = deltas;
9146
- this.skinnedGroupsCloth[at] = cloth?.particles ?? null;
9147
- this.skinnedGroupsClothBinding[at] = cloth?.binding ?? null;
9887
+ twin = {
9888
+ group,
9889
+ palette,
9890
+ deltas,
9891
+ cloth: cloth?.particles ?? null,
9892
+ clothBinding: cloth?.binding ?? null,
9893
+ };
9894
+ byMaterial.set(base, twin);
9148
9895
  }
9149
- return this.skinnedGroups[at];
9896
+ return twin.group;
9150
9897
  }
9151
9898
  /** The layout matching a pair of vertex flags. A pipeline and its bind group must agree. */
9152
9899
  flatLayoutFor(skinned, morphed) {
@@ -9185,8 +9932,14 @@ export class WebGPURenderer {
9185
9932
  if (this.drawBudget.dropped > 0) {
9186
9933
  grew = this.perDraw.growTo(roomFor(this.drawBudget.used)) || grew;
9187
9934
  }
9188
- if (this.materialBudget.dropped > 0) {
9189
- grew = this.perFrame.growTo(roomFor(this.materialBudget.used)) || grew;
9935
+ if (this.materialsRefused > 0) {
9936
+ grew = this.materialStore.growTo(roomFor(this.materialBudget.used)) || grew;
9937
+ }
9938
+ if (this.passesRefused > 0) {
9939
+ grew = this.passRing.growTo(roomFor(this.passesAsked)) || grew;
9940
+ }
9941
+ if (this.viewsRefused > 0) {
9942
+ grew = this.viewRing.growTo(roomFor(this.viewsAsked)) || grew;
9190
9943
  }
9191
9944
  if (grew)
9192
9945
  this.rebuildFlatGroupsForNewRings();
@@ -9207,9 +9960,10 @@ export class WebGPURenderer {
9207
9960
  */
9208
9961
  rebuildFlatGroupsForNewRings() {
9209
9962
  this.flatBindGroups.clear();
9963
+ this.flatGroupEpoch += 1;
9210
9964
  this.overlayGroups.clear();
9211
- this.flatEnvironment = this.flatTextures('uEnvironment').view;
9212
9965
  this.skinnedGroups.length = 0;
9966
+ this.flatEnvironment = this.flatTextures('uEnvironment').view;
9213
9967
  const albedo = this.albedo;
9214
9968
  const normalMap = this.normalMap;
9215
9969
  const ormMap = this.ormMap;
@@ -9229,6 +9983,7 @@ export class WebGPURenderer {
9229
9983
  this.bindGroup = this.flatGroupForMaps(this.albedo, this.normalMap, this.ormMap, this.emissiveMap, this.modelMap);
9230
9984
  /* The open material lives in a slot of a buffer that no longer exists. */
9231
9985
  this.materials.dirty();
9986
+ this.reopenPass();
9232
9987
  }
9233
9988
  /**
9234
9989
  * The environment view the cached flat groups bind: the grid, or the blank stand-in while the
@@ -9260,7 +10015,9 @@ export class WebGPURenderer {
9260
10015
  */
9261
10016
  rebuildFlatBindGroup() {
9262
10017
  this.flatBindGroups.clear();
10018
+ this.flatGroupEpoch += 1;
9263
10019
  this.overlayGroups.clear();
10020
+ this.skinnedGroups.length = 0;
9264
10021
  this.flatEnvironment = this.flatTextures('uEnvironment').view;
9265
10022
  if (this.albedo === null &&
9266
10023
  this.normalMap === null &&
@@ -10768,6 +11525,7 @@ export class WebGPURenderer {
10768
11525
  /* Uncorrected, for the reason the shaft's own copy of this line carries: the shader does its
10769
11526
  own `* 0.5 + 0.5`, and correcting the matrix as well doubles it. */
10770
11527
  f.set(env?.lightViewProj ?? this.mediumIdentity, at('uLightViewProj'));
11528
+ f.set((env === null ? this.mediumIdentity : this.movingLightFor(env)), at('uMovingLightViewProj'));
10771
11529
  ints[at('uPeeledShadowEnabled')] = this.peelView === null || !this.peelFilled ? 0 : 1;
10772
11530
  device.queue.writeBuffer(this.mediumUniforms, 0, this.mediumStaging);
10773
11531
  const g = this.mediumUpsampleFloats;
@@ -11158,20 +11916,32 @@ export class WebGPURenderer {
11158
11916
  * `reflectionSurfacePass.ts` holds and tests for the depth its frame draw wrote. Replayed when the
11159
11917
  * frame's pass has ended, before the trace reads them. See `shaders/flat/reflectionSurface.ts`.
11160
11918
  */
11161
- keepReflectionSurface(mesh, frameKey, base, skinned, morphed, deltas, layer, eight, clothed, modelKind, slot, material, instanceBuffer, instances, indirect) {
11919
+ keepReflectionSurface(mesh, frameKey, base, skinned, morphed, deltas, layer, eight, clothed, modelKind, slot, material, instanceBuffer, instances, indirect,
11920
+ /** A batch playing a bone animation, whose surface half moves as its draw does. */
11921
+ animated = null) {
11162
11922
  const surface = this.reflectionSurface;
11163
11923
  const present = this.meshPresent.get(base);
11164
11924
  if (surface === null || present === undefined)
11165
11925
  return;
11926
+ const animation = animated === null ? null : this.animationViews(animated);
11166
11927
  const key = `${frameKey}|rs`;
11167
11928
  const pipeline = this.pipelines.peek(key) ??
11168
- flatPipeline(this.pipelines, this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, key, present, false, skinned, morphed, true, layer, instanceBuffer !== null, 'none', this.materialDoubleSided, false, eight, clothed, modelKind, 'whole', true);
11169
- const command = surface.take();
11929
+ flatPipeline(this.pipelines, this.surface.device, animation === null ? this.flatLayoutFor(skinned, morphed) : this.animatedBindGroupLayout, this.variant, key, present, false, skinned, morphed, true, layer, instanceBuffer !== null, 'none', this.materialDoubleSided, false, eight, clothed, modelKind, 'whole', true, animation !== null);
11930
+ /* Still compiling where draws skip: the probe stands in for this surface until it lands. */
11931
+ if (pipeline === null)
11932
+ return;
11933
+ /* A static list's surface halves go into its own pool, beside its draws. */
11934
+ const command = this.staticRecording === null ? surface.take() : takeRecorded(this.staticRecording.surface);
11170
11935
  command.pipeline = pipeline;
11171
- command.bindGroup = this.flatBindGroupFor(skinned, morphed, deltas);
11936
+ command.bindGroup =
11937
+ animated === null || animation === null
11938
+ ? this.flatBindGroupFor(skinned, morphed, deltas)
11939
+ : this.animatedGroup(animated, animation);
11172
11940
  command.offsetA = slot;
11173
- command.offsetB = material;
11174
- command.offsetCount = 2;
11941
+ command.offsetB = this.passOffset;
11942
+ command.offsetC = material;
11943
+ command.offsetD = this.drawViewOffset;
11944
+ command.offsetCount = 4;
11175
11945
  let index = 0;
11176
11946
  for (; index < mesh.vertexBuffers.length; index++) {
11177
11947
  command.vertexBuffers[index] = mesh.vertexBuffers[index];
@@ -11180,6 +11950,7 @@ export class WebGPURenderer {
11180
11950
  command.vertexBuffers[index++] = instanceBuffer;
11181
11951
  command.vertexCount = index;
11182
11952
  command.indexBuffer = mesh.indexBuffer;
11953
+ command.indexFormat = mesh.indexFormat;
11183
11954
  command.indexed = true;
11184
11955
  command.count = mesh.indexCount;
11185
11956
  command.instances = instances;
@@ -11191,7 +11962,36 @@ export class WebGPURenderer {
11191
11962
  * light alone and its colour alone into the blur's targets and test for the depth its frame half
11192
11963
  * wrote.
11193
11964
  */
11965
+ /**
11966
+ * Whether a split skin draw's two kept halves have pipelines, starting the compile of either that
11967
+ * has not: a skin drawn without them would lose its diffuse light for the frames they take, so a
11968
+ * split skin draws all three halves or none. See `PipelineCache.lit`.
11969
+ */
11970
+ skinHalvesReady(frameKey, base, skinned, morphed, layer, eight, clothed) {
11971
+ let ready = true;
11972
+ for (const half of SKIN_KEPT_HALVES) {
11973
+ const pipeline = this.skinHalfPipeline(half, frameKey, base, skinned, morphed, layer, eight, clothed);
11974
+ if (pipeline === null)
11975
+ ready = false;
11976
+ }
11977
+ return ready;
11978
+ }
11979
+ /** One kept half's pipeline, or none where it is still compiling: see `PipelineCache.lit`. */
11980
+ skinHalfPipeline(half, frameKey, base, skinned, morphed, layer, eight, clothed) {
11981
+ const pipelines = this.skinPipelines;
11982
+ const present = this.meshPresent.get(base);
11983
+ if (pipelines === null || present === undefined)
11984
+ return null;
11985
+ const key = `${frameKey}|${half === 'diffuse' ? 'sd' : 'sa'}`;
11986
+ return (pipelines.peek(key) ??
11987
+ flatPipeline(pipelines, this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, key, present, false, skinned, morphed, true, layer, false, 'none', this.materialDoubleSided, false, eight, clothed, 'skin', half));
11988
+ }
11194
11989
  keepSkinDiffuse(mesh, frameKey, base, skinned, morphed, deltas, layer, eight, clothed, slot, material) {
11990
+ /* A skin's halves are not bundled: a static list holding one replays draw by draw. */
11991
+ if (this.staticRecording !== null) {
11992
+ this.staticRecording.immediate = true;
11993
+ return;
11994
+ }
11195
11995
  const skin = this.skinScatter;
11196
11996
  const pipelines = this.skinPipelines;
11197
11997
  const present = this.meshPresent.get(base);
@@ -11200,20 +12000,24 @@ export class WebGPURenderer {
11200
12000
  const bindGroup = this.flatBindGroupFor(skinned, morphed, deltas);
11201
12001
  /* The light alone, which the blur spreads, then the colour alone, which it is multiplied by. */
11202
12002
  for (const half of SKIN_KEPT_HALVES) {
11203
- const key = `${frameKey}|${half === 'diffuse' ? 'sd' : 'sa'}`;
11204
- const pipeline = pipelines.peek(key) ??
11205
- flatPipeline(pipelines, this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, key, present, false, skinned, morphed, true, layer, false, 'none', this.materialDoubleSided, false, eight, clothed, 'skin', half);
12003
+ const pipeline = this.skinHalfPipeline(half, frameKey, base, skinned, morphed, layer, eight, clothed);
12004
+ /* Both landed before the draw was let through: see `skinHalvesReady`. */
12005
+ if (pipeline === null)
12006
+ return;
11206
12007
  const command = half === 'diffuse' ? skin.take() : skin.takeAlbedo();
11207
12008
  command.pipeline = pipeline;
11208
12009
  command.bindGroup = bindGroup;
11209
12010
  command.offsetA = slot;
11210
- command.offsetB = material;
11211
- command.offsetCount = 2;
12011
+ command.offsetB = this.passOffset;
12012
+ command.offsetC = material;
12013
+ command.offsetD = this.drawViewOffset;
12014
+ command.offsetCount = 4;
11212
12015
  for (let index = 0; index < mesh.vertexBuffers.length; index++) {
11213
12016
  command.vertexBuffers[index] = mesh.vertexBuffers[index];
11214
12017
  }
11215
12018
  command.vertexCount = mesh.vertexBuffers.length;
11216
12019
  command.indexBuffer = mesh.indexBuffer;
12020
+ command.indexFormat = mesh.indexFormat;
11217
12021
  command.indexed = true;
11218
12022
  command.count = mesh.indexCount;
11219
12023
  command.instances = 1;
@@ -11635,6 +12439,9 @@ export class WebGPURenderer {
11635
12439
  * site reads as one `if` either way.
11636
12440
  */
11637
12441
  recordDraw(reads, writes) {
12442
+ /* A static list recording takes its draws into its own pool, never the frame graph's. */
12443
+ if (this.staticRecording !== null)
12444
+ return takeRecorded(this.staticRecording.frame);
11638
12445
  /* The order-independent replay owns its pass and issues directly into it; a recorded command
11639
12446
  would be replayed into the frame's passes instead, which is not where these belong. */
11640
12447
  if (this.oitEncoder !== null)
@@ -11649,6 +12456,7 @@ export class WebGPURenderer {
11649
12456
  command.offsetCount = 0;
11650
12457
  command.vertexCount = 0;
11651
12458
  command.indexBuffer = null;
12459
+ command.indexFormat = 'uint32';
11652
12460
  command.indexed = false;
11653
12461
  command.instances = 1;
11654
12462
  command.indirect = null;
@@ -11682,10 +12490,26 @@ export class WebGPURenderer {
11682
12490
  }
11683
12491
  /** One recorded draw, issued into an open pass. Shared by the graph's replay and `runLate`. */
11684
12492
  issueCommand(pass, command) {
12493
+ /* A static list, recorded once: one call, and the pass's bindings are its to set again. */
12494
+ if (command.bundles !== null) {
12495
+ pass.executeBundles(command.bundles);
12496
+ return;
12497
+ }
11685
12498
  if (command.pipeline === null || command.bindGroup === null)
11686
12499
  return;
11687
12500
  pass.setPipeline(command.pipeline);
11688
- if (command.offsetCount === 2) {
12501
+ if (command.offsetCount === 4) {
12502
+ pass.setBindGroup(0, command.bindGroup, [
12503
+ command.offsetA,
12504
+ command.offsetB,
12505
+ command.offsetC,
12506
+ command.offsetD,
12507
+ ]);
12508
+ }
12509
+ else if (command.offsetCount === 3) {
12510
+ pass.setBindGroup(0, command.bindGroup, [command.offsetA, command.offsetB, command.offsetC]);
12511
+ }
12512
+ else if (command.offsetCount === 2) {
11689
12513
  pass.setBindGroup(0, command.bindGroup, [command.offsetA, command.offsetB]);
11690
12514
  }
11691
12515
  else if (command.offsetCount === 1) {
@@ -11700,7 +12524,7 @@ export class WebGPURenderer {
11700
12524
  pass.setVertexBuffer(i, buffer);
11701
12525
  }
11702
12526
  if (command.indexed && command.indexBuffer !== null) {
11703
- pass.setIndexBuffer(command.indexBuffer, 'uint32');
12527
+ pass.setIndexBuffer(command.indexBuffer, command.indexFormat);
11704
12528
  if (command.indirect !== null)
11705
12529
  pass.drawIndexedIndirect(command.indirect, 0);
11706
12530
  else
@@ -11951,7 +12775,7 @@ export class WebGPURenderer {
11951
12775
  this.pass?.end();
11952
12776
  this.pass = null;
11953
12777
  if (!this.surface.lost)
11954
- this.surface.device.queue.submit([this.encoder.finish()]);
12778
+ this.submit([this.encoder.finish()]);
11955
12779
  this.encoder = null;
11956
12780
  }
11957
12781
  endFrame() {
@@ -12015,7 +12839,7 @@ export class WebGPURenderer {
12015
12839
  * there would have nothing to ride on. See `GpuTimestamps.resolve`.
12016
12840
  */
12017
12841
  this.gpuTimer.resolve(this.encoder);
12018
- this.surface.device.queue.submit([this.encoder.finish()]);
12842
+ this.submit([this.encoder.finish()]);
12019
12843
  this.encoder = null;
12020
12844
  this.framePresented = true;
12021
12845
  /* Latched rather than assigned: `framePresented` goes back to false every `beginFrame`. */
@@ -12676,7 +13500,7 @@ export class WebGPURenderer {
12676
13500
  definition.dispatch(this.computeContext);
12677
13501
  pass.end();
12678
13502
  this.flushRings();
12679
- device.queue.submit([encoder.finish()]);
13503
+ this.submit([encoder.finish()]);
12680
13504
  }
12681
13505
  /** Let go of a definition. A handle kept past this dispatches nothing; see `ComputeHandle`. */
12682
13506
  unregisterCompute(handle) {
@@ -13730,7 +14554,7 @@ export class WebGPURenderer {
13730
14554
  .end();
13731
14555
  }
13732
14556
  }
13733
- device.queue.submit([clear.finish()]);
14557
+ this.submit([clear.finish()]);
13734
14558
  }
13735
14559
  /**
13736
14560
  * Declare where a grid's probes stand, and allocate the layers for them.
@@ -13941,7 +14765,7 @@ export class WebGPURenderer {
13941
14765
  if (whole)
13942
14766
  this.buildProbeChain(encoder, probe, this.probeSweeps?.writeLayer(layer) ?? layer);
13943
14767
  this.flushRings();
13944
- device.queue.submit([encoder.finish()]);
14768
+ this.submit([encoder.finish()]);
13945
14769
  this.rewindRings(skinMark);
13946
14770
  this.encoderEpoch = frameEpoch;
13947
14771
  this.encoder = savedEncoder;
@@ -13951,6 +14775,7 @@ export class WebGPURenderer {
13951
14775
  this.perFrameInts[this.materialField('uOutputTransform')] = heldGrade;
13952
14776
  this.perFrameFloats[this.materialField('uOutputExposure')] = heldExposure;
13953
14777
  this.materials.dirty();
14778
+ this.reopenPass();
13954
14779
  this.probePassActive = false;
13955
14780
  this.probeBounce = false;
13956
14781
  this.rebindEnvironment();
@@ -13998,7 +14823,7 @@ export class WebGPURenderer {
13998
14823
  this.uploadProbeFaces(probe, image);
13999
14824
  const encoder = device.createCommandEncoder({ label: 'probe.environmentImage' });
14000
14825
  this.buildProbeChain(encoder, probe, 0);
14001
- device.queue.submit([encoder.finish()]);
14826
+ this.submit([encoder.finish()]);
14002
14827
  this.markProbeFilled(0);
14003
14828
  this.probeAmbient = options?.irradiance ?? true;
14004
14829
  this.rebuildFlatBindGroup();
@@ -14023,7 +14848,7 @@ export class WebGPURenderer {
14023
14848
  const { device } = this.surface;
14024
14849
  const encoder = device.createCommandEncoder({ label: 'probe.layerImage' });
14025
14850
  this.buildProbeChain(encoder, probe, this.probeSweeps?.writeLayer(layer) ?? layer);
14026
- device.queue.submit([encoder.finish()]);
14851
+ this.submit([encoder.finish()]);
14027
14852
  this.markProbeFilled(layer);
14028
14853
  this.probeAmbient = options?.irradiance ?? true;
14029
14854
  this.rebuildFlatBindGroup();