@driftengine/core 4.8.7 → 4.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/CHANGELOG.json +123 -11
  2. package/README.md +43 -38
  3. package/dist/index.d.ts +8 -1
  4. package/dist/index.js +11 -3
  5. package/dist/render/ambientHarmonics.d.ts +1 -1
  6. package/dist/render/ambientHarmonics.js +1 -1
  7. package/dist/render/areaLights.d.ts +4 -4
  8. package/dist/render/areaLights.js +1 -1
  9. package/dist/render/atmosphere.d.ts +13 -0
  10. package/dist/render/atmosphere.js +9 -1
  11. package/dist/render/backend/api.d.ts +19 -4
  12. package/dist/render/backend/createRenderer.d.ts +6 -0
  13. package/dist/render/backend/createRenderer.js +4 -1
  14. package/dist/render/backend/select.js +11 -5
  15. package/dist/render/backend/webgl2/boneAnimations.d.ts +29 -0
  16. package/dist/render/backend/webgl2/boneAnimations.js +80 -0
  17. package/dist/render/backend/webgl2/instanced.d.ts +5 -1
  18. package/dist/render/backend/webgl2/instanced.js +13 -1
  19. package/dist/render/backend/webgl2/renderer.d.ts +180 -4
  20. package/dist/render/backend/webgl2/renderer.js +550 -40
  21. package/dist/render/backend/webgl2/skinPaletteTexture.d.ts +5 -3
  22. package/dist/render/backend/webgl2/skinPaletteTexture.js +6 -4
  23. package/dist/render/backend/webgpu/boneAnimations.d.ts +34 -0
  24. package/dist/render/backend/webgpu/boneAnimations.js +76 -0
  25. package/dist/render/backend/webgpu/buffers.d.ts +19 -4
  26. package/dist/render/backend/webgpu/buffers.js +18 -11
  27. package/dist/render/backend/webgpu/compressedUpload.d.ts +0 -1
  28. package/dist/render/backend/webgpu/compressedUpload.js +53 -8
  29. package/dist/render/backend/webgpu/depthPass.d.ts +59 -3
  30. package/dist/render/backend/webgpu/depthPass.js +49 -14
  31. package/dist/render/backend/webgpu/device.d.ts +7 -1
  32. package/dist/render/backend/webgpu/device.js +31 -3
  33. package/dist/render/backend/webgpu/drawCommand.d.ts +18 -4
  34. package/dist/render/backend/webgpu/drawCommand.js +10 -3
  35. package/dist/render/backend/webgpu/flatPass.d.ts +100 -42
  36. package/dist/render/backend/webgpu/flatPass.js +191 -43
  37. package/dist/render/backend/webgpu/instanced.d.ts +14 -1
  38. package/dist/render/backend/webgpu/instanced.js +24 -1
  39. package/dist/render/backend/webgpu/materialStore.d.ts +101 -0
  40. package/dist/render/backend/webgpu/materialStore.js +265 -0
  41. package/dist/render/backend/webgpu/pipelineCache.d.ts +6 -0
  42. package/dist/render/backend/webgpu/pipelineCache.js +7 -0
  43. package/dist/render/backend/webgpu/postPass.d.ts +51 -9
  44. package/dist/render/backend/webgpu/postPass.js +16 -2
  45. package/dist/render/backend/webgpu/reflectionSurfacePass.d.ts +30 -0
  46. package/dist/render/backend/webgpu/reflectionSurfacePass.js +100 -0
  47. package/dist/render/backend/webgpu/renderer.d.ts +283 -37
  48. package/dist/render/backend/webgpu/renderer.js +1363 -202
  49. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +7 -3
  50. package/dist/render/backend/webgpu/skinPaletteRing.js +13 -7
  51. package/dist/render/backend/webgpu/skinPaletteTexture.d.ts +6 -0
  52. package/dist/render/backend/webgpu/skinPaletteTexture.js +7 -1
  53. package/dist/render/backend/webgpu/staticBundles.d.ts +125 -0
  54. package/dist/render/backend/webgpu/staticBundles.js +241 -0
  55. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +10 -3
  56. package/dist/render/backend/webgpu/surfaceTexturePass.js +20 -2
  57. package/dist/render/backend/webgpu/uniformRing.d.ts +21 -0
  58. package/dist/render/backend/webgpu/uniformRing.js +9 -1
  59. package/dist/render/bloomChain.d.ts +3 -3
  60. package/dist/render/boneAnimation.d.ts +92 -0
  61. package/dist/render/boneAnimation.js +133 -0
  62. package/dist/render/compressedSource.d.ts +33 -13
  63. package/dist/render/compressedSource.js +70 -25
  64. package/dist/render/displayRange.d.ts +31 -0
  65. package/dist/render/displayRange.js +47 -0
  66. package/dist/render/filmicCurve.d.ts +74 -0
  67. package/dist/render/filmicCurve.js +203 -0
  68. package/dist/render/fog.d.ts +4 -1
  69. package/dist/render/fog.js +12 -2
  70. package/dist/render/frame/resources.d.ts +2 -2
  71. package/dist/render/frame/resources.js +6 -0
  72. package/dist/render/frameReflections.d.ts +40 -0
  73. package/dist/render/frameReflections.js +48 -0
  74. package/dist/render/glCompressed.js +83 -0
  75. package/dist/render/instances.d.ts +26 -0
  76. package/dist/render/lightBudget.d.ts +10 -0
  77. package/dist/render/lightBudget.js +10 -0
  78. package/dist/render/lightmap.d.ts +1 -1
  79. package/dist/render/mesh.d.ts +5 -0
  80. package/dist/render/mesh.js +6 -0
  81. package/dist/render/pass.d.ts +49 -0
  82. package/dist/render/pointLightSelection.d.ts +4 -4
  83. package/dist/render/reflectionSurfaceTargets.d.ts +28 -0
  84. package/dist/render/reflectionSurfaceTargets.js +99 -0
  85. package/dist/render/renderQuality.d.ts +39 -8
  86. package/dist/render/renderQuality.js +4 -0
  87. package/dist/render/sceneTarget.d.ts +5 -0
  88. package/dist/render/sceneTarget.js +24 -1
  89. package/dist/render/screenSpaceReflection.d.ts +7 -1
  90. package/dist/render/screenSpaceReflection.js +5 -0
  91. package/dist/render/shaders/arcane.d.ts +1 -1
  92. package/dist/render/shaders/bolt.d.ts +1 -1
  93. package/dist/render/shaders/boneAnimation.d.ts +19 -0
  94. package/dist/render/shaders/boneAnimation.js +63 -0
  95. package/dist/render/shaders/caustics.d.ts +1 -1
  96. package/dist/render/shaders/depth.d.ts +7 -0
  97. package/dist/render/shaders/depth.js +35 -10
  98. package/dist/render/shaders/film.d.ts +1 -1
  99. package/dist/render/shaders/filmLook.js +2 -1
  100. package/dist/render/shaders/fire.d.ts +1 -1
  101. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  102. package/dist/render/shaders/flat/glassTint.js +2 -0
  103. package/dist/render/shaders/flat/index.d.ts +16 -0
  104. package/dist/render/shaders/flat/index.js +33 -5
  105. package/dist/render/shaders/flat/main.d.ts +1 -1
  106. package/dist/render/shaders/flat/main.js +43 -5
  107. package/dist/render/shaders/flat/models.js +1 -1
  108. package/dist/render/shaders/flat/overlay.d.ts +16 -0
  109. package/dist/render/shaders/flat/overlay.js +117 -0
  110. package/dist/render/shaders/flat/pointShadow.js +3 -3
  111. package/dist/render/shaders/flat/preamble.js +38 -24
  112. package/dist/render/shaders/flat/reflectionSurface.d.ts +21 -0
  113. package/dist/render/shaders/flat/reflectionSurface.js +61 -0
  114. package/dist/render/shaders/fog.d.ts +1 -1
  115. package/dist/render/shaders/fog.js +9 -2
  116. package/dist/render/shaders/fringe.d.ts +29 -0
  117. package/dist/render/shaders/fringe.js +55 -0
  118. package/dist/render/shaders/generated/arcane.wgsl.d.ts +1 -1
  119. package/dist/render/shaders/generated/arcane.wgsl.js +1 -1
  120. package/dist/render/shaders/generated/bolt.wgsl.d.ts +1 -1
  121. package/dist/render/shaders/generated/bolt.wgsl.js +1 -1
  122. package/dist/render/shaders/generated/caustics.wgsl.d.ts +1 -1
  123. package/dist/render/shaders/generated/caustics.wgsl.js +1 -1
  124. package/dist/render/shaders/generated/depth.wgsl.d.ts +93 -0
  125. package/dist/render/shaders/generated/depth.wgsl.js +93 -0
  126. package/dist/render/shaders/generated/film.wgsl.d.ts +1 -1
  127. package/dist/render/shaders/generated/film.wgsl.js +1 -1
  128. package/dist/render/shaders/generated/fire.wgsl.d.ts +1 -1
  129. package/dist/render/shaders/generated/fire.wgsl.js +1 -1
  130. package/dist/render/shaders/generated/flat.wgsl.d.ts +3254 -2920
  131. package/dist/render/shaders/generated/flat.wgsl.js +3292 -2957
  132. package/dist/render/shaders/generated/line.wgsl.d.ts +1 -1
  133. package/dist/render/shaders/generated/line.wgsl.js +1 -1
  134. package/dist/render/shaders/generated/particle.wgsl.d.ts +4 -4
  135. package/dist/render/shaders/generated/particle.wgsl.js +4 -4
  136. package/dist/render/shaders/generated/rush.wgsl.d.ts +56 -26
  137. package/dist/render/shaders/generated/rush.wgsl.js +56 -26
  138. package/dist/render/shaders/generated/scatter.wgsl.d.ts +1 -1
  139. package/dist/render/shaders/generated/scatter.wgsl.js +1 -1
  140. package/dist/render/shaders/generated/smoke.wgsl.d.ts +1 -1
  141. package/dist/render/shaders/generated/smoke.wgsl.js +1 -1
  142. package/dist/render/shaders/generated/ssrResolve.wgsl.d.ts +29 -0
  143. package/dist/render/shaders/generated/ssrResolve.wgsl.js +29 -0
  144. package/dist/render/shaders/generated/ssrTrace.wgsl.d.ts +22 -2
  145. package/dist/render/shaders/generated/ssrTrace.wgsl.js +22 -2
  146. package/dist/render/shaders/generated/water.wgsl.d.ts +1 -1
  147. package/dist/render/shaders/generated/water.wgsl.js +1 -1
  148. package/dist/render/shaders/gpudriven/fog.wgsl.d.ts +2 -2
  149. package/dist/render/shaders/gpudriven/fog.wgsl.js +11 -3
  150. package/dist/render/shaders/line.d.ts +1 -1
  151. package/dist/render/shaders/outputTransform.d.ts +7 -0
  152. package/dist/render/shaders/outputTransform.js +28 -14
  153. package/dist/render/shaders/particle.d.ts +3 -3
  154. package/dist/render/shaders/rush.js +54 -11
  155. package/dist/render/shaders/scatter.d.ts +1 -1
  156. package/dist/render/shaders/smoke.d.ts +1 -1
  157. package/dist/render/shaders/ssrResolve.d.ts +17 -0
  158. package/dist/render/shaders/ssrResolve.js +51 -0
  159. package/dist/render/shaders/ssrTrace.js +26 -0
  160. package/dist/render/shaders/vertexChannel.d.ts +7 -0
  161. package/dist/render/shaders/vertexChannel.js +26 -0
  162. package/dist/render/shaders/water.d.ts +1 -1
  163. package/dist/render/ssrPass.d.ts +16 -0
  164. package/dist/render/ssrPass.js +106 -1
  165. package/dist/render/staticDraws.d.ts +47 -0
  166. package/dist/render/staticDraws.js +43 -0
  167. package/dist/render/surfaceOverlay.d.ts +132 -0
  168. package/dist/render/surfaceOverlay.js +85 -0
  169. package/dist/render/surfaceTexture.d.ts +57 -7
  170. package/dist/render/surfaceTexture.js +125 -17
  171. package/dist/render/temporalAa.d.ts +14 -0
  172. package/dist/render/temporalAa.js +20 -0
  173. package/dist/render/vertexDefaults.d.ts +17 -0
  174. package/dist/render/vertexDefaults.js +24 -0
  175. package/package.json +6 -6
  176. package/src/index.ts +24 -2
  177. package/src/render/ambientHarmonics.ts +1 -1
  178. package/src/render/areaLights.ts +4 -4
  179. package/src/render/atmosphere.ts +22 -1
  180. package/src/render/backend/api.ts +22 -3
  181. package/src/render/backend/createRenderer.ts +18 -1
  182. package/src/render/backend/select.ts +10 -5
  183. package/src/render/backend/webgl2/boneAnimations.ts +105 -0
  184. package/src/render/backend/webgl2/instanced.ts +20 -1
  185. package/src/render/backend/webgl2/renderer.ts +672 -37
  186. package/src/render/backend/webgl2/skinPaletteTexture.ts +6 -4
  187. package/src/render/backend/webgpu/boneAnimations.ts +97 -0
  188. package/src/render/backend/webgpu/buffers.ts +42 -16
  189. package/src/render/backend/webgpu/compressedUpload.ts +59 -9
  190. package/src/render/backend/webgpu/depthPass.ts +51 -10
  191. package/src/render/backend/webgpu/device.ts +42 -3
  192. package/src/render/backend/webgpu/drawCommand.ts +22 -4
  193. package/src/render/backend/webgpu/flatPass.ts +250 -40
  194. package/src/render/backend/webgpu/instanced.ts +32 -1
  195. package/src/render/backend/webgpu/materialStore.ts +276 -0
  196. package/src/render/backend/webgpu/pipelineCache.ts +7 -0
  197. package/src/render/backend/webgpu/postPass.ts +18 -0
  198. package/src/render/backend/webgpu/reflectionSurfacePass.ts +114 -0
  199. package/src/render/backend/webgpu/renderer.ts +1655 -230
  200. package/src/render/backend/webgpu/skinPaletteRing.ts +13 -7
  201. package/src/render/backend/webgpu/skinPaletteTexture.ts +7 -1
  202. package/src/render/backend/webgpu/staticBundles.ts +363 -0
  203. package/src/render/backend/webgpu/surfaceTexturePass.ts +29 -3
  204. package/src/render/backend/webgpu/uniformRing.ts +10 -1
  205. package/src/render/bloomChain.ts +3 -3
  206. package/src/render/boneAnimation.ts +223 -0
  207. package/src/render/compressedSource.ts +121 -29
  208. package/src/render/displayRange.ts +61 -0
  209. package/src/render/filmicCurve.ts +229 -0
  210. package/src/render/fog.ts +19 -3
  211. package/src/render/frame/resources.ts +6 -0
  212. package/src/render/frameReflections.ts +72 -0
  213. package/src/render/glCompressed.ts +83 -0
  214. package/src/render/instances.ts +27 -0
  215. package/src/render/lightBudget.ts +11 -0
  216. package/src/render/lightmap.ts +1 -1
  217. package/src/render/mesh.ts +6 -0
  218. package/src/render/pass.ts +53 -1
  219. package/src/render/pointLightSelection.ts +4 -4
  220. package/src/render/reflectionSurfaceTargets.ts +105 -0
  221. package/src/render/renderQuality.ts +44 -8
  222. package/src/render/sceneTarget.ts +30 -2
  223. package/src/render/screenSpaceReflection.ts +7 -1
  224. package/src/render/shaders/boneAnimation.ts +63 -0
  225. package/src/render/shaders/depth.ts +45 -10
  226. package/src/render/shaders/filmLook.ts +2 -1
  227. package/src/render/shaders/flat/glassTint.ts +5 -1
  228. package/src/render/shaders/flat/index.ts +49 -5
  229. package/src/render/shaders/flat/main.ts +43 -5
  230. package/src/render/shaders/flat/models.ts +1 -1
  231. package/src/render/shaders/flat/overlay.ts +117 -0
  232. package/src/render/shaders/flat/pointShadow.ts +3 -3
  233. package/src/render/shaders/flat/preamble.ts +38 -24
  234. package/src/render/shaders/flat/reflectionSurface.ts +61 -0
  235. package/src/render/shaders/fog.ts +9 -2
  236. package/src/render/shaders/fringe.ts +59 -0
  237. package/src/render/shaders/generated/arcane.wgsl.ts +1 -1
  238. package/src/render/shaders/generated/bolt.wgsl.ts +1 -1
  239. package/src/render/shaders/generated/caustics.wgsl.ts +1 -1
  240. package/src/render/shaders/generated/depth.wgsl.ts +95 -0
  241. package/src/render/shaders/generated/film.wgsl.ts +1 -1
  242. package/src/render/shaders/generated/fire.wgsl.ts +1 -1
  243. package/src/render/shaders/generated/flat.wgsl.ts +3292 -2957
  244. package/src/render/shaders/generated/line.wgsl.ts +1 -1
  245. package/src/render/shaders/generated/particle.wgsl.ts +4 -4
  246. package/src/render/shaders/generated/rush.wgsl.ts +56 -26
  247. package/src/render/shaders/generated/scatter.wgsl.ts +1 -1
  248. package/src/render/shaders/generated/smoke.wgsl.ts +1 -1
  249. package/src/render/shaders/generated/ssrResolve.wgsl.ts +30 -0
  250. package/src/render/shaders/generated/ssrTrace.wgsl.ts +22 -2
  251. package/src/render/shaders/generated/water.wgsl.ts +1 -1
  252. package/src/render/shaders/gpudriven/fog.wgsl.ts +11 -3
  253. package/src/render/shaders/outputTransform.ts +30 -14
  254. package/src/render/shaders/rush.ts +54 -11
  255. package/src/render/shaders/ssrResolve.ts +52 -0
  256. package/src/render/shaders/ssrTrace.ts +26 -0
  257. package/src/render/shaders/vertexChannel.ts +27 -0
  258. package/src/render/ssrPass.ts +138 -1
  259. package/src/render/staticDraws.ts +95 -0
  260. package/src/render/surfaceOverlay.ts +195 -0
  261. package/src/render/surfaceTexture.ts +161 -27
  262. package/src/render/temporalAa.ts +20 -0
  263. package/src/render/vertexDefaults.ts +28 -0
