@driftengine/core 3.63.0 → 4.0.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 (428) hide show
  1. package/CHANGELOG.json +111 -0
  2. package/NOTICE +20 -0
  3. package/README.md +1 -1
  4. package/dist/core/loop.js +18 -2
  5. package/dist/core/storage.js +5 -0
  6. package/dist/geometry/meshBuilder.d.ts +1 -1
  7. package/dist/geometry/meshBuilder.js +6 -2
  8. package/dist/index.d.ts +69 -4
  9. package/dist/index.js +112 -2
  10. package/dist/input/input.js +6 -1
  11. package/dist/math/exact.d.ts +13 -3
  12. package/dist/math/exact.js +68 -3
  13. package/dist/math/frustum.d.ts +17 -2
  14. package/dist/math/frustum.js +24 -0
  15. package/dist/render/backend/api.d.ts +34 -1
  16. package/dist/render/backend/api.js +22 -1
  17. package/dist/render/backend/budget.d.ts +7 -6
  18. package/dist/render/backend/budget.js +7 -6
  19. package/dist/render/backend/createRenderer.d.ts +57 -0
  20. package/dist/render/backend/createRenderer.js +51 -2
  21. package/dist/render/backend/materialChanges.d.ts +41 -0
  22. package/dist/render/backend/materialChanges.js +38 -0
  23. package/dist/render/backend/select.js +8 -0
  24. package/dist/render/backend/webgl2/renderer.d.ts +80 -1
  25. package/dist/render/backend/webgl2/renderer.js +208 -10
  26. package/dist/render/backend/webgpu/buffers.d.ts +20 -1
  27. package/dist/render/backend/webgpu/buffers.js +116 -58
  28. package/dist/render/backend/webgpu/fieldCompose.d.ts +156 -0
  29. package/dist/render/backend/webgpu/fieldCompose.js +396 -0
  30. package/dist/render/backend/webgpu/giFieldPass.d.ts +66 -0
  31. package/dist/render/backend/webgpu/giFieldPass.js +137 -0
  32. package/dist/render/backend/webgpu/gpuDrivenPass.d.ts +728 -0
  33. package/dist/render/backend/webgpu/gpuDrivenPass.js +2473 -0
  34. package/dist/render/backend/webgpu/linePass.d.ts +4 -14
  35. package/dist/render/backend/webgpu/linePass.js +4 -33
  36. package/dist/render/backend/webgpu/probeBake.d.ts +148 -0
  37. package/dist/render/backend/webgpu/probeBake.js +493 -0
  38. package/dist/render/backend/webgpu/renderer.d.ts +379 -24
  39. package/dist/render/backend/webgpu/renderer.js +1369 -76
  40. package/dist/render/backend/webgpu/scatterPass.js +15 -1
  41. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +19 -9
  42. package/dist/render/backend/webgpu/surfaceTexturePass.js +58 -32
  43. package/dist/render/causticsRenderer.d.ts +3 -1
  44. package/dist/render/causticsRenderer.js +4 -1
  45. package/dist/render/depthConvention.d.ts +12 -0
  46. package/dist/render/depthConvention.js +14 -0
  47. package/dist/render/deviceFeatures.d.ts +32 -0
  48. package/dist/render/deviceFeatures.js +41 -0
  49. package/dist/render/fog.d.ts +79 -0
  50. package/dist/render/fog.js +138 -0
  51. package/dist/render/frame/alias.d.ts +30 -0
  52. package/dist/render/frame/alias.js +61 -0
  53. package/dist/render/frame/deps.d.ts +37 -0
  54. package/dist/render/frame/deps.js +122 -0
  55. package/dist/render/frame/flushGraph.d.ts +43 -0
  56. package/dist/render/frame/flushGraph.js +84 -0
  57. package/dist/render/frame/graphSchedule.d.ts +49 -0
  58. package/dist/render/frame/graphSchedule.js +120 -0
  59. package/dist/render/frame/index.d.ts +15 -0
  60. package/dist/render/frame/index.js +8 -0
  61. package/dist/render/frame/lifetime.d.ts +24 -0
  62. package/dist/render/frame/lifetime.js +36 -0
  63. package/dist/render/frame/passTimings.d.ts +42 -0
  64. package/dist/render/frame/passTimings.js +38 -0
  65. package/dist/render/frame/validate.d.ts +15 -0
  66. package/dist/render/frame/validate.js +22 -0
  67. package/dist/render/frame/virtual.d.ts +44 -0
  68. package/dist/render/frame/virtual.js +61 -0
  69. package/dist/render/gi/chain.d.ts +82 -0
  70. package/dist/render/gi/chain.js +135 -0
  71. package/dist/render/gi/denoise.d.ts +114 -0
  72. package/dist/render/gi/denoise.js +169 -0
  73. package/dist/render/gi/fieldScene.d.ts +125 -0
  74. package/dist/render/gi/fieldScene.js +219 -0
  75. package/dist/render/gi/globalField.d.ts +157 -0
  76. package/dist/render/gi/globalField.js +373 -0
  77. package/dist/render/gi/probeGridFit.d.ts +24 -0
  78. package/dist/render/gi/probeGridFit.js +71 -0
  79. package/dist/render/gi/probeTrace.d.ts +82 -0
  80. package/dist/render/gi/probeTrace.js +128 -0
  81. package/dist/render/gi/probeVolume.d.ts +136 -0
  82. package/dist/render/gi/probeVolume.js +191 -0
  83. package/dist/render/gi/reflection.d.ts +73 -0
  84. package/dist/render/gi/reflection.js +109 -0
  85. package/dist/render/gi/traceField.d.ts +91 -0
  86. package/dist/render/gi/traceField.js +129 -0
  87. package/dist/render/gi/traceScreen.d.ts +68 -0
  88. package/dist/render/gi/traceScreen.js +82 -0
  89. package/dist/render/gpuScene.d.ts +38 -0
  90. package/dist/render/gpuScene.js +41 -0
  91. package/dist/render/gpudriven/alphaTest.d.ts +28 -0
  92. package/dist/render/gpudriven/alphaTest.js +32 -0
  93. package/dist/render/gpudriven/clusterUpload.d.ts +59 -0
  94. package/dist/render/gpudriven/clusterUpload.js +75 -0
  95. package/dist/render/gpudriven/clusterWorld.d.ts +57 -0
  96. package/dist/render/gpudriven/clusterWorld.js +155 -0
  97. package/dist/render/gpudriven/compact.d.ts +51 -0
  98. package/dist/render/gpudriven/compact.js +68 -0
  99. package/dist/render/gpudriven/cullClusters.d.ts +129 -0
  100. package/dist/render/gpudriven/cullClusters.js +237 -0
  101. package/dist/render/gpudriven/decodeTables.d.ts +64 -0
  102. package/dist/render/gpudriven/decodeTables.js +219 -0
  103. package/dist/render/gpudriven/frustum.d.ts +25 -0
  104. package/dist/render/gpudriven/frustum.js +59 -0
  105. package/dist/render/gpudriven/geometryHarness.d.ts +15 -0
  106. package/dist/render/gpudriven/geometryHarness.js +31 -0
  107. package/dist/render/gpudriven/hzb.d.ts +60 -0
  108. package/dist/render/gpudriven/hzb.js +97 -0
  109. package/dist/render/gpudriven/ibl.d.ts +85 -0
  110. package/dist/render/gpudriven/ibl.js +129 -0
  111. package/dist/render/gpudriven/indirect.d.ts +67 -0
  112. package/dist/render/gpudriven/indirect.js +77 -0
  113. package/dist/render/gpudriven/instances.d.ts +43 -0
  114. package/dist/render/gpudriven/instances.js +172 -0
  115. package/dist/render/gpudriven/lit.d.ts +165 -0
  116. package/dist/render/gpudriven/lit.js +165 -0
  117. package/dist/render/gpudriven/lodCut.d.ts +28 -0
  118. package/dist/render/gpudriven/lodCut.js +38 -0
  119. package/dist/render/gpudriven/materialBin.d.ts +60 -0
  120. package/dist/render/gpudriven/materialBin.js +108 -0
  121. package/dist/render/gpudriven/materialTable.d.ts +88 -0
  122. package/dist/render/gpudriven/materialTable.js +115 -0
  123. package/dist/render/gpudriven/pipeline.d.ts +135 -0
  124. package/dist/render/gpudriven/pipeline.js +237 -0
  125. package/dist/render/gpudriven/rangeAlloc.d.ts +50 -0
  126. package/dist/render/gpudriven/rangeAlloc.js +112 -0
  127. package/dist/render/gpudriven/sceneUpload.d.ts +96 -0
  128. package/dist/render/gpudriven/sceneUpload.js +115 -0
  129. package/dist/render/gpudriven/shadeBins.d.ts +92 -0
  130. package/dist/render/gpudriven/shadeBins.js +182 -0
  131. package/dist/render/gpudriven/shadow.d.ts +136 -0
  132. package/dist/render/gpudriven/shadow.js +224 -0
  133. package/dist/render/gpudriven/shadowCamera.d.ts +87 -0
  134. package/dist/render/gpudriven/shadowCamera.js +345 -0
  135. package/dist/render/gpudriven/streamScene.d.ts +194 -0
  136. package/dist/render/gpudriven/streamScene.js +459 -0
  137. package/dist/render/gpudriven/surfaceFrame.d.ts +25 -0
  138. package/dist/render/gpudriven/surfaceFrame.js +51 -0
  139. package/dist/render/gpudriven/twoPhase.d.ts +122 -0
  140. package/dist/render/gpudriven/twoPhase.js +160 -0
  141. package/dist/render/gpudriven/visbuffer.d.ts +25 -0
  142. package/dist/render/gpudriven/visbuffer.js +37 -0
  143. package/dist/render/halfFloat.d.ts +10 -0
  144. package/dist/render/halfFloat.js +23 -4
  145. package/dist/render/inference/attentionTiled.d.ts +27 -0
  146. package/dist/render/inference/attentionTiled.js +117 -0
  147. package/dist/render/inference/denseKernels.d.ts +29 -0
  148. package/dist/render/inference/denseKernels.js +312 -0
  149. package/dist/render/inference/device.d.ts +21 -0
  150. package/dist/render/inference/device.js +32 -0
  151. package/dist/render/inference/deviceGraph.d.ts +34 -0
  152. package/dist/render/inference/deviceGraph.js +17 -0
  153. package/dist/render/inference/fuse.d.ts +15 -0
  154. package/dist/render/inference/fuse.js +37 -0
  155. package/dist/render/inference/kernelKit.d.ts +52 -0
  156. package/dist/render/inference/kernelKit.js +70 -0
  157. package/dist/render/inference/kernels.d.ts +3 -0
  158. package/dist/render/inference/kernels.js +11 -0
  159. package/dist/render/inference/plan.d.ts +13 -0
  160. package/dist/render/inference/plan.js +18 -0
  161. package/dist/render/inference/reuse.d.ts +39 -0
  162. package/dist/render/inference/reuse.js +65 -0
  163. package/dist/render/inference/runner.d.ts +31 -0
  164. package/dist/render/inference/runner.js +175 -0
  165. package/dist/render/inference/schedule.d.ts +39 -0
  166. package/dist/render/inference/schedule.js +59 -0
  167. package/dist/render/inference/shapeKernels.d.ts +10 -0
  168. package/dist/render/inference/shapeKernels.js +80 -0
  169. package/dist/render/inference/spatialKernels.d.ts +21 -0
  170. package/dist/render/inference/spatialKernels.js +179 -0
  171. package/dist/render/inference/splatRaster.wgsl.d.ts +75 -0
  172. package/dist/render/inference/splatRaster.wgsl.js +337 -0
  173. package/dist/render/inference/splatTiles.d.ts +79 -0
  174. package/dist/render/inference/splatTiles.js +161 -0
  175. package/dist/render/lineBatch.d.ts +2 -5
  176. package/dist/render/lineBatch.js +4 -24
  177. package/dist/render/mesh.d.ts +4 -0
  178. package/dist/render/mesh.js +45 -1
  179. package/dist/render/pass.d.ts +145 -8
  180. package/dist/render/prefilterEnvMap.d.ts +22 -0
  181. package/dist/render/prefilterEnvMap.js +31 -0
  182. package/dist/render/readback.d.ts +30 -0
  183. package/dist/render/readback.js +47 -0
  184. package/dist/render/recon/clamp.d.ts +36 -0
  185. package/dist/render/recon/clamp.js +92 -0
  186. package/dist/render/recon/disocclusion.d.ts +60 -0
  187. package/dist/render/recon/disocclusion.js +109 -0
  188. package/dist/render/recon/frameGen.d.ts +81 -0
  189. package/dist/render/recon/frameGen.js +150 -0
  190. package/dist/render/recon/frameSizes.d.ts +34 -0
  191. package/dist/render/recon/frameSizes.js +58 -0
  192. package/dist/render/recon/jitter.d.ts +60 -0
  193. package/dist/render/recon/jitter.js +118 -0
  194. package/dist/render/recon/motionVectors.d.ts +55 -0
  195. package/dist/render/recon/motionVectors.js +107 -0
  196. package/dist/render/recon/reproject.d.ts +41 -0
  197. package/dist/render/recon/reproject.js +131 -0
  198. package/dist/render/recon/resolve.d.ts +90 -0
  199. package/dist/render/recon/resolve.js +313 -0
  200. package/dist/render/recon/tier.d.ts +24 -0
  201. package/dist/render/recon/tier.js +8 -0
  202. package/dist/render/reflectionProbe.d.ts +26 -0
  203. package/dist/render/reflectionProbe.js +33 -0
  204. package/dist/render/renderQuality.d.ts +69 -0
  205. package/dist/render/renderQuality.js +22 -0
  206. package/dist/render/screenSpaceReflection.d.ts +23 -0
  207. package/dist/render/screenSpaceReflection.js +22 -3
  208. package/dist/render/sdfFont.js +2 -0
  209. package/dist/render/segmentQuads.d.ts +13 -0
  210. package/dist/render/segmentQuads.js +32 -0
  211. package/dist/render/shaders/flat/lobes.d.ts +27 -0
  212. package/dist/render/shaders/flat/lobes.js +33 -3
  213. package/dist/render/shaders/flat/preamble.d.ts +7 -0
  214. package/dist/render/shaders/flat/preamble.js +24 -1
  215. package/dist/render/shaders/flat/probeGrid.d.ts +1 -1
  216. package/dist/render/shaders/flat/probeGrid.js +71 -2
  217. package/dist/render/shaders/generated/flat.wgsl.d.ts +48 -8
  218. package/dist/render/shaders/generated/flat.wgsl.js +64 -24
  219. package/dist/render/shaders/gi/composeField.wgsl.d.ts +56 -0
  220. package/dist/render/shaders/gi/composeField.wgsl.js +231 -0
  221. package/dist/render/shaders/gi/marchField.wgsl.d.ts +30 -0
  222. package/dist/render/shaders/gi/marchField.wgsl.js +141 -0
  223. package/dist/render/shaders/gi/probeBake.wgsl.d.ts +113 -0
  224. package/dist/render/shaders/gi/probeBake.wgsl.js +635 -0
  225. package/dist/render/shaders/gi/probeTrace.wgsl.d.ts +72 -0
  226. package/dist/render/shaders/gi/probeTrace.wgsl.js +261 -0
  227. package/dist/render/shaders/gi/probeVolume.wgsl.d.ts +49 -0
  228. package/dist/render/shaders/gi/probeVolume.wgsl.js +242 -0
  229. package/dist/render/shaders/gi/sampleField.wgsl.d.ts +32 -0
  230. package/dist/render/shaders/gi/sampleField.wgsl.js +134 -0
  231. package/dist/render/shaders/gpudriven/blendRaster.wgsl.d.ts +26 -0
  232. package/dist/render/shaders/gpudriven/blendRaster.wgsl.js +339 -0
  233. package/dist/render/shaders/gpudriven/compact.wgsl.d.ts +29 -0
  234. package/dist/render/shaders/gpudriven/compact.wgsl.js +58 -0
  235. package/dist/render/shaders/gpudriven/cull.wgsl.d.ts +90 -0
  236. package/dist/render/shaders/gpudriven/cull.wgsl.js +423 -0
  237. package/dist/render/shaders/gpudriven/decode.wgsl.d.ts +72 -0
  238. package/dist/render/shaders/gpudriven/decode.wgsl.js +349 -0
  239. package/dist/render/shaders/gpudriven/environment.wgsl.d.ts +44 -0
  240. package/dist/render/shaders/gpudriven/environment.wgsl.js +137 -0
  241. package/dist/render/shaders/gpudriven/fog.wgsl.d.ts +28 -0
  242. package/dist/render/shaders/gpudriven/fog.wgsl.js +107 -0
  243. package/dist/render/shaders/gpudriven/frameBlock.wgsl.d.ts +13 -0
  244. package/dist/render/shaders/gpudriven/frameBlock.wgsl.js +62 -0
  245. package/dist/render/shaders/gpudriven/materialBin.wgsl.d.ts +51 -0
  246. package/dist/render/shaders/gpudriven/materialBin.wgsl.js +112 -0
  247. package/dist/render/shaders/gpudriven/materialTable.wgsl.d.ts +25 -0
  248. package/dist/render/shaders/gpudriven/materialTable.wgsl.js +43 -0
  249. package/dist/render/shaders/gpudriven/meshTransform.wgsl.d.ts +18 -0
  250. package/dist/render/shaders/gpudriven/meshTransform.wgsl.js +37 -0
  251. package/dist/render/shaders/gpudriven/present.wgsl.d.ts +43 -0
  252. package/dist/render/shaders/gpudriven/present.wgsl.js +127 -0
  253. package/dist/render/shaders/gpudriven/shade.wgsl.d.ts +127 -0
  254. package/dist/render/shaders/gpudriven/shade.wgsl.js +874 -0
  255. package/dist/render/shaders/gpudriven/shadow.wgsl.d.ts +44 -0
  256. package/dist/render/shaders/gpudriven/shadow.wgsl.js +308 -0
  257. package/dist/render/shaders/gpudriven/visbufferRaster.wgsl.d.ts +28 -0
  258. package/dist/render/shaders/gpudriven/visbufferRaster.wgsl.js +245 -0
  259. package/dist/render/shaders/network.wgsl.d.ts +101 -0
  260. package/dist/render/shaders/network.wgsl.js +206 -0
  261. package/dist/render/shaders/recon/motion.wgsl.d.ts +44 -0
  262. package/dist/render/shaders/recon/motion.wgsl.js +96 -0
  263. package/dist/render/shaders/recon/resolve.wgsl.d.ts +51 -0
  264. package/dist/render/shaders/recon/resolve.wgsl.js +733 -0
  265. package/dist/render/temporalAa.js +8 -33
  266. package/dist/render/textLayout.js +12 -6
  267. package/dist/render/textRenderer.d.ts +3 -1
  268. package/dist/render/textRenderer.js +4 -1
  269. package/dist/render/translucentQueue.d.ts +6 -0
  270. package/dist/render/translucentQueue.js +11 -0
  271. package/dist/render/windStreakRenderer.d.ts +2 -1
  272. package/dist/render/windStreakRenderer.js +3 -1
  273. package/dist/world/cell.d.ts +63 -0
  274. package/dist/world/cell.js +181 -0
  275. package/dist/world/cellStream.d.ts +103 -0
  276. package/dist/world/cellStream.js +198 -0
  277. package/dist/world/freeze.d.ts +81 -0
  278. package/dist/world/freeze.js +114 -0
  279. package/dist/world/rebase.d.ts +29 -0
  280. package/dist/world/rebase.js +41 -0
  281. package/package.json +8 -3
  282. package/scripts/frames.mjs +92 -0
  283. package/scripts/gpuCompute.mjs +323 -0
  284. package/scripts/png.d.mts +16 -0
  285. package/scripts/png.mjs +25 -0
  286. package/src/core/loop.ts +19 -2
  287. package/src/core/storage.ts +5 -0
  288. package/src/geometry/meshBuilder.ts +5 -1
  289. package/src/index.ts +246 -4
  290. package/src/input/input.ts +6 -1
  291. package/src/math/exact.ts +69 -3
  292. package/src/math/frustum.ts +52 -3
  293. package/src/render/backend/api.ts +42 -1
  294. package/src/render/backend/budget.ts +7 -6
  295. package/src/render/backend/createRenderer.ts +92 -3
  296. package/src/render/backend/materialChanges.ts +54 -0
  297. package/src/render/backend/select.ts +8 -0
  298. package/src/render/backend/webgl2/renderer.ts +211 -9
  299. package/src/render/backend/webgpu/buffers.ts +182 -69
  300. package/src/render/backend/webgpu/fieldCompose.ts +482 -0
  301. package/src/render/backend/webgpu/giFieldPass.ts +200 -0
  302. package/src/render/backend/webgpu/gpuDrivenPass.ts +2993 -0
  303. package/src/render/backend/webgpu/linePass.ts +4 -39
  304. package/src/render/backend/webgpu/probeBake.ts +588 -0
  305. package/src/render/backend/webgpu/renderer.ts +1481 -76
  306. package/src/render/backend/webgpu/scatterPass.ts +15 -1
  307. package/src/render/backend/webgpu/surfaceTexturePass.ts +63 -36
  308. package/src/render/causticsRenderer.ts +5 -2
  309. package/src/render/depthConvention.ts +15 -0
  310. package/src/render/deviceFeatures.ts +77 -0
  311. package/src/render/fog.ts +185 -0
  312. package/src/render/frame/alias.ts +97 -0
  313. package/src/render/frame/deps.ts +155 -0
  314. package/src/render/frame/flushGraph.ts +135 -0
  315. package/src/render/frame/graphSchedule.ts +170 -0
  316. package/src/render/frame/index.ts +52 -0
  317. package/src/render/frame/lifetime.ts +61 -0
  318. package/src/render/frame/passTimings.ts +69 -0
  319. package/src/render/frame/validate.ts +40 -0
  320. package/src/render/frame/virtual.ts +91 -0
  321. package/src/render/gi/chain.ts +225 -0
  322. package/src/render/gi/denoise.ts +276 -0
  323. package/src/render/gi/fieldScene.ts +264 -0
  324. package/src/render/gi/globalField.ts +543 -0
  325. package/src/render/gi/probeGridFit.ts +89 -0
  326. package/src/render/gi/probeTrace.ts +143 -0
  327. package/src/render/gi/probeVolume.ts +288 -0
  328. package/src/render/gi/reflection.ts +163 -0
  329. package/src/render/gi/traceField.ts +223 -0
  330. package/src/render/gi/traceScreen.ts +106 -0
  331. package/src/render/gpuScene.ts +80 -0
  332. package/src/render/gpudriven/alphaTest.ts +33 -0
  333. package/src/render/gpudriven/clusterUpload.ts +123 -0
  334. package/src/render/gpudriven/clusterWorld.ts +239 -0
  335. package/src/render/gpudriven/compact.ts +80 -0
  336. package/src/render/gpudriven/cullClusters.ts +333 -0
  337. package/src/render/gpudriven/decodeTables.ts +332 -0
  338. package/src/render/gpudriven/frustum.ts +70 -0
  339. package/src/render/gpudriven/geometryHarness.ts +51 -0
  340. package/src/render/gpudriven/hzb.ts +113 -0
  341. package/src/render/gpudriven/ibl.ts +147 -0
  342. package/src/render/gpudriven/indirect.ts +89 -0
  343. package/src/render/gpudriven/instances.ts +234 -0
  344. package/src/render/gpudriven/lit.ts +329 -0
  345. package/src/render/gpudriven/lodCut.ts +62 -0
  346. package/src/render/gpudriven/materialBin.ts +131 -0
  347. package/src/render/gpudriven/materialTable.ts +164 -0
  348. package/src/render/gpudriven/pipeline.ts +336 -0
  349. package/src/render/gpudriven/rangeAlloc.ts +122 -0
  350. package/src/render/gpudriven/sceneUpload.ts +173 -0
  351. package/src/render/gpudriven/shadeBins.ts +222 -0
  352. package/src/render/gpudriven/shadow.ts +285 -0
  353. package/src/render/gpudriven/shadowCamera.ts +406 -0
  354. package/src/render/gpudriven/streamScene.ts +575 -0
  355. package/src/render/gpudriven/surfaceFrame.ts +68 -0
  356. package/src/render/gpudriven/twoPhase.ts +214 -0
  357. package/src/render/gpudriven/visbuffer.ts +41 -0
  358. package/src/render/halfFloat.ts +22 -4
  359. package/src/render/inference/attentionTiled.ts +132 -0
  360. package/src/render/inference/denseKernels.ts +369 -0
  361. package/src/render/inference/device.ts +44 -0
  362. package/src/render/inference/deviceGraph.ts +39 -0
  363. package/src/render/inference/fuse.ts +47 -0
  364. package/src/render/inference/kernelKit.ts +120 -0
  365. package/src/render/inference/kernels.ts +18 -0
  366. package/src/render/inference/plan.ts +26 -0
  367. package/src/render/inference/reuse.ts +79 -0
  368. package/src/render/inference/runner.ts +204 -0
  369. package/src/render/inference/schedule.ts +80 -0
  370. package/src/render/inference/shapeKernels.ts +112 -0
  371. package/src/render/inference/spatialKernels.ts +220 -0
  372. package/src/render/inference/splatRaster.wgsl.ts +348 -0
  373. package/src/render/inference/splatTiles.ts +186 -0
  374. package/src/render/lineBatch.ts +4 -24
  375. package/src/render/mesh.ts +46 -1
  376. package/src/render/pass.ts +155 -10
  377. package/src/render/prefilterEnvMap.ts +33 -0
  378. package/src/render/readback.ts +73 -0
  379. package/src/render/recon/clamp.ts +113 -0
  380. package/src/render/recon/disocclusion.ts +143 -0
  381. package/src/render/recon/frameGen.ts +191 -0
  382. package/src/render/recon/frameSizes.ts +64 -0
  383. package/src/render/recon/jitter.ts +126 -0
  384. package/src/render/recon/motionVectors.ts +138 -0
  385. package/src/render/recon/reproject.ts +179 -0
  386. package/src/render/recon/resolve.ts +444 -0
  387. package/src/render/recon/tier.ts +31 -0
  388. package/src/render/reflectionProbe.ts +34 -0
  389. package/src/render/renderQuality.ts +87 -0
  390. package/src/render/screenSpaceReflection.ts +37 -3
  391. package/src/render/sdfFont.ts +2 -0
  392. package/src/render/segmentQuads.ts +39 -0
  393. package/src/render/shaders/flat/lobes.ts +35 -3
  394. package/src/render/shaders/flat/preamble.ts +25 -1
  395. package/src/render/shaders/flat/probeGrid.ts +77 -2
  396. package/src/render/shaders/generated/flat.wgsl.ts +64 -24
  397. package/src/render/shaders/gi/composeField.wgsl.ts +239 -0
  398. package/src/render/shaders/gi/marchField.wgsl.ts +146 -0
  399. package/src/render/shaders/gi/probeBake.wgsl.ts +649 -0
  400. package/src/render/shaders/gi/probeTrace.wgsl.ts +269 -0
  401. package/src/render/shaders/gi/probeVolume.wgsl.ts +246 -0
  402. package/src/render/shaders/gi/sampleField.wgsl.ts +140 -0
  403. package/src/render/shaders/gpudriven/blendRaster.wgsl.ts +343 -0
  404. package/src/render/shaders/gpudriven/compact.wgsl.ts +59 -0
  405. package/src/render/shaders/gpudriven/cull.wgsl.ts +433 -0
  406. package/src/render/shaders/gpudriven/decode.wgsl.ts +373 -0
  407. package/src/render/shaders/gpudriven/environment.wgsl.ts +141 -0
  408. package/src/render/shaders/gpudriven/fog.wgsl.ts +110 -0
  409. package/src/render/shaders/gpudriven/frameBlock.wgsl.ts +62 -0
  410. package/src/render/shaders/gpudriven/materialBin.wgsl.ts +116 -0
  411. package/src/render/shaders/gpudriven/materialTable.wgsl.ts +46 -0
  412. package/src/render/shaders/gpudriven/meshTransform.wgsl.ts +37 -0
  413. package/src/render/shaders/gpudriven/present.wgsl.ts +129 -0
  414. package/src/render/shaders/gpudriven/shade.wgsl.ts +884 -0
  415. package/src/render/shaders/gpudriven/shadow.wgsl.ts +322 -0
  416. package/src/render/shaders/gpudriven/visbufferRaster.wgsl.ts +251 -0
  417. package/src/render/shaders/network.wgsl.ts +238 -0
  418. package/src/render/shaders/recon/motion.wgsl.ts +101 -0
  419. package/src/render/shaders/recon/resolve.wgsl.ts +741 -0
  420. package/src/render/temporalAa.ts +9 -33
  421. package/src/render/textLayout.ts +12 -6
  422. package/src/render/textRenderer.ts +5 -2
  423. package/src/render/translucentQueue.ts +11 -0
  424. package/src/render/windStreakRenderer.ts +4 -2
  425. package/src/world/cell.ts +231 -0
  426. package/src/world/cellStream.ts +278 -0
  427. package/src/world/freeze.ts +157 -0
  428. package/src/world/rebase.ts +56 -0
