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