@driftengine/core 4.9.0 → 4.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/CHANGELOG.json +62 -11
  2. package/README.md +43 -38
  3. package/dist/index.d.ts +3 -1
  4. package/dist/index.js +4 -3
  5. package/dist/render/ambientHarmonics.d.ts +1 -1
  6. package/dist/render/ambientHarmonics.js +1 -1
  7. package/dist/render/areaLights.d.ts +4 -4
  8. package/dist/render/areaLights.js +1 -1
  9. package/dist/render/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 +90 -2
  16. package/dist/render/backend/webgl2/renderer.js +253 -24
  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 +19 -4
  22. package/dist/render/backend/webgpu/buffers.js +18 -11
  23. package/dist/render/backend/webgpu/compressedUpload.d.ts +0 -1
  24. package/dist/render/backend/webgpu/compressedUpload.js +53 -8
  25. package/dist/render/backend/webgpu/depthPass.d.ts +59 -3
  26. package/dist/render/backend/webgpu/depthPass.js +49 -14
  27. package/dist/render/backend/webgpu/drawCommand.d.ts +18 -4
  28. package/dist/render/backend/webgpu/drawCommand.js +10 -3
  29. package/dist/render/backend/webgpu/flatPass.d.ts +85 -42
  30. package/dist/render/backend/webgpu/flatPass.js +136 -20
  31. package/dist/render/backend/webgpu/instanced.d.ts +14 -1
  32. package/dist/render/backend/webgpu/instanced.js +24 -1
  33. package/dist/render/backend/webgpu/materialStore.d.ts +101 -0
  34. package/dist/render/backend/webgpu/materialStore.js +265 -0
  35. package/dist/render/backend/webgpu/pipelineCache.d.ts +6 -0
  36. package/dist/render/backend/webgpu/pipelineCache.js +7 -0
  37. package/dist/render/backend/webgpu/renderer.d.ts +152 -35
  38. package/dist/render/backend/webgpu/renderer.js +787 -142
  39. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +7 -3
  40. package/dist/render/backend/webgpu/skinPaletteRing.js +13 -7
  41. package/dist/render/backend/webgpu/skinPaletteTexture.d.ts +6 -0
  42. package/dist/render/backend/webgpu/skinPaletteTexture.js +7 -1
  43. package/dist/render/backend/webgpu/staticBundles.d.ts +125 -0
  44. package/dist/render/backend/webgpu/staticBundles.js +241 -0
  45. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +10 -3
  46. package/dist/render/backend/webgpu/surfaceTexturePass.js +20 -2
  47. package/dist/render/backend/webgpu/uniformRing.d.ts +21 -0
  48. package/dist/render/backend/webgpu/uniformRing.js +9 -1
  49. package/dist/render/bloomChain.d.ts +3 -3
  50. package/dist/render/boneAnimation.d.ts +92 -0
  51. package/dist/render/boneAnimation.js +133 -0
  52. package/dist/render/compressedSource.d.ts +33 -13
  53. package/dist/render/compressedSource.js +70 -25
  54. package/dist/render/glCompressed.js +83 -0
  55. package/dist/render/instances.d.ts +26 -0
  56. package/dist/render/lightBudget.d.ts +10 -0
  57. package/dist/render/lightBudget.js +10 -0
  58. package/dist/render/lightmap.d.ts +1 -1
  59. package/dist/render/mesh.d.ts +5 -0
  60. package/dist/render/mesh.js +6 -0
  61. package/dist/render/pointLightSelection.d.ts +4 -4
  62. package/dist/render/screenSpaceReflection.d.ts +2 -1
  63. package/dist/render/shaders/boneAnimation.d.ts +19 -0
  64. package/dist/render/shaders/boneAnimation.js +63 -0
  65. package/dist/render/shaders/depth.d.ts +7 -0
  66. package/dist/render/shaders/depth.js +35 -10
  67. package/dist/render/shaders/flat/index.d.ts +6 -0
  68. package/dist/render/shaders/flat/index.js +27 -5
  69. package/dist/render/shaders/flat/main.d.ts +1 -1
  70. package/dist/render/shaders/flat/main.js +15 -5
  71. package/dist/render/shaders/flat/models.js +1 -1
  72. package/dist/render/shaders/flat/overlay.js +1 -1
  73. package/dist/render/shaders/flat/pointShadow.js +2 -2
  74. package/dist/render/shaders/flat/preamble.js +38 -24
  75. package/dist/render/shaders/generated/depth.wgsl.d.ts +93 -0
  76. package/dist/render/shaders/generated/depth.wgsl.js +93 -0
  77. package/dist/render/shaders/generated/flat.wgsl.d.ts +3077 -2903
  78. package/dist/render/shaders/generated/flat.wgsl.js +3100 -2925
  79. package/dist/render/shaders/outputTransform.d.ts +7 -0
  80. package/dist/render/shaders/outputTransform.js +28 -14
  81. package/dist/render/shaders/vertexChannel.d.ts +7 -0
  82. package/dist/render/shaders/vertexChannel.js +26 -0
  83. package/dist/render/staticDraws.d.ts +47 -0
  84. package/dist/render/staticDraws.js +43 -0
  85. package/dist/render/surfaceTexture.d.ts +38 -7
  86. package/dist/render/surfaceTexture.js +56 -17
  87. package/package.json +6 -6
  88. package/src/index.ts +8 -2
  89. package/src/render/ambientHarmonics.ts +1 -1
  90. package/src/render/areaLights.ts +4 -4
  91. package/src/render/backend/api.ts +13 -3
  92. package/src/render/backend/select.ts +10 -5
  93. package/src/render/backend/webgl2/boneAnimations.ts +105 -0
  94. package/src/render/backend/webgl2/instanced.ts +20 -1
  95. package/src/render/backend/webgl2/renderer.ts +315 -24
  96. package/src/render/backend/webgl2/skinPaletteTexture.ts +6 -4
  97. package/src/render/backend/webgpu/boneAnimations.ts +97 -0
  98. package/src/render/backend/webgpu/buffers.ts +42 -16
  99. package/src/render/backend/webgpu/compressedUpload.ts +59 -9
  100. package/src/render/backend/webgpu/depthPass.ts +51 -10
  101. package/src/render/backend/webgpu/drawCommand.ts +22 -4
  102. package/src/render/backend/webgpu/flatPass.ts +187 -14
  103. package/src/render/backend/webgpu/instanced.ts +32 -1
  104. package/src/render/backend/webgpu/materialStore.ts +276 -0
  105. package/src/render/backend/webgpu/pipelineCache.ts +7 -0
  106. package/src/render/backend/webgpu/renderer.ts +928 -155
  107. package/src/render/backend/webgpu/skinPaletteRing.ts +13 -7
  108. package/src/render/backend/webgpu/skinPaletteTexture.ts +7 -1
  109. package/src/render/backend/webgpu/staticBundles.ts +363 -0
  110. package/src/render/backend/webgpu/surfaceTexturePass.ts +29 -3
  111. package/src/render/backend/webgpu/uniformRing.ts +10 -1
  112. package/src/render/bloomChain.ts +3 -3
  113. package/src/render/boneAnimation.ts +223 -0
  114. package/src/render/compressedSource.ts +121 -29
  115. package/src/render/glCompressed.ts +83 -0
  116. package/src/render/instances.ts +27 -0
  117. package/src/render/lightBudget.ts +11 -0
  118. package/src/render/lightmap.ts +1 -1
  119. package/src/render/mesh.ts +6 -0
  120. package/src/render/pointLightSelection.ts +4 -4
  121. package/src/render/screenSpaceReflection.ts +2 -1
  122. package/src/render/shaders/boneAnimation.ts +63 -0
  123. package/src/render/shaders/depth.ts +45 -10
  124. package/src/render/shaders/flat/index.ts +33 -5
  125. package/src/render/shaders/flat/main.ts +15 -5
  126. package/src/render/shaders/flat/models.ts +1 -1
  127. package/src/render/shaders/flat/overlay.ts +1 -1
  128. package/src/render/shaders/flat/pointShadow.ts +2 -2
  129. package/src/render/shaders/flat/preamble.ts +38 -24
  130. package/src/render/shaders/generated/depth.wgsl.ts +95 -0
  131. package/src/render/shaders/generated/flat.wgsl.ts +3100 -2925
  132. package/src/render/shaders/outputTransform.ts +30 -14
  133. package/src/render/shaders/vertexChannel.ts +27 -0
  134. package/src/render/staticDraws.ts +95 -0
  135. package/src/render/surfaceTexture.ts +94 -27
@@ -61,7 +61,7 @@ import { GpuClothBinding, GpuClothParticles, clothStandIn } from './clothTexture
61
61
  import { MODEL_PARAM_FLOATS, eyeAxisInWorld, packModel, skinProfileDistance, } from '../../surfaceModel.js';
62
62
  import { OVERLAY_FLOATS, overlayLays, packSurfaceOverlay, } from '../../surfaceOverlay.js';
63
63
  import { frameReflectionsRefused, resolveFrameReflections, } from '../../frameReflections.js';
64
- import { ReflectionSurfacePass } from './reflectionSurfacePass.js';
64
+ import { REFLECTION_SURFACE_FORMAT, ReflectionSurfacePass } from './reflectionSurfacePass.js';
65
65
  import { validateClothBinding } from '../../clothBindingData.js';
66
66
  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
67
  import { createFilmBindGroup, createFilmBindGroupLayout, filmPipeline, FILM_FRAG_FIELDS, FILM_FRAG_SIZE, FILM_VERT_FIELDS, FILM_VERT_SIZE, } from './filmPass.js';
@@ -97,10 +97,15 @@ import { VIEW_MODEL_DEPTH_SHARE, viewModelDepthRange } from '../../viewModel.js'
97
97
  import { GlassCasterList } from '../../glassCasters.js';
98
98
  import { GpuGlassTint } from './glassTintPass.js';
99
99
  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';
100
+ import { VERTEX_LAYOUT, createGpuMesh, createGpuMeshIncremental, } from './buffers.js';
101
101
  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';
102
+ 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';
103
+ import { GpuBoneAnimation } from './boneAnimations.js';
104
+ import { packBoneAnimation } from '../../boneAnimation.js';
105
+ import { MaterialStore, hashWords } from './materialStore.js';
106
+ import { GpuStaticDraws, encodeBundle, takeRecorded } from './staticBundles.js';
107
+ import { captureStaticDraws, replayStaticDraws } from '../../staticDraws.js';
108
+ 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
109
  import { PipelineCache } from './pipelineCache.js';
105
110
  import { SKIN_TARGET_FORMAT, SkinScatterPass } from './skinScatterPass.js';
106
111
  import { SKY_FIELDS, SKY_UNIFORM_SIZE, createSkyBindGroup, createSkyBindGroupLayout, skyPipeline, } from './skyPass.js';
@@ -359,6 +364,13 @@ const MAX_WATER_BODIES_PER_FRAME = 16;
359
364
  * numbers, because that is precisely the defect the ring exists to end.
360
365
  */
361
366
  const MAX_BATCHES_PER_FRAME = 8;
367
+ /**
368
+ * Mesh passes a frame may open before the ring grows: the frame's own, a mirror's, a capture's, and
369
+ * a probe face's, which gives its slot back once submitted. A frame past it loses the draws of the
370
+ * passes that found no slot, counted on the `materials` line, and the ring is grown for the next.
371
+ * Each slot is the pass's block, 3,328 to 6,656 bytes by variant, so the ring is 213 to 426 KB.
372
+ */
373
+ const PASSES_PER_FRAME = 64;
362
374
  /** Buffer usage flags, from the specification. See `buffers.ts` for why they are not read off the global. */