@@ -59,17 +59,23 @@ import { sceneCaptureTexels } from '../../sceneCapture.js';
59
59
  import { gpuCompressedFormats } from './compressedUpload.js';
60
60
  import { GpuClothBinding, GpuClothParticles, clothStandIn } from './clothTextures.js';
61
61
  import { MODEL_PARAM_FLOATS, eyeAxisInWorld, packModel, skinProfileDistance, } from '../../surfaceModel.js';
62
+ import { OVERLAY_FLOATS, overlayLays, packSurfaceOverlay, } from '../../surfaceOverlay.js';
63
+ import { frameReflectionsRefused, resolveFrameReflections, } from '../../frameReflections.js';
64
+ import { REFLECTION_SURFACE_FORMAT, ReflectionSurfacePass } from './reflectionSurfacePass.js';
62
65
  import { validateClothBinding } from '../../clothBindingData.js';
63
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';
64
67
  import { createFilmBindGroup, createFilmBindGroupLayout, filmPipeline, FILM_FRAG_FIELDS, FILM_FRAG_SIZE, FILM_VERT_FIELDS, FILM_VERT_SIZE, } from './filmPass.js';
65
68
  import { reflectionTargetSize } from '../../planarReflectionDraw.js';
66
69
  import { drawingBufferSize } from '../../drawingBuffer.js';
67
- import { MOTION_BLUR_MAX_UV, OUTPUT_TRANSFORM_CODE, RUSH_REACH_UV } from '../../vertexDefaults.js';
70
+ import { MOTION_BLUR_MAX_UV, OBJECT_BLUR_UNKEPT, OUTPUT_TRANSFORM_CODE, RUSH_REACH_UV, clampMotionShare, forwardTransformCode, } from '../../vertexDefaults.js';
71
+ import { FRINGE_FLOATS, resolveFringe } from '../../shaders/fringe.js';
72
+ import { displayHeadroom } from '../../displayRange.js';
73
+ import { DEFAULT_FILMIC_CURVE, FILMIC_CLAMPED, FILMIC_CONSTANTS, FILMIC_WITHOUT_COMPOSITE, resolveFilmicCurve, } from '../../filmicCurve.js';
68
74
  import { ExposurePass } from './exposurePass.js';
69
75
  import { AO_STORE } from '../../shaders/ambientOcclusion.js';
70
76
  import { clampAutoExposure } from '../../shaders/exposure.js';
71
77
  import { clampLocalExposure } from '../../shaders/localExposure.js';
72
- import { createDepthResolveLayout, createPostStageLayout, depthResolvePipeline, postPipeline, postPipelineTargets, sceneColorFormat, AO_BLUR_FIELDS, AO_BLUR_FRAG_WGSL, AO_BLUR_SIZE, AO_BLUR_TEXTURES, AO_BLUR_UNIFORMS, AO_FRAG_FIELDS, AO_FRAG_SIZE, AO_FRAG_WGSL, AO_TEXTURES, AO_UNIFORMS, BLOOM_DOWNSAMPLE_FIELDS, BLOOM_DOWNSAMPLE_FRAG_WGSL, BLOOM_PREFILTER_FIELDS, BLOOM_PREFILTER_FRAG_WGSL, BLOOM_STAGE_SIZE, BLOOM_TEXTURES, BLOOM_UNIFORMS, BLOOM_UPSAMPLE_FIELDS, TINTED_ADD_BLEND, BLOOM_UPSAMPLE_FRAG_WGSL, RESOLVED_DEPTH_FORMAT, RUSH_FRAG_FIELDS, RUSH_FRAG_SIZE, RUSH_FRAG_WGSL, RUSH_TEXTURES, RUSH_UNIFORMS, TAA_FRAG_FIELDS, TAA_FRAG_SIZE, TAA_TEXTURES, TAA_UNIFORMS, TEMPORAL_RESOLVE_FRAG_WGSL, MEDIUM_FRAG_FIELDS, MEDIUM_FRAG_SIZE, MEDIUM_FRAG_WGSL, MEDIUM_TEXTURES, MEDIUM_TRANSMIT_BLEND, MEDIUM_UNIFORMS, MEDIUM_UPSAMPLE_FIELDS, MEDIUM_UPSAMPLE_FRAG_WGSL, MEDIUM_UPSAMPLE_SIZE, MEDIUM_UPSAMPLE_TEXTURES, MEDIUM_UPSAMPLE_UNIFORMS, OIT_COMPOSITE_BLEND, OIT_RESOLVE_FRAG_WGSL, OIT_TEXTURES, DECAL_FRAG_FIELDS, DECAL_FRAG_SIZE, DECAL_MULTIPLY_BLEND, DECAL_PROJECT_FRAG_WGSL, DECAL_TEXTURES, DECAL_UNIFORMS, PREMULTIPLIED_OVER_BLEND, REFLECTION_OVER_BLEND, SSR_FRAG_FIELDS, SSR_FRAG_SIZE, SSR_RESOLVE_FRAG_WGSL, SSR_RESOLVE_TEXTURES, SSR_TEXTURES, SSR_TRACE_FRAG_WGSL, SSR_UNIFORMS, createOitResolveLayout, createSsrResolveLayout, } from './postPass.js';
78
+ import { createDepthResolveLayout, createPostStageLayout, depthResolvePipeline, postPipeline, postPipelineTargets, sceneColorFormat, AO_BLUR_FIELDS, AO_BLUR_FRAG_WGSL, AO_BLUR_SIZE, AO_BLUR_TEXTURES, AO_BLUR_UNIFORMS, AO_FRAG_FIELDS, AO_FRAG_SIZE, AO_FRAG_WGSL, AO_TEXTURES, AO_UNIFORMS, BLOOM_DOWNSAMPLE_FIELDS, BLOOM_DOWNSAMPLE_FRAG_WGSL, BLOOM_PREFILTER_FIELDS, BLOOM_PREFILTER_FRAG_WGSL, BLOOM_STAGE_SIZE, BLOOM_TEXTURES, BLOOM_UNIFORMS, BLOOM_UPSAMPLE_FIELDS, TINTED_ADD_BLEND, BLOOM_UPSAMPLE_FRAG_WGSL, RESOLVED_DEPTH_FORMAT, RUSH_FRAG_FIELDS, RUSH_FRAG_SIZE, RUSH_FRAG_WGSL, RUSH_TEXTURES, RUSH_UNIFORMS, TAA_FRAG_FIELDS, TAA_FRAG_SIZE, TAA_TEXTURES, TAA_UNIFORMS, TEMPORAL_RESOLVE_FRAG_WGSL, MEDIUM_FRAG_FIELDS, MEDIUM_FRAG_SIZE, MEDIUM_FRAG_WGSL, MEDIUM_TEXTURES, MEDIUM_TRANSMIT_BLEND, MEDIUM_UNIFORMS, MEDIUM_UPSAMPLE_FIELDS, MEDIUM_UPSAMPLE_FRAG_WGSL, MEDIUM_UPSAMPLE_SIZE, MEDIUM_UPSAMPLE_TEXTURES, MEDIUM_UPSAMPLE_UNIFORMS, OIT_COMPOSITE_BLEND, OIT_RESOLVE_FRAG_WGSL, OIT_TEXTURES, DECAL_FRAG_FIELDS, DECAL_FRAG_SIZE, DECAL_MULTIPLY_BLEND, DECAL_PROJECT_FRAG_WGSL, DECAL_TEXTURES, DECAL_UNIFORMS, PREMULTIPLIED_OVER_BLEND, FRAME_REFLECTION_BLEND, REFLECTION_OVER_BLEND, SSR_FRAG_FIELDS, SSR_FRAG_SIZE, SSR_MATERIAL_FIELDS, SSR_MATERIAL_RESOLVE_FRAG_WGSL, SSR_MATERIAL_SIZE, SSR_MATERIAL_TEXTURES, SSR_MATERIAL_UNIFORMS, SSR_RESOLVE_FRAG_WGSL, SSR_RESOLVE_TEXTURES, SSR_TEXTURES, SSR_TRACE_FRAG_WGSL, SSR_UNIFORMS, createOitResolveLayout, createSsrResolveLayout, } from './postPass.js';
73
79
  import { GRADE_PLACEHOLDER_SIZE, identityGradeLut, validateGradeLut, } from '../../colourGrade.js';
74
80
  import { FILM_LOOK_WITHOUT_COMPOSITE, clampGrain, clampVignette, grainSeed, } from '../../shaders/filmLook.js';
75
81
  import { createProbeMipBindGroupLayout, createProbePrefilterBindGroupLayout, PREFILTER_UNIFORM_OFFSETS, probeLevels, probeMipPipeline, probePrefilterPipeline, PROBE_FACES, PROBE_FACE_BASIS, PROBE_FAR_M, PROBE_FOV_DEG, PROBE_MIP_UNIFORM_SIZE, PROBE_NEAR_M, } from './probePass.js';
@@ -91,10 +97,15 @@ import { VIEW_MODEL_DEPTH_SHARE, viewModelDepthRange } from '../../viewModel.js'
91
97
  import { GlassCasterList } from '../../glassCasters.js';
92
98
  import { GpuGlassTint } from './glassTintPass.js';
93
99
  import { SUN_DYNAMIC_LAYER, SUN_GLASS_MOVING_LAYER, SUN_GLASS_STATIC_LAYER, SUN_PEELED_LAYER, SUN_STATIC_LAYER, } from '../../shadowMap.js';
94
- import { VERTEX_LAYOUT, createGpuMesh, createGpuMeshIncremental } from './buffers.js';
100
+ import { VERTEX_LAYOUT, createGpuMesh, createGpuMeshIncremental, } from './buffers.js';
95
101
  import { SkinPaletteRing } from './skinPaletteRing.js';
96
- import { DEPTH_FORMAT, FLAT_VERT_FIELDS, FLAT_VERT_SIZE, createFlatBindGroup, createFlatBindGroupLayout, flatFragmentBindings, flatVariant, flatVertexBindings, flatPipeline, flatPipelineAsync, } from './flatPass.js';
97
- 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';
98
109
  import { PipelineCache } from './pipelineCache.js';
99
110
  import { SKIN_TARGET_FORMAT, SkinScatterPass } from './skinScatterPass.js';
100
111
  import { SKY_FIELDS, SKY_UNIFORM_SIZE, createSkyBindGroup, createSkyBindGroupLayout, skyPipeline, } from './skyPass.js';
@@ -215,34 +226,20 @@ const MAX_SKY_DRAWS = 64;
215
226
  /**
216
227
  * Whether the environment probe's permutation fits this device's per-stage binding limits.
217
228
  *
218
- * The widest flat variant declares one albedo, three directional shadow maps, twelve
219
- * point-shadow cubes and the probe: seventeen textures and seventeen samplers. WebGPU's default
220
- * is sixteen of each and an adapter is free to offer no more — this one offers 48 textures and
221
- * exactly 16 samplers, so the samplers are what decide it.
229
+ * **Two ceilings, counted separately**: sampled textures and samplers, since textures that share a
230
+ * sampler (`// wgsl:share` in `flat/preamble.ts`) count once against the second. The widest variant
231
+ * binds sixteen textures and twelve samplers, which is exactly WebGPU's default texture ceiling, and
232
+ * `select.ts` asks for the adapter's own — 48 here — so the probe does not depend on that request.
222
233
  */
