@driftengine/core 3.62.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.json +122 -0
- package/NOTICE +20 -0
- package/README.md +1 -1
- package/dist/core/loop.js +18 -2
- package/dist/core/storage.js +5 -0
- package/dist/geometry/meshBuilder.d.ts +1 -1
- package/dist/geometry/meshBuilder.js +6 -2
- package/dist/index.d.ts +70 -4
- package/dist/index.js +123 -2
- package/dist/input/input.js +6 -1
- package/dist/math/exact.d.ts +13 -3
- package/dist/math/exact.js +68 -3
- package/dist/math/frustum.d.ts +17 -2
- package/dist/math/frustum.js +24 -0
- package/dist/render/backend/api.d.ts +34 -1
- package/dist/render/backend/api.js +22 -1
- package/dist/render/backend/budget.d.ts +7 -6
- package/dist/render/backend/budget.js +7 -6
- package/dist/render/backend/createRenderer.d.ts +57 -0
- package/dist/render/backend/createRenderer.js +51 -2
- package/dist/render/backend/materialChanges.d.ts +41 -0
- package/dist/render/backend/materialChanges.js +38 -0
- package/dist/render/backend/select.js +8 -0
- package/dist/render/backend/webgl2/renderer.d.ts +80 -1
- package/dist/render/backend/webgl2/renderer.js +208 -10
- package/dist/render/backend/webgpu/buffers.d.ts +20 -1
- package/dist/render/backend/webgpu/buffers.js +116 -58
- package/dist/render/backend/webgpu/fieldCompose.d.ts +156 -0
- package/dist/render/backend/webgpu/fieldCompose.js +396 -0
- package/dist/render/backend/webgpu/giFieldPass.d.ts +66 -0
- package/dist/render/backend/webgpu/giFieldPass.js +137 -0
- package/dist/render/backend/webgpu/gpuDrivenPass.d.ts +728 -0
- package/dist/render/backend/webgpu/gpuDrivenPass.js +2473 -0
- package/dist/render/backend/webgpu/linePass.d.ts +4 -14
- package/dist/render/backend/webgpu/linePass.js +4 -33
- package/dist/render/backend/webgpu/probeBake.d.ts +148 -0
- package/dist/render/backend/webgpu/probeBake.js +493 -0
- package/dist/render/backend/webgpu/renderer.d.ts +379 -24
- package/dist/render/backend/webgpu/renderer.js +1369 -76
- package/dist/render/backend/webgpu/scatterPass.js +15 -1
- package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +47 -14
- package/dist/render/backend/webgpu/surfaceTexturePass.js +128 -80
- package/dist/render/causticsRenderer.d.ts +3 -1
- package/dist/render/causticsRenderer.js +4 -1
- package/dist/render/depthConvention.d.ts +12 -0
- package/dist/render/depthConvention.js +14 -0
- package/dist/render/deviceFeatures.d.ts +32 -0
- package/dist/render/deviceFeatures.js +41 -0
- package/dist/render/fog.d.ts +79 -0
- package/dist/render/fog.js +138 -0
- package/dist/render/frame/alias.d.ts +30 -0
- package/dist/render/frame/alias.js +61 -0
- package/dist/render/frame/deps.d.ts +37 -0
- package/dist/render/frame/deps.js +122 -0
- package/dist/render/frame/flushGraph.d.ts +43 -0
- package/dist/render/frame/flushGraph.js +84 -0
- package/dist/render/frame/graphSchedule.d.ts +49 -0
- package/dist/render/frame/graphSchedule.js +120 -0
- package/dist/render/frame/index.d.ts +15 -0
- package/dist/render/frame/index.js +8 -0
- package/dist/render/frame/lifetime.d.ts +24 -0
- package/dist/render/frame/lifetime.js +36 -0
- package/dist/render/frame/passTimings.d.ts +42 -0
- package/dist/render/frame/passTimings.js +38 -0
- package/dist/render/frame/validate.d.ts +15 -0
- package/dist/render/frame/validate.js +22 -0
- package/dist/render/frame/virtual.d.ts +44 -0
- package/dist/render/frame/virtual.js +61 -0
- package/dist/render/gi/chain.d.ts +82 -0
- package/dist/render/gi/chain.js +135 -0
- package/dist/render/gi/denoise.d.ts +114 -0
- package/dist/render/gi/denoise.js +169 -0
- package/dist/render/gi/fieldScene.d.ts +125 -0
- package/dist/render/gi/fieldScene.js +219 -0
- package/dist/render/gi/globalField.d.ts +157 -0
- package/dist/render/gi/globalField.js +373 -0
- package/dist/render/gi/probeGridFit.d.ts +24 -0
- package/dist/render/gi/probeGridFit.js +71 -0
- package/dist/render/gi/probeTrace.d.ts +82 -0
- package/dist/render/gi/probeTrace.js +128 -0
- package/dist/render/gi/probeVolume.d.ts +136 -0
- package/dist/render/gi/probeVolume.js +191 -0
- package/dist/render/gi/reflection.d.ts +73 -0
- package/dist/render/gi/reflection.js +109 -0
- package/dist/render/gi/traceField.d.ts +91 -0
- package/dist/render/gi/traceField.js +129 -0
- package/dist/render/gi/traceScreen.d.ts +68 -0
- package/dist/render/gi/traceScreen.js +82 -0
- package/dist/render/gpuScene.d.ts +38 -0
- package/dist/render/gpuScene.js +41 -0
- package/dist/render/gpudriven/alphaTest.d.ts +28 -0
- package/dist/render/gpudriven/alphaTest.js +32 -0
- package/dist/render/gpudriven/clusterUpload.d.ts +59 -0
- package/dist/render/gpudriven/clusterUpload.js +75 -0
- package/dist/render/gpudriven/clusterWorld.d.ts +57 -0
- package/dist/render/gpudriven/clusterWorld.js +155 -0
- package/dist/render/gpudriven/compact.d.ts +51 -0
- package/dist/render/gpudriven/compact.js +68 -0
- package/dist/render/gpudriven/cullClusters.d.ts +129 -0
- package/dist/render/gpudriven/cullClusters.js +237 -0
- package/dist/render/gpudriven/decodeTables.d.ts +64 -0
- package/dist/render/gpudriven/decodeTables.js +219 -0
- package/dist/render/gpudriven/frustum.d.ts +25 -0
- package/dist/render/gpudriven/frustum.js +59 -0
- package/dist/render/gpudriven/geometryHarness.d.ts +15 -0
- package/dist/render/gpudriven/geometryHarness.js +31 -0
- package/dist/render/gpudriven/hzb.d.ts +60 -0
- package/dist/render/gpudriven/hzb.js +97 -0
- package/dist/render/gpudriven/ibl.d.ts +85 -0
- package/dist/render/gpudriven/ibl.js +129 -0
- package/dist/render/gpudriven/indirect.d.ts +67 -0
- package/dist/render/gpudriven/indirect.js +77 -0
- package/dist/render/gpudriven/instances.d.ts +43 -0
- package/dist/render/gpudriven/instances.js +172 -0
- package/dist/render/gpudriven/lit.d.ts +165 -0
- package/dist/render/gpudriven/lit.js +165 -0
- package/dist/render/gpudriven/lodCut.d.ts +28 -0
- package/dist/render/gpudriven/lodCut.js +38 -0
- package/dist/render/gpudriven/materialBin.d.ts +60 -0
- package/dist/render/gpudriven/materialBin.js +108 -0
- package/dist/render/gpudriven/materialTable.d.ts +88 -0
- package/dist/render/gpudriven/materialTable.js +115 -0
- package/dist/render/gpudriven/pipeline.d.ts +135 -0
- package/dist/render/gpudriven/pipeline.js +237 -0
- package/dist/render/gpudriven/rangeAlloc.d.ts +50 -0
- package/dist/render/gpudriven/rangeAlloc.js +112 -0
- package/dist/render/gpudriven/sceneUpload.d.ts +96 -0
- package/dist/render/gpudriven/sceneUpload.js +115 -0
- package/dist/render/gpudriven/shadeBins.d.ts +92 -0
- package/dist/render/gpudriven/shadeBins.js +182 -0
- package/dist/render/gpudriven/shadow.d.ts +136 -0
- package/dist/render/gpudriven/shadow.js +224 -0
- package/dist/render/gpudriven/shadowCamera.d.ts +87 -0
- package/dist/render/gpudriven/shadowCamera.js +345 -0
- package/dist/render/gpudriven/streamScene.d.ts +194 -0
- package/dist/render/gpudriven/streamScene.js +459 -0
- package/dist/render/gpudriven/surfaceFrame.d.ts +25 -0
- package/dist/render/gpudriven/surfaceFrame.js +51 -0
- package/dist/render/gpudriven/twoPhase.d.ts +122 -0
- package/dist/render/gpudriven/twoPhase.js +160 -0
- package/dist/render/gpudriven/visbuffer.d.ts +25 -0
- package/dist/render/gpudriven/visbuffer.js +37 -0
- package/dist/render/halfFloat.d.ts +10 -0
- package/dist/render/halfFloat.js +23 -4
- package/dist/render/inference/attentionTiled.d.ts +27 -0
- package/dist/render/inference/attentionTiled.js +117 -0
- package/dist/render/inference/denseKernels.d.ts +29 -0
- package/dist/render/inference/denseKernels.js +312 -0
- package/dist/render/inference/device.d.ts +21 -0
- package/dist/render/inference/device.js +32 -0
- package/dist/render/inference/deviceGraph.d.ts +34 -0
- package/dist/render/inference/deviceGraph.js +17 -0
- package/dist/render/inference/fuse.d.ts +15 -0
- package/dist/render/inference/fuse.js +37 -0
- package/dist/render/inference/kernelKit.d.ts +52 -0
- package/dist/render/inference/kernelKit.js +70 -0
- package/dist/render/inference/kernels.d.ts +3 -0
- package/dist/render/inference/kernels.js +11 -0
- package/dist/render/inference/plan.d.ts +13 -0
- package/dist/render/inference/plan.js +18 -0
- package/dist/render/inference/reuse.d.ts +39 -0
- package/dist/render/inference/reuse.js +65 -0
- package/dist/render/inference/runner.d.ts +31 -0
- package/dist/render/inference/runner.js +175 -0
- package/dist/render/inference/schedule.d.ts +39 -0
- package/dist/render/inference/schedule.js +59 -0
- package/dist/render/inference/shapeKernels.d.ts +10 -0
- package/dist/render/inference/shapeKernels.js +80 -0
- package/dist/render/inference/spatialKernels.d.ts +21 -0
- package/dist/render/inference/spatialKernels.js +179 -0
- package/dist/render/inference/splatRaster.wgsl.d.ts +75 -0
- package/dist/render/inference/splatRaster.wgsl.js +337 -0
- package/dist/render/inference/splatTiles.d.ts +79 -0
- package/dist/render/inference/splatTiles.js +161 -0
- package/dist/render/lineBatch.d.ts +2 -5
- package/dist/render/lineBatch.js +4 -24
- package/dist/render/mesh.d.ts +4 -0
- package/dist/render/mesh.js +45 -1
- package/dist/render/pass.d.ts +145 -8
- package/dist/render/prefilterEnvMap.d.ts +22 -0
- package/dist/render/prefilterEnvMap.js +31 -0
- package/dist/render/readback.d.ts +30 -0
- package/dist/render/readback.js +47 -0
- package/dist/render/recon/clamp.d.ts +36 -0
- package/dist/render/recon/clamp.js +92 -0
- package/dist/render/recon/disocclusion.d.ts +60 -0
- package/dist/render/recon/disocclusion.js +109 -0
- package/dist/render/recon/frameGen.d.ts +81 -0
- package/dist/render/recon/frameGen.js +150 -0
- package/dist/render/recon/frameSizes.d.ts +34 -0
- package/dist/render/recon/frameSizes.js +58 -0
- package/dist/render/recon/jitter.d.ts +60 -0
- package/dist/render/recon/jitter.js +118 -0
- package/dist/render/recon/motionVectors.d.ts +55 -0
- package/dist/render/recon/motionVectors.js +107 -0
- package/dist/render/recon/reproject.d.ts +41 -0
- package/dist/render/recon/reproject.js +131 -0
- package/dist/render/recon/resolve.d.ts +90 -0
- package/dist/render/recon/resolve.js +313 -0
- package/dist/render/recon/tier.d.ts +24 -0
- package/dist/render/recon/tier.js +8 -0
- package/dist/render/reflectionProbe.d.ts +26 -0
- package/dist/render/reflectionProbe.js +33 -0
- package/dist/render/renderQuality.d.ts +69 -0
- package/dist/render/renderQuality.js +22 -0
- package/dist/render/screenSpaceReflection.d.ts +23 -0
- package/dist/render/screenSpaceReflection.js +22 -3
- package/dist/render/sdfFont.js +2 -0
- package/dist/render/segmentQuads.d.ts +13 -0
- package/dist/render/segmentQuads.js +32 -0
- package/dist/render/shaders/flat/lobes.d.ts +27 -0
- package/dist/render/shaders/flat/lobes.js +33 -3
- package/dist/render/shaders/flat/preamble.d.ts +7 -0
- package/dist/render/shaders/flat/preamble.js +24 -1
- package/dist/render/shaders/flat/probeGrid.d.ts +1 -1
- package/dist/render/shaders/flat/probeGrid.js +71 -2
- package/dist/render/shaders/generated/flat.wgsl.d.ts +48 -8
- package/dist/render/shaders/generated/flat.wgsl.js +64 -24
- package/dist/render/shaders/gi/composeField.wgsl.d.ts +56 -0
- package/dist/render/shaders/gi/composeField.wgsl.js +231 -0
- package/dist/render/shaders/gi/marchField.wgsl.d.ts +30 -0
- package/dist/render/shaders/gi/marchField.wgsl.js +141 -0
- package/dist/render/shaders/gi/probeBake.wgsl.d.ts +113 -0
- package/dist/render/shaders/gi/probeBake.wgsl.js +635 -0
- package/dist/render/shaders/gi/probeTrace.wgsl.d.ts +72 -0
- package/dist/render/shaders/gi/probeTrace.wgsl.js +261 -0
- package/dist/render/shaders/gi/probeVolume.wgsl.d.ts +49 -0
- package/dist/render/shaders/gi/probeVolume.wgsl.js +242 -0
- package/dist/render/shaders/gi/sampleField.wgsl.d.ts +32 -0
- package/dist/render/shaders/gi/sampleField.wgsl.js +134 -0
- package/dist/render/shaders/gpudriven/blendRaster.wgsl.d.ts +26 -0
- package/dist/render/shaders/gpudriven/blendRaster.wgsl.js +339 -0
- package/dist/render/shaders/gpudriven/compact.wgsl.d.ts +29 -0
- package/dist/render/shaders/gpudriven/compact.wgsl.js +58 -0
- package/dist/render/shaders/gpudriven/cull.wgsl.d.ts +90 -0
- package/dist/render/shaders/gpudriven/cull.wgsl.js +423 -0
- package/dist/render/shaders/gpudriven/decode.wgsl.d.ts +72 -0
- package/dist/render/shaders/gpudriven/decode.wgsl.js +349 -0
- package/dist/render/shaders/gpudriven/environment.wgsl.d.ts +44 -0
- package/dist/render/shaders/gpudriven/environment.wgsl.js +137 -0
- package/dist/render/shaders/gpudriven/fog.wgsl.d.ts +28 -0
- package/dist/render/shaders/gpudriven/fog.wgsl.js +107 -0
- package/dist/render/shaders/gpudriven/frameBlock.wgsl.d.ts +13 -0
- package/dist/render/shaders/gpudriven/frameBlock.wgsl.js +62 -0
- package/dist/render/shaders/gpudriven/materialBin.wgsl.d.ts +51 -0
- package/dist/render/shaders/gpudriven/materialBin.wgsl.js +112 -0
- package/dist/render/shaders/gpudriven/materialTable.wgsl.d.ts +25 -0
- package/dist/render/shaders/gpudriven/materialTable.wgsl.js +43 -0
- package/dist/render/shaders/gpudriven/meshTransform.wgsl.d.ts +18 -0
- package/dist/render/shaders/gpudriven/meshTransform.wgsl.js +37 -0
- package/dist/render/shaders/gpudriven/present.wgsl.d.ts +43 -0
- package/dist/render/shaders/gpudriven/present.wgsl.js +127 -0
- package/dist/render/shaders/gpudriven/shade.wgsl.d.ts +127 -0
- package/dist/render/shaders/gpudriven/shade.wgsl.js +874 -0
- package/dist/render/shaders/gpudriven/shadow.wgsl.d.ts +44 -0
- package/dist/render/shaders/gpudriven/shadow.wgsl.js +308 -0
- package/dist/render/shaders/gpudriven/visbufferRaster.wgsl.d.ts +28 -0
- package/dist/render/shaders/gpudriven/visbufferRaster.wgsl.js +245 -0
- package/dist/render/shaders/network.wgsl.d.ts +101 -0
- package/dist/render/shaders/network.wgsl.js +206 -0
- package/dist/render/shaders/recon/motion.wgsl.d.ts +44 -0
- package/dist/render/shaders/recon/motion.wgsl.js +96 -0
- package/dist/render/shaders/recon/resolve.wgsl.d.ts +51 -0
- package/dist/render/shaders/recon/resolve.wgsl.js +733 -0
- package/dist/render/temporalAa.js +8 -33
- package/dist/render/textLayout.js +12 -6
- package/dist/render/textRenderer.d.ts +3 -1
- package/dist/render/textRenderer.js +4 -1
- package/dist/render/translucentQueue.d.ts +6 -0
- package/dist/render/translucentQueue.js +11 -0
- package/dist/render/windStreakRenderer.d.ts +2 -1
- package/dist/render/windStreakRenderer.js +3 -1
- package/dist/world/cell.d.ts +63 -0
- package/dist/world/cell.js +181 -0
- package/dist/world/cellStream.d.ts +103 -0
- package/dist/world/cellStream.js +198 -0
- package/dist/world/freeze.d.ts +81 -0
- package/dist/world/freeze.js +114 -0
- package/dist/world/rebase.d.ts +29 -0
- package/dist/world/rebase.js +41 -0
- package/package.json +8 -3
- package/scripts/frames.mjs +92 -0
- package/scripts/gpuCompute.mjs +323 -0
- package/scripts/png.d.mts +16 -0
- package/scripts/png.mjs +25 -0
- package/src/core/loop.ts +19 -2
- package/src/core/storage.ts +5 -0
- package/src/geometry/meshBuilder.ts +5 -1
- package/src/index.ts +261 -4
- package/src/input/input.ts +6 -1
- package/src/math/exact.ts +69 -3
- package/src/math/frustum.ts +52 -3
- package/src/render/backend/api.ts +42 -1
- package/src/render/backend/budget.ts +7 -6
- package/src/render/backend/createRenderer.ts +92 -3
- package/src/render/backend/materialChanges.ts +54 -0
- package/src/render/backend/select.ts +8 -0
- package/src/render/backend/webgl2/renderer.ts +211 -9
- package/src/render/backend/webgpu/buffers.ts +182 -69
- package/src/render/backend/webgpu/fieldCompose.ts +482 -0
- package/src/render/backend/webgpu/giFieldPass.ts +200 -0
- package/src/render/backend/webgpu/gpuDrivenPass.ts +2993 -0
- package/src/render/backend/webgpu/linePass.ts +4 -39
- package/src/render/backend/webgpu/probeBake.ts +588 -0
- package/src/render/backend/webgpu/renderer.ts +1481 -76
- package/src/render/backend/webgpu/scatterPass.ts +15 -1
- package/src/render/backend/webgpu/surfaceTexturePass.ts +158 -88
- package/src/render/causticsRenderer.ts +5 -2
- package/src/render/depthConvention.ts +15 -0
- package/src/render/deviceFeatures.ts +77 -0
- package/src/render/fog.ts +185 -0
- package/src/render/frame/alias.ts +97 -0
- package/src/render/frame/deps.ts +155 -0
- package/src/render/frame/flushGraph.ts +135 -0
- package/src/render/frame/graphSchedule.ts +170 -0
- package/src/render/frame/index.ts +52 -0
- package/src/render/frame/lifetime.ts +61 -0
- package/src/render/frame/passTimings.ts +69 -0
- package/src/render/frame/validate.ts +40 -0
- package/src/render/frame/virtual.ts +91 -0
- package/src/render/gi/chain.ts +225 -0
- package/src/render/gi/denoise.ts +276 -0
- package/src/render/gi/fieldScene.ts +264 -0
- package/src/render/gi/globalField.ts +543 -0
- package/src/render/gi/probeGridFit.ts +89 -0
- package/src/render/gi/probeTrace.ts +143 -0
- package/src/render/gi/probeVolume.ts +288 -0
- package/src/render/gi/reflection.ts +163 -0
- package/src/render/gi/traceField.ts +223 -0
- package/src/render/gi/traceScreen.ts +106 -0
- package/src/render/gpuScene.ts +80 -0
- package/src/render/gpudriven/alphaTest.ts +33 -0
- package/src/render/gpudriven/clusterUpload.ts +123 -0
- package/src/render/gpudriven/clusterWorld.ts +239 -0
- package/src/render/gpudriven/compact.ts +80 -0
- package/src/render/gpudriven/cullClusters.ts +333 -0
- package/src/render/gpudriven/decodeTables.ts +332 -0
- package/src/render/gpudriven/frustum.ts +70 -0
- package/src/render/gpudriven/geometryHarness.ts +51 -0
- package/src/render/gpudriven/hzb.ts +113 -0
- package/src/render/gpudriven/ibl.ts +147 -0
- package/src/render/gpudriven/indirect.ts +89 -0
- package/src/render/gpudriven/instances.ts +234 -0
- package/src/render/gpudriven/lit.ts +329 -0
- package/src/render/gpudriven/lodCut.ts +62 -0
- package/src/render/gpudriven/materialBin.ts +131 -0
- package/src/render/gpudriven/materialTable.ts +164 -0
- package/src/render/gpudriven/pipeline.ts +336 -0
- package/src/render/gpudriven/rangeAlloc.ts +122 -0
- package/src/render/gpudriven/sceneUpload.ts +173 -0
- package/src/render/gpudriven/shadeBins.ts +222 -0
- package/src/render/gpudriven/shadow.ts +285 -0
- package/src/render/gpudriven/shadowCamera.ts +406 -0
- package/src/render/gpudriven/streamScene.ts +575 -0
- package/src/render/gpudriven/surfaceFrame.ts +68 -0
- package/src/render/gpudriven/twoPhase.ts +214 -0
- package/src/render/gpudriven/visbuffer.ts +41 -0
- package/src/render/halfFloat.ts +22 -4
- package/src/render/inference/attentionTiled.ts +132 -0
- package/src/render/inference/denseKernels.ts +369 -0
- package/src/render/inference/device.ts +44 -0
- package/src/render/inference/deviceGraph.ts +39 -0
- package/src/render/inference/fuse.ts +47 -0
- package/src/render/inference/kernelKit.ts +120 -0
- package/src/render/inference/kernels.ts +18 -0
- package/src/render/inference/plan.ts +26 -0
- package/src/render/inference/reuse.ts +79 -0
- package/src/render/inference/runner.ts +204 -0
- package/src/render/inference/schedule.ts +80 -0
- package/src/render/inference/shapeKernels.ts +112 -0
- package/src/render/inference/spatialKernels.ts +220 -0
- package/src/render/inference/splatRaster.wgsl.ts +348 -0
- package/src/render/inference/splatTiles.ts +186 -0
- package/src/render/lineBatch.ts +4 -24
- package/src/render/mesh.ts +46 -1
- package/src/render/pass.ts +155 -10
- package/src/render/prefilterEnvMap.ts +33 -0
- package/src/render/readback.ts +73 -0
- package/src/render/recon/clamp.ts +113 -0
- package/src/render/recon/disocclusion.ts +143 -0
- package/src/render/recon/frameGen.ts +191 -0
- package/src/render/recon/frameSizes.ts +64 -0
- package/src/render/recon/jitter.ts +126 -0
- package/src/render/recon/motionVectors.ts +138 -0
- package/src/render/recon/reproject.ts +179 -0
- package/src/render/recon/resolve.ts +444 -0
- package/src/render/recon/tier.ts +31 -0
- package/src/render/reflectionProbe.ts +34 -0
- package/src/render/renderQuality.ts +87 -0
- package/src/render/screenSpaceReflection.ts +37 -3
- package/src/render/sdfFont.ts +2 -0
- package/src/render/segmentQuads.ts +39 -0
- package/src/render/shaders/flat/lobes.ts +35 -3
- package/src/render/shaders/flat/preamble.ts +25 -1
- package/src/render/shaders/flat/probeGrid.ts +77 -2
- package/src/render/shaders/generated/flat.wgsl.ts +64 -24
- package/src/render/shaders/gi/composeField.wgsl.ts +239 -0
- package/src/render/shaders/gi/marchField.wgsl.ts +146 -0
- package/src/render/shaders/gi/probeBake.wgsl.ts +649 -0
- package/src/render/shaders/gi/probeTrace.wgsl.ts +269 -0
- package/src/render/shaders/gi/probeVolume.wgsl.ts +246 -0
- package/src/render/shaders/gi/sampleField.wgsl.ts +140 -0
- package/src/render/shaders/gpudriven/blendRaster.wgsl.ts +343 -0
- package/src/render/shaders/gpudriven/compact.wgsl.ts +59 -0
- package/src/render/shaders/gpudriven/cull.wgsl.ts +433 -0
- package/src/render/shaders/gpudriven/decode.wgsl.ts +373 -0
- package/src/render/shaders/gpudriven/environment.wgsl.ts +141 -0
- package/src/render/shaders/gpudriven/fog.wgsl.ts +110 -0
- package/src/render/shaders/gpudriven/frameBlock.wgsl.ts +62 -0
- package/src/render/shaders/gpudriven/materialBin.wgsl.ts +116 -0
- package/src/render/shaders/gpudriven/materialTable.wgsl.ts +46 -0
- package/src/render/shaders/gpudriven/meshTransform.wgsl.ts +37 -0
- package/src/render/shaders/gpudriven/present.wgsl.ts +129 -0
- package/src/render/shaders/gpudriven/shade.wgsl.ts +884 -0
- package/src/render/shaders/gpudriven/shadow.wgsl.ts +322 -0
- package/src/render/shaders/gpudriven/visbufferRaster.wgsl.ts +251 -0
- package/src/render/shaders/network.wgsl.ts +238 -0
- package/src/render/shaders/recon/motion.wgsl.ts +101 -0
- package/src/render/shaders/recon/resolve.wgsl.ts +741 -0
- package/src/render/temporalAa.ts +9 -33
- package/src/render/textLayout.ts +12 -6
- package/src/render/textRenderer.ts +5 -2
- package/src/render/translucentQueue.ts +11 -0
- package/src/render/windStreakRenderer.ts +4 -2
- package/src/world/cell.ts +231 -0
- package/src/world/cellStream.ts +278 -0
- package/src/world/freeze.ts +157 -0
- 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
|
+
}
|