@@ -2,7 +2,13 @@ import { mat4 } from 'gl-matrix';
2
2
  import { TEMPORAL_HISTORY_BLEND, TemporalHistory, jitterOffset, jitterProjection, } from '../../temporalAa.js';
3
3
  import { OIT_MULTISAMPLE_REFUSAL } from '../../orderIndependent.js';
4
4
  import { TranslucentQueue } from '../../translucentQueue.js';
5
- import { DEPTH_CLEAR, MAX_DEPTH_LAYER, REVERSED_DEPTH } from '../../depthConvention.js';
5
+ import { DEPTH_CLEAR, DEPTH_COMPARE_EQUAL, MAX_DEPTH_LAYER, REVERSED_DEPTH, } from '../../depthConvention.js';
6
+ import { RECONSTRUCTION_MULTISAMPLE_REFUSAL, reconRenderSize } from '../../recon/frameSizes.js';
7
+ import { jitterOffset as jitterOffsetFor, reconJitterPhases } from '../../recon/jitter.js';
8
+ import { RECON_HISTORY_FORMAT, RECON_PARAM_FLOATS, RECON_WORKGROUP, reconResolveWgsl, } from '../../shaders/recon/resolve.wgsl.js';
9
+ import { MOTION_DRAW_FLOATS, MOTION_DRAW_STRIDE, MOTION_FRAME_FLOATS, MOTION_TARGET_FORMAT, RECON_MOTION_WGSL, } from '../../shaders/recon/motion.wgsl.js';
10
+ import { DEFAULT_DISOCCLUSION } from '../../recon/disocclusion.js';
11
+ import { DEFAULT_RECON_QUALITY } from '../../recon/resolve.js';
6
12
  import { clamp } from '../../../math/scalar.js';
7
13
  import { Camera } from '../../camera.js';
8
14
  import { PickableSet } from '../../pickable.js';