363
375
  const USAGE_UNIFORM_DST = 0x0040 | 0x0008;
364
376
  /**
@@ -859,6 +871,8 @@ export class WebGPURenderer {
859
871
  skinnedBindGroupLayout;
860
872
  morphedBindGroupLayout;
861
873
  bothBindGroupLayout;
874
+ /** The instanced layout of a batch playing a bone animation. See `animatedGroup`. */
875
+ animatedBindGroupLayout;
862
876
  /** The morph weights the following draws use, or null for none. */
863
877
  morphWeights = null;
864
878
  /**
@@ -871,14 +885,15 @@ export class WebGPURenderer {
871
885
  * group per character per frame — an allocation in the frame loop, which `AGENTS.md` forbids.
872
886
  * Keyed by slot instead, the groups are built once and reused for the life of the renderer.
873
887
  */
888
+ /*
889
+ * **And by material within a slot**, keyed by the unskinned group each was built beside. One
890
+ * group a slot was rebuilt whenever the material beside it changed, so a character drawn in
891
+ * thirty-odd sections, each its own material, built one a section a frame — about ninety a frame
892
+ * for two characters, reported by a consumer. A slot's map holds one entry a material its rigs
893
+ * are drawn in, and is emptied wherever the unskinned groups are, whose identities it is keyed
894
+ * by.
895
+ */
874
896
  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
897
  /** The cloth the next skinned draws are placed by, or null. See `setCloth`. */
883
898
  cloth = null;
884
899
  clothStandCache = null;