223
234
  function probeFits(device) {
224
235
  /*
225
- * **Two ceilings, counted separately, and they are no longer the same number.**
226
- *
227
- * The widest permutation declares seventeen sampled textures — one albedo, three directional
228
- * shadow maps, the point-shadow array, a normal map and the probe's own cube — **seven**, where
229
- * it was seventeen while every point light's shadow was a `samplerCube` of its own. `select.ts` asks
230
- * for the adapter's ceiling, which is 48 on this machine against a device default of 16, so
231
- * six fits everywhere and the probe no longer depends on the request being granted.
232
- *
233
- * It used to declare seventeen *samplers* too, and that was the binding constraint: this
234
- * adapter offers exactly sixteen and no request can raise it, so the probe stood down and
235
- * reflective surfaces kept a gradient. The shadow bindings share one declaration — see
236
- * `// wgsl:share shadow` in `flat/preamble.ts`, which costs nothing because they were already
237
- * one object — so the count is four, and the fourth is the normal map's: it is a colour image
238
- * and wants filtering, which a non-filtering shadow sampler cannot give it.
239
- *
240
- * **Counted from the generated bindings rather than typed**: `FLAT_BINDINGS.flatFrag`'s
241
- * widest variant is `directionalShadows+environmentProbe+pointShadows`, seven textures and
242
- * four samplers. A stale number here reads as a device declining a feature it can run.
243
- */
244
- const TEXTURES = 7;
245
- const SAMPLERS = 4;
236
+ * **Counted from the generated bindings rather than typed** (`probeStageCeilings`). It was typed
237
+ * here as seven textures and four samplers while the widest variant grew to sixteen and twelve
238
+ * with the material maps, glass, the fixture atlas and DriftLight: a stale number reads as a
239
+ * device declining a feature it can run, or, the way this one had drifted, as one admitted to a
240
+ * permutation it cannot bind.
241
+ */
242
+ const { textures: TEXTURES, samplers: SAMPLERS } = probeStageCeilings();
246
243
  const fits = device.limits.maxSampledTexturesPerShaderStage >= TEXTURES &&
247
244
  device.limits.maxSamplersPerShaderStage >= SAMPLERS;
248
245
  if (!fits) {
@@ -279,6 +276,7 @@ const VERB_PASS = 3;
279
276
  /** What the depth snapshot copies from, and what it copies into. */
280
277
  const DEPTH_SOURCE = maskOf('sceneDepth');
281
278
  const DEPTH_SNAPSHOT = maskOf('depthSnapshot');
279
+ const COLOR_SNAPSHOT = maskOf('colorSnapshot');
282
280
  /** The mirror's own colour and depth. */
283
281
  const MIRROR_TARGET = maskOf('mirrorColor', 'mirrorDepth');
284
282
  /**
@@ -366,6 +364,13 @@ const MAX_WATER_BODIES_PER_FRAME = 16;
366
364
  * numbers, because that is precisely the defect the ring exists to end.
367
365
  */
368
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;
369
374
  /** Buffer usage flags, from the specification. See `buffers.ts` for why they are not read off the global. */
370
375
  const USAGE_UNIFORM_DST = 0x0040 | 0x0008;
371
376
  /**
@@ -866,6 +871,8 @@ export class WebGPURenderer {
866
871
  skinnedBindGroupLayout;
867
872
  morphedBindGroupLayout;
868
873
  bothBindGroupLayout;
874
+ /** The instanced layout of a batch playing a bone animation. See `animatedGroup`. */
875
+ animatedBindGroupLayout;
869
876
  /** The morph weights the following draws use, or null for none. */
870
877
  morphWeights = null;
871
878
  /**
@@ -878,14 +885,15 @@ export class WebGPURenderer {
878
885
  * group per character per frame — an allocation in the frame loop, which `AGENTS.md` forbids.
879
886
  * Keyed by slot instead, the groups are built once and reused for the life of the renderer.
880
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
+ */
881
896
  skinnedGroups = [];
882
- /** The unskinned group each was built beside, for identity-based invalidation. */
883
- skinnedGroupsBeside = [];
884
- skinnedGroupsPalette = [];
885
- skinnedGroupsDeltas = [];
886
- /** The cloth views each skinned twin was built with, compared by identity like the palette. */
887
- skinnedGroupsCloth = [];
888
- skinnedGroupsClothBinding = [];
889
897
  /** The cloth the next skinned draws are placed by, or null. See `setCloth`. */
890
898
  cloth = null;
891
899
  clothStandCache = null;
@@ -953,22 +961,51 @@ export class WebGPURenderer {
953
961
  /** Every distinct cache above, so a mesh's pipelines are built for each. One entry when aliased. */
954
962
  pipelineTargets;
955
963
  bindGroupLayout;
956
- /** 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. */
957
965
  perDraw;
958
- /** The lights, fog and camera, settled once per frame by `bindMeshPass`. */
959
- perFrame;
960
966
  /**
961
- * 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`.
962
978
  *
963
979
  * Authoritative between draws: a setter changes one field here and marks the block dirty, and
964
- * the next `drawMesh` takes a slot and copies the whole thing in. That keeps a setter free of
965
- * 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.
966
982
  */
967
983
  perFrameStaging;
968
984
  perFrameFloats;
969
985
  perFrameInts;
986
+ /** The two halves of the scratch, as the words a slot is written from. */
987
+ passInts;
988
+ materialInts;
970
989
  /** The slot the open material occupies, and whether the next draw must take a new one. */
971
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;
972
1009
  /*
973
1010
  * The frame graph's recording, and the machinery that replays it.
974
1011
  *
@@ -1221,7 +1258,7 @@ export class WebGPURenderer {
1221
1258
  variant;
1222
1259
  /** Whether this profile compiled the point-shadow path, and so has sentinels to write. */
1223
1260
  pointShadowsCompiled;
1224
- /** 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. */
1225
1262
  fragment;
1226
1263
  depth = null;
1227
1264
  depthView = null;
@@ -1280,6 +1317,17 @@ export class WebGPURenderer {
1280
1317
  */
1281
1318
  refractSnapshot = null;
1282
1319
  refractSnapshotView = null;
1320
+ /**
1321
+ * The frame's colour as a registered pass reads it (`reads: ['colorSnapshot']`): the scene's own
1322
+ * size and format, refilled at each such pass's draw. Allocated only once a pass declares the
1323
+ * read, beside the frame's targets, and never in the frame loop. See `takePassColour`.
1324
+ */
1325
+ passColour = null;
1326
+ passColourView = null;
1327
+ /** Registered passes that read the colour, queued this frame for after the upscale. */
1328
+ latePasses = [];
1329
+ /** A late pass draws unjittered, as every late draw does. */
1330
+ latePassJitter = new Float32Array(2);
1283
1331
  /** Whether this frame has taken it. One snapshot serves every refracting draw. */
1284
1332
  refractSnapshotTaken = false;
1285
1333
  /**
@@ -1782,8 +1830,18 @@ export class WebGPURenderer {
1782
1830
  reflectionQueue = new ReflectionQueue();
1783
1831
  ssrLayout;
1784
1832
  ssrResolveLayout;
1833
+ /** The frame's materials' reflections, resolved; null where off. See `frameReflections.ts`. */
1834
+ frameReflections;
1835
+ /** Their surface half, null where they are off or cannot be drawn: `reflectionSurfacePass.ts`. */
1836
+ reflectionSurface;
1837
+ /** The material resolve: its layout, its one block (the blur) and its group over the three maps. */
1838
+ ssrMaterialLayout;
1839
+ ssrMaterialUniforms;
1840
+ ssrMaterialFloats = new Float32Array(SSR_MATERIAL_SIZE / 4);
1841
+ ssrMaterialGroup = null;
1785
1842
  ssrUniforms;
1786
- ssrStaging = new ArrayBuffer(SSR_SLOT * MAX_REFLECTIVE_SURFACES);
1843
+ /* One slot a surface, and one more for the frame's materials' trace. */
1844
+ ssrStaging = new ArrayBuffer(SSR_SLOT * (MAX_REFLECTIVE_SURFACES + 1));
1787
1845
  ssrFloats = new Float32Array(this.ssrStaging);
1788
1846
  ssrReflection = null;
1789
1847
  ssrReflectionView = null;
@@ -1920,7 +1978,7 @@ export class WebGPURenderer {
1920
1978
  recordNode(this.arena, VERB_SCOPE, 0, MIRROR_TARGET, 0, 0);
1921
1979
  this.pass?.end();
1922
1980
  this.flushRings();
1923
- this.surface.device.queue.submit([this.encoder.finish()]);
1981
+ this.submit([this.encoder.finish()]);
1924
1982
  this.encoder = this.surface.device.createCommandEncoder({ label: 'reflection' });
1925
1983
  /*
1926
1984
  * **Opened on demand, like the frame's own.** Nothing has been recorded into the mirror yet,
@@ -1978,7 +2036,8 @@ export class WebGPURenderer {
1978
2036
  this.windStreakFrags,
1979
2037
  this.scatterVerts,
1980
2038
  this.scatterFrags,
1981
- this.perFrame,
2039
+ this.passRing,
2040
+ this.viewRing,
1982
2041
  this.filmVerts,
1983
2042
  this.filmFrags,
1984
2043
  this.insetUniforms,
@@ -1999,6 +2058,7 @@ export class WebGPURenderer {
1999
2058
  this.instanceCulls?.flush();
2000
2059
  for (const ring of this.rings())
2001
2060
  ring.flush();
2061
+ this.materialStore.flush();
2002
2062
  }
2003
2063
  /** Where every ring stands before work that submits an encoder of its own. */
2004
2064
  ringMarks = [];
@@ -2018,6 +2078,8 @@ export class WebGPURenderer {
2018
2078
  const rings = this.rings();
2019
2079
  for (let i = 0; i < rings.length; i++)
2020
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();
2021
2083
  this.skinPalettes.rewind(skinMark);
2022
2084
  if (this.skinPaletteSlot >= skinMark)
2023
2085
  this.skinPaletteSlot = -1;
@@ -2263,9 +2325,16 @@ export class WebGPURenderer {
2263
2325
  camera.updateMatrices(target.width / target.height);
2264
2326
  const { device } = this.surface;
2265
2327
  const { depth, colorMsaa } = target.attachments(this.samples, this.pipelines.format);
2266
- const savedPass = this.pass;
2328
+ /*
2329
+ * **Flushed, and only then set aside.** The flush replays what the frame has recorded, and
2330
+ * replaying opens the frame's pass where none is open yet; saved before it, the pass given back
2331
+ * afterwards was the null that stood there, while the one the flush opened was left open on the
2332
+ * frame's encoder and every later pass on it was refused — a black frame for any capture taken
2333
+ * after the frame had drawn something. `scripts/capture-check.mjs` takes one at three points.
2334
+ */
2267
2335
  if (this.quality.frameGraph)
2268
2336
  this.flushGraph();
2337
+ const savedPass = this.pass;
2269
2338
  const savedEncoder = this.encoder;
2270
2339
  this.markRings();
2271
2340
  const skinMark = this.skinPalettes.mark();
@@ -2308,7 +2377,7 @@ export class WebGPURenderer {
2308
2377
  pass.end();
2309
2378
  this.pass = null;
2310
2379
  this.flushRings();
2311
- device.queue.submit([encoder.finish()]);
2380
+ this.submit([encoder.finish()]);
2312
2381
  this.rewindRings(skinMark);
2313
2382
  this.encoderEpoch = frameEpoch;
2314
2383
  this.encoder = savedEncoder;
@@ -2316,6 +2385,7 @@ export class WebGPURenderer {
2316
2385
  this.perFrameInts[this.materialField('uOutputTransform')] = heldGrade;
2317
2386
  this.perFrameFloats[this.materialField('uOutputExposure')] = heldExposure;
2318
2387
  this.materials.dirty();
2388
+ this.reopenPass();
2319
2389
  this.capturePassActive = false;
2320
2390
  }
2321
2391
  return true;
@@ -2331,10 +2401,11 @@ export class WebGPURenderer {
2331
2401
  updateSurfaceTexture(texture, source) {
2332
2402
  if (this.surface.lost)
2333
2403
  return;
2334
- const replaced = texture.update(source);
2404
+ const replaced = texture.update(source, this.compressedFormats);
2335
2405
  if (replaced === null)
2336
2406
  return;
2337
- /* 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. */
2338
2409
  this.retiredTextures.push(replaced);
2339
2410
  this.forgetBindingsOf(texture);
2340
2411
  }
@@ -2371,6 +2442,8 @@ export class WebGPURenderer {
2371
2442
  this.emissiveMap = null;
2372
2443
  if (this.modelMap === texture)
2373
2444
  this.modelMap = null;
2445
+ if (this.overlayMaps === texture)
2446
+ this.overlayMaps = null;
2374
2447
  this.forgetBindingsOf(texture);
2375
2448
  texture.dispose();
2376
2449
  this.captureTargets.get(texture)?.dispose();
@@ -2388,6 +2461,9 @@ export class WebGPURenderer {
2388
2461
  /* Same reasoning, for whichever SDF labels were bound against this atlas. */
2389
2462
  this.sdfTextBindGroups.delete(texture);
2390
2463
  this.flatBindGroups.clear();
2464
+ this.flatGroupEpoch += 1;
2465
+ this.overlayGroups.clear();
2466
+ this.skinnedGroups.length = 0;
2391
2467
  /*
2392
2468
  * The blank one is built with the albedo held aside, because `buildFlatBindGroup` reads
2393
2469
  * `this.albedo` for that slot — leaving it in place would put a real colour map into the
@@ -2549,6 +2625,13 @@ export class WebGPURenderer {
2549
2625
  const emissiveMap = (material?.emissive ?? null);
2550
2626
  const modelMap = (material?.modelMap ?? null);
2551
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();
2552
2635
  /* What the blur spreads this skin by: the last material to name a profile says. */
2553
2636
  const model = material?.model;
2554
2637
  warnUnpagedLightmap(model, modelMap);
@@ -2568,7 +2651,8 @@ export class WebGPURenderer {
2568
2651
  const cut = this.materialField('uCutout');
2569
2652
  f[cut] = cutoff;
2570
2653
  f[cut + 1] = CUTOUT_RESOLVE_CODE[this.cutoutResolveStaged];
2571
- 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;
2572
2656
  /* The material's lighting channels, which the lamps' are compared with. See `uCutout`. */
2573
2657
  f[cut + 3] = lightChannelsOf(material?.lightChannels);
2574
2658
  i[flags + 3] = material?.doubleSided === true ? 1 : 0;
@@ -2723,16 +2807,45 @@ export class WebGPURenderer {
2723
2807
  packAmbientSH(coefficients, this.ambientSH);
2724
2808
  this.material((f) => f.set(this.ambientSH, this.materialField('uAmbientSH')));
2725
2809
  }
2810
+ /**
2811
+ * The overlay the following draws wear, or null for none: pass state, put back to none by
2812
+ * `bindMeshPass`. See `renderer.ts` and `surfaceOverlay.ts`. The numbers go in the material block,
2813
+ * which every variant declares; the atlas goes in a twin of each flat group (`overlayGroup`).
2814
+ */
2815
+ setSurfaceOverlay(overlay) {
2816
+ if (this.surface.lost)
2817
+ return;
2818
+ packSurfaceOverlay(overlay, this.overlayFloats);
2819
+ /* The first overlay laid is when the lit pipelines start carrying one. */
2820
+ if (overlayLays(overlay))
2821
+ this.enableLit('SURFACE_OVERLAY');
2822
+ this.overlayMaps = (overlay?.maps ?? null);
2823
+ this.material((f) => f.set(this.overlayFloats, this.materialField('uOverlay')));
2824
+ }
2726
2825
  /** How reflective the surfaces drawn next are. Clamped, as `renderer.ts` clamps it. */
2727
2826
  setSurfaceReflectivity(amount) {
2728
- this.material((f) => {
2729
- f[this.materialField('uEnvironmentDials') + 1] = Math.min(1, Math.max(0, amount));
2730
- });
2827
+ this.passReflectivity = Math.min(1, Math.max(0, amount));
2828
+ this.writeEnvironmentDials();
2731
2829
  }
2732
2830
  /** How bright the environment the surfaces drawn next reflect is. See `renderer.ts`. */
2733
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() {
2734
2845
  this.material((f) => {
2735
- 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;
2736
2849
  });
2737
2850
  }
2738
2851
  setSurfaceGrain(amount) {
@@ -3203,7 +3316,7 @@ export class WebGPURenderer {
3203
3316
  }
3204
3317
  this.pass?.end();
3205
3318
  this.flushRings();
3206
- this.surface.device.queue.submit([this.encoder.finish()]);
3319
+ this.submit([this.encoder.finish()]);
3207
3320
  this.encoder = this.surface.device.createCommandEncoder({ label: 'frame.resumed' });
3208
3321
  this.reflectionPassActive = false;
3209
3322
  this.reflectionReady = true;
@@ -3393,7 +3506,7 @@ export class WebGPURenderer {
3393
3506
  */
3394
3507
  gradeCode() {
3395
3508
  return !this.hasComposite || !this.quality.hdrScene
3396
- ? (OUTPUT_TRANSFORM_CODE[this.quality.outputTransform] ?? 0)
3509
+ ? forwardTransformCode(this.quality.outputTransform)
3397
3510
  : 0;
3398
3511
  }
3399
3512
  gradeExposure() {
@@ -4587,6 +4700,7 @@ export class WebGPURenderer {
4587
4700
  */
4588
4701
  setWind(windX, windZ, windGust, timeSeconds) {
4589
4702
  resolveScatterDeform(windX, windZ, windGust, timeSeconds, null, this.frameWind);
4703
+ this.reopenViews();
4590
4704
  }
4591
4705
  /**
4592
4706
  * The five wind fields of one shadow-draw slot.
@@ -4604,21 +4718,6 @@ export class WebGPURenderer {
4604
4718
  this.shadowDraws.writeFloat(slot, DEPTH_VERT_FIELDS.uWindTime.offset, d.time);
4605
4719
  this.shadowDraws.writeFloats(slot, DEPTH_VERT_FIELDS.uWindSpatialPhase.offset, d.spatialPhase);
4606
4720
  }
4607
- /**
4608
- * The five wind fields of one per-draw slot.
4609
- *
4610
- * **Never called for an instanced draw.** That variant declares none of these, and
4611
- * `flatPass.ts` records what writing a field a variant does not declare costs: the block is
4612
- * shorter, so the write lands on whatever occupies that offset instead.
4613
- */
4614
- writeWind(slot) {
4615
- const d = this.frameWind;
4616
- this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uWindDirection.offset, d.direction);
4617
- this.perDraw.writeFloat(slot, FLAT_VERT_FIELDS.uWindSpeed.offset, d.bend);
4618
- this.perDraw.writeFloat(slot, FLAT_VERT_FIELDS.uWindGust.offset, d.gust);
4619
- this.perDraw.writeFloat(slot, FLAT_VERT_FIELDS.uWindTime.offset, d.time);
4620
- this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uWindSpatialPhase.offset, d.spatialPhase);
4621
- }
4622
4721
  /** Refilled per frame rather than allocated; see `resolvePointLights`. */
4623
4722
  lights = {
4624
4723
  count: 0,
@@ -4768,6 +4867,9 @@ export class WebGPURenderer {
4768
4867
  * at submit, and a texture's view is new when an update changes its size.
4769
4868
  */
4770
4869
  shadowCutoutLayout;
4870
+ /** A crowd's casters: the plain and cutout layouts with a bone animation's three textures. */
4871
+ shadowAnimatedLayout;
4872
+ shadowAnimatedCutoutLayout;
4771
4873
  cutoutGroups = new Map();
4772
4874
  cutoutPeelGroups = new Map();
4773
4875
  shadowPeelPassUniforms = null;
@@ -4980,19 +5082,47 @@ export class WebGPURenderer {
4980
5082
  if (count === 0)
4981
5083
  return;
4982
5084
  const cutout = cutoutOf(material);
4983
- 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;
4984
5097
  const slot = this.shadowDraws.allocate();
4985
5098
  if (slot === null)
4986
5099
  return;
4987
5100
  /* Only the light matrix: the instanced depth variant has no uModel, its placement being
4988
5101
  the four attribute columns the batch's buffer supplies. */
4989
5102
  this.shadowDraws.writeFloats(slot, DEPTH_VERT_FIELDS.uLightViewProj.offset, this.correctedLightViewProj);
4990
- if (cutout !== null && cutoutGroup !== null) {
5103
+ const cuts = cutoutTexture !== null && cutoutGroup !== null;
5104
+ if (cutout !== null && cuts) {
4991
5105
  this.writeCutout(slot, cutout, DEPTH_INSTANCED_CUTOUT_VERT_FIELDS);
4992
5106
  }
4993
- 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
+ }
4994
5116
  const cull = material?.doubleSided === true ? 'none' : this.depthCullMode;
4995
- 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));
4996
5126
  pass.setBindGroup(0, cutoutGroup ?? this.shadowGroup(), [slot]);
4997
5127
  let index = 0;
4998
5128
  for (; index < geometry.vertexBuffers.length; index++) {
@@ -5313,6 +5443,10 @@ export class WebGPURenderer {
5313
5443
  this.skinPalettes = new SkinPaletteRing(quality.drawsPerFrame);
5314
5444
  this.maxDrawingBufferPixels = quality.maxDrawingBufferPixels;
5315
5445
  this.exposure = quality.outputExposure;
5446
+ resolveFilmicCurve(DEFAULT_FILMIC_CURVE, this.filmConstants);
5447
+ if (quality.outputTransform === 'filmic' && !(quality.screenEffects && quality.hdrScene)) {
5448
+ console.warn(FILMIC_WITHOUT_COMPOSITE);
5449
+ }
5316
5450
  this.flushSchedule =
5317
5451
  quality.frameGraph && quality.identifierGraph
5318
5452
  ? createFlushSchedule(MAX_DRAWS_PER_FRAME)
@@ -5382,6 +5516,25 @@ export class WebGPURenderer {
5382
5516
  console.warn("WebGPU: skinScattering 'screen-space' needs screenEffects to add into; skin is pre-integrated");
5383
5517
  }
5384
5518
  this.skinScatter = skinScreen ? new SkinScatterPass(device) : null;
5519
+ /*
5520
+ * The frame's materials' reflections, where the frame can carry them: scene light to swap the
5521
+ * probe's share in, and one sample, which the trace's depth copy and both maps are. Said once
5522
+ * where it cannot. See `frameReflections.ts`.
5523
+ */
5524
+ this.frameReflections = resolveFrameReflections(quality.screenSpaceReflections);
5525
+ const reflectionsRefusal = this.frameReflections === null
5526
+ ? null
5527
+ : !quality.screenEffects || !quality.hdrScene
5528
+ ? 'the frame keeps no scene light to swap the probe in (`screenEffects` and `hdrScene`)'
5529
+ : this.samples > 1
5530
+ ? 'a multisampled frame has no single depth or colour to march through'
5531
+ : null;
5532
+ if (reflectionsRefusal !== null)
5533
+ console.warn(`WebGPU: ${frameReflectionsRefused(reflectionsRefusal)}`);
5534
+ this.reflectionSurface =
5535
+ this.frameReflections !== null && reflectionsRefusal === null
5536
+ ? new ReflectionSurfacePass(device)
5537
+ : null;
5385
5538
  this.skinPipelines = skinScreen
5386
5539
  ? new PipelineCache(device, SKIN_TARGET_FORMAT, this.samples, glassShadows, quality.clusteredLights)
5387
5540
  : null;
@@ -5410,7 +5563,7 @@ export class WebGPURenderer {
5410
5563
  pointShadows: quality.pointShadows,
5411
5564
  });
5412
5565
  this.pointShadowsCompiled = quality.pointShadows;
5413
- this.fragment = flatFragmentBindings(this.variant);
5566
+ this.fragment = flatFragmentScratch(this.variant);
5414
5567
  this.bindGroupLayout = createFlatBindGroupLayout(device, this.variant);
5415
5568
  /*
5416
5569
  * The skinned twin, built eagerly because it is a descriptor rather than a compile — WebGPU
@@ -5420,24 +5573,32 @@ export class WebGPURenderer {
5420
5573
  this.skinnedBindGroupLayout = createFlatBindGroupLayout(device, this.variant, true);
5421
5574
  this.morphedBindGroupLayout = createFlatBindGroupLayout(device, this.variant, false, true);
5422
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);
5423
5578
  this.perDraw = new UniformRing(device, FLAT_VERT_SIZE, quality.drawsPerFrame, USAGE_UNIFORM_DST, 'flat.vertRing', this.drawBudget);
5424
- this.perFrameStaging = new ArrayBuffer(this.fragment.uniformSize);
5579
+ const { passSize, materialSize } = this.fragment;
5580
+ this.perFrameStaging = new ArrayBuffer(passSize + materialSize);
5425
5581
  this.perFrameFloats = new Float32Array(this.perFrameStaging);
5426
5582
  this.perFrameInts = new Int32Array(this.perFrameStaging);
5427
- /*
5428
- * **A ring, because the block holds material state and a material changes between draws.**
5429
- *
5430
- * `setSurfaceTexture`, `setSurfaceGrain` and their siblings are pass state on both backends
5431
- * — a material covers many draws, so the alternative is rebinding the same values twenty
5432
- * times to say the same thing. On WebGL2 each is a `uniform1f` that lands immediately. Here
5433
- * they live in this block, and `queue.writeBuffer` does not interleave with recorded
5434
- * commands, so one buffer rewritten between two draws gives *both* the second value. Every
5435
- * surface in a scene would wear the last material set, which is bug 7's shape at pass scale.
5436
- *
5437
- * A slot per *material change* rather than per draw: `drawMesh` reuses the open slot until
5438
- * something dirties it, so a scene drawing a hundred meshes of one material spends one.
5439
- */
5440
- 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');
5441
5602
  /*
5442
5603
  * A one-pixel stand-in for the albedo binding. The albedo's flag is zero for untextured
5443
5604
  * geometry, which is nearly all of it, so the shader never samples this — but WebGPU
@@ -5570,7 +5731,7 @@ export class WebGPURenderer {
5570
5731
  })
5571
5732
  .end();
5572
5733
  }
5573
- device.queue.submit([clear.finish()]);
5734
+ this.submit([clear.finish()]);
5574
5735
  /*
5575
5736
  * `nearest` on a depth texture sampled as an ordinary float, matching what
5576
5737
  * `shadowMap.ts` asks WebGL2 for. A filtering sampler cannot be used on a depth format
@@ -5737,6 +5898,11 @@ export class WebGPURenderer {
5737
5898
  addressModeU: 'clamp-to-edge',
5738
5899
  addressModeV: 'clamp-to-edge',
5739
5900
  });
5901
+ /* Or a surface overlay's atlas, in the twin a draw wearing one binds: `overlayGroup`. */
5902
+ const maps = this.overlayBinding ? this.overlayMaps : null;
5903
+ if (maps !== null) {
5904
+ return { view: maps.layerView() ?? blankView, sampler: this.refractSampler };
5905
+ }
5740
5906
  return { view: this.refractSnapshotView ?? blankView, sampler: this.refractSampler };
5741
5907
  }
5742
5908
  /*
@@ -5867,6 +6033,8 @@ export class WebGPURenderer {
5867
6033
  { binding: RUSH_TEXTURES.uExposureHeld },
5868
6034
  /* Local exposure's grid, read filtered between tiles. See `localExposure.ts`. */
5869
6035
  { binding: RUSH_TEXTURES.uExposureLocal },
6036
+ /* Each mover's own motion: the reconstruction's target, or the scene where there is none. */
6037
+ { binding: RUSH_TEXTURES.uMotion },
5870
6038
  ]);
5871
6039
  this.rushUniforms = device.createBuffer({
5872
6040
  label: 'post.rushUniforms',
@@ -5884,11 +6052,24 @@ export class WebGPURenderer {
5884
6052
  this.ssrLayout = createPostStageLayout(device, SSR_UNIFORMS, SSR_FRAG_SIZE, [
5885
6053
  { binding: SSR_TEXTURES.uSsrDepth, filterable: false },
5886
6054
  { binding: SSR_TEXTURES.uSsrScene },
6055
+ /* The frame's materials, for the trace that reads them: `frameReflections.ts`. */
6056
+ { binding: SSR_TEXTURES.uSsrProbeMap },
6057
+ { binding: SSR_TEXTURES.uSsrTintMap },
5887
6058
  ]);
5888
6059
  this.ssrResolveLayout = createSsrResolveLayout(device);
6060
+ this.ssrMaterialLayout = createPostStageLayout(device, SSR_MATERIAL_UNIFORMS, SSR_MATERIAL_SIZE, [
6061
+ { binding: SSR_MATERIAL_TEXTURES.uSsrReflection },
6062
+ { binding: SSR_MATERIAL_TEXTURES.uSsrProbeMap },
6063
+ { binding: SSR_MATERIAL_TEXTURES.uSsrTintMap },
6064
+ ]);
6065
+ this.ssrMaterialUniforms = device.createBuffer({
6066
+ label: 'post.ssrMaterialUniforms',
6067
+ size: SSR_MATERIAL_SIZE,
6068
+ usage: USAGE_UNIFORM_DST,
6069
+ });
5889
6070
  this.ssrUniforms = device.createBuffer({
5890
6071
  label: 'post.ssrUniforms',
5891
- size: SSR_SLOT * MAX_REFLECTIVE_SURFACES,
6072
+ size: SSR_SLOT * (MAX_REFLECTIVE_SURFACES + 1),
5892
6073
  usage: USAGE_UNIFORM_DST,
5893
6074
  });
5894
6075
  this.decalUniforms = device.createBuffer({
@@ -6038,6 +6219,8 @@ export class WebGPURenderer {
6038
6219
  this.shadowLayout = createDepthBindGroupLayout(device);
6039
6220
  this.shadowSkinnedLayout = createDepthBindGroupLayout(device, true);
6040
6221
  this.shadowCutoutLayout = createDepthBindGroupLayout(device, false, true);
6222
+ this.shadowAnimatedLayout = createDepthBindGroupLayout(device, false, false, true);
6223
+ this.shadowAnimatedCutoutLayout = createDepthBindGroupLayout(device, false, true, true);
6041
6224
  this.shadowDraws = new UniformRing(device,
6042
6225
  /* The cutout block is the plain one and two fields more, and a slot must hold either. */
6043
6226
  Math.max(DEPTH_VERT_SIZE, DEPTH_CUTOUT_VERT_SIZE), quality.drawsPerFrame, USAGE_UNIFORM_DST, 'shadow.drawRing', this.shadowDrawBudget);
@@ -6304,7 +6487,11 @@ export class WebGPURenderer {
6304
6487
  /* Last frame's snapshot is last frame's scene. */
6305
6488
  this.refractSnapshotTaken = false;
6306
6489
  this.skinScattered = false;
6490
+ /* One frame of the jitter's sequence however many times the frame binds its camera. */
6491
+ this.temporalHistory.nextFrame();
6492
+ this.latePasses.length = 0;
6307
6493
  this.skinScatter?.reset();
6494
+ this.reflectionSurface?.reset();
6308
6495
  this.plumeVerts.reset();
6309
6496
  this.plumeFrags.reset();
6310
6497
  this.waterVerts.reset();
@@ -6332,9 +6519,18 @@ export class WebGPURenderer {
6332
6519
  this.lineVerts.reset();
6333
6520
  this.lineFrags.reset();
6334
6521
  this.skyUniforms.reset();
6335
- /* The material ring, and the open slot with it: last frame's slots are gone. */
6336
- 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;
6337
6532
  this.materials.dirty();
6533
+ this.reopenPass();
6338
6534
  /* Spent by `runMotion` at the end of the last frame; a frame states its movers afresh. */
6339
6535
  this.motionPass?.reset();
6340
6536
  this.latePass?.reset();
@@ -6632,6 +6828,37 @@ export class WebGPURenderer {
6632
6828
  },
6633
6829
  };
6634
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
+ }
6635
6862
  /**
6636
6863
  * Rewrite a mesh's positions, and its normals where the caller has them.
6637
6864
  *
@@ -6652,20 +6879,35 @@ export class WebGPURenderer {
6652
6879
  * `changeFrames.ts`.
6653
6880
  */
6654
6881
  const motion = mesh.motion;
6655
- let previous = null;
6882
+ let staged = null;
6656
6883
  if (motion !== null && this.reconstructionWanted) {
6657
6884
  const frame = frameOfChange(this.moverFrame, this.inFrame);
6885
+ const size = mesh.vertexBuffers[0]?.size ?? 0;
6886
+ let previous = null;
6658
6887
  if (keepsPrevious(motion.changed, frame)) {
6659
6888
  motion.previous ??= this.surface.device.createBuffer({
6660
6889
  label: 'recon.dynamicPrevious',
6661
- size: mesh.vertexBuffers[0]?.size ?? 0,
6890
+ size,
6662
6891
  usage: 0x0020 | 0x0008, // VERTEX | COPY_DST
6663
6892
  });
6664
6893
  previous = motion.previous;
6665
6894
  }
6666
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;
6667
6909
  }
6668
- mesh.update(this.surface.device, positions, normals, previous);
6910
+ mesh.update(this.surface.device, positions, normals, staged);
6669
6911
  }
6670
6912
  /**
6671
6913
  * Settle everything the frame shares: the camera, the lights and the fog.
@@ -6855,12 +7097,20 @@ export class WebGPURenderer {
6855
7097
  * number, so this is a copy and is marked as one; the two must move together.
6856
7098
  */
6857
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;
6858
7105
  f[at('uEnvironmentDials') + 1] = 0;
6859
7106
  /* No weighting and no dither until a pass or a crossfade asks. See `setDitherFade`. */
6860
7107
  f[at('uWriteMode')] = 0;
6861
7108
  f[at('uWriteMode') + 1] = 0;
6862
7109
  /* 1, not 0: a multiplier's identity is one. See `setEnvironmentGain`. */
6863
7110
  f[at('uEnvironmentDials') + 2] = 1;
7111
+ /* No overlay until a draw sets one. See `setSurfaceOverlay`. */
7112
+ this.overlayFloats.fill(0);
7113
+ f.fill(0, at('uOverlay'), at('uOverlay') + OVERLAY_FLOATS);
6864
7114
  /* The frame's own ambient until a draw asks for its own. See `setAmbientSH`. */
6865
7115
  f.fill(0, at('uAmbientSH'), at('uAmbientSH') + AMBIENT_SH_FLOATS);
6866
7116
  /*
@@ -6962,6 +7212,8 @@ export class WebGPURenderer {
6962
7212
  if (this.quality.nightEmissive)
6963
7213
  f[at('uNightEmissive')] = env.nightEmissive ?? 0;
6964
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;
6965
7217
  f.set(env.highlightMax, at('uHighlightMax'));
6966
7218
  /* The gain in the far corner's w. See `uHighlightMax`. */
6967
7219
  f[at('uHighlightMax') + 3] = env.highlightGain;
@@ -7214,6 +7466,7 @@ export class WebGPURenderer {
7214
7466
  * same terms here, which is why a pass cannot inherit a material from the one before it.
7215
7467
  */
7216
7468
  this.materials.dirty();
7469
+ this.reopenPass();
7217
7470
  /*
7218
7471
  * A pass starts with no material, exactly as `renderer.ts` resets these terms — **every map,
7219
7472
  * not the albedo alone**. `setMaterial` skips the lookup when the maps it is given are the ones
@@ -7229,35 +7482,112 @@ export class WebGPURenderer {
7229
7482
  this.emissiveMap = null;
7230
7483
  this.modelMap = null;
7231
7484
  this.bindGroup = this.blankAlbedoBindGroup;
7485
+ /* Nor an overlay; its numbers are zeroed with the block's other defaults. */
7486
+ this.overlayMaps = null;
7232
7487
  this.uvScale[0] = 1;
7233
7488
  this.uvScale[1] = 1;
7234
7489
  this.uvOffset[0] = 0;
7235
7490
  this.uvOffset[1] = 0;
7236
7491
  }
7237
7492
  /**
7238
- * 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.
7239
7495
  *
7240
- * **A copy per material change, not per draw.** A scene drawing a hundred meshes of one
7241
- * material spends one slot and one copy; a scene switching material every draw spends one
7242
- * 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.
7243
7500
  */
7244
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
+ }
7245
7518
  if (this.materials.open)
7246
7519
  return this.materials.slot;
7247
- const slot = this.perFrame.allocate();
7520
+ const slot = this.materialStore.place(this.materialInts);
7248
7521
  if (slot === null) {
7249
- if (!this.warnedMaterialsFull) {
7250
- this.warnedMaterialsFull = true;
7251
- 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.`);
7252
- }
7522
+ this.materialsRefused += 1;
7523
+ this.warnMaterialsFull();
7253
7524
  return null;
7254
7525
  }
7255
- this.perFrame.writeBlock(slot, this.perFrameInts);
7256
7526
  this.materials.slot = slot;
7257
7527
  return slot;
7258
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
+ }
7259
7588
  /** Said once rather than every frame, for the reason `warnedFull` gives. */
7260
7589
  warnedMaterialsFull = false;
7590
+ warnedPassesFull = false;
7261
7591
  warnedSkyFull = false;
7262
7592
  warnedParticlesTwice = false;
7263
7593
  /**
@@ -7292,8 +7622,12 @@ export class WebGPURenderer {
7292
7622
  * What this saves is the GPU's share, which for a heavy mesh is most of the cost and for a
7293
7623
  * trivial one is very little.
7294
7624
  */
7295
- 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)) {
7296
7629
  return;
7630
+ }
7297
7631
  /*
7298
7632
  * And the other half of the same question, where the profile asked for one. Gated on
7299
7633
  * `cullDraws` alongside the frustum test, because both are the renderer doing a consumer's
@@ -7310,6 +7644,9 @@ export class WebGPURenderer {
7310
7644
  */
7311
7645
  if (this.tracksMotionNow())
7312
7646
  this.recordDrawMotion(mesh, model, previousModel);
7647
+ else if (previousModel !== null && this.quality.cameraMotionBlur * this.motionBlurScale > 0) {
7648
+ this.warnObjectBlur();
7649
+ }
7313
7650
  noteMapsWithoutUvs(mesh, mesh.hasUvs, this.materialHasMaps);
7314
7651
  /* An opaque draw meets the medium as the dial says: `submitMesh` reads an absent `fog` as
7315
7652
  true, which is a translucent draw's default and would put every opaque draw back in the fog
@@ -7455,17 +7792,17 @@ export class WebGPURenderer {
7455
7792
  `TranslucentMeshOptions.reconstructed`. */
7456
7793
  const lands = blend && options.reconstructed !== true && this.drawsLate();
7457
7794
  const late = lands || (blend && this.oitReplaying && this.reconstructing);
7458
- 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;
7459
7800
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uModel.offset, model);
7460
7801
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uTint.offset, tint ?? WHITE);
7461
- /* The frame's wind, for a mesh carrying a sway lane. Written on this path and not the
7462
- instanced one, which declares none of these fields. */
7463
- this.writeWind(slot);
7464
7802
  /* The material's repeats, not a constant: geometry authored in metres is textured at
7465
7803
  whatever density the material asks for without rebuilding it. */
7466
7804
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvScale.offset, this.uvScale);
7467
7805
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvOffset.offset, this.uvOffset);
7468
- this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uLightViewProj.offset, this.lightViewProj);
7469
7806
  /*
7470
7807
  * Whether this mesh's tangent frame is real. WebGPU has no disabled attribute, so location 10
7471
7808
  * is always backed — by the caller's data or by the constant buffer — which makes the binding
@@ -7640,8 +7977,10 @@ export class WebGPURenderer {
7640
7977
  command.pipeline = pipeline;
7641
7978
  command.bindGroup = this.flatBindGroupFor(skinned, morphed, deltas);
7642
7979
  command.offsetA = slot;
7643
- command.offsetB = material;
7644
- command.offsetCount = 2;
7980
+ command.offsetB = this.passOffset;
7981
+ command.offsetC = material;
7982
+ command.offsetD = this.drawViewOffset;
7983
+ command.offsetCount = 4;
7645
7984
  for (let index = 0; index < mesh.vertexBuffers.length; index++) {
7646
7985
  command.vertexBuffers[index] = mesh.vertexBuffers[index];
7647
7986
  }
@@ -7655,7 +7994,12 @@ export class WebGPURenderer {
7655
7994
  if (pass === null)
7656
7995
  return;
7657
7996
  pass.setPipeline(pipeline);
7658
- 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
+ ]);
7659
8003
  for (let index = 0; index < mesh.vertexBuffers.length; index++) {
7660
8004
  pass.setVertexBuffer(index, mesh.vertexBuffers[index]);
7661
8005
  }
@@ -7665,6 +8009,10 @@ export class WebGPURenderer {
7665
8009
  if (splits) {
7666
8010
  this.keepSkinDiffuse(mesh, key, base, skinned, morphed, deltas, layer, eight, clothed, slot, material);
7667
8011
  }
8012
+ /* And its surface half, for the frame's materials' reflections. See `keepReflectionSurface`. */
8013
+ if (!blend && lit && depthWrite && this.reflectionSurfaceKeeps()) {
8014
+ this.keepReflectionSurface(mesh, key, base, skinned, morphed, deltas, layer, eight, clothed, modelKind, slot, material, null, 1, null);
8015
+ }
7668
8016
  }
7669
8017
  /* Back to the defaults for whatever is drawn next, exactly as `renderer.ts` restores them —
7670
8018
  unconditionally, because the scratch state above is shared by every draw after this one
@@ -7731,9 +8079,87 @@ export class WebGPURenderer {
7731
8079
  'an ordinary mesh, or drop the channel — the sway, sky and alpha lanes cannot reach an ' +
7732
8080
  'instanced pipeline.');
7733
8081
  }
7734
- 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);
7735
8090
  return batch;
7736
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
+ }
7737
8163
  /** The device instance culls, built on the first culling draw. See `InstanceCullPass`. */
7738
8164
  instanceCulls = null;
7739
8165
  /** Counts frames, so a batch knows when its cull slots were last taken. */
@@ -7766,7 +8192,10 @@ export class WebGPURenderer {
7766
8192
  this.submitInstanced(batch, data, 1, false, {});
7767
8193
  /* A batch that moved this frame pairs each slot with last frame's; the batch is the identity. */
7768
8194
  const previous = batch.previousBuffer;
7769
- 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()) {
7770
8199
  const paired = pairedInstances(batch.previousCount, Math.min(data.count, batch.capacity));
7771
8200
  if (paired > 0)
7772
8201
  this.motionPass?.recordInstanced(batch.mesh, batch.buffer, previous, paired);
@@ -7794,6 +8223,7 @@ export class WebGPURenderer {
7794
8223
  const glassy = resolveGlass(options.glass, this.glassScratch);
7795
8224
  if ((refraction <= 0 && !glassy) || !this.takeRefractSnapshot())
7796
8225
  return false;
8226
+ this.seeThroughBound = true;
7797
8227
  const glass = this.glassScratch;
7798
8228
  /* Strength, path thickness, transmission, frost: one vector, for the budget the shader states. */
7799
8229
  const seeAt = this.materialField('uSeeThrough');
@@ -7806,6 +8236,8 @@ export class WebGPURenderer {
7806
8236
  this.perFrameFloats[tintAt] = refractTint?.[0] ?? 1;
7807
8237
  this.perFrameFloats[tintAt + 1] = refractTint?.[1] ?? 1;
7808
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;
7809
8241
  const glassTintAt = this.materialField('uGlassTint');
7810
8242
  this.perFrameFloats[glassTintAt] = glass.tint[0];
7811
8243
  this.perFrameFloats[glassTintAt + 1] = glass.tint[1];
@@ -7814,6 +8246,7 @@ export class WebGPURenderer {
7814
8246
  }
7815
8247
  /** Put the scratch back after `bindSeeThrough`: refraction and glass off. */
7816
8248
  releaseSeeThrough() {
8249
+ this.seeThroughBound = false;
7817
8250
  const seeAt = this.materialField('uSeeThrough');
7818
8251
  this.perFrameFloats[seeAt] = 0;
7819
8252
  this.perFrameFloats[seeAt + 2] = 0;
@@ -7869,10 +8302,17 @@ export class WebGPURenderer {
7869
8302
  /* A culling batch's camera draw: the whole batch against the view and the occluders on the CPU,
7870
8303
  then instance by instance on the device into an indirect draw. Blended draws keep every
7871
8304
  instance and their order. See `InstancedOptions.cull`. */
7872
- const culling = !blend && batch.cull;
8305
+ const culling = !blend && batch.cull && this.staticRecording === null;
7873
8306
  if (culling && !batchBoxVisible(batch.box, this.frustum, this.occlusion))
7874
8307
  return;
7875
- 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;
7876
8316
  const instanceBuffer = cut === null ? batch.buffer : cut.instances;
7877
8317
  const slot = this.perDraw.allocate();
7878
8318
  if (slot === null) {
@@ -7884,11 +8324,19 @@ export class WebGPURenderer {
7884
8324
  }
7885
8325
  /* Late unless it moves with what it lies on. See `TranslucentMeshOptions.reconstructed`. */
7886
8326
  const lands = blend && options.reconstructed !== true && this.drawsLate();
7887
- 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;
7888
8331
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvScale.offset, this.uvScale);
7889
8332
  this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uUvOffset.offset, this.uvOffset);
7890
- this.perDraw.writeFloats(slot, FLAT_VERT_FIELDS.uLightViewProj.offset, this.lightViewProj);
7891
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
+ }
7892
8340
  const dimmed = opacity < 1;
7893
8341
  if (dimmed) {
7894
8342
  this.perFrameFloats[this.materialField('uOpacity')] = opacity;
@@ -7925,7 +8373,7 @@ export class WebGPURenderer {
7925
8373
  a third vertex buffer and a different vertex module, and sharing a cache entry with the
7926
8374
  plain one would hand a draw the wrong layout. */
7927
8375
  const adds = blend && options.additive === true;
7928
- const key = `${base}|inst${blend ? (adds ? '|add' : '|blend') : ''}` +
8376
+ const key = `${base}|inst${animation === null ? '' : '|anim'}${blend ? (adds ? '|add' : '|blend') : ''}` +
7929
8377
  `${depthWrite ? '' : '|nodepth'}${layer > 0 ? `|dl${layer}` : ''}` +
7930
8378
  `${this.materialDoubleSided ? '|2s' : ''}${coverage ? '|a2c' : ''}` +
7931
8379
  `${modelKind === null ? '' : `|m:${modelKind}`}`;
@@ -7936,7 +8384,7 @@ export class WebGPURenderer {
7936
8384
  if (present === undefined) {
7937
8385
  throw new Error(`WebGPU: no mesh attributes recorded for ${base}; createMesh records them`);
7938
8386
  }
7939
- 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);
7940
8388
  }
7941
8389
  const material = this.materialSlotForDraw();
7942
8390
  if (material !== null) {
@@ -7945,10 +8393,15 @@ export class WebGPURenderer {
7945
8393
  : this.recordDraw(0, this.currentTarget());
7946
8394
  if (command !== null) {
7947
8395
  command.pipeline = pipeline;
7948
- command.bindGroup = this.flatBindGroupFor(false, false, null);
8396
+ command.bindGroup =
8397
+ animation === null
8398
+ ? this.flatBindGroupFor(false, false, null)
8399
+ : this.animatedGroup(batch, animation);
7949
8400
  command.offsetA = slot;
7950
- command.offsetB = material;
7951
- command.offsetCount = 2;
8401
+ command.offsetB = this.passOffset;
8402
+ command.offsetC = material;
8403
+ command.offsetD = this.drawViewOffset;
8404
+ command.offsetCount = 4;
7952
8405
  let index = 0;
7953
8406
  for (; index < mesh.vertexBuffers.length; index++) {
7954
8407
  command.vertexBuffers[index] = mesh.vertexBuffers[index];
@@ -7966,7 +8419,9 @@ export class WebGPURenderer {
7966
8419
  if (pass === null)
7967
8420
  return;
7968
8421
  pass.setPipeline(pipeline);
7969
- 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]);
7970
8425
  let index = 0;
7971
8426
  for (; index < mesh.vertexBuffers.length; index++) {
7972
8427
  pass.setVertexBuffer(index, mesh.vertexBuffers[index]);
@@ -7978,6 +8433,10 @@ export class WebGPURenderer {
7978
8433
  else
7979
8434
  pass.drawIndexedIndirect(cut.args, 0);
7980
8435
  }
8436
+ /* And its surface half, for the frame's materials' reflections, with the same survivors. */
8437
+ if (!blend && lit && depthWrite && this.reflectionSurfaceKeeps()) {
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);
8439
+ }
7981
8440
  }
7982
8441
  /* Back to the defaults, unconditionally and for the reason `drawMesh` gives at length: this
7983
8442
  scratch block is cumulative state shared by every draw after this one, and an early return
@@ -8169,6 +8628,7 @@ export class WebGPURenderer {
8169
8628
  if (view === null)
8170
8629
  return false;
8171
8630
  this.lightViewProj.set(lightViewProj);
8631
+ this.reopenViews();
8172
8632
  mat4.multiply(this.correctedLightViewProj, SHADOW_CLIP_CORRECTION, lightViewProj);
8173
8633
  this.shadowLayerIsPeel = peel;
8174
8634
  this.shadowLayerIsDynamic = dynamic;
@@ -8276,6 +8736,241 @@ export class WebGPURenderer {
8276
8736
  this.sceneCasterMaterial = undefined;
8277
8737
  casters(this.sceneCasterSink);
8278
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;
8279
8974
  drawShadowCasters(casters) {
8280
8975
  if (this.surface.lost || this.shadowPass === null)
8281
8976
  return;
@@ -8567,7 +9262,7 @@ export class WebGPURenderer {
8567
9262
  }
8568
9263
  this.shadowDraws.flush();
8569
9264
  this.scatterDepthDraws.flush();
8570
- this.surface.device.queue.submit([this.shadowEncoder.finish()]);
9265
+ this.submit([this.shadowEncoder.finish()]);
8571
9266
  /* A growth's old texture, copied out of by what was just submitted and needed no longer. */
8572
9267
  this.retiredSunTexture?.destroy();
8573
9268
  this.retiredSunTexture = null;
@@ -8606,6 +9301,20 @@ export class WebGPURenderer {
8606
9301
  warnedNoPointShadowArray = false;
8607
9302
  /** Said once, for the retired ambient dial. */
8608
9303
  warnedEnvironmentAmbient = false;
9304
+ /** The overlay the draws wear, `uOverlay`: see `setSurfaceOverlay`. */
9305
+ overlayFloats = new Float32Array(OVERLAY_FLOATS);
9306
+ /** Its atlas, bound where the refraction copy goes in a twin of each flat group. */
9307
+ overlayMaps = null;
9308
+ /**
9309
+ * Each flat group's twin with the atlas in the refraction slot, by the group and the atlas, built
9310
+ * on the first draw that wears it and dropped wherever `flatBindGroups` is.
9311
+ */
9312
+ overlayGroups = new Map();
9313
+ /** True while a twin is built: what `flatTextures` hands the refraction slot then. */
9314
+ overlayBinding = false;
9315
+ /** True between `bindSeeThrough` and `releaseSeeThrough`: that draw reads the copy, not the atlas. */
9316
+ seeThroughBound = false;
9317
+ warnedOverlayRefraction = false;
8609
9318
  castingWorldIndices = new Int32Array(POINT_SHADOW_POOL).fill(-1);
8610
9319
  resolvedPointShadows = createResolvedPointShadows(MAX_POINT_LIGHTS);
8611
9320
  /** One face's view-projection, rebuilt per face rather than allocated. */
@@ -8787,7 +9496,7 @@ export class WebGPURenderer {
8787
9496
  * its own slot.
8788
9497
  */
8789
9498
  this.pointShadowArray?.flush();
8790
- this.surface.device.queue.submit([this.shadowEncoder.finish()]);
9499
+ this.submit([this.shadowEncoder.finish()]);
8791
9500
  this.shadowEncoder = null;
8792
9501
  this.shadowPass = null;
8793
9502
  this.gpuTimer.end();
@@ -8954,42 +9663,89 @@ export class WebGPURenderer {
8954
9663
  tintTarget.resolve(encoder, layer, face, offered);
8955
9664
  }
8956
9665
  buildFlatBindGroup() {
8957
- 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);
9667
+ }
9668
+ /**
9669
+ * The open material's flat group, or its twin with the surface overlay's atlas in the refraction
9670
+ * slot where the draw wears one and does not refract. A twin is built from the live material,
9671
+ * which `bindGroup` always is, and kept by the group and the atlas.
9672
+ *
9673
+ * **A draw that refracts, or is glass, keeps the frame's copy** and its overlay reads no image —
9674
+ * the shader asks the same of `uSeeThrough` (`overlayImagesBound`) — said once.
9675
+ */
9676
+ overlayGroup() {
9677
+ const maps = this.overlayMaps;
9678
+ if (maps === null)
9679
+ return this.bindGroup;
9680
+ if (this.seeThroughBound) {
9681
+ if (!this.warnedOverlayRefraction) {
9682
+ this.warnedOverlayRefraction = true;
9683
+ console.warn('WebGPU: a draw that refracts or is glass wears a surface overlay with maps. The frame ' +
9684
+ "copy it reads takes the overlay's slot, so its rim, dissolve and wrinkles read no image.");
9685
+ }
9686
+ return this.bindGroup;
9687
+ }
9688
+ let byMaps = this.overlayGroups.get(this.bindGroup);
9689
+ if (byMaps === undefined) {
9690
+ byMaps = new Map();
9691
+ this.overlayGroups.set(this.bindGroup, byMaps);
9692
+ }
9693
+ let twin = byMaps.get(maps);
9694
+ if (twin === undefined) {
9695
+ this.overlayBinding = true;
9696
+ twin = this.buildFlatBindGroup();
9697
+ this.overlayBinding = false;
9698
+ this.bindGroupBudget.ask();
9699
+ byMaps.set(maps, twin);
9700
+ }
9701
+ return twin;
8958
9702
  }
8959
9703
  /**
8960
9704
  * The bind group a draw uses, skinned or not.
8961
9705
  *
8962
- * **The skinned twin is cached against the two things that can invalidate it**, rather than
8963
- * against a flag somebody has to remember to clear at six call sites: the unskinned group it was
8964
- * built beside — which is rebuilt whenever the material or a shadow slot moves — and the palette
8965
- * view, which changes only when the texture is reallocated. Comparing object identity means a
8966
- * 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
8967
9711
  * `SkinPaletteTexture.view()` caches: a fresh view every call never compares equal and would
8968
9712
  * rebuild this every draw.
8969
9713
  */
8970
9714
  flatBindGroupFor(skinned, morphed, deltas) {
9715
+ const base = this.overlayGroup();
8971
9716
  if (!skinned && !morphed)
8972
- return this.bindGroup;
9717
+ return base;
8973
9718
  const palette = skinned ? this.skinPalettes.view(this.skinPaletteSlot) : null;
8974
9719
  /* Slot `k` caches at `k + 1`; index 0 is the morphed draw that is not skinned and has no
8975
9720
  slot of its own. See `skinnedGroups`. */
8976
9721
  const at = skinned ? this.skinPaletteSlot + 1 : 0;
8977
9722
  /* A skinned group carries the cloth's views, or the stand-in's: see `clothViews`. */
8978
9723
  const cloth = skinned ? this.clothViews() : null;
8979
- if ((this.skinnedGroups[at] ?? null) === null ||
8980
- this.skinnedGroupsBeside[at] !== this.bindGroup ||
8981
- this.skinnedGroupsPalette[at] !== palette ||
8982
- this.skinnedGroupsDeltas[at] !== deltas ||
8983
- this.skinnedGroupsCloth[at] !== (cloth?.particles ?? null) ||
8984
- this.skinnedGroupsClothBinding[at] !== (cloth?.binding ?? null)) {
8985
- this.skinnedGroups[at] = createFlatBindGroup(this.surface.device, this.flatLayoutFor(skinned, morphed), this.variant, this.perDraw.buffer, this.perFrame.buffer, this.flatTextures, palette, deltas, cloth);
8986
- this.skinnedGroupsBeside[at] = this.bindGroup;
8987
- this.skinnedGroupsPalette[at] = palette;
8988
- this.skinnedGroupsDeltas[at] = deltas;
8989
- this.skinnedGroupsCloth[at] = cloth?.particles ?? null;
8990
- this.skinnedGroupsClothBinding[at] = cloth?.binding ?? null;
8991
- }
8992
- return this.skinnedGroups[at];
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)) {
9735
+ /* Built from the live material, with the overlay's atlas where `base` is its twin. */
9736
+ this.overlayBinding = base !== this.bindGroup;
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);
9738
+ this.overlayBinding = false;
9739
+ twin = {
9740
+ group,
9741
+ palette,
9742
+ deltas,
9743
+ cloth: cloth?.particles ?? null,
9744
+ clothBinding: cloth?.binding ?? null,
9745
+ };
9746
+ byMaterial.set(base, twin);
9747
+ }
9748
+ return twin.group;
8993
9749
  }
8994
9750
  /** The layout matching a pair of vertex flags. A pipeline and its bind group must agree. */
8995
9751
  flatLayoutFor(skinned, morphed) {
@@ -9028,8 +9784,14 @@ export class WebGPURenderer {
9028
9784
  if (this.drawBudget.dropped > 0) {
9029
9785
  grew = this.perDraw.growTo(roomFor(this.drawBudget.used)) || grew;
9030
9786
  }
9031
- if (this.materialBudget.dropped > 0) {
9032
- 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;
9033
9795
  }
9034
9796
  if (grew)
9035
9797
  this.rebuildFlatGroupsForNewRings();
@@ -9050,8 +9812,10 @@ export class WebGPURenderer {
9050
9812
  */
9051
9813
  rebuildFlatGroupsForNewRings() {
9052
9814
  this.flatBindGroups.clear();
9053
- this.flatEnvironment = this.flatTextures('uEnvironment').view;
9815
+ this.flatGroupEpoch += 1;
9816
+ this.overlayGroups.clear();
9054
9817
  this.skinnedGroups.length = 0;
9818
+ this.flatEnvironment = this.flatTextures('uEnvironment').view;
9055
9819
  const albedo = this.albedo;
9056
9820
  const normalMap = this.normalMap;
9057
9821
  const ormMap = this.ormMap;
@@ -9071,6 +9835,7 @@ export class WebGPURenderer {
9071
9835
  this.bindGroup = this.flatGroupForMaps(this.albedo, this.normalMap, this.ormMap, this.emissiveMap, this.modelMap);
9072
9836
  /* The open material lives in a slot of a buffer that no longer exists. */
9073
9837
  this.materials.dirty();
9838
+ this.reopenPass();
9074
9839
  }
9075
9840
  /**
9076
9841
  * The environment view the cached flat groups bind: the grid, or the blank stand-in while the
@@ -9102,6 +9867,9 @@ export class WebGPURenderer {
9102
9867
  */
9103
9868
  rebuildFlatBindGroup() {
9104
9869
  this.flatBindGroups.clear();
9870
+ this.flatGroupEpoch += 1;
9871
+ this.overlayGroups.clear();
9872
+ this.skinnedGroups.length = 0;
9105
9873
  this.flatEnvironment = this.flatTextures('uEnvironment').view;
9106
9874
  if (this.albedo === null &&
9107
9875
  this.normalMap === null &&
@@ -9327,9 +10095,14 @@ export class WebGPURenderer {
9327
10095
  ? (OUTPUT_TRANSFORM_CODE[this.quality.outputTransform] ?? 0)
9328
10096
  : 0;
9329
10097
  f[at('uOutputExposure')] = graded ? this.exposure : 1;
10098
+ f.set(this.filmA, at('uFilmA'));
10099
+ f.set(this.filmB, at('uFilmB'));
10100
+ f[at('uDisplayHeadroom')] = this.headroom;
9330
10101
  f.set(this.reprojection, at('uReprojection'));
9331
10102
  f[at('uMotionStrength')] = motionStrength;
9332
- f[at('uMotionMax')] = MOTION_BLUR_MAX_UV;
10103
+ f[at('uMotionMax')] = this.motionBlurMax;
10104
+ /* Each mover's own motion, where the reconstruction drew its target this frame. */
10105
+ f[at('uObjectMotion')] = this.reconstructing && this.reconMotionView !== null ? 1 : 0;
9333
10106
  f[at('uAoStrength')] = this.frameProjection === null ? 0 : aoStrength;
9334
10107
  /*
9335
10108
  * The occlusion read where this frame's jitter put it (`uAoOffset`): measured from the jittered
@@ -9373,6 +10146,7 @@ export class WebGPURenderer {
9373
10146
  f[at('uGradeSize')] = this.gradeSize;
9374
10147
  /* The lens and the print. See `setVignette` and `setFilmGrain`. */
9375
10148
  f[at('uVignette')] = this.vignette;
10149
+ f.set(this.fringe, at('uFringe'));
9376
10150
  f[at('uAutoExposure')] = adapting ? this.autoExposure : 0;
9377
10151
  f[at('uLocalExposure')] = measuring && localising ? this.localExposure : 0;
9378
10152
  f[at('uGrain')] = this.grain;
@@ -9410,7 +10184,7 @@ export class WebGPURenderer {
9410
10184
  return;
9411
10185
  if (this.aoTargetView === null)
9412
10186
  return;
9413
- this.rushBindGroup = this.buildRushBindGroup('post.rushBindGroup', this.sceneColorView);
10187
+ this.rushBindGroup = this.buildRushBindGroup('post.rushBindGroup', this.sceneColorView, this.sceneColorView);
9414
10188
  /*
9415
10189
  * **And the reconstruction's twin, from the same three call sites.** A colour grade replaces
9416
10190
  * the lookup table and a resize replaces the depth and the occlusion target, and both groups
@@ -9418,11 +10192,13 @@ export class WebGPURenderer {
9418
10192
  * come to read a stale view on exactly the frames a consumer changed something.
9419
10193
  */
9420
10194
  if (this.reconShownView !== null) {
9421
- this.reconRushGroup = this.buildRushBindGroup('recon.rushBindGroup', this.reconShownView);
10195
+ this.reconRushGroup = this.buildRushBindGroup('recon.rushBindGroup', this.reconShownView, this.reconMotionView ?? this.reconShownView);
9422
10196
  }
9423
10197
  }
9424
10198
  /** The composite's group over one scene source. See `rebuildRushBindGroup`. */
9425
- buildRushBindGroup(label, sceneView) {
10199
+ buildRushBindGroup(label, sceneView,
10200
+ /** Each mover's own motion, or a stand-in the shader is told not to read. */
10201
+ motionView) {
9426
10202
  const { device } = this.surface;
9427
10203
  if (this.resolvedDepthView === null || this.aoTargetView === null)
9428
10204
  return null;
@@ -9451,6 +10227,7 @@ export class WebGPURenderer {
9451
10227
  and a declared binding still needs a complete texture, as the bloom fallback says. */
9452
10228
  ...entry(RUSH_TEXTURES.uExposureHeld, this.exposurePass?.heldView ?? sceneView),
9453
10229
  ...entry(RUSH_TEXTURES.uExposureLocal, this.exposurePass?.localView ?? sceneView),
10230
+ ...entry(RUSH_TEXTURES.uMotion, motionView),
9454
10231
  ],
9455
10232
  layout: this.rushLayout,
9456
10233
  });
@@ -9707,7 +10484,7 @@ export class WebGPURenderer {
9707
10484
  this.reconSharpenGroups[write] = build(`recon.sharpen${String(write)}`, target, this.reconShownView, records, nextRecords, swings, nextSwings);
9708
10485
  }
9709
10486
  /* The composite's own group, over the resolved picture instead of the scene target. */
9710
- this.reconRushGroup = this.buildRushBindGroup('recon.rushBindGroup', this.reconShownView);
10487
+ this.reconRushGroup = this.buildRushBindGroup('recon.rushBindGroup', this.reconShownView, this.reconMotionView ?? this.reconShownView);
9711
10488
  /* And the scene's, because `ensureComposite` built it before this one existed. */
9712
10489
  this.rebuildRushBindGroup();
9713
10490
  /* And the bloom's, which blooms the resolved picture: see `rebuildBloomGroups`. */
@@ -9796,7 +10573,7 @@ export class WebGPURenderer {
9796
10573
  if (!this.reconstructing || late === null || shown === null || depth === null)
9797
10574
  return;
9798
10575
  const oitSet = this.oitActive && this.translucentQueue.length > 0;
9799
- if (late.pending === 0 && !oitSet)
10576
+ if (late.pending === 0 && !oitSet && this.latePasses.length === 0)
9800
10577
  return;
9801
10578
  const width = Math.max(1, this.surface.canvas.width);
9802
10579
  const height = Math.max(1, this.surface.canvas.height);
@@ -9808,21 +10585,60 @@ export class WebGPURenderer {
9808
10585
  this.refractSnapshotTaken = true;
9809
10586
  }
9810
10587
  this.lateRefractWanted = false;
9811
- if (late.pending === 0)
9812
- return;
9813
- const pass = encoder.beginRenderPass({
9814
- label: 'recon.late',
9815
- timestampWrites: this.gpuTimer.writesFor(),
9816
- colorAttachments: [{ view: shown, loadOp: 'load', storeOp: 'store' }],
9817
- depthStencilAttachment: {
9818
- view: late.depthView(width, height),
9819
- depthLoadOp: 'load',
9820
- /* Kept: the order-independent set replays against it after this pass. */
9821
- depthStoreOp: 'store',
9822
- },
9823
- });
9824
- late.replay(pass, this.issueBound);
9825
- pass.end();
10588
+ if (late.pending > 0) {
10589
+ const pass = encoder.beginRenderPass({
10590
+ label: 'recon.late',
10591
+ timestampWrites: this.gpuTimer.writesFor(),
10592
+ colorAttachments: [{ view: shown, loadOp: 'load', storeOp: 'store' }],
10593
+ depthStencilAttachment: {
10594
+ view: late.depthView(width, height),
10595
+ depthLoadOp: 'load',
10596
+ /* Kept: the order-independent set replays against it after this pass. */
10597
+ depthStoreOp: 'store',
10598
+ },
10599
+ });
10600
+ late.replay(pass, this.issueBound);
10601
+ pass.end();
10602
+ }
10603
+ this.runLatePasses(encoder, shown, late.depthView(width, height));
10604
+ }
10605
+ /**
10606
+ * The registered passes `drawPass` queued for after the upscale: each handed a fresh copy of the
10607
+ * reconstructed picture with every late draw in it, in the output-size texture the late
10608
+ * refraction keeps (its own reads are done by now: they were recorded before this copy), then
10609
+ * drawn over the picture in a pass of its own.
10610
+ */
10611
+ runLatePasses(encoder, shown, depth) {
10612
+ const copy = this.refractSnapshot;
10613
+ const source = this.reconShown;
10614
+ for (const handle of this.latePasses) {
10615
+ const definition = passAt(this.passes, handle);
10616
+ if (definition === undefined)
10617
+ continue;
10618
+ if (copy !== null && source !== null) {
10619
+ encoder.copyTextureToTexture({ texture: source }, { texture: copy }, [
10620
+ copy.width,
10621
+ copy.height,
10622
+ ]);
10623
+ }
10624
+ const pass = encoder.beginRenderPass({
10625
+ label: 'recon.latePass',
10626
+ timestampWrites: this.gpuTimer.writesFor(),
10627
+ colorAttachments: [{ view: shown, loadOp: 'load', storeOp: 'store' }],
10628
+ depthStencilAttachment: { view: depth, depthLoadOp: 'load', depthStoreOp: 'store' },
10629
+ });
10630
+ const context = this.passContext;
10631
+ context.pass = pass;
10632
+ context.outputTransform = this.gradeCode();
10633
+ context.outputExposure = this.gradeExposure();
10634
+ context.jitter = this.latePassJitter;
10635
+ context.format = RECON_HISTORY_FORMAT;
10636
+ context.depthFormat = DEPTH_FORMAT;
10637
+ context.samples = 1;
10638
+ definition.draw(context);
10639
+ context.jitter = this.passJitter;
10640
+ pass.end();
10641
+ }
9826
10642
  }
9827
10643
  /** Whether this draw lands in the reconstructed picture. See `lateRouting.ts`. */
9828
10644
  tracksMotionNow() {
@@ -10189,7 +11005,10 @@ export class WebGPURenderer {
10189
11005
  * other is the disagreement the parity rule exists to prevent. Said once rather than dropped.
10190
11006
  */
10191
11007
  runReflections(encoder) {
10192
- if (this.reflectionQueue.length === 0)
11008
+ const frameReflections = this.frameReflections;
11009
+ const surfaceHalf = this.reflectionSurface;
11010
+ const materials = frameReflections !== null && surfaceHalf !== null;
11011
+ if (this.reflectionQueue.length === 0 && !materials)
10193
11012
  return;
10194
11013
  const scene = this.sceneColorView;
10195
11014
  const reflection = this.ssrReflectionView;
@@ -10234,9 +11053,63 @@ export class WebGPURenderer {
10234
11053
  f[base + at('uSsrThickness')] = surface.thicknessM;
10235
11054
  f[base + at('uSsrSteps')] = surface.steps;
10236
11055
  f[base + at('uSsrEdgeFade')] = REFLECTION_EDGE_FADE;
11056
+ f[base + at('uSsrMaterial')] = 0;
10237
11057
  slot++;
10238
11058
  });
11059
+ const regions = slot;
11060
+ /* The frame's materials' trace takes the slot after the surfaces': the box, its axis and its
11061
+ strength unread, the march and the ceiling its own. See `frameReflections.ts`. */
11062
+ if (materials) {
11063
+ const base = (slot * SSR_SLOT) / 4;
11064
+ f.set(this.ssrDepthToWorld, base + at('uSsrDepthToWorld'));
11065
+ f.set(this.ssrViewProj, base + at('uSsrViewProj'));
11066
+ f.set(this.frameEye, base + at('uSsrEye'));
11067
+ f.fill(0, base + at('uSsrAxis'), base + at('uSsrAxis') + 3);
11068
+ f.fill(1, base + at('uSsrTint'), base + at('uSsrTint') + 3);
11069
+ f[base + at('uSsrStrength')] = 1;
11070
+ f[base + at('uSsrFresnel')] = 0;
11071
+ f[base + at('uSsrFacingCos')] = -1;
11072
+ f[base + at('uSsrReach')] = frameReflections.reachM;
11073
+ f[base + at('uSsrThickness')] = frameReflections.thicknessM;
11074
+ f[base + at('uSsrSteps')] = frameReflections.steps;
11075
+ f[base + at('uSsrEdgeFade')] = REFLECTION_EDGE_FADE;
11076
+ f[base + at('uSsrMaterial')] = 1;
11077
+ f[base + at('uSsrMaxRoughness')] = frameReflections.maxRoughness;
11078
+ slot++;
11079
+ }
10239
11080
  device.queue.writeBuffer(this.ssrUniforms, 0, this.ssrStaging, 0, slot * SSR_SLOT);
11081
+ const tracePipeline = postPipeline(this.pipelines, device, this.ssrLayout, 'post.ssrTrace', SSR_TRACE_FRAG_WGSL, this.pipelines.format, PREMULTIPLIED_OVER_BLEND);
11082
+ if (materials && this.depthView !== null && this.ssrMaterialGroup !== null) {
11083
+ /* The frame's opaque lit draws again, into what the trace and the resolve read. */
11084
+ surfaceHalf.draw(encoder, this.depthView, this.issueBound);
11085
+ const trace = encoder.beginRenderPass({
11086
+ label: 'post.ssr.materialTrace',
11087
+ colorAttachments: [
11088
+ { view: reflection, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
11089
+ ],
11090
+ });
11091
+ trace.setPipeline(tracePipeline);
11092
+ trace.setBindGroup(0, group, [regions * SSR_SLOT]);
11093
+ trace.draw(3);
11094
+ trace.end();
11095
+ /* The blur's texel and its radius at roughness 1, as a share of the frame's height. */
11096
+ const m = this.ssrMaterialFloats;
11097
+ const blurAt = this.postField(SSR_MATERIAL_FIELDS, 'uSsrBlur');
11098
+ m[blurAt] = 1 / Math.max(width, 1);
11099
+ m[blurAt + 1] = 1 / Math.max(height, 1);
11100
+ m[blurAt + 2] = frameReflections.blur * height;
11101
+ device.queue.writeBuffer(this.ssrMaterialUniforms, 0, m);
11102
+ const resolve = encoder.beginRenderPass({
11103
+ label: 'post.ssr.materialResolve',
11104
+ colorAttachments: [{ view: scene, loadOp: 'load', storeOp: 'store' }],
11105
+ });
11106
+ resolve.setPipeline(postPipeline(this.pipelines, device, this.ssrMaterialLayout, 'post.ssrMaterialResolve', SSR_MATERIAL_RESOLVE_FRAG_WGSL, this.pipelines.format, FRAME_REFLECTION_BLEND));
11107
+ resolve.setBindGroup(0, this.ssrMaterialGroup, [0]);
11108
+ resolve.draw(3);
11109
+ resolve.end();
11110
+ }
11111
+ if (regions === 0)
11112
+ return;
10240
11113
  /* Cleared whole and then scissored per surface: the resolve reads every pixel, and one left
10241
11114
  from the last frame would composite that reflection over this frame's scene. */
10242
11115
  const trace = encoder.beginRenderPass({
@@ -10245,7 +11118,7 @@ export class WebGPURenderer {
10245
11118
  { view: reflection, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
10246
11119
  ],
10247
11120
  });
10248
- trace.setPipeline(postPipeline(this.pipelines, device, this.ssrLayout, 'post.ssrTrace', SSR_TRACE_FRAG_WGSL, this.pipelines.format, PREMULTIPLIED_OVER_BLEND));
11121
+ trace.setPipeline(tracePipeline);
10249
11122
  slot = 0;
10250
11123
  this.reflectionQueue.replay((surface) => {
10251
11124
  const index = slot++;
@@ -10877,6 +11750,59 @@ export class WebGPURenderer {
10877
11750
  !this.overlayActive &&
10878
11751
  this.oitMode === 'none');
10879
11752
  }
11753
+ /** Whether a draw now is the frame's own, and so has a surface half to keep. */
11754
+ reflectionSurfaceKeeps() {
11755
+ return (this.reflectionSurface !== null &&
11756
+ !this.reflectionPassActive &&
11757
+ !this.probePassActive &&
11758
+ !this.capturePassActive &&
11759
+ !this.framePresented &&
11760
+ !this.overlayActive &&
11761
+ this.oitMode === 'none');
11762
+ }
11763
+ /**
11764
+ * An opaque lit draw's surface half, kept for the frame's materials' reflections: the draw again,
11765
+ * with the same slots, buffers and bind group, through the pipeline with `REFLECTION_SURFACE` on —
11766
+ * which writes what the pixel reflects of its environment into the two targets
11767
+ * `reflectionSurfacePass.ts` holds and tests for the depth its frame draw wrote. Replayed when the
11768
+ * frame's pass has ended, before the trace reads them. See `shaders/flat/reflectionSurface.ts`.
11769
+ */
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) {
11773
+ const surface = this.reflectionSurface;
11774
+ const present = this.meshPresent.get(base);
11775
+ if (surface === null || present === undefined)
11776
+ return;
11777
+ const animation = animated === null ? null : this.animationViews(animated);
11778
+ const key = `${frameKey}|rs`;
11779
+ const pipeline = this.pipelines.peek(key) ??
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);
11783
+ command.pipeline = pipeline;
11784
+ command.bindGroup =
11785
+ animated === null || animation === null
11786
+ ? this.flatBindGroupFor(skinned, morphed, deltas)
11787
+ : this.animatedGroup(animated, animation);
11788
+ command.offsetA = slot;
11789
+ command.offsetB = this.passOffset;
11790
+ command.offsetC = material;
11791
+ command.offsetD = this.drawViewOffset;
11792
+ command.offsetCount = 4;
11793
+ let index = 0;
11794
+ for (; index < mesh.vertexBuffers.length; index++) {
11795
+ command.vertexBuffers[index] = mesh.vertexBuffers[index];
11796
+ }
11797
+ if (instanceBuffer !== null)
11798
+ command.vertexBuffers[index++] = instanceBuffer;
11799
+ command.vertexCount = index;
11800
+ command.indexBuffer = mesh.indexBuffer;
11801
+ command.indexed = true;
11802
+ command.count = mesh.indexCount;
11803
+ command.instances = instances;
11804
+ command.indirect = indirect;
11805
+ }
10880
11806
  /**
10881
11807
  * A skin draw's diffuse and colour halves, kept for the spread: the frame's draw again, twice,
10882
11808
  * with the same slots, buffers and bind group, through the pipelines that write the diffuse's
@@ -10884,6 +11810,11 @@ export class WebGPURenderer {
10884
11810
  * wrote.
10885
11811
  */
10886
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
+ }
10887
11818
  const skin = this.skinScatter;
10888
11819
  const pipelines = this.skinPipelines;
10889
11820
  const present = this.meshPresent.get(base);
@@ -10899,8 +11830,10 @@ export class WebGPURenderer {
10899
11830
  command.pipeline = pipeline;
10900
11831
  command.bindGroup = bindGroup;
10901
11832
  command.offsetA = slot;
10902
- command.offsetB = material;
10903
- command.offsetCount = 2;
11833
+ command.offsetB = this.passOffset;
11834
+ command.offsetC = material;
11835
+ command.offsetD = this.drawViewOffset;
11836
+ command.offsetCount = 4;
10904
11837
  for (let index = 0; index < mesh.vertexBuffers.length; index++) {
10905
11838
  command.vertexBuffers[index] = mesh.vertexBuffers[index];
10906
11839
  }
@@ -10924,13 +11857,19 @@ export class WebGPURenderer {
10924
11857
  const skin = this.skinScatter;
10925
11858
  if (skin === null || this.skinScattered)
10926
11859
  return;
11860
+ /*
11861
+ * **Not the frame's picture, so not the frame's moment**: a blended draw in a mirror, a probe
11862
+ * bake or a scene capture lands over none of the frame's skin. Latching there switched the
11863
+ * spread off for the frame, so a capture with a glowing sign in it left every face drawn after
11864
+ * it flat. `scripts/capture-check.mjs`.
11865
+ */
11866
+ if (this.reflectionPassActive || this.probePassActive || this.capturePassActive)
11867
+ return;
10927
11868
  /* Latched whether or not skin was kept: a skin drawn after this point is the whole surface. */
10928
11869
  this.skinScattered = true;
10929
11870
  if (!skin.pending)
10930
11871
  return;
10931
- if (this.reflectionPassActive || this.probePassActive || this.oitReplaying)
10932
- return;
10933
- if (this.capturePassActive)
11872
+ if (this.oitReplaying)
10934
11873
  return;
10935
11874
  const encoder = this.encoder;
10936
11875
  const depth = this.depthView;
@@ -11045,6 +11984,66 @@ export class WebGPURenderer {
11045
11984
  this.volumeDepthTaken = true;
11046
11985
  return true;
11047
11986
  }
11987
+ /** The colour copy a registered pass reads, at the scene's size and format. See `passColour`. */
11988
+ allocatePassColour() {
11989
+ const source = this.sceneColor;
11990
+ if (source === null || this.passColour !== null)
11991
+ return;
11992
+ this.passColour = this.surface.device.createTexture({
11993
+ label: 'pass.sceneColor',
11994
+ size: [source.width, source.height],
11995
+ format: this.pipelines.format,
11996
+ usage: 0x4 | 0x2, // TEXTURE_BINDING | COPY_DST
11997
+ });
11998
+ this.passColourView = this.passColour.createView();
11999
+ }
12000
+ /**
12001
+ * The frame's colour so far, into `passColour`, for a registered pass that reads it: the
12002
+ * boundary `takeRefractSnapshot` describes — flush, end the pass, copy what its end resolved,
12003
+ * reopen with `load` — and refused where that one is, in a mirror, a probe bake or a capture,
12004
+ * whose pictures are not the frame's. **Not latched**, unlike refraction's: each reading pass
12005
+ * sees the passes drawn before it, at a full-frame copy each.
12006
+ */
12007
+ takePassColour() {
12008
+ if (this.reflectionPassActive || this.probePassActive || this.capturePassActive)
12009
+ return;
12010
+ const copy = this.passColour;
12011
+ const source = this.sceneColor;
12012
+ const encoder = this.encoder;
12013
+ if (copy === null || source === null || encoder === null || this.pass === null)
12014
+ return;
12015
+ if (this.quality.frameGraph)
12016
+ this.flushGraph();
12017
+ this.pass.end();
12018
+ encoder.copyTextureToTexture({ texture: source }, { texture: copy }, [copy.width, copy.height]);
12019
+ this.pass = this.reopenFramePass('pass.colourTaken');
12020
+ }
12021
+ /**
12022
+ * The frame's pass again after a copy between passes, loading everything already drawn: the same
12023
+ * attachments the copy's caller ended, with nothing cleared.
12024
+ */
12025
+ reopenFramePass(label) {
12026
+ const encoder = this.encoder;
12027
+ const target = this.compositeTarget();
12028
+ if (encoder === null || target === null)
12029
+ return null;
12030
+ const multisampled = this.compositeMsaa() !== null;
12031
+ return encoder.beginRenderPass({
12032
+ label,
12033
+ timestampWrites: this.gpuTimer.writesFor(),
12034
+ colorAttachments: [
12035
+ {
12036
+ view: this.compositeMsaa() ?? target,
12037
+ resolveTarget: multisampled ? target : undefined,
12038
+ loadOp: 'load',
12039
+ storeOp: resolvedStoreOp(multisampled, false, this.quality.discardResolvedAttachments),
12040
+ },
12041
+ ],
12042
+ depthStencilAttachment: this.depthView === null
12043
+ ? undefined
12044
+ : { view: this.depthView, depthLoadOp: 'load', depthStoreOp: 'store' },
12045
+ });
12046
+ }
11048
12047
  /**
11049
12048
  * The pass to record into, reopening one on the canvas if the frame has already gone.
11050
12049
  *
@@ -11226,6 +12225,8 @@ export class WebGPURenderer {
11226
12225
  /* And a march reads the same copy, for the same reason and with the same failure if it is
11227
12226
  discarded: a depth of zero is the far plane reversed, so every ray finds nothing. */
11228
12227
  this.reflectionQueue.length > 0 ||
12228
+ /* And the frame's materials' reflections, whose surface half tests against the depth too. */
12229
+ this.reflectionSurface !== null ||
11229
12230
  /* And the medium's own march, which stops where the frame's depth says a surface is. Left
11230
12231
  out of this, every ray runs to `maxDistance` through the walls and the fog is drawn on
11231
12232
  top of the room rather than in it. */
@@ -11259,6 +12260,9 @@ export class WebGPURenderer {
11259
12260
  * site reads as one `if` either way.
11260
12261
  */
11261
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);
11262
12266
  /* The order-independent replay owns its pass and issues directly into it; a recorded command
11263
12267
  would be replayed into the frame's passes instead, which is not where these belong. */
11264
12268
  if (this.oitEncoder !== null)
@@ -11306,10 +12310,26 @@ export class WebGPURenderer {
11306
12310
  }
11307
12311
  /** One recorded draw, issued into an open pass. Shared by the graph's replay and `runLate`. */
11308
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
+ }
11309
12318
  if (command.pipeline === null || command.bindGroup === null)
11310
12319
  return;
11311
12320
  pass.setPipeline(command.pipeline);
11312
- 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) {
11313
12333
  pass.setBindGroup(0, command.bindGroup, [command.offsetA, command.offsetB]);
11314
12334
  }
11315
12335
  else if (command.offsetCount === 1) {
@@ -11575,7 +12595,7 @@ export class WebGPURenderer {
11575
12595
  this.pass?.end();
11576
12596
  this.pass = null;
11577
12597
  if (!this.surface.lost)
11578
- this.surface.device.queue.submit([this.encoder.finish()]);
12598
+ this.submit([this.encoder.finish()]);
11579
12599
  this.encoder = null;
11580
12600
  }
11581
12601
  endFrame() {
@@ -11639,7 +12659,7 @@ export class WebGPURenderer {
11639
12659
  * there would have nothing to ride on. See `GpuTimestamps.resolve`.
11640
12660
  */
11641
12661
  this.gpuTimer.resolve(this.encoder);
11642
- this.surface.device.queue.submit([this.encoder.finish()]);
12662
+ this.submit([this.encoder.finish()]);
11643
12663
  this.encoder = null;
11644
12664
  this.framePresented = true;
11645
12665
  /* Latched rather than assigned: `framePresented` goes back to false every `beginFrame`. */
@@ -12042,6 +13062,9 @@ export class WebGPURenderer {
12042
13062
  const reads = maskOf(...(definition.reads ?? []));
12043
13063
  this.passReads.set(handle, reads);
12044
13064
  this.passReadsUnion |= reads;
13065
+ /* The colour copy a reader needs, now rather than at its first draw: see `passColour`. */
13066
+ if ((reads & COLOR_SNAPSHOT) !== 0 && this.passColour === null)
13067
+ this.allocatePassColour();
12045
13068
  if (definition.prepare !== undefined)
12046
13069
  this.preparingPasses++;
12047
13070
  definition.init?.(this.passDevice());
@@ -12061,6 +13084,8 @@ export class WebGPURenderer {
12061
13084
  environment: null,
12062
13085
  distanceField: null,
12063
13086
  jitter: this.passJitter,
13087
+ sceneDepth: null,
13088
+ sceneColor: null,
12064
13089
  };
12065
13090
  /**
12066
13091
  * The object handed to a pass as `PrepareContext.environment`, rebuilt only when it changes.
@@ -12146,6 +13171,15 @@ export class WebGPURenderer {
12146
13171
  * than merely what happens.
12147
13172
  */
12148
13173
  this.prepareContext.distanceField = this.fieldComposer?.field ?? null;
13174
+ /* Settled for the frame: `ensureComposite` has run, so the view stays until the pass draws. */
13175
+ this.prepareContext.sceneDepth = this.resolvedDepthView;
13176
+ /*
13177
+ * Under a reconstruction a colour reader draws after the upscale (see `drawPass`), so it reads
13178
+ * the output-size copy the late refraction keeps rather than the render's.
13179
+ */
13180
+ this.prepareContext.sceneColor = this.reconstructionWanted
13181
+ ? this.refractSnapshotView
13182
+ : this.passColourView;
12149
13183
  for (const definition of this.passes.definitions) {
12150
13184
  definition?.prepare?.(this.prepareContext);
12151
13185
  }
@@ -12163,6 +13197,35 @@ export class WebGPURenderer {
12163
13197
  const definition = passAt(this.passes, handle);
12164
13198
  if (definition === undefined)
12165
13199
  return;
13200
+ const reads = this.passReads.get(handle) ?? 0;
13201
+ /*
13202
+ * **Under a reconstruction, a pass that reads the frame's colour draws after the blended draws
13203
+ * do.** Those land after the upscale, at the output's size, so a pass drawn among them at the
13204
+ * render's would read a frame without them and then be covered by them — a full-screen effect
13205
+ * with the hit sparks drawn sharp over it. Queued instead, and drawn by `runLate` over the
13206
+ * reconstructed picture, after every late draw, whatever order it was asked in.
13207
+ */
13208
+ if ((reads & COLOR_SNAPSHOT) !== 0 && this.drawsLate()) {
13209
+ this.latePasses.push(handle);
13210
+ return;
13211
+ }
13212
+ /*
13213
+ * A pass that reads the depth copy gets it with everything drawn so far: the copy is taken
13214
+ * here, once a frame, as a soft-edged sprite takes it. Before `openPass` below, because taking
13215
+ * it ends the frame's pass and opens another.
13216
+ */
13217
+ if ((reads & DEPTH_SNAPSHOT) !== 0 && !this.framePresented && this.openPass() !== null) {
13218
+ this.takeVolumeDepth();
13219
+ }
13220
+ /*
13221
+ * And one that reads the colour gets it afresh, with everything drawn before it — a full-screen
13222
+ * pass drawn last reads the whole frame and an effect drawn mid-frame reads the frame so far.
13223
+ * A copy a reading pass, where the depth above is one a frame: what a colour reader draws is
13224
+ * itself part of what the next one should see.
13225
+ */
13226
+ if ((reads & COLOR_SNAPSHOT) !== 0 && !this.framePresented && this.openPass() !== null) {
13227
+ this.takePassColour();
13228
+ }
12166
13229
  if (!this.quality.frameGraph) {
12167
13230
  const pass = this.openPass();
12168
13231
  if (pass === null)
@@ -12170,7 +13233,7 @@ export class WebGPURenderer {
12170
13233
  definition.draw(this.fillPassContext(pass));
12171
13234
  return;
12172
13235
  }
12173
- recordNode(this.arena, VERB_PASS, this.passReads.get(handle) ?? 0, this.currentTarget(), handle, 0);
13236
+ recordNode(this.arena, VERB_PASS, reads, this.currentTarget(), handle, 0);
12174
13237
  if (this.framePresented)
12175
13238
  this.queueOverlayFlush();
12176
13239
  }
@@ -12257,7 +13320,7 @@ export class WebGPURenderer {
12257
13320
  definition.dispatch(this.computeContext);
12258
13321
  pass.end();
12259
13322
  this.flushRings();
12260
- device.queue.submit([encoder.finish()]);
13323
+ this.submit([encoder.finish()]);
12261
13324
  }
12262
13325
  /** Let go of a definition. A handle kept past this dispatches nothing; see `ComputeHandle`. */
12263
13326
  unregisterCompute(handle) {
@@ -12279,8 +13342,8 @@ export class WebGPURenderer {
12279
13342
  bindClusters(camera, env, f, i, at) {
12280
13343
  const binner = this.clusterBinner;
12281
13344
  i[at('uClustered')] = binner === null ? 0 : 1;
12282
- if (binner === null)
12283
- return;
13345
+ /* Written whether or not the lights are clustered: a surface overlay's rim finds its place on
13346
+ the screen through the same two (\`overlayScreen\`). */
12284
13347
  const p = camera.projection;
12285
13348
  const tanHalfFovY = 1 / (p[5] ?? 1);
12286
13349
  const aspect = (p[5] ?? 1) / (p[0] ?? 1);
@@ -12296,6 +13359,8 @@ export class WebGPURenderer {
12296
13359
  f[frustumAt + 2] = tanHalfFovY;
12297
13360
  f[frustumAt + 3] = aspect;
12298
13361
  f.set(camera.view, at('uView'));
13362
+ if (binner === null)
13363
+ return;
12299
13364
  /* The rectangles past the fixed arm's go into the table. See `ClusterLightSet.areas`. */
12300
13365
  const set = fillClusterLightSet(env, this.clusterLights, this.shadedAreaLights);
12301
13366
  /* A slot is read only under `POINT_SHADOWS`; see `lightHasFixture`. */
@@ -12596,6 +13661,43 @@ export class WebGPURenderer {
12596
13661
  }
12597
13662
  /** This frame's exposure into the tone curve. Starts at the profile's own. */
12598
13663
  exposure;
13664
+ /**
13665
+ * The `filmic` curve every later frame is graded through, until it is set again. See
13666
+ * `RendererApi.setFilmicCurve`; the constants are solved here once, not per frame or per pixel.
13667
+ */
13668
+ setFilmicCurve(curve) {
13669
+ this.filmCurve = curve;
13670
+ if (resolveFilmicCurve(curve, this.filmConstants, this.headroom) && !this.warnedFilmClamped) {
13671
+ this.warnedFilmClamped = true;
13672
+ console.warn(FILMIC_CLAMPED);
13673
+ }
13674
+ }
13675
+ warnedFilmClamped = false;
13676
+ /** The curve last set, so a new headroom can solve it again. */
13677
+ filmCurve = DEFAULT_FILMIC_CURVE;
13678
+ /** The display's peak over paper white; 1 on a standard range. See `setDisplayLuminance`. */
13679
+ headroom = 1;
13680
+ /** The range the frame goes out in. See `RendererApi.displayRange`. */
13681
+ get displayRange() {
13682
+ return this.surface.displayRange;
13683
+ }
13684
+ /** Why it is that range, in words. */
13685
+ get displayRangeReason() {
13686
+ return this.surface.displayRangeReason;
13687
+ }
13688
+ /**
13689
+ * How bright paper white and the display's peak are, in any one unit (nits, usually), held until
13690
+ * changed: their ratio is how far above white the highlights may run on a high range display,
13691
+ * and nothing changes on a standard one. See `RendererApi.setDisplayLuminance`.
13692
+ */
13693
+ setDisplayLuminance(paperWhite, peak) {
13694
+ this.headroom = displayHeadroom(this.surface.displayRange, paperWhite, peak);
13695
+ resolveFilmicCurve(this.filmCurve, this.filmConstants, this.headroom);
13696
+ }
13697
+ /** The curve's eight constants, and the two views the composite uploads them through. */
13698
+ filmConstants = new Float32Array(FILMIC_CONSTANTS);
13699
+ filmA = this.filmConstants.subarray(0, 4);
13700
+ filmB = this.filmConstants.subarray(4, 8);
12599
13701
  /*
12600
13702
  * -----------------------------------------------------------------------------------------
12601
13703
  * The three screen effects, which this backend cannot yet honour — and says so.
@@ -12639,6 +13741,8 @@ export class WebGPURenderer {
12639
13741
  veilColor = [0, 0, 0];
12640
13742
  veilAlpha = 0;
12641
13743
  vignette = 0;
13744
+ /** The lens's colour fringe: red's and green's pull and where it begins. See `fringe.ts`. */
13745
+ fringe = new Float32Array(FRINGE_FLOATS);
12642
13746
  grain = 0;
12643
13747
  grainSeed = 0;
12644
13748
  warnedLookWithoutComposite = false;
@@ -12660,6 +13764,20 @@ export class WebGPURenderer {
12660
13764
  }
12661
13765
  this.vignette = clampVignette(strength);
12662
13766
  }
13767
+ /**
13768
+ * The lens's colour fringe, held until changed: `intensity` is a percentage, 0 is none, and red
13769
+ * and green are pulled toward the centre by their wavelength's distance from blue, growing from
13770
+ * `start` — a share of the half-frame, 0 the centre — to the whole of it at the edge. Applied to
13771
+ * scene light at the composite's first read. Needs `screenEffects`, and says so once rather than
13772
+ * doing nothing. See `shaders/fringe.ts`.
13773
+ */
13774
+ setChromaticAberration(intensity, start = 0) {
13775
+ if (!this.hasComposite) {
13776
+ this.warnLookWithoutComposite(intensity);
13777
+ return;
13778
+ }
13779
+ resolveFringe(intensity, start, this.fringe);
13780
+ }
12663
13781
  /**
12664
13782
  * Film grain, held until changed: an amplitude in display values (0 is none, 0.03 is a fine
12665
13783
  * grain) and this frame's seed. **The seed is the caller's**, because the engine takes time from
@@ -12793,9 +13911,26 @@ export class WebGPURenderer {
12793
13911
  setSpeedRush(strength) {
12794
13912
  this.rushStrength = Math.min(Math.max(strength, 0), 1);
12795
13913
  }
12796
- /** How much of the frame's camera motion blur to apply, 0 to 1. */
12797
- setCameraMotionBlur(scale) {
13914
+ /**
13915
+ * How much of the frame's motion blur to apply, 0 to 1, and the longest smear as a share of the
13916
+ * frame, both held until changed. See `RendererApi.setCameraMotionBlur`.
13917
+ */
13918
+ setCameraMotionBlur(scale, maxShare = MOTION_BLUR_MAX_UV) {
12798
13919
  this.motionBlurScale = Math.min(Math.max(scale, 0), 1);
13920
+ this.motionBlurMax = clampMotionShare(maxShare);
13921
+ }
13922
+ /** The longest smear, as a share of the frame. See `setCameraMotionBlur`. */
13923
+ motionBlurMax = MOTION_BLUR_MAX_UV;
13924
+ warnedObjectBlur = false;
13925
+ /**
13926
+ * Said once: a draw named where it was last frame while the frame blurs, and this frame keeps no
13927
+ * motion target to blur it by — the camera's motion alone smears it. See `OBJECT_BLUR_UNKEPT`.
13928
+ */
13929
+ warnObjectBlur() {
13930
+ if (this.warnedObjectBlur)
13931
+ return;
13932
+ this.warnedObjectBlur = true;
13933
+ console.warn(OBJECT_BLUR_UNKEPT);
12799
13934
  }
12800
13935
  /**
12801
13936
  * The camera has cut: this frame is a new shot, not the next moment of the last one.
@@ -13239,7 +14374,7 @@ export class WebGPURenderer {
13239
14374
  .end();
13240
14375
  }
13241
14376
  }
13242
- device.queue.submit([clear.finish()]);
14377
+ this.submit([clear.finish()]);
13243
14378
  }
13244
14379
  /**
13245
14380
  * Declare where a grid's probes stand, and allocate the layers for them.
@@ -13342,18 +14477,19 @@ export class WebGPURenderer {
13342
14477
  * `bindMeshPass` and `drawMesh`, which record into whatever `this.pass` is. That is the same
13343
14478
  * arrangement `bakeFace` uses for a point light's cube, and it is why a bake must not be
13344
14479
  * opened inside a frame's pass by a caller who then expects the frame to continue.
13345
- */
13346
- const savedPass = this.pass;
13347
- /*
14480
+ *
13348
14481
  * **A bake is a boundary, and boundaries flush.** The mirror says why in its own words: what
13349
14482
  * has been recorded and not yet replayed belongs to the pass that is about to end, and leaving
13350
14483
  * it replays it into whichever pass opens next — a draw in the wrong target rather than a
13351
14484
  * missing one. This is the fourth such boundary in the backend and the only one that was never
13352
14485
  * given the treatment, which is what let a bake carry the frame's pending draws into a cube
13353
- * face.
14486
+ * face. **The pass is saved after the flush**, which opens it where none was: saved before,
14487
+ * the null given back left the flushed pass open and the frame's encoder locked. See
14488
+ * `captureScene`.
13354
14489
  */
13355
14490
  if (this.quality.frameGraph)
13356
14491
  this.flushGraph();
14492
+ const savedPass = this.pass;
13357
14493
  const savedEncoder = this.encoder;
13358
14494
  /*
13359
14495
  * **The rings are marked here and given back after the submit below**, because this bake is its
@@ -13449,7 +14585,7 @@ export class WebGPURenderer {
13449
14585
  if (whole)
13450
14586
  this.buildProbeChain(encoder, probe, this.probeSweeps?.writeLayer(layer) ?? layer);
13451
14587
  this.flushRings();
13452
- device.queue.submit([encoder.finish()]);
14588
+ this.submit([encoder.finish()]);
13453
14589
  this.rewindRings(skinMark);
13454
14590
  this.encoderEpoch = frameEpoch;
13455
14591
  this.encoder = savedEncoder;
@@ -13459,6 +14595,7 @@ export class WebGPURenderer {
13459
14595
  this.perFrameInts[this.materialField('uOutputTransform')] = heldGrade;
13460
14596
  this.perFrameFloats[this.materialField('uOutputExposure')] = heldExposure;
13461
14597
  this.materials.dirty();
14598
+ this.reopenPass();
13462
14599
  this.probePassActive = false;
13463
14600
  this.probeBounce = false;
13464
14601
  this.rebindEnvironment();
@@ -13506,7 +14643,7 @@ export class WebGPURenderer {
13506
14643
  this.uploadProbeFaces(probe, image);
13507
14644
  const encoder = device.createCommandEncoder({ label: 'probe.environmentImage' });
13508
14645
  this.buildProbeChain(encoder, probe, 0);
13509
- device.queue.submit([encoder.finish()]);
14646
+ this.submit([encoder.finish()]);
13510
14647
  this.markProbeFilled(0);
13511
14648
  this.probeAmbient = options?.irradiance ?? true;
13512
14649
  this.rebuildFlatBindGroup();
@@ -13531,7 +14668,7 @@ export class WebGPURenderer {
13531
14668
  const { device } = this.surface;
13532
14669
  const encoder = device.createCommandEncoder({ label: 'probe.layerImage' });
13533
14670
  this.buildProbeChain(encoder, probe, this.probeSweeps?.writeLayer(layer) ?? layer);
13534
- device.queue.submit([encoder.finish()]);
14671
+ this.submit([encoder.finish()]);
13535
14672
  this.markProbeFilled(layer);
13536
14673
  this.probeAmbient = options?.irradiance ?? true;
13537
14674
  this.rebuildFlatBindGroup();
@@ -13938,6 +15075,7 @@ export class WebGPURenderer {
13938
15075
  this.sceneColor,
13939
15076
  this.sceneColorMsaa,
13940
15077
  this.refractSnapshot,
15078
+ this.passColour,
13941
15079
  this.resolvedDepth,
13942
15080
  this.aoTarget,
13943
15081
  this.aoScratch,
@@ -14003,6 +15141,10 @@ export class WebGPURenderer {
14003
15141
  usage: 0x4 | 0x2 | 0x10, // TEXTURE_BINDING | COPY_DST | RENDER_ATTACHMENT
14004
15142
  });
14005
15143
  this.refractSnapshotView = this.refractSnapshot.createView();
15144
+ this.passColour = null;
15145
+ this.passColourView = null;
15146
+ if ((this.passReadsUnion & COLOR_SNAPSHOT) !== 0)
15147
+ this.allocatePassColour();
14006
15148
  this.refractMips = new SnapshotMips(device, this.pipelines, this.refractSnapshot, reconstructed ? RECON_HISTORY_FORMAT : format);
14007
15149
  /*
14008
15150
  * **Every cached flat bind group is stale the moment this view changes**, and the failure is
@@ -14218,6 +15360,10 @@ export class WebGPURenderer {
14218
15360
  usage: USAGE,
14219
15361
  });
14220
15362
  this.ssrReflectionView = this.ssrReflection.createView();
15363
+ /* The frame's materials, at the scene's size; a stand-in where nothing traces them. */
15364
+ this.reflectionSurface?.size(width, height);
15365
+ const probeMap = this.reflectionSurface?.probeView ?? this.blankView;
15366
+ const tintMap = this.reflectionSurface?.tintView ?? this.blankView;
14221
15367
  this.ssrBindGroup =
14222
15368
  this.resolvedDepthView === null || this.sceneColorView === null
14223
15369
  ? null
@@ -14231,8 +15377,23 @@ export class WebGPURenderer {
14231
15377
  },
14232
15378
  ...entry(SSR_TEXTURES.uSsrDepth, this.resolvedDepthView, true),
14233
15379
  ...entry(SSR_TEXTURES.uSsrScene, this.sceneColorView),
15380
+ ...entry(SSR_TEXTURES.uSsrProbeMap, probeMap),
15381
+ ...entry(SSR_TEXTURES.uSsrTintMap, tintMap),
14234
15382
  ],
14235
15383
  });
15384
+ this.ssrMaterialGroup = device.createBindGroup({
15385
+ label: 'post.ssrMaterialGroup',
15386
+ layout: this.ssrMaterialLayout,
15387
+ entries: [
15388
+ {
15389
+ binding: SSR_MATERIAL_UNIFORMS,
15390
+ resource: { buffer: this.ssrMaterialUniforms, size: SSR_MATERIAL_SIZE },
15391
+ },
15392
+ ...entry(SSR_MATERIAL_TEXTURES.uSsrReflection, this.ssrReflectionView),
15393
+ ...entry(SSR_MATERIAL_TEXTURES.uSsrProbeMap, probeMap),
15394
+ ...entry(SSR_MATERIAL_TEXTURES.uSsrTintMap, tintMap),
15395
+ ],
15396
+ });
14236
15397
  this.ssrResolveGroup = device.createBindGroup({
14237
15398
  label: 'post.ssrResolveGroup',
14238
15399
  layout: this.ssrResolveLayout,