@@ -13,11 +19,11 @@ import { BLOOM_FILTER_RADIUS_UV, BLOOM_LEVELS, bloomLevelSizes, bloomProfileWarn
13
19
  import { createResolvedWater, resolveWater, waterAppearance, } from '../../waterDraw.js';
14
20
  import { createResolvedLightVolume, DEPTH_01_TO_CLIP_Y_DOWN, resolveLightVolume, volumeMediumGain, } from '../../lightVolumeDraw.js';
15
21
  import { WIND_STREAK_DEFAULTS, createResolvedWindStreaks, resolveWindStreaks, } from '../../windStreakDraw.js';
16
- import { expandBoltSegments } from '../../segmentQuads.js';
22
+ import { expandBoltSegments, expandLineSegments } from '../../segmentQuads.js';
17
23
  import { LIVE_POINT_SHADOW_MAPS, MAX_POINT_LIGHTS, POINT_SHADOW_POOL } from '../../lightBudget.js';
18
24
  import { roughnessForLevel } from '../../prefilterEnvMap.js';
19
25
  import { equirectToCubeFaces } from '../../equirectToCube.js';
20
- import { ProbeGrid, SINGLE_PROBE, UNIT_STEP, WORLD_ORIGIN, sameGrid } from '../../probeGrid.js';
26
+ import { MAX_ENV_PROBES, ProbeGrid, SINGLE_PROBE, UNIT_STEP, WORLD_ORIGIN, sameGrid, } from '../../probeGrid.js';
21
27
  import { ggxMaxLevelFor, irradianceLevelFor, octahedralEdgeFor } from '../../prefilterEnvMap.js';
22
28
  import { environmentTexels } from './environmentTexels.js';
23
29
  import { packIesAtlas } from '../../iesProfile.js';
@@ -65,16 +71,33 @@ import { PARTICLE_VERT_FIELDS, PARTICLE_VERT_SIZE, createGpuParticles, createPar
65
71
  import { WIND_STREAK_FRAG_FIELDS, WIND_STREAK_FRAG_SIZE, WIND_STREAK_VERT_FIELDS, WIND_STREAK_VERT_SIZE, createGpuWindStreaks, createWindStreakBindGroup, createWindStreakBindGroupLayout, windStreakPipeline, } from './windStreakPass.js';
66
72
  import { FLOCK_FRAG_FIELDS, FLOCK_FRAG_SIZE, FLOCK_VERT_FIELDS, FLOCK_VERT_SIZE, createFlockBindGroup, createFlockBindGroupLayout, createGpuFlock, flockPipeline, } from './flockPass.js';
67
73
  import { BOLT_FRAG_FIELDS, BOLT_FRAG_SIZE, BOLT_VERT_FIELDS, BOLT_VERT_SIZE, boltPipeline, createBoltBindGroup, createBoltBindGroupLayout, createGpuBolts, } from './boltPass.js';
68
- import { LINE_FRAG_FIELDS, LINE_FRAG_SIZE, LINE_VERT_FIELDS, LINE_VERT_SIZE, createGpuLines, createLineBindGroup, createLineBindGroupLayout, expandLineSegments, linePipeline, } from './linePass.js';
74
+ import { LINE_FRAG_FIELDS, LINE_FRAG_SIZE, LINE_VERT_FIELDS, LINE_VERT_SIZE, createGpuLines, createLineBindGroup, createLineBindGroupLayout, linePipeline, } from './linePass.js';
69
75
  import { LIGHT_VOLUME_VERT_FIELDS, LIGHT_VOLUME_VERT_SIZE, createLightVolumeBindGroup, createLightVolumeBindGroupLayout, lightVolumeFragmentBindings, lightVolumePipeline, } from './lightVolumePass.js';
70
76
  import { aimReflection, createGpuReflection } from './reflectionPass.js';
71
77
  import { FrameBudget } from '../budget.js';
78
+ import { MaterialChanges, ownsMaterial } from '../materialChanges.js';
72
79
  import { DYNAMIC_ALIGNMENT as DYNAMIC_UNIFORM_ALIGNMENT, UniformRing } from './uniformRing.js';
73
80
  import { DecalQueue, MAX_DRAWN_DECALS } from '../../decalQueue.js';
81
+ import { DistanceFieldScene } from '../../gi/fieldScene.js';
82
+ import { DEFAULT_FIELD_COMPOSE, FieldComposer } from './fieldCompose.js';
83
+ import { ProbeBaker } from './probeBake.js';
84
+ import { distanceFieldBounds } from '../../gi/fieldScene.js';
85
+ import { fitProbeGrid } from '../../gi/probeGridFit.js';
86
+ /**
87
+ * Metres between probes when the renderer fits a grid itself.
88
+ *
89
+ * **Two, which is a room rather than a building.** The spacing is what decides how local the light
90
+ * can be: a bounce off a wall reaches a surface two metres away and no nearer detail than that.
91
+ * `fitProbeGrid` widens it uniformly when the budget bites, so a large scene gets a coarser grid
92
+ * rather than a truncated one.
93
+ */
94
+ const INDIRECT_PROBE_SPACING = 2;
95
+ /** What the trace is given when a frame never said what was lighting it. */
96
+ const NO_SUN = [0, 0, 0];
74
97
  import { CLIP_Y_FLIP, MAX_REFLECTIVE_SURFACES, REFLECTION_EDGE_FADE, ReflectionQueue, } from '../../screenSpaceReflection.js';
75
98
  import { decalScissor } from '../../decalProjector.js';
76
99
  import { createCommandPool, resetPool, takeCommand } from './drawCommand.js';
77
- import { RESOURCE_COUNT, createArena, keptNodes, maskOf, nodeCount, nodeState, nodeVerb, recordNode, resetArena, schedule, scratchFor, } from '../../frame/index.js';
100
+ import { RESOURCE_COUNT, createArena, createFlushSchedule, keptNodes, maskOf, nodeCount, nodeState, nodeVerb, recordNode, resetArena, schedule, scheduleFlush, scratchFor, } from '../../frame/index.js';
78
101
  import { createFrustum, frustumFromViewProjection } from '../../../math/frustum.js';
79
102
  import { OcclusionBuffer } from '../../occlusion.js';
80
103
  import { boundsVisible } from '../../visibility.js';
@@ -352,20 +375,32 @@ function resolvedStoreOp(multisampled, terminal, allowed) {
352
375
  return multisampled && terminal && allowed ? 'discard' : 'store';
353
376
  }
354
377
  /**
355
- * Clip space differs between the two APIs, in two ways, and both must be corrected.
378
+ * Clip space differs between the two APIs in **one** way, and the other half of `CLIP_CORRECTION`
379
+ * cancels something this repository writes itself.
356
380
  *
357
- * **Y points the other way on screen.** WebGPU's framebuffer origin is the top-left corner
358
- * and OpenGL's is the bottom-left, so a projection built for WebGL2 draws the world upside
359
- * down here. The first frame this backend rendered was a colonnade hanging from the ceiling.
381
+ * **Depth lands in a different range, and that half is a real difference between the APIs.**
382
+ * OpenGL clips z to [-1, 1] and WebGPU to [0, 1], so an uncorrected matrix throws away the near
383
+ * half of the depth buffer and compares what remains against the wrong distances.
360
384
  *
361
- * **Depth lands in a different range.** OpenGL clips z to [-1, 1] and WebGPU to [0, 1], so
362
- * an uncorrected matrix throws away the near half of the depth buffer and compares what
363
- * remains against the wrong distances.
385
+ * **The Y negation cancels one the generator writes, and this comment used to say otherwise.**
386
+ * It claimed the flip was the framebuffer origin: WebGPU's is the top-left and OpenGL's is the
387
+ * bottom-left, so a projection built for WebGL2 "draws the world upside down here". That is not
388
+ * what either viewport transform does — both put clip `y = +1` at the top of the image, because
389
+ * OpenGL measures its window upward from the bottom-left and WebGPU measures its framebuffer
390
+ * downward from the top-left. An uncorrected projection lands the same way up on both.
364
391
  *
365
- * Correcting the matrix rather than the shaders is deliberate: the shaders are generated
366
- * from the GLSL the WebGL2 path uses, and a Y flip written into them would be a difference
367
- * between the backends that no generator could keep honest. Here it is four numbers, applied
368
- * once a frame, in one place.
392
+ * What does flip it is `naga`. Every module in `shaders/generated/` ends its vertex entry point
393
+ * with `gl_Position.y = -gl_Position.y`, which is how that translator adapts a GLSL shader to
394
+ * WebGPU — and the first frame this backend rendered, a colonnade hanging from the ceiling, was
395
+ * *that* line and not the API. So the pair is: the generator negates, this negates back, and the
396
+ * net effect on a generated shader is the depth remap alone. `shaders/generated.test.ts` pins the
397
+ * generator's half so the two cannot drift apart.
398
+ *
399
+ * **Which makes it the wrong matrix for a shader somebody wrote by hand**, since there is no
400
+ * negation to cancel — see `DEPTH_CORRECTION` below, and `PassDevice` for the seam where a
401
+ * contributed pass chooses. Wiring the GPU-driven pipeline's hand-written raster found it: the
402
+ * picture was an exact vertical mirror of the forward path's, and `frontFace: 'cw'` made it draw
403
+ * the right faces of a mirrored world.
369
404
  *
370
405
  * **This one is for a target that gets presented. `SHADOW_CLIP_CORRECTION` is for one that
371
406
  * gets sampled, and they are not the same transform.**
@@ -431,6 +466,36 @@ const CLIP_CORRECTION = new Float32Array([
431
466
  * against, applied by accident. Without the flip it culls what it says it culls, and matches
432
467
  * what `renderer.ts` does through the directional pass.
433
468
  */
469
+ /**
470
+ * `CLIP_CORRECTION` without the Y negation: the depth remap alone, reversed.
471
+ *
472
+ * **What a hand-written WGSL shader wants.** The negation in `CLIP_CORRECTION` is there to cancel
473
+ * the one `naga` writes into every generated vertex entry point; a shader that was not generated
474
+ * carries no such line, so taking that matrix mirrors its picture vertically — and mirrors its
475
+ * triangle winding with it, which is how it reads as a culling problem rather than as a flip.
476
+ *
477
+ * The depth half is not optional and is the reason this is a matrix rather than nothing: OpenGL
478
+ * clips z to [-1, 1] and WebGPU to [0, 1], and the row is `0.5 - 0.5z` rather than `0.5z + 0.5`
479
+ * because `depthConvention.ts` puts the near plane at one.
480
+ */
481
+ const DEPTH_CORRECTION = new Float32Array([
482
+ 1,
483
+ 0,
484
+ 0,
485
+ 0,
486
+ 0,
487
+ 1,
488
+ 0,
489
+ 0,
490
+ 0,
491
+ 0,
492
+ REVERSED_DEPTH ? -0.5 : 0.5,
493
+ 0,
494
+ 0,
495
+ 0,
496
+ 0.5,
497
+ 1,
498
+ ]);
434
499
  const SHADOW_CLIP_CORRECTION = new Float32Array([
435
500
  1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 1,
436
501
  ]);
@@ -652,6 +717,16 @@ const NO_CLIP_PLANE = new Float32Array(4);
652
717
  * a while telling consumers that working effects did nothing. All three are read by
653
718
  * `composite()` now, and each has been compared against WebGL2 on `demo/dev/probe.html`.
654
719
  */
720
+ /**
721
+ * The y negation every generated vertex stage ends with, as a matrix.
722
+ *
723
+ * **A hand-written shader has no such line, so it has to carry it.** `CLIP_CORRECTION` negates y
724
+ * precisely to cancel the one `naga` writes, and the pair's net effect on a generated shader is the
725
+ * depth remap alone — §3 rows 56 and 57. The motion pass has to put its vertices exactly where the
726
+ * scene put them or its depth test admits nothing, so the renderer multiplies this through the
727
+ * matrix the scene drew with and hands over the product.
728
+ */
729
+ const MOTION_CLIP_FLIP = mat4.fromValues(1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
655
730
  export class WebGPURenderer {
656
731
  /**
657
732
  * Real timestamp queries, where the adapter offers them.
@@ -799,8 +874,8 @@ export class WebGPURenderer {
799
874
  perFrameStaging;
800
875
  perFrameFloats;
801
876
  perFrameInts;
802
- /** The slot the open material occupies, or -1 when the next draw must take a new one. */
803
- materialSlot = -1;
877
+ /** The slot the open material occupies, and whether the next draw must take a new one. */
878
+ materials = new MaterialChanges();
804
879
  /*
805
880
  * The frame graph's recording, and the machinery that replays it.
806
881
  *
@@ -846,6 +921,7 @@ export class WebGPURenderer {
846
921
  textBudget = this.budget.line('text draws', MAX_OVERLAYS);
847
922
  sdfTextBudget = this.budget.line('sdf text draws', MAX_OVERLAYS);
848
923
  lineBudget = this.budget.line('line draws', MAX_OVERLAYS);
924
+ panelBudget = this.budget.line('panels', MAX_OVERLAYS);
849
925
  /**
850
926
  * What the frame just drawn asked for, and what it was refused.
851
927
  *
@@ -866,6 +942,12 @@ export class WebGPURenderer {
866
942
  * scatter, plumes and text reached it long before the draw ring was full.
867
943
  */
868
944
  replayScratch = new Int32Array(MAX_DRAWS_PER_FRAME);
945
+ /**
946
+ * The identifier graph's working set, where `quality.identifierGraph` asks for it and not
947
+ * otherwise: a renderer scheduling by masks carries none of it. Sized with the arena, and grown
948
+ * by `scheduleFlush` the way the arena grows.
949
+ */
950
+ flushSchedule;
869
951
  scheduledPasses = Array.from({ length: 64 }, () => ({
870
952
  writes: 0,
871
953
  first: 0,
@@ -1127,6 +1209,26 @@ export class WebGPURenderer {
1127
1209
  bloomGroups = [];
1128
1210
  frameWidth = 1;
1129
1211
  frameHeight = 1;
1212
+ /**
1213
+ * What the scene is drawn at, which is the drawing buffer unless a reconstruction is enlarging it.
1214
+ *
1215
+ * **Everything before the resolve is this size and everything after it is the drawing buffer's.**
1216
+ * The scene colour and its multisampled twin, the depth, the refraction snapshot, the resolved
1217
+ * depth, the occlusion pair, the medium, the translucent buffers, the reflection march, the
1218
+ * decals, the temporal history and the frame pass's own viewport are all before it; the composite
1219
+ * and the overlay are after it, and they are what the viewer's pixels are counted in.
1220
+ *
1221
+ * **The composite is what enlarges the picture, and it does so by existing.** It draws a
1222
+ * full-screen triangle into the swap chain sampling the scene by `uv` through a linear sampler, so
1223
+ * a smaller scene target is magnified by that sample and nothing else changes — which is why this
1224
+ * needs `screenEffects` and is refused without it: with no composite the world draws straight into
1225
+ * the swap chain and there is nothing to enlarge from.
1226
+ *
1227
+ * Equal to the drawing buffer whenever `quality.reconstruction` is 0, which is the default, and
1228
+ * `recon/frameSizes.ts` is the one place the two are related.
1229
+ */
1230
+ renderWidth = 0;
1231
+ renderHeight = 0;
1130
1232
  /** The view-projection this frame was drawn with, for the reprojection motion blur needs. */
1131
1233
  previousViewProj = new Float32Array(16);
1132
1234
  reprojection = new Float32Array(16);
@@ -1220,6 +1322,94 @@ export class WebGPURenderer {
1220
1322
  temporalJitterX = 0;
1221
1323
  temporalJitterY = 0;
1222
1324
  temporalJittering = false;
1325
+ /**
1326
+ * Whether this frame is being reconstructed, settled once in `bindMeshPass`.
1327
+ *
1328
+ * It is not `quality.reconstruction > 0` on its own: a mirror pass is skipped for the same reason
1329
+ * the temporal jitter skips one — a reflection is not the picture the history holds — and without
1330
+ * a composite there is nothing to enlarge the render from.
1331
+ */
1332
+ reconstructing = false;
1333
+ /** Said once rather than every frame, as the translucent set's own refusal is. */
1334
+ reconMultisampleSaid = false;
1335
+ reconFrameIndex = -1;
1336
+ reconJitter = new Float32Array(2);
1337
+ reconPreviousJitter = new Float32Array(2);
1338
+ /**
1339
+ * `reconJitter` as a fraction of the clip square, which is what a contributed pass is handed.
1340
+ * See `PrepareContext.jitter`; zero on a frame that is not reconstructed.
1341
+ */
1342
+ passJitter = new Float32Array(2);
1343
+ /**
1344
+ * The reconstruction's own targets, allocated only while a consumer asks for one.
1345
+ *
1346
+ * **Two histories rather than one**, for the reason the temporal resolve keeps two: a dispatch
1347
+ * cannot read the texture it is writing. The shown picture is a third, because the history holds
1348
+ * the *unsharpened* result — sharpening into the history would sharpen a sharpened picture every
1349
+ * frame, which is the halo the reference's own clamp exists to prevent, compounded.
1350
+ *
1351
+ * The motion target is render size and carries a drawn object's own motion where something wrote
1352
+ * one. **Nothing writes it yet**, so it is cleared every frame and every pixel takes the camera's
1353
+ * motion, which the resolve derives from the depth and the reprojection. Static geometry is
1354
+ * exact that way and a moving object ghosts until the motion pass lands.
1355
+ */
1356
+ reconHistories = [null, null];
1357
+ reconHistoryViews = [null, null];
1358
+ reconWrite = 0;
1359
+ reconHasHistory = false;
1360
+ reconShown = null;
1361
+ reconShownView = null;
1362
+ reconMotion = null;
1363
+ reconMotionView = null;
1364
+ reconPreviousDepth = null;
1365
+ reconPreviousDepthView = null;
1366
+ reconParams = null;
1367
+ reconStaging = new ArrayBuffer(RECON_PARAM_FLOATS * 4);
1368
+ reconFloats = new Float32Array(this.reconStaging);
1369
+ reconInts = new Uint32Array(this.reconStaging);
1370
+ reconLayout = null;
1371
+ reconResolvePipeline = null;
1372
+ reconSharpenPipeline = null;
1373
+ reconGroups = [null, null];
1374
+ reconSharpenGroups = [null, null];
1375
+ reconRushGroup = null;
1376
+ /**
1377
+ * What the motion pass will draw, recorded by `drawMesh` and spent once a frame.
1378
+ *
1379
+ * A parallel array of meshes and one flat matrix buffer rather than an array of objects, because
1380
+ * this is written on the frame's hot path and the house rule about allocating there binds it: the
1381
+ * matrices are copied into a buffer that only ever grows, and the count is what resets.
1382
+ */
1383
+ motionMeshes = [];
1384
+ motionMatrices = new Float32Array(0);
1385
+ motionCount = 0;
1386
+ motionLayout = null;
1387
+ /**
1388
+ * One pipeline per vertex stride, because a stride is a property of the *mesh*.
1389
+ *
1390
+ * Which optional attributes a mesh supplied decides how far apart its positions are, and this
1391
+ * pass reads nothing but positions — so two meshes drawn by the same shader still need two
1392
+ * vertex layouts. A map rather than a cache key on the pipeline cache: there is one shader here
1393
+ * and a handful of strides in any scene.
1394
+ */
1395
+ motionPipelines = new Map();
1396
+ motionModule = null;
1397
+ motionPipelineLayout = null;
1398
+ motionFrameBuffer = null;
1399
+ motionDrawBuffer = null;
1400
+ motionDrawCapacity = 0;
1401
+ motionGroup = null;
1402
+ motionFrameStaging = new Float32Array(MOTION_FRAME_FLOATS);
1403
+ motionDrawStaging = new Uint8Array(0);
1404
+ /** The scene's own clip transform with the generated shaders' y negation folded in. */
1405
+ motionRaster = mat4.create();
1406
+ reconSizeKey = '';
1407
+ /** Last frame's raw view-projection and its inverse, and the two eyes. */
1408
+ reconPreviousRawViewProj = mat4.create();
1409
+ reconInverseViewProj = mat4.create();
1410
+ reconPreviousInverseViewProj = mat4.create();
1411
+ reconEye = new Float32Array(3);
1412
+ reconPreviousEye = new Float32Array(3);
1223
1413
  temporalHistoryUsable = false;
1224
1414
  aoStaging = new ArrayBuffer(AO_FRAG_SIZE);
1225
1415
  aoFloats = new Float32Array(this.aoStaging);
@@ -1258,6 +1448,22 @@ export class WebGPURenderer {
1258
1448
  get hasComposite() {
1259
1449
  return this.quality.screenEffects;
1260
1450
  }
1451
+ /**
1452
+ * Whether this renderer is set up to reconstruct, asked in one place by everything that cares.
1453
+ *
1454
+ * **One condition and not three**, which §3 row 132 is the lesson of: the frame's flag, the size
1455
+ * the targets are built at and the size the world is drawn at all turn on the same question, and
1456
+ * three copies of it are three lines no test can fail — each covering the others, so breaking any
1457
+ * one of them changes nothing anybody can see.
1458
+ *
1459
+ * A composite, because the composite is what enlarges the render; one sample, because the motion
1460
+ * pass draws against the depth the scene left and every attachment in a pass must agree about its
1461
+ * sample count — `RECONSTRUCTION_MULTISAMPLE_REFUSAL` says the rest. Whether a *frame* is
1462
+ * reconstructed adds one more question, the mirror's, and that one belongs to the frame.
1463
+ */
1464
+ get reconstructionWanted() {
1465
+ return this.quality.reconstruction > 0 && this.hasComposite && this.samples === 1;
1466
+ }
1261
1467
  /** What a frame pass writes into: the scene target where there is one, the swap chain else. */
1262
1468
  /**
1263
1469
  * This frame's swap-chain view, taken on first use rather than at `beginFrame`.
@@ -1359,6 +1565,31 @@ export class WebGPURenderer {
1359
1565
  * the frame has drawn, so it cannot run until the opaque world is finished. See `DecalQueue`.
1360
1566
  */
1361
1567
  decalQueue = new DecalQueue();
1568
+ /** The fields this frame declared, composed at `endFrame`. See `addDistanceField`. */
1569
+ distanceFields = new DistanceFieldScene();
1570
+ /**
1571
+ * The composition of them, built at the first frame that has both the flag and a field.
1572
+ *
1573
+ * **Not at construction**, because it is 1.4 MB of device buffers and a compute pipeline, and a
1574
+ * profile with `indirectLight` on is entitled to declare no fields at all — a scene that lights
1575
+ * itself entirely from a baked probe grid, for one. Nothing is allocated until something would
1576
+ * be composed.
1577
+ */
1578
+ fieldComposer = null;
1579
+ /**
1580
+ * The probes' own bake, built at the first frame that has the flag, a field and an array.
1581
+ *
1582
+ * Not at construction, for `fieldComposer`'s reason: it is two compute pipelines, a render
1583
+ * pipeline and a handful of buffers, and a profile with the flag on may never declare a field.
1584
+ */
1585
+ probeBaker = null;
1586
+ /** Which array the baker's views were built against, so a replacement is noticed. */
1587
+ bakerArray = null;
1588
+ /** Reused, because deriving a grid from the declared fields runs in the frame path. */
1589
+ fieldBoundsMin = new Float32Array(3);
1590
+ fieldBoundsMax = new Float32Array(3);
1591
+ /** Said once rather than every frame, as the other refusals are. */
1592
+ indirectGridRefused = false;
1362
1593
  decalLayout;
1363
1594
  decalUniforms;
1364
1595
  decalStaging = new ArrayBuffer(DECAL_SLOT * MAX_DRAWN_DECALS);
@@ -1394,8 +1625,21 @@ export class WebGPURenderer {
1394
1625
  decalScissorRect = new Int32Array(4);
1395
1626
  oitResolveLayout;
1396
1627
  oitResolveGroup = null;
1397
- /** Reused, because the frame loop may not allocate. Mirrors the WebGL2 backend's own. */
1398
- oitReplayOptions = { lit: true, fog: true, toneMapped: true, depthWrite: false, depthLayer: 0, tint: null };
1628
+ /**
1629
+ * Reused, because the frame loop may not allocate. Mirrors the WebGL2 backend's own, which it
1630
+ * had stopped doing: the other one carried a pane's refraction and this one did not.
1631
+ */
1632
+ oitReplayOptions = {
1633
+ lit: true,
1634
+ fog: true,
1635
+ toneMapped: true,
1636
+ depthWrite: false,
1637
+ depthLayer: 0,
1638
+ tint: null,
1639
+ refraction: 0,
1640
+ refractTint: undefined,
1641
+ thicknessM: 0,
1642
+ };
1399
1643
  reflectionPassActive = false;
1400
1644
  /** Said once: a mirror asked for outside a frame is a call-order fault, not a device limit. */
1401
1645
  saidMirrorOutsideFrame = false;
@@ -1728,16 +1972,27 @@ export class WebGPURenderer {
1728
1972
  createSurfaceTexture(source, options = {}) {
1729
1973
  return new GpuSurfaceTexture(this.surface.device, this.pipelines, source, options);
1730
1974
  }
1975
+ /**
1976
+ * GPU textures an `updateSurfaceTexture` replaced, destroyed when the next frame begins.
1977
+ *
1978
+ * Not at once, because a draw recorded earlier in the frame the update landed in may still read
1979
+ * one, and a destroyed texture in a submit loses the whole command buffer. Every encoder a frame
1980
+ * opens is submitted by the time the next one begins.
1981
+ */
1982
+ retiredTextures = [];
1731
1983
  updateSurfaceTexture(texture, source) {
1732
1984
  if (this.surface.lost)
1733
1985
  return;
1734
- texture.update(source);
1986
+ const replaced = texture.update(source);
1987
+ if (replaced === null)
1988
+ return;
1989
+ /* A new size is a new view, so every group holding the old one is dropped — see below. */
1990
+ this.retiredTextures.push(replaced);
1991
+ this.forgetBindingsOf(texture);
1735
1992
  }
1736
1993
  disposeSurfaceTexture(texture) {
1737
1994
  if (this.surface.lost)
1738
1995
  return;
1739
- /* Same reasoning, for whichever SDF labels were bound against this atlas. */
1740
- this.sdfTextBindGroups.delete(texture);
1741
1996
  if (this.albedo === texture) {
1742
1997
  this.albedo = null;
1743
1998
  this.bindGroup = this.blankAlbedoBindGroup;
@@ -1766,6 +2021,18 @@ export class WebGPURenderer {
1766
2021
  this.ormMap = null;
1767
2022
  if (this.emissiveMap === texture)
1768
2023
  this.emissiveMap = null;
2024
+ this.forgetBindingsOf(texture);
2025
+ texture.dispose();
2026
+ }
2027
+ /**
2028
+ * Drop every bind group that may hold `texture`'s view, and rebuild the ones bound now.
2029
+ *
2030
+ * For a texture going away and for one whose view has been replaced, which are the same problem:
2031
+ * a group holding the old view hands a destroyed texture to the next draw that asks.
2032
+ */
2033
+ forgetBindingsOf(texture) {
2034
+ /* Same reasoning, for whichever SDF labels were bound against this atlas. */
2035
+ this.sdfTextBindGroups.delete(texture);
1769
2036
  this.flatBindGroups.clear();
1770
2037
  /*
1771
2038
  * The blank one is built with the albedo held aside, because `buildFlatBindGroup` reads
@@ -1777,7 +2044,6 @@ export class WebGPURenderer {
1777
2044
  this.blankAlbedoBindGroup = this.buildFlatBindGroup();
1778
2045
  this.albedo = held;
1779
2046
  this.bindGroup = this.flatGroupForMaps(held, this.normalMap, this.ormMap, this.emissiveMap);
1780
- texture.dispose();
1781
2047
  }
1782
2048
  /**
1783
2049
  * Choose the surface texture the following `drawMesh` calls sample, or null for none.
@@ -1932,7 +2198,7 @@ export class WebGPURenderer {
1932
2198
  if (this.surface.lost)
1933
2199
  return;
1934
2200
  write(this.perFrameFloats, this.perFrameInts);
1935
- this.materialSlot = -1;
2201
+ this.materials.dirty();
1936
2202
  }
1937
2203
  materialField(name) {
1938
2204
  return (this.fragment.fields[name]?.offset ?? 0) / 4;
@@ -2014,8 +2280,17 @@ export class WebGPURenderer {
2014
2280
  return;
2015
2281
  const vertexSlot = this.panelVerts.allocate();
2016
2282
  const fragmentSlot = this.panelFrags.allocate();
2017
- if (vertexSlot === null || fragmentSlot === null)
2283
+ if (vertexSlot === null || fragmentSlot === null) {
2284
+ /*
2285
+ * Said, as its text and line siblings say it: this returned without a word until 2026-09-19,
2286
+ * so an interface of panels lost everything past its sixty-fourth here and nothing on WebGL2.
2287
+ */
2288
+ if (!this.warnedPanelsFull) {
2289
+ this.warnedPanelsFull = true;
2290
+ console.warn(`WebGPU: more than ${MAX_OVERLAYS} panels in a frame; the rest are skipped. renderer.frameBudget names the line and the count.`);
2291
+ }
2018
2292
  return;
2293
+ }
2019
2294
  const at = (name) => PANEL_VERT_FIELDS[name]?.offset ?? 0;
2020
2295
  this.panelVerts.writeFloats(vertexSlot, at('uRect'), [
2021
2296
  rect.left,
@@ -2054,6 +2329,8 @@ export class WebGPURenderer {
2054
2329
  pass.draw(PANEL_VERTEX_COUNT);
2055
2330
  }
2056
2331
  }
2332
+ /** Said once rather than every frame, for the reason `warnedFull` gives. */
2333
+ warnedPanelsFull = false;
2057
2334
  /** A 3D text object. See `textPass.ts`; the layout it holds is shared with WebGL2. */
2058
2335
  createText() {
2059
2336
  return new GpuText(this.surface.device);
@@ -2981,6 +3258,11 @@ export class WebGPURenderer {
2981
3258
  it ends the current pass to copy the depth out and opens another in its place. */
2982
3259
  if (!this.canDraw())
2983
3260
  return;
3261
+ /* Geometry that has not all arrived is not drawn, which is the surface's contract and what
3262
+ every other mesh verb here and the other backend's volume already kept. This one drew a
3263
+ beam from a buffer still filling, and counted it, until 2026-09-19. See `Mesh.complete`. */
3264
+ if (!mesh.complete)
3265
+ return;
2984
3266
  /* At zero it draws nothing rather than adding black, so a caller may keep the call in. That
2985
3267
  now covers a clear night too: a beam with no medium to light is not drawn at all. */
2986
3268
  const shown = options.medium === undefined
@@ -4045,6 +4327,41 @@ export class WebGPURenderer {
4045
4327
  get canvas() {
4046
4328
  return this.surface.canvas;
4047
4329
  }
4330
+ /**
4331
+ * The size the world is drawn at this frame, which is **not** the canvas when a reconstruction
4332
+ * is enlarging it.
4333
+ *
4334
+ * **A contributed pass that fills a target of its own must size it from here.** The GPU-driven
4335
+ * rig sized its own targets from `canvas.width` and drew a full drawing-buffer picture into a
4336
+ * scene target two thirds as wide: the frame pass clipped it to the top-left corner and the
4337
+ * composite magnified that corner back over the screen, which is a plausible-looking picture of
4338
+ * the wrong part of the world. The device raises nothing — a viewport smaller than the thing
4339
+ * drawn is legal — so there is no failure to see, only a scene that has moved.
4340
+ *
4341
+ * Valid from `beginFrame`; before the first frame it is the canvas, which is what a pass sizing
4342
+ * itself at registration wants.
4343
+ */
4344
+ get sceneWidth() {
4345
+ return this.sceneSize().width;
4346
+ }
4347
+ get sceneHeight() {
4348
+ return this.sceneSize().height;
4349
+ }
4350
+ /**
4351
+ * The render size this frame will use, computed rather than remembered.
4352
+ *
4353
+ * **Computed, because a pass asks before the frame it is asking about.** `GpuDrivenPass.resize`
4354
+ * runs before `beginFrame` — that is when `prepare` records the pipeline — so a stored field
4355
+ * would answer with the previous frame's size, which is right every frame but the one where it
4356
+ * changed. The first frame after a reconstruction is switched on is exactly the frame that
4357
+ * matters.
4358
+ */
4359
+ sceneSize() {
4360
+ const { canvas } = this.surface;
4361
+ return this.reconstructionWanted
4362
+ ? reconRenderSize(canvas.width, canvas.height, this.quality.reconstruction)
4363
+ : { width: Math.max(1, canvas.width), height: Math.max(1, canvas.height) };
4364
+ }
4048
4365
  /**
4049
4366
  * Whether the device has gone.
4050
4367
  *
@@ -4156,6 +4473,10 @@ export class WebGPURenderer {
4156
4473
  quality = this.quality;
4157
4474
  this.maxDrawingBufferPixels = quality.maxDrawingBufferPixels;
4158
4475
  this.exposure = quality.outputExposure;
4476
+ this.flushSchedule =
4477
+ quality.frameGraph && quality.identifierGraph
4478
+ ? createFlushSchedule(MAX_DRAWS_PER_FRAME)
4479
+ : null;
4159
4480
  const { device } = surface;
4160
4481
  /* Off unless a consumer asked: see `RenderQuality.gpuTiming`. A timer that can
4161
4482
  invalidate a command buffer must not be something everybody pays for. */
@@ -4903,7 +5224,8 @@ export class WebGPURenderer {
4903
5224
  */
4904
5225
  this.panelLayout = createPanelBindGroupLayout(device);
4905
5226
  this.panelCorners = createPanelCorners(device);
4906
- this.panelVerts = new UniformRing(device, PANEL_VERT_SIZE, MAX_OVERLAYS, USAGE_UNIFORM_DST, 'panel.vertRing');
5227
+ /* The budget on one of the pair only: a panel is one ask, whichever ring runs out. */
5228
+ this.panelVerts = new UniformRing(device, PANEL_VERT_SIZE, MAX_OVERLAYS, USAGE_UNIFORM_DST, 'panel.vertRing', this.panelBudget);
4907
5229
  this.panelFrags = new UniformRing(device, PANEL_FRAG_SIZE, MAX_OVERLAYS, USAGE_UNIFORM_DST, 'panel.fragRing');
4908
5230
  this.panelBindGroup = createPanelBindGroup(device, this.panelLayout, this.panelVerts.buffer, this.panelFrags.buffer);
4909
5231
  this.textLayout = createTextBindGroupLayout(device);
@@ -5040,6 +5362,10 @@ export class WebGPURenderer {
5040
5362
  * uncaught exceptions a second in somebody's error reporting.
5041
5363
  */
5042
5364
  beginFrame(clearColor) {
5365
+ /* Whatever an update replaced during the last frame; nothing recorded can read it now. */
5366
+ for (const texture of this.retiredTextures)
5367
+ texture.destroy();
5368
+ this.retiredTextures.length = 0;
5043
5369
  /*
5044
5370
  * **Decided here and held for the frame**, so a draw cannot be recorded under one rule and
5045
5371
  * replayed under another. Multisampling is excluded: the two buffers are single-sampled and a
@@ -5049,6 +5375,8 @@ export class WebGPURenderer {
5049
5375
  this.translucentQueue.reset();
5050
5376
  this.decalQueue.reset();
5051
5377
  this.reflectionQueue.reset();
5378
+ /* Redeclared every frame, so a field a consumer stops declaring stops lighting. */
5379
+ this.distanceFields.reset();
5052
5380
  this.oitActive =
5053
5381
  this.quality.orderIndependent && this.quality.screenEffects && this.samples === 1;
5054
5382
  /* Read off the line above rather than restating its last term: with the other two true, the
@@ -5122,7 +5450,9 @@ export class WebGPURenderer {
5122
5450
  this.lineFrags.reset();
5123
5451
  /* The material ring, and the open slot with it: last frame's slots are gone. */
5124
5452
  this.perFrame.reset();
5125
- this.materialSlot = -1;
5453
+ this.materials.dirty();
5454
+ /* Spent by `runMotion` at the end of the last frame; a frame states its movers afresh. */
5455
+ this.motionCount = 0;
5126
5456
  /*
5127
5457
  * The canvas rather than the swap chain's texture, and the difference is that reading this
5128
5458
  * does not *acquire* anything. See `swapView`.
@@ -5133,8 +5463,17 @@ export class WebGPURenderer {
5133
5463
  * without the target it draws into, and a scene is free never to call `resize`.
5134
5464
  */
5135
5465
  const { canvas } = this.surface;
5136
- this.ensureDepth(canvas.width, canvas.height);
5137
- this.ensureComposite(canvas.width, canvas.height);
5466
+ /*
5467
+ * **The render size first, because every target below is sized from it.** Off — and without a
5468
+ * composite to enlarge from — it is the drawing buffer exactly, which is what makes a frame
5469
+ * with no reconstruction byte-for-byte the frame this renderer has always drawn.
5470
+ */
5471
+ const render = this.sceneSize();
5472
+ this.renderWidth = render.width;
5473
+ this.renderHeight = render.height;
5474
+ this.ensureDepth(render.width, render.height);
5475
+ this.ensureComposite(render.width, render.height);
5476
+ this.ensureReconstruction(render.width, render.height, Math.max(1, canvas.width), Math.max(1, canvas.height));
5138
5477
  /*
5139
5478
  * **Not acquired here, and that is the fix for a frame flashing black.**
5140
5479
  *
@@ -5152,6 +5491,7 @@ export class WebGPURenderer {
5152
5491
  */
5153
5492
  this.frameSwapView = null;
5154
5493
  this.encoder = this.surface.device.createCommandEncoder();
5494
+ this.settleReconJitter();
5155
5495
  /*
5156
5496
  * Passes that own a target fill it here — after the encoder exists and before anything opens
5157
5497
  * the frame's render pass, including the eager path twenty lines below. See
@@ -5418,6 +5758,8 @@ export class WebGPURenderer {
5418
5758
  */
5419
5759
  if (!this.reflectionPassActive) {
5420
5760
  mat4.copy(this.frameRawViewProj, camera.viewProjection);
5761
+ /* The eye in world space, which the resolve's normals are turned to face. */
5762
+ this.reconEye.set(camera.position);
5421
5763
  this.frameEye.set(camera.position);
5422
5764
  /* Held for the medium's march, which runs long after every draw and needs the sun the
5423
5765
  frame was lit by. Referenced rather than copied, exactly as `renderer.ts` holds its
@@ -5431,16 +5773,53 @@ export class WebGPURenderer {
5431
5773
  * is skipped by the same guard that skips it for motion blur: it is not the picture the
5432
5774
  * history holds, and jittering it would resolve a reflection against the viewer's frame.
5433
5775
  */
5776
+ /*
5777
+ * **Reconstruction jitters instead of the temporal resolve, never as well as it.** Both
5778
+ * accumulate a history out of a jittered sequence, and running them together would resolve the
5779
+ * frame twice — once at the render size and again at the output size, against a history whose
5780
+ * own frames had already been mixed. So this is a choice rather than a pair, reconstruction
5781
+ * wins where a consumer asked for both, and `temporalResolve` is skipped while it runs.
5782
+ *
5783
+ * Its sequence is its own: `reconJitterPhases` takes eight positions for every output pixel a
5784
+ * render pixel covers, where the temporal resolve's eight are enough at one to one.
5785
+ */
5786
+ this.reconstructing = this.reconstructionWanted && !this.reflectionPassActive;
5787
+ if (this.quality.reconstruction > 0 && this.samples > 1 && !this.reconMultisampleSaid) {
5788
+ this.reconMultisampleSaid = true;
5789
+ console.warn(RECONSTRUCTION_MULTISAMPLE_REFUSAL);
5790
+ }
5791
+ if (this.reconstructing) {
5792
+ /*
5793
+ * The offset `settleReconJitter` chose for the frame at `beginFrame`, applied here and not
5794
+ * advanced here: a frame binding its mesh pass twice is one frame of the sequence.
5795
+ *
5796
+ * **The y offset is negated on the way in, and that is not a preference.** This shifts the
5797
+ * *corrected* matrix, whose y `CLIP_CORRECTION` has already negated so that the negation
5798
+ * every generated vertex stage ends with cancels out — §3 row 56. So an offset added here
5799
+ * arrives on screen with its sign reversed, while x, which nothing negates, arrives as
5800
+ * given. The temporal resolve cannot see this because its shader never reads the offset; the
5801
+ * reconstruction reads it at every texel it unprojects, and a sign error there moves every
5802
+ * sample two jitters away from where the depth says it is.
5803
+ */
5804
+ this.viewProj = jitterProjection(this.jitteredViewProj, this.correctedViewProj, this.reconJitter[0], -this.reconJitter[1], this.renderWidth, this.renderHeight);
5805
+ this.temporalJittering = false;
5806
+ this.temporalHistoryUsable = false;
5807
+ }
5434
5808
  this.temporalJittering =
5435
- this.quality.temporalAa && this.quality.screenEffects && !this.reflectionPassActive;
5809
+ !this.reconstructing &&
5810
+ this.quality.temporalAa &&
5811
+ this.quality.screenEffects &&
5812
+ !this.reflectionPassActive;
5436
5813
  if (this.temporalJittering) {
5437
- this.temporalHistoryUsable = this.temporalHistory.openFrame(Math.max(1, this.surface.canvas.width), Math.max(1, this.surface.canvas.height));
5814
+ /* The scene's size, not the drawing buffer's: the history is a picture of the render. */
5815
+ this.temporalHistoryUsable = this.temporalHistory.openFrame(this.renderWidth, this.renderHeight);
5438
5816
  const [jx, jy] = jitterOffset(this.temporalHistory.frameIndex);
5439
5817
  this.temporalJitterX = jx;
5440
5818
  this.temporalJitterY = jy;
5441
- this.viewProj = jitterProjection(this.jitteredViewProj, this.correctedViewProj, jx, jy, Math.max(1, this.surface.canvas.width), Math.max(1, this.surface.canvas.height));
5819
+ /* A jitter is half a *render* texel, which is what the sequence is spread over. */
5820
+ this.viewProj = jitterProjection(this.jitteredViewProj, this.correctedViewProj, jx, jy, this.renderWidth, this.renderHeight);
5442
5821
  }
5443
- else {
5822
+ else if (!this.reconstructing) {
5444
5823
  this.temporalHistoryUsable = false;
5445
5824
  this.viewProj = this.correctedViewProj;
5446
5825
  }
@@ -5692,6 +6071,8 @@ export class WebGPURenderer {
5692
6071
  * face size, and the array's stops one level below the diffuse term.
5693
6072
  */
5694
6073
  f[at('uEnvironmentEdge')] = this.probeEdge;
6074
+ /* Off until the indirect pass fills the moment layers. See `quality.indirectLight`. */
6075
+ f[at('uProbeVisibilityEnabled')] = 0;
5695
6076
  f[at('uEnvironmentIrradianceLevel')] = irradianceLevelFor(this.probeEdge);
5696
6077
  f[at('uProbeGridAmbient')] = usable && this.probeAmbient ? 1 : 0;
5697
6078
  }
@@ -5881,7 +6262,7 @@ export class WebGPURenderer {
5881
6262
  * block rather than uploading it: the next draw takes the slot. `renderer.ts` resets the
5882
6263
  * same terms here, which is why a pass cannot inherit a material from the one before it.
5883
6264
  */
5884
- this.materialSlot = -1;
6265
+ this.materials.dirty();
5885
6266
  /* A pass starts with no material, exactly as `renderer.ts` resets these terms. */
5886
6267
  this.albedo = null;
5887
6268
  this.bindGroup = this.blankAlbedoBindGroup;
@@ -5896,8 +6277,8 @@ export class WebGPURenderer {
5896
6277
  * each, which is what it asked for.
5897
6278
  */
5898
6279
  materialSlotForDraw() {
5899
- if (this.materialSlot >= 0)
5900
- return this.materialSlot;
6280
+ if (this.materials.open)
6281
+ return this.materials.slot;
5901
6282
  const slot = this.perFrame.allocate();
5902
6283
  if (slot === null) {
5903
6284
  if (!this.warnedMaterialsFull) {
@@ -5907,7 +6288,7 @@ export class WebGPURenderer {
5907
6288
  return null;
5908
6289
  }
5909
6290
  this.perFrame.writeBlock(slot, this.perFrameInts);
5910
- this.materialSlot = slot;
6291
+ this.materials.slot = slot;
5911
6292
  return slot;
5912
6293
  }
5913
6294
  /** Said once rather than every frame, for the reason `warnedFull` gives. */
@@ -5926,7 +6307,9 @@ export class WebGPURenderer {
5926
6307
  * surfaces should win got the answer on WebGL2 and a per-pixel coin toss here — with nothing
5927
6308
  * failing and nothing in the parity ledger saying so.
5928
6309
  */
5929
- drawMesh(mesh, model, depthLayer = 0, tint = null) {
6310
+ drawMesh(mesh, model, depthLayer = 0, tint = null,
6311
+ /** Where it was last frame. See `RendererApi.drawMesh`, and `runMotion` for what reads it. */
6312
+ previousModel = null) {
5930
6313
  /*
5931
6314
  * Skipped where the flag asks and the bounds say so.
5932
6315
  *
@@ -5947,6 +6330,13 @@ export class WebGPURenderer {
5947
6330
  */
5948
6331
  if (this.quality.cullDraws && this.occluded(mesh.bounds, model))
5949
6332
  return;
6333
+ /*
6334
+ * Recorded after the two culls rather than before: a mesh the frame does not draw writes no
6335
+ * depth, so a motion pass drawing it would either be depth-rejected everywhere — wasted — or,
6336
+ * where it happened to pass, write motion for a surface no pixel of the frame belongs to.
6337
+ */
6338
+ if (previousModel !== null)
6339
+ this.recordMotion(mesh, model, previousModel);
5950
6340
  this.submitMesh(mesh, model, tint, 1, false, { depthLayer });
5951
6341
  }
5952
6342
  /**
@@ -6115,7 +6505,6 @@ export class WebGPURenderer {
6115
6505
  /* Material state exactly like `uOpacity`, dirtied for the accumulation pass and restored
6116
6506
  below — the revealage pass draws with it at zero and its colour multiplied away. */
6117
6507
  this.perFrameFloats[this.materialField('uOitWeighted')] = this.oitMode === 'accum' ? 1 : 0;
6118
- this.materialSlot = -1;
6119
6508
  }
6120
6509
  /* Same shape as `dimmed`: a slot is only worth taking when either differs from the default
6121
6510
  `bindMeshPass` already wrote, and both are put back together once the draw is submitted. */
@@ -6128,13 +6517,11 @@ export class WebGPURenderer {
6128
6517
  this.perFrameInts[this.materialField('uLightingEnabled')] = 0;
6129
6518
  if (!fog)
6130
6519
  this.perFrameInts[this.materialField('uFogEnabled')] = 0;
6131
- this.materialSlot = -1;
6132
6520
  }
6133
6521
  /* `1` is sRGB alone: the conversion without the curve. `Math.min` rather than a literal, so
6134
6522
  a renderer asked for `none` stays at none. See `TranslucentMeshOptions.toneMapped`. */
6135
6523
  if (!toneMapped) {
6136
6524
  this.perFrameInts[this.materialField('uOutputTransform')] = Math.min(this.gradeCode(), 1);
6137
- this.materialSlot = -1;
6138
6525
  }
6139
6526
  /*
6140
6527
  * **Refraction, and the snapshot it reads is taken here or the draw does not refract.**
@@ -6155,8 +6542,12 @@ export class WebGPURenderer {
6155
6542
  this.perFrameFloats[tintAt + 1] = refractTint?.[1] ?? 1;
6156
6543
  this.perFrameFloats[tintAt + 2] = refractTint?.[2] ?? 1;
6157
6544
  this.perFrameFloats[this.materialField('uRefractThickness')] = options.thicknessM ?? 0;
6158
- this.materialSlot = -1;
6159
6545
  }
6546
+ /* Taken for itself when any of those differs from the pass, and put back below: the rule both
6547
+ backends count material changes by. See `materialChanges.ts`. */
6548
+ const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting });
6549
+ if (own)
6550
+ this.materials.dirty();
6160
6551
  const base = mesh.key ?? 'flat:s0:u0';
6161
6552
  /*
6162
6553
  * A rigged mesh takes the skinned vertex variant, an unrigged one the plain variant, and the
@@ -6274,25 +6665,23 @@ export class WebGPURenderer {
6274
6665
  if (dimmed) {
6275
6666
  this.perFrameFloats[this.materialField('uOpacity')] = 1;
6276
6667
  this.perFrameFloats[this.materialField('uOitWeighted')] = 0;
6277
- this.materialSlot = -1;
6278
6668
  }
6279
6669
  if (unlitOrUnfogged) {
6280
6670
  if (!lit)
6281
6671
  this.perFrameInts[this.materialField('uLightingEnabled')] = 1;
6282
6672
  if (!fog)
6283
6673
  this.perFrameInts[this.materialField('uFogEnabled')] = 1;
6284
- this.materialSlot = -1;
6285
6674
  }
6286
6675
  if (refracting) {
6287
6676
  /* Back to off for every other draw in the frame, exactly as `uOpacity` is: this scratch is
6288
6677
  shared, so a strength left set is worn by everything drawn after it. */
6289
6678
  this.perFrameFloats[this.materialField('uRefractStrength')] = 0;
6290
- this.materialSlot = -1;
6291
6679
  }
6292
6680
  if (!toneMapped) {
6293
6681
  this.perFrameInts[this.materialField('uOutputTransform')] = this.gradeCode();
6294
- this.materialSlot = -1;
6295
6682
  }
6683
+ if (own)
6684
+ this.materials.dirty();
6296
6685
  }
6297
6686
  /** Release geometry. */
6298
6687
  disposeMesh(mesh) {
@@ -6398,7 +6787,6 @@ export class WebGPURenderer {
6398
6787
  const dimmed = opacity < 1;
6399
6788
  if (dimmed) {
6400
6789
  this.perFrameFloats[this.materialField('uOpacity')] = opacity;
6401
- this.materialSlot = -1;
6402
6790
  }
6403
6791
  const lit = options.lit ?? true;
6404
6792
  const fog = options.fog ?? true;
@@ -6409,14 +6797,16 @@ export class WebGPURenderer {
6409
6797
  this.perFrameInts[this.materialField('uLightingEnabled')] = 0;
6410
6798
  if (!fog)
6411
6799
  this.perFrameInts[this.materialField('uFogEnabled')] = 0;
6412
- this.materialSlot = -1;
6413
6800
  }
6414
6801
  if (!toneMapped) {
6415
6802
  this.perFrameInts[this.materialField('uOutputTransform')] = Math.min(this.gradeCode(), 1);
6416
- this.materialSlot = -1;
6417
6803
  }
6418
6804
  const depthWrite = options.depthWrite ?? true;
6419
6805
  const layer = Math.min(Math.max(Math.round(options.depthLayer ?? 0), 0), MAX_DEPTH_LAYER);
6806
+ /* An instanced draw does not refract. See `materialChanges.ts`. */
6807
+ const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting: false });
6808
+ if (own)
6809
+ this.materials.dirty();
6420
6810
  const base = mesh.key ?? 'flat:s0:u0';
6421
6811
  /* `|inst` in the key, for the reason every other suffix is there: an instanced pipeline binds
6422
6812
  a third vertex buffer and a different vertex module, and sharing a cache entry with the
@@ -6472,17 +6862,16 @@ export class WebGPURenderer {
6472
6862
  past the restore leaves the rest of the frame unlit, unfogged or undimmed. */
6473
6863
  if (dimmed) {
6474
6864
  this.perFrameFloats[this.materialField('uOpacity')] = 1;
6475
- this.materialSlot = -1;
6476
6865
  }
6477
6866
  if (unlitOrUnfogged) {
6478
6867
  this.perFrameInts[this.materialField('uLightingEnabled')] = 1;
6479
6868
  this.perFrameInts[this.materialField('uFogEnabled')] = 1;
6480
- this.materialSlot = -1;
6481
6869
  }
6482
6870
  if (!toneMapped) {
6483
6871
  this.perFrameInts[this.materialField('uOutputTransform')] = this.gradeCode();
6484
- this.materialSlot = -1;
6485
6872
  }
6873
+ if (own)
6874
+ this.materials.dirty();
6486
6875
  }
6487
6876
  /* -- Scatter ------------------------------------------------------------------------ */
6488
6877
  /** Upload a base mesh and size its per-instance buffers. See `createGpuScatter`. */
@@ -7166,7 +7555,7 @@ export class WebGPURenderer {
7166
7555
  this.emissiveMap = emissiveMap;
7167
7556
  this.bindGroup = this.flatGroupForMaps(this.albedo, this.normalMap, this.ormMap, this.emissiveMap);
7168
7557
  /* The open material lives in a slot of a buffer that no longer exists. */
7169
- this.materialSlot = -1;
7558
+ this.materials.dirty();
7170
7559
  }
7171
7560
  /**
7172
7561
  * Rebuild the group for the open material, and drop every cached one.
@@ -7293,7 +7682,7 @@ export class WebGPURenderer {
7293
7682
  * consumer running the temporal resolve without motion blur would have reprojected through a
7294
7683
  * `previousViewProj` nothing had ever written.
7295
7684
  */
7296
- const wantsReprojection = blur > 0 || this.temporalJittering;
7685
+ const wantsReprojection = blur > 0 || this.temporalJittering || this.reconstructing;
7297
7686
  if (wantsReprojection && this.viewProj !== null) {
7298
7687
  if (this.hasPreviousView) {
7299
7688
  mat4.invert(this.reprojection, this.correctedViewProj);
@@ -7348,6 +7737,15 @@ export class WebGPURenderer {
7348
7737
  this.runMedium(encoder);
7349
7738
  /* After the depth it reprojects through, and before anything reads the scene. */
7350
7739
  this.temporalResolve(encoder);
7740
+ /*
7741
+ * **Reconstruction stands where the temporal resolve stands**, for the same reasons: after
7742
+ * everything that draws into the scene and after the depth it reprojects through, and before
7743
+ * bloom and the composite read the picture. It cannot copy its result back over the scene the
7744
+ * way the temporal resolve does — the two are different sizes — so the composite is given a
7745
+ * bind group of its own instead, which is the second group `temporalResolve` declined.
7746
+ */
7747
+ this.runMotion(encoder);
7748
+ this.runReconstruction(encoder);
7351
7749
  const aoStrength = this.quality.ambientOcclusion;
7352
7750
  if (aoStrength > 0 && this.frameProjection !== null)
7353
7751
  this.runOcclusion(encoder);
@@ -7401,9 +7799,10 @@ export class WebGPURenderer {
7401
7799
  ],
7402
7800
  });
7403
7801
  pass.setPipeline(postPipeline(this.pipelines, device, this.rushLayout, 'post.rush', RUSH_FRAG_WGSL, this.surface.format));
7404
- if (this.rushBindGroup === null)
7802
+ const rush = this.reconstructing ? this.reconRushGroup : this.rushBindGroup;
7803
+ if (rush === null)
7405
7804
  return;
7406
- pass.setBindGroup(0, this.rushBindGroup, [0]);
7805
+ pass.setBindGroup(0, rush, [0]);
7407
7806
  pass.draw(3);
7408
7807
  pass.end();
7409
7808
  }
@@ -7425,6 +7824,15 @@ export class WebGPURenderer {
7425
7824
  if (this.aoTargetView === null)
7426
7825
  return;
7427
7826
  this.rushBindGroup = this.buildRushBindGroup('post.rushBindGroup', this.sceneColorView);
7827
+ /*
7828
+ * **And the reconstruction's twin, from the same three call sites.** A colour grade replaces
7829
+ * the lookup table and a resize replaces the depth and the occlusion target, and both groups
7830
+ * hold all three — so a second place that rebuilt only one of them is how the composite would
7831
+ * come to read a stale view on exactly the frames a consumer changed something.
7832
+ */
7833
+ if (this.reconShownView !== null) {
7834
+ this.reconRushGroup = this.buildRushBindGroup('recon.rushBindGroup', this.reconShownView);
7835
+ }
7428
7836
  }
7429
7837
  /** The composite's group over one scene source. See `rebuildRushBindGroup`. */
7430
7838
  buildRushBindGroup(label, sceneView) {
@@ -7473,6 +7881,479 @@ export class WebGPURenderer {
7473
7881
  return field.offset / 4;
7474
7882
  }
7475
7883
  /** Sample zero of the depth, into something the generated shaders can sample. */
7884
+ /**
7885
+ * Allocate what a reconstruction needs, and release it when nobody is asking.
7886
+ *
7887
+ * Sized by a key rather than by comparing four numbers, because three of the six targets are the
7888
+ * output's size and three are the render's, and a comparison that checked one pair would rebuild
7889
+ * half of them on a resize and keep the other half at the old size — which the device accepts,
7890
+ * a bind group holding views of two different sizes being perfectly legal, and which draws a
7891
+ * picture with a piece of it at the wrong scale.
7892
+ */
7893
+ ensureReconstruction(renderWidth, renderHeight, outputWidth, outputHeight) {
7894
+ const wanted = this.reconstructionWanted;
7895
+ const key = wanted ? `${renderWidth}x${renderHeight}:${outputWidth}x${outputHeight}` : '';
7896
+ if (key === this.reconSizeKey)
7897
+ return;
7898
+ this.reconSizeKey = key;
7899
+ for (const dead of [
7900
+ ...this.reconHistories,
7901
+ this.reconShown,
7902
+ this.reconMotion,
7903
+ this.reconPreviousDepth,
7904
+ ]) {
7905
+ dead?.destroy();
7906
+ }
7907
+ this.reconHistories = [null, null];
7908
+ this.reconHistoryViews = [null, null];
7909
+ this.reconShown = null;
7910
+ this.reconShownView = null;
7911
+ this.reconMotion = null;
7912
+ this.reconMotionView = null;
7913
+ this.reconPreviousDepth = null;
7914
+ this.reconPreviousDepthView = null;
7915
+ this.reconGroups = [null, null];
7916
+ this.reconSharpenGroups = [null, null];
7917
+ this.reconRushGroup = null;
7918
+ /* A history of a size that no longer exists is not a history of this frame. */
7919
+ this.reconHasHistory = false;
7920
+ if (!wanted)
7921
+ return;
7922
+ const { device } = this.surface;
7923
+ const HISTORY_USAGE = 0x4 | 0x8; // TEXTURE_BINDING | STORAGE_BINDING
7924
+ for (let i = 0; i < 2; i += 1) {
7925
+ const texture = device.createTexture({
7926
+ label: `recon.history${String(i)}`,
7927
+ size: [outputWidth, outputHeight],
7928
+ format: RECON_HISTORY_FORMAT,
7929
+ usage: HISTORY_USAGE,
7930
+ });
7931
+ this.reconHistories[i] = texture;
7932
+ this.reconHistoryViews[i] = texture.createView();
7933
+ }
7934
+ this.reconShown = device.createTexture({
7935
+ label: 'recon.shown',
7936
+ size: [outputWidth, outputHeight],
7937
+ format: RECON_HISTORY_FORMAT,
7938
+ usage: HISTORY_USAGE,
7939
+ });
7940
+ this.reconShownView = this.reconShown.createView();
7941
+ this.reconMotion = device.createTexture({
7942
+ label: 'recon.motion',
7943
+ size: [renderWidth, renderHeight],
7944
+ format: RECON_HISTORY_FORMAT,
7945
+ /* An attachment as well, because the pass that will write it is a draw. Nothing writes it
7946
+ today and nothing needs to clear it either: WebGPU zero-initialises a new texture, and
7947
+ a zero in the fourth channel is what tells the resolve to derive the camera's motion. */
7948
+ usage: 0x4 | 0x10,
7949
+ });
7950
+ this.reconMotionView = this.reconMotion.createView();
7951
+ this.reconPreviousDepth = device.createTexture({
7952
+ label: 'recon.previousDepth',
7953
+ size: [renderWidth, renderHeight],
7954
+ format: RESOLVED_DEPTH_FORMAT,
7955
+ usage: 0x4 | 0x2, // TEXTURE_BINDING | COPY_DST
7956
+ });
7957
+ this.reconPreviousDepthView = this.reconPreviousDepth.createView();
7958
+ this.reconParams ??= device.createBuffer({
7959
+ label: 'recon.params',
7960
+ size: this.reconStaging.byteLength,
7961
+ usage: 0x40 | 0x8, // UNIFORM | COPY_DST
7962
+ });
7963
+ this.buildReconPipelines();
7964
+ this.rebuildReconGroups();
7965
+ }
7966
+ /**
7967
+ * The two dispatches, over one explicit layout.
7968
+ *
7969
+ * **Explicit and not `layout: 'auto'`**, which §3 row 51 is about: an inferred layout names only
7970
+ * the bindings its own entry point reads, and these two read different subsets of one module —
7971
+ * the sharpen touches nothing but the history and the target. Two inferred layouts cannot share
7972
+ * a bind group, and building two groups over the same resources is two things to keep in step.
7973
+ */
7974
+ buildReconPipelines() {
7975
+ if (this.reconLayout !== null)
7976
+ return;
7977
+ const { device } = this.surface;
7978
+ const COMPUTE = 0x4; // GPUShaderStage.COMPUTE
7979
+ const sampled = (binding, sampleType) => ({
7980
+ binding,
7981
+ visibility: COMPUTE,
7982
+ texture: { sampleType, viewDimension: '2d' },
7983
+ });
7984
+ this.reconLayout = device.createBindGroupLayout({
7985
+ label: 'recon.layout',
7986
+ entries: [
7987
+ sampled(0, 'float'),
7988
+ /* r32float is not filterable, and the resolve only ever loads a depth texel. */
7989
+ sampled(1, 'unfilterable-float'),
7990
+ sampled(2, 'float'),
7991
+ sampled(3, 'unfilterable-float'),
7992
+ sampled(4, 'float'),
7993
+ { binding: 5, visibility: COMPUTE, sampler: { type: 'filtering' } },
7994
+ { binding: 6, visibility: COMPUTE, buffer: { type: 'uniform' } },
7995
+ {
7996
+ binding: 7,
7997
+ visibility: COMPUTE,
7998
+ storageTexture: {
7999
+ access: 'write-only',
8000
+ format: RECON_HISTORY_FORMAT,
8001
+ viewDimension: '2d',
8002
+ },
8003
+ },
8004
+ ],
8005
+ });
8006
+ const module = device.createShaderModule({ label: 'recon.resolve', code: reconResolveWgsl() });
8007
+ const layout = device.createPipelineLayout({ bindGroupLayouts: [this.reconLayout] });
8008
+ this.reconResolvePipeline = device.createComputePipeline({
8009
+ label: 'recon.resolve',
8010
+ layout,
8011
+ compute: { module, entryPoint: 'resolveMain' },
8012
+ });
8013
+ this.reconSharpenPipeline = device.createComputePipeline({
8014
+ label: 'recon.sharpen',
8015
+ layout,
8016
+ compute: { module, entryPoint: 'sharpenMain' },
8017
+ });
8018
+ }
8019
+ /**
8020
+ * The four groups, one a direction of the ping-pong and one a dispatch.
8021
+ *
8022
+ * The resolve reads history `1 - write` and writes history `write`; the sharpen reads history
8023
+ * `write` and writes the shown picture. A group holds the views it was built with, so this is
8024
+ * called whenever any of them is replaced.
8025
+ */
8026
+ rebuildReconGroups() {
8027
+ const layout = this.reconLayout;
8028
+ const params = this.reconParams;
8029
+ if (layout === null || params === null)
8030
+ return;
8031
+ if (this.sceneColorView === null || this.resolvedDepthView === null)
8032
+ return;
8033
+ if (this.reconMotionView === null || this.reconPreviousDepthView === null)
8034
+ return;
8035
+ if (this.reconShownView === null)
8036
+ return;
8037
+ const { device } = this.surface;
8038
+ const build = (label, history, target) => device.createBindGroup({
8039
+ label,
8040
+ layout,
8041
+ entries: [
8042
+ { binding: 0, resource: this.sceneColorView },
8043
+ { binding: 1, resource: this.resolvedDepthView },
8044
+ { binding: 2, resource: this.reconMotionView },
8045
+ { binding: 3, resource: this.reconPreviousDepthView },
8046
+ { binding: 4, resource: history },
8047
+ { binding: 5, resource: this.postSampler },
8048
+ { binding: 6, resource: { buffer: params } },
8049
+ { binding: 7, resource: target },
8050
+ ],
8051
+ });
8052
+ for (let write = 0; write < 2; write += 1) {
8053
+ const read = this.reconHistoryViews[1 - write];
8054
+ const target = this.reconHistoryViews[write];
8055
+ if (read === null || target === null)
8056
+ continue;
8057
+ this.reconGroups[write] = build(`recon.resolve${String(write)}`, read, target);
8058
+ this.reconSharpenGroups[write] = build(`recon.sharpen${String(write)}`, target, this.reconShownView);
8059
+ }
8060
+ /* The composite's own group, over the resolved picture instead of the scene target. */
8061
+ this.reconRushGroup = this.buildRushBindGroup('recon.rushBindGroup', this.reconShownView);
8062
+ /* And the scene's, because `ensureComposite` built it before this one existed. */
8063
+ this.rebuildRushBindGroup();
8064
+ }
8065
+ /**
8066
+ * Remember that this mesh moved, so the motion pass can draw where it was.
8067
+ *
8068
+ * Silent unless a reconstruction is running: a scene that always states its previous transforms
8069
+ * — which is the right thing for a scene to do, the alternative being to know what the renderer
8070
+ * is configured as — must cost nothing at all when nobody is going to read them.
8071
+ */
8072
+ recordMotion(mesh, model, previousModel) {
8073
+ if (!this.reconstructing)
8074
+ return;
8075
+ const at = this.motionCount;
8076
+ const wanted = (at + 1) * MOTION_DRAW_FLOATS;
8077
+ if (this.motionMatrices.length < wanted) {
8078
+ /* Doubling, so a scene settles on one allocation rather than one a draw. */
8079
+ const grown = new Float32Array(Math.max(wanted, this.motionMatrices.length * 2, 64));
8080
+ grown.set(this.motionMatrices);
8081
+ this.motionMatrices = grown;
8082
+ }
8083
+ this.motionMatrices.set(model, at * MOTION_DRAW_FLOATS);
8084
+ this.motionMatrices.set(previousModel, at * MOTION_DRAW_FLOATS + 16);
8085
+ this.motionMeshes[at] = mesh;
8086
+ this.motionCount = at + 1;
8087
+ }
8088
+ /**
8089
+ * The pipeline the motion pass draws with, built once.
8090
+ *
8091
+ * **The position buffer alone**, because that is all the shader reads and a mesh keeps one buffer
8092
+ * an attribute — so this binds buffer zero and nothing else, whatever else the mesh carries.
8093
+ */
8094
+ buildMotionPipeline() {
8095
+ if (this.motionLayout !== null)
8096
+ return;
8097
+ const { device } = this.surface;
8098
+ this.motionLayout = device.createBindGroupLayout({
8099
+ label: 'recon.motion.layout',
8100
+ entries: [
8101
+ { binding: 0, visibility: 0x1, buffer: { type: 'uniform' } },
8102
+ {
8103
+ binding: 1,
8104
+ visibility: 0x1,
8105
+ buffer: { type: 'uniform', hasDynamicOffset: true },
8106
+ },
8107
+ ],
8108
+ });
8109
+ this.motionModule = device.createShaderModule({
8110
+ label: 'recon.motion',
8111
+ code: RECON_MOTION_WGSL,
8112
+ });
8113
+ this.motionPipelineLayout = device.createPipelineLayout({
8114
+ bindGroupLayouts: [this.motionLayout],
8115
+ });
8116
+ }
8117
+ /** The pipeline for one vertex stride, built on first sight of it. */
8118
+ motionPipelineFor(stride) {
8119
+ const held = this.motionPipelines.get(stride);
8120
+ if (held !== undefined)
8121
+ return held;
8122
+ const module = this.motionModule;
8123
+ const layout = this.motionPipelineLayout;
8124
+ if (module === null || layout === null)
8125
+ return null;
8126
+ const { device } = this.surface;
8127
+ const pipeline = device.createRenderPipeline({
8128
+ label: `recon.motion.${String(stride)}`,
8129
+ layout,
8130
+ vertex: {
8131
+ module,
8132
+ entryPoint: 'motionVert',
8133
+ /* Positions are the layout's first attribute and are never optional, so they are at offset
8134
+ zero of every stride this can be handed. */
8135
+ buffers: [
8136
+ {
8137
+ arrayStride: stride,
8138
+ attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }],
8139
+ },
8140
+ ],
8141
+ },
8142
+ fragment: { module, entryPoint: 'motionFrag', targets: [{ format: MOTION_TARGET_FORMAT }] },
8143
+ /*
8144
+ * **The scene's own winding**, because this draws the scene's geometry through the scene's
8145
+ * clip transform — the product the renderer hands over already carries the negation that
8146
+ * `naga` writes into every generated vertex stage, so the faces come out the way they did
8147
+ * and a `cw` front face here would draw the inside of every mesh. §3 row 57.
8148
+ */
8149
+ primitive: { topology: 'triangle-list', cullMode: 'back' },
8150
+ depthStencil: {
8151
+ format: DEPTH_FORMAT,
8152
+ /*
8153
+ * **Tested and not written.** The frame's depth is finished; this pass only wants the
8154
+ * pixels the scene actually kept, and `DEPTH_COMPARE_EQUAL` is the comparison that admits
8155
+ * exactly them — the same transform on the same positions gives the same depth, which is
8156
+ * why the rasterisation matrix is handed over whole rather than rebuilt here.
8157
+ *
8158
+ * **And the equality is not fragile here**, which was worth measuring rather than
8159
+ * assuming: this driver contracts multiply-adds (§3 row 107) and two shader modules can
8160
+ * contract a matrix product differently, so an equality across them could have admitted
8161
+ * only some of a surface. Run with `greater-equal` instead, the ghost measured the same
8162
+ * to the pixel — 5,942 either way — so nothing is being dropped.
8163
+ */
8164
+ depthWriteEnabled: false,
8165
+ depthCompare: DEPTH_COMPARE_EQUAL,
8166
+ },
8167
+ });
8168
+ this.motionPipelines.set(stride, pipeline);
8169
+ return pipeline;
8170
+ }
8171
+ /**
8172
+ * Where every mover was last frame, into the motion target.
8173
+ *
8174
+ * **After everything that draws and before the resolve reads it.** The frame's depth has to be
8175
+ * finished for the equality test to mean anything, and the resolve is what consumes the result.
8176
+ */
8177
+ runMotion(encoder) {
8178
+ const target = this.reconMotionView;
8179
+ const depth = this.depthView;
8180
+ if (!this.reconstructing || target === null || depth === null)
8181
+ return;
8182
+ const { device } = this.surface;
8183
+ this.buildMotionPipeline();
8184
+ const layout = this.motionLayout;
8185
+ if (layout === null)
8186
+ return;
8187
+ /*
8188
+ * **Cleared every frame, even when nothing moved.** A texel left from last frame is a flag
8189
+ * saying "a draw wrote this", and the resolve would take a motion belonging to a surface that
8190
+ * is no longer there. The clear is the pass's own load operation, so an empty queue still runs
8191
+ * it — which is why this is not returned from before the pass is opened.
8192
+ */
8193
+ const pass = encoder.beginRenderPass({
8194
+ label: 'recon.motion',
8195
+ timestampWrites: this.gpuTimer.writesFor(),
8196
+ colorAttachments: [
8197
+ { view: target, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
8198
+ ],
8199
+ depthStencilAttachment: {
8200
+ view: depth,
8201
+ depthLoadOp: 'load',
8202
+ depthStoreOp: 'store',
8203
+ depthReadOnly: false,
8204
+ },
8205
+ });
8206
+ const count = this.motionCount;
8207
+ if (count > 0) {
8208
+ const bytes = count * MOTION_DRAW_STRIDE;
8209
+ if (this.motionDrawCapacity < count) {
8210
+ this.motionDrawBuffer?.destroy();
8211
+ this.motionDrawCapacity = Math.max(count, this.motionDrawCapacity * 2, 16);
8212
+ this.motionDrawBuffer = device.createBuffer({
8213
+ label: 'recon.motion.draws',
8214
+ size: this.motionDrawCapacity * MOTION_DRAW_STRIDE,
8215
+ usage: 0x40 | 0x8, // UNIFORM | COPY_DST
8216
+ });
8217
+ this.motionDrawStaging = new Uint8Array(this.motionDrawCapacity * MOTION_DRAW_STRIDE);
8218
+ this.motionGroup = null;
8219
+ }
8220
+ this.motionFrameBuffer ??= device.createBuffer({
8221
+ label: 'recon.motion.frame',
8222
+ size: MOTION_FRAME_FLOATS * 4,
8223
+ usage: 0x40 | 0x8,
8224
+ });
8225
+ this.motionGroup ??= device.createBindGroup({
8226
+ label: 'recon.motion.group',
8227
+ layout,
8228
+ entries: [
8229
+ { binding: 0, resource: { buffer: this.motionFrameBuffer } },
8230
+ {
8231
+ binding: 1,
8232
+ resource: { buffer: this.motionDrawBuffer, size: MOTION_DRAW_STRIDE },
8233
+ },
8234
+ ],
8235
+ });
8236
+ /* The scene's clip transform, with the generated stages' y negation folded in. */
8237
+ mat4.multiply(this.motionRaster, MOTION_CLIP_FLIP, this.viewProj ?? this.correctedViewProj);
8238
+ this.motionFrameStaging.set(this.motionRaster, 0);
8239
+ this.motionFrameStaging.set(this.frameRawViewProj, 16);
8240
+ this.motionFrameStaging.set(this.reconPreviousRawViewProj, 32);
8241
+ device.queue.writeBuffer(this.motionFrameBuffer, 0, this.motionFrameStaging);
8242
+ const draws = new Float32Array(this.motionDrawStaging.buffer);
8243
+ for (let i = 0; i < count; i += 1) {
8244
+ draws.set(this.motionMatrices.subarray(i * MOTION_DRAW_FLOATS, (i + 1) * MOTION_DRAW_FLOATS), (i * MOTION_DRAW_STRIDE) / 4);
8245
+ }
8246
+ device.queue.writeBuffer(this.motionDrawBuffer, 0, this.motionDrawStaging, 0, bytes);
8247
+ let bound = null;
8248
+ for (let i = 0; i < count; i += 1) {
8249
+ const mesh = this.motionMeshes[i];
8250
+ if (mesh === undefined)
8251
+ continue;
8252
+ const pipeline = this.motionPipelineFor(mesh.vertexStride);
8253
+ if (pipeline === null)
8254
+ continue;
8255
+ if (pipeline !== bound) {
8256
+ pass.setPipeline(pipeline);
8257
+ bound = pipeline;
8258
+ }
8259
+ pass.setBindGroup(0, this.motionGroup, [i * MOTION_DRAW_STRIDE]);
8260
+ pass.setVertexBuffer(0, mesh.vertexBuffers[0]);
8261
+ pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
8262
+ pass.drawIndexed(mesh.indexCount);
8263
+ }
8264
+ }
8265
+ pass.end();
8266
+ }
8267
+ /**
8268
+ * DriftTR tier 0: the jittered render at the render size into the picture at the output size.
8269
+ *
8270
+ * `recon/resolve.ts` is the reference this is held to and `scripts/recon-parity.mjs` compares the
8271
+ * two; nothing here decides anything the reference does not.
8272
+ */
8273
+ runReconstruction(encoder) {
8274
+ if (!this.reconstructing)
8275
+ return;
8276
+ const resolve = this.reconResolvePipeline;
8277
+ const sharpen = this.reconSharpenPipeline;
8278
+ const params = this.reconParams;
8279
+ const write = this.reconWrite;
8280
+ const group = this.reconGroups[write];
8281
+ const sharpenGroup = this.reconSharpenGroups[write];
8282
+ if (resolve === null || sharpen === null || params === null)
8283
+ return;
8284
+ if (group === null || sharpenGroup === null)
8285
+ return;
8286
+ const { device } = this.surface;
8287
+ const outputWidth = Math.max(1, this.surface.canvas.width);
8288
+ const outputHeight = Math.max(1, this.surface.canvas.height);
8289
+ /*
8290
+ * **The unjittered matrices**, which is what the reference's own header insists on: the
8291
+ * history stands for the scene as it would have been drawn with no jitter, and the motion the
8292
+ * resolve derives carries none. `frameRawViewProj` is the camera's own view-projection, in
8293
+ * OpenGL's depth convention — which is the convention the shader's accessor converts the
8294
+ * renderer's reversed buffer into.
8295
+ */
8296
+ mat4.invert(this.reconInverseViewProj, this.frameRawViewProj);
8297
+ const f = this.reconFloats;
8298
+ const u = this.reconInts;
8299
+ u[0] = this.renderWidth;
8300
+ u[1] = this.renderHeight;
8301
+ u[2] = outputWidth;
8302
+ u[3] = outputHeight;
8303
+ f[4] = this.reconJitter[0];
8304
+ f[5] = this.reconJitter[1];
8305
+ f[6] = this.reconPreviousJitter[0];
8306
+ f[7] = this.reconPreviousJitter[1];
8307
+ f.set(this.reconInverseViewProj, 8);
8308
+ f.set(this.reconPreviousRawViewProj, 24);
8309
+ f.set(this.reconPreviousInverseViewProj, 40);
8310
+ f.set(this.reconEye, 56);
8311
+ f.set(this.reconPreviousEye, 60);
8312
+ f[64] = DEFAULT_RECON_QUALITY.alpha;
8313
+ f[65] = DEFAULT_RECON_QUALITY.sharpen;
8314
+ f[66] = DEFAULT_RECON_QUALITY.clampGamma;
8315
+ u[67] = this.reconHasHistory ? 1 : 0;
8316
+ f[68] = DEFAULT_DISOCCLUSION.depthTolerance;
8317
+ f[69] = DEFAULT_DISOCCLUSION.motionScale;
8318
+ f[70] = DEFAULT_DISOCCLUSION.normalFloor;
8319
+ f[71] = DEFAULT_DISOCCLUSION.normalCeiling;
8320
+ device.queue.writeBuffer(params, 0, this.reconStaging);
8321
+ const groupsX = Math.ceil(outputWidth / RECON_WORKGROUP);
8322
+ const groupsY = Math.ceil(outputHeight / RECON_WORKGROUP);
8323
+ const pass = encoder.beginComputePass({
8324
+ label: 'recon.resolve',
8325
+ timestampWrites: this.gpuTimer.writesFor(),
8326
+ });
8327
+ pass.setPipeline(resolve);
8328
+ pass.setBindGroup(0, group);
8329
+ pass.dispatchWorkgroups(groupsX, groupsY, 1);
8330
+ /*
8331
+ * **The sharpen in the same pass, and that is legal because it reads a different texture.**
8332
+ * The resolve wrote history `write`; the sharpen reads it and writes the shown picture. Two
8333
+ * dispatches in one compute pass see each other's writes in recording order, and the rule
8334
+ * §3 row 52 is about — one buffer writable and readable in a single synchronisation scope —
8335
+ * does not bite, because no resource here is both.
8336
+ */
8337
+ pass.setPipeline(sharpen);
8338
+ pass.setBindGroup(0, sharpenGroup);
8339
+ pass.dispatchWorkgroups(groupsX, groupsY, 1);
8340
+ pass.end();
8341
+ /* Last frame's depth, for the disocclusion test and the normals derived from it. */
8342
+ const depth = this.resolvedDepth;
8343
+ const previous = this.reconPreviousDepth;
8344
+ if (depth !== null && previous !== null) {
8345
+ encoder.copyTextureToTexture({ texture: depth }, { texture: previous }, [
8346
+ previous.width,
8347
+ previous.height,
8348
+ 1,
8349
+ ]);
8350
+ }
8351
+ this.reconWrite = 1 - write;
8352
+ this.reconHasHistory = true;
8353
+ mat4.copy(this.reconPreviousRawViewProj, this.frameRawViewProj);
8354
+ mat4.copy(this.reconPreviousInverseViewProj, this.reconInverseViewProj);
8355
+ this.reconPreviousEye.set(this.reconEye);
8356
+ }
7476
8357
  resolveDepth(encoder) {
7477
8358
  const target = this.resolvedDepthView;
7478
8359
  const source = this.depthView;
@@ -7510,6 +8391,12 @@ export class WebGPURenderer {
7510
8391
  * must still write, or the history it leaves for the next frame is whatever was in the texture.
7511
8392
  */
7512
8393
  temporalResolve(encoder) {
8394
+ /*
8395
+ * **One guard, not two.** A second test on `reconstructing` here read as belt and braces and
8396
+ * was worse than nothing: it and the `!reconstructing` in `temporalJittering`'s own condition
8397
+ * each covered the other, so breaking either one changed no behaviour and no test could see
8398
+ * it. `temporalJittering` is where the choice is made, and it is made once.
8399
+ */
7513
8400
  if (!this.temporalJittering)
7514
8401
  return;
7515
8402
  const write = this.taaWrite;
@@ -7521,8 +8408,8 @@ export class WebGPURenderer {
7521
8408
  const f = this.taaFloats;
7522
8409
  const at = (name) => this.postField(TAA_FRAG_FIELDS, name);
7523
8410
  f.set(this.reprojection, at('uReprojection'));
7524
- f[at('uTexel')] = 1 / Math.max(1, this.surface.canvas.width);
7525
- f[at('uTexel') + 1] = 1 / Math.max(1, this.surface.canvas.height);
8411
+ f[at('uTexel')] = 1 / this.renderWidth;
8412
+ f[at('uTexel') + 1] = 1 / this.renderHeight;
7526
8413
  f[at('uHistoryBlend')] = this.temporalHistoryUsable ? TEMPORAL_HISTORY_BLEND : 0;
7527
8414
  device.queue.writeBuffer(this.taaUniforms, 0, this.taaStaging);
7528
8415
  const pass = encoder.beginRenderPass({
@@ -7592,8 +8479,9 @@ export class WebGPURenderer {
7592
8479
  if (!this.frameCameraSeen)
7593
8480
  return;
7594
8481
  const { device } = this.surface;
7595
- const width = Math.max(1, this.surface.canvas.width);
7596
- const height = Math.max(1, this.surface.canvas.height);
8482
+ /* The scene's size: this marches through the picture's own texels, not the viewer's. */
8483
+ const width = this.renderWidth;
8484
+ const height = this.renderHeight;
7597
8485
  const view = this.frameRawViewProj;
7598
8486
  mat4.invert(this.ssrDepthToWorld, view);
7599
8487
  mat4.multiply(this.ssrDepthToWorld, this.ssrDepthToWorld, DEPTH_01_TO_CLIP_Y_DOWN);
@@ -7674,8 +8562,9 @@ export class WebGPURenderer {
7674
8562
  if (!this.frameCameraSeen)
7675
8563
  return;
7676
8564
  const { device } = this.surface;
7677
- const width = Math.max(1, this.surface.canvas.width);
7678
- const height = Math.max(1, this.surface.canvas.height);
8565
+ /* The scene's size: a mark is projected onto the depth this frame drew, at that depth's size. */
8566
+ const width = this.renderWidth;
8567
+ const height = this.renderHeight;
7679
8568
  const view = this.frameRawViewProj;
7680
8569
  mat4.invert(this.decalDepthToWorld, view);
7681
8570
  mat4.multiply(this.decalDepthToWorld, this.decalDepthToWorld, DEPTH_01_TO_CLIP_Y_DOWN);
@@ -7744,6 +8633,23 @@ export class WebGPURenderer {
7744
8633
  }
7745
8634
  const { device } = this.surface;
7746
8635
  const options = this.oitReplayOptions;
8636
+ /*
8637
+ * **A refracting pane reads the frame behind it, and here that frame is finished.** So the
8638
+ * copy it reads is taken now, from the resolved scene, once for the whole set — the immediate
8639
+ * path takes it by ending the scene's pass, and that pass has already ended. Only when
8640
+ * something in the set refracts, because a copy of the frame is not free.
8641
+ */
8642
+ if (this.translucentQueue.refracts && !this.refractSnapshotTaken) {
8643
+ const snapshot = this.refractSnapshot;
8644
+ const source = this.sceneColor;
8645
+ if (snapshot !== null && source !== null) {
8646
+ encoder.copyTextureToTexture({ texture: source }, { texture: snapshot }, [
8647
+ snapshot.width,
8648
+ snapshot.height,
8649
+ ]);
8650
+ this.refractSnapshotTaken = true;
8651
+ }
8652
+ }
7747
8653
  const buffers = [
7748
8654
  { mode: 'accum', view: accum, clear: [0, 0, 0, 0] },
7749
8655
  /* Cleared to one: nothing has covered this pixel yet. Cleared to zero the resolve would show
@@ -7773,6 +8679,12 @@ export class WebGPURenderer {
7773
8679
  options.depthWrite = false;
7774
8680
  options.depthLayer = draw.depthLayer;
7775
8681
  options.tint = draw.tint;
8682
+ /* Replayed too, as the other backend replays it: a pane that refracts under sorted
8683
+ blending and stands clear under this effect reads as a bug in the effect. It did stand
8684
+ clear here until 2026-09-19. */
8685
+ options.refraction = draw.refraction;
8686
+ options.refractTint = draw.refractTint;
8687
+ options.thicknessM = draw.thicknessM;
7776
8688
  this.drawTranslucentMesh(draw.mesh, draw.model, draw.opacity, options);
7777
8689
  });
7778
8690
  this.oitReplaying = false;
@@ -8137,6 +9049,10 @@ export class WebGPURenderer {
8137
9049
  return false;
8138
9050
  if (this.refractSnapshotTaken)
8139
9051
  return true;
9052
+ /* The replay of an order-independent set runs after the scene's pass has ended, and `runOit`
9053
+ took the copy for it before it began; there is no pass here to end and reopen. */
9054
+ if (this.oitReplaying)
9055
+ return false;
8140
9056
  const snapshot = this.refractSnapshot;
8141
9057
  const source = this.sceneColor;
8142
9058
  const encoder = this.encoder;
@@ -8297,7 +9213,15 @@ export class WebGPURenderer {
8297
9213
  * recorded so far writes the scene target, so this must come out as exactly one pass;
8298
9214
  * anything else means a node declared a write it does not perform.
8299
9215
  */
8300
- const passes = schedule(this.arena, this.pendingClearMask, liveOut, this.scheduledPasses);
9216
+ /*
9217
+ * **Either scheduler, into the same records**, so nothing below knows which ran. The identifier
9218
+ * graph is the one that can also express the second pipeline's frame; running it here is what
9219
+ * shows it is a generalisation of this one rather than a different renderer — see
9220
+ * `quality.identifierGraph` for the captures that say so.
9221
+ */
9222
+ const passes = this.flushSchedule === null
9223
+ ? schedule(this.arena, this.pendingClearMask, liveOut, this.scheduledPasses)
9224
+ : scheduleFlush(this.arena, this.pendingClearMask, liveOut, this.flushSchedule, this.scheduledPasses);
8301
9225
  this.passesLastFlush = passes;
8302
9226
  let discards = 0;
8303
9227
  let clears = 0;
@@ -8415,6 +9339,14 @@ export class WebGPURenderer {
8415
9339
  once and ran against a discarded attachment that read zero everywhere — which is why
8416
9340
  there is one of these and why this line is in it. */
8417
9341
  (this.quality.temporalAa && this.quality.screenEffects) ||
9342
+ /*
9343
+ * **And so does a reconstruction, for the same reason, and it was missing for the same
9344
+ * reason.** Without it the resolve read a discarded attachment as zero — the far plane
9345
+ * reversed — so every surface stood at infinity: a turning camera reprojected correctly,
9346
+ * since a turn moves every depth alike, and a sliding one moved nothing, and the history
9347
+ * trailed every slide by the parallax it never saw.
9348
+ */
9349
+ this.reconstructing ||
8418
9350
  /*
8419
9351
  * **The order-independent passes attach the depth and test against it**, so a frame running
8420
9352
  * them needs it to survive the main pass. Without this it is discarded, those passes test
@@ -8775,6 +9707,15 @@ export class WebGPURenderer {
8775
9707
  this.flushRings();
8776
9708
  framePass.end();
8777
9709
  this.pass = null;
9710
+ /*
9711
+ * **The one window in the frame where every declaration is in and no render pass is open.**
9712
+ * A consumer declares its fields wherever in its own frame the objects live, so the earliest
9713
+ * the set is complete is after the last verb; and a compute pass cannot be recorded inside a
9714
+ * render pass, so the latest it can be recorded is before the composite opens its own. The
9715
+ * probes traced from it are read by the next frame's shading, which is what a probe volume
9716
+ * does anyway — it accumulates.
9717
+ */
9718
+ this.runIndirectLight(this.encoder);
8778
9719
  if (this.hasComposite)
8779
9720
  this.composite(this.encoder);
8780
9721
  /* And where there is no composite there is no depth to read, so a frame that submitted marks
@@ -8830,7 +9771,17 @@ export class WebGPURenderer {
8830
9771
  const width = Math.max(1, this.budgeted.width);
8831
9772
  const height = Math.max(1, this.budgeted.height);
8832
9773
  this.surface.configure(width, height);
8833
- this.ensureComposite(width, height);
9774
+ /*
9775
+ * **The same split `beginFrame` makes, and it has to be made twice.** This path resized the
9776
+ * composite to the drawing buffer while the frame resized it to the render size, so the two
9777
+ * fought every time a canvas changed — and the reconstruction's bind groups, built here
9778
+ * against the previous scene depth, held a view of a texture this call had just destroyed.
9779
+ * The device's words for that are "destroyed texture used in a submit", on the next frame,
9780
+ * after a resize, which is as far from the cause as a message gets.
9781
+ */
9782
+ const render = this.sceneSize();
9783
+ this.ensureComposite(render.width, render.height);
9784
+ this.ensureReconstruction(render.width, render.height, width, height);
8834
9785
  this.ensureReflection(width, height);
8835
9786
  }
8836
9787
  /**
@@ -8897,6 +9848,195 @@ export class WebGPURenderer {
8897
9848
  addOccluder(min, max, model) {
8898
9849
  this.occlusion?.addOccluder(min, max, model);
8899
9850
  }
9851
+ /**
9852
+ * Declare a baked distance field that indirect light may be traced against.
9853
+ *
9854
+ * **The seam is `addOccluder`'s, and for `addOccluder`'s reason.** A distance field is the shape
9855
+ * of a thing it is safe to be *traced against*, and only a consumer knows which of its objects
9856
+ * that is true of — the walls and the terrain, not the leaves, not the water, not the thing it
9857
+ * is about to delete. A renderer that derived the field from whatever was drawn would trace
9858
+ * against the foliage and light the room with a hedge.
9859
+ *
9860
+ * **Declared anywhere in the frame and composed at its end**, which is the one window where
9861
+ * every declaration is in and the encoder has no render pass open. Cleared at `beginFrame` with
9862
+ * the decal and reflection queues, so a consumer redeclares every frame and a field it stops
9863
+ * declaring stops lighting.
9864
+ *
9865
+ * The field is in the model matrix's own space and the matrix must carry uniform scale only —
9866
+ * `composeGlobalField` gives the reason: a distance is not preserved by a non-uniform scale, and
9867
+ * the factor to correct by depends on the direction to the nearest surface, which is exactly
9868
+ * what a distance field does not record.
9869
+ *
9870
+ * **`albedo` is what colour this placement's surface is, and it decides what the bounce carries.**
9871
+ * What leaves a surface is the light arriving times its albedo, so a ray that lands on a wall
9872
+ * and does not know its colour returns the same answer for a red wall and a white one — measured
9873
+ * on `demo/dev/bounce.html` before this argument existed. It is a placement's rather than a
9874
+ * field's, because the same baked shape is placed many times and two pillars cut from one mould
9875
+ * may be painted differently. White where it is left out, so every existing caller is unchanged.
9876
+ */
9877
+ addDistanceField(field, model, albedo) {
9878
+ this.distanceFields.record(field, model, albedo);
9879
+ }
9880
+ /** Forget the fields declared this frame, for a consumer that rebuilds its list mid-frame. */
9881
+ clearDistanceFields() {
9882
+ this.distanceFields.reset();
9883
+ }
9884
+ /**
9885
+ * Compose what the frame declared, or nothing at all.
9886
+ *
9887
+ * **Nothing at all is the ordinary case and must cost nothing.** `quality.indirectLight` is off
9888
+ * by default and a frame that declared no fields has nothing to compose, so both are answered
9889
+ * before the composer exists — which is what keeps the eighteen scenes byte-identical with the
9890
+ * flag off rather than merely close.
9891
+ */
9892
+ runIndirectLight(encoder) {
9893
+ /*
9894
+ * **The flag is read here and in no other place.** It used to be read again by each of the two
9895
+ * steps, and a perturbation proved neither of those could fail: the bake needs a composed
9896
+ * field, and only the composition makes one. A guard another guard covers is a line no test
9897
+ * can hold — this session has had to remove three of them.
9898
+ */
9899
+ if (!this.quality.indirectLight)
9900
+ return;
9901
+ this.composeDistanceField(encoder);
9902
+ this.bakeIndirectProbes(encoder);
9903
+ }
9904
+ composeDistanceField(encoder) {
9905
+ /*
9906
+ * **An empty frame still reaches the composer, and a test had to find that.** The early return
9907
+ * used to cover the whole method, so a frame that declared nothing left `composed` true from
9908
+ * the frame before — and the bake traced against a field built out of geometry the consumer
9909
+ * had stopped declaring. What the guard is actually for is not *building* a composer for a
9910
+ * profile that never declares anything, so that is all it now guards.
9911
+ */
9912
+ if (this.fieldComposer === null) {
9913
+ if (this.distanceFields.length === 0)
9914
+ return;
9915
+ this.fieldComposer = new FieldComposer(this.surface.device, DEFAULT_FIELD_COMPOSE);
9916
+ }
9917
+ this.fieldComposer.compose(encoder, this.distanceFields, this.frameEye);
9918
+ }
9919
+ /** The world field this frame composed, or null where none was. For a pass that marches it. */
9920
+ get distanceField() {
9921
+ return this.fieldComposer?.field ?? null;
9922
+ }
9923
+ /**
9924
+ * Refresh this frame's share of the probe grid from the world field, or do nothing at all.
9925
+ *
9926
+ * **Doing nothing is the ordinary case and must cost nothing**, which is why every condition is
9927
+ * answered before the baker exists: a frame may have composed no field — including a frame that
9928
+ * declared none after a frame that did, where the composer exists and holds nothing — and a
9929
+ * profile may have asked for no probe array. With either false this returns having allocated
9930
+ * nothing and recorded nothing, which is what keeps the scenes byte-identical.
9931
+ *
9932
+ * **The grid is the declared one, or one fitted to the fields themselves.** A consumer that
9933
+ * placed its probes always wins. One that did not gets a grid around whatever it said its light
9934
+ * may be traced against — which is the right extent rather than a convenient one, because a grid
9935
+ * sized to everything drawn would spend its probes on the inside of the sky.
9936
+ */
9937
+ bakeIndirectProbes(encoder) {
9938
+ const field = this.fieldComposer?.field;
9939
+ if (field === undefined || field === null)
9940
+ return;
9941
+ /*
9942
+ * **The grid is settled before the array is read, and the first version read it first.**
9943
+ * Fitting a grid calls `setProbeGrid`, which reallocates the array for the new layer count —
9944
+ * so a baker built from the array captured a line earlier held the one-layer array the
9945
+ * renderer starts with, and asked it for a view of layer 1. The device refused in those words.
9946
+ * It is the trap `setProbeGrid` already documents for the flat bind groups: a group holds
9947
+ * whatever texture existed when it was built, for ever, with no error anywhere until the
9948
+ * sizes disagree.
9949
+ */
9950
+ const grid = this.indirectGrid();
9951
+ if (grid === null)
9952
+ return;
9953
+ const array = this.probeArray;
9954
+ const view = this.probeView;
9955
+ if (array === null || view === null)
9956
+ return;
9957
+ if (this.probeBaker !== null && this.bakerArray !== array) {
9958
+ this.probeBaker.dispose();
9959
+ this.probeBaker = null;
9960
+ }
9961
+ this.probeBaker ??= new ProbeBaker(this.surface.device, {
9962
+ array,
9963
+ view,
9964
+ sampler: this.probeSampler,
9965
+ edge: this.probeEdge,
9966
+ format: this.pipelines.format,
9967
+ });
9968
+ this.bakerArray = array;
9969
+ /*
9970
+ * **Whether the scene rasterised this grid first**, which decides two things: that the
9971
+ * roughness chain is already valid and must not be overwritten by a diffuse map, and that the
9972
+ * probes hold direct light for the trace to bounce. A grid that was never rasterised is traced
9973
+ * anyway and converges on whatever it started with — see `ProbeBaker`'s header.
9974
+ */
9975
+ /*
9976
+ * The frame's own sun, taken from the environment `bindMeshPass` was given rather than from a
9977
+ * capture — which is what makes a bounce follow a light that moved. A frame that drew no mesh
9978
+ * has no environment and traces with the sun off, which is the honest answer for a frame that
9979
+ * never said what was lighting it.
9980
+ */
9981
+ const env = this.frameEnv;
9982
+ this.probeBaker.bake(encoder, field, grid, this.probeBaked, {
9983
+ direction: env?.directionalDir ?? NO_SUN,
9984
+ colour: env?.directionalColor ?? NO_SUN,
9985
+ /* What a ray that left the world finds. The frame's own ambient, not a capture. */
9986
+ sky: env?.ambient ?? NO_SUN,
9987
+ });
9988
+ /*
9989
+ * **The bake is what fills the layers now, so it is what marks them filled.** `probeBaked`
9990
+ * gates the whole array, and a grid nothing rasterised would otherwise never be sampled — the
9991
+ * lit pass would read the hemispheric gradient and the traced light would go nowhere.
9992
+ */
9993
+ if (this.probeBaker.complete) {
9994
+ for (let layer = 0; layer < grid.layers; layer += 1)
9995
+ this.markProbeFilled(layer);
9996
+ }
9997
+ }
9998
+ /** The declared grid, or one fitted to this frame's fields, or null where neither is possible. */
9999
+ indirectGrid() {
10000
+ if (this.probes !== null)
10001
+ return this.probes;
10002
+ if (!distanceFieldBounds(this.distanceFields, this.fieldBoundsMin, this.fieldBoundsMax)) {
10003
+ return null;
10004
+ }
10005
+ const fitted = fitProbeGrid({ min: this.fieldBoundsMin, max: this.fieldBoundsMax }, INDIRECT_PROBE_SPACING, MAX_ENV_PROBES);
10006
+ if (!this.setProbeGrid(fitted)) {
10007
+ if (!this.indirectGridRefused) {
10008
+ this.indirectGridRefused = true;
10009
+ console.warn('[driftengine] indirect light needs a probe grid and this profile has no probe array, ' +
10010
+ 'so nothing is traced. Ask for a quality profile with the environment probe enabled.');
10011
+ }
10012
+ return null;
10013
+ }
10014
+ return this.probes;
10015
+ }
10016
+ /**
10017
+ * The device milliseconds the last composed field took, or null where nothing measured it.
10018
+ *
10019
+ * One frame behind, because a timestamp is read back and a readback that blocked would be the
10020
+ * measurement changing what it measures. Null also where no field has ever been composed, and
10021
+ * where the device was not asked for `timestamp-query`.
10022
+ */
10023
+ get distanceFieldMs() {
10024
+ return this.fieldComposer?.composeMs ?? null;
10025
+ }
10026
+ /**
10027
+ * The device milliseconds the last traced probe refresh took, or null where nothing measured it.
10028
+ *
10029
+ * **A refresh rather than a grid**, which is `PROBES_PER_FRAME` probes and is what a frame
10030
+ * actually pays. One frame behind, for the reason `distanceFieldMs` gives.
10031
+ */
10032
+ get indirectBakeMs() {
10033
+ return this.probeBaker?.bakeMs ?? null;
10034
+ }
10035
+ /** Ask for the last frame's figures, once the frame that recorded them has been submitted. */
10036
+ readDistanceFieldTimings() {
10037
+ this.fieldComposer?.readTimings();
10038
+ this.probeBaker?.readTimings();
10039
+ }
8900
10040
  /**
8901
10041
  * Whether a mesh is entirely behind the occluders declared this frame.
8902
10042
  *
@@ -8937,6 +10077,7 @@ export class WebGPURenderer {
8937
10077
  pass: null,
8938
10078
  outputTransform: 0,
8939
10079
  outputExposure: 1,
10080
+ jitter: this.passJitter,
8940
10081
  };
8941
10082
  /**
8942
10083
  * Let something outside this file draw into the frame. See `Renderer.registerPass`.
@@ -8966,7 +10107,53 @@ export class WebGPURenderer {
8966
10107
  prepareContext = {
8967
10108
  backend: 'webgpu',
8968
10109
  encoder: null,
10110
+ environment: null,
10111
+ distanceField: null,
10112
+ jitter: this.passJitter,
8969
10113
  };
10114
+ /**
10115
+ * The object handed to a pass as `PrepareContext.environment`, rebuilt only when it changes.
10116
+ *
10117
+ * **Mutated in place for the reason the context above is**: this runs once a frame for every
10118
+ * registered pass, and the house rule about per-frame allocation binds the seam as much as the
10119
+ * verbs. The view is the only field that can change identity, and it changes when the grid's
10120
+ * layer count does.
10121
+ */
10122
+ passEnvironment = {
10123
+ view: null,
10124
+ sampler: null,
10125
+ edge: 0,
10126
+ maxLod: 0,
10127
+ irradianceLevel: 0,
10128
+ irradiance: false,
10129
+ layers: 0,
10130
+ };
10131
+ /**
10132
+ * The frame's reconstruction jitter: one step of the sequence a frame, taken before any pass
10133
+ * prepares.
10134
+ *
10135
+ * **Here and not in `bindMeshPass`, for two reasons that were both defects.** A registered pass
10136
+ * prepares at `beginFrame`, before any mesh pass is bound, so a jitter chosen there could not
10137
+ * reach it — and the GPU-driven pipeline draws its whole world in `prepare`, unjittered, into a
10138
+ * resolve that un-jitters everything. And a frame that bound its mesh pass twice took two steps
10139
+ * of the sequence and resolved against the second.
10140
+ *
10141
+ * The sizes are the frame's by now: `ensureReconstruction` has run.
10142
+ */
10143
+ settleReconJitter() {
10144
+ if (!this.reconstructionWanted) {
10145
+ this.passJitter[0] = 0;
10146
+ this.passJitter[1] = 0;
10147
+ return;
10148
+ }
10149
+ this.reconFrameIndex += 1;
10150
+ this.reconPreviousJitter.set(this.reconJitter);
10151
+ jitterOffsetFor(this.reconFrameIndex, reconJitterPhases(this.renderWidth, this.surface.canvas.width), this.reconJitter);
10152
+ /* A render pixel is two over the render's size of the clip square; y upward, as the resolve's
10153
+ rows and the camera's own matrix both run. */
10154
+ this.passJitter[0] = (2 * this.reconJitter[0]) / this.renderWidth;
10155
+ this.passJitter[1] = (2 * this.reconJitter[1]) / this.renderHeight;
10156
+ }
8970
10157
  /**
8971
10158
  * Give every pass that owns a target its chance to fill it, before the frame's own opens.
8972
10159
  *
@@ -8981,6 +10168,33 @@ export class WebGPURenderer {
8981
10168
  if (this.preparingPasses === 0)
8982
10169
  return;
8983
10170
  this.prepareContext.encoder = encoder;
10171
+ /*
10172
+ * **The same test the lit pass applies to its own binding**: a view exists from the moment the
10173
+ * array is created, and what makes it *readable* is a completed bake that is not the one
10174
+ * currently open. Offering it earlier hands a pass a texture of undefined contents, which is
10175
+ * the failure `probeBaked` was introduced for on the forward path.
10176
+ */
10177
+ const usable = this.probeView !== null && this.probeBaked && !this.probePassActive;
10178
+ if (usable) {
10179
+ const environment = this.passEnvironment;
10180
+ environment.view = this.probeView;
10181
+ environment.sampler = this.probeSampler;
10182
+ environment.edge = this.probeEdge;
10183
+ environment.maxLod = this.probeMaxLod;
10184
+ environment.irradianceLevel = irradianceLevelFor(this.probeEdge);
10185
+ environment.irradiance = this.probeAmbient;
10186
+ environment.layers = this.probes?.layers ?? 1;
10187
+ this.prepareContext.environment = environment;
10188
+ }
10189
+ else {
10190
+ this.prepareContext.environment = null;
10191
+ }
10192
+ /*
10193
+ * The field the *previous* frame composed, because this runs at `beginFrame` and the
10194
+ * composition runs at `endFrame`. `PrepareContext.distanceField` says why that is right rather
10195
+ * than merely what happens.
10196
+ */
10197
+ this.prepareContext.distanceField = this.fieldComposer?.field ?? null;
8984
10198
  for (const definition of this.passes.definitions) {
8985
10199
  definition?.prepare?.(this.prepareContext);
8986
10200
  }
@@ -9172,7 +10386,12 @@ export class WebGPURenderer {
9172
10386
  depthFormat: DEPTH_FORMAT,
9173
10387
  /* The same four numbers this backend's own verbs are projected through. See `PassDevice`. */
9174
10388
  clipCorrection: CLIP_CORRECTION,
10389
+ /* And the one a pass that wrote its own WGSL wants, which is the same without the Y
10390
+ negation — that negation cancels `naga`'s, and a hand-written shader has none. */
10391
+ depthCorrection: DEPTH_CORRECTION,
9175
10392
  samples: this.samples,
10393
+ /* Whether a pass drawing the world owes the frame its jitter and its depth. See `PassDevice`. */
10394
+ reconstruction: this.reconstructionWanted,
9176
10395
  };
9177
10396
  }
9178
10397
  /**
@@ -9223,8 +10442,13 @@ export class WebGPURenderer {
9223
10442
  const cssWidth = Math.max(canvas.clientWidth, 1);
9224
10443
  const cssHeight = Math.max(canvas.clientHeight, 1);
9225
10444
  const origin = canvas.getBoundingClientRect();
9226
- const scaleX = canvas.width / cssWidth;
9227
- const scaleY = canvas.height / cssHeight;
10445
+ /*
10446
+ * **CSS pixels to the scene's pixels, not the viewer's.** A viewport is state on the frame's
10447
+ * own pass, which draws into the render-size targets, so a box measured in the drawing buffer
10448
+ * would be scaled by the reconstruction ratio and land off the side of a smaller attachment.
10449
+ */
10450
+ const scaleX = this.renderWidth / cssWidth;
10451
+ const scaleY = this.renderHeight / cssHeight;
9228
10452
  const left = rect.left - origin.left;
9229
10453
  const top = rect.top - origin.top;
9230
10454
  const w = Math.max(1, Math.round(rect.width * scaleX));
@@ -9251,8 +10475,9 @@ export class WebGPURenderer {
9251
10475
  * edge for the frames it is half-off. That asymmetry is the cost of the clamp, it is
9252
10476
  * recorded in the parity ledger, and the alternative was an exception.
9253
10477
  */
9254
- const targetWidth = canvas.width;
9255
- const targetHeight = canvas.height;
10478
+ /* The attachment this pass draws into, which is the scene's size — see `renderWidth`. */
10479
+ const targetWidth = this.renderWidth;
10480
+ const targetHeight = this.renderHeight;
9256
10481
  const clampedX = Math.max(0, Math.min(x, targetWidth));
9257
10482
  const clampedY = Math.max(0, Math.min(y, targetHeight));
9258
10483
  const clampedW = Math.max(0, Math.min(x + w, targetWidth) - clampedX);
@@ -9324,9 +10549,9 @@ export class WebGPURenderer {
9324
10549
  const pass = this.pass;
9325
10550
  if (this.surface.lost || pass === null)
9326
10551
  return;
9327
- const canvas = this.surface.context.canvas;
9328
- pass.setViewport(0, 0, canvas.width, canvas.height, 0, 1);
9329
- pass.setScissorRect(0, 0, canvas.width, canvas.height);
10552
+ /* The whole of what this pass draws into, which is the scene's size and not the viewer's. */
10553
+ pass.setViewport(0, 0, this.renderWidth, this.renderHeight, 0, 1);
10554
+ pass.setScissorRect(0, 0, this.renderWidth, this.renderHeight);
9330
10555
  }
9331
10556
  /**
9332
10557
  * Copy a rectangle of the frame into a 2D canvas, pixel for pixel.
@@ -9777,7 +11002,22 @@ export class WebGPURenderer {
9777
11002
  get probeBaked() {
9778
11003
  return this.probes !== null && this.probeFilledCount === this.probes.layers;
9779
11004
  }
9780
- /** Record that one layer now holds a convolution, without counting a rebake twice. */
11005
+ /**
11006
+ * Record that one layer now holds a convolution, without counting a rebake twice.
11007
+ *
11008
+ * **And rebuild the flat groups on the last one**, which is the third time this backend has paid
11009
+ * for a bind group holding whatever texture existed when it was built. `flatTextures` resolves
11010
+ * `uEnvironment` to a one-texel **white** stand-in while `probeBaked` is false, so a grid filled
11011
+ * by the trace rather than by `bakeProbeGrid` — which runs before any frame and therefore before
11012
+ * any group — leaves every flat group holding that white for the life of the renderer. The
11013
+ * uniform beside it flips to "use the grid" and the grid the shader reads is pure white.
11014
+ *
11015
+ * Measured on `demo/dev/bounce.html?seed=0`: every surface came back at exactly its own albedo
11016
+ * times 255, with the sun switched off and the trace pinned to a constant. It had been recorded
11017
+ * as undefined memory in the probe array; it is a deliberate white texture, read on purpose.
11018
+ * The refraction snapshot's own note two hundred lines below says the same thing about the same
11019
+ * mistake, and `setProbeGrid` says it about the array being replaced.
11020
+ */
9781
11021
  markProbeFilled(layer) {
9782
11022
  if (layer < 0 || layer >= this.probeFilled.length)
9783
11023
  return;
@@ -9785,6 +11025,8 @@ export class WebGPURenderer {
9785
11025
  return;
9786
11026
  this.probeFilled[layer] = 1;
9787
11027
  this.probeFilledCount++;
11028
+ if (this.probeBaked)
11029
+ this.rebuildFlatGroupsForNewRings();
9788
11030
  }
9789
11031
  /**
9790
11032
  * The array every probe is a layer of, sized for a grid.
@@ -9805,6 +11047,46 @@ export class WebGPURenderer {
9805
11047
  this.probeView = this.probeArray.createView({ dimension: '2d-array' });
9806
11048
  this.probeFilled = new Uint8Array(layers);
9807
11049
  this.probeFilledCount = 0;
11050
+ /*
11051
+ * **Cleared here, because a texture no pass has written holds undefined contents** — the same
11052
+ * sentence the directional shadow maps are cleared under, arrived at from the other end.
11053
+ *
11054
+ * A traced probe reads the array's own irradiance level back as the light that has already
11055
+ * bounced, so an unwritten array is not "nothing has bounced yet": it is whatever the
11056
+ * allocator left, amplified by every refresh. Measured on `demo/dev/bounce.html` with
11057
+ * `?seed=0`, a grid nothing had rasterised settled on a uniform **209 of 255** in every
11058
+ * channel, which reads as a working ambient term and is memory.
11059
+ *
11060
+ * **Every level, not only the one the bake writes.** The roughness chain is sampled by
11061
+ * reflective surfaces long before any bake has filled it, and a first fill writes the whole
11062
+ * chain only for a layer the scheduler has reached.
11063
+ */
11064
+ const clear = device.createCommandEncoder({ label: 'probe.clear' });
11065
+ const levels = irradianceLevelFor(this.probeEdge) + 1;
11066
+ for (let layer = 0; layer < layers; layer += 1) {
11067
+ for (let level = 0; level < levels; level += 1) {
11068
+ clear
11069
+ .beginRenderPass({
11070
+ label: 'probe.clear',
11071
+ colorAttachments: [
11072
+ {
11073
+ view: this.probeArray.createView({
11074
+ dimension: '2d',
11075
+ baseArrayLayer: layer,
11076
+ arrayLayerCount: 1,
11077
+ baseMipLevel: level,
11078
+ mipLevelCount: 1,
11079
+ }),
11080
+ clearValue: { r: 0, g: 0, b: 0, a: 1 },
11081
+ loadOp: 'clear',
11082
+ storeOp: 'store',
11083
+ },
11084
+ ],
11085
+ })
11086
+ .end();
11087
+ }
11088
+ }
11089
+ device.queue.submit([clear.finish()]);
9808
11090
  }
9809
11091
  /**
9810
11092
  * Declare where a grid's probes stand, and allocate the layers for them.
@@ -9955,7 +11237,7 @@ export class WebGPURenderer {
9955
11237
  const heldExposure = this.perFrameFloats[this.materialField('uOutputExposure')] ?? 1;
9956
11238
  this.perFrameInts[this.materialField('uOutputTransform')] = 0;
9957
11239
  this.perFrameFloats[this.materialField('uOutputExposure')] = 1;
9958
- this.materialSlot = -1;
11240
+ this.materials.dirty();
9959
11241
  for (let face = 0; face < PROBE_FACES.length; face++) {
9960
11242
  const aim = PROBE_FACES[face];
9961
11243
  if (aim === undefined)
@@ -10029,7 +11311,7 @@ export class WebGPURenderer {
10029
11311
  this.markProbeFilled(layer);
10030
11312
  this.perFrameInts[this.materialField('uOutputTransform')] = heldGrade;
10031
11313
  this.perFrameFloats[this.materialField('uOutputExposure')] = heldExposure;
10032
- this.materialSlot = -1;
11314
+ this.materials.dirty();
10033
11315
  this.probePassActive = false;
10034
11316
  this.rebuildFlatBindGroup();
10035
11317
  return true;
@@ -10479,7 +11761,13 @@ export class WebGPURenderer {
10479
11761
  label: 'post.resolvedDepth',
10480
11762
  size: [width, height],
10481
11763
  format: RESOLVED_DEPTH_FORMAT,
10482
- usage: USAGE,
11764
+ /*
11765
+ * **`COPY_SRC` only where a reconstruction is going to keep it**, because a usage is a
11766
+ * promise about the whole life of the texture and every frame that does not reconstruct
11767
+ * would be paying for one it never makes. A reconstruction copies this into its own target
11768
+ * at the end of the frame, which is what its disocclusion tests against next frame.
11769
+ */
11770
+ usage: this.quality.reconstruction > 0 ? USAGE | 0x1 : USAGE,
10483
11771
  });
10484
11772
  this.resolvedDepthView = this.resolvedDepth.createView();
10485
11773
  /*
@@ -10891,6 +12179,9 @@ export class WebGPURenderer {
10891
12179
  * the 2026-08-13 rule asks for — one decision, bound twice.
10892
12180
  */
10893
12181
  dispose() {
12182
+ for (const texture of this.retiredTextures)
12183
+ texture.destroy();
12184
+ this.retiredTextures.length = 0;
10894
12185
  const passDevice = this.passDevice();
10895
12186
  drainRegistry(this.passes, (definition) => definition.dispose?.(passDevice));
10896
12187
  this.passReads.clear();
@@ -10900,6 +12191,8 @@ export class WebGPURenderer {
10900
12191
  this.preparingPasses = 0;
10901
12192
  const computeDevice = this.computeDevice();
10902
12193
  drainRegistry(this.computes, (definition) => definition.dispose?.(computeDevice));
12194
+ this.fieldComposer?.dispose();
12195
+ this.fieldComposer = null;
10903
12196
  this.pass = null;
10904
12197
  this.encoder = null;
10905
12198
  this.skinPalettes.dispose();