@@ -946,22 +961,51 @@ export class WebGPURenderer {
946
961
  /** Every distinct cache above, so a mesh's pipelines are built for each. One entry when aliased. */
947
962
  pipelineTargets;
948
963
  bindGroupLayout;
949
- /** One slot per draw, uploaded once a frame. See `UniformRing`. */
964
+ /** One slot per draw, uploaded once a frame. See `UniformRing`. Swapped while a static list records. */
950
965
  perDraw;
951
- /** The lights, fog and camera, settled once per frame by `bindMeshPass`. */
952
- perFrame;
953
966
  /**
954
- * The material block as the CPU holds it, patched by the setters and copied into a slot.
967
+ * The lights, fog and camera, a slot per mesh pass a frame opens: `bindMeshPass` writes them, and
968
+ * the first draw after it takes a slot. A ring rather than one buffer because a frame opens more
969
+ * than one pass — a mirror's, a capture's, a probe face's — and a queue write does not interleave
970
+ * with the draws recorded between them. See `UniformRing`.
971
+ */
972
+ passRing;
973
+ /** Each material's block, kept across frames for as long as it does not change. */
974
+ materialStore;
975
+ /**
976
+ * Both blocks as the CPU holds them, the pass's then the material's, patched by name and copied
977
+ * into a slot by the next draw after something changed them. See `FlatFragmentScratch`.
955
978
  *
956
979
  * 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.
980
+ * the next `drawMesh` places it. That keeps a setter free of any knowledge of slots, which is
981
+ * what lets it be called anywhere a scene wants.
959
982
  */
960
983
  perFrameStaging;
961
984
  perFrameFloats;
962
985
  perFrameInts;
986
+ /** The two halves of the scratch, as the words a slot is written from. */
987
+ passInts;
988
+ materialInts;
963
989
  /** The slot the open material occupies, and whether the next draw must take a new one. */
964
990
  materials = new MaterialChanges();
991
+ /** The same for the pass's block: reopened by `bindMeshPass` and by nothing a material sets. */
992
+ passBlock = new MaterialChanges();
993
+ /**
994
+ * The vertex stage's view block — the camera, the light's matrix, the wind — a slot a pass, and a
995
+ * second, unjittered, for a blended draw landing after the upscale. Reopened with the pass, and by
996
+ * `setWind` and `beginShadowPass`, which change what it holds.
997
+ */
998
+ viewRing;
999
+ viewBlock = new MaterialChanges();
1000
+ lateViewBlock = new MaterialChanges();
1001
+ /** The view slot the draw being submitted bound, read by the halves it keeps for later passes. */
1002
+ drawViewOffset = 0;
1003
+ /** Pass slots asked for and refused this frame, and materials refused: what grows the two. */
1004
+ passesAsked = 0;
1005
+ passesRefused = 0;
1006
+ viewsAsked = 0;
1007
+ viewsRefused = 0;
1008
+ materialsRefused = 0;
965
1009
  /*
966
1010
  * The frame graph's recording, and the machinery that replays it.
967
1011
  *
@@ -1214,7 +1258,7 @@ export class WebGPURenderer {
1214
1258
  variant;
1215
1259
  /** Whether this profile compiled the point-shadow path, and so has sentinels to write. */
1216
1260
  pointShadowsCompiled;
1217
- /** What the generator recorded about that permutation: sizes, offsets and bindings. */
1261
+ /** What the generator recorded about that permutation, as the renderer's scratch lays it out. */
1218
1262
  fragment;
1219
1263
  depth = null;
1220
1264
  depthView = null;
@@ -1934,7 +1978,7 @@ export class WebGPURenderer {
1934
1978
  recordNode(this.arena, VERB_SCOPE, 0, MIRROR_TARGET, 0, 0);
1935
1979
  this.pass?.end();
1936
1980
  this.flushRings();
1937
- this.surface.device.queue.submit([this.encoder.finish()]);
1981
+ this.submit([this.encoder.finish()]);
1938
1982
  this.encoder = this.surface.device.createCommandEncoder({ label: 'reflection' });
1939
1983
  /*
1940
1984
  * **Opened on demand, like the frame's own.** Nothing has been recorded into the mirror yet,
@@ -1992,7 +2036,8 @@ export class WebGPURenderer {
1992
2036
  this.windStreakFrags,
1993
2037
  this.scatterVerts,
1994
2038
  this.scatterFrags,
1995
- this.perFrame,
2039
+ this.passRing,
2040
+ this.viewRing,
1996
2041
  this.filmVerts,
1997
2042
  this.filmFrags,
1998
2043
  this.insetUniforms,
@@ -2013,6 +2058,7 @@ export class WebGPURenderer {
2013
2058
  this.instanceCulls?.flush();
2014
2059
  for (const ring of this.rings())
2015
2060
  ring.flush();
2061
+ this.materialStore.flush();
2016
2062
  }
2017
2063
  /** Where every ring stands before work that submits an encoder of its own. */
2018
2064
  ringMarks = [];
@@ -2032,6 +2078,8 @@ export class WebGPURenderer {
2032
2078
  const rings = this.rings();
2033
2079
  for (let i = 0; i < rings.length; i++)
2034
2080
  rings[i].rewind(this.ringMarks[i] ?? 0);
2081
+ /* The open pass may sit in a slot just given back, which the next pass would overwrite. */
2082
+ this.reopenPass();
2035
2083
  this.skinPalettes.rewind(skinMark);
2036
2084
  if (this.skinPaletteSlot >= skinMark)
2037
2085
  this.skinPaletteSlot = -1;
@@ -2329,7 +2377,7 @@ export class WebGPURenderer {
2329
2377
  pass.end();
2330
2378
  this.pass = null;
2331
2379
  this.flushRings();
2332
- device.queue.submit([encoder.finish()]);
2380
+ this.submit([encoder.finish()]);
2333
2381
  this.rewindRings(skinMark);
2334
2382
  this.encoderEpoch = frameEpoch;
2335
2383
  this.encoder = savedEncoder;
@@ -2337,6 +2385,7 @@ export class WebGPURenderer {
2337
2385
  this.perFrameInts[this.materialField('uOutputTransform')] = heldGrade;
2338
2386
  this.perFrameFloats[this.materialField('uOutputExposure')] = heldExposure;
2339
2387
  this.materials.dirty();
2388
+ this.reopenPass();
2340
2389
  this.capturePassActive = false;
2341
2390
  }
2342
2391
  return true;
@@ -2352,10 +2401,11 @@ export class WebGPURenderer {
2352
2401
  updateSurfaceTexture(texture, source) {
2353
2402
  if (this.surface.lost)
2354
2403
  return;
2355
- const replaced = texture.update(source);
2404
+ const replaced = texture.update(source, this.compressedFormats);
2356
2405
  if (replaced === null)
2357
2406
  return;
2358
- /* A new size is a new view, so every group holding the old one is dropped — see below. */
2407
+ /* A new size or format is a new view, so every group holding the old one is dropped — see
2408
+ below. */
2359
2409
  this.retiredTextures.push(replaced);
2360
2410
  this.forgetBindingsOf(texture);
2361
2411
  }
@@ -2411,7 +2461,9 @@ export class WebGPURenderer {
2411
2461
  /* Same reasoning, for whichever SDF labels were bound against this atlas. */
2412
2462
  this.sdfTextBindGroups.delete(texture);
2413
2463
  this.flatBindGroups.clear();
2464
+ this.flatGroupEpoch += 1;
2414
2465
  this.overlayGroups.clear();
2466
+ this.skinnedGroups.length = 0;
2415
2467
  /*
2416
2468
  * The blank one is built with the albedo held aside, because `buildFlatBindGroup` reads
2417
2469
  * `this.albedo` for that slot — leaving it in place would put a real colour map into the
@@ -2573,6 +2625,13 @@ export class WebGPURenderer {
2573
2625
  const emissiveMap = (material?.emissive ?? null);
2574
2626
  const modelMap = (material?.modelMap ?? null);
2575
2627
  this.materialModel = material?.model ?? null;
2628
+ const reflectivity = material?.reflectivity;
2629
+ const environmentGain = material?.environmentGain;
2630
+ this.materialReflectivity =
2631
+ reflectivity === undefined ? null : Math.min(1, Math.max(0, reflectivity));
2632
+ this.materialEnvironmentGain =
2633
+ environmentGain === undefined ? null : Math.max(0, environmentGain);
2634
+ this.writeEnvironmentDials();
2576
2635
  /* What the blur spreads this skin by: the last material to name a profile says. */
2577
2636
  const model = material?.model;
2578
2637
  warnUnpagedLightmap(model, modelMap);
@@ -2592,7 +2651,8 @@ export class WebGPURenderer {
2592
2651
  const cut = this.materialField('uCutout');
2593
2652
  f[cut] = cutoff;
2594
2653
  f[cut + 1] = CUTOUT_RESOLVE_CODE[this.cutoutResolveStaged];
2595
- f[cut + 2] = this.cutoutFrame;
2654
+ /* z unused: the dither's frame is the pass's, in `uHighlightMin.w`. See `uCutout`. */
2655
+ f[cut + 2] = 0;
2596
2656
  /* The material's lighting channels, which the lamps' are compared with. See `uCutout`. */
2597
2657
  f[cut + 3] = lightChannelsOf(material?.lightChannels);
2598
2658
  i[flags + 3] = material?.doubleSided === true ? 1 : 0;
@@ -2764,14 +2824,28 @@ export class WebGPURenderer {
2764
2824
  }
2765
2825
  /** How reflective the surfaces drawn next are. Clamped, as `renderer.ts` clamps it. */
2766
2826
  setSurfaceReflectivity(amount) {
2767
- this.material((f) => {
2768
- f[this.materialField('uEnvironmentDials') + 1] = Math.min(1, Math.max(0, amount));
2769
- });
2827
+ this.passReflectivity = Math.min(1, Math.max(0, amount));
2828
+ this.writeEnvironmentDials();
2770
2829
  }
2771
2830
  /** How bright the environment the surfaces drawn next reflect is. See `renderer.ts`. */
2772
2831
  setEnvironmentGain(gain) {
2832
+ this.passEnvironmentGain = Math.max(0, gain);
2833
+ this.writeEnvironmentDials();
2834
+ }
2835
+ /**
2836
+ * The reflectivity and environment gain the setters gave the pass, and the ones the material
2837
+ * set now states, or null where it states none (`SurfaceMaterial.reflectivity`). A draw wears the
2838
+ * material's where it has one and the pass's otherwise, the rule `renderer.ts` keeps too.
2839
+ */
2840
+ passReflectivity = 0;
2841
+ passEnvironmentGain = 1;
2842
+ materialReflectivity = null;
2843
+ materialEnvironmentGain = null;
2844
+ writeEnvironmentDials() {
2773
2845
  this.material((f) => {
2774
- f[this.materialField('uEnvironmentDials') + 2] = Math.max(0, gain);
2846
+ const dials = this.materialField('uEnvironmentDials');
2847
+ f[dials + 1] = this.materialReflectivity ?? this.passReflectivity;
2848
+ f[dials + 2] = this.materialEnvironmentGain ?? this.passEnvironmentGain;
2775
2849
  });
2776
2850
  }
2777
2851
  setSurfaceGrain(amount) {
@@ -3242,7 +3316,7 @@ export class WebGPURenderer {
3242
3316
  }
3243
3317
  this.pass?.end();
3244
3318
  this.flushRings();
3245
- this.surface.device.queue.submit([this.encoder.finish()]);
3319
+ this.submit([this.encoder.finish()]);
3246
3320
  this.encoder = this.surface.device.createCommandEncoder({ label: 'frame.resumed' });
3247
3321
  this.reflectionPassActive = false;
3248
3322
  this.reflectionReady = true;
@@ -4626,6 +4700,7 @@ export class WebGPURenderer {
4626
4700
  */
4627
4701
  setWind(windX, windZ, windGust, timeSeconds) {
4628
4702
  resolveScatterDeform(windX, windZ, windGust, timeSeconds, null, this.frameWind);
4703
+ this.reopenViews();
4629
4704
  }
4630
4705
  /**
4631
4706
  * The five wind fields of one shadow-draw slot.
@@ -4643,21 +4718,6 @@ export class WebGPURenderer {
4643
4718
  this.shadowDraws.writeFloat(slot, DEPTH_VERT_FIELDS.uWindTime.offset, d.time);
4644
4719
  this.shadowDraws.writeFloats(slot, DEPTH_VERT_FIELDS.uWindSpatialPhase.offset, d.spatialPhase);
4645
4720
  }
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
4721
  /** Refilled per frame rather than allocated; see `resolvePointLights`. */
4662
4722
  lights = {
4663
4723
  count: 0,
@@ -4807,6 +4867,9 @@ export class WebGPURenderer {
4807
4867
  * at submit, and a texture's view is new when an update changes its size.
4808
4868
  */
4809
4869
  shadowCutoutLayout;
4870
+ /** A crowd's casters: the plain and cutout layouts with a bone animation's three textures. */
4871
+ shadowAnimatedLayout;
4872
+ shadowAnimatedCutoutLayout;
4810
4873
  cutoutGroups = new Map();
4811
4874
  cutoutPeelGroups = new Map();
4812
4875
  shadowPeelPassUniforms = null;
@@ -5019,19 +5082,47 @@ export class WebGPURenderer {
5019
5082
  if (count === 0)
5020
5083
  return;
5021
5084
  const cutout = cutoutOf(material);
5022
- const cutoutGroup = cutout === null ? null : this.cutoutGroup(cutout.albedo);
5085
+ /* A crowd casts where its clip has moved it, through the animated variant. */
5086
+ const animation = gpuBatch.animation === null ? null : this.animationViews(gpuBatch);
5087
+ if (gpuBatch.animation !== null && animation === null)
5088
+ return;
5089
+ const cutoutTexture = cutout === null ? null : cutout.albedo;
5090
+ const cutoutGroup = animation !== null
5091
+ ? this.animatedDepthGroup(gpuBatch, animation, cutoutTexture)
5092
+ : cutoutTexture === null
5093
+ ? null
5094
+ : this.cutoutGroup(cutoutTexture);
5095
+ if (animation !== null && cutoutGroup === null)
5096
+ return;
5023
5097
  const slot = this.shadowDraws.allocate();
5024
5098
  if (slot === null)
5025
5099
  return;
5026
5100
  /* Only the light matrix: the instanced depth variant has no uModel, its placement being
5027
5101
  the four attribute columns the batch's buffer supplies. */
5028
5102
  this.shadowDraws.writeFloats(slot, DEPTH_VERT_FIELDS.uLightViewProj.offset, this.correctedLightViewProj);
5029
- if (cutout !== null && cutoutGroup !== null) {
5103
+ const cuts = cutoutTexture !== null && cutoutGroup !== null;
5104
+ if (cutout !== null && cuts) {
5030
5105
  this.writeCutout(slot, cutout, DEPTH_INSTANCED_CUTOUT_VERT_FIELDS);
5031
5106
  }
5032
- const cuts = cutoutGroup !== null;
5107
+ const clip = gpuBatch.animation;
5108
+ if (animation !== null && clip !== null) {
5109
+ const fields = cuts
5110
+ ? DEPTH_INSTANCED_ANIMATED_CUTOUT_VERT_FIELDS
5111
+ : DEPTH_INSTANCED_ANIMATED_VERT_FIELDS;
5112
+ this.shadowDraws.writeFloat(slot, fields.uSceneTime.offset, this.animationTime);
5113
+ this.shadowDraws.writeFloat(slot, fields.uBoneClip.offset, clip.framesPerSecond);
5114
+ this.shadowDraws.writeFloat(slot, fields.uBoneClip.offset + 4, clip.boneScale);
5115
+ }
5033
5116
  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));
5117
+ const animated = animation !== null;
5118
+ pass.setPipeline(depthPipeline(this.pipelines, this.surface.device, animated
5119
+ ? cuts
5120
+ ? this.shadowAnimatedCutoutLayout
5121
+ : this.shadowAnimatedLayout
5122
+ : cuts
5123
+ ? this.shadowCutoutLayout
5124
+ : this.shadowLayout, `${cuts ? 'depth-instanced-cutout' : 'depth-instanced'}${animated ? '-animated' : ''}` +
5125
+ `|${cull}|${geometry.key ?? ''}`, this.presentOf(geometry), cull, false, true, cuts, false, false, animated));
5035
5126
  pass.setBindGroup(0, cutoutGroup ?? this.shadowGroup(), [slot]);
5036
5127
  let index = 0;
5037
5128
  for (; index < geometry.vertexBuffers.length; index++) {
@@ -5472,7 +5563,7 @@ export class WebGPURenderer {
5472
5563
  pointShadows: quality.pointShadows,
5473
5564
  });
5474
5565
  this.pointShadowsCompiled = quality.pointShadows;
5475
- this.fragment = flatFragmentBindings(this.variant);
5566
+ this.fragment = flatFragmentScratch(this.variant);
5476
5567
  this.bindGroupLayout = createFlatBindGroupLayout(device, this.variant);
5477
5568
  /*
5478
5569
  * The skinned twin, built eagerly because it is a descriptor rather than a compile — WebGPU
@@ -5482,24 +5573,32 @@ export class WebGPURenderer {
5482
5573
  this.skinnedBindGroupLayout = createFlatBindGroupLayout(device, this.variant, true);
5483
5574
  this.morphedBindGroupLayout = createFlatBindGroupLayout(device, this.variant, false, true);
5484
5575
  this.bothBindGroupLayout = createFlatBindGroupLayout(device, this.variant, true, true);
5576
+ /* And the instanced twin that plays a bone animation: its clip's and clocks' textures. */
5577
+ this.animatedBindGroupLayout = createFlatBindGroupLayout(device, this.variant, false, false, true);
5485
5578
  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);
5579
+ const { passSize, materialSize } = this.fragment;
5580
+ this.perFrameStaging = new ArrayBuffer(passSize + materialSize);
5487
5581
  this.perFrameFloats = new Float32Array(this.perFrameStaging);
5488
5582
  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);
5583
+ this.passInts = new Int32Array(this.perFrameStaging, 0, passSize / 4);
5584
+ this.materialInts = new Int32Array(this.perFrameStaging, passSize, materialSize / 4);
5585
+ /*
5586
+ * **Two blocks, because they change at two rates.** `setSurfaceTexture`, `setSurfaceGrain` and
5587
+ * their siblings are pass state on both backends — a material covers many draws — and on WebGL2
5588
+ * each is a `uniform1f` that lands immediately. Here they live in a block, and
5589
+ * `queue.writeBuffer` does not interleave with recorded commands, so one buffer rewritten between
5590
+ * two draws gives *both* the second value: every surface would wear the last material set, which
5591
+ * is bug 7's shape at pass scale. So each draw binds a slot.
5592
+ *
5593
+ * It was one block until 4.10.0, lights and all, copied whole into a ring slot per material
5594
+ * change and uploaded every frame: a stage of four hundred materials sent 4.8 MB a frame to say
5595
+ * what it had said the frame before. Now the pass's block takes a ring slot per pass, and a
5596
+ * material's block a slot of the store, where an unchanged material stays from frame to frame.
5597
+ */
5598
+ this.passRing = new UniformRing(device, passSize, PASSES_PER_FRAME, USAGE_UNIFORM_DST, 'flat.passRing');
5599
+ this.materialStore = new MaterialStore(device, materialSize, this.quality.materialChangesPerFrame, USAGE_UNIFORM_DST, 'flat.materials', this.materialBudget);
5600
+ /* Two a pass at most: the jittered view and the unjittered one. */
5601
+ this.viewRing = new UniformRing(device, FLAT_VIEW_SIZE, 2 * PASSES_PER_FRAME, USAGE_UNIFORM_DST, 'flat.viewRing');
5503
5602
  /*
5504
5603
  * A one-pixel stand-in for the albedo binding. The albedo's flag is zero for untextured
5505
5604
  * geometry, which is nearly all of it, so the shader never samples this — but WebGPU
@@ -5632,7 +5731,7 @@ export class WebGPURenderer {
5632
5731
  })
5633
5732
  .end();
5634
5733
  }
5635
- device.queue.submit([clear.finish()]);
5734
+ this.submit([clear.finish()]);
5636
5735
  /*
5637
5736
  * `nearest` on a depth texture sampled as an ordinary float, matching what
5638
5737
  * `shadowMap.ts` asks WebGL2 for. A filtering sampler cannot be used on a depth format
@@ -6120,6 +6219,8 @@ export class WebGPURenderer {
6120
6219
  this.shadowLayout = createDepthBindGroupLayout(device);
6121
6220
  this.shadowSkinnedLayout = createDepthBindGroupLayout(device, true);
6122
6221
  this.shadowCutoutLayout = createDepthBindGroupLayout(device, false, true);
6222
+ this.shadowAnimatedLayout = createDepthBindGroupLayout(device, false, false, true);
6223
+ this.shadowAnimatedCutoutLayout = createDepthBindGroupLayout(device, false, true, true);
6123
6224
  this.shadowDraws = new UniformRing(device,
6124
6225
  /* The cutout block is the plain one and two fields more, and a slot must hold either. */
6125
6226
  Math.max(DEPTH_VERT_SIZE, DEPTH_CUTOUT_VERT_SIZE), quality.drawsPerFrame, USAGE_UNIFORM_DST, 'shadow.drawRing', this.shadowDrawBudget);
@@ -6418,9 +6519,18 @@ export class WebGPURenderer {
6418
6519
  this.lineVerts.reset();
6419
6520
  this.lineFrags.reset();
6420
6521
  this.skyUniforms.reset();
6421
- /* The material ring, and the open slot with it: last frame's slots are gone. */
6422
- this.perFrame.reset();
6522
+ /* The pass ring, and the open slots with it: last frame's pass slots are gone. Its materials
6523
+ stay in the store, which a draw finds them in again. */
6524
+ this.passRing.reset();
6525
+ this.viewRing.reset();
6526
+ this.materialStore.beginFrame();
6527
+ this.passesAsked = 0;
6528
+ this.passesRefused = 0;
6529
+ this.viewsAsked = 0;
6530
+ this.viewsRefused = 0;
6531
+ this.materialsRefused = 0;
6423
6532
  this.materials.dirty();
6533
+ this.reopenPass();
6424
6534
  /* Spent by `runMotion` at the end of the last frame; a frame states its movers afresh. */
6425
6535
  this.motionPass?.reset();
6426
6536
  this.latePass?.reset();
@@ -6718,6 +6828,37 @@ export class WebGPURenderer {
6718
6828
  },
6719
6829
  };
6720
6830
  }
6831
+ /**
6832
+ * The copies this frame's mesh rewrites owe (`StagedRewrite`), recorded since the last submit and
6833
+ * sent at the head of the next one: one encoder however many meshes are rewritten. Null while
6834
+ * nothing is owed.
6835
+ */
6836
+ pendingRewrites = null;
6837
+ /** One record handed to every staged rewrite, refilled each call, so a rewrite allocates nothing. */
6838
+ stagedRewrite = {
6839
+ encoder: null,
6840
+ staging: null,
6841
+ previous: null,
6842
+ };
6843
+ rewriteCopies() {
6844
+ this.pendingRewrites ??= this.surface.device.createCommandEncoder({
6845
+ label: 'mesh.rewriteCopies',
6846
+ });
6847
+ return this.pendingRewrites;
6848
+ }
6849
+ /**
6850
+ * Submit `buffers`, led by any copies the frame's mesh rewrites owe. **Every submit of this
6851
+ * renderer goes through here**, because any of them may draw a rewritten mesh — the frame's own,
6852
+ * a mirror's, a shadow's, a probe's — and the copies must be ahead of the first that does.
6853
+ */
6854
+ submit(buffers) {
6855
+ const rewrites = this.pendingRewrites;
6856
+ if (rewrites !== null) {
6857
+ this.pendingRewrites = null;
6858
+ buffers.unshift(rewrites.finish());
6859
+ }
6860
+ this.surface.device.queue.submit(buffers);
6861
+ }
6721
6862
  /**
6722
6863
  * Rewrite a mesh's positions, and its normals where the caller has them.
6723
6864
  *
@@ -6738,20 +6879,35 @@ export class WebGPURenderer {
6738
6879
  * `changeFrames.ts`.
6739
6880
  */
6740
6881
  const motion = mesh.motion;
6741
- let previous = null;
6882
+ let staged = null;
6742
6883
  if (motion !== null && this.reconstructionWanted) {
6743
6884
  const frame = frameOfChange(this.moverFrame, this.inFrame);
6885
+ const size = mesh.vertexBuffers[0]?.size ?? 0;
6886
+ let previous = null;
6744
6887
  if (keepsPrevious(motion.changed, frame)) {
6745
6888
  motion.previous ??= this.surface.device.createBuffer({
6746
6889
  label: 'recon.dynamicPrevious',
6747
- size: mesh.vertexBuffers[0]?.size ?? 0,
6890
+ size,
6748
6891
  usage: 0x0020 | 0x0008, // VERTEX | COPY_DST
6749
6892
  });
6750
6893
  previous = motion.previous;
6751
6894
  }
6752
6895
  motion.changed = frame;
6896
+ /*
6897
+ * **Staged, and copied in behind the copy aside, in an encoder the frame's next submit leads
6898
+ * with**: no submit of the rewrite's own. See `StagedRewrite` and `submit`.
6899
+ */
6900
+ motion.staging ??= this.surface.device.createBuffer({
6901
+ label: 'mesh.stagedPositions',
6902
+ size,
6903
+ usage: 0x0004 | 0x0008, // COPY_SRC | COPY_DST
6904
+ });
6905
+ staged = this.stagedRewrite;
6906
+ staged.encoder = this.rewriteCopies();
6907
+ staged.staging = motion.staging;
6908
+ staged.previous = previous;
6753
6909
  }
6754
- mesh.update(this.surface.device, positions, normals, previous);
6910
+ mesh.update(this.surface.device, positions, normals, staged);
6755
6911
  }
6756
6912
  /**
6757
6913
  * Settle everything the frame shares: the camera, the lights and the fog.
@@ -6941,6 +7097,11 @@ export class WebGPURenderer {
6941
7097
  * number, so this is a copy and is marked as one; the two must move together.
6942
7098
  */
6943
7099
  f[at('uGrain')] = 1;
7100
+ /* No reflection and an environment at its own brightness, from the pass and from no material. */
7101
+ this.passReflectivity = 0;
7102
+ this.passEnvironmentGain = 1;
7103
+ this.materialReflectivity = null;
7104
+ this.materialEnvironmentGain = null;
6944
7105
  f[at('uEnvironmentDials') + 1] = 0;
6945
7106
  /* No weighting and no dither until a pass or a crossfade asks. See `setDitherFade`. */
6946
7107
  f[at('uWriteMode')] = 0;
@@ -7051,6 +7212,8 @@ export class WebGPURenderer {
7051
7212
  if (this.quality.nightEmissive)
7052
7213
  f[at('uNightEmissive')] = env.nightEmissive ?? 0;
7053
7214
  f.set(env.highlightMin, at('uHighlightMin'));
7215
+ /* The frame the cutout dither moves with, in the near corner's w. See `uHighlightMin`. */
7216
+ f[at('uHighlightMin') + 3] = this.cutoutFrame;
7054
7217
  f.set(env.highlightMax, at('uHighlightMax'));
7055
7218
  /* The gain in the far corner's w. See `uHighlightMax`. */
7056
7219
  f[at('uHighlightMax') + 3] = env.highlightGain;
@@ -7303,6 +7466,7 @@ export class WebGPURenderer {
7303
7466
  * same terms here, which is why a pass cannot inherit a material from the one before it.
7304
7467
  */
7305
7468
  this.materials.dirty();
7469
+ this.reopenPass();
7306
7470
  /*
7307
7471
  * A pass starts with no material, exactly as `renderer.ts` resets these terms — **every map,
7308
7472
  * not the albedo alone**. `setMaterial` skips the lookup when the maps it is given are the ones
@@ -7326,29 +7490,104 @@ export class WebGPURenderer {
7326
7490
  this.uvOffset[1] = 0;
7327
7491
  }
7328
7492
  /**
7329
- * The slot the next draw's material lives in, taking a new one when something dirtied it.
7493
+ * The slot the next draw's material lives in, placing it when something dirtied it, with the
7494
+ * pass's slot taken first where `bindMeshPass` reopened it: `passOffset` is that one's offset.
7330
7495
  *
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.
7496
+ * **A placement per material change, not per draw.** A scene drawing a hundred meshes of one
7497
+ * material places it once; a scene switching material every draw places one each, and a material
7498
+ * the store already holds — the same numbers as a draw before it, this frame or an earlier one —
7499
+ * is bound where it is and uploads nothing.
7334
7500
  */
7335
7501
  materialSlotForDraw() {
7502
+ if (!this.passBlock.open) {
7503
+ this.passesAsked += 1;
7504
+ const pass = this.passRing.allocate();
7505
+ if (pass === null) {
7506
+ this.passesRefused += 1;
7507
+ this.materialBudget.ask();
7508
+ this.materialBudget.drop();
7509
+ if (!this.warnedPassesFull) {
7510
+ this.warnedPassesFull = true;
7511
+ 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.`);
7512
+ }
7513
+ return null;
7514
+ }
7515
+ this.passRing.writeBlock(pass, this.passInts);
7516
+ this.passBlock.slot = pass;
7517
+ }
7336
7518
  if (this.materials.open)
7337
7519
  return this.materials.slot;
7338
- const slot = this.perFrame.allocate();
7520
+ const slot = this.materialStore.place(this.materialInts);
7339
7521
  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
- }
7522
+ this.materialsRefused += 1;
7523
+ this.warnMaterialsFull();
7344
7524
  return null;
7345
7525
  }
7346
- this.perFrame.writeBlock(slot, this.perFrameInts);
7347
7526
  this.materials.slot = slot;
7348
7527
  return slot;
7349
7528
  }
7529
+ /** The pass slot `materialSlotForDraw` left open, read beside the material's by every lit draw. */
7530
+ get passOffset() {
7531
+ return this.passBlock.slot;
7532
+ }
7533
+ /** A new pass: its block, and both its views, are taken afresh by the next draw. */
7534
+ reopenPass() {
7535
+ this.passBlock.dirty();
7536
+ this.reopenViews();
7537
+ }
7538
+ /** What a view holds changed — the wind, the light's matrix — so the next draw takes a new one. */
7539
+ reopenViews() {
7540
+ this.viewBlock.dirty();
7541
+ this.lateViewBlock.dirty();
7542
+ }
7543
+ /**
7544
+ * The view slot a lit draw reads its camera from: the pass's, or its unjittered twin for a blended
7545
+ * draw landing after the upscale. Taken by the first draw that needs it after the pass, the wind or
7546
+ * the light's matrix changed, and shared by every draw until then. Null when the ring is full, which
7547
+ * skips the draw and grows the ring for the next frame, as the pass ring does.
7548
+ */
7549
+ viewSlotFor(late, viewProj) {
7550
+ const block = late ? this.lateViewBlock : this.viewBlock;
7551
+ if (block.open)
7552
+ return block.slot;
7553
+ this.viewsAsked += 1;
7554
+ const slot = this.viewRing.allocate();
7555
+ if (slot === null) {
7556
+ this.viewsRefused += 1;
7557
+ this.materialBudget.ask();
7558
+ this.materialBudget.drop();
7559
+ if (!this.warnedPassesFull) {
7560
+ this.warnedPassesFull = true;
7561
+ 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.`);
7562
+ }
7563
+ return null;
7564
+ }
7565
+ this.writeView(this.viewRing, slot, viewProj);
7566
+ block.slot = slot;
7567
+ return slot;
7568
+ }
7569
+ /** A view block: the camera, the light's matrix and the frame's wind, into `ring` at `slot`. */
7570
+ writeView(ring, slot, viewProj) {
7571
+ ring.writeFloats(slot, FLAT_VIEW_FIELDS.uViewProj.offset, viewProj);
7572
+ ring.writeFloats(slot, FLAT_VIEW_FIELDS.uLightViewProj.offset, this.lightViewProj);
7573
+ /* The scene's clock, which a bone animation's instances read their moment from. */
7574
+ ring.writeFloat(slot, FLAT_VIEW_FIELDS.uSceneTime.offset, this.animationTime);
7575
+ const d = this.frameWind;
7576
+ ring.writeFloats(slot, FLAT_VIEW_FIELDS.uWindDirection.offset, d.direction);
7577
+ ring.writeFloat(slot, FLAT_VIEW_FIELDS.uWindSpeed.offset, d.bend);
7578
+ ring.writeFloat(slot, FLAT_VIEW_FIELDS.uWindGust.offset, d.gust);
7579
+ ring.writeFloat(slot, FLAT_VIEW_FIELDS.uWindTime.offset, d.time);
7580
+ ring.writeFloats(slot, FLAT_VIEW_FIELDS.uWindSpatialPhase.offset, d.spatialPhase);
7581
+ }
7582
+ warnMaterialsFull() {
7583
+ if (this.warnedMaterialsFull)
7584
+ return;
7585
+ this.warnedMaterialsFull = true;
7586
+ 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.`);
7587
+ }
7350
7588
  /** Said once rather than every frame, for the reason `warnedFull` gives. */
7351
7589
  warnedMaterialsFull = false;
7590
+ warnedPassesFull = false;
7352
7591
  warnedSkyFull = false;
7353
7592
  warnedParticlesTwice = false;
7354
7593
  /**
@@ -7383,8 +7622,12 @@ export class WebGPURenderer {
7383
7622
  * What this saves is the GPU's share, which for a heavy mesh is most of the cost and for a
7384
7623
  * trivial one is very little.
7385
7624
  */
7386
- if (this.quality.cullDraws && !this.visible(mesh.bounds, model))
7625
+ /* A static list draws every entry in every view, since a bundle cannot leave one out. */
7626
+ if (this.quality.cullDraws &&
7627
+ this.staticRecording === null &&
7628
+ !this.visible(mesh.bounds, model)) {
7387
7629
  return;
7630
+ }
7388
7631
  /*
7389
7632
  * And the other half of the same question, where the profile asked for one. Gated on
7390
7633
  * `cullDraws` alongside the frustum test, because both are the renderer doing a consumer's
@@ -7549,17 +7792,17 @@ export class WebGPURenderer {
7549
7792
  `TranslucentMeshOptions.reconstructed`. */
7550
7793
  const lands = blend && options.reconstructed !== true && this.drawsLate();
7551
7794
  const late = lands || (blend && this.oitReplaying && this.reconstructing);
7552
- this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uViewProj.offset, late ? this.correctedViewProj : this.viewProj);
7795
+ /* The camera, the light's matrix and the wind are the pass's, in its view block. */
7796
+ const view = this.viewSlotFor(late, late ? this.correctedViewProj : this.viewProj);
7797
+ if (view === null)
7798
+ return;
7799
+ this.drawViewOffset = view;
7553
7800
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uModel.offset, model);
7554
7801
  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
7802
  /* The material's repeats, not a constant: geometry authored in metres is textured at
7559
7803
  whatever density the material asks for without rebuilding it. */
7560
7804
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvScale.offset, this.uvScale);
7561
7805
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvOffset.offset, this.uvOffset);
7562
- this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uLightViewProj.offset, this.lightViewProj);
7563
7806
  /*
7564
7807
  * Whether this mesh's tangent frame is real. WebGPU has no disabled attribute, so location 10
7565
7808
  * is always backed — by the caller's data or by the constant buffer — which makes the binding
@@ -7734,8 +7977,10 @@ export class WebGPURenderer {
7734
7977
  command.pipeline = pipeline;
7735
7978
  command.bindGroup = this.flatBindGroupFor(skinned, morphed, deltas);
7736
7979
  command.offsetA = slot;
7737
- command.offsetB = material;
7738
- command.offsetCount = 2;
7980
+ command.offsetB = this.passOffset;
7981
+ command.offsetC = material;
7982
+ command.offsetD = this.drawViewOffset;
7983
+ command.offsetCount = 4;
7739
7984
  for (let index = 0; index < mesh.vertexBuffers.length; index++) {
7740
7985
  command.vertexBuffers[index] = mesh.vertexBuffers[index];
7741
7986
  }
@@ -7749,7 +7994,12 @@ export class WebGPURenderer {
7749
7994
  if (pass === null)
7750
7995
  return;
7751
7996
  pass.setPipeline(pipeline);
7752
- pass.setBindGroup(0, this.flatBindGroupFor(skinned, morphed, deltas), [slot, material]);
7997
+ pass.setBindGroup(0, this.flatBindGroupFor(skinned, morphed, deltas), [
7998
+ slot,
7999
+ this.passOffset,
8000
+ material,
8001
+ this.drawViewOffset,
8002
+ ]);
7753
8003
  for (let index = 0; index < mesh.vertexBuffers.length; index++) {
7754
8004
  pass.setVertexBuffer(index, mesh.vertexBuffers[index]);
7755
8005
  }
@@ -7829,9 +8079,87 @@ export class WebGPURenderer {
7829
8079
  'an ordinary mesh, or drop the channel — the sway, sky and alpha lanes cannot reach an ' +
7830
8080
  'instanced pipeline.');
7831
8081
  }
7832
- const batch = new GpuInstancedBatch(this.surface.device, mesh, capacity, `flat.instances:${mesh.key ?? 'mesh'}`, options.cull ?? false);
8082
+ const animation = (options.animation ?? null);
8083
+ /* The bone a vertex follows is its second coordinates' u, which a mesh carries in its grain
8084
+ lane; a mesh with none has nothing to name a bone by. See `boneAnimation.ts`. */
8085
+ if (animation !== null && !this.presentOf(mesh)['grain']) {
8086
+ throw new Error('WebGPU: a bone animation reads each vertex’s bone from the mesh’s second coordinates ' +
8087
+ '(`MeshData.lightmapUvs`), and this mesh has none.');
8088
+ }
8089
+ const batch = new GpuInstancedBatch(this.surface.device, mesh, capacity, `flat.instances:${mesh.key ?? 'mesh'}`, options.cull ?? false, animation);
7833
8090
  return batch;
7834
8091
  }
8092
+ /** See `Renderer.setAnimationTime`. Every view taken after it reads the new clock. */
8093
+ setAnimationTime(seconds) {
8094
+ if (!Number.isFinite(seconds) || seconds === this.animationTime)
8095
+ return;
8096
+ this.animationTime = seconds;
8097
+ this.reopenViews();
8098
+ }
8099
+ /** The clock `setAnimationTime` set, 0 until it is. */
8100
+ animationTime = 0;
8101
+ /** See `Renderer.createBoneAnimation`. */
8102
+ createBoneAnimation(clip) {
8103
+ return new GpuBoneAnimation(this.surface.device, packBoneAnimation(clip));
8104
+ }
8105
+ /** See `Renderer.disposeBoneAnimation`. A batch still playing it draws nothing after. */
8106
+ disposeBoneAnimation(animation) {
8107
+ const clip = animation;
8108
+ if (!clip.disposed)
8109
+ clip.dispose();
8110
+ }
8111
+ /** An animated batch's clip and clocks, as its groups bind them. */
8112
+ animationViews(batch) {
8113
+ const animation = batch.animation;
8114
+ const clocks = batch.clocks;
8115
+ if (animation === null || clocks === null || animation.disposed)
8116
+ return null;
8117
+ return { places: animation.placesView, turns: animation.turnsView, clocks: clocks.view };
8118
+ }
8119
+ /** An animated batch's groups, dropped wherever the renderer drops its own. */
8120
+ freshAnimatedGroups(batch) {
8121
+ if (batch.groupEpoch === this.flatGroupEpoch)
8122
+ return;
8123
+ batch.litGroups.clear();
8124
+ batch.depthGroups.clear();
8125
+ batch.depthPeelGroups.clear();
8126
+ batch.groupEpoch = this.flatGroupEpoch;
8127
+ }
8128
+ /**
8129
+ * The lit group an animated batch draws with: the material's group, or its overlay twin, again
8130
+ * with the clip's and the clocks' textures — kept on the batch by the group it was built beside,
8131
+ * as a skinned twin is kept by its palette slot.
8132
+ */
8133
+ animatedGroup(batch, views) {
8134
+ this.freshAnimatedGroups(batch);
8135
+ const base = this.overlayGroup();
8136
+ let group = batch.litGroups.get(base);
8137
+ if (group === undefined) {
8138
+ this.overlayBinding = base !== this.bindGroup;
8139
+ 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);
8140
+ this.overlayBinding = false;
8141
+ this.bindGroupBudget.ask();
8142
+ batch.litGroups.set(base, group);
8143
+ }
8144
+ return group;
8145
+ }
8146
+ /** An animated caster's shadow group, by what it cuts with and whether this is the peel. */
8147
+ animatedDepthGroup(batch, views, cutout) {
8148
+ this.freshAnimatedGroups(batch);
8149
+ const peel = this.shadowLayerIsPeel && this.shadowPeelPassUniforms !== null;
8150
+ const groups = peel ? batch.depthPeelGroups : batch.depthGroups;
8151
+ const held = groups.get(cutout);
8152
+ if (held !== undefined)
8153
+ return held;
8154
+ const passUniforms = peel ? this.shadowPeelPassUniforms : this.shadowPassUniforms;
8155
+ const previousView = peel ? this.sunStaticArrayView : this.shadowBlankView;
8156
+ const previousSampler = peel ? this.shadowPeelSampler : this.shadowBlankSampler;
8157
+ if (passUniforms === null || previousView === null || previousSampler === null)
8158
+ return null;
8159
+ 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);
8160
+ groups.set(cutout, group);
8161
+ return group;
8162
+ }
7835
8163
  /** The device instance culls, built on the first culling draw. See `InstanceCullPass`. */
7836
8164
  instanceCulls = null;
7837
8165
  /** Counts frames, so a batch knows when its cull slots were last taken. */
@@ -7864,7 +8192,10 @@ export class WebGPURenderer {
7864
8192
  this.submitInstanced(batch, data, 1, false, {});
7865
8193
  /* A batch that moved this frame pairs each slot with last frame's; the batch is the identity. */
7866
8194
  const previous = batch.previousBuffer;
7867
- if (previous !== null && batch.changed === this.moverFrame && this.tracksMotionNow()) {
8195
+ if (previous !== null &&
8196
+ this.staticRecording === null &&
8197
+ batch.changed === this.moverFrame &&
8198
+ this.tracksMotionNow()) {
7868
8199
  const paired = pairedInstances(batch.previousCount, Math.min(data.count, batch.capacity));
7869
8200
  if (paired > 0)
7870
8201
  this.motionPass?.recordInstanced(batch.mesh, batch.buffer, previous, paired);
@@ -7905,6 +8236,8 @@ export class WebGPURenderer {
7905
8236
  this.perFrameFloats[tintAt] = refractTint?.[0] ?? 1;
7906
8237
  this.perFrameFloats[tintAt + 1] = refractTint?.[1] ?? 1;
7907
8238
  this.perFrameFloats[tintAt + 2] = refractTint?.[2] ?? 1;
8239
+ /* w: this framebuffer's first row is the top, so up the screen is down the rows. */
8240
+ this.perFrameFloats[tintAt + 3] = -1;
7908
8241
  const glassTintAt = this.materialField('uGlassTint');
7909
8242
  this.perFrameFloats[glassTintAt] = glass.tint[0];
7910
8243
  this.perFrameFloats[glassTintAt + 1] = glass.tint[1];
@@ -7969,10 +8302,17 @@ export class WebGPURenderer {
7969
8302
  /* A culling batch's camera draw: the whole batch against the view and the occluders on the CPU,
7970
8303
  then instance by instance on the device into an indirect draw. Blended draws keep every
7971
8304
  instance and their order. See `InstancedOptions.cull`. */
7972
- const culling = !blend && batch.cull;
8305
+ const culling = !blend && batch.cull && this.staticRecording === null;
7973
8306
  if (culling && !batchBoxVisible(batch.box, this.frustum, this.occlusion))
7974
8307
  return;
7975
- const cut = culling && cullsInstances(count, mesh.indexCount) ? this.takeCullSlot(batch, count) : null;
8308
+ /* A batch playing a bone animation reads its clocks by instance index, which a compaction
8309
+ would reorder: it is culled whole and never instance by instance. */
8310
+ const animation = batch.animation === null ? null : this.animationViews(batch);
8311
+ if (batch.animation !== null && animation === null)
8312
+ return;
8313
+ const cut = culling && animation === null && cullsInstances(count, mesh.indexCount)
8314
+ ? this.takeCullSlot(batch, count)
8315
+ : null;
7976
8316
  const instanceBuffer = cut === null ? batch.buffer : cut.instances;
7977
8317
  const slot = this.perDraw.allocate();
7978
8318
  if (slot === null) {
@@ -7984,11 +8324,19 @@ export class WebGPURenderer {
7984
8324
  }
7985
8325
  /* Late unless it moves with what it lies on. See `TranslucentMeshOptions.reconstructed`. */
7986
8326
  const lands = blend && options.reconstructed !== true && this.drawsLate();
7987
- this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uViewProj.offset, lands ? this.correctedViewProj : this.viewProj);
8327
+ const view = this.viewSlotFor(lands, lands ? this.correctedViewProj : this.viewProj);
8328
+ if (view === null)
8329
+ return;
8330
+ this.drawViewOffset = view;
7988
8331
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvScale.offset, this.uvScale);
7989
8332
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvOffset.offset, this.uvOffset);
7990
- this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uLightViewProj.offset, this.lightViewProj);
7991
8333
  this.perDraw.writeInt(slot, FLAT_VERT_FIELDS.uHasTangents.offset, mesh.hasTangents ? 1 : 0);
8334
+ /* The clip's speed and bone scale, read by the animated variant's own field. */
8335
+ const clip = batch.animation;
8336
+ if (animation !== null && clip !== null) {
8337
+ this.perDraw.writeFloat(slot, FLAT_ANIMATED_VERT_FIELDS.uBoneClip.offset, clip.framesPerSecond);
8338
+ this.perDraw.writeFloat(slot, FLAT_ANIMATED_VERT_FIELDS.uBoneClip.offset + 4, clip.boneScale);
8339
+ }
7992
8340
  const dimmed = opacity < 1;
7993
8341
  if (dimmed) {
7994
8342
  this.perFrameFloats[this.materialField('uOpacity')] = opacity;
@@ -8025,7 +8373,7 @@ export class WebGPURenderer {
8025
8373
  a third vertex buffer and a different vertex module, and sharing a cache entry with the
8026
8374
  plain one would hand a draw the wrong layout. */
8027
8375
  const adds = blend && options.additive === true;
8028
- const key = `${base}|inst${blend ? (adds ? '|add' : '|blend') : ''}` +
8376
+ const key = `${base}|inst${animation === null ? '' : '|anim'}${blend ? (adds ? '|add' : '|blend') : ''}` +
8029
8377
  `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
8030
8378
  `${this.materialDoubleSided ? '|2s' : ''}${coverage ? '|a2c' : ''}` +
8031
8379
  `${modelKind === null ? '' : `|m:${modelKind}`}`;
@@ -8036,7 +8384,7 @@ export class WebGPURenderer {
8036
8384
  if (present === undefined) {
8037
8385
  throw new Error(`WebGPU: no mesh attributes recorded for ${base}; createMesh records them`);
8038
8386
  }
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);
8387
+ 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
8388
  }
8041
8389
  const material = this.materialSlotForDraw();
8042
8390
  if (material !== null) {
@@ -8045,10 +8393,15 @@ export class WebGPURenderer {
8045
8393
  : this.recordDraw(0, this.currentTarget());
8046
8394
  if (command !== null) {
8047
8395
  command.pipeline = pipeline;
8048
- command.bindGroup = this.flatBindGroupFor(false, false, null);
8396
+ command.bindGroup =
8397
+ animation === null
8398
+ ? this.flatBindGroupFor(false, false, null)
8399
+ : this.animatedGroup(batch, animation);
8049
8400
  command.offsetA = slot;
8050
- command.offsetB = material;
8051
- command.offsetCount = 2;
8401
+ command.offsetB = this.passOffset;
8402
+ command.offsetC = material;
8403
+ command.offsetD = this.drawViewOffset;
8404
+ command.offsetCount = 4;
8052
8405
  let index = 0;
8053
8406
  for (; index < mesh.vertexBuffers.length; index++) {
8054
8407
  command.vertexBuffers[index] = mesh.vertexBuffers[index];
@@ -8066,7 +8419,9 @@ export class WebGPURenderer {
8066
8419
  if (pass === null)
8067
8420
  return;
8068
8421
  pass.setPipeline(pipeline);
8069
- pass.setBindGroup(0, this.flatBindGroupFor(false, false, null), [slot, material]);
8422
+ pass.setBindGroup(0, animation === null
8423
+ ? this.flatBindGroupFor(false, false, null)
8424
+ : this.animatedGroup(batch, animation), [slot, this.passOffset, material, this.drawViewOffset]);
8070
8425
  let index = 0;
8071
8426
  for (; index < mesh.vertexBuffers.length; index++) {
8072
8427
  pass.setVertexBuffer(index, mesh.vertexBuffers[index]);
@@ -8080,7 +8435,7 @@ export class WebGPURenderer {
8080
8435
  }
8081
8436
  /* And its surface half, for the frame's materials' reflections, with the same survivors. */
8082
8437
  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);
8438
+ 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
8439
  }
8085
8440
  }
8086
8441
  /* Back to the defaults, unconditionally and for the reason `drawMesh` gives at length: this
@@ -8273,6 +8628,7 @@ export class WebGPURenderer {
8273
8628
  if (view === null)
8274
8629
  return false;
8275
8630
  this.lightViewProj.set(lightViewProj);
8631
+ this.reopenViews();
8276
8632
  mat4.multiply(this.correctedLightViewProj, SHADOW_CLIP_CORRECTION, lightViewProj);
8277
8633
  this.shadowLayerIsPeel = peel;
8278
8634
  this.shadowLayerIsDynamic = dynamic;
@@ -8380,6 +8736,241 @@ export class WebGPURenderer {
8380
8736
  this.sceneCasterMaterial = undefined;
8381
8737
  casters(this.sceneCasterSink);
8382
8738
  }
8739
+ /* -- Static draws ---------------------------------------------------------------- */
8740
+ /** See `Renderer.createStaticDraws`. Recorded on its first draw, into each view it is drawn in. */
8741
+ createStaticDraws(casters) {
8742
+ return new GpuStaticDraws(this.surface.device, captureStaticDraws(casters), this.fragment.passSize, FLAT_VIEW_SIZE, FLAT_VERT_SIZE);
8743
+ }
8744
+ /**
8745
+ * See `Renderer.drawStaticDraws`. One bundle executed, after a pass block and a view block are
8746
+ * written for this view; the list is recorded first where it has no recording that is current for
8747
+ * this target and starting state. See `staticBundles.ts`.
8748
+ */
8749
+ drawStaticDraws(draws) {
8750
+ if (this.surface.lost)
8751
+ return;
8752
+ const list = draws;
8753
+ if (list.disposed) {
8754
+ this.warnStaticDisposed();
8755
+ return;
8756
+ }
8757
+ if (!this.canDraw() || this.viewProj === null)
8758
+ return;
8759
+ /* A list's draws are rigid and wear no overlay: put back to none, as `bindMeshPass` does. */
8760
+ this.setSkinPalette(null);
8761
+ this.setMorphWeights(null);
8762
+ this.setCloth(null);
8763
+ this.setSurfaceOverlay(null);
8764
+ this.setMaterial(null);
8765
+ this.sceneCasterMaterial = undefined;
8766
+ /* A probe face gives its slots back after every face; a list replays there draw by draw. */
8767
+ if (this.probePassActive) {
8768
+ this.replayStaticImmediately(list);
8769
+ return;
8770
+ }
8771
+ /* The copy first: taking one may grow the list's rings, which no recording before can bind. */
8772
+ const copy = list.takeCopy(this.moverFrame);
8773
+ const current = this.staticRecordingFor(list);
8774
+ if (current === null || current.immediate) {
8775
+ this.replayStaticImmediately(list);
8776
+ return;
8777
+ }
8778
+ current.usedIn = this.moverFrame;
8779
+ list.writePass(copy, this.passInts);
8780
+ this.writeView(list.views, list.viewOffset(copy), this.viewProj);
8781
+ list.uploadCopy(copy);
8782
+ const cache = current.cache;
8783
+ let frame = current.frameBundles[copy];
8784
+ if (frame === undefined || frame === null) {
8785
+ frame = encodeBundle(this.surface.device, {
8786
+ colorFormats: [cache.format],
8787
+ depthStencilFormat: DEPTH_FORMAT,
8788
+ sampleCount: cache.sampleCount,
8789
+ depthReadOnly: false,
8790
+ }, current.frame, list.passOffset(copy), list.viewOffset(copy), 'static.frame');
8791
+ current.frameBundles[copy] = frame;
8792
+ }
8793
+ const command = this.recordDraw(0, this.currentTarget());
8794
+ if (command !== null) {
8795
+ command.bundles = frame;
8796
+ command.pipeline = null;
8797
+ command.bindGroup = null;
8798
+ }
8799
+ else {
8800
+ this.openPass()?.executeBundles(frame);
8801
+ }
8802
+ const surface = this.reflectionSurface;
8803
+ if (current.surfaceKept && surface !== null && current.surface.taken > 0) {
8804
+ let halves = current.surfaceBundles[copy];
8805
+ if (halves === undefined || halves === null) {
8806
+ halves = encodeBundle(this.surface.device, {
8807
+ colorFormats: [null, REFLECTION_SURFACE_FORMAT, REFLECTION_SURFACE_FORMAT],
8808
+ depthStencilFormat: DEPTH_FORMAT,
8809
+ sampleCount: 1,
8810
+ depthReadOnly: true,
8811
+ }, current.surface, list.passOffset(copy), list.viewOffset(copy), 'static.surface');
8812
+ current.surfaceBundles[copy] = halves;
8813
+ }
8814
+ const kept = surface.take();
8815
+ kept.bundles = halves;
8816
+ }
8817
+ /* Counted as the draws would have been, so a frame reports the same on both backends. */
8818
+ for (let draw = 0; draw < current.frame.taken; draw++)
8819
+ this.drawBudget.ask();
8820
+ if (!this.staticJustRecorded) {
8821
+ for (let ask = 0; ask < current.materialAsks; ask++)
8822
+ this.materialBudget.ask();
8823
+ }
8824
+ this.staticJustRecorded = false;
8825
+ }
8826
+ /** A list drawn the way `drawSceneCasters` draws, a draw at a time, for where a bundle cannot be. */
8827
+ replayStaticImmediately(list) {
8828
+ this.staticJustRecorded = false;
8829
+ replayStaticDraws(list, this.sceneCasterSink);
8830
+ this.setMaterial(null);
8831
+ this.sceneCasterMaterial = undefined;
8832
+ }
8833
+ /** See `Renderer.disposeStaticDraws`. Its pins let go; its meshes and materials are the caller's. */
8834
+ disposeStaticDraws(draws) {
8835
+ const list = draws;
8836
+ if (list.disposed)
8837
+ return;
8838
+ list.dispose(this.unpinFor(list));
8839
+ }
8840
+ /** The recording to replay `list` from here, made now if none is current; null if it cannot be. */
8841
+ staticRecordingFor(list) {
8842
+ /* Geometry still arriving is not drawn yet, so a recording now would leave it out for good. */
8843
+ for (const entry of list.entries) {
8844
+ const mesh = entry.kind === 'mesh' ? entry.mesh : entry.batch.mesh;
8845
+ if (!mesh.complete)
8846
+ return null;
8847
+ }
8848
+ const cache = this.targetPipelines();
8849
+ const start = this.materialInts;
8850
+ const startHash = hashWords(start, start.length);
8851
+ const surfaceKept = this.reflectionSurfaceKeeps();
8852
+ let recording = list.find(cache, start, startHash, this.cutoutResolveStaged, surfaceKept);
8853
+ if (recording !== null &&
8854
+ recording.cacheVersion === cache.version &&
8855
+ recording.groupEpoch === this.flatGroupEpoch &&
8856
+ recording.storeEpoch === this.materialStore.epoch &&
8857
+ recording.ringEpoch === list.ringEpoch) {
8858
+ return recording;
8859
+ }
8860
+ const unpin = this.unpinFor(list);
8861
+ if (recording === null) {
8862
+ recording = list.open(cache, start, startHash, this.cutoutResolveStaged, surfaceKept, unpin);
8863
+ }
8864
+ else {
8865
+ list.clear(recording, recording.storeEpoch === this.materialStore.epoch ? unpin : () => { });
8866
+ }
8867
+ this.recordStatic(list, recording);
8868
+ return recording;
8869
+ }
8870
+ /** How a list's pins are let go: through the store, unless it has forgotten them since. */
8871
+ unpinFor(list) {
8872
+ const epoch = this.materialStore.epoch;
8873
+ return (offset) => {
8874
+ if (this.materialStore.epoch === epoch && list.pinEpoch === epoch) {
8875
+ this.materialStore.unpin(offset);
8876
+ }
8877
+ };
8878
+ }
8879
+ /**
8880
+ * Record `list` into `recording`: its entries through the scene-caster path, with the list's
8881
+ * rings and group cache swapped in for the frame's and the pass and view slots held at the list's
8882
+ * first copy, so every command the path records binds the list's buffers and offsets the bundle
8883
+ * can rewrite per copy. Everything swapped is put back, whatever the path throws.
8884
+ */
8885
+ recordStatic(list, recording) {
8886
+ if (list.groupEpoch !== this.flatGroupEpoch) {
8887
+ list.groups = new Map();
8888
+ list.blank = null;
8889
+ list.groupEpoch = this.flatGroupEpoch;
8890
+ }
8891
+ list.drawRing.reset();
8892
+ const perDraw = this.perDraw;
8893
+ const passRing = this.passRing;
8894
+ const viewRing = this.viewRing;
8895
+ const groups = this.flatBindGroups;
8896
+ const blank = this.blankAlbedoBindGroup;
8897
+ const bindGroup = this.bindGroup;
8898
+ const passSlot = this.passBlock.slot;
8899
+ const viewSlot = this.viewBlock.slot;
8900
+ const lateSlot = this.lateViewBlock.slot;
8901
+ const asked = this.materialBudget.used;
8902
+ this.perDraw = list.drawRing;
8903
+ this.passRing = list.passes;
8904
+ this.viewRing = list.views;
8905
+ this.flatBindGroups = list.groups;
8906
+ /* The blank group over the list's rings, built as the frame's is: with no maps held. */
8907
+ list.blank ??= this.buildFlatBindGroup();
8908
+ this.blankAlbedoBindGroup = list.blank;
8909
+ this.bindGroup = this.flatGroupForMaps(null, null, null, null, null);
8910
+ this.passBlock.slot = list.passOffset(0);
8911
+ this.viewBlock.slot = list.viewOffset(0);
8912
+ this.lateViewBlock.slot = list.viewOffset(0);
8913
+ this.materials.dirty();
8914
+ this.staticRecording = recording;
8915
+ try {
8916
+ this.sceneCasterMaterial = undefined;
8917
+ replayStaticDraws(list, this.sceneCasterSink);
8918
+ }
8919
+ finally {
8920
+ this.staticRecording = null;
8921
+ this.perDraw = perDraw;
8922
+ this.passRing = passRing;
8923
+ this.viewRing = viewRing;
8924
+ this.flatBindGroups = groups;
8925
+ this.blankAlbedoBindGroup = blank;
8926
+ this.bindGroup = bindGroup;
8927
+ this.passBlock.slot = passSlot;
8928
+ this.viewBlock.slot = viewSlot;
8929
+ this.lateViewBlock.slot = lateSlot;
8930
+ this.materials.dirty();
8931
+ this.sceneCasterMaterial = undefined;
8932
+ this.setMaterial(null);
8933
+ }
8934
+ recording.materialAsks = this.materialBudget.used - asked;
8935
+ const store = this.materialStore;
8936
+ recording.cacheVersion = recording.cache.version;
8937
+ recording.groupEpoch = this.flatGroupEpoch;
8938
+ recording.storeEpoch = store.epoch;
8939
+ recording.ringEpoch = list.ringEpoch;
8940
+ /* A skin split into halves is replayed draw by draw, so nothing recorded is kept or pinned. */
8941
+ if (recording.immediate) {
8942
+ resetPool(recording.frame);
8943
+ resetPool(recording.surface);
8944
+ return;
8945
+ }
8946
+ /* Every material the commands bind, pinned once, so nothing displaces what a bundle bakes. */
8947
+ for (const pool of [recording.frame, recording.surface]) {
8948
+ for (let i = 0; i < pool.taken; i++) {
8949
+ const offset = pool.commands[i].offsetC;
8950
+ if (!recording.pins.includes(offset)) {
8951
+ store.pin(offset);
8952
+ recording.pins.push(offset);
8953
+ }
8954
+ }
8955
+ }
8956
+ list.pinEpoch = store.epoch;
8957
+ list.uploadDraws();
8958
+ this.staticJustRecorded = true;
8959
+ }
8960
+ /** The recording under way, which `recordDraw` and the side passes take their commands into. */
8961
+ staticRecording = null;
8962
+ /** Whether the replay about to count its material changes recorded them, and so counted them. */
8963
+ staticJustRecorded = false;
8964
+ /** A disposed list drawn, said once: the frame loop does not throw. */
8965
+ warnStaticDisposed() {
8966
+ if (this.warnedStaticDisposed)
8967
+ return;
8968
+ this.warnedStaticDisposed = true;
8969
+ console.warn('drawStaticDraws: this list was disposed, and draws nothing.');
8970
+ }
8971
+ warnedStaticDisposed = false;
8972
+ /** Bumped wherever the lit groups are dropped, which a static list's own groups follow. */
8973
+ flatGroupEpoch = 0;
8383
8974
  drawShadowCasters(casters) {
8384
8975
  if (this.surface.lost || this.shadowPass === null)
8385
8976
  return;
@@ -8671,7 +9262,7 @@ export class WebGPURenderer {
8671
9262
  }
8672
9263
  this.shadowDraws.flush();
8673
9264
  this.scatterDepthDraws.flush();
8674
- this.surface.device.queue.submit([this.shadowEncoder.finish()]);
9265
+ this.submit([this.shadowEncoder.finish()]);
8675
9266
  /* A growth's old texture, copied out of by what was just submitted and needed no longer. */
8676
9267
  this.retiredSunTexture?.destroy();
8677
9268
  this.retiredSunTexture = null;
@@ -8905,7 +9496,7 @@ export class WebGPURenderer {
8905
9496
  * its own slot.
8906
9497
  */
8907
9498
  this.pointShadowArray?.flush();
8908
- this.surface.device.queue.submit([this.shadowEncoder.finish()]);
9499
+ this.submit([this.shadowEncoder.finish()]);
8909
9500
  this.shadowEncoder = null;
8910
9501
  this.shadowPass = null;
8911
9502
  this.gpuTimer.end();
@@ -9072,7 +9663,7 @@ export class WebGPURenderer {
9072
9663
  tintTarget.resolve(encoder, layer, face, offered);
9073
9664
  }
9074
9665
  buildFlatBindGroup() {
9075
- return createFlatBindGroup(this.surface.device, this.bindGroupLayout, this.variant, this.perDraw.buffer, this.perFrame.buffer, this.flatTextures);
9666
+ return createFlatBindGroup(this.surface.device, this.bindGroupLayout, this.variant, this.perDraw.buffer, this.passRing.buffer, this.materialStore.buffer, this.viewRing.buffer, this.flatTextures);
9076
9667
  }
9077
9668
  /**
9078
9669
  * The open material's flat group, or its twin with the surface overlay's atlas in the refraction
@@ -9112,11 +9703,11 @@ export class WebGPURenderer {
9112
9703
  /**
9113
9704
  * The bind group a draw uses, skinned or not.
9114
9705
  *
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
9706
+ * **The skinned twin is kept by palette slot and by the unskinned group it was built beside**,
9707
+ * which is one a material, and compared against the views it holds — the palette's, which
9708
+ * changes only when the slot's texture grows, the morph deltas and the cloth's — rather than
9709
+ * against a flag somebody has to remember to clear at six call sites. Comparing object identity
9710
+ * means a missed invalidation is not possible rather than merely unlikely, and it is why
9120
9711
  * `SkinPaletteTexture.view()` caches: a fresh view every call never compares equal and would
9121
9712
  * rebuild this every draw.
9122
9713
  */
@@ -9130,23 +9721,31 @@ export class WebGPURenderer {
9130
9721
  const at = skinned ? this.skinPaletteSlot + 1 : 0;
9131
9722
  /* A skinned group carries the cloth's views, or the stand-in's: see `clothViews`. */
9132
9723
  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)) {
9724
+ let byMaterial = this.skinnedGroups[at];
9725
+ if (byMaterial === undefined) {
9726
+ byMaterial = new Map();
9727
+ this.skinnedGroups[at] = byMaterial;
9728
+ }
9729
+ let twin = byMaterial.get(base);
9730
+ if (twin === undefined ||
9731
+ twin.palette !== palette ||
9732
+ twin.deltas !== deltas ||
9733
+ twin.cloth !== (cloth?.particles ?? null) ||
9734
+ twin.clothBinding !== (cloth?.binding ?? null)) {
9139
9735
  /* Built from the live material, with the overlay's atlas where `base` is its twin. */
9140
9736
  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);
9737
+ 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
9738
  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;
9739
+ twin = {
9740
+ group,
9741
+ palette,
9742
+ deltas,
9743
+ cloth: cloth?.particles ?? null,
9744
+ clothBinding: cloth?.binding ?? null,
9745
+ };
9746
+ byMaterial.set(base, twin);
9148
9747
  }
9149
- return this.skinnedGroups[at];
9748
+ return twin.group;
9150
9749
  }
9151
9750
  /** The layout matching a pair of vertex flags. A pipeline and its bind group must agree. */
9152
9751
  flatLayoutFor(skinned, morphed) {
@@ -9185,8 +9784,14 @@ export class WebGPURenderer {
9185
9784
  if (this.drawBudget.dropped > 0) {
9186
9785
  grew = this.perDraw.growTo(roomFor(this.drawBudget.used)) || grew;
9187
9786
  }
9188
- if (this.materialBudget.dropped > 0) {
9189
- grew = this.perFrame.growTo(roomFor(this.materialBudget.used)) || grew;
9787
+ if (this.materialsRefused > 0) {
9788
+ grew = this.materialStore.growTo(roomFor(this.materialBudget.used)) || grew;
9789
+ }
9790
+ if (this.passesRefused > 0) {
9791
+ grew = this.passRing.growTo(roomFor(this.passesAsked)) || grew;
9792
+ }
9793
+ if (this.viewsRefused > 0) {
9794
+ grew = this.viewRing.growTo(roomFor(this.viewsAsked)) || grew;
9190
9795
  }
9191
9796
  if (grew)
9192
9797
  this.rebuildFlatGroupsForNewRings();
@@ -9207,9 +9812,10 @@ export class WebGPURenderer {
9207
9812
  */
9208
9813
  rebuildFlatGroupsForNewRings() {
9209
9814
  this.flatBindGroups.clear();
9815
+ this.flatGroupEpoch += 1;
9210
9816
  this.overlayGroups.clear();
9211
- this.flatEnvironment = this.flatTextures('uEnvironment').view;
9212
9817
  this.skinnedGroups.length = 0;
9818
+ this.flatEnvironment = this.flatTextures('uEnvironment').view;
9213
9819
  const albedo = this.albedo;
9214
9820
  const normalMap = this.normalMap;
9215
9821
  const ormMap = this.ormMap;
@@ -9229,6 +9835,7 @@ export class WebGPURenderer {
9229
9835
  this.bindGroup = this.flatGroupForMaps(this.albedo, this.normalMap, this.ormMap, this.emissiveMap, this.modelMap);
9230
9836
  /* The open material lives in a slot of a buffer that no longer exists. */
9231
9837
  this.materials.dirty();
9838
+ this.reopenPass();
9232
9839
  }
9233
9840
  /**
9234
9841
  * The environment view the cached flat groups bind: the grid, or the blank stand-in while the
@@ -9260,7 +9867,9 @@ export class WebGPURenderer {
9260
9867
  */
9261
9868
  rebuildFlatBindGroup() {
9262
9869
  this.flatBindGroups.clear();
9870
+ this.flatGroupEpoch += 1;
9263
9871
  this.overlayGroups.clear();
9872
+ this.skinnedGroups.length = 0;
9264
9873
  this.flatEnvironment = this.flatTextures('uEnvironment').view;
9265
9874
  if (this.albedo === null &&
9266
9875
  this.normalMap === null &&
@@ -11158,20 +11767,29 @@ export class WebGPURenderer {
11158
11767
  * `reflectionSurfacePass.ts` holds and tests for the depth its frame draw wrote. Replayed when the
11159
11768
  * frame's pass has ended, before the trace reads them. See `shaders/flat/reflectionSurface.ts`.
11160
11769
  */
11161
- keepReflectionSurface(mesh, frameKey, base, skinned, morphed, deltas, layer, eight, clothed, modelKind, slot, material, instanceBuffer, instances, indirect) {
11770
+ keepReflectionSurface(mesh, frameKey, base, skinned, morphed, deltas, layer, eight, clothed, modelKind, slot, material, instanceBuffer, instances, indirect,
11771
+ /** A batch playing a bone animation, whose surface half moves as its draw does. */
11772
+ animated = null) {
11162
11773
  const surface = this.reflectionSurface;
11163
11774
  const present = this.meshPresent.get(base);
11164
11775
  if (surface === null || present === undefined)
11165
11776
  return;
11777
+ const animation = animated === null ? null : this.animationViews(animated);
11166
11778
  const key = `${frameKey}|rs`;
11167
11779
  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();
11780
+ 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);
11781
+ /* A static list's surface halves go into its own pool, beside its draws. */
11782
+ const command = this.staticRecording === null ? surface.take() : takeRecorded(this.staticRecording.surface);
11170
11783
  command.pipeline = pipeline;
11171
- command.bindGroup = this.flatBindGroupFor(skinned, morphed, deltas);
11784
+ command.bindGroup =
11785
+ animated === null || animation === null
11786
+ ? this.flatBindGroupFor(skinned, morphed, deltas)
11787
+ : this.animatedGroup(animated, animation);
11172
11788
  command.offsetA = slot;
11173
- command.offsetB = material;
11174
- command.offsetCount = 2;
11789
+ command.offsetB = this.passOffset;
11790
+ command.offsetC = material;
11791
+ command.offsetD = this.drawViewOffset;
11792
+ command.offsetCount = 4;
11175
11793
  let index = 0;
11176
11794
  for (; index < mesh.vertexBuffers.length; index++) {
11177
11795
  command.vertexBuffers[index] = mesh.vertexBuffers[index];
@@ -11192,6 +11810,11 @@ export class WebGPURenderer {
11192
11810
  * wrote.
11193
11811
  */
11194
11812
  keepSkinDiffuse(mesh, frameKey, base, skinned, morphed, deltas, layer, eight, clothed, slot, material) {
11813
+ /* A skin's halves are not bundled: a static list holding one replays draw by draw. */
11814
+ if (this.staticRecording !== null) {
11815
+ this.staticRecording.immediate = true;
11816
+ return;
11817
+ }
11195
11818
  const skin = this.skinScatter;
11196
11819
  const pipelines = this.skinPipelines;
11197
11820
  const present = this.meshPresent.get(base);
@@ -11207,8 +11830,10 @@ export class WebGPURenderer {
11207
11830
  command.pipeline = pipeline;
11208
11831
  command.bindGroup = bindGroup;
11209
11832
  command.offsetA = slot;
11210
- command.offsetB = material;
11211
- command.offsetCount = 2;
11833
+ command.offsetB = this.passOffset;
11834
+ command.offsetC = material;
11835
+ command.offsetD = this.drawViewOffset;
11836
+ command.offsetCount = 4;
11212
11837
  for (let index = 0; index < mesh.vertexBuffers.length; index++) {
11213
11838
  command.vertexBuffers[index] = mesh.vertexBuffers[index];
11214
11839
  }
@@ -11635,6 +12260,9 @@ export class WebGPURenderer {
11635
12260
  * site reads as one `if` either way.
11636
12261
  */
11637
12262
  recordDraw(reads, writes) {
12263
+ /* A static list recording takes its draws into its own pool, never the frame graph's. */
12264
+ if (this.staticRecording !== null)
12265
+ return takeRecorded(this.staticRecording.frame);
11638
12266
  /* The order-independent replay owns its pass and issues directly into it; a recorded command
11639
12267
  would be replayed into the frame's passes instead, which is not where these belong. */
11640
12268
  if (this.oitEncoder !== null)
@@ -11682,10 +12310,26 @@ export class WebGPURenderer {
11682
12310
  }
11683
12311
  /** One recorded draw, issued into an open pass. Shared by the graph's replay and `runLate`. */
11684
12312
  issueCommand(pass, command) {
12313
+ /* A static list, recorded once: one call, and the pass's bindings are its to set again. */
12314
+ if (command.bundles !== null) {
12315
+ pass.executeBundles(command.bundles);
12316
+ return;
12317
+ }
11685
12318
  if (command.pipeline === null || command.bindGroup === null)
11686
12319
  return;
11687
12320
  pass.setPipeline(command.pipeline);
11688
- if (command.offsetCount === 2) {
12321
+ if (command.offsetCount === 4) {
12322
+ pass.setBindGroup(0, command.bindGroup, [
12323
+ command.offsetA,
12324
+ command.offsetB,
12325
+ command.offsetC,
12326
+ command.offsetD,
12327
+ ]);
12328
+ }
12329
+ else if (command.offsetCount === 3) {
12330
+ pass.setBindGroup(0, command.bindGroup, [command.offsetA, command.offsetB, command.offsetC]);
12331
+ }
12332
+ else if (command.offsetCount === 2) {
11689
12333
  pass.setBindGroup(0, command.bindGroup, [command.offsetA, command.offsetB]);
11690
12334
  }
11691
12335
  else if (command.offsetCount === 1) {
@@ -11951,7 +12595,7 @@ export class WebGPURenderer {
11951
12595
  this.pass?.end();
11952
12596
  this.pass = null;
11953
12597
  if (!this.surface.lost)
11954
- this.surface.device.queue.submit([this.encoder.finish()]);
12598
+ this.submit([this.encoder.finish()]);
11955
12599
  this.encoder = null;
11956
12600
  }
11957
12601
  endFrame() {
@@ -12015,7 +12659,7 @@ export class WebGPURenderer {
12015
12659
  * there would have nothing to ride on. See `GpuTimestamps.resolve`.
12016
12660
  */
12017
12661
  this.gpuTimer.resolve(this.encoder);
12018
- this.surface.device.queue.submit([this.encoder.finish()]);
12662
+ this.submit([this.encoder.finish()]);
12019
12663
  this.encoder = null;
12020
12664
  this.framePresented = true;
12021
12665
  /* Latched rather than assigned: `framePresented` goes back to false every `beginFrame`. */
@@ -12676,7 +13320,7 @@ export class WebGPURenderer {
12676
13320
  definition.dispatch(this.computeContext);
12677
13321
  pass.end();
12678
13322
  this.flushRings();
12679
- device.queue.submit([encoder.finish()]);
13323
+ this.submit([encoder.finish()]);
12680
13324
  }
12681
13325
  /** Let go of a definition. A handle kept past this dispatches nothing; see `ComputeHandle`. */
12682
13326
  unregisterCompute(handle) {
@@ -13730,7 +14374,7 @@ export class WebGPURenderer {
13730
14374
  .end();
13731
14375
  }
13732
14376
  }
13733
- device.queue.submit([clear.finish()]);
14377
+ this.submit([clear.finish()]);
13734
14378
  }
13735
14379
  /**
13736
14380
  * Declare where a grid's probes stand, and allocate the layers for them.
@@ -13941,7 +14585,7 @@ export class WebGPURenderer {
13941
14585
  if (whole)
13942
14586
  this.buildProbeChain(encoder, probe, this.probeSweeps?.writeLayer(layer) ?? layer);
13943
14587
  this.flushRings();
13944
- device.queue.submit([encoder.finish()]);
14588
+ this.submit([encoder.finish()]);
13945
14589
  this.rewindRings(skinMark);
13946
14590
  this.encoderEpoch = frameEpoch;
13947
14591
  this.encoder = savedEncoder;
@@ -13951,6 +14595,7 @@ export class WebGPURenderer {
13951
14595
  this.perFrameInts[this.materialField('uOutputTransform')] = heldGrade;
13952
14596
  this.perFrameFloats[this.materialField('uOutputExposure')] = heldExposure;
13953
14597
  this.materials.dirty();
14598
+ this.reopenPass();
13954
14599
  this.probePassActive = false;
13955
14600
  this.probeBounce = false;
13956
14601
  this.rebindEnvironment();
@@ -13998,7 +14643,7 @@ export class WebGPURenderer {
13998
14643
  this.uploadProbeFaces(probe, image);
13999
14644
  const encoder = device.createCommandEncoder({ label: 'probe.environmentImage' });
14000
14645
  this.buildProbeChain(encoder, probe, 0);
14001
- device.queue.submit([encoder.finish()]);
14646
+ this.submit([encoder.finish()]);
14002
14647
  this.markProbeFilled(0);
14003
14648
  this.probeAmbient = options?.irradiance ?? true;
14004
14649
  this.rebuildFlatBindGroup();
@@ -14023,7 +14668,7 @@ export class WebGPURenderer {
14023
14668
  const { device } = this.surface;
14024
14669
  const encoder = device.createCommandEncoder({ label: 'probe.layerImage' });
14025
14670
  this.buildProbeChain(encoder, probe, this.probeSweeps?.writeLayer(layer) ?? layer);
14026
- device.queue.submit([encoder.finish()]);
14671
+ this.submit([encoder.finish()]);
14027
14672
  this.markProbeFilled(layer);
14028
14673
  this.probeAmbient = options?.irradiance ?? true;
14029
14674
  this.rebuildFlatBindGroup();