@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
@@ -0,0 +1,2473 @@
1
+ /**
2
+ * The GPU-driven frame, as commands on a real encoder.
3
+ *
4
+ * **Every stage this runs already existed, twice.** `gpudriven/*.ts` is the arithmetic in
5
+ * TypeScript, `shaders/gpudriven/*.wgsl.ts` is the same arithmetic in WGSL, and
6
+ * `scripts/gpu-parity.mjs` runs both over generated input on a real device until they agree —
7
+ * nine checks as this was written. `gpudriven/pipeline.ts` says what each stage reads and writes, as
8
+ * frame-graph edges, and **the frame is scheduled by it**: each `prepare` records the stages,
9
+ * `scheduleGraph` keeps the ones something reads, and this encodes those in the order kept. What
10
+ * was missing, and what this is, is the encoder: the buffers, the bind groups, the targets and the
11
+ * two indirect draws.
12
+ *
13
+ * **It is a contributed pass, not a branch inside the renderer, and that is the design.**
14
+ * `registerPass` hands a definition a `GPUDevice` at registration and a `GPUCommandEncoder` once a
15
+ * frame before the frame's own render pass opens — which is exactly the window a pipeline that
16
+ * fills its own targets needs, and it is the seam `pass.ts` exists to provide. The alternative is
17
+ * rerouting `drawMesh` through clusters, which would mean a second copy of the standard material
18
+ * to draw anything at all; the plan refuses that in the same words this file does.
19
+ * `createRenderer({ pipeline: 'gpu-driven' })` is the *permission* — it fails at boot on a backend
20
+ * that cannot run this — and this is the implementation. A consumer states both.
21
+ *
22
+ * **What it draws.** Vertex colour times a material tint, one directional term with its specular
23
+ * highlight and its shadow, the forward path's hemispheric ambient or a baked probe's irradiance,
24
+ * the room a surface reflects, metalness, a glow scaled by the frame's clock, and DriftTexture
25
+ * materials decoded per pixel — the standard material's subset, which `CAPABILITIES.md` §3 lists
26
+ * with what it leaves out. The rigs that draw it are `DRAFT_SCENES`, never `SCENES`.
27
+ *
28
+ * **A mesh wholly outside the view is dropped before the cut reads a cluster of it**, since
29
+ * 2026-09-17: `instanceCull` flags each mesh against the frustum, the cut leaves a flagged mesh's
30
+ * clusters unselected, and the cluster cull skips whatever the cut did not select. `instances.ts`
31
+ * says why the flag is read there and not by the cluster cull: eight storage bindings a stage, and
32
+ * the cluster cull already had seven.
33
+ *
34
+ * **The shadow is this pipeline's own, and it costs what it costs.** One depth-only draw of every
35
+ * cluster from the light, into a 2048 map: **0.46 ms** on a million triangles, which is nearly as
36
+ * much again as the frame itself. There is no culling in it — the frame's cull is against the
37
+ * camera and a caster behind the viewer still shadows what is in front of them — so the obvious
38
+ * next measurement is a cull against the light's own box.
39
+ *
40
+ * **The visibility buffer is copied to a buffer once a frame, and that is a real cost.** The
41
+ * binning passes read `array<u32>` and a render attachment cannot be bound as one. The copy is
42
+ * ordered and correct where a fragment shader writing storage is neither — two fragments passing
43
+ * the depth test for one pixel race, and the loser can land last. `width` is rounded up to a
44
+ * multiple of 64 so that `bytesPerRow` is a multiple of 256 with no padding, which is what keeps
45
+ * the buffer linear: a padded row would put undefined words where the binning reads pixels, and
46
+ * an undefined word that happens to be zero is triangle 0 of cluster 0 — a real triangle.
47
+ *
48
+ * **The shading's pixel centre is the flipped row, and this is the one convention trap here.**
49
+ * WebGPU's framebuffer origin is the top-left, so row zero is where clip `y` is `+1`; `screenOf`
50
+ * maps clip `y = +1` to `height`. So the pixel at framebuffer row `y` is at screen `height - y`,
51
+ * and the flip is applied to the pixel rather than to the shared function — which is checked
52
+ * against the reference and must not learn about a framebuffer. Flipping all four points of a
53
+ * barycentric evaluation leaves the weights unchanged, which is why this is the whole of it. It is
54
+ * true of either projection, because it is a property of the viewport transform and not of the
55
+ * matrix.
56
+ *
57
+ * **The projection is `depthCorrection` and not `clipCorrection`.** The Y negation in the latter
58
+ * cancels the one `naga` writes into every *generated* vertex entry point; nothing here was
59
+ * generated, so taking it mirrors the picture — and mirrors the winding with it, which is how it
60
+ * reads as a culling problem. `PassDevice` carries both matrices and says which is which.
61
+ */
62
+ import { DEPTH_CLEAR, DEPTH_COMPARE, DEPTH_COMPARE_EQUAL, DEPTH_FORMAT, SHADOW_DEPTH_CLEAR, } from '../../depthConvention.js';
63
+ import { DEFAULT_RENDER_QUALITY } from '../../renderQuality.js';
64
+ import { SHADOW_FORMAT } from './depthPass.js';
65
+ import { SHADOW_CULL_CORRECTION, SHADOW_LOOKUP_CORRECTION, SHADOW_RASTER_CORRECTION, correctShadowMatrix, fitShadow, lightConeEye, } from '../../gpudriven/shadowCamera.js';
66
+ import { DRAW_INDIRECT_WORDS, PHASE_ONE, PHASE_TWO } from '../../gpudriven/compact.js';
67
+ import { BIN_DISPATCH_WORDS, BIN_GROUP_SIZE } from '../../gpudriven/materialBin.js';
68
+ import { CLUSTER_INDEX_CAP } from '../../gpudriven/indirect.js';
69
+ import { FRUSTUM_FLOATS, frustumPlanes } from '../../gpudriven/frustum.js';
70
+ import { VIS_EMPTY } from '../../gpudriven/visbuffer.js';
71
+ import { jitterClip } from '../../recon/jitter.js';
72
+ import { GPU_DRIVEN_ORDERED, GPU_DRIVEN_PASSES, GPU_DRIVEN_RESOURCES, gpuDrivenLive, gpuDrivenRefusal, gpuDrivenSupported, recordGpuDrivenStages, } from '../../gpudriven/pipeline.js';
73
+ import { createDeps, resetDeps } from '../../frame/deps.js';
74
+ import { createGraphPasses, createGraphScratch, scheduleGraph } from '../../frame/graphSchedule.js';
75
+ import { hzbMipCount, hzbMipSize } from '../../gpudriven/hzb.js';
76
+ import { CULL_CLUSTERS_WGSL, CULL_CONES, CULL_INSTANCES_WGSL, CULL_SETTINGS_FLOATS, HZB_REDUCE_WGSL, HZB_SEED_WGSL, LOD_CUT_WGSL, SHADOW_HZB_SEED_WGSL, } from '../../shaders/gpudriven/cull.wgsl.js';
77
+ import { COMPACT_CLUSTERS_WGSL } from '../../shaders/gpudriven/compact.wgsl.js';
78
+ import { MATERIAL_COUNT_WGSL, MATERIAL_OFFSETS_WGSL, MATERIAL_SCATTER_WGSL, } from '../../shaders/gpudriven/materialBin.wgsl.js';
79
+ import { MEDIUM_FOG } from '../../fog.js';
80
+ import { MATERIAL_SLOTS } from '../../shaders/gpudriven/materialTable.wgsl.js';
81
+ import { SHADE_MATERIAL_WGSL } from '../../shaders/gpudriven/shade.wgsl.js';
82
+ import { GPU_DRIVEN_BLIT_DEPTH_WGSL, GPU_DRIVEN_BLIT_WGSL, } from '../../shaders/gpudriven/present.wgsl.js';
83
+ import { StreamingScene } from '../../gpudriven/streamScene.js';
84
+ import { VISBUFFER_RASTER_WGSL } from '../../shaders/gpudriven/visbufferRaster.wgsl.js';
85
+ import { BLEND_RASTER_WGSL, BLEND_RESOLVE_WGSL } from '../../shaders/gpudriven/blendRaster.wgsl.js';
86
+ import { GPU_DRIVEN_VERTEX_FLOATS } from '../../gpudriven/sceneUpload.js';
87
+ import { GPU_DRIVEN_MATERIAL_FLOATS, collectPrograms, writeMaterialTable, } from '../../gpudriven/materialTable.js';
88
+ import { packDecodeTables } from '../../gpudriven/decodeTables.js';
89
+ import { DECODE_NODES_BYTES, DECODE_WEIGHTS_BYTES } from '../../shaders/gpudriven/decode.wgsl.js';
90
+ /* Bit values rather than the globals, which Node does not define — every `createTexture` in this
91
+ backend already does the same, so a module can be imported by a test that never sees a GPU. */
92
+ const COPY_SRC = 0x0004;
93
+ /* A texture's copy bit is not a buffer's: 0x01 against 0x0004, and using the buffer's here reads
94
+ as TEXTURE_BINDING, which is a usage the texture already had. The device caught it. */
95
+ const TEXTURE_COPY_SRC = 0x01;
96
+ /* And the same again for copying in: 0x02 on a texture, where the buffer's 0x0008 is
97
+ STORAGE_BINDING. The latent array's upload was refused over it; textureUsage.test.ts holds it. */
98
+ const TEXTURE_COPY_DST = 0x02;
99
+ const COPY_DST = 0x0008;
100
+ const UNIFORM = 0x0040;
101
+ const STORAGE = 0x0080;
102
+ const INDIRECT = 0x0100;
103
+ const TEXTURE_BINDING = 0x04;
104
+ const STORAGE_BINDING = 0x08;
105
+ const RENDER_ATTACHMENT = 0x10;
106
+ const QUERY_RESOLVE = 0x0200;
107
+ const MAP_READ = 0x0001;
108
+ /** The jitter of a frame that is not reconstructed, until `prepare` is handed the frame's own. */
109
+ const NO_JITTER = new Float32Array(2);
110
+ const MAP_MODE_READ = 0x0001;
111
+ /**
112
+ * Floats a material owns: tint rgb, emissive, roughness, specular, reflectivity, metalness.
113
+ *
114
+ * Five blocks since 2026-09-17 — the three texture programs, the UV scale and the map strengths
115
+ * joined the eight — and `gpudriven/materialTable.ts` writes them.
116
+ */
117
+ export const MATERIAL_FLOATS = GPU_DRIVEN_MATERIAL_FLOATS;
118
+ /** Floats a vertex in the one interleaved buffer: position, normal, colour, uv. */
119
+ export const VERTEX_FLOATS = GPU_DRIVEN_VERTEX_FLOATS;
120
+ /**
121
+ * Floats the shading pass's frame block holds. Matches `FRAME_BLOCK_WGSL`'s `Frame`.
122
+ *
123
+ * Two matrices and fifteen vectors: the camera's, the light's, then the eye, the light's direction
124
+ * and colour, the sky, the ground, the target's size, the shadow's settings in two, the probe's in
125
+ * two, the medium's four, and the visibility buffer's row.
126
+ */
127
+ const FRAME_FLOATS = 92;
128
+ /** Where each block starts in it, so the writer and the shader cannot drift apart. */
129
+ const FRAME_LIGHT_VIEW_PROJ = 16;
130
+ const FRAME_EYE = 32;
131
+ const FRAME_LIGHT_DIR = 36;
132
+ const FRAME_LIGHT_COLOUR = 40;
133
+ const FRAME_SKY = 44;
134
+ const FRAME_GROUND = 48;
135
+ const FRAME_SIZE = 52;
136
+ const FRAME_SHADOW = 56;
137
+ const FRAME_SHADOW_LIMITS = 60;
138
+ const FRAME_ENVIRONMENT = 64;
139
+ const FRAME_ENVIRONMENT_MIX = 68;
140
+ const FRAME_FOG_COLOUR = 72;
141
+ const FRAME_FOG_UNDERWATER = 76;
142
+ const FRAME_FOG_HEIGHT = 80;
143
+ const FRAME_FOG_RANGE = 84;
144
+ const FRAME_PITCH = 88;
145
+ /**
146
+ * Pixels the internal width is rounded up to.
147
+ *
148
+ * `copyTextureToBuffer` wants `bytesPerRow` to be a multiple of 256, and a visibility texel is
149
+ * four bytes — so a width that is a multiple of 64 needs no padding and the buffer stays linear.
150
+ * See the header for why a padded row is not merely wasteful.
151
+ */
152
+ const WIDTH_ALIGN = 64;
153
+ /** What a frame must leave behind: the colour the blit presents and the history phase one reads. */
154
+ const FRAME_LIVE = gpuDrivenLive(GPU_DRIVEN_RESOURCES);
155
+ /** Identifiers one frame's graph can name, which sizes the scheduler's working set once. */
156
+ const FRAME_IDENTIFIERS = Object.keys(GPU_DRIVEN_RESOURCES).length;
157
+ /** Floats a pyramid level is aligned to, so a level's byte offset is 256-aligned. */
158
+ const LEVEL_ALIGN = 64;
159
+ /**
160
+ * The two weighted-transparency targets' formats, which are `flatPass.ts`'s `oitTarget`'s.
161
+ *
162
+ * Sixteen-bit float for the accumulation because it holds a weighted sum that runs to thousands,
163
+ * and one byte for the revealage because it holds a product of numbers between zero and one.
164
+ */
165
+ const OIT_ACCUM_FORMAT = 'rgba16float';
166
+ const OIT_REVEAL_FORMAT = 'r8unorm';
167
+ /**
168
+ * The strides a streamed upload writes at, which are `StreamingScene`'s own.
169
+ *
170
+ * Named here rather than imported so the two files can be read side by side; `streamScene.ts`
171
+ * carries the same three and `gpuDrivenPass.test.ts` asserts the buffers are sized by them.
172
+ */
173
+ const STREAM_CULL_FLOATS = 8;
174
+ const STREAM_LOD_FLOATS = 6;
175
+ const STREAM_META_WORDS = 4;
176
+ /**
177
+ * What the frame is timed in, one pair of timestamps each.
178
+ *
179
+ * **Measured rather than reasoned, because the whole wave rests on a cost claim.** A frame-time
180
+ * reading from `requestAnimationFrame` measures the submission loop and nothing else — with vsync
181
+ * off it reports half a millisecond for a scene of a million triangles, because the device is
182
+ * still working when the callback returns. These are the device's own clock.
183
+ *
184
+ * Only where the device has `timestamp-query`, which `createRenderer` asks for when
185
+ * `quality.gpuTiming` is set — `?gputiming=1` on the harness pages. Without it every figure is
186
+ * `null`, which is `gpuTimer.ts`'s rule: zero is a claim and unmeasured is not zero.
187
+ */
188
+ export const GPU_DRIVEN_STAGES = [
189
+ 'shadow',
190
+ 'instanceCull',
191
+ 'cut',
192
+ 'phaseOneDraw',
193
+ 'pyramid',
194
+ 'phaseTwoCull',
195
+ 'phaseTwoDraw',
196
+ 'blendCull',
197
+ 'blendDraw',
198
+ 'blendResolve',
199
+ 'bin',
200
+ 'shade',
201
+ ];
202
+ /**
203
+ * The frame's four draw lists, each counted by the clusters it drew: phase one (what was visible
204
+ * last frame and still is), phase two (what the pyramid found newly visible), the blended half, and
205
+ * the light's map.
206
+ *
207
+ * **The occlusion cull's own figure.** A cluster the cull kept is one instance of its list's
208
+ * indirect draw, so the second word of each list's arguments is that list's count — the number a
209
+ * city in a street and above its roofs is there to show. Read back only where `countDrawn` asks.
210
+ */
211
+ export const GPU_DRIVEN_LISTS = ['phaseOne', 'phaseTwo', 'blend', 'shadow'];
212
+ /**
213
+ * The pipeline, as one registered pass.
214
+ *
215
+ * Built once at registration, recorded once a frame in `prepare`, and composited in `draw`.
216
+ * `setView` and `resize` are the two things a caller says, and neither allocates on a frame that
217
+ * did not change size.
218
+ */
219
+ /**
220
+ * Refuse, by name, a storage buffer the device cannot bind whole.
221
+ *
222
+ * **A buffer that can be created is not a buffer that can be bound**: `maxBufferSize` and
223
+ * `maxStorageBufferBindingSize` are two limits, and every buffer here is bound whole. Past the
224
+ * second, what a device answers is an invalid bind group — a warning a frame, a hundred frames,
225
+ * and a scene that is simply absent, with nothing naming the capacity that caused it. The voxel
226
+ * sandbox's port at a radius of ten did exactly that. `select.ts` asks the adapter for all the
227
+ * binding it offers; this is what a device that offers too little says.
228
+ */
229
+ /**
230
+ * What a buffer's size comes from, which is what a refusal tells the caller to make smaller: most
231
+ * are the scene's declared capacity, the targets are the frame's pixels, and the light's pyramid is
232
+ * its map.
233
+ */
234
+ const SIZED_BY = {
235
+ capacity: "The scene's declared capacity sizes it: a smaller capacity fits",
236
+ frame: "The frame's size sizes it: a smaller frame fits",
237
+ map: "The shadow map's size sizes it: a smaller mapSize fits",
238
+ };
239
+ function bindable(device, label, bytes, sizedBy = 'capacity') {
240
+ const ceiling = device.limits
241
+ ?.maxStorageBufferBindingSize;
242
+ if (typeof ceiling === 'number' && bytes > ceiling) {
243
+ throw new Error(`[driftengine] the gpu-driven ${label} needs ${bytes} bytes bound at once, and this device ` +
244
+ `binds at most ${ceiling}. ${SIZED_BY[sizedBy]}, or an adapter that offers a larger ` +
245
+ `maxStorageBufferBindingSize.`);
246
+ }
247
+ }
248
+ /**
249
+ * The two buffers a streaming scene writes its geometry into, as the scene sees them.
250
+ *
251
+ * **The scene keeps no copy of its vertices and indices**, so this is where they live: written
252
+ * when a mesh is placed, from a scratch array the scene reuses, which `queue.writeBuffer` copies
253
+ * before it returns. Offsets and sizes are whole records, so always whole multiples of four bytes.
254
+ */
255
+ function geometrySink(queue, indices, vertices) {
256
+ return {
257
+ writeVertices(firstVertex, rows, count) {
258
+ queue.writeBuffer(vertices, firstVertex * VERTEX_FLOATS * 4, rows, 0, count * VERTEX_FLOATS);
259
+ },
260
+ writeIndices(firstIndex, values, count) {
261
+ queue.writeBuffer(indices, firstIndex * 4, values, 0, count);
262
+ },
263
+ };
264
+ }
265
+ export class GpuDrivenPass {
266
+ label = 'driftengine.gpu-driven';
267
+ scene;
268
+ materials;
269
+ device = null;
270
+ width = 0;
271
+ height = 0;
272
+ /** The width the targets are actually allocated at: `width` rounded up. See `WIDTH_ALIGN`. */
273
+ stride = 0;
274
+ frameParams = new Float32Array(FRAME_FLOATS);
275
+ planes = new Float32Array(FRUSTUM_FLOATS);
276
+ /** The light's box as six planes, from the matrix the lookup reads. */
277
+ lightPlanes = new Float32Array(FRUSTUM_FLOATS);
278
+ cullSettings = new Float32Array(CULL_SETTINGS_FLOATS);
279
+ lodParams = new Float32Array(7);
280
+ binSettings = new Uint32Array(4);
281
+ viewProj = new Float32Array(16);
282
+ corrected = new Float32Array(16);
283
+ /** The light's own, in the OpenGL convention, and the two corrections of it. */
284
+ light = new Float32Array(16);
285
+ lightRaster = new Float32Array(16);
286
+ lightLookup = new Float32Array(16);
287
+ /** The scene's bounding sphere, computed once: centre and radius. */
288
+ bounds = new Float32Array(4);
289
+ /** Which programs each material names, and the programs themselves, once each. */
290
+ programs;
291
+ /** Those programs flattened into what the shading pass binds. */
292
+ decode;
293
+ /**
294
+ * Every cluster's bounds, cone and error **through the transform of the mesh it belongs to**.
295
+ *
296
+ * Computed at construction and held, because it is what both the cull buffer and the LOD buffer
297
+ * are written from and what the light's box is fitted to — one transform, read three times. This
298
+ * pipeline never moves a mesh, so it is a property of the scene; a pipeline that did would have
299
+ * to rewrite these two buffers on the frame it moved one.
300
+ */
301
+ /**
302
+ * Each mesh's world sphere, round its clusters' world spheres — `instances.ts`. Fitted here
303
+ * rather than at mount so a cluster naming a mesh the transforms do not have is refused where the
304
+ * scene was handed in.
305
+ */
306
+ meshCount;
307
+ shadowSpan = 1;
308
+ shadowSize;
309
+ /** What the frame offered this time, or null. Read in `writeFrame` and nowhere else. */
310
+ environment = null;
311
+ shadowMaxDistance;
312
+ shadowMaxSlope;
313
+ shadowTaps;
314
+ clipCorrection = null;
315
+ view = null;
316
+ /* Static, uploaded once. */
317
+ clusterCull = null;
318
+ clusterLod = null;
319
+ clusterMeta = null;
320
+ instanceSpheres = null;
321
+ indices = null;
322
+ vertices = null;
323
+ transformBuffer = null;
324
+ materialOf = null;
325
+ materialTable = null;
326
+ jobs = null;
327
+ /**
328
+ * **The light's own front half, in the camera's two phases**: the camera's cut with no mesh
329
+ * hidden, a cull against the light's box from the sun's side, and a compaction into the list each
330
+ * half of the map draws — phase one what the light kept last frame, phase two the rest against a
331
+ * pyramid of what phase one drew. Every buffer the light writes is its own. See `drawShadow`.
332
+ */
333
+ shadowList = null;
334
+ shadowListTwo = null;
335
+ shadowArgs = null;
336
+ shadowArgsTwo = null;
337
+ shadowSelected = null;
338
+ shadowKeep = null;
339
+ shadowPlanes = null;
340
+ shadowCullSettings = null;
341
+ shadowCullSettingsTwo = null;
342
+ /**
343
+ * The light's cull settings: its cone eye, stood `LIGHT_CONE_DISTANCE` toward the sun from the
344
+ * camera's (`lightConeEye`), whether to read the pyramid, the pyramid's shape, the count and the
345
+ * cones, which are on. One array, written into phase one's buffer and then phase two's.
346
+ */
347
+ shadowCullValues = new Float32Array(CULL_SETTINGS_FLOATS);
348
+ shadowConeEye = [0, 0, 0];
349
+ shadowCompactSettings = null;
350
+ shadowCompactSettingsTwo = null;
351
+ /**
352
+ * **The light's history, one word a cluster, in two halves that swap each frame the map is
353
+ * drawn** — and only then, so a frame with no shadow leaves the next one reading what the last
354
+ * one wrote. One history per view, as `twoPhase.ts` says: the camera's would put what the camera
355
+ * saw into the light's phase one.
356
+ */
357
+ shadowHistories = null;
358
+ shadowFlipped = false;
359
+ /**
360
+ * The light's pyramid: half its map and every level below it (`shadowPyramidBase`), stored
361
+ * turned over so the camera's reduction and cull read it, and where each level starts.
362
+ */
363
+ shadowHzb = null;
364
+ shadowLevels = [];
365
+ shadowLevelStart = null;
366
+ shadowSeedSize = null;
367
+ /** The raster's matrix with its depth turned over, which is what the light's cull projects by. */
368
+ shadowCullMatrix = null;
369
+ lightCull = new Float32Array(16);
370
+ instanceNone = null;
371
+ shadowLodGroup = null;
372
+ shadowCullGroups = null;
373
+ /** Each half's compaction, for each way round the history is: `[flipped][phase]`. */
374
+ shadowCompactGroups = null;
375
+ shadowSeedGroup = null;
376
+ shadowReduceGroups = [];
377
+ shadowSeedPipeline = null;
378
+ shadowMatrix = null;
379
+ shadowTexture = null;
380
+ decodeNodes = null;
381
+ decodeWeights = null;
382
+ latents = null;
383
+ latentsView = null;
384
+ latentClamp = null;
385
+ latentRepeat = null;
386
+ shadowView = null;
387
+ /**
388
+ * What is bound as the probe where the frame has none.
389
+ *
390
+ * **One texel, one layer, and it exists because a bind group must supply every binding.** The
391
+ * alternative is a second pipeline and a second layout for the unlit case, which is two copies
392
+ * of the shading pass to avoid one 4-byte texture. The renderer binds a stand-in in exactly the
393
+ * same situation and for exactly the same reason — see `depthPass.ts` on the peel.
394
+ */
395
+ emptyEnvironment = null;
396
+ emptyEnvironmentView = null;
397
+ emptySampler = null;
398
+ /** The probe the last frame offered, so the bind group is rebuilt only when it changes. */
399
+ environmentView = null;
400
+ environmentSampler = null;
401
+ /* Per frame. */
402
+ planesBuffer = null;
403
+ viewProjBuffer = null;
404
+ /**
405
+ * The two numbers the raster's alpha test needs and nothing else in that stage carries.
406
+ *
407
+ * **A buffer of its own rather than four more floats on `viewProjBuffer`**, which the instance
408
+ * cull also binds and declares as `array<f32, 16>`: growing a buffer two shaders read means both
409
+ * have an opinion about what is past the sixteenth float, and one of them would be wrong on the
410
+ * day somebody used the spare.
411
+ */
412
+ rasterParams = null;
413
+ /**
414
+ * The transparent half's own buffers, textures and pipelines.
415
+ *
416
+ * **`zeroHistory` is what makes the cut's second run see every cluster.** The compaction
417
+ * filters on `history[i] != 0` against its phase, which is how the two occlusion phases divide
418
+ * the scene between them; the blended set is not divided, so it is compacted as phase one against
419
+ * a history that is all zeros — every cluster then lands on the side that draws. `historySink` is
420
+ * the history that run writes and nothing reads, which is the price of leaving the compaction
421
+ * shader alone rather than growing it a third phase nothing else would ever take.
422
+ */
423
+ blendList = null;
424
+ blendArgs = null;
425
+ blendLodBuffer = null;
426
+ blendCompactSettings = null;
427
+ zeroHistory = null;
428
+ historySink = null;
429
+ oitAccum = null;
430
+ oitReveal = null;
431
+ oitAccumView = null;
432
+ oitRevealView = null;
433
+ blendPipeline = null;
434
+ blendResolvePipeline = null;
435
+ blendLodGroup = null;
436
+ blendCompactGroup = null;
437
+ blendRasterGroup = null;
438
+ blendResolveGroup = null;
439
+ /** Seven floats: the cut's six, and a seventh saying which half of the frame it is selecting. */
440
+ lodParamsBlend = new Float32Array(7);
441
+ rasterParamValues = new Float32Array(4);
442
+ /** One a phase, for the reason `writeFrame` gives. */
443
+ /** Phase one, phase two, and the blended run: see `CULL_CONES` for why the third exists. */
444
+ cullBuffers = null;
445
+ lodBuffer = null;
446
+ /** One word a mesh, written by the instance cull and read by the cut. */
447
+ instanceFlags = null;
448
+ frameBuffer = null;
449
+ selected = null;
450
+ keep = null;
451
+ history = null;
452
+ drawn = null;
453
+ lists = null;
454
+ args = null;
455
+ phaseSettings = null;
456
+ /* Sized to the target. */
457
+ visibilityTexture = null;
458
+ depthTexture = null;
459
+ colourTexture = null;
460
+ colourView = null;
461
+ visibility = null;
462
+ hzb = null;
463
+ levelStart = null;
464
+ levels = [];
465
+ binCounts = null;
466
+ binOffsets = null;
467
+ binCursors = null;
468
+ binDispatch = null;
469
+ binPixels = null;
470
+ binSettingsBuffer = null;
471
+ /* Pipelines. */
472
+ instancePipeline = null;
473
+ lodPipeline = null;
474
+ cullPipeline = null;
475
+ compactPipeline = null;
476
+ seedPipeline = null;
477
+ reducePipeline = null;
478
+ countPipeline = null;
479
+ offsetsPipeline = null;
480
+ scatterPipeline = null;
481
+ shadePipeline = null;
482
+ rasterPipeline = null;
483
+ shadowPipeline = null;
484
+ blitPipeline = null;
485
+ /** See `GpuDrivenShadowOptions.presentDepth`: what the consumer asked for. */
486
+ presentDepthAsked;
487
+ /**
488
+ * Whether the blit hands the frame this pass's depth: when asked, and whenever the renderer
489
+ * reconstructs, which reprojects every pixel's history through that depth. Settled at `init`,
490
+ * where the device says which; the pipeline follows it.
491
+ */
492
+ presentDepth = false;
493
+ /**
494
+ * The frame's jitter, as `prepare` was last handed it: zero until then, and on every frame that
495
+ * is not reconstructed. See `PrepareContext.jitter`.
496
+ */
497
+ jitter = NO_JITTER;
498
+ /** See `GpuDrivenShadowOptions.followRadius`. Unset, the map is fitted to the whole scene. */
499
+ shadowFollow;
500
+ frameDepthFormat = null;
501
+ /* Bind groups that outlive a resize. */
502
+ lodGroup = null;
503
+ instanceGroup = null;
504
+ compactGroups = null;
505
+ rasterGroups = null;
506
+ shadowGroup = null;
507
+ shadowGroupTwo = null;
508
+ jobGroups = [];
509
+ /* Bind groups rebuilt on resize. */
510
+ cullGroups = null;
511
+ seedGroup = null;
512
+ reduceGroups = [];
513
+ countGroup = null;
514
+ offsetsGroup = null;
515
+ scatterGroup = null;
516
+ shadeGroup = null;
517
+ blitGroup = null;
518
+ /* The device's own clock. Null everywhere when the feature was not granted. */
519
+ queries = null;
520
+ queryResolve = null;
521
+ queryRead = null;
522
+ queryCopied = false;
523
+ queryMapping = false;
524
+ stageMs = new Float64Array(GPU_DRIVEN_STAGES.length);
525
+ timed = false;
526
+ /**
527
+ * **Whether phase two and the blended run test clusters against the depth pyramid** — the
528
+ * occlusion cull — **and the light's second half against its own.** On by default and off only
529
+ * to measure it: the instance cull is the frustum alone and phase one never reads a pyramid, so
530
+ * with this off each view draws every cluster its frustum and cones keep, and the drawn count's
531
+ * difference is the occlusion cull's own share. The pyramids are still built; a frame that
532
+ * measures does not also save their cost.
533
+ */
534
+ occlusion = true;
535
+ /**
536
+ * **Whether the pass reads back how many clusters each list drew.** Off, it encodes nothing for
537
+ * it and makes no buffer, which is why it is asked for rather than always on: the figure is a
538
+ * demo's readout and a measurement's, and a consumer who reads neither pays nothing.
539
+ */
540
+ countDrawn = false;
541
+ /* The four counts' copy, made the first frame one is asked for. */
542
+ counts = null;
543
+ countsCopied = false;
544
+ countsMapping = false;
545
+ /* A word a list, and the map's second half beside its first. */
546
+ drawnCounts = new Uint32Array(GPU_DRIVEN_LISTS.length + 1);
547
+ counted = false;
548
+ /** Whether `history` and `drawn` have been swapped an odd number of times. */
549
+ flipped = false;
550
+ /*
551
+ * **This frame's graph**, recorded and scheduled every `prepare` and allocated once here: twelve
552
+ * nodes and the identifiers `GPU_DRIVEN_RESOURCES` names. `shape` is written in place for the
553
+ * same reason.
554
+ */
555
+ graph = createDeps(GPU_DRIVEN_PASSES.length, 64);
556
+ scheduled = createGraphPasses(GPU_DRIVEN_PASSES.length);
557
+ graphScratch = createGraphScratch(GPU_DRIVEN_PASSES.length, FRAME_IDENTIFIERS);
558
+ shape = {
559
+ shadowed: true,
560
+ blended: false,
561
+ };
562
+ gradeWarned = false;
563
+ constructor(scene, materials, shadow = {}) {
564
+ if (materials.length > MATERIAL_SLOTS) {
565
+ throw new Error(`[driftengine] the gpu-driven pass holds ${MATERIAL_SLOTS} materials and was given ` +
566
+ `${materials.length}. See MATERIAL_SLOTS in shade.wgsl.ts for why it is a fixed table.`);
567
+ }
568
+ /*
569
+ * **Packed at construction so a program past a limit is refused where it was handed in**,
570
+ * with the limit in the message, rather than at the first frame on a device.
571
+ */
572
+ this.programs = collectPrograms(materials);
573
+ this.decode = packDecodeTables(this.programs.programs);
574
+ this.scene = scene;
575
+ this.materials = materials;
576
+ this.shadowSize = shadow.mapSize ?? DEFAULT_RENDER_QUALITY.directionalShadowMapSize;
577
+ this.shadowMaxDistance =
578
+ shadow.maxDistance ?? DEFAULT_RENDER_QUALITY.directionalShadowMaxDistance;
579
+ this.shadowMaxSlope = shadow.maxSlope ?? DEFAULT_RENDER_QUALITY.directionalShadowMaxSlope;
580
+ this.shadowTaps = shadow.taps ?? DEFAULT_RENDER_QUALITY.shadowFilterTaps;
581
+ this.presentDepthAsked = shadow.presentDepth === true;
582
+ this.shadowFollow = shadow.followRadius;
583
+ /*
584
+ * **The scene's extent, and it moves now.** This used to be computed once here, because the
585
+ * clusters were uploaded at construction and the transforms with them — a box that stood still
586
+ * and shadow edges that stayed put, which `shadowCamera.ts` records the price of. A streaming
587
+ * scene keeps its own bounds and `prepare` refits them when a mesh arrives or leaves, so the
588
+ * box follows the world and its edges crawl. That is a named limitation rather than an
589
+ * oversight: `specs/2026-09-18-gpu-driven-living-scene-design.md` §3.4 says why neither demo
590
+ * this was built for cares, and that cascades are the eventual answer.
591
+ */
592
+ this.meshCount = scene.capacity.meshes;
593
+ scene.sceneBounds(this.bounds);
594
+ }
595
+ /** How many clusters the scene holds, which every per-cluster dispatch is sized by. */
596
+ get clusterCount() {
597
+ return this.scene.capacity.clusters;
598
+ }
599
+ /**
600
+ * What this frame is looking at. Called before `beginFrame`, once, by the caller.
601
+ *
602
+ * Held by reference rather than copied: the caller owns a `GpuDrivenView` it fills each frame,
603
+ * and copying it here would allocate on a per-frame path for no gain.
604
+ */
605
+ setView(view) {
606
+ this.view = view;
607
+ }
608
+ /**
609
+ * Size the targets. Called on mount and whenever the frame's own size changes.
610
+ *
611
+ * **Dispose and recreate rather than resize**, which is `passTarget.ts`'s rule and is right for
612
+ * the same reason: a resize is not a per-frame path and a mutable target is one more thing to
613
+ * keep consistent.
614
+ */
615
+ resize(width, height) {
616
+ const wanted = Math.max(1, Math.floor(width));
617
+ const tall = Math.max(1, Math.floor(height));
618
+ if (wanted === this.width && tall === this.height)
619
+ return;
620
+ this.width = wanted;
621
+ this.height = tall;
622
+ this.stride = Math.ceil(wanted / WIDTH_ALIGN) * WIDTH_ALIGN;
623
+ if (this.device !== null)
624
+ this.buildTargets(this.device);
625
+ }
626
+ init(device) {
627
+ if (device.backend !== 'webgpu') {
628
+ /* Loud at registration rather than silent at the first frame: the refusal is the same
629
+ sentence `createRenderer` throws, because it is the same fact about the backend. */
630
+ throw new Error(`[driftengine] ${gpuDrivenRefusal('webgl2')}`);
631
+ }
632
+ if (!gpuDrivenSupported('webgpu'))
633
+ throw new Error(`[driftengine] ${gpuDrivenRefusal('webgpu')}`);
634
+ this.device = device.device;
635
+ /*
636
+ * **The frame's depth format, kept because `buildPipelines` is handed a `GPUDevice`** and a
637
+ * raw device does not know what the frame it belongs to attached. Only read where the depth is
638
+ * actually handed over.
639
+ */
640
+ this.frameDepthFormat = device.depthFormat;
641
+ /*
642
+ * **Handed over under reconstruction whether or not it was asked for.** A pixel of this
643
+ * pipeline's with no depth in the frame reads as the clear — infinitely far — so the resolve
644
+ * never moves its history for a sliding camera, and everything this pass drew trails the
645
+ * picture. The city came back with its windows forty pixels from the native frame.
646
+ */
647
+ this.presentDepth = this.presentDepthAsked || device.reconstruction;
648
+ /*
649
+ * **`depthCorrection`, not `clipCorrection`, and the difference is one negated row.** The Y
650
+ * negation in the other matrix cancels the one `naga` writes into every generated vertex
651
+ * entry point; these shaders are hand-written and carry no such line, so taking it mirrors the
652
+ * picture vertically. See `PassDevice`.
653
+ */
654
+ this.clipCorrection = new Float32Array(device.depthCorrection);
655
+ if (device.device.features.has('timestamp-query')) {
656
+ const pairs = GPU_DRIVEN_STAGES.length * 2;
657
+ this.queries = device.device.createQuerySet({
658
+ label: 'gpu-driven timings',
659
+ type: 'timestamp',
660
+ count: pairs,
661
+ });
662
+ this.queryResolve = device.device.createBuffer({
663
+ label: 'gpu-driven timings resolve',
664
+ size: pairs * 8,
665
+ usage: QUERY_RESOLVE | COPY_SRC,
666
+ });
667
+ this.queryRead = device.device.createBuffer({
668
+ label: 'gpu-driven timings read',
669
+ size: pairs * 8,
670
+ usage: COPY_DST | MAP_READ,
671
+ });
672
+ }
673
+ this.buildBuffers(device.device);
674
+ this.buildPipelines(device.device, device.format, device.samples);
675
+ if (this.width > 0)
676
+ this.buildTargets(device.device);
677
+ }
678
+ storage(device, label, data, extra = 0) {
679
+ const size = Math.max(4, Math.ceil(data.byteLength / 4) * 4);
680
+ bindable(device, label, size);
681
+ const buffer = device.createBuffer({
682
+ label: `gpu-driven ${label}`,
683
+ size,
684
+ usage: STORAGE | COPY_DST | extra,
685
+ });
686
+ device.queue.writeBuffer(buffer, 0, data);
687
+ return buffer;
688
+ }
689
+ empty(device, label, bytes, extra = 0, sizedBy = 'capacity') {
690
+ const size = Math.max(4, bytes);
691
+ bindable(device, label, size, sizedBy);
692
+ return device.createBuffer({
693
+ label: `gpu-driven ${label}`,
694
+ size,
695
+ usage: STORAGE | COPY_DST | extra,
696
+ });
697
+ }
698
+ /**
699
+ * Every buffer the scene lives in, at its declared capacity.
700
+ *
701
+ * **Called `buildStatic` until 2026-09-18**, and the word was the whole of what changed: these
702
+ * are sized by `scene.capacity` rather than by what the scene currently holds, so there is room
703
+ * for a mesh that has not arrived yet. A static scene declares a capacity equal to its content,
704
+ * so nothing about it moves.
705
+ *
706
+ * **The scene writes two of them itself and keeps no copy of either.** `vertices` and `indices`
707
+ * are made empty at capacity and handed to the scene as a `GeometrySink`; the interleave —
708
+ * twelve floats a vertex, position, normal, colour, uv and glow — is the scene's. It is one
709
+ * buffer rather than five because of a device limit: a compute stage may bind eight storage
710
+ * buffers by default, and the shading pass wanted twelve and was refused at pipeline creation.
711
+ *
712
+ * **The two culling buffers are already in the world.** What a bake writes is a cluster in its
713
+ * mesh's own space, and this pass used to upload that — while the raster and the shading both
714
+ * multiplied by `transforms[mesh]`. A mesh placed anywhere but the origin was then frustum-culled
715
+ * against the wrong region, cone-culled with the wrong orientation, LOD-cut on a misprojected
716
+ * error and occlusion-tested on the wrong rectangle, and every rig in `DRAFT_SCENES` stood at the
717
+ * origin so nothing ever said so. `StreamingScene` computes them when a mesh is placed.
718
+ */
719
+ buildBuffers(device) {
720
+ const count = this.scene.capacity.clusters;
721
+ this.clusterCull = this.storage(device, 'cluster bounds', this.scene.cull);
722
+ this.clusterLod = this.storage(device, 'cluster errors', this.scene.lod);
723
+ this.clusterMeta = this.storage(device, 'cluster meta', this.scene.meta);
724
+ this.instanceSpheres = this.storage(device, 'instance spheres', this.scene.instanceSpheres);
725
+ const capacity = this.scene.capacity;
726
+ const indices = this.empty(device, 'indices', capacity.indices * 4);
727
+ const vertices = this.empty(device, 'vertices', capacity.vertices * VERTEX_FLOATS * 4);
728
+ this.indices = indices;
729
+ this.vertices = vertices;
730
+ this.transformBuffer = this.storage(device, 'transforms', this.scene.transforms);
731
+ this.materialOf = this.storage(device, 'material of', this.scene.materialOf);
732
+ /*
733
+ * **The journal is taken and thrown away, because these buffers now hold all of it.** A scene
734
+ * filled before the pass was mounted has every span outstanding, and without this the first
735
+ * frame would upload the whole scene a second time — which is correct and wasteful, and would
736
+ * make "a frame that changed nothing uploads nothing" false for exactly one frame.
737
+ */
738
+ /*
739
+ * **The scene writes its own geometry into these two, and keeps none of it.** A mesh placed
740
+ * before this pass mounted arrives now; every one after arrives when it is placed. A scene
741
+ * already attached to a pass is refused here, by name.
742
+ */
743
+ this.scene.attach(geometrySink(device.queue, indices, vertices));
744
+ this.scene.takeDirty();
745
+ this.materialTable = device.createBuffer({
746
+ label: 'gpu-driven materials',
747
+ size: MATERIAL_SLOTS * MATERIAL_FLOATS * 4,
748
+ usage: UNIFORM | COPY_DST,
749
+ });
750
+ /*
751
+ * **Roughness defaults to one and specular to zero**, which is the matte this pipeline drew
752
+ * before the highlight existed — so a caller that says nothing gets the frame it already had.
753
+ * The table is written through a DataView because one of its blocks is integers.
754
+ */
755
+ device.queue.writeBuffer(this.materialTable, 0, writeMaterialTable(this.materials, this.programs.indices, MATERIAL_SLOTS));
756
+ /*
757
+ * One 256-byte block a material, holding the one number a shading dispatch cannot otherwise
758
+ * learn: which material it is. There is no builtin for "which dispatch am I", and a shader per
759
+ * material would be a pipeline per material.
760
+ */
761
+ const jobs = new Uint32Array(Math.max(1, this.materials.length) * 64);
762
+ for (let m = 0; m < this.materials.length; m += 1)
763
+ jobs[m * 64] = m;
764
+ this.jobs = device.createBuffer({
765
+ label: 'gpu-driven jobs',
766
+ size: Math.max(256, jobs.byteLength),
767
+ usage: UNIFORM | COPY_DST,
768
+ });
769
+ device.queue.writeBuffer(this.jobs, 0, jobs);
770
+ /*
771
+ * **The light's own list, and it is not the visibility pass's.** The raster's vertex stage keys
772
+ * off `clusterList[instance_index]`, so a shadow draw is the same shader with a different list
773
+ * — and the frame's list is culled against the *camera*: a caster standing behind the viewer is
774
+ * off screen and still throwing a shadow across everything in front of them.
775
+ *
776
+ * **It was the identity until 2026-09-18**: every cluster slot the scene had room for, live or
777
+ * not, at every level at once, 384 vertex invocations each. The voxel sandbox's port measured
778
+ * that at 2.10 ms of shadow at radius 6 and 9.04 at radius 14 — a cost set by the memory budget
779
+ * rather than by anything the light could see. Now the front half runs a fourth time, as the
780
+ * blended half runs a second: the camera's cut with no mesh hidden, a cull against the light's
781
+ * own box with the pyramid and the cones off, and a compaction into this list against a history
782
+ * of zeros. **No new binding in any stage** — new buffers and groups, the same three pipelines.
783
+ */
784
+ this.shadowList = this.empty(device, 'shadow list', count * 4);
785
+ this.shadowListTwo = this.empty(device, 'shadow list two', count * 4);
786
+ /* Each half's draw block: a cluster's index cap and a count the compaction adds to. */
787
+ const shadowArgs = new Uint32Array(DRAW_INDIRECT_WORDS);
788
+ shadowArgs[0] = CLUSTER_INDEX_CAP;
789
+ this.shadowArgs = this.storage(device, 'draw shadow', shadowArgs, INDIRECT | COPY_SRC);
790
+ this.shadowArgsTwo = this.storage(device, 'draw shadow two', shadowArgs, INDIRECT | COPY_SRC);
791
+ this.shadowSelected = this.empty(device, 'shadow selected', count * 4);
792
+ this.shadowKeep = this.empty(device, 'shadow keep', count * 4);
793
+ this.shadowPlanes = this.empty(device, 'shadow planes', FRUSTUM_FLOATS * 4);
794
+ this.shadowHistories = [
795
+ this.empty(device, 'shadow history a', count * 4),
796
+ this.empty(device, 'shadow history b', count * 4),
797
+ ];
798
+ /* The pyramid, from half the map down: its shape is the map's, which is fixed at mount. */
799
+ const shadowBase = hzbMipSize(this.shadowSize, this.shadowSize);
800
+ const shadowPyramid = this.pyramidLevels(device, 'shadow level', shadowBase.width, shadowBase.height);
801
+ this.shadowLevels = shadowPyramid.levels;
802
+ this.shadowHzb = this.empty(device, 'shadow pyramid', shadowPyramid.floats * 4, 0, 'map');
803
+ this.shadowLevelStart = this.storage(device, 'shadow level starts', shadowPyramid.starts);
804
+ this.shadowSeedSize = this.storage(device, 'shadow seed size', Uint32Array.from([this.shadowSize, this.shadowSize]));
805
+ this.shadowCullMatrix = this.empty(device, 'shadow cull matrix', 16 * 4);
806
+ /* The pyramid's shape, the count and the cones now; the eye and the switch each frame. */
807
+ this.shadowCullValues[4] = shadowBase.width;
808
+ this.shadowCullValues[5] = shadowBase.height;
809
+ this.shadowCullValues[6] = shadowPyramid.levels.length;
810
+ this.shadowCullValues[7] = count;
811
+ this.shadowCullValues[CULL_CONES] = 1;
812
+ this.shadowCullSettings = this.storage(device, 'cull settings, shadow', this.shadowCullValues);
813
+ this.shadowCullSettingsTwo = this.storage(device, 'cull settings, shadow two', this.shadowCullValues);
814
+ this.shadowCompactSettings = this.storage(device, 'phase shadow', Uint32Array.from([PHASE_ONE, count]));
815
+ this.shadowCompactSettingsTwo = this.storage(device, 'phase shadow two', Uint32Array.from([PHASE_TWO, count]));
816
+ /* Nonzero is hidden, and the light hides nothing: see `CULL_INSTANCES_WGSL`. */
817
+ this.instanceNone = this.empty(device, 'instance flags, none', this.meshCount * 4);
818
+ this.shadowMatrix = this.empty(device, 'shadow matrix', 16 * 4);
819
+ /*
820
+ * **Its own target, because a contributed pass is handed none.** `PassDevice` carries a device,
821
+ * the frame's formats and the sample count, and nothing else — there is no attachment of the
822
+ * forward path's to sample even where one exists. Depth only: what a shadow map holds is how
823
+ * far the light reached, and a colour target here would be written and never read.
824
+ */
825
+ this.shadowTexture = device.createTexture({
826
+ label: 'gpu-driven shadow',
827
+ size: { width: this.shadowSize, height: this.shadowSize },
828
+ format: SHADOW_FORMAT,
829
+ usage: RENDER_ATTACHMENT | TEXTURE_BINDING,
830
+ });
831
+ this.shadowView = this.shadowTexture.createView();
832
+ this.emptyEnvironment = device.createTexture({
833
+ label: 'gpu-driven no environment',
834
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
835
+ format: 'rgba16float',
836
+ usage: TEXTURE_BINDING | TEXTURE_COPY_DST,
837
+ });
838
+ this.emptyEnvironmentView = this.emptyEnvironment.createView({ dimension: '2d-array' });
839
+ this.emptySampler = device.createSampler({
840
+ label: 'gpu-driven no environment',
841
+ magFilter: 'linear',
842
+ minFilter: 'linear',
843
+ });
844
+ /*
845
+ * **The decode resources, created whether or not a material is textured.** The shading
846
+ * pipeline's layout is inferred from the module, and the module reads them — so a pass with no
847
+ * textures binds one layer one texel across and two tables of zeros, which no program names.
848
+ */
849
+ this.decodeNodes = device.createBuffer({
850
+ label: 'gpu-driven decode nodes',
851
+ size: DECODE_NODES_BYTES,
852
+ usage: UNIFORM | COPY_DST,
853
+ });
854
+ device.queue.writeBuffer(this.decodeNodes, 0, this.decode.nodes);
855
+ this.decodeWeights = device.createBuffer({
856
+ label: 'gpu-driven decode weights',
857
+ size: DECODE_WEIGHTS_BYTES,
858
+ usage: UNIFORM | COPY_DST,
859
+ });
860
+ device.queue.writeBuffer(this.decodeWeights, 0, this.decode.weights);
861
+ const edge0 = this.decode.layerSize;
862
+ this.latents = device.createTexture({
863
+ label: 'gpu-driven latents',
864
+ size: { width: edge0, height: edge0, depthOrArrayLayers: this.decode.layerCount },
865
+ format: 'rgba8unorm',
866
+ mipLevelCount: this.decode.levels.length,
867
+ usage: TEXTURE_BINDING | TEXTURE_COPY_DST,
868
+ });
869
+ this.decode.levels.forEach((bytes, level) => {
870
+ const edge = Math.max(1, edge0 >> level);
871
+ device.queue.writeTexture({ texture: this.latents, mipLevel: level }, bytes, { bytesPerRow: edge * 4, rowsPerImage: edge }, { width: edge, height: edge, depthOrArrayLayers: this.decode.layerCount });
872
+ });
873
+ this.latentsView = this.latents.createView({ dimension: '2d-array' });
874
+ const filtered = { magFilter: 'linear', minFilter: 'linear', mipmapFilter: 'linear' };
875
+ this.latentClamp = device.createSampler({
876
+ label: 'gpu-driven latents, clamped',
877
+ ...filtered,
878
+ addressModeU: 'clamp-to-edge',
879
+ addressModeV: 'clamp-to-edge',
880
+ });
881
+ this.latentRepeat = device.createSampler({
882
+ label: 'gpu-driven latents, repeated',
883
+ ...filtered,
884
+ addressModeU: 'repeat',
885
+ addressModeV: 'repeat',
886
+ });
887
+ this.planesBuffer = this.empty(device, 'planes', FRUSTUM_FLOATS * 4);
888
+ this.viewProjBuffer = this.empty(device, 'view projection', 16 * 4);
889
+ this.rasterParams = device.createBuffer({
890
+ label: 'gpu-driven raster params',
891
+ size: 4 * 4,
892
+ usage: UNIFORM | COPY_DST,
893
+ });
894
+ this.cullBuffers = [
895
+ this.empty(device, 'cull settings, phase one', CULL_SETTINGS_FLOATS * 4),
896
+ this.empty(device, 'cull settings, phase two', CULL_SETTINGS_FLOATS * 4),
897
+ this.empty(device, 'cull settings, blend', CULL_SETTINGS_FLOATS * 4),
898
+ ];
899
+ this.lodBuffer = this.empty(device, 'lod params', 8 * 4);
900
+ this.blendLodBuffer = this.empty(device, 'lod params blend', 8 * 4);
901
+ this.instanceFlags = this.empty(device, 'instance flags', this.meshCount * 4);
902
+ this.frameBuffer = device.createBuffer({
903
+ label: 'gpu-driven frame',
904
+ size: FRAME_FLOATS * 4,
905
+ usage: UNIFORM | COPY_DST,
906
+ });
907
+ this.selected = this.empty(device, 'selected', count * 4);
908
+ this.keep = this.empty(device, 'keep', count * 4);
909
+ this.history = this.empty(device, 'history a', count * 4);
910
+ this.drawn = this.empty(device, 'history b', count * 4);
911
+ this.lists = [
912
+ this.empty(device, 'list one', count * 4),
913
+ this.empty(device, 'list two', count * 4),
914
+ ];
915
+ const args = new Uint32Array(DRAW_INDIRECT_WORDS);
916
+ args[0] = CLUSTER_INDEX_CAP;
917
+ this.args = [
918
+ this.storage(device, 'draw one', args, INDIRECT | COPY_SRC),
919
+ this.storage(device, 'draw two', args, INDIRECT | COPY_SRC),
920
+ ];
921
+ this.phaseSettings = [
922
+ this.storage(device, 'phase one', Uint32Array.from([PHASE_ONE, count])),
923
+ this.storage(device, 'phase two', Uint32Array.from([PHASE_TWO, count])),
924
+ ];
925
+ /*
926
+ * **The transparent half's list, and the trick that makes one compaction serve both.**
927
+ *
928
+ * `COMPACT_CLUSTERS_WGSL` keeps a cluster only where `history[i] != 0` disagrees with the
929
+ * phase, which is how the two occlusion phases divide the opaque scene between them. The
930
+ * blended set is not divided — every blended cluster in the cut is drawn, once — so it is
931
+ * compacted as **phase two against a history of zeros**: `seen` is false for every cluster, the
932
+ * phase says false too, and none of them is skipped. `historySink` catches the history that run
933
+ * writes and nothing reads.
934
+ *
935
+ * The alternative was a third phase in the shader, which would be a branch every opaque frame
936
+ * pays for and a case `scripts/gpu-parity.mjs`'s `compactPhase` check would have to grow.
937
+ */
938
+ this.blendList = this.empty(device, 'list blend', count * 4);
939
+ this.blendArgs = this.storage(device, 'draw blend', args, INDIRECT | COPY_SRC);
940
+ this.blendCompactSettings = this.storage(device, 'phase blend', Uint32Array.from([PHASE_TWO, count]));
941
+ this.zeroHistory = this.empty(device, 'history zero', count * 4);
942
+ this.historySink = this.empty(device, 'history sink', count * 4);
943
+ }
944
+ buildPipelines(device, format, samples) {
945
+ const compute = (label, code) => device.createComputePipeline({
946
+ label: `gpu-driven ${label}`,
947
+ layout: 'auto',
948
+ compute: { module: device.createShaderModule({ label, code }), entryPoint: 'main' },
949
+ });
950
+ this.instancePipeline = compute('cull instances', CULL_INSTANCES_WGSL);
951
+ this.lodPipeline = compute('lod cut', LOD_CUT_WGSL);
952
+ this.cullPipeline = compute('cull clusters', CULL_CLUSTERS_WGSL);
953
+ this.compactPipeline = compute('compact', COMPACT_CLUSTERS_WGSL);
954
+ this.seedPipeline = compute('hzb seed', HZB_SEED_WGSL);
955
+ this.shadowSeedPipeline = compute('shadow hzb seed', SHADOW_HZB_SEED_WGSL);
956
+ this.reducePipeline = compute('hzb reduce', HZB_REDUCE_WGSL);
957
+ this.countPipeline = compute('bin count', MATERIAL_COUNT_WGSL);
958
+ this.offsetsPipeline = compute('bin offsets', MATERIAL_OFFSETS_WGSL);
959
+ this.scatterPipeline = compute('bin scatter', MATERIAL_SCATTER_WGSL);
960
+ this.shadePipeline = compute('shade', SHADE_MATERIAL_WGSL);
961
+ const raster = device.createShaderModule({ label: 'visbuffer', code: VISBUFFER_RASTER_WGSL });
962
+ this.rasterPipeline = device.createRenderPipeline({
963
+ label: 'gpu-driven visbuffer',
964
+ layout: 'auto',
965
+ vertex: { module: raster, entryPoint: 'vertexMain' },
966
+ fragment: { module: raster, entryPoint: 'fragmentMain', targets: [{ format: 'r32uint' }] },
967
+ /*
968
+ * **The default winding, which is the forward path's, once the projection is the right
969
+ * one.** This read `frontFace: 'cw'` for an afternoon and the reason was a mirror: taking
970
+ * `clipCorrection` in a hand-written shader negates Y once too often, and a Y flip reverses
971
+ * screen winding — so a single-sided surface vanished under `ccw` and `cw` drew the correct
972
+ * faces of a vertically mirrored world. Two captures of one scene settled it, the same
973
+ * geometry through `drawMesh` and through here, mirrored about the canvas centre to the
974
+ * pixel. See `depthCorrection` in `PassDevice`.
975
+ */
976
+ primitive: { topology: 'triangle-list', cullMode: 'back' },
977
+ depthStencil: { format: DEPTH_FORMAT, depthWriteEnabled: true, depthCompare: DEPTH_COMPARE },
978
+ });
979
+ /*
980
+ * **The same vertex stage as the frame's, with no fragment stage at all.** What a depth-only
981
+ * pass needs is exactly the transform the visibility raster already does, so sharing the module
982
+ * is what keeps the map and the frame agreeing about where a triangle is — a second copy of
983
+ * that transform would be a shadow standing slightly beside its caster with nothing to compare.
984
+ *
985
+ * **`frontFace: 'cw'` travels with the light matrix's Y negation and neither is inert.** The
986
+ * flip reverses screen winding, so the default would cull precisely the faces this pass exists
987
+ * to record — and what that draws is not a missing shadow, it is a scene uniformly in shadow,
988
+ * because every fragment compares itself against a depth recorded behind it. `depthPass.ts`
989
+ * paid for that lesson on the other pipeline and its comment is the long version.
990
+ *
991
+ * **Not reversed**: `depthConvention.ts` leaves shadow maps conventional, so this clears to
992
+ * `SHADOW_DEPTH_CLEAR` and compares with `less` while the frame beside it clears to 0 and
993
+ * compares with `greater`. A scatter batch already cast no shadow at all on this backend for
994
+ * taking the frame's pair here.
995
+ *
996
+ * The bias is `depthPass.ts`'s, which is `shadowMap.ts`'s on the other backend: a map with no
997
+ * separation between a caster and its receiver is acne everywhere the two are the same surface.
998
+ */
999
+ this.shadowPipeline = device.createRenderPipeline({
1000
+ label: 'gpu-driven shadow',
1001
+ layout: 'auto',
1002
+ vertex: { module: raster, entryPoint: 'vertexMain' },
1003
+ primitive: { topology: 'triangle-list', cullMode: 'back', frontFace: 'cw' },
1004
+ depthStencil: {
1005
+ format: SHADOW_FORMAT,
1006
+ depthWriteEnabled: true,
1007
+ depthCompare: 'less',
1008
+ depthBias: 4,
1009
+ depthBiasSlopeScale: 1.1,
1010
+ },
1011
+ });
1012
+ /*
1013
+ * **Six entries until the raster gained an alpha test, and seven after it.** This pass shares
1014
+ * that module's vertex stage, and the stage now reads `materialOf` to carry a material into the
1015
+ * fragment stage this pipeline does not have — so `layout: 'auto'` derives a seventh binding
1016
+ * here too, and a bind group of six is refused outright. It failed exactly that way, and what
1017
+ * it drew was nothing: the shadow encoder's whole command buffer was invalid.
1018
+ *
1019
+ * The buffer is bound rather than the read moved, because the alternative is reading the
1020
+ * material per fragment instead of per vertex and an opaque scene would pay for it.
1021
+ */
1022
+ const shadowRasterFor = (list) => device.createBindGroup({
1023
+ layout: this.shadowPipeline.getBindGroupLayout(0),
1024
+ entries: [
1025
+ { binding: 0, resource: { buffer: this.shadowMatrix } },
1026
+ { binding: 1, resource: { buffer: list } },
1027
+ { binding: 2, resource: { buffer: this.clusterMeta } },
1028
+ { binding: 3, resource: { buffer: this.indices } },
1029
+ { binding: 4, resource: { buffer: this.vertices } },
1030
+ { binding: 5, resource: { buffer: this.transformBuffer } },
1031
+ { binding: 6, resource: { buffer: this.materialOf } },
1032
+ ],
1033
+ });
1034
+ this.shadowGroup = shadowRasterFor(this.shadowList);
1035
+ this.shadowGroupTwo = shadowRasterFor(this.shadowListTwo);
1036
+ /*
1037
+ * **The transparent half's raster: one indirect draw into two targets at once.**
1038
+ *
1039
+ * The accumulation sums and the revealage multiplies, which is the whole mechanism — both
1040
+ * commute, so the frame does not depend on the order the panes arrived in. `flatPass.ts`'s
1041
+ * `oitTarget` sets the same pair on the forward path; it needs two passes because it replays a
1042
+ * draw queue through a shader that writes one target, and this writes both at once.
1043
+ *
1044
+ * **No culling**, because a pane is a surface seen from both sides and there is no interior to
1045
+ * hide. The opaque raster culls back faces, where a closed hull's far side is a wasted draw.
1046
+ */
1047
+ const blend = device.createShaderModule({ label: 'blend raster', code: BLEND_RASTER_WGSL });
1048
+ this.blendPipeline = device.createRenderPipeline({
1049
+ label: 'gpu-driven blend',
1050
+ layout: 'auto',
1051
+ vertex: { module: blend, entryPoint: 'blendVert' },
1052
+ fragment: {
1053
+ module: blend,
1054
+ entryPoint: 'blendFrag',
1055
+ targets: [
1056
+ {
1057
+ format: OIT_ACCUM_FORMAT,
1058
+ blend: {
1059
+ color: { srcFactor: 'one', dstFactor: 'one', operation: 'add' },
1060
+ alpha: { srcFactor: 'one', dstFactor: 'one', operation: 'add' },
1061
+ },
1062
+ },
1063
+ {
1064
+ format: OIT_REVEAL_FORMAT,
1065
+ blend: {
1066
+ color: { srcFactor: 'zero', dstFactor: 'one-minus-src-alpha', operation: 'add' },
1067
+ alpha: { srcFactor: 'zero', dstFactor: 'one-minus-src-alpha', operation: 'add' },
1068
+ },
1069
+ },
1070
+ ],
1071
+ },
1072
+ primitive: { topology: 'triangle-list', cullMode: 'none' },
1073
+ /* Read-only: a pane behind a wall is rejected, and two panes do not hide each other. */
1074
+ depthStencil: {
1075
+ format: DEPTH_FORMAT,
1076
+ depthWriteEnabled: false,
1077
+ depthCompare: DEPTH_COMPARE,
1078
+ },
1079
+ });
1080
+ /*
1081
+ * **Premultiplied over, where `renderer.ts`'s composite is a lerp**, and `blendRaster.wgsl.ts`
1082
+ * carries why: this pipeline's colour target has a coverage mask in its alpha, which the blit
1083
+ * reads to decide whether the pass drew a pixel at all, and a lerp would leave a pane over
1084
+ * empty sky marked as undrawn.
1085
+ */
1086
+ const resolve = device.createShaderModule({ label: 'blend resolve', code: BLEND_RESOLVE_WGSL });
1087
+ this.blendResolvePipeline = device.createRenderPipeline({
1088
+ label: 'gpu-driven blend resolve',
1089
+ layout: 'auto',
1090
+ vertex: { module: resolve, entryPoint: 'resolveVert' },
1091
+ fragment: {
1092
+ module: resolve,
1093
+ entryPoint: 'resolveFrag',
1094
+ targets: [
1095
+ {
1096
+ format: 'rgba16float',
1097
+ blend: {
1098
+ color: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
1099
+ alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
1100
+ },
1101
+ },
1102
+ ],
1103
+ },
1104
+ primitive: { topology: 'triangle-list' },
1105
+ });
1106
+ /*
1107
+ * **The frame's depth format has to be this pass's, and it is asserted rather than assumed.**
1108
+ * Both take `DEPTH_FORMAT` from `depthConvention.ts` today, so there is no conversion here at
1109
+ * all — and a backend that ever differed would not fail, it would produce a depth test that is
1110
+ * silently inverted or quantised, which is the shape of defect this repository writes refusals
1111
+ * for. Only checked where the depth is actually handed over.
1112
+ */
1113
+ if (this.presentDepth && this.frameDepthFormat !== DEPTH_FORMAT) {
1114
+ throw new Error(`[driftengine] the gpu-driven pass was asked to present its depth into a frame whose ` +
1115
+ `depth format is ${String(this.frameDepthFormat)}, and its own is ${DEPTH_FORMAT}. Both ` +
1116
+ `read DEPTH_FORMAT in depthConvention.ts, so this is a backend that has diverged from it.`);
1117
+ }
1118
+ const blitLabel = this.presentDepth ? 'gpu-driven blit depth' : 'gpu-driven blit';
1119
+ const blit = device.createShaderModule({
1120
+ label: blitLabel,
1121
+ code: this.presentDepth ? GPU_DRIVEN_BLIT_DEPTH_WGSL : GPU_DRIVEN_BLIT_WGSL,
1122
+ });
1123
+ this.blitPipeline = device.createRenderPipeline({
1124
+ label: blitLabel,
1125
+ layout: 'auto',
1126
+ vertex: { module: blit, entryPoint: 'vertexMain' },
1127
+ fragment: {
1128
+ module: blit,
1129
+ entryPoint: 'fragmentMain',
1130
+ targets: [
1131
+ {
1132
+ format,
1133
+ /*
1134
+ * **Premultiplied over, because that is what the target holds.** Colour with a
1135
+ * coverage in its alpha: one wherever the visibility buffer covered a pixel, so over
1136
+ * is a plain write there, and a blended surface's own coverage where nothing else of
1137
+ * this pipeline was — a pane against the sky. Written as opaque, that pane was painted
1138
+ * over black and the sky behind it thrown away; `depth-share-check.mjs` measured a
1139
+ * pane over a dark backdrop and over a bright one come back as the same pixel.
1140
+ */
1141
+ blend: {
1142
+ color: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
1143
+ alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
1144
+ },
1145
+ },
1146
+ ],
1147
+ },
1148
+ primitive: { topology: 'triangle-list' },
1149
+ multisample: { count: samples },
1150
+ /*
1151
+ * **Over whatever is there unless the caller asked to share a depth buffer.** Without the
1152
+ * flag this pass *is* the scene and the sky behind it is a backdrop; with it, a forward-path
1153
+ * mesh can stand behind this pipeline's geometry, which needs the depth written and needs the
1154
+ * comparison to be the frame's own rather than `always`.
1155
+ *
1156
+ * **The frame's own comparison or equal**, and the equal is for a pane with nothing of this
1157
+ * pipeline behind it. Its pixel carries the depth the pass cleared to, which is the frame's
1158
+ * own clear, so a strict test refuses it over the sky every time and the pane is gone; with
1159
+ * equal it passes there and writes back the value that was already there. Over geometry the
1160
+ * frame drew *before* the blit such a pixel still fails, which is why a consumer draws its
1161
+ * own meshes after this pass rather than before it.
1162
+ */
1163
+ depthStencil: {
1164
+ format: DEPTH_FORMAT,
1165
+ depthWriteEnabled: this.presentDepth,
1166
+ depthCompare: this.presentDepth ? DEPTH_COMPARE_EQUAL : 'always',
1167
+ },
1168
+ });
1169
+ const lodFor = (params) => device.createBindGroup({
1170
+ layout: this.lodPipeline.getBindGroupLayout(0),
1171
+ entries: [
1172
+ { binding: 0, resource: { buffer: params } },
1173
+ { binding: 1, resource: { buffer: this.clusterLod } },
1174
+ { binding: 2, resource: { buffer: this.selected } },
1175
+ { binding: 3, resource: { buffer: this.clusterMeta } },
1176
+ { binding: 4, resource: { buffer: this.instanceFlags } },
1177
+ { binding: 5, resource: { buffer: this.materialOf } },
1178
+ { binding: 6, resource: { buffer: this.materialTable } },
1179
+ ],
1180
+ });
1181
+ /* The same cut with one number changed: which half of the frame it is being asked for. */
1182
+ this.lodGroup = lodFor(this.lodBuffer);
1183
+ this.blendLodGroup = lodFor(this.blendLodBuffer);
1184
+ /*
1185
+ * **The camera's cut, with no mesh hidden and a selection of its own.** The camera's parameters,
1186
+ * so a surface is shadowed at the level it is drawn at; not the camera's instance flags, which
1187
+ * drop a tree behind the viewer that is still shadowing the path in front of them.
1188
+ */
1189
+ this.shadowLodGroup = device.createBindGroup({
1190
+ layout: this.lodPipeline.getBindGroupLayout(0),
1191
+ entries: [
1192
+ { binding: 0, resource: { buffer: this.lodBuffer } },
1193
+ { binding: 1, resource: { buffer: this.clusterLod } },
1194
+ { binding: 2, resource: { buffer: this.shadowSelected } },
1195
+ { binding: 3, resource: { buffer: this.clusterMeta } },
1196
+ { binding: 4, resource: { buffer: this.instanceNone } },
1197
+ { binding: 5, resource: { buffer: this.materialOf } },
1198
+ { binding: 6, resource: { buffer: this.materialTable } },
1199
+ ],
1200
+ });
1201
+ /*
1202
+ * **Both halves of the light's compaction, both ways round the history**, made once: the light's
1203
+ * history swaps only on a frame that draws the map, so which way round is the light's own flag
1204
+ * and not the camera's.
1205
+ */
1206
+ const shadowCompactFor = (settings, list, args, read, write) => device.createBindGroup({
1207
+ layout: this.compactPipeline.getBindGroupLayout(0),
1208
+ entries: [
1209
+ { binding: 0, resource: { buffer: settings } },
1210
+ { binding: 1, resource: { buffer: this.shadowSelected } },
1211
+ { binding: 2, resource: { buffer: this.shadowKeep } },
1212
+ { binding: 3, resource: { buffer: read } },
1213
+ { binding: 4, resource: { buffer: list } },
1214
+ { binding: 5, resource: { buffer: write } },
1215
+ { binding: 6, resource: { buffer: args } },
1216
+ ],
1217
+ });
1218
+ const [historyA, historyB] = this.shadowHistories;
1219
+ const halves = (read, write) => [
1220
+ shadowCompactFor(this.shadowCompactSettings, this.shadowList, this.shadowArgs, read, write),
1221
+ shadowCompactFor(this.shadowCompactSettingsTwo, this.shadowListTwo, this.shadowArgsTwo, read, write),
1222
+ ];
1223
+ this.shadowCompactGroups = [halves(historyA, historyB), halves(historyB, historyA)];
1224
+ /*
1225
+ * **The light's cull, each half, against the light's own pyramid** through the raster's matrix
1226
+ * turned over; phase one's settings switch the pyramid off. Made once, because nothing it
1227
+ * binds is sized by the frame.
1228
+ */
1229
+ const shadowCullFor = (settings) => device.createBindGroup({
1230
+ layout: this.cullPipeline.getBindGroupLayout(0),
1231
+ entries: [
1232
+ { binding: 0, resource: { buffer: this.shadowPlanes } },
1233
+ { binding: 1, resource: { buffer: this.shadowCullMatrix } },
1234
+ { binding: 2, resource: { buffer: settings } },
1235
+ { binding: 3, resource: { buffer: this.clusterCull } },
1236
+ { binding: 4, resource: { buffer: this.shadowHzb } },
1237
+ { binding: 5, resource: { buffer: this.shadowLevelStart } },
1238
+ { binding: 6, resource: { buffer: this.shadowKeep } },
1239
+ { binding: 7, resource: { buffer: this.shadowSelected } },
1240
+ ],
1241
+ });
1242
+ this.shadowCullGroups = [
1243
+ shadowCullFor(this.shadowCullSettings),
1244
+ shadowCullFor(this.shadowCullSettingsTwo),
1245
+ ];
1246
+ this.shadowSeedGroup = device.createBindGroup({
1247
+ layout: this.shadowSeedPipeline.getBindGroupLayout(0),
1248
+ entries: [
1249
+ { binding: 0, resource: { buffer: this.shadowSeedSize } },
1250
+ { binding: 1, resource: this.shadowView },
1251
+ { binding: 2, resource: { buffer: this.shadowHzb } },
1252
+ ],
1253
+ });
1254
+ this.shadowReduceGroups = [];
1255
+ for (let level = 1; level < this.shadowLevels.length; level += 1) {
1256
+ const src = this.shadowLevels[level - 1];
1257
+ this.shadowReduceGroups.push(device.createBindGroup({
1258
+ layout: this.reducePipeline.getBindGroupLayout(0),
1259
+ entries: [
1260
+ { binding: 0, resource: { buffer: src.size } },
1261
+ { binding: 1, resource: { buffer: this.shadowHzb } },
1262
+ ],
1263
+ }));
1264
+ }
1265
+ this.blendCompactGroup = device.createBindGroup({
1266
+ layout: this.compactPipeline.getBindGroupLayout(0),
1267
+ entries: [
1268
+ { binding: 0, resource: { buffer: this.blendCompactSettings } },
1269
+ { binding: 1, resource: { buffer: this.selected } },
1270
+ { binding: 2, resource: { buffer: this.keep } },
1271
+ { binding: 3, resource: { buffer: this.zeroHistory } },
1272
+ { binding: 4, resource: { buffer: this.blendList } },
1273
+ { binding: 5, resource: { buffer: this.historySink } },
1274
+ { binding: 6, resource: { buffer: this.blendArgs } },
1275
+ ],
1276
+ });
1277
+ /*
1278
+ * **The corrected matrix, which is the one the cluster planes come from**, so a mesh and its
1279
+ * clusters are judged against one frustum. The shader extracts its planes per invocation, which
1280
+ * is six rows' arithmetic a mesh; a scene has meshes by the handful.
1281
+ */
1282
+ this.instanceGroup = device.createBindGroup({
1283
+ layout: this.instancePipeline.getBindGroupLayout(0),
1284
+ entries: [
1285
+ { binding: 0, resource: { buffer: this.viewProjBuffer } },
1286
+ { binding: 1, resource: { buffer: this.instanceSpheres } },
1287
+ { binding: 2, resource: { buffer: this.instanceFlags } },
1288
+ ],
1289
+ });
1290
+ this.jobGroups = [];
1291
+ for (let m = 0; m < this.materials.length; m += 1) {
1292
+ this.jobGroups.push(device.createBindGroup({
1293
+ layout: this.shadePipeline.getBindGroupLayout(1),
1294
+ entries: [
1295
+ { binding: 0, resource: { buffer: this.jobs, offset: m * 256, size: 16 } },
1296
+ ],
1297
+ }));
1298
+ }
1299
+ this.buildSwappedGroups(device);
1300
+ }
1301
+ /**
1302
+ * The groups that name `history` and `drawn`, rebuilt when the two swap.
1303
+ *
1304
+ * **Two sets rather than a copy**, because the alternative is copying a buffer the size of the
1305
+ * cluster count every frame to keep one binding pointing at the right half. A bind group is
1306
+ * cheap to build and this happens once a frame; making it twice and alternating would be
1307
+ * cheaper still, and is what this does — `flipped` picks the pair.
1308
+ */
1309
+ buildSwappedGroups(device) {
1310
+ const compact = this.compactPipeline;
1311
+ const raster = this.rasterPipeline;
1312
+ const [listOne, listTwo] = this.lists;
1313
+ const [argsOne, argsTwo] = this.args;
1314
+ const [phaseOne, phaseTwo] = this.phaseSettings;
1315
+ const read = this.flipped ? this.drawn : this.history;
1316
+ const write = this.flipped ? this.history : this.drawn;
1317
+ const compactFor = (settings, list, args) => device.createBindGroup({
1318
+ layout: compact.getBindGroupLayout(0),
1319
+ entries: [
1320
+ { binding: 0, resource: { buffer: settings } },
1321
+ { binding: 1, resource: { buffer: this.selected } },
1322
+ { binding: 2, resource: { buffer: this.keep } },
1323
+ { binding: 3, resource: { buffer: read } },
1324
+ { binding: 4, resource: { buffer: list } },
1325
+ { binding: 5, resource: { buffer: write } },
1326
+ { binding: 6, resource: { buffer: args } },
1327
+ ],
1328
+ });
1329
+ this.compactGroups = [
1330
+ compactFor(phaseOne, listOne, argsOne),
1331
+ compactFor(phaseTwo, listTwo, argsTwo),
1332
+ ];
1333
+ const rasterFor = (list) => device.createBindGroup({
1334
+ layout: raster.getBindGroupLayout(0),
1335
+ entries: [
1336
+ { binding: 0, resource: { buffer: this.viewProjBuffer } },
1337
+ { binding: 1, resource: { buffer: list } },
1338
+ { binding: 2, resource: { buffer: this.clusterMeta } },
1339
+ { binding: 3, resource: { buffer: this.indices } },
1340
+ { binding: 4, resource: { buffer: this.vertices } },
1341
+ { binding: 5, resource: { buffer: this.transformBuffer } },
1342
+ { binding: 6, resource: { buffer: this.materialOf } },
1343
+ { binding: 7, resource: { buffer: this.materialTable } },
1344
+ { binding: 8, resource: { buffer: this.rasterParams } },
1345
+ { binding: 9, resource: this.latentsView },
1346
+ { binding: 10, resource: this.latentClamp },
1347
+ { binding: 11, resource: this.latentRepeat },
1348
+ { binding: 12, resource: { buffer: this.decodeNodes } },
1349
+ { binding: 13, resource: { buffer: this.decodeWeights } },
1350
+ ],
1351
+ });
1352
+ this.rasterGroups = [rasterFor(listOne), rasterFor(listTwo)];
1353
+ }
1354
+ disposeTargets() {
1355
+ this.oitAccum?.destroy();
1356
+ this.oitReveal?.destroy();
1357
+ this.oitAccum = null;
1358
+ this.oitReveal = null;
1359
+ this.oitAccumView = null;
1360
+ this.oitRevealView = null;
1361
+ this.visibilityTexture?.destroy();
1362
+ this.depthTexture?.destroy();
1363
+ this.colourTexture?.destroy();
1364
+ this.visibility?.destroy();
1365
+ this.hzb?.destroy();
1366
+ this.levelStart?.destroy();
1367
+ for (const level of this.levels)
1368
+ level.size.destroy();
1369
+ this.binCounts?.destroy();
1370
+ this.binOffsets?.destroy();
1371
+ this.binCursors?.destroy();
1372
+ this.binDispatch?.destroy();
1373
+ this.binPixels?.destroy();
1374
+ this.binSettingsBuffer?.destroy();
1375
+ this.levels = [];
1376
+ }
1377
+ /**
1378
+ * A pyramid's levels over a base this size, down to one texel: each level padded so its byte
1379
+ * offset is one a bind group may name, with its own size block — the source's shape, where it
1380
+ * starts, and where its output goes, because the reduction addresses both ends of one buffer
1381
+ * itself (see `HZB_REDUCE_WGSL`). The camera's is over the frame; the light's over half its map.
1382
+ */
1383
+ pyramidLevels(device, label, width, height) {
1384
+ const count = hzbMipCount(width, height);
1385
+ const starts = new Uint32Array(count);
1386
+ let floats = 0;
1387
+ let levelWidth = width;
1388
+ let levelHeight = height;
1389
+ const levels = [];
1390
+ for (let level = 0; level < count; level += 1) {
1391
+ starts[level] = floats;
1392
+ const texels = Math.ceil((levelWidth * levelHeight) / LEVEL_ALIGN) * LEVEL_ALIGN;
1393
+ levels.push({
1394
+ width: levelWidth,
1395
+ height: levelHeight,
1396
+ start: floats,
1397
+ size: this.storage(device, `${label} ${level}`, Uint32Array.from([levelWidth, levelHeight, floats, floats + texels])),
1398
+ });
1399
+ floats += texels;
1400
+ const next = hzbMipSize(levelWidth, levelHeight);
1401
+ levelWidth = next.width;
1402
+ levelHeight = next.height;
1403
+ }
1404
+ return { levels, floats, starts };
1405
+ }
1406
+ buildTargets(device) {
1407
+ this.disposeTargets();
1408
+ const { stride, height } = this;
1409
+ const pixels = stride * height;
1410
+ this.visibilityTexture = device.createTexture({
1411
+ label: 'gpu-driven visibility',
1412
+ size: { width: stride, height },
1413
+ format: 'r32uint',
1414
+ usage: RENDER_ATTACHMENT | TEXTURE_COPY_SRC,
1415
+ });
1416
+ this.depthTexture = device.createTexture({
1417
+ label: 'gpu-driven depth',
1418
+ size: { width: stride, height },
1419
+ format: DEPTH_FORMAT,
1420
+ usage: RENDER_ATTACHMENT | TEXTURE_BINDING,
1421
+ });
1422
+ this.colourTexture = device.createTexture({
1423
+ label: 'gpu-driven colour',
1424
+ size: { width: stride, height },
1425
+ format: 'rgba16float',
1426
+ /* `RENDER_ATTACHMENT` only so a clear-only render pass can empty it each frame: a storage
1427
+ texture has no clear, and the alternative is a dispatch over every pixel. */
1428
+ usage: STORAGE_BINDING | TEXTURE_BINDING | RENDER_ATTACHMENT,
1429
+ });
1430
+ this.colourView = this.colourTexture.createView();
1431
+ this.oitAccum = device.createTexture({
1432
+ label: 'gpu-driven oit accum',
1433
+ size: { width: stride, height },
1434
+ format: OIT_ACCUM_FORMAT,
1435
+ usage: RENDER_ATTACHMENT | TEXTURE_BINDING,
1436
+ });
1437
+ this.oitReveal = device.createTexture({
1438
+ label: 'gpu-driven oit reveal',
1439
+ size: { width: stride, height },
1440
+ format: OIT_REVEAL_FORMAT,
1441
+ usage: RENDER_ATTACHMENT | TEXTURE_BINDING,
1442
+ });
1443
+ this.oitAccumView = this.oitAccum.createView();
1444
+ this.oitRevealView = this.oitReveal.createView();
1445
+ this.visibility = this.empty(device, 'visibility', pixels * 4, COPY_DST, 'frame');
1446
+ /*
1447
+ * **Over the width the raster drew, not the padded row.** The cull maps a cluster's rectangle
1448
+ * across the base's width; over the row, a cluster was tested against the depth of a point
1449
+ * further right than it stood, and at the right edge against the padding, which hides nothing.
1450
+ * A storage buffer has no row alignment to keep, so the pyramid takes the picture's own size.
1451
+ */
1452
+ const pyramid = this.pyramidLevels(device, 'level', this.width, height);
1453
+ this.levels = pyramid.levels;
1454
+ this.hzb = this.empty(device, 'pyramid', pyramid.floats * 4, 0, 'frame');
1455
+ this.levelStart = this.storage(device, 'level starts', pyramid.starts);
1456
+ const materials = Math.max(1, this.materials.length);
1457
+ this.binCounts = this.empty(device, 'bin counts', materials * 4);
1458
+ /* Copied into the cursors each frame, so the scatter can consume a copy and leave these
1459
+ readable — which is what tells a shading group where its material's list begins. */
1460
+ this.binOffsets = this.empty(device, 'bin offsets', materials * 4, COPY_SRC);
1461
+ this.binCursors = this.empty(device, 'bin cursors', materials * 4, COPY_DST);
1462
+ this.binDispatch = this.empty(device, 'bin dispatch', materials * BIN_DISPATCH_WORDS * 4, INDIRECT);
1463
+ this.binPixels = this.empty(device, 'bin pixels', pixels * 4, 0, 'frame');
1464
+ this.binSettings[0] = pixels;
1465
+ this.binSettings[1] = materials;
1466
+ this.binSettings[2] = BIN_GROUP_SIZE;
1467
+ this.binSettingsBuffer = this.storage(device, 'bin settings', this.binSettings);
1468
+ this.buildSizedGroups(device);
1469
+ }
1470
+ buildSizedGroups(device) {
1471
+ const cullGroup = (settings) => device.createBindGroup({
1472
+ layout: this.cullPipeline.getBindGroupLayout(0),
1473
+ entries: [
1474
+ { binding: 0, resource: { buffer: this.planesBuffer } },
1475
+ { binding: 1, resource: { buffer: this.viewProjBuffer } },
1476
+ { binding: 2, resource: { buffer: settings } },
1477
+ { binding: 3, resource: { buffer: this.clusterCull } },
1478
+ { binding: 4, resource: { buffer: this.hzb } },
1479
+ { binding: 5, resource: { buffer: this.levelStart } },
1480
+ { binding: 6, resource: { buffer: this.keep } },
1481
+ { binding: 7, resource: { buffer: this.selected } },
1482
+ ],
1483
+ });
1484
+ const [phaseOne, phaseTwo, blend] = this.cullBuffers;
1485
+ this.cullGroups = [cullGroup(phaseOne), cullGroup(phaseTwo), cullGroup(blend)];
1486
+ const base = this.levels[0];
1487
+ this.seedGroup = device.createBindGroup({
1488
+ layout: this.seedPipeline.getBindGroupLayout(0),
1489
+ entries: [
1490
+ { binding: 0, resource: { buffer: base.size } },
1491
+ { binding: 1, resource: this.depthTexture.createView() },
1492
+ { binding: 2, resource: { buffer: this.hzb } },
1493
+ ],
1494
+ });
1495
+ /*
1496
+ * **The whole pyramid, once, as one writable binding per level.** Binding a sub-range read-only
1497
+ * beside a writable one is refused: a buffer used writably in a compute pass may be used no
1498
+ * other way in that scope, whatever the offsets. So the level's size block carries where its
1499
+ * source starts and where its output goes, and the shader indexes both.
1500
+ */
1501
+ this.reduceGroups = [];
1502
+ for (let level = 1; level < this.levels.length; level += 1) {
1503
+ const src = this.levels[level - 1];
1504
+ this.reduceGroups.push(device.createBindGroup({
1505
+ layout: this.reducePipeline.getBindGroupLayout(0),
1506
+ entries: [
1507
+ { binding: 0, resource: { buffer: src.size } },
1508
+ { binding: 1, resource: { buffer: this.hzb } },
1509
+ ],
1510
+ }));
1511
+ }
1512
+ const binEntries = [
1513
+ { binding: 0, resource: { buffer: this.binSettingsBuffer } },
1514
+ { binding: 1, resource: { buffer: this.visibility } },
1515
+ { binding: 2, resource: { buffer: this.materialOf } },
1516
+ ];
1517
+ this.countGroup = device.createBindGroup({
1518
+ layout: this.countPipeline.getBindGroupLayout(0),
1519
+ entries: [...binEntries, { binding: 3, resource: { buffer: this.binCounts } }],
1520
+ });
1521
+ /*
1522
+ * **The prefix sum binds three of the five, because `layout: 'auto'` infers a layout from what
1523
+ * the shader actually reads.** `MATERIAL_OFFSETS_WGSL` shares a preamble with the other two
1524
+ * and never touches the visibility buffer or the material table, so those bindings are not in
1525
+ * its layout and naming them is an error rather than a redundancy. The device says so by
1526
+ * index: "binding index 1 not present in the bind group layout".
1527
+ */
1528
+ this.offsetsGroup = device.createBindGroup({
1529
+ layout: this.offsetsPipeline.getBindGroupLayout(0),
1530
+ entries: [
1531
+ { binding: 0, resource: { buffer: this.binSettingsBuffer } },
1532
+ { binding: 3, resource: { buffer: this.binCounts } },
1533
+ { binding: 4, resource: { buffer: this.binOffsets } },
1534
+ { binding: 5, resource: { buffer: this.binDispatch } },
1535
+ ],
1536
+ });
1537
+ this.scatterGroup = device.createBindGroup({
1538
+ layout: this.scatterPipeline.getBindGroupLayout(0),
1539
+ entries: [
1540
+ ...binEntries,
1541
+ { binding: 3, resource: { buffer: this.binCursors } },
1542
+ { binding: 4, resource: { buffer: this.binPixels } },
1543
+ ],
1544
+ });
1545
+ this.shadeGroup = device.createBindGroup({
1546
+ layout: this.shadePipeline.getBindGroupLayout(0),
1547
+ entries: [
1548
+ { binding: 0, resource: { buffer: this.frameBuffer } },
1549
+ { binding: 1, resource: { buffer: this.visibility } },
1550
+ { binding: 2, resource: { buffer: this.binPixels } },
1551
+ { binding: 3, resource: { buffer: this.binOffsets } },
1552
+ { binding: 4, resource: { buffer: this.binCounts } },
1553
+ { binding: 5, resource: { buffer: this.clusterMeta } },
1554
+ { binding: 6, resource: { buffer: this.indices } },
1555
+ { binding: 7, resource: { buffer: this.vertices } },
1556
+ { binding: 8, resource: { buffer: this.transformBuffer } },
1557
+ { binding: 9, resource: { buffer: this.materialTable } },
1558
+ { binding: 10, resource: this.colourView },
1559
+ { binding: 11, resource: this.shadowView },
1560
+ {
1561
+ binding: 12,
1562
+ resource: this.environmentView ?? this.emptyEnvironmentView,
1563
+ },
1564
+ { binding: 13, resource: this.environmentSampler ?? this.emptySampler },
1565
+ { binding: 14, resource: this.latentsView },
1566
+ { binding: 15, resource: this.latentClamp },
1567
+ { binding: 16, resource: this.latentRepeat },
1568
+ { binding: 17, resource: { buffer: this.decodeNodes } },
1569
+ { binding: 18, resource: { buffer: this.decodeWeights } },
1570
+ ],
1571
+ });
1572
+ this.blitGroup = device.createBindGroup({
1573
+ layout: this.blitPipeline.getBindGroupLayout(0),
1574
+ entries: [
1575
+ { binding: 0, resource: this.colourTexture.createView() },
1576
+ /* The depth the raster wrote, sampled so the blit can hand it to the frame. */
1577
+ ...(this.presentDepth
1578
+ ? [
1579
+ {
1580
+ binding: 1,
1581
+ resource: this.depthTexture.createView(),
1582
+ },
1583
+ ]
1584
+ : []),
1585
+ ],
1586
+ });
1587
+ /* Everything the transparent raster reads: the same scene, the same frame, its own list. */
1588
+ this.blendRasterGroup = device.createBindGroup({
1589
+ layout: this.blendPipeline.getBindGroupLayout(0),
1590
+ entries: [
1591
+ { binding: 0, resource: { buffer: this.viewProjBuffer } },
1592
+ { binding: 1, resource: { buffer: this.blendList } },
1593
+ { binding: 2, resource: { buffer: this.clusterMeta } },
1594
+ { binding: 3, resource: { buffer: this.indices } },
1595
+ { binding: 4, resource: { buffer: this.vertices } },
1596
+ { binding: 5, resource: { buffer: this.transformBuffer } },
1597
+ { binding: 6, resource: { buffer: this.materialOf } },
1598
+ { binding: 7, resource: { buffer: this.materialTable } },
1599
+ { binding: 8, resource: { buffer: this.frameBuffer } },
1600
+ {
1601
+ binding: 9,
1602
+ resource: this.environmentView ?? this.emptyEnvironmentView,
1603
+ },
1604
+ { binding: 10, resource: this.environmentSampler ?? this.emptySampler },
1605
+ { binding: 11, resource: this.latentsView },
1606
+ { binding: 12, resource: this.latentClamp },
1607
+ { binding: 13, resource: this.latentRepeat },
1608
+ { binding: 14, resource: { buffer: this.decodeNodes } },
1609
+ { binding: 15, resource: { buffer: this.decodeWeights } },
1610
+ ],
1611
+ });
1612
+ this.blendResolveGroup = device.createBindGroup({
1613
+ layout: this.blendResolvePipeline.getBindGroupLayout(0),
1614
+ entries: [
1615
+ { binding: 0, resource: this.oitAccumView },
1616
+ { binding: 1, resource: this.oitRevealView },
1617
+ ],
1618
+ });
1619
+ }
1620
+ /**
1621
+ * Take the frame's probe, and say whether the shading pass's bind group has to be rebuilt.
1622
+ *
1623
+ * **Compared by identity rather than copied**, because a texture view is the thing that changes
1624
+ * when a grid is rebaked at a different size and nothing else in the block does. A frame that
1625
+ * offers the same view as the last one rebuilds nothing.
1626
+ */
1627
+ adoptEnvironment(environment) {
1628
+ const view = environment?.view ?? null;
1629
+ const sampler = environment?.sampler ?? null;
1630
+ const changed = view !== this.environmentView || sampler !== this.environmentSampler;
1631
+ this.environmentView = view;
1632
+ this.environmentSampler = sampler;
1633
+ this.environment = environment;
1634
+ return changed;
1635
+ }
1636
+ /** Fill the per-frame blocks from the view the caller set. */
1637
+ /**
1638
+ * Upload the records the scene has changed since the last frame (its geometry went up when it
1639
+ * was placed), and nothing else.
1640
+ *
1641
+ * **Before anything is recorded into the encoder, because a dispatch reads what a queue write
1642
+ * has not landed.** `queue.writeBuffer` is ordered against other queue work and *not* against an
1643
+ * encoder's commands, which this backend has already paid for once: rewriting one params buffer
1644
+ * between dispatches gave every dispatch the last write, and the fix there was one bind group per
1645
+ * dispatch at a 256-aligned offset. Here the ordering is simply respected.
1646
+ *
1647
+ * **A frame that changed nothing uploads nothing**, which is what makes a static scene cost what
1648
+ * it cost before this existed.
1649
+ */
1650
+ applyStream(queue) {
1651
+ const scene = this.scene;
1652
+ const dirty = scene.takeDirty();
1653
+ if (dirty.clusters.length === 0 && dirty.meshes.length === 0)
1654
+ return;
1655
+ for (const span of dirty.clusters) {
1656
+ const count = span.to - span.from;
1657
+ queue.writeBuffer(this.clusterCull, span.from * STREAM_CULL_FLOATS * 4, scene.cull, span.from * STREAM_CULL_FLOATS, count * STREAM_CULL_FLOATS);
1658
+ queue.writeBuffer(this.clusterLod, span.from * STREAM_LOD_FLOATS * 4, scene.lod, span.from * STREAM_LOD_FLOATS, count * STREAM_LOD_FLOATS);
1659
+ queue.writeBuffer(this.clusterMeta, span.from * STREAM_META_WORDS * 4, scene.meta, span.from * STREAM_META_WORDS, count * STREAM_META_WORDS);
1660
+ queue.writeBuffer(this.materialOf, span.from * 4, scene.materialOf, span.from, count);
1661
+ }
1662
+ for (const span of dirty.meshes) {
1663
+ const count = span.to - span.from;
1664
+ queue.writeBuffer(this.transformBuffer, span.from * 16 * 4, scene.transforms, span.from * 16, count * 16);
1665
+ queue.writeBuffer(this.instanceSpheres, span.from * 4 * 4, scene.instanceSpheres, span.from * 4, count * 4);
1666
+ }
1667
+ /*
1668
+ * **Refitted, because the sphere the light is fitted to is a property of the scene.** It stood
1669
+ * still while the geometry did; a world that streams moves it, so its edges crawl — the known
1670
+ * limitation §3.4 of the design names and neither demo this was built for depends on.
1671
+ */
1672
+ scene.sceneBounds(this.bounds);
1673
+ }
1674
+ /** The radius of the sphere the shadow camera is fitted to. Diagnostic, and a test reads it. */
1675
+ shadowExtent() {
1676
+ return this.bounds[3];
1677
+ }
1678
+ writeFrame(device) {
1679
+ const view = this.view;
1680
+ if (view === null || this.clipCorrection === null)
1681
+ return false;
1682
+ /*
1683
+ * **Jittered as the renderer's own verbs are, before the correction**, so a reconstructed
1684
+ * frame's resolve finds this pipeline's surfaces where it un-jitters them to. Zero, and so the
1685
+ * consumer's matrix bit for bit, on a frame that is not reconstructed.
1686
+ */
1687
+ jitterClip(this.viewProj, view.viewProj, this.jitter);
1688
+ multiply(this.corrected, this.clipCorrection, this.viewProj);
1689
+ frustumPlanes(this.corrected, this.planes);
1690
+ /*
1691
+ * **Fitted every frame, because the sun moves and the eye may.** The box is the scene's
1692
+ * bounding sphere seen from wherever the light is now, or — once the scene is larger than
1693
+ * `followRadius` — a sphere of that radius around the eye, snapped to whole texels. See
1694
+ * `fitShadow`; a few dozen multiplies either way, and a day cycle needs nothing else.
1695
+ */
1696
+ this.shadowSpan = fitShadow(this.bounds, view.eye, this.shadowFollow, view.lightDir, this.shadowSize, this.light);
1697
+ correctShadowMatrix(SHADOW_RASTER_CORRECTION, this.light, this.lightRaster);
1698
+ correctShadowMatrix(SHADOW_LOOKUP_CORRECTION, this.light, this.lightLookup);
1699
+ /* In the lookup's convention, depth 0 to 1, which is the one the cull's two depth planes read. */
1700
+ frustumPlanes(this.lightLookup, this.lightPlanes);
1701
+ const params = this.frameParams;
1702
+ params.set(this.corrected, 0);
1703
+ params.set(this.lightLookup, FRAME_LIGHT_VIEW_PROJ);
1704
+ params[FRAME_EYE] = view.eye[0];
1705
+ params[FRAME_EYE + 1] = view.eye[1];
1706
+ params[FRAME_EYE + 2] = view.eye[2];
1707
+ params[FRAME_LIGHT_DIR] = view.lightDir[0];
1708
+ params[FRAME_LIGHT_DIR + 1] = view.lightDir[1];
1709
+ params[FRAME_LIGHT_DIR + 2] = view.lightDir[2];
1710
+ params[FRAME_LIGHT_COLOUR] = view.lightColour[0];
1711
+ params[FRAME_LIGHT_COLOUR + 1] = view.lightColour[1];
1712
+ params[FRAME_LIGHT_COLOUR + 2] = view.lightColour[2];
1713
+ params[FRAME_SKY] = view.ambient[0];
1714
+ params[FRAME_SKY + 1] = view.ambient[1];
1715
+ params[FRAME_SKY + 2] = view.ambient[2];
1716
+ params[FRAME_GROUND] = view.ambientGround[0];
1717
+ params[FRAME_GROUND + 1] = view.ambientGround[1];
1718
+ params[FRAME_GROUND + 2] = view.ambientGround[2];
1719
+ /*
1720
+ * **The width the raster drew at, and the row the buffer stores, apart.** The shading pass
1721
+ * finds a pixel by the row and places a triangle's corners by the width; with one number for
1722
+ * both, every frame whose width is not a multiple of `WIDTH_ALIGN` placed its corners as
1723
+ * though it were wider, and every texture coordinate slid sideways.
1724
+ */
1725
+ params[FRAME_SIZE] = this.width;
1726
+ params[FRAME_SIZE + 1] = this.height;
1727
+ params[FRAME_PITCH] = this.stride;
1728
+ params[FRAME_SIZE + 2] = view.time ?? 0;
1729
+ params[FRAME_SIZE + 3] = this.decode.layerSize;
1730
+ params[FRAME_SHADOW] = Math.min(Math.max(view.shadowStrength ?? 1, 0), 1);
1731
+ params[FRAME_SHADOW + 1] = this.shadowSize;
1732
+ params[FRAME_SHADOW + 2] = this.shadowSpan;
1733
+ params[FRAME_SHADOW + 3] = this.shadowMaxDistance;
1734
+ params[FRAME_SHADOW_LIMITS] = this.shadowMaxSlope;
1735
+ params[FRAME_SHADOW_LIMITS + 1] = this.shadowTaps;
1736
+ /*
1737
+ * **The probe's four numbers, or zeros.** `environment.w` at zero is what turns both
1738
+ * image-based terms off in the shader — the radiance falls back to the two-colour gradient the
1739
+ * forward path reflects in the same situation, and the split sum's selector goes with it,
1740
+ * because that fit is the correct share of a prefiltered chain and of nothing else.
1741
+ */
1742
+ const room = this.environment;
1743
+ params[FRAME_ENVIRONMENT] = room?.edge ?? 0;
1744
+ params[FRAME_ENVIRONMENT + 1] = room?.maxLod ?? 0;
1745
+ params[FRAME_ENVIRONMENT + 2] = room?.irradianceLevel ?? 0;
1746
+ params[FRAME_ENVIRONMENT + 3] = room === null ? 0 : 1;
1747
+ /*
1748
+ * **A view with no fog writes a density of zero**, which `mediumFog` answers zero to at every
1749
+ * distance — so the block is always present and the shader never branches on whether it is.
1750
+ */
1751
+ const fog = view.fog;
1752
+ params[FRAME_FOG_COLOUR] = fog?.colour[0] ?? 0;
1753
+ params[FRAME_FOG_COLOUR + 1] = fog?.colour[1] ?? 0;
1754
+ params[FRAME_FOG_COLOUR + 2] = fog?.colour[2] ?? 0;
1755
+ params[FRAME_FOG_COLOUR + 3] = fog?.eyeY ?? 0;
1756
+ params[FRAME_FOG_UNDERWATER] = fog?.underwaterColour[0] ?? 0;
1757
+ params[FRAME_FOG_UNDERWATER + 1] = fog?.underwaterColour[1] ?? 0;
1758
+ params[FRAME_FOG_UNDERWATER + 2] = fog?.underwaterColour[2] ?? 0;
1759
+ params[FRAME_FOG_UNDERWATER + 3] = fog?.underwaterFactor ?? 0;
1760
+ params[FRAME_FOG_HEIGHT] = fog?.heightFalloff ?? 0;
1761
+ params[FRAME_FOG_HEIGHT + 1] = fog?.density ?? 0;
1762
+ params[FRAME_FOG_RANGE] = fog?.near ?? 0;
1763
+ params[FRAME_FOG_RANGE + 1] = fog?.far ?? 0;
1764
+ params[FRAME_FOG_RANGE + 2] = fog?.mode ?? MEDIUM_FOG;
1765
+ params[FRAME_FOG_RANGE + 3] = fog?.underwaterDensity ?? 0;
1766
+ /* The scene's own decision about whether its grid supplies the ambient, as it is on the
1767
+ forward path: `ProbeBakeOptions.irradiance` reaches here as `PassEnvironment.irradiance`. */
1768
+ params[FRAME_ENVIRONMENT_MIX] = room !== null && room.irradiance ? 1 : 0;
1769
+ /* Every glow's scale, as `uEmissiveGain * uNightFactor` is on the forward path. */
1770
+ params[FRAME_ENVIRONMENT_MIX + 1] = (view.emissiveGain ?? 1) * (view.nightFactor ?? 1);
1771
+ this.lodParams[0] = this.height;
1772
+ this.lodParams[1] = view.fovY;
1773
+ this.lodParams[2] = view.lodThreshold;
1774
+ this.lodParams[3] = view.eye[0];
1775
+ this.lodParams[4] = view.eye[1];
1776
+ this.lodParams[5] = view.eye[2];
1777
+ /* Zero: the opaque half. The blended half is the same six numbers with a one here. */
1778
+ this.lodParams[6] = 0;
1779
+ this.lodParamsBlend.set(this.lodParams);
1780
+ this.lodParamsBlend[6] = 1;
1781
+ const base = this.levels[0];
1782
+ this.cullSettings[0] = view.eye[0];
1783
+ this.cullSettings[1] = view.eye[1];
1784
+ this.cullSettings[2] = view.eye[2];
1785
+ this.cullSettings[4] = base.width;
1786
+ this.cullSettings[5] = base.height;
1787
+ this.cullSettings[6] = this.levels.length;
1788
+ this.cullSettings[7] = this.clusterCount;
1789
+ const queue = device.queue;
1790
+ queue.writeBuffer(this.viewProjBuffer, 0, this.corrected);
1791
+ /* The same two numbers the shading pass reads out of its frame block, for the alpha test. */
1792
+ this.rasterParamValues[0] = params[FRAME_SIZE + 2];
1793
+ this.rasterParamValues[1] = params[FRAME_SIZE + 3];
1794
+ queue.writeBuffer(this.rasterParams, 0, this.rasterParamValues);
1795
+ queue.writeBuffer(this.shadowMatrix, 0, this.lightRaster);
1796
+ queue.writeBuffer(this.shadowPlanes, 0, this.lightPlanes);
1797
+ correctShadowMatrix(SHADOW_CULL_CORRECTION, this.lightRaster, this.lightCull);
1798
+ queue.writeBuffer(this.shadowCullMatrix, 0, this.lightCull);
1799
+ lightConeEye(view.eye, view.lightDir, this.shadowConeEye);
1800
+ this.shadowCullValues[0] = this.shadowConeEye[0];
1801
+ this.shadowCullValues[1] = this.shadowConeEye[1];
1802
+ this.shadowCullValues[2] = this.shadowConeEye[2];
1803
+ /* Two buffers for the reason the camera's phases have two: a queue write lands when made. */
1804
+ this.shadowCullValues[3] = 0;
1805
+ queue.writeBuffer(this.shadowCullSettings, 0, this.shadowCullValues);
1806
+ this.shadowCullValues[3] = this.occlusion ? 1 : 0;
1807
+ queue.writeBuffer(this.shadowCullSettingsTwo, 0, this.shadowCullValues);
1808
+ queue.writeBuffer(this.planesBuffer, 0, this.planes);
1809
+ queue.writeBuffer(this.frameBuffer, 0, params);
1810
+ queue.writeBuffer(this.lodBuffer, 0, this.lodParams);
1811
+ if (this.blendLodBuffer !== null) {
1812
+ queue.writeBuffer(this.blendLodBuffer, 0, this.lodParamsBlend);
1813
+ }
1814
+ /*
1815
+ * **Phase one has no pyramid, and it has a buffer of its own to say so.** The pyramid it would
1816
+ * test against is last frame's, seen from last frame's camera, and a cluster that one wrongly
1817
+ * hides is lost for the frame: phase one does not draw it, and phase two judges only what the
1818
+ * history does not hold.
1819
+ *
1820
+ * This was one buffer written twice — 0 here and 1 just before phase two — and a queue write
1821
+ * lands when it is made, while the frame's commands run at submit. So phase one read the 1.
1822
+ * `gpuDrivenPass.test.ts` reads each dispatch's settings as the device would.
1823
+ */
1824
+ const [phaseOne, phaseTwo, blend] = this.cullBuffers;
1825
+ this.cullSettings[CULL_CONES] = 1;
1826
+ this.cullSettings[3] = 0;
1827
+ queue.writeBuffer(phaseOne, 0, this.cullSettings);
1828
+ /* Phase two's pyramid, and the blended run's after it, unless occlusion was switched off. */
1829
+ this.cullSettings[3] = this.occlusion ? 1 : 0;
1830
+ queue.writeBuffer(phaseTwo, 0, this.cullSettings);
1831
+ /*
1832
+ * **Phase two's settings with the cones off, in a buffer of its own** for the reason phase one
1833
+ * has one: a queue write lands when it is made, so one buffer written twice is read as the
1834
+ * second write by every dispatch in the frame.
1835
+ */
1836
+ this.cullSettings[CULL_CONES] = 0;
1837
+ queue.writeBuffer(blend, 0, this.cullSettings);
1838
+ return true;
1839
+ }
1840
+ prepare(ctx) {
1841
+ if (ctx.backend !== 'webgpu')
1842
+ return;
1843
+ const device = this.device;
1844
+ if (device === null || this.levels.length === 0)
1845
+ return;
1846
+ /* Before `writeFrame`, which reads what this took, and before the groups it may invalidate. */
1847
+ if (this.adoptEnvironment(ctx.environment))
1848
+ this.buildSizedGroups(device);
1849
+ /* Last frame's figures, now that last frame's encoder has been submitted. See `readTimings`. */
1850
+ this.readTimings();
1851
+ this.readCounts();
1852
+ /* Before `writeFrame`, which fits the shadow to bounds this may have moved. */
1853
+ this.applyStream(device.queue);
1854
+ this.jitter = ctx.jitter;
1855
+ if (!this.writeFrame(device))
1856
+ return;
1857
+ const encoder = ctx.encoder;
1858
+ const [argsOne, argsTwo] = this.args;
1859
+ /*
1860
+ * **Asked of the material table rather than of the frame**, because it cannot change within
1861
+ * one: the table is written when the pass is built. A scene with nothing blended schedules the
1862
+ * three transparent stages away entirely and encodes the frame it encoded before they existed.
1863
+ */
1864
+ this.shape.blended = this.materials.some((material) => material.blend === true);
1865
+ /* `instanceCount` is the one word the compaction touches; the other three never change. */
1866
+ encoder.clearBuffer(argsOne, 4, 4);
1867
+ encoder.clearBuffer(argsTwo, 4, 4);
1868
+ if (this.shape.blended)
1869
+ encoder.clearBuffer(this.blendArgs, 4, 4);
1870
+ encoder.clearBuffer(this.writeHistory());
1871
+ encoder.clearBuffer(this.binCounts);
1872
+ /*
1873
+ * **Scheduled, then encoded.** The shading reads the map only where the frame is shadowed, and
1874
+ * the test is the shader's own — it returns before the lookup when the strength is not above
1875
+ * zero — so a frame that asked for no shadow schedules the stage that draws one away.
1876
+ */
1877
+ this.shape.shadowed = !((this.frameParams[FRAME_SHADOW] ?? 0) <= 0);
1878
+ resetDeps(this.graph);
1879
+ const first = recordGpuDrivenStages(this.graph, GPU_DRIVEN_RESOURCES, this.shape);
1880
+ const passes = scheduleGraph(this.graph, this.graph.count, FRAME_LIVE, GPU_DRIVEN_ORDERED, this.scheduled, this.graphScratch);
1881
+ for (let p = 0; p < passes; p += 1) {
1882
+ const pass = this.scheduled[p];
1883
+ if (pass === undefined)
1884
+ break;
1885
+ for (let node = pass.first; node < pass.first + pass.count; node += 1) {
1886
+ this.encodeStage(encoder, GPU_DRIVEN_PASSES[node - first]);
1887
+ }
1888
+ }
1889
+ if (this.queries !== null && this.queryResolve !== null && this.queryRead !== null) {
1890
+ if (!this.queryCopied && !this.queryMapping) {
1891
+ const pairs = GPU_DRIVEN_STAGES.length * 2;
1892
+ encoder.resolveQuerySet(this.queries, 0, pairs, this.queryResolve, 0);
1893
+ encoder.copyBufferToBuffer(this.queryResolve, 0, this.queryRead, 0, pairs * 8);
1894
+ this.queryCopied = true;
1895
+ }
1896
+ }
1897
+ if (this.countDrawn)
1898
+ this.copyCounts(device, encoder);
1899
+ this.flipped = !this.flipped;
1900
+ this.buildSwappedGroups(device);
1901
+ }
1902
+ /**
1903
+ * One stage of the frame, onto the encoder. `pipeline.ts` says what each reads and writes, and
1904
+ * the schedule says whether it runs; this is what it issues.
1905
+ */
1906
+ encodeStage(encoder, stage) {
1907
+ const groups = this.clusterGroups();
1908
+ const pixels = Math.ceil((this.stride * this.height) / BIN_GROUP_SIZE);
1909
+ switch (stage) {
1910
+ case 'clearColour':
1911
+ /*
1912
+ * **Emptied, because the shading writes only what the frame covered.** Alpha is what the
1913
+ * blit reads to decide whether this pipeline drew a pixel at all; left over from last frame
1914
+ * it paints a stale picture over the sky, and never cleared at all it paints black over it.
1915
+ */
1916
+ encoder
1917
+ .beginRenderPass({
1918
+ label: 'gpu-driven clear colour',
1919
+ colorAttachments: [
1920
+ {
1921
+ view: this.colourView,
1922
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
1923
+ loadOp: 'clear',
1924
+ storeOp: 'store',
1925
+ },
1926
+ ],
1927
+ })
1928
+ .end();
1929
+ return;
1930
+ case 'shadow':
1931
+ this.drawShadow(encoder);
1932
+ return;
1933
+ case 'instanceCull': {
1934
+ /* Before the cut, which reads what this writes. See `instances.ts`. */
1935
+ const instances = encoder.beginComputePass({
1936
+ label: 'gpu-driven instance cull',
1937
+ timestampWrites: this.writesFor('instanceCull'),
1938
+ });
1939
+ instances.setPipeline(this.instancePipeline);
1940
+ instances.setBindGroup(0, this.instanceGroup);
1941
+ instances.dispatchWorkgroups(Math.max(1, Math.ceil(this.meshCount / 64)));
1942
+ instances.end();
1943
+ return;
1944
+ }
1945
+ case 'cut': {
1946
+ const first = encoder.beginComputePass({
1947
+ label: 'gpu-driven cut and phase one cull',
1948
+ timestampWrites: this.writesFor('cut'),
1949
+ });
1950
+ first.setPipeline(this.lodPipeline);
1951
+ first.setBindGroup(0, this.lodGroup);
1952
+ first.dispatchWorkgroups(groups);
1953
+ first.setPipeline(this.cullPipeline);
1954
+ first.setBindGroup(0, this.cullGroups[0]);
1955
+ first.dispatchWorkgroups(groups);
1956
+ first.setPipeline(this.compactPipeline);
1957
+ first.setBindGroup(0, this.compactGroups[0]);
1958
+ first.dispatchWorkgroups(groups);
1959
+ first.end();
1960
+ return;
1961
+ }
1962
+ case 'phaseOneDraw':
1963
+ this.drawPhase(encoder, 0, true);
1964
+ return;
1965
+ case 'pyramid': {
1966
+ /* From the depth phase one just wrote. This is the whole reason for two halves. */
1967
+ const pyramid = encoder.beginComputePass({
1968
+ label: 'gpu-driven depth pyramid',
1969
+ timestampWrites: this.writesFor('pyramid'),
1970
+ });
1971
+ const base = this.levels[0];
1972
+ pyramid.setPipeline(this.seedPipeline);
1973
+ pyramid.setBindGroup(0, this.seedGroup);
1974
+ pyramid.dispatchWorkgroups(Math.ceil(base.width / 8), Math.ceil(base.height / 8));
1975
+ pyramid.setPipeline(this.reducePipeline);
1976
+ for (let level = 1; level < this.levels.length; level += 1) {
1977
+ const size = this.levels[level];
1978
+ pyramid.setBindGroup(0, this.reduceGroups[level - 1]);
1979
+ pyramid.dispatchWorkgroups(Math.ceil(size.width / 8), Math.ceil(size.height / 8));
1980
+ }
1981
+ pyramid.end();
1982
+ return;
1983
+ }
1984
+ case 'phaseTwoCull': {
1985
+ /* The same cull with the pyramid switched on, over what phase one did not hold. */
1986
+ const second = encoder.beginComputePass({
1987
+ label: 'gpu-driven phase two cull',
1988
+ timestampWrites: this.writesFor('phaseTwoCull'),
1989
+ });
1990
+ second.setPipeline(this.cullPipeline);
1991
+ second.setBindGroup(0, this.cullGroups[1]);
1992
+ second.dispatchWorkgroups(groups);
1993
+ second.setPipeline(this.compactPipeline);
1994
+ second.setBindGroup(0, this.compactGroups[1]);
1995
+ second.dispatchWorkgroups(groups);
1996
+ second.end();
1997
+ return;
1998
+ }
1999
+ case 'phaseTwoDraw':
2000
+ this.drawPhase(encoder, 1, false);
2001
+ return;
2002
+ case 'blendCull': {
2003
+ /*
2004
+ * **The front half, run again over the other half of the scene.** Three dispatches, the
2005
+ * same three the opaque cut issues and the same three pipelines: which clusters, at which
2006
+ * level of detail, and pack the survivors into a list. Only the cut's seventh parameter
2007
+ * and the buffers the compaction writes into are different.
2008
+ *
2009
+ * **The cull runs with the pyramid on**, which is the phase-two settings: the depth is
2010
+ * complete by now, so a pane wholly behind a wall is occluded exactly as an opaque cluster
2011
+ * there would be.
2012
+ */
2013
+ const cut = encoder.beginComputePass({
2014
+ label: 'gpu-driven blend cull',
2015
+ timestampWrites: this.writesFor('blendCull'),
2016
+ });
2017
+ cut.setPipeline(this.lodPipeline);
2018
+ cut.setBindGroup(0, this.blendLodGroup);
2019
+ cut.dispatchWorkgroups(groups);
2020
+ cut.setPipeline(this.cullPipeline);
2021
+ /* Phase two's settings with the cone test off: the blended raster draws both sides. */
2022
+ cut.setBindGroup(0, this.cullGroups[2]);
2023
+ cut.dispatchWorkgroups(groups);
2024
+ cut.setPipeline(this.compactPipeline);
2025
+ cut.setBindGroup(0, this.blendCompactGroup);
2026
+ cut.dispatchWorkgroups(groups);
2027
+ cut.end();
2028
+ return;
2029
+ }
2030
+ case 'blendDraw': {
2031
+ const pass = encoder.beginRenderPass({
2032
+ label: 'gpu-driven blend draw',
2033
+ colorAttachments: [
2034
+ {
2035
+ view: this.oitAccumView,
2036
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
2037
+ loadOp: 'clear',
2038
+ storeOp: 'store',
2039
+ },
2040
+ {
2041
+ /*
2042
+ * **Cleared to one: nothing has covered this pixel yet.** Cleared to zero the resolve
2043
+ * would show no scene anywhere the pass ran, which is a black frame that looks like
2044
+ * the blend working. `renderer.ts`'s own pass carries the same line.
2045
+ */
2046
+ view: this.oitRevealView,
2047
+ clearValue: { r: 1, g: 1, b: 1, a: 1 },
2048
+ loadOp: 'clear',
2049
+ storeOp: 'store',
2050
+ },
2051
+ ],
2052
+ depthStencilAttachment: {
2053
+ view: this.depthTexture.createView(),
2054
+ depthReadOnly: true,
2055
+ },
2056
+ timestampWrites: this.writesFor('blendDraw'),
2057
+ });
2058
+ pass.setPipeline(this.blendPipeline);
2059
+ pass.setBindGroup(0, this.blendRasterGroup);
2060
+ pass.setViewport(0, 0, this.width, this.height, 0, 1);
2061
+ pass.drawIndirect(this.blendArgs, 0);
2062
+ pass.end();
2063
+ return;
2064
+ }
2065
+ case 'blendResolve': {
2066
+ const pass = encoder.beginRenderPass({
2067
+ label: 'gpu-driven blend resolve',
2068
+ colorAttachments: [
2069
+ { view: this.colourView, loadOp: 'load', storeOp: 'store' },
2070
+ ],
2071
+ timestampWrites: this.writesFor('blendResolve'),
2072
+ });
2073
+ pass.setPipeline(this.blendResolvePipeline);
2074
+ pass.setBindGroup(0, this.blendResolveGroup);
2075
+ pass.draw(3);
2076
+ pass.end();
2077
+ return;
2078
+ }
2079
+ case 'visibilityCopy':
2080
+ /* The binning reads a buffer and a render attachment is not one. See the header. */
2081
+ encoder.copyTextureToBuffer({ texture: this.visibilityTexture }, { buffer: this.visibility, bytesPerRow: this.stride * 4 }, { width: this.stride, height: this.height });
2082
+ return;
2083
+ case 'bin': {
2084
+ const binning = encoder.beginComputePass({
2085
+ label: 'gpu-driven material bins',
2086
+ timestampWrites: this.writesFor('bin'),
2087
+ });
2088
+ binning.setPipeline(this.countPipeline);
2089
+ binning.setBindGroup(0, this.countGroup);
2090
+ binning.dispatchWorkgroups(pixels);
2091
+ binning.setPipeline(this.offsetsPipeline);
2092
+ binning.setBindGroup(0, this.offsetsGroup);
2093
+ binning.dispatchWorkgroups(1);
2094
+ binning.end();
2095
+ return;
2096
+ }
2097
+ case 'cursorCopy':
2098
+ /* The cursors start as a copy of the offsets, because the offsets must survive the scatter. */
2099
+ encoder.copyBufferToBuffer(this.binOffsets, 0, this.binCursors, 0, Math.max(1, this.materials.length) * 4);
2100
+ return;
2101
+ case 'shade': {
2102
+ const shade = encoder.beginComputePass({
2103
+ label: 'gpu-driven scatter and shade',
2104
+ timestampWrites: this.writesFor('shade'),
2105
+ });
2106
+ shade.setPipeline(this.scatterPipeline);
2107
+ shade.setBindGroup(0, this.scatterGroup);
2108
+ shade.dispatchWorkgroups(pixels);
2109
+ shade.setPipeline(this.shadePipeline);
2110
+ shade.setBindGroup(0, this.shadeGroup);
2111
+ for (let m = 0; m < this.jobGroups.length; m += 1) {
2112
+ shade.setBindGroup(1, this.jobGroups[m]);
2113
+ /* Indirect, from the block the prefix sum wrote: an empty bin is zero groups. */
2114
+ shade.dispatchWorkgroupsIndirect(this.binDispatch, m * BIN_DISPATCH_WORDS * 4);
2115
+ }
2116
+ shade.end();
2117
+ return;
2118
+ }
2119
+ }
2120
+ }
2121
+ /**
2122
+ * The device milliseconds each stage took, or null where nothing measured them.
2123
+ *
2124
+ * One frame behind, because a timestamp is read back and a readback that blocked would be the
2125
+ * measurement changing what it measures. `null` until a frame has completed a map.
2126
+ */
2127
+ stageTime(stage) {
2128
+ if (!this.timed)
2129
+ return null;
2130
+ return this.stageMs[GPU_DRIVEN_STAGES.indexOf(stage)] ?? null;
2131
+ }
2132
+ /** Every stage added up, which is what the pipeline costs the device. */
2133
+ get totalMs() {
2134
+ if (!this.timed)
2135
+ return null;
2136
+ let total = 0;
2137
+ for (const value of this.stageMs)
2138
+ total += value;
2139
+ return total;
2140
+ }
2141
+ /** The pair of slots a stage writes, or nothing where the device cannot time. */
2142
+ writesFor(stage) {
2143
+ if (this.queries === null)
2144
+ return undefined;
2145
+ const at = GPU_DRIVEN_STAGES.indexOf(stage) * 2;
2146
+ return { querySet: this.queries, beginningOfPassWriteIndex: at, endOfPassWriteIndex: at + 1 };
2147
+ }
2148
+ /**
2149
+ * Ask for last frame's figures, once the frame that recorded them has been submitted.
2150
+ *
2151
+ * **At the start of the next frame rather than at the end of this one**, which is the whole of
2152
+ * the care this needs: `mapAsync` settles against the work already enqueued, and the copy is
2153
+ * still sitting in an encoder the renderer has not submitted yet.
2154
+ */
2155
+ readTimings() {
2156
+ const read = this.queryRead;
2157
+ if (read === null || !this.queryCopied || this.queryMapping)
2158
+ return;
2159
+ this.queryMapping = true;
2160
+ void read
2161
+ .mapAsync(MAP_MODE_READ)
2162
+ .then(() => {
2163
+ const stamps = new BigUint64Array(read.getMappedRange().slice(0));
2164
+ for (let stage = 0; stage < GPU_DRIVEN_STAGES.length; stage += 1) {
2165
+ const began = stamps[stage * 2] ?? 0n;
2166
+ const ended = stamps[stage * 2 + 1] ?? 0n;
2167
+ this.stageMs[stage] = ended > began ? Number(ended - began) / 1e6 : 0;
2168
+ }
2169
+ this.timed = true;
2170
+ read.unmap();
2171
+ })
2172
+ .catch(() => {
2173
+ /* A device lost mid-map answers nothing, which is the honest figure for a frame that
2174
+ did not finish. The next frame asks again. */
2175
+ })
2176
+ .finally(() => {
2177
+ this.queryMapping = false;
2178
+ this.queryCopied = false;
2179
+ });
2180
+ }
2181
+ /**
2182
+ * How many clusters `list` drew, one frame behind as the stage times are, or null until a frame
2183
+ * has been read — and null always where `countDrawn` was not asked for.
2184
+ */
2185
+ drawnClusters(list) {
2186
+ if (!this.counted)
2187
+ return null;
2188
+ const at = GPU_DRIVEN_LISTS.indexOf(list);
2189
+ const counted = this.drawnCounts[at];
2190
+ /* The map is drawn in two halves, and its count is both. */
2191
+ return list === 'shadow' ? counted + this.drawnCounts[at + 1] : counted;
2192
+ }
2193
+ /**
2194
+ * Each list's count into the read buffer, **after the whole frame**, so every draw that reads an
2195
+ * argument block has been encoded before the copy out of it. The buffer is cleared first, so a
2196
+ * list the frame did not draw — no blended material, no shadow — reads as none rather than as
2197
+ * whatever the last frame that drew it left. Nothing while a read is in flight: a buffer that is
2198
+ * mapped cannot be copied into. (A copy not yet read cannot be here: `readCounts` runs at the top
2199
+ * of every frame this ends, and maps whatever was copied.)
2200
+ */
2201
+ copyCounts(device, encoder) {
2202
+ if (this.countsMapping)
2203
+ return;
2204
+ const bytes = this.drawnCounts.byteLength;
2205
+ this.counts ??= device.createBuffer({
2206
+ label: 'gpu-driven drawn',
2207
+ size: bytes,
2208
+ usage: MAP_READ | COPY_DST,
2209
+ });
2210
+ const [argsOne, argsTwo] = this.args;
2211
+ encoder.clearBuffer(this.counts, 0, bytes);
2212
+ encoder.copyBufferToBuffer(argsOne, 4, this.counts, 0, 4);
2213
+ encoder.copyBufferToBuffer(argsTwo, 4, this.counts, 4, 4);
2214
+ if (this.shape.blended) {
2215
+ encoder.copyBufferToBuffer(this.blendArgs, 4, this.counts, 8, 4);
2216
+ }
2217
+ if (this.shape.shadowed) {
2218
+ encoder.copyBufferToBuffer(this.shadowArgs, 4, this.counts, 12, 4);
2219
+ encoder.copyBufferToBuffer(this.shadowArgsTwo, 4, this.counts, 16, 4);
2220
+ }
2221
+ this.countsCopied = true;
2222
+ }
2223
+ /** Last frame's counts, asked for as `readTimings` asks for its figures and for its reason. */
2224
+ readCounts() {
2225
+ const read = this.counts;
2226
+ if (read === null || !this.countsCopied || this.countsMapping)
2227
+ return;
2228
+ this.countsMapping = true;
2229
+ void read
2230
+ .mapAsync(MAP_MODE_READ)
2231
+ .then(() => {
2232
+ this.drawnCounts.set(new Uint32Array(read.getMappedRange().slice(0)));
2233
+ this.counted = true;
2234
+ read.unmap();
2235
+ })
2236
+ .catch(() => {
2237
+ /* A device lost mid-map answers nothing; the next frame asks again. */
2238
+ })
2239
+ .finally(() => {
2240
+ this.countsMapping = false;
2241
+ this.countsCopied = false;
2242
+ });
2243
+ }
2244
+ /** How many workgroups one dispatch over every cluster takes. */
2245
+ clusterGroups() {
2246
+ return Math.max(1, Math.ceil(this.clusterCount / 64));
2247
+ }
2248
+ /** The half of the ping-pong this frame writes, which next frame will read. */
2249
+ writeHistory() {
2250
+ return this.flipped ? this.history : this.drawn;
2251
+ }
2252
+ /**
2253
+ * The map: what the light's own cull kept, drawn from the light, depth only — **in the camera's
2254
+ * two halves.**
2255
+ *
2256
+ * **The front half's three pipelines over buffers of the light's, twice**: the cut, the cull
2257
+ * against the light's box from the sun's side, and a compaction of what the light kept last
2258
+ * frame, drawn; then a pyramid of that depth, the same cull against it, and a compaction of the
2259
+ * rest, drawn over it. A three-degree sun over a city sees a strip of it a kilometre deep, and
2260
+ * everything behind the first row toward the sun is in that row's shadow — which is exactly what
2261
+ * the pyramid of the first half says, and what the frustum and the cones alone could not.
2262
+ * `CLUSTER_INDEX_CAP` vertices by one instance a listed cluster, as in the frame's own draw.
2263
+ *
2264
+ * **One figure for the stage**: the first compute pass writes where the stage began and the
2265
+ * second render pass where it ended, so `shadow` in the stage times is all of it.
2266
+ */
2267
+ drawShadow(encoder) {
2268
+ const groups = this.clusterGroups();
2269
+ const writes = this.writesFor('shadow');
2270
+ const [cullOne, cullTwo] = this.shadowCullGroups;
2271
+ const way = this.shadowFlipped ? 1 : 0;
2272
+ const [compactOne, compactTwo] = this.shadowCompactGroups[way];
2273
+ /* The two words the compactions count in, and the half of the history this frame writes. */
2274
+ encoder.clearBuffer(this.shadowArgs, 4, 4);
2275
+ encoder.clearBuffer(this.shadowArgsTwo, 4, 4);
2276
+ encoder.clearBuffer(this.shadowHistories[1 - way]);
2277
+ const first = encoder.beginComputePass({
2278
+ label: 'gpu-driven shadow cull',
2279
+ timestampWrites: writes === undefined
2280
+ ? undefined
2281
+ : {
2282
+ querySet: writes.querySet,
2283
+ beginningOfPassWriteIndex: writes.beginningOfPassWriteIndex,
2284
+ },
2285
+ });
2286
+ first.setPipeline(this.lodPipeline);
2287
+ first.setBindGroup(0, this.shadowLodGroup);
2288
+ first.dispatchWorkgroups(groups);
2289
+ first.setPipeline(this.cullPipeline);
2290
+ first.setBindGroup(0, cullOne);
2291
+ first.dispatchWorkgroups(groups);
2292
+ first.setPipeline(this.compactPipeline);
2293
+ first.setBindGroup(0, compactOne);
2294
+ first.dispatchWorkgroups(groups);
2295
+ first.end();
2296
+ this.drawShadowHalf(encoder, 0, undefined);
2297
+ /* From the depth the first half just wrote. This is the whole reason for two halves. */
2298
+ const second = encoder.beginComputePass({ label: 'gpu-driven shadow occlusion' });
2299
+ const base = this.shadowLevels[0];
2300
+ second.setPipeline(this.shadowSeedPipeline);
2301
+ second.setBindGroup(0, this.shadowSeedGroup);
2302
+ second.dispatchWorkgroups(Math.ceil(base.width / 8), Math.ceil(base.height / 8));
2303
+ second.setPipeline(this.reducePipeline);
2304
+ for (let level = 1; level < this.shadowLevels.length; level += 1) {
2305
+ const size = this.shadowLevels[level];
2306
+ second.setBindGroup(0, this.shadowReduceGroups[level - 1]);
2307
+ second.dispatchWorkgroups(Math.ceil(size.width / 8), Math.ceil(size.height / 8));
2308
+ }
2309
+ second.setPipeline(this.cullPipeline);
2310
+ second.setBindGroup(0, cullTwo);
2311
+ second.dispatchWorkgroups(groups);
2312
+ second.setPipeline(this.compactPipeline);
2313
+ second.setBindGroup(0, compactTwo);
2314
+ second.dispatchWorkgroups(groups);
2315
+ second.end();
2316
+ this.drawShadowHalf(encoder, 1, writes);
2317
+ this.shadowFlipped = !this.shadowFlipped;
2318
+ }
2319
+ /** One half of the map: the first clears it, the second draws over it and closes the stage's time. */
2320
+ drawShadowHalf(encoder, half, writes) {
2321
+ const pass = encoder.beginRenderPass({
2322
+ label: half === 0 ? 'gpu-driven shadow' : 'gpu-driven shadow, phase 2',
2323
+ colorAttachments: [],
2324
+ depthStencilAttachment: {
2325
+ view: this.shadowView,
2326
+ depthClearValue: SHADOW_DEPTH_CLEAR,
2327
+ depthLoadOp: half === 0 ? 'clear' : 'load',
2328
+ depthStoreOp: 'store',
2329
+ },
2330
+ timestampWrites: writes === undefined
2331
+ ? undefined
2332
+ : { querySet: writes.querySet, endOfPassWriteIndex: writes.endOfPassWriteIndex },
2333
+ });
2334
+ pass.setPipeline(this.shadowPipeline);
2335
+ pass.setBindGroup(0, (half === 0 ? this.shadowGroup : this.shadowGroupTwo));
2336
+ pass.drawIndirect((half === 0 ? this.shadowArgs : this.shadowArgsTwo), 0);
2337
+ pass.end();
2338
+ }
2339
+ drawPhase(encoder, phase, clear) {
2340
+ const pass = encoder.beginRenderPass({
2341
+ label: `gpu-driven phase ${phase + 1}`,
2342
+ colorAttachments: [
2343
+ {
2344
+ view: this.visibilityTexture.createView(),
2345
+ /* Cleared to the sentinel, not to zero: zero is triangle 0 of cluster 0, a real one. */
2346
+ clearValue: { r: VIS_EMPTY, g: 0, b: 0, a: 0 },
2347
+ loadOp: clear ? 'clear' : 'load',
2348
+ storeOp: 'store',
2349
+ },
2350
+ ],
2351
+ depthStencilAttachment: {
2352
+ view: this.depthTexture.createView(),
2353
+ depthClearValue: DEPTH_CLEAR,
2354
+ depthLoadOp: clear ? 'clear' : 'load',
2355
+ depthStoreOp: 'store',
2356
+ },
2357
+ timestampWrites: this.writesFor(phase === 0 ? 'phaseOneDraw' : 'phaseTwoDraw'),
2358
+ });
2359
+ pass.setPipeline(this.rasterPipeline);
2360
+ pass.setBindGroup(0, this.rasterGroups[phase]);
2361
+ /* The used region, inside a target widened for the copy's row alignment. */
2362
+ pass.setViewport(0, 0, this.width, this.height, 0, 1);
2363
+ pass.drawIndirect(this.args[phase], 0);
2364
+ pass.end();
2365
+ }
2366
+ draw(ctx) {
2367
+ if (ctx.backend !== 'webgpu')
2368
+ return;
2369
+ if (this.blitPipeline === null || this.blitGroup === null)
2370
+ return;
2371
+ if (ctx.outputTransform !== 0 && !this.gradeWarned) {
2372
+ this.gradeWarned = true;
2373
+ /*
2374
+ * The pass writes linear light and expects a composite to grade it. A frame with no
2375
+ * composite grades per pass, and applying the curve here would be a second copy of it —
2376
+ * which is the thing `SHADE_MATERIAL_WGSL` is built to avoid. Said once, out loud, because
2377
+ * a silently ungraded frame is a difference somebody would attribute to the pipeline.
2378
+ */
2379
+ console.warn('[driftengine] the gpu-driven pass writes linear light and this frame has no composite ' +
2380
+ 'to grade it, so the picture will be flat. Use a quality profile with a scene target.');
2381
+ }
2382
+ ctx.pass.setPipeline(this.blitPipeline);
2383
+ ctx.pass.setBindGroup(0, this.blitGroup);
2384
+ ctx.pass.draw(3);
2385
+ }
2386
+ dispose(device) {
2387
+ /* Before the buffers go: a write to a destroyed buffer is a device error. See `detach`. */
2388
+ this.scene.detach();
2389
+ if (device.backend !== 'webgpu')
2390
+ return;
2391
+ this.disposeTargets();
2392
+ this.shadowTexture?.destroy();
2393
+ this.emptyEnvironment?.destroy();
2394
+ this.latents?.destroy();
2395
+ this.queries?.destroy();
2396
+ this.queryResolve?.destroy();
2397
+ this.queryRead?.destroy();
2398
+ this.counts?.destroy();
2399
+ for (const buffer of [
2400
+ this.clusterCull,
2401
+ this.clusterLod,
2402
+ this.clusterMeta,
2403
+ this.instanceSpheres,
2404
+ this.instanceFlags,
2405
+ this.indices,
2406
+ this.vertices,
2407
+ this.transformBuffer,
2408
+ this.materialOf,
2409
+ this.materialTable,
2410
+ this.decodeNodes,
2411
+ this.decodeWeights,
2412
+ this.jobs,
2413
+ this.shadowList,
2414
+ this.shadowMatrix,
2415
+ this.planesBuffer,
2416
+ this.viewProjBuffer,
2417
+ this.rasterParams,
2418
+ ...(this.cullBuffers ?? []),
2419
+ this.lodBuffer,
2420
+ this.blendLodBuffer,
2421
+ this.blendList,
2422
+ this.blendArgs,
2423
+ this.blendCompactSettings,
2424
+ this.zeroHistory,
2425
+ this.historySink,
2426
+ this.shadowArgs,
2427
+ this.shadowArgsTwo,
2428
+ this.shadowListTwo,
2429
+ this.shadowSelected,
2430
+ this.shadowKeep,
2431
+ this.shadowPlanes,
2432
+ this.shadowCullSettings,
2433
+ this.shadowCullSettingsTwo,
2434
+ this.shadowCompactSettings,
2435
+ this.shadowCompactSettingsTwo,
2436
+ ...(this.shadowHistories ?? []),
2437
+ this.shadowHzb,
2438
+ this.shadowLevelStart,
2439
+ this.shadowSeedSize,
2440
+ this.shadowCullMatrix,
2441
+ ...this.shadowLevels.map((level) => level.size),
2442
+ this.instanceNone,
2443
+ this.frameBuffer,
2444
+ this.selected,
2445
+ this.keep,
2446
+ this.history,
2447
+ this.drawn,
2448
+ ...(this.lists ?? []),
2449
+ ...(this.args ?? []),
2450
+ ...(this.phaseSettings ?? []),
2451
+ ]) {
2452
+ buffer?.destroy();
2453
+ }
2454
+ this.device = null;
2455
+ }
2456
+ }
2457
+ /**
2458
+ * `out = a · b`, column-major, the same order `renderer.ts` applies `CLIP_CORRECTION` in.
2459
+ *
2460
+ * Its own four lines rather than `gl-matrix`, because `render/backend/webgpu/` does not import it
2461
+ * and one multiply is not a reason to start.
2462
+ */
2463
+ function multiply(out, a, b) {
2464
+ for (let column = 0; column < 4; column += 1) {
2465
+ for (let row = 0; row < 4; row += 1) {
2466
+ let sum = 0;
2467
+ for (let k = 0; k < 4; k += 1) {
2468
+ sum += a[k * 4 + row] * b[column * 4 + k];
2469
+ }
2470
+ out[column * 4 + row] = sum;
2471
+ }
2472
+ }
2473
+ }