@driftengine/core 3.63.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.json +111 -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 +69 -4
- package/dist/index.js +112 -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 +19 -9
- package/dist/render/backend/webgpu/surfaceTexturePass.js +58 -32
- 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 +246 -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 +63 -36
- 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
|
@@ -8,7 +8,29 @@ import {
|
|
|
8
8
|
} from '../../temporalAa.ts';
|
|
9
9
|
import { OIT_MULTISAMPLE_REFUSAL } from '../../orderIndependent.ts';
|
|
10
10
|
import { TranslucentQueue } from '../../translucentQueue.ts';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
DEPTH_CLEAR,
|
|
13
|
+
DEPTH_COMPARE_EQUAL,
|
|
14
|
+
MAX_DEPTH_LAYER,
|
|
15
|
+
REVERSED_DEPTH,
|
|
16
|
+
} from '../../depthConvention.ts';
|
|
17
|
+
import { RECONSTRUCTION_MULTISAMPLE_REFUSAL, reconRenderSize } from '../../recon/frameSizes.ts';
|
|
18
|
+
import { jitterOffset as jitterOffsetFor, reconJitterPhases } from '../../recon/jitter.ts';
|
|
19
|
+
import {
|
|
20
|
+
RECON_HISTORY_FORMAT,
|
|
21
|
+
RECON_PARAM_FLOATS,
|
|
22
|
+
RECON_WORKGROUP,
|
|
23
|
+
reconResolveWgsl,
|
|
24
|
+
} from '../../shaders/recon/resolve.wgsl.ts';
|
|
25
|
+
import {
|
|
26
|
+
MOTION_DRAW_FLOATS,
|
|
27
|
+
MOTION_DRAW_STRIDE,
|
|
28
|
+
MOTION_FRAME_FLOATS,
|
|
29
|
+
MOTION_TARGET_FORMAT,
|
|
30
|
+
RECON_MOTION_WGSL,
|
|
31
|
+
} from '../../shaders/recon/motion.wgsl.ts';
|
|
32
|
+
import { DEFAULT_DISOCCLUSION } from '../../recon/disocclusion.ts';
|
|
33
|
+
import { DEFAULT_RECON_QUALITY } from '../../recon/resolve.ts';
|
|
12
34
|
import type { ReadonlyMat4 } from 'gl-matrix';
|
|
13
35
|
|
|
14
36
|
import type { Vec3 } from '../../../math/color.ts';
|
|
@@ -58,11 +80,18 @@ import {
|
|
|
58
80
|
import type { WindStreakOptions } from '../../windStreakRenderer.ts';
|
|
59
81
|
import type { WindField } from '../../windField.ts';
|
|
60
82
|
import type { FlockParams } from '../../flockRenderer.ts';
|
|
61
|
-
import { expandBoltSegments } from '../../segmentQuads.ts';
|
|
83
|
+
import { expandBoltSegments, expandLineSegments } from '../../segmentQuads.ts';
|
|
62
84
|
import { LIVE_POINT_SHADOW_MAPS, MAX_POINT_LIGHTS, POINT_SHADOW_POOL } from '../../lightBudget.ts';
|
|
63
85
|
import { roughnessForLevel } from '../../prefilterEnvMap.ts';
|
|
64
86
|
import { equirectToCubeFaces } from '../../equirectToCube.ts';
|
|
65
|
-
import {
|
|
87
|
+
import {
|
|
88
|
+
MAX_ENV_PROBES,
|
|
89
|
+
ProbeGrid,
|
|
90
|
+
SINGLE_PROBE,
|
|
91
|
+
UNIT_STEP,
|
|
92
|
+
WORLD_ORIGIN,
|
|
93
|
+
sameGrid,
|
|
94
|
+
} from '../../probeGrid.ts';
|
|
66
95
|
import type { ProbeGridOptions } from '../../probeGrid.ts';
|
|
67
96
|
import { ggxMaxLevelFor, irradianceLevelFor, octahedralEdgeFor } from '../../prefilterEnvMap.ts';
|
|
68
97
|
import { environmentTexels } from './environmentTexels.ts';
|
|
@@ -413,7 +442,6 @@ import {
|
|
|
413
442
|
createGpuLines,
|
|
414
443
|
createLineBindGroup,
|
|
415
444
|
createLineBindGroupLayout,
|
|
416
|
-
expandLineSegments,
|
|
417
445
|
linePipeline,
|
|
418
446
|
type GpuLines,
|
|
419
447
|
} from './linePass.ts';
|
|
@@ -428,8 +456,29 @@ import {
|
|
|
428
456
|
} from './lightVolumePass.ts';
|
|
429
457
|
import { aimReflection, createGpuReflection, type GpuReflection } from './reflectionPass.ts';
|
|
430
458
|
import { FrameBudget } from '../budget.ts';
|
|
459
|
+
import { MaterialChanges, ownsMaterial } from '../materialChanges.ts';
|
|
431
460
|
import { DYNAMIC_ALIGNMENT as DYNAMIC_UNIFORM_ALIGNMENT, UniformRing } from './uniformRing.ts';
|
|
432
461
|
import { DecalQueue, MAX_DRAWN_DECALS } from '../../decalQueue.ts';
|
|
462
|
+
import { DistanceFieldScene } from '../../gi/fieldScene.ts';
|
|
463
|
+
import { DEFAULT_FIELD_COMPOSE, FieldComposer } from './fieldCompose.ts';
|
|
464
|
+
import { ProbeBaker } from './probeBake.ts';
|
|
465
|
+
import { distanceFieldBounds } from '../../gi/fieldScene.ts';
|
|
466
|
+
import { fitProbeGrid } from '../../gi/probeGridFit.ts';
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Metres between probes when the renderer fits a grid itself.
|
|
470
|
+
*
|
|
471
|
+
* **Two, which is a room rather than a building.** The spacing is what decides how local the light
|
|
472
|
+
* can be: a bounce off a wall reaches a surface two metres away and no nearer detail than that.
|
|
473
|
+
* `fitProbeGrid` widens it uniformly when the budget bites, so a large scene gets a coarser grid
|
|
474
|
+
* rather than a truncated one.
|
|
475
|
+
*/
|
|
476
|
+
const INDIRECT_PROBE_SPACING = 2;
|
|
477
|
+
|
|
478
|
+
/** What the trace is given when a frame never said what was lighting it. */
|
|
479
|
+
const NO_SUN: readonly number[] = [0, 0, 0];
|
|
480
|
+
import type { ComposedField } from './fieldCompose.ts';
|
|
481
|
+
import type { FieldSource } from '../../gi/globalField.ts';
|
|
433
482
|
import {
|
|
434
483
|
CLIP_Y_FLIP,
|
|
435
484
|
MAX_REFLECTIVE_SURFACES,
|
|
@@ -444,6 +493,7 @@ import type { CommandPool, DrawCommand } from './drawCommand.ts';
|
|
|
444
493
|
import {
|
|
445
494
|
RESOURCE_COUNT,
|
|
446
495
|
createArena,
|
|
496
|
+
createFlushSchedule,
|
|
447
497
|
keptNodes,
|
|
448
498
|
maskOf,
|
|
449
499
|
nodeCount,
|
|
@@ -452,6 +502,7 @@ import {
|
|
|
452
502
|
recordNode,
|
|
453
503
|
resetArena,
|
|
454
504
|
schedule,
|
|
505
|
+
scheduleFlush,
|
|
455
506
|
scratchFor,
|
|
456
507
|
} from '../../frame/index.ts';
|
|
457
508
|
import { createFrustum, frustumFromViewProjection } from '../../../math/frustum.ts';
|
|
@@ -475,11 +526,12 @@ import type {
|
|
|
475
526
|
PassContext,
|
|
476
527
|
PassDefinition,
|
|
477
528
|
PassDevice,
|
|
529
|
+
PassEnvironment,
|
|
478
530
|
PassHandle,
|
|
479
531
|
PassRegistry,
|
|
480
532
|
PrepareContext,
|
|
481
533
|
} from '../../pass.ts';
|
|
482
|
-
import type { Arena, ScheduledPass } from '../../frame/index.ts';
|
|
534
|
+
import type { Arena, FlushSchedule, ScheduledPass } from '../../frame/index.ts';
|
|
483
535
|
import { GpuInstancedBatch } from './instanced.ts';
|
|
484
536
|
import type { MeshInstances } from '../../instances.ts';
|
|
485
537
|
|
|
@@ -799,20 +851,32 @@ function resolvedStoreOp(multisampled: boolean, terminal: boolean, allowed: bool
|
|
|
799
851
|
}
|
|
800
852
|
|
|
801
853
|
/**
|
|
802
|
-
* Clip space differs between the two APIs
|
|
854
|
+
* Clip space differs between the two APIs in **one** way, and the other half of `CLIP_CORRECTION`
|
|
855
|
+
* cancels something this repository writes itself.
|
|
856
|
+
*
|
|
857
|
+
* **Depth lands in a different range, and that half is a real difference between the APIs.**
|
|
858
|
+
* OpenGL clips z to [-1, 1] and WebGPU to [0, 1], so an uncorrected matrix throws away the near
|
|
859
|
+
* half of the depth buffer and compares what remains against the wrong distances.
|
|
803
860
|
*
|
|
804
|
-
* **Y
|
|
805
|
-
*
|
|
806
|
-
*
|
|
861
|
+
* **The Y negation cancels one the generator writes, and this comment used to say otherwise.**
|
|
862
|
+
* It claimed the flip was the framebuffer origin: WebGPU's is the top-left and OpenGL's is the
|
|
863
|
+
* bottom-left, so a projection built for WebGL2 "draws the world upside down here". That is not
|
|
864
|
+
* what either viewport transform does — both put clip `y = +1` at the top of the image, because
|
|
865
|
+
* OpenGL measures its window upward from the bottom-left and WebGPU measures its framebuffer
|
|
866
|
+
* downward from the top-left. An uncorrected projection lands the same way up on both.
|
|
807
867
|
*
|
|
808
|
-
*
|
|
809
|
-
*
|
|
810
|
-
*
|
|
868
|
+
* What does flip it is `naga`. Every module in `shaders/generated/` ends its vertex entry point
|
|
869
|
+
* with `gl_Position.y = -gl_Position.y`, which is how that translator adapts a GLSL shader to
|
|
870
|
+
* WebGPU — and the first frame this backend rendered, a colonnade hanging from the ceiling, was
|
|
871
|
+
* *that* line and not the API. So the pair is: the generator negates, this negates back, and the
|
|
872
|
+
* net effect on a generated shader is the depth remap alone. `shaders/generated.test.ts` pins the
|
|
873
|
+
* generator's half so the two cannot drift apart.
|
|
811
874
|
*
|
|
812
|
-
*
|
|
813
|
-
*
|
|
814
|
-
*
|
|
815
|
-
*
|
|
875
|
+
* **Which makes it the wrong matrix for a shader somebody wrote by hand**, since there is no
|
|
876
|
+
* negation to cancel — see `DEPTH_CORRECTION` below, and `PassDevice` for the seam where a
|
|
877
|
+
* contributed pass chooses. Wiring the GPU-driven pipeline's hand-written raster found it: the
|
|
878
|
+
* picture was an exact vertical mirror of the forward path's, and `frontFace: 'cw'` made it draw
|
|
879
|
+
* the right faces of a mirrored world.
|
|
816
880
|
*
|
|
817
881
|
* **This one is for a target that gets presented. `SHADOW_CLIP_CORRECTION` is for one that
|
|
818
882
|
* gets sampled, and they are not the same transform.**
|
|
@@ -879,6 +943,37 @@ const CLIP_CORRECTION = new Float32Array([
|
|
|
879
943
|
* against, applied by accident. Without the flip it culls what it says it culls, and matches
|
|
880
944
|
* what `renderer.ts` does through the directional pass.
|
|
881
945
|
*/
|
|
946
|
+
/**
|
|
947
|
+
* `CLIP_CORRECTION` without the Y negation: the depth remap alone, reversed.
|
|
948
|
+
*
|
|
949
|
+
* **What a hand-written WGSL shader wants.** The negation in `CLIP_CORRECTION` is there to cancel
|
|
950
|
+
* the one `naga` writes into every generated vertex entry point; a shader that was not generated
|
|
951
|
+
* carries no such line, so taking that matrix mirrors its picture vertically — and mirrors its
|
|
952
|
+
* triangle winding with it, which is how it reads as a culling problem rather than as a flip.
|
|
953
|
+
*
|
|
954
|
+
* The depth half is not optional and is the reason this is a matrix rather than nothing: OpenGL
|
|
955
|
+
* clips z to [-1, 1] and WebGPU to [0, 1], and the row is `0.5 - 0.5z` rather than `0.5z + 0.5`
|
|
956
|
+
* because `depthConvention.ts` puts the near plane at one.
|
|
957
|
+
*/
|
|
958
|
+
const DEPTH_CORRECTION = new Float32Array([
|
|
959
|
+
1,
|
|
960
|
+
0,
|
|
961
|
+
0,
|
|
962
|
+
0,
|
|
963
|
+
0,
|
|
964
|
+
1,
|
|
965
|
+
0,
|
|
966
|
+
0,
|
|
967
|
+
0,
|
|
968
|
+
0,
|
|
969
|
+
REVERSED_DEPTH ? -0.5 : 0.5,
|
|
970
|
+
0,
|
|
971
|
+
0,
|
|
972
|
+
0,
|
|
973
|
+
0.5,
|
|
974
|
+
1,
|
|
975
|
+
]);
|
|
976
|
+
|
|
882
977
|
const SHADOW_CLIP_CORRECTION = new Float32Array([
|
|
883
978
|
1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 1,
|
|
884
979
|
]);
|
|
@@ -1158,6 +1253,17 @@ const NO_CLIP_PLANE = new Float32Array(4);
|
|
|
1158
1253
|
* a while telling consumers that working effects did nothing. All three are read by
|
|
1159
1254
|
* `composite()` now, and each has been compared against WebGL2 on `demo/dev/probe.html`.
|
|
1160
1255
|
*/
|
|
1256
|
+
/**
|
|
1257
|
+
* The y negation every generated vertex stage ends with, as a matrix.
|
|
1258
|
+
*
|
|
1259
|
+
* **A hand-written shader has no such line, so it has to carry it.** `CLIP_CORRECTION` negates y
|
|
1260
|
+
* precisely to cancel the one `naga` writes, and the pair's net effect on a generated shader is the
|
|
1261
|
+
* depth remap alone — §3 rows 56 and 57. The motion pass has to put its vertices exactly where the
|
|
1262
|
+
* scene put them or its depth test admits nothing, so the renderer multiplies this through the
|
|
1263
|
+
* matrix the scene drew with and hands over the product.
|
|
1264
|
+
*/
|
|
1265
|
+
const MOTION_CLIP_FLIP = mat4.fromValues(1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
|
|
1266
|
+
|
|
1161
1267
|
export class WebGPURenderer implements RendererApi {
|
|
1162
1268
|
/**
|
|
1163
1269
|
* Real timestamp queries, where the adapter offers them.
|
|
@@ -1315,8 +1421,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
1315
1421
|
private readonly perFrameStaging: ArrayBuffer;
|
|
1316
1422
|
private readonly perFrameFloats: Float32Array;
|
|
1317
1423
|
private readonly perFrameInts: Int32Array;
|
|
1318
|
-
/** The slot the open material occupies,
|
|
1319
|
-
private
|
|
1424
|
+
/** The slot the open material occupies, and whether the next draw must take a new one. */
|
|
1425
|
+
private readonly materials = new MaterialChanges();
|
|
1320
1426
|
|
|
1321
1427
|
/*
|
|
1322
1428
|
* The frame graph's recording, and the machinery that replays it.
|
|
@@ -1369,6 +1475,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
1369
1475
|
private readonly textBudget = this.budget.line('text draws', MAX_OVERLAYS);
|
|
1370
1476
|
private readonly sdfTextBudget = this.budget.line('sdf text draws', MAX_OVERLAYS);
|
|
1371
1477
|
private readonly lineBudget = this.budget.line('line draws', MAX_OVERLAYS);
|
|
1478
|
+
private readonly panelBudget = this.budget.line('panels', MAX_OVERLAYS);
|
|
1372
1479
|
|
|
1373
1480
|
/**
|
|
1374
1481
|
* What the frame just drawn asked for, and what it was refused.
|
|
@@ -1391,6 +1498,12 @@ export class WebGPURenderer implements RendererApi {
|
|
|
1391
1498
|
* scatter, plumes and text reached it long before the draw ring was full.
|
|
1392
1499
|
*/
|
|
1393
1500
|
private replayScratch: Int32Array = new Int32Array(MAX_DRAWS_PER_FRAME);
|
|
1501
|
+
/**
|
|
1502
|
+
* The identifier graph's working set, where `quality.identifierGraph` asks for it and not
|
|
1503
|
+
* otherwise: a renderer scheduling by masks carries none of it. Sized with the arena, and grown
|
|
1504
|
+
* by `scheduleFlush` the way the arena grows.
|
|
1505
|
+
*/
|
|
1506
|
+
private readonly flushSchedule: FlushSchedule | null;
|
|
1394
1507
|
private readonly scheduledPasses: ScheduledPass[] = Array.from({ length: 64 }, () => ({
|
|
1395
1508
|
writes: 0,
|
|
1396
1509
|
first: 0,
|
|
@@ -1663,6 +1776,26 @@ export class WebGPURenderer implements RendererApi {
|
|
|
1663
1776
|
private bloomGroups: GPUBindGroup[] = [];
|
|
1664
1777
|
private frameWidth = 1;
|
|
1665
1778
|
private frameHeight = 1;
|
|
1779
|
+
/**
|
|
1780
|
+
* What the scene is drawn at, which is the drawing buffer unless a reconstruction is enlarging it.
|
|
1781
|
+
*
|
|
1782
|
+
* **Everything before the resolve is this size and everything after it is the drawing buffer's.**
|
|
1783
|
+
* The scene colour and its multisampled twin, the depth, the refraction snapshot, the resolved
|
|
1784
|
+
* depth, the occlusion pair, the medium, the translucent buffers, the reflection march, the
|
|
1785
|
+
* decals, the temporal history and the frame pass's own viewport are all before it; the composite
|
|
1786
|
+
* and the overlay are after it, and they are what the viewer's pixels are counted in.
|
|
1787
|
+
*
|
|
1788
|
+
* **The composite is what enlarges the picture, and it does so by existing.** It draws a
|
|
1789
|
+
* full-screen triangle into the swap chain sampling the scene by `uv` through a linear sampler, so
|
|
1790
|
+
* a smaller scene target is magnified by that sample and nothing else changes — which is why this
|
|
1791
|
+
* needs `screenEffects` and is refused without it: with no composite the world draws straight into
|
|
1792
|
+
* the swap chain and there is nothing to enlarge from.
|
|
1793
|
+
*
|
|
1794
|
+
* Equal to the drawing buffer whenever `quality.reconstruction` is 0, which is the default, and
|
|
1795
|
+
* `recon/frameSizes.ts` is the one place the two are related.
|
|
1796
|
+
*/
|
|
1797
|
+
private renderWidth = 0;
|
|
1798
|
+
private renderHeight = 0;
|
|
1666
1799
|
/** The view-projection this frame was drawn with, for the reprojection motion blur needs. */
|
|
1667
1800
|
private readonly previousViewProj = new Float32Array(16);
|
|
1668
1801
|
private readonly reprojection = new Float32Array(16);
|
|
@@ -1758,6 +1891,94 @@ export class WebGPURenderer implements RendererApi {
|
|
|
1758
1891
|
private temporalJitterX = 0;
|
|
1759
1892
|
private temporalJitterY = 0;
|
|
1760
1893
|
private temporalJittering = false;
|
|
1894
|
+
/**
|
|
1895
|
+
* Whether this frame is being reconstructed, settled once in `bindMeshPass`.
|
|
1896
|
+
*
|
|
1897
|
+
* It is not `quality.reconstruction > 0` on its own: a mirror pass is skipped for the same reason
|
|
1898
|
+
* the temporal jitter skips one — a reflection is not the picture the history holds — and without
|
|
1899
|
+
* a composite there is nothing to enlarge the render from.
|
|
1900
|
+
*/
|
|
1901
|
+
private reconstructing = false;
|
|
1902
|
+
/** Said once rather than every frame, as the translucent set's own refusal is. */
|
|
1903
|
+
private reconMultisampleSaid = false;
|
|
1904
|
+
private reconFrameIndex = -1;
|
|
1905
|
+
private readonly reconJitter = new Float32Array(2);
|
|
1906
|
+
private readonly reconPreviousJitter = new Float32Array(2);
|
|
1907
|
+
/**
|
|
1908
|
+
* `reconJitter` as a fraction of the clip square, which is what a contributed pass is handed.
|
|
1909
|
+
* See `PrepareContext.jitter`; zero on a frame that is not reconstructed.
|
|
1910
|
+
*/
|
|
1911
|
+
private readonly passJitter = new Float32Array(2);
|
|
1912
|
+
/**
|
|
1913
|
+
* The reconstruction's own targets, allocated only while a consumer asks for one.
|
|
1914
|
+
*
|
|
1915
|
+
* **Two histories rather than one**, for the reason the temporal resolve keeps two: a dispatch
|
|
1916
|
+
* cannot read the texture it is writing. The shown picture is a third, because the history holds
|
|
1917
|
+
* the *unsharpened* result — sharpening into the history would sharpen a sharpened picture every
|
|
1918
|
+
* frame, which is the halo the reference's own clamp exists to prevent, compounded.
|
|
1919
|
+
*
|
|
1920
|
+
* The motion target is render size and carries a drawn object's own motion where something wrote
|
|
1921
|
+
* one. **Nothing writes it yet**, so it is cleared every frame and every pixel takes the camera's
|
|
1922
|
+
* motion, which the resolve derives from the depth and the reprojection. Static geometry is
|
|
1923
|
+
* exact that way and a moving object ghosts until the motion pass lands.
|
|
1924
|
+
*/
|
|
1925
|
+
private reconHistories: (GPUTexture | null)[] = [null, null];
|
|
1926
|
+
private reconHistoryViews: (GPUTextureView | null)[] = [null, null];
|
|
1927
|
+
private reconWrite = 0;
|
|
1928
|
+
private reconHasHistory = false;
|
|
1929
|
+
private reconShown: GPUTexture | null = null;
|
|
1930
|
+
private reconShownView: GPUTextureView | null = null;
|
|
1931
|
+
private reconMotion: GPUTexture | null = null;
|
|
1932
|
+
private reconMotionView: GPUTextureView | null = null;
|
|
1933
|
+
private reconPreviousDepth: GPUTexture | null = null;
|
|
1934
|
+
private reconPreviousDepthView: GPUTextureView | null = null;
|
|
1935
|
+
private reconParams: GPUBuffer | null = null;
|
|
1936
|
+
private readonly reconStaging = new ArrayBuffer(RECON_PARAM_FLOATS * 4);
|
|
1937
|
+
private readonly reconFloats = new Float32Array(this.reconStaging);
|
|
1938
|
+
private readonly reconInts = new Uint32Array(this.reconStaging);
|
|
1939
|
+
private reconLayout: GPUBindGroupLayout | null = null;
|
|
1940
|
+
private reconResolvePipeline: GPUComputePipeline | null = null;
|
|
1941
|
+
private reconSharpenPipeline: GPUComputePipeline | null = null;
|
|
1942
|
+
private reconGroups: (GPUBindGroup | null)[] = [null, null];
|
|
1943
|
+
private reconSharpenGroups: (GPUBindGroup | null)[] = [null, null];
|
|
1944
|
+
private reconRushGroup: GPUBindGroup | null = null;
|
|
1945
|
+
/**
|
|
1946
|
+
* What the motion pass will draw, recorded by `drawMesh` and spent once a frame.
|
|
1947
|
+
*
|
|
1948
|
+
* A parallel array of meshes and one flat matrix buffer rather than an array of objects, because
|
|
1949
|
+
* this is written on the frame's hot path and the house rule about allocating there binds it: the
|
|
1950
|
+
* matrices are copied into a buffer that only ever grows, and the count is what resets.
|
|
1951
|
+
*/
|
|
1952
|
+
private readonly motionMeshes: GpuMesh[] = [];
|
|
1953
|
+
private motionMatrices = new Float32Array(0);
|
|
1954
|
+
private motionCount = 0;
|
|
1955
|
+
private motionLayout: GPUBindGroupLayout | null = null;
|
|
1956
|
+
/**
|
|
1957
|
+
* One pipeline per vertex stride, because a stride is a property of the *mesh*.
|
|
1958
|
+
*
|
|
1959
|
+
* Which optional attributes a mesh supplied decides how far apart its positions are, and this
|
|
1960
|
+
* pass reads nothing but positions — so two meshes drawn by the same shader still need two
|
|
1961
|
+
* vertex layouts. A map rather than a cache key on the pipeline cache: there is one shader here
|
|
1962
|
+
* and a handful of strides in any scene.
|
|
1963
|
+
*/
|
|
1964
|
+
private readonly motionPipelines = new Map<number, GPURenderPipeline>();
|
|
1965
|
+
private motionModule: GPUShaderModule | null = null;
|
|
1966
|
+
private motionPipelineLayout: GPUPipelineLayout | null = null;
|
|
1967
|
+
private motionFrameBuffer: GPUBuffer | null = null;
|
|
1968
|
+
private motionDrawBuffer: GPUBuffer | null = null;
|
|
1969
|
+
private motionDrawCapacity = 0;
|
|
1970
|
+
private motionGroup: GPUBindGroup | null = null;
|
|
1971
|
+
private readonly motionFrameStaging = new Float32Array(MOTION_FRAME_FLOATS);
|
|
1972
|
+
private motionDrawStaging = new Uint8Array(0);
|
|
1973
|
+
/** The scene's own clip transform with the generated shaders' y negation folded in. */
|
|
1974
|
+
private readonly motionRaster = mat4.create();
|
|
1975
|
+
private reconSizeKey = '';
|
|
1976
|
+
/** Last frame's raw view-projection and its inverse, and the two eyes. */
|
|
1977
|
+
private readonly reconPreviousRawViewProj = mat4.create();
|
|
1978
|
+
private readonly reconInverseViewProj = mat4.create();
|
|
1979
|
+
private readonly reconPreviousInverseViewProj = mat4.create();
|
|
1980
|
+
private readonly reconEye = new Float32Array(3);
|
|
1981
|
+
private readonly reconPreviousEye = new Float32Array(3);
|
|
1761
1982
|
private temporalHistoryUsable = false;
|
|
1762
1983
|
|
|
1763
1984
|
private readonly aoStaging = new ArrayBuffer(AO_FRAG_SIZE);
|
|
@@ -1799,6 +2020,23 @@ export class WebGPURenderer implements RendererApi {
|
|
|
1799
2020
|
return this.quality.screenEffects;
|
|
1800
2021
|
}
|
|
1801
2022
|
|
|
2023
|
+
/**
|
|
2024
|
+
* Whether this renderer is set up to reconstruct, asked in one place by everything that cares.
|
|
2025
|
+
*
|
|
2026
|
+
* **One condition and not three**, which §3 row 132 is the lesson of: the frame's flag, the size
|
|
2027
|
+
* the targets are built at and the size the world is drawn at all turn on the same question, and
|
|
2028
|
+
* three copies of it are three lines no test can fail — each covering the others, so breaking any
|
|
2029
|
+
* one of them changes nothing anybody can see.
|
|
2030
|
+
*
|
|
2031
|
+
* A composite, because the composite is what enlarges the render; one sample, because the motion
|
|
2032
|
+
* pass draws against the depth the scene left and every attachment in a pass must agree about its
|
|
2033
|
+
* sample count — `RECONSTRUCTION_MULTISAMPLE_REFUSAL` says the rest. Whether a *frame* is
|
|
2034
|
+
* reconstructed adds one more question, the mirror's, and that one belongs to the frame.
|
|
2035
|
+
*/
|
|
2036
|
+
private get reconstructionWanted(): boolean {
|
|
2037
|
+
return this.quality.reconstruction > 0 && this.hasComposite && this.samples === 1;
|
|
2038
|
+
}
|
|
2039
|
+
|
|
1802
2040
|
/** What a frame pass writes into: the scene target where there is one, the swap chain else. */
|
|
1803
2041
|
/**
|
|
1804
2042
|
* This frame's swap-chain view, taken on first use rather than at `beginFrame`.
|
|
@@ -1902,6 +2140,31 @@ export class WebGPURenderer implements RendererApi {
|
|
|
1902
2140
|
* the frame has drawn, so it cannot run until the opaque world is finished. See `DecalQueue`.
|
|
1903
2141
|
*/
|
|
1904
2142
|
private readonly decalQueue = new DecalQueue();
|
|
2143
|
+
/** The fields this frame declared, composed at `endFrame`. See `addDistanceField`. */
|
|
2144
|
+
private readonly distanceFields = new DistanceFieldScene();
|
|
2145
|
+
/**
|
|
2146
|
+
* The composition of them, built at the first frame that has both the flag and a field.
|
|
2147
|
+
*
|
|
2148
|
+
* **Not at construction**, because it is 1.4 MB of device buffers and a compute pipeline, and a
|
|
2149
|
+
* profile with `indirectLight` on is entitled to declare no fields at all — a scene that lights
|
|
2150
|
+
* itself entirely from a baked probe grid, for one. Nothing is allocated until something would
|
|
2151
|
+
* be composed.
|
|
2152
|
+
*/
|
|
2153
|
+
private fieldComposer: FieldComposer | null = null;
|
|
2154
|
+
/**
|
|
2155
|
+
* The probes' own bake, built at the first frame that has the flag, a field and an array.
|
|
2156
|
+
*
|
|
2157
|
+
* Not at construction, for `fieldComposer`'s reason: it is two compute pipelines, a render
|
|
2158
|
+
* pipeline and a handful of buffers, and a profile with the flag on may never declare a field.
|
|
2159
|
+
*/
|
|
2160
|
+
private probeBaker: ProbeBaker | null = null;
|
|
2161
|
+
/** Which array the baker's views were built against, so a replacement is noticed. */
|
|
2162
|
+
private bakerArray: GPUTexture | null = null;
|
|
2163
|
+
/** Reused, because deriving a grid from the declared fields runs in the frame path. */
|
|
2164
|
+
private readonly fieldBoundsMin = new Float32Array(3);
|
|
2165
|
+
private readonly fieldBoundsMax = new Float32Array(3);
|
|
2166
|
+
/** Said once rather than every frame, as the other refusals are. */
|
|
2167
|
+
private indirectGridRefused = false;
|
|
1905
2168
|
private decalLayout!: GPUBindGroupLayout;
|
|
1906
2169
|
private decalUniforms!: GPUBuffer;
|
|
1907
2170
|
private readonly decalStaging = new ArrayBuffer(DECAL_SLOT * MAX_DRAWN_DECALS);
|
|
@@ -1937,7 +2200,10 @@ export class WebGPURenderer implements RendererApi {
|
|
|
1937
2200
|
private readonly decalScissorRect = new Int32Array(4);
|
|
1938
2201
|
private oitResolveLayout!: GPUBindGroupLayout;
|
|
1939
2202
|
private oitResolveGroup: GPUBindGroup | null = null;
|
|
1940
|
-
/**
|
|
2203
|
+
/**
|
|
2204
|
+
* Reused, because the frame loop may not allocate. Mirrors the WebGL2 backend's own, which it
|
|
2205
|
+
* had stopped doing: the other one carried a pane's refraction and this one did not.
|
|
2206
|
+
*/
|
|
1941
2207
|
private readonly oitReplayOptions: {
|
|
1942
2208
|
lit: boolean;
|
|
1943
2209
|
fog: boolean;
|
|
@@ -1945,7 +2211,20 @@ export class WebGPURenderer implements RendererApi {
|
|
|
1945
2211
|
depthWrite: boolean;
|
|
1946
2212
|
depthLayer: number;
|
|
1947
2213
|
tint: Vec3 | null;
|
|
1948
|
-
|
|
2214
|
+
refraction: number;
|
|
2215
|
+
refractTint: Vec3 | undefined;
|
|
2216
|
+
thicknessM: number;
|
|
2217
|
+
} = {
|
|
2218
|
+
lit: true,
|
|
2219
|
+
fog: true,
|
|
2220
|
+
toneMapped: true,
|
|
2221
|
+
depthWrite: false,
|
|
2222
|
+
depthLayer: 0,
|
|
2223
|
+
tint: null,
|
|
2224
|
+
refraction: 0,
|
|
2225
|
+
refractTint: undefined,
|
|
2226
|
+
thicknessM: 0,
|
|
2227
|
+
};
|
|
1949
2228
|
|
|
1950
2229
|
private reflectionPassActive = false;
|
|
1951
2230
|
/** Said once: a mirror asked for outside a frame is a call-order fault, not a device limit. */
|
|
@@ -2328,15 +2607,26 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2328
2607
|
return new GpuSurfaceTexture(this.surface.device, this.pipelines, source, options);
|
|
2329
2608
|
}
|
|
2330
2609
|
|
|
2610
|
+
/**
|
|
2611
|
+
* GPU textures an `updateSurfaceTexture` replaced, destroyed when the next frame begins.
|
|
2612
|
+
*
|
|
2613
|
+
* Not at once, because a draw recorded earlier in the frame the update landed in may still read
|
|
2614
|
+
* one, and a destroyed texture in a submit loses the whole command buffer. Every encoder a frame
|
|
2615
|
+
* opens is submitted by the time the next one begins.
|
|
2616
|
+
*/
|
|
2617
|
+
private readonly retiredTextures: GPUTexture[] = [];
|
|
2618
|
+
|
|
2331
2619
|
updateSurfaceTexture(texture: GpuSurfaceTexture, source: TexImageSource): void {
|
|
2332
2620
|
if (this.surface.lost) return;
|
|
2333
|
-
texture.update(source);
|
|
2621
|
+
const replaced = texture.update(source);
|
|
2622
|
+
if (replaced === null) return;
|
|
2623
|
+
/* A new size is a new view, so every group holding the old one is dropped — see below. */
|
|
2624
|
+
this.retiredTextures.push(replaced);
|
|
2625
|
+
this.forgetBindingsOf(texture);
|
|
2334
2626
|
}
|
|
2335
2627
|
|
|
2336
2628
|
disposeSurfaceTexture(texture: GpuSurfaceTexture): void {
|
|
2337
2629
|
if (this.surface.lost) return;
|
|
2338
|
-
/* Same reasoning, for whichever SDF labels were bound against this atlas. */
|
|
2339
|
-
this.sdfTextBindGroups.delete(texture);
|
|
2340
2630
|
if (this.albedo === texture) {
|
|
2341
2631
|
this.albedo = null;
|
|
2342
2632
|
this.bindGroup = this.blankAlbedoBindGroup;
|
|
@@ -2362,6 +2652,19 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2362
2652
|
if (this.normalMap === texture) this.normalMap = null;
|
|
2363
2653
|
if (this.ormMap === texture) this.ormMap = null;
|
|
2364
2654
|
if (this.emissiveMap === texture) this.emissiveMap = null;
|
|
2655
|
+
this.forgetBindingsOf(texture);
|
|
2656
|
+
texture.dispose();
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
/**
|
|
2660
|
+
* Drop every bind group that may hold `texture`'s view, and rebuild the ones bound now.
|
|
2661
|
+
*
|
|
2662
|
+
* For a texture going away and for one whose view has been replaced, which are the same problem:
|
|
2663
|
+
* a group holding the old view hands a destroyed texture to the next draw that asks.
|
|
2664
|
+
*/
|
|
2665
|
+
private forgetBindingsOf(texture: GpuSurfaceTexture): void {
|
|
2666
|
+
/* Same reasoning, for whichever SDF labels were bound against this atlas. */
|
|
2667
|
+
this.sdfTextBindGroups.delete(texture);
|
|
2365
2668
|
this.flatBindGroups.clear();
|
|
2366
2669
|
/*
|
|
2367
2670
|
* The blank one is built with the albedo held aside, because `buildFlatBindGroup` reads
|
|
@@ -2373,7 +2676,6 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2373
2676
|
this.blankAlbedoBindGroup = this.buildFlatBindGroup();
|
|
2374
2677
|
this.albedo = held;
|
|
2375
2678
|
this.bindGroup = this.flatGroupForMaps(held, this.normalMap, this.ormMap, this.emissiveMap);
|
|
2376
|
-
texture.dispose();
|
|
2377
2679
|
}
|
|
2378
2680
|
|
|
2379
2681
|
/**
|
|
@@ -2537,7 +2839,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2537
2839
|
private material(write: (f: Float32Array, i: Int32Array) => void): void {
|
|
2538
2840
|
if (this.surface.lost) return;
|
|
2539
2841
|
write(this.perFrameFloats, this.perFrameInts);
|
|
2540
|
-
this.
|
|
2842
|
+
this.materials.dirty();
|
|
2541
2843
|
}
|
|
2542
2844
|
|
|
2543
2845
|
private materialField(name: string): number {
|
|
@@ -2628,7 +2930,19 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2628
2930
|
if (alpha <= 0 || rect.width <= 0 || rect.height <= 0) return;
|
|
2629
2931
|
const vertexSlot = this.panelVerts.allocate();
|
|
2630
2932
|
const fragmentSlot = this.panelFrags.allocate();
|
|
2631
|
-
if (vertexSlot === null || fragmentSlot === null)
|
|
2933
|
+
if (vertexSlot === null || fragmentSlot === null) {
|
|
2934
|
+
/*
|
|
2935
|
+
* Said, as its text and line siblings say it: this returned without a word until 2026-09-19,
|
|
2936
|
+
* so an interface of panels lost everything past its sixty-fourth here and nothing on WebGL2.
|
|
2937
|
+
*/
|
|
2938
|
+
if (!this.warnedPanelsFull) {
|
|
2939
|
+
this.warnedPanelsFull = true;
|
|
2940
|
+
console.warn(
|
|
2941
|
+
`WebGPU: more than ${MAX_OVERLAYS} panels in a frame; the rest are skipped. renderer.frameBudget names the line and the count.`,
|
|
2942
|
+
);
|
|
2943
|
+
}
|
|
2944
|
+
return;
|
|
2945
|
+
}
|
|
2632
2946
|
|
|
2633
2947
|
const at = (name: string): number => PANEL_VERT_FIELDS[name]?.offset ?? 0;
|
|
2634
2948
|
this.panelVerts.writeFloats(vertexSlot, at('uRect'), [
|
|
@@ -2669,6 +2983,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
2669
2983
|
}
|
|
2670
2984
|
}
|
|
2671
2985
|
|
|
2986
|
+
/** Said once rather than every frame, for the reason `warnedFull` gives. */
|
|
2987
|
+
private warnedPanelsFull = false;
|
|
2988
|
+
|
|
2672
2989
|
/** A 3D text object. See `textPass.ts`; the layout it holds is shared with WebGL2. */
|
|
2673
2990
|
createText(): GpuText {
|
|
2674
2991
|
return new GpuText(this.surface.device);
|
|
@@ -3757,6 +4074,10 @@ export class WebGPURenderer implements RendererApi {
|
|
|
3757
4074
|
before `takeVolumeDepth`, which is the one thing here that genuinely needs one open —
|
|
3758
4075
|
it ends the current pass to copy the depth out and opens another in its place. */
|
|
3759
4076
|
if (!this.canDraw()) return;
|
|
4077
|
+
/* Geometry that has not all arrived is not drawn, which is the surface's contract and what
|
|
4078
|
+
every other mesh verb here and the other backend's volume already kept. This one drew a
|
|
4079
|
+
beam from a buffer still filling, and counted it, until 2026-09-19. See `Mesh.complete`. */
|
|
4080
|
+
if (!mesh.complete) return;
|
|
3760
4081
|
/* At zero it draws nothing rather than adding black, so a caller may keep the call in. That
|
|
3761
4082
|
now covers a clear night too: a beam with no medium to light is not drawn at all. */
|
|
3762
4083
|
const shown =
|
|
@@ -5036,6 +5357,44 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5036
5357
|
return this.surface.canvas;
|
|
5037
5358
|
}
|
|
5038
5359
|
|
|
5360
|
+
/**
|
|
5361
|
+
* The size the world is drawn at this frame, which is **not** the canvas when a reconstruction
|
|
5362
|
+
* is enlarging it.
|
|
5363
|
+
*
|
|
5364
|
+
* **A contributed pass that fills a target of its own must size it from here.** The GPU-driven
|
|
5365
|
+
* rig sized its own targets from `canvas.width` and drew a full drawing-buffer picture into a
|
|
5366
|
+
* scene target two thirds as wide: the frame pass clipped it to the top-left corner and the
|
|
5367
|
+
* composite magnified that corner back over the screen, which is a plausible-looking picture of
|
|
5368
|
+
* the wrong part of the world. The device raises nothing — a viewport smaller than the thing
|
|
5369
|
+
* drawn is legal — so there is no failure to see, only a scene that has moved.
|
|
5370
|
+
*
|
|
5371
|
+
* Valid from `beginFrame`; before the first frame it is the canvas, which is what a pass sizing
|
|
5372
|
+
* itself at registration wants.
|
|
5373
|
+
*/
|
|
5374
|
+
get sceneWidth(): number {
|
|
5375
|
+
return this.sceneSize().width;
|
|
5376
|
+
}
|
|
5377
|
+
|
|
5378
|
+
get sceneHeight(): number {
|
|
5379
|
+
return this.sceneSize().height;
|
|
5380
|
+
}
|
|
5381
|
+
|
|
5382
|
+
/**
|
|
5383
|
+
* The render size this frame will use, computed rather than remembered.
|
|
5384
|
+
*
|
|
5385
|
+
* **Computed, because a pass asks before the frame it is asking about.** `GpuDrivenPass.resize`
|
|
5386
|
+
* runs before `beginFrame` — that is when `prepare` records the pipeline — so a stored field
|
|
5387
|
+
* would answer with the previous frame's size, which is right every frame but the one where it
|
|
5388
|
+
* changed. The first frame after a reconstruction is switched on is exactly the frame that
|
|
5389
|
+
* matters.
|
|
5390
|
+
*/
|
|
5391
|
+
private sceneSize(): { width: number; height: number } {
|
|
5392
|
+
const { canvas } = this.surface;
|
|
5393
|
+
return this.reconstructionWanted
|
|
5394
|
+
? reconRenderSize(canvas.width, canvas.height, this.quality.reconstruction)
|
|
5395
|
+
: { width: Math.max(1, canvas.width), height: Math.max(1, canvas.height) };
|
|
5396
|
+
}
|
|
5397
|
+
|
|
5039
5398
|
/**
|
|
5040
5399
|
* Whether the device has gone.
|
|
5041
5400
|
*
|
|
@@ -5162,6 +5521,10 @@ export class WebGPURenderer implements RendererApi {
|
|
|
5162
5521
|
quality = this.quality;
|
|
5163
5522
|
this.maxDrawingBufferPixels = quality.maxDrawingBufferPixels;
|
|
5164
5523
|
this.exposure = quality.outputExposure;
|
|
5524
|
+
this.flushSchedule =
|
|
5525
|
+
quality.frameGraph && quality.identifierGraph
|
|
5526
|
+
? createFlushSchedule(MAX_DRAWS_PER_FRAME)
|
|
5527
|
+
: null;
|
|
5165
5528
|
|
|
5166
5529
|
const { device } = surface;
|
|
5167
5530
|
/* Off unless a consumer asked: see `RenderQuality.gpuTiming`. A timer that can
|
|
@@ -6111,12 +6474,14 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6111
6474
|
*/
|
|
6112
6475
|
this.panelLayout = createPanelBindGroupLayout(device);
|
|
6113
6476
|
this.panelCorners = createPanelCorners(device);
|
|
6477
|
+
/* The budget on one of the pair only: a panel is one ask, whichever ring runs out. */
|
|
6114
6478
|
this.panelVerts = new UniformRing(
|
|
6115
6479
|
device,
|
|
6116
6480
|
PANEL_VERT_SIZE,
|
|
6117
6481
|
MAX_OVERLAYS,
|
|
6118
6482
|
USAGE_UNIFORM_DST,
|
|
6119
6483
|
'panel.vertRing',
|
|
6484
|
+
this.panelBudget,
|
|
6120
6485
|
);
|
|
6121
6486
|
this.panelFrags = new UniformRing(
|
|
6122
6487
|
device,
|
|
@@ -6409,6 +6774,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6409
6774
|
* uncaught exceptions a second in somebody's error reporting.
|
|
6410
6775
|
*/
|
|
6411
6776
|
beginFrame(clearColor: Vec3): void {
|
|
6777
|
+
/* Whatever an update replaced during the last frame; nothing recorded can read it now. */
|
|
6778
|
+
for (const texture of this.retiredTextures) texture.destroy();
|
|
6779
|
+
this.retiredTextures.length = 0;
|
|
6412
6780
|
/*
|
|
6413
6781
|
* **Decided here and held for the frame**, so a draw cannot be recorded under one rule and
|
|
6414
6782
|
* replayed under another. Multisampling is excluded: the two buffers are single-sampled and a
|
|
@@ -6418,6 +6786,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6418
6786
|
this.translucentQueue.reset();
|
|
6419
6787
|
this.decalQueue.reset();
|
|
6420
6788
|
this.reflectionQueue.reset();
|
|
6789
|
+
/* Redeclared every frame, so a field a consumer stops declaring stops lighting. */
|
|
6790
|
+
this.distanceFields.reset();
|
|
6421
6791
|
this.oitActive =
|
|
6422
6792
|
this.quality.orderIndependent && this.quality.screenEffects && this.samples === 1;
|
|
6423
6793
|
/* Read off the line above rather than restating its last term: with the other two true, the
|
|
@@ -6492,7 +6862,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6492
6862
|
this.lineFrags.reset();
|
|
6493
6863
|
/* The material ring, and the open slot with it: last frame's slots are gone. */
|
|
6494
6864
|
this.perFrame.reset();
|
|
6495
|
-
this.
|
|
6865
|
+
this.materials.dirty();
|
|
6866
|
+
/* Spent by `runMotion` at the end of the last frame; a frame states its movers afresh. */
|
|
6867
|
+
this.motionCount = 0;
|
|
6496
6868
|
/*
|
|
6497
6869
|
* The canvas rather than the swap chain's texture, and the difference is that reading this
|
|
6498
6870
|
* does not *acquire* anything. See `swapView`.
|
|
@@ -6503,8 +6875,22 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6503
6875
|
* without the target it draws into, and a scene is free never to call `resize`.
|
|
6504
6876
|
*/
|
|
6505
6877
|
const { canvas } = this.surface;
|
|
6506
|
-
|
|
6507
|
-
|
|
6878
|
+
/*
|
|
6879
|
+
* **The render size first, because every target below is sized from it.** Off — and without a
|
|
6880
|
+
* composite to enlarge from — it is the drawing buffer exactly, which is what makes a frame
|
|
6881
|
+
* with no reconstruction byte-for-byte the frame this renderer has always drawn.
|
|
6882
|
+
*/
|
|
6883
|
+
const render = this.sceneSize();
|
|
6884
|
+
this.renderWidth = render.width;
|
|
6885
|
+
this.renderHeight = render.height;
|
|
6886
|
+
this.ensureDepth(render.width, render.height);
|
|
6887
|
+
this.ensureComposite(render.width, render.height);
|
|
6888
|
+
this.ensureReconstruction(
|
|
6889
|
+
render.width,
|
|
6890
|
+
render.height,
|
|
6891
|
+
Math.max(1, canvas.width),
|
|
6892
|
+
Math.max(1, canvas.height),
|
|
6893
|
+
);
|
|
6508
6894
|
/*
|
|
6509
6895
|
* **Not acquired here, and that is the fix for a frame flashing black.**
|
|
6510
6896
|
*
|
|
@@ -6522,6 +6908,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6522
6908
|
*/
|
|
6523
6909
|
this.frameSwapView = null;
|
|
6524
6910
|
this.encoder = this.surface.device.createCommandEncoder();
|
|
6911
|
+
this.settleReconJitter();
|
|
6525
6912
|
/*
|
|
6526
6913
|
* Passes that own a target fill it here — after the encoder exists and before anything opens
|
|
6527
6914
|
* the frame's render pass, including the eager path twenty lines below. See
|
|
@@ -6822,6 +7209,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6822
7209
|
*/
|
|
6823
7210
|
if (!this.reflectionPassActive) {
|
|
6824
7211
|
mat4.copy(this.frameRawViewProj, camera.viewProjection);
|
|
7212
|
+
/* The eye in world space, which the resolve's normals are turned to face. */
|
|
7213
|
+
this.reconEye.set(camera.position);
|
|
6825
7214
|
this.frameEye.set(camera.position);
|
|
6826
7215
|
/* Held for the medium's march, which runs long after every draw and needs the sun the
|
|
6827
7216
|
frame was lit by. Referenced rather than copied, exactly as `renderer.ts` holds its
|
|
@@ -6835,25 +7224,69 @@ export class WebGPURenderer implements RendererApi {
|
|
|
6835
7224
|
* is skipped by the same guard that skips it for motion blur: it is not the picture the
|
|
6836
7225
|
* history holds, and jittering it would resolve a reflection against the viewer's frame.
|
|
6837
7226
|
*/
|
|
7227
|
+
/*
|
|
7228
|
+
* **Reconstruction jitters instead of the temporal resolve, never as well as it.** Both
|
|
7229
|
+
* accumulate a history out of a jittered sequence, and running them together would resolve the
|
|
7230
|
+
* frame twice — once at the render size and again at the output size, against a history whose
|
|
7231
|
+
* own frames had already been mixed. So this is a choice rather than a pair, reconstruction
|
|
7232
|
+
* wins where a consumer asked for both, and `temporalResolve` is skipped while it runs.
|
|
7233
|
+
*
|
|
7234
|
+
* Its sequence is its own: `reconJitterPhases` takes eight positions for every output pixel a
|
|
7235
|
+
* render pixel covers, where the temporal resolve's eight are enough at one to one.
|
|
7236
|
+
*/
|
|
7237
|
+
this.reconstructing = this.reconstructionWanted && !this.reflectionPassActive;
|
|
7238
|
+
if (this.quality.reconstruction > 0 && this.samples > 1 && !this.reconMultisampleSaid) {
|
|
7239
|
+
this.reconMultisampleSaid = true;
|
|
7240
|
+
console.warn(RECONSTRUCTION_MULTISAMPLE_REFUSAL);
|
|
7241
|
+
}
|
|
7242
|
+
if (this.reconstructing) {
|
|
7243
|
+
/*
|
|
7244
|
+
* The offset `settleReconJitter` chose for the frame at `beginFrame`, applied here and not
|
|
7245
|
+
* advanced here: a frame binding its mesh pass twice is one frame of the sequence.
|
|
7246
|
+
*
|
|
7247
|
+
* **The y offset is negated on the way in, and that is not a preference.** This shifts the
|
|
7248
|
+
* *corrected* matrix, whose y `CLIP_CORRECTION` has already negated so that the negation
|
|
7249
|
+
* every generated vertex stage ends with cancels out — §3 row 56. So an offset added here
|
|
7250
|
+
* arrives on screen with its sign reversed, while x, which nothing negates, arrives as
|
|
7251
|
+
* given. The temporal resolve cannot see this because its shader never reads the offset; the
|
|
7252
|
+
* reconstruction reads it at every texel it unprojects, and a sign error there moves every
|
|
7253
|
+
* sample two jitters away from where the depth says it is.
|
|
7254
|
+
*/
|
|
7255
|
+
this.viewProj = jitterProjection(
|
|
7256
|
+
this.jitteredViewProj,
|
|
7257
|
+
this.correctedViewProj,
|
|
7258
|
+
this.reconJitter[0] as number,
|
|
7259
|
+
-(this.reconJitter[1] as number),
|
|
7260
|
+
this.renderWidth,
|
|
7261
|
+
this.renderHeight,
|
|
7262
|
+
) as Float32Array;
|
|
7263
|
+
this.temporalJittering = false;
|
|
7264
|
+
this.temporalHistoryUsable = false;
|
|
7265
|
+
}
|
|
6838
7266
|
this.temporalJittering =
|
|
6839
|
-
this.
|
|
7267
|
+
!this.reconstructing &&
|
|
7268
|
+
this.quality.temporalAa &&
|
|
7269
|
+
this.quality.screenEffects &&
|
|
7270
|
+
!this.reflectionPassActive;
|
|
6840
7271
|
if (this.temporalJittering) {
|
|
7272
|
+
/* The scene's size, not the drawing buffer's: the history is a picture of the render. */
|
|
6841
7273
|
this.temporalHistoryUsable = this.temporalHistory.openFrame(
|
|
6842
|
-
|
|
6843
|
-
|
|
7274
|
+
this.renderWidth,
|
|
7275
|
+
this.renderHeight,
|
|
6844
7276
|
);
|
|
6845
7277
|
const [jx, jy] = jitterOffset(this.temporalHistory.frameIndex);
|
|
6846
7278
|
this.temporalJitterX = jx;
|
|
6847
7279
|
this.temporalJitterY = jy;
|
|
7280
|
+
/* A jitter is half a *render* texel, which is what the sequence is spread over. */
|
|
6848
7281
|
this.viewProj = jitterProjection(
|
|
6849
7282
|
this.jitteredViewProj,
|
|
6850
7283
|
this.correctedViewProj,
|
|
6851
7284
|
jx,
|
|
6852
7285
|
jy,
|
|
6853
|
-
|
|
6854
|
-
|
|
7286
|
+
this.renderWidth,
|
|
7287
|
+
this.renderHeight,
|
|
6855
7288
|
) as Float32Array;
|
|
6856
|
-
} else {
|
|
7289
|
+
} else if (!this.reconstructing) {
|
|
6857
7290
|
this.temporalHistoryUsable = false;
|
|
6858
7291
|
this.viewProj = this.correctedViewProj;
|
|
6859
7292
|
}
|
|
@@ -7112,6 +7545,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7112
7545
|
* face size, and the array's stops one level below the diffuse term.
|
|
7113
7546
|
*/
|
|
7114
7547
|
f[at('uEnvironmentEdge')] = this.probeEdge;
|
|
7548
|
+
/* Off until the indirect pass fills the moment layers. See `quality.indirectLight`. */
|
|
7549
|
+
f[at('uProbeVisibilityEnabled')] = 0;
|
|
7115
7550
|
f[at('uEnvironmentIrradianceLevel')] = irradianceLevelFor(this.probeEdge);
|
|
7116
7551
|
f[at('uProbeGridAmbient')] = usable && this.probeAmbient ? 1 : 0;
|
|
7117
7552
|
}
|
|
@@ -7316,7 +7751,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7316
7751
|
* block rather than uploading it: the next draw takes the slot. `renderer.ts` resets the
|
|
7317
7752
|
* same terms here, which is why a pass cannot inherit a material from the one before it.
|
|
7318
7753
|
*/
|
|
7319
|
-
this.
|
|
7754
|
+
this.materials.dirty();
|
|
7320
7755
|
/* A pass starts with no material, exactly as `renderer.ts` resets these terms. */
|
|
7321
7756
|
this.albedo = null;
|
|
7322
7757
|
this.bindGroup = this.blankAlbedoBindGroup;
|
|
@@ -7332,7 +7767,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7332
7767
|
* each, which is what it asked for.
|
|
7333
7768
|
*/
|
|
7334
7769
|
private materialSlotForDraw(): number | null {
|
|
7335
|
-
if (this.
|
|
7770
|
+
if (this.materials.open) return this.materials.slot;
|
|
7336
7771
|
const slot = this.perFrame.allocate();
|
|
7337
7772
|
if (slot === null) {
|
|
7338
7773
|
if (!this.warnedMaterialsFull) {
|
|
@@ -7344,7 +7779,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7344
7779
|
return null;
|
|
7345
7780
|
}
|
|
7346
7781
|
this.perFrame.writeBlock(slot, this.perFrameInts);
|
|
7347
|
-
this.
|
|
7782
|
+
this.materials.slot = slot;
|
|
7348
7783
|
return slot;
|
|
7349
7784
|
}
|
|
7350
7785
|
|
|
@@ -7370,6 +7805,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7370
7805
|
model: ArrayLike<number>,
|
|
7371
7806
|
depthLayer = 0,
|
|
7372
7807
|
tint: Vec3 | null = null,
|
|
7808
|
+
/** Where it was last frame. See `RendererApi.drawMesh`, and `runMotion` for what reads it. */
|
|
7809
|
+
previousModel: ArrayLike<number> | null = null,
|
|
7373
7810
|
): void {
|
|
7374
7811
|
/*
|
|
7375
7812
|
* Skipped where the flag asks and the bounds say so.
|
|
@@ -7389,6 +7826,12 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7389
7826
|
* did before.
|
|
7390
7827
|
*/
|
|
7391
7828
|
if (this.quality.cullDraws && this.occluded(mesh.bounds, model as ReadonlyMat4)) return;
|
|
7829
|
+
/*
|
|
7830
|
+
* Recorded after the two culls rather than before: a mesh the frame does not draw writes no
|
|
7831
|
+
* depth, so a motion pass drawing it would either be depth-rejected everywhere — wasted — or,
|
|
7832
|
+
* where it happened to pass, write motion for a surface no pixel of the frame belongs to.
|
|
7833
|
+
*/
|
|
7834
|
+
if (previousModel !== null) this.recordMotion(mesh, model, previousModel);
|
|
7392
7835
|
this.submitMesh(mesh, model, tint, 1, false, { depthLayer });
|
|
7393
7836
|
}
|
|
7394
7837
|
|
|
@@ -7586,7 +8029,6 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7586
8029
|
/* Material state exactly like `uOpacity`, dirtied for the accumulation pass and restored
|
|
7587
8030
|
below — the revealage pass draws with it at zero and its colour multiplied away. */
|
|
7588
8031
|
this.perFrameFloats[this.materialField('uOitWeighted')] = this.oitMode === 'accum' ? 1 : 0;
|
|
7589
|
-
this.materialSlot = -1;
|
|
7590
8032
|
}
|
|
7591
8033
|
/* Same shape as `dimmed`: a slot is only worth taking when either differs from the default
|
|
7592
8034
|
`bindMeshPass` already wrote, and both are put back together once the draw is submitted. */
|
|
@@ -7597,13 +8039,11 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7597
8039
|
if (unlitOrUnfogged) {
|
|
7598
8040
|
if (!lit) this.perFrameInts[this.materialField('uLightingEnabled')] = 0;
|
|
7599
8041
|
if (!fog) this.perFrameInts[this.materialField('uFogEnabled')] = 0;
|
|
7600
|
-
this.materialSlot = -1;
|
|
7601
8042
|
}
|
|
7602
8043
|
/* `1` is sRGB alone: the conversion without the curve. `Math.min` rather than a literal, so
|
|
7603
8044
|
a renderer asked for `none` stays at none. See `TranslucentMeshOptions.toneMapped`. */
|
|
7604
8045
|
if (!toneMapped) {
|
|
7605
8046
|
this.perFrameInts[this.materialField('uOutputTransform')] = Math.min(this.gradeCode(), 1);
|
|
7606
|
-
this.materialSlot = -1;
|
|
7607
8047
|
}
|
|
7608
8048
|
/*
|
|
7609
8049
|
* **Refraction, and the snapshot it reads is taken here or the draw does not refract.**
|
|
@@ -7624,8 +8064,11 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7624
8064
|
this.perFrameFloats[tintAt + 1] = refractTint?.[1] ?? 1;
|
|
7625
8065
|
this.perFrameFloats[tintAt + 2] = refractTint?.[2] ?? 1;
|
|
7626
8066
|
this.perFrameFloats[this.materialField('uRefractThickness')] = options.thicknessM ?? 0;
|
|
7627
|
-
this.materialSlot = -1;
|
|
7628
8067
|
}
|
|
8068
|
+
/* Taken for itself when any of those differs from the pass, and put back below: the rule both
|
|
8069
|
+
backends count material changes by. See `materialChanges.ts`. */
|
|
8070
|
+
const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting });
|
|
8071
|
+
if (own) this.materials.dirty();
|
|
7629
8072
|
const base = (mesh as GpuMesh & { key?: string }).key ?? 'flat:s0:u0';
|
|
7630
8073
|
/*
|
|
7631
8074
|
* A rigged mesh takes the skinned vertex variant, an unrigged one the plain variant, and the
|
|
@@ -7758,23 +8201,20 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7758
8201
|
if (dimmed) {
|
|
7759
8202
|
this.perFrameFloats[this.materialField('uOpacity')] = 1;
|
|
7760
8203
|
this.perFrameFloats[this.materialField('uOitWeighted')] = 0;
|
|
7761
|
-
this.materialSlot = -1;
|
|
7762
8204
|
}
|
|
7763
8205
|
if (unlitOrUnfogged) {
|
|
7764
8206
|
if (!lit) this.perFrameInts[this.materialField('uLightingEnabled')] = 1;
|
|
7765
8207
|
if (!fog) this.perFrameInts[this.materialField('uFogEnabled')] = 1;
|
|
7766
|
-
this.materialSlot = -1;
|
|
7767
8208
|
}
|
|
7768
8209
|
if (refracting) {
|
|
7769
8210
|
/* Back to off for every other draw in the frame, exactly as `uOpacity` is: this scratch is
|
|
7770
8211
|
shared, so a strength left set is worn by everything drawn after it. */
|
|
7771
8212
|
this.perFrameFloats[this.materialField('uRefractStrength')] = 0;
|
|
7772
|
-
this.materialSlot = -1;
|
|
7773
8213
|
}
|
|
7774
8214
|
if (!toneMapped) {
|
|
7775
8215
|
this.perFrameInts[this.materialField('uOutputTransform')] = this.gradeCode();
|
|
7776
|
-
this.materialSlot = -1;
|
|
7777
8216
|
}
|
|
8217
|
+
if (own) this.materials.dirty();
|
|
7778
8218
|
}
|
|
7779
8219
|
|
|
7780
8220
|
/** Release geometry. */
|
|
@@ -7912,7 +8352,6 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7912
8352
|
const dimmed = opacity < 1;
|
|
7913
8353
|
if (dimmed) {
|
|
7914
8354
|
this.perFrameFloats[this.materialField('uOpacity')] = opacity;
|
|
7915
|
-
this.materialSlot = -1;
|
|
7916
8355
|
}
|
|
7917
8356
|
const lit = options.lit ?? true;
|
|
7918
8357
|
const fog = options.fog ?? true;
|
|
@@ -7921,15 +8360,16 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7921
8360
|
if (unlitOrUnfogged) {
|
|
7922
8361
|
if (!lit) this.perFrameInts[this.materialField('uLightingEnabled')] = 0;
|
|
7923
8362
|
if (!fog) this.perFrameInts[this.materialField('uFogEnabled')] = 0;
|
|
7924
|
-
this.materialSlot = -1;
|
|
7925
8363
|
}
|
|
7926
8364
|
if (!toneMapped) {
|
|
7927
8365
|
this.perFrameInts[this.materialField('uOutputTransform')] = Math.min(this.gradeCode(), 1);
|
|
7928
|
-
this.materialSlot = -1;
|
|
7929
8366
|
}
|
|
7930
8367
|
|
|
7931
8368
|
const depthWrite = options.depthWrite ?? true;
|
|
7932
8369
|
const layer = Math.min(Math.max(Math.round(options.depthLayer ?? 0), 0), MAX_DEPTH_LAYER);
|
|
8370
|
+
/* An instanced draw does not refract. See `materialChanges.ts`. */
|
|
8371
|
+
const own = ownsMaterial({ opacity, lit, fog, toneMapped, refracting: false });
|
|
8372
|
+
if (own) this.materials.dirty();
|
|
7933
8373
|
const base = (mesh as GpuMesh & { key?: string }).key ?? 'flat:s0:u0';
|
|
7934
8374
|
/* `|inst` in the key, for the reason every other suffix is there: an instanced pipeline binds
|
|
7935
8375
|
a third vertex buffer and a different vertex module, and sharing a cache entry with the
|
|
@@ -7999,17 +8439,15 @@ export class WebGPURenderer implements RendererApi {
|
|
|
7999
8439
|
past the restore leaves the rest of the frame unlit, unfogged or undimmed. */
|
|
8000
8440
|
if (dimmed) {
|
|
8001
8441
|
this.perFrameFloats[this.materialField('uOpacity')] = 1;
|
|
8002
|
-
this.materialSlot = -1;
|
|
8003
8442
|
}
|
|
8004
8443
|
if (unlitOrUnfogged) {
|
|
8005
8444
|
this.perFrameInts[this.materialField('uLightingEnabled')] = 1;
|
|
8006
8445
|
this.perFrameInts[this.materialField('uFogEnabled')] = 1;
|
|
8007
|
-
this.materialSlot = -1;
|
|
8008
8446
|
}
|
|
8009
8447
|
if (!toneMapped) {
|
|
8010
8448
|
this.perFrameInts[this.materialField('uOutputTransform')] = this.gradeCode();
|
|
8011
|
-
this.materialSlot = -1;
|
|
8012
8449
|
}
|
|
8450
|
+
if (own) this.materials.dirty();
|
|
8013
8451
|
}
|
|
8014
8452
|
|
|
8015
8453
|
/* -- Scatter ------------------------------------------------------------------------ */
|
|
@@ -8826,7 +9264,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8826
9264
|
this.emissiveMap,
|
|
8827
9265
|
);
|
|
8828
9266
|
/* The open material lives in a slot of a buffer that no longer exists. */
|
|
8829
|
-
this.
|
|
9267
|
+
this.materials.dirty();
|
|
8830
9268
|
}
|
|
8831
9269
|
|
|
8832
9270
|
/**
|
|
@@ -8970,7 +9408,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
8970
9408
|
* consumer running the temporal resolve without motion blur would have reprojected through a
|
|
8971
9409
|
* `previousViewProj` nothing had ever written.
|
|
8972
9410
|
*/
|
|
8973
|
-
const wantsReprojection = blur > 0 || this.temporalJittering;
|
|
9411
|
+
const wantsReprojection = blur > 0 || this.temporalJittering || this.reconstructing;
|
|
8974
9412
|
if (wantsReprojection && this.viewProj !== null) {
|
|
8975
9413
|
if (this.hasPreviousView) {
|
|
8976
9414
|
mat4.invert(this.reprojection, this.correctedViewProj);
|
|
@@ -9025,6 +9463,15 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9025
9463
|
if (mediumActive(this.mediumOptions, this.quality.globalMediumSteps)) this.runMedium(encoder);
|
|
9026
9464
|
/* After the depth it reprojects through, and before anything reads the scene. */
|
|
9027
9465
|
this.temporalResolve(encoder);
|
|
9466
|
+
/*
|
|
9467
|
+
* **Reconstruction stands where the temporal resolve stands**, for the same reasons: after
|
|
9468
|
+
* everything that draws into the scene and after the depth it reprojects through, and before
|
|
9469
|
+
* bloom and the composite read the picture. It cannot copy its result back over the scene the
|
|
9470
|
+
* way the temporal resolve does — the two are different sizes — so the composite is given a
|
|
9471
|
+
* bind group of its own instead, which is the second group `temporalResolve` declined.
|
|
9472
|
+
*/
|
|
9473
|
+
this.runMotion(encoder);
|
|
9474
|
+
this.runReconstruction(encoder);
|
|
9028
9475
|
const aoStrength = this.quality.ambientOcclusion;
|
|
9029
9476
|
if (aoStrength > 0 && this.frameProjection !== null) this.runOcclusion(encoder);
|
|
9030
9477
|
const bloomStrength = this.quality.bloom * this.bloomScale;
|
|
@@ -9087,8 +9534,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9087
9534
|
this.surface.format,
|
|
9088
9535
|
),
|
|
9089
9536
|
);
|
|
9090
|
-
|
|
9091
|
-
|
|
9537
|
+
const rush = this.reconstructing ? this.reconRushGroup : this.rushBindGroup;
|
|
9538
|
+
if (rush === null) return;
|
|
9539
|
+
pass.setBindGroup(0, rush, [0]);
|
|
9092
9540
|
pass.draw(3);
|
|
9093
9541
|
pass.end();
|
|
9094
9542
|
}
|
|
@@ -9109,6 +9557,15 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9109
9557
|
if (this.sceneColorView === null || this.resolvedDepthView === null) return;
|
|
9110
9558
|
if (this.aoTargetView === null) return;
|
|
9111
9559
|
this.rushBindGroup = this.buildRushBindGroup('post.rushBindGroup', this.sceneColorView);
|
|
9560
|
+
/*
|
|
9561
|
+
* **And the reconstruction's twin, from the same three call sites.** A colour grade replaces
|
|
9562
|
+
* the lookup table and a resize replaces the depth and the occlusion target, and both groups
|
|
9563
|
+
* hold all three — so a second place that rebuilt only one of them is how the composite would
|
|
9564
|
+
* come to read a stale view on exactly the frames a consumer changed something.
|
|
9565
|
+
*/
|
|
9566
|
+
if (this.reconShownView !== null) {
|
|
9567
|
+
this.reconRushGroup = this.buildRushBindGroup('recon.rushBindGroup', this.reconShownView);
|
|
9568
|
+
}
|
|
9112
9569
|
}
|
|
9113
9570
|
|
|
9114
9571
|
/** The composite's group over one scene source. See `rebuildRushBindGroup`. */
|
|
@@ -9162,6 +9619,506 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9162
9619
|
}
|
|
9163
9620
|
|
|
9164
9621
|
/** Sample zero of the depth, into something the generated shaders can sample. */
|
|
9622
|
+
/**
|
|
9623
|
+
* Allocate what a reconstruction needs, and release it when nobody is asking.
|
|
9624
|
+
*
|
|
9625
|
+
* Sized by a key rather than by comparing four numbers, because three of the six targets are the
|
|
9626
|
+
* output's size and three are the render's, and a comparison that checked one pair would rebuild
|
|
9627
|
+
* half of them on a resize and keep the other half at the old size — which the device accepts,
|
|
9628
|
+
* a bind group holding views of two different sizes being perfectly legal, and which draws a
|
|
9629
|
+
* picture with a piece of it at the wrong scale.
|
|
9630
|
+
*/
|
|
9631
|
+
private ensureReconstruction(
|
|
9632
|
+
renderWidth: number,
|
|
9633
|
+
renderHeight: number,
|
|
9634
|
+
outputWidth: number,
|
|
9635
|
+
outputHeight: number,
|
|
9636
|
+
): void {
|
|
9637
|
+
const wanted = this.reconstructionWanted;
|
|
9638
|
+
const key = wanted ? `${renderWidth}x${renderHeight}:${outputWidth}x${outputHeight}` : '';
|
|
9639
|
+
if (key === this.reconSizeKey) return;
|
|
9640
|
+
this.reconSizeKey = key;
|
|
9641
|
+
|
|
9642
|
+
for (const dead of [
|
|
9643
|
+
...this.reconHistories,
|
|
9644
|
+
this.reconShown,
|
|
9645
|
+
this.reconMotion,
|
|
9646
|
+
this.reconPreviousDepth,
|
|
9647
|
+
]) {
|
|
9648
|
+
dead?.destroy();
|
|
9649
|
+
}
|
|
9650
|
+
this.reconHistories = [null, null];
|
|
9651
|
+
this.reconHistoryViews = [null, null];
|
|
9652
|
+
this.reconShown = null;
|
|
9653
|
+
this.reconShownView = null;
|
|
9654
|
+
this.reconMotion = null;
|
|
9655
|
+
this.reconMotionView = null;
|
|
9656
|
+
this.reconPreviousDepth = null;
|
|
9657
|
+
this.reconPreviousDepthView = null;
|
|
9658
|
+
this.reconGroups = [null, null];
|
|
9659
|
+
this.reconSharpenGroups = [null, null];
|
|
9660
|
+
this.reconRushGroup = null;
|
|
9661
|
+
/* A history of a size that no longer exists is not a history of this frame. */
|
|
9662
|
+
this.reconHasHistory = false;
|
|
9663
|
+
if (!wanted) return;
|
|
9664
|
+
|
|
9665
|
+
const { device } = this.surface;
|
|
9666
|
+
const HISTORY_USAGE = 0x4 | 0x8; // TEXTURE_BINDING | STORAGE_BINDING
|
|
9667
|
+
for (let i = 0; i < 2; i += 1) {
|
|
9668
|
+
const texture = device.createTexture({
|
|
9669
|
+
label: `recon.history${String(i)}`,
|
|
9670
|
+
size: [outputWidth, outputHeight],
|
|
9671
|
+
format: RECON_HISTORY_FORMAT,
|
|
9672
|
+
usage: HISTORY_USAGE,
|
|
9673
|
+
});
|
|
9674
|
+
this.reconHistories[i] = texture;
|
|
9675
|
+
this.reconHistoryViews[i] = texture.createView();
|
|
9676
|
+
}
|
|
9677
|
+
this.reconShown = device.createTexture({
|
|
9678
|
+
label: 'recon.shown',
|
|
9679
|
+
size: [outputWidth, outputHeight],
|
|
9680
|
+
format: RECON_HISTORY_FORMAT,
|
|
9681
|
+
usage: HISTORY_USAGE,
|
|
9682
|
+
});
|
|
9683
|
+
this.reconShownView = this.reconShown.createView();
|
|
9684
|
+
this.reconMotion = device.createTexture({
|
|
9685
|
+
label: 'recon.motion',
|
|
9686
|
+
size: [renderWidth, renderHeight],
|
|
9687
|
+
format: RECON_HISTORY_FORMAT,
|
|
9688
|
+
/* An attachment as well, because the pass that will write it is a draw. Nothing writes it
|
|
9689
|
+
today and nothing needs to clear it either: WebGPU zero-initialises a new texture, and
|
|
9690
|
+
a zero in the fourth channel is what tells the resolve to derive the camera's motion. */
|
|
9691
|
+
usage: 0x4 | 0x10,
|
|
9692
|
+
});
|
|
9693
|
+
this.reconMotionView = this.reconMotion.createView();
|
|
9694
|
+
this.reconPreviousDepth = device.createTexture({
|
|
9695
|
+
label: 'recon.previousDepth',
|
|
9696
|
+
size: [renderWidth, renderHeight],
|
|
9697
|
+
format: RESOLVED_DEPTH_FORMAT,
|
|
9698
|
+
usage: 0x4 | 0x2, // TEXTURE_BINDING | COPY_DST
|
|
9699
|
+
});
|
|
9700
|
+
this.reconPreviousDepthView = this.reconPreviousDepth.createView();
|
|
9701
|
+
|
|
9702
|
+
this.reconParams ??= device.createBuffer({
|
|
9703
|
+
label: 'recon.params',
|
|
9704
|
+
size: this.reconStaging.byteLength,
|
|
9705
|
+
usage: 0x40 | 0x8, // UNIFORM | COPY_DST
|
|
9706
|
+
});
|
|
9707
|
+
this.buildReconPipelines();
|
|
9708
|
+
this.rebuildReconGroups();
|
|
9709
|
+
}
|
|
9710
|
+
|
|
9711
|
+
/**
|
|
9712
|
+
* The two dispatches, over one explicit layout.
|
|
9713
|
+
*
|
|
9714
|
+
* **Explicit and not `layout: 'auto'`**, which §3 row 51 is about: an inferred layout names only
|
|
9715
|
+
* the bindings its own entry point reads, and these two read different subsets of one module —
|
|
9716
|
+
* the sharpen touches nothing but the history and the target. Two inferred layouts cannot share
|
|
9717
|
+
* a bind group, and building two groups over the same resources is two things to keep in step.
|
|
9718
|
+
*/
|
|
9719
|
+
private buildReconPipelines(): void {
|
|
9720
|
+
if (this.reconLayout !== null) return;
|
|
9721
|
+
const { device } = this.surface;
|
|
9722
|
+
const COMPUTE = 0x4; // GPUShaderStage.COMPUTE
|
|
9723
|
+
const sampled = (binding: number, sampleType: GPUTextureSampleType) => ({
|
|
9724
|
+
binding,
|
|
9725
|
+
visibility: COMPUTE,
|
|
9726
|
+
texture: { sampleType, viewDimension: '2d' as const },
|
|
9727
|
+
});
|
|
9728
|
+
this.reconLayout = device.createBindGroupLayout({
|
|
9729
|
+
label: 'recon.layout',
|
|
9730
|
+
entries: [
|
|
9731
|
+
sampled(0, 'float'),
|
|
9732
|
+
/* r32float is not filterable, and the resolve only ever loads a depth texel. */
|
|
9733
|
+
sampled(1, 'unfilterable-float'),
|
|
9734
|
+
sampled(2, 'float'),
|
|
9735
|
+
sampled(3, 'unfilterable-float'),
|
|
9736
|
+
sampled(4, 'float'),
|
|
9737
|
+
{ binding: 5, visibility: COMPUTE, sampler: { type: 'filtering' as const } },
|
|
9738
|
+
{ binding: 6, visibility: COMPUTE, buffer: { type: 'uniform' as const } },
|
|
9739
|
+
{
|
|
9740
|
+
binding: 7,
|
|
9741
|
+
visibility: COMPUTE,
|
|
9742
|
+
storageTexture: {
|
|
9743
|
+
access: 'write-only' as const,
|
|
9744
|
+
format: RECON_HISTORY_FORMAT,
|
|
9745
|
+
viewDimension: '2d' as const,
|
|
9746
|
+
},
|
|
9747
|
+
},
|
|
9748
|
+
],
|
|
9749
|
+
});
|
|
9750
|
+
const module = device.createShaderModule({ label: 'recon.resolve', code: reconResolveWgsl() });
|
|
9751
|
+
const layout = device.createPipelineLayout({ bindGroupLayouts: [this.reconLayout] });
|
|
9752
|
+
this.reconResolvePipeline = device.createComputePipeline({
|
|
9753
|
+
label: 'recon.resolve',
|
|
9754
|
+
layout,
|
|
9755
|
+
compute: { module, entryPoint: 'resolveMain' },
|
|
9756
|
+
});
|
|
9757
|
+
this.reconSharpenPipeline = device.createComputePipeline({
|
|
9758
|
+
label: 'recon.sharpen',
|
|
9759
|
+
layout,
|
|
9760
|
+
compute: { module, entryPoint: 'sharpenMain' },
|
|
9761
|
+
});
|
|
9762
|
+
}
|
|
9763
|
+
|
|
9764
|
+
/**
|
|
9765
|
+
* The four groups, one a direction of the ping-pong and one a dispatch.
|
|
9766
|
+
*
|
|
9767
|
+
* The resolve reads history `1 - write` and writes history `write`; the sharpen reads history
|
|
9768
|
+
* `write` and writes the shown picture. A group holds the views it was built with, so this is
|
|
9769
|
+
* called whenever any of them is replaced.
|
|
9770
|
+
*/
|
|
9771
|
+
private rebuildReconGroups(): void {
|
|
9772
|
+
const layout = this.reconLayout;
|
|
9773
|
+
const params = this.reconParams;
|
|
9774
|
+
if (layout === null || params === null) return;
|
|
9775
|
+
if (this.sceneColorView === null || this.resolvedDepthView === null) return;
|
|
9776
|
+
if (this.reconMotionView === null || this.reconPreviousDepthView === null) return;
|
|
9777
|
+
if (this.reconShownView === null) return;
|
|
9778
|
+
const { device } = this.surface;
|
|
9779
|
+
const build = (label: string, history: GPUTextureView, target: GPUTextureView) =>
|
|
9780
|
+
device.createBindGroup({
|
|
9781
|
+
label,
|
|
9782
|
+
layout,
|
|
9783
|
+
entries: [
|
|
9784
|
+
{ binding: 0, resource: this.sceneColorView as GPUTextureView },
|
|
9785
|
+
{ binding: 1, resource: this.resolvedDepthView as GPUTextureView },
|
|
9786
|
+
{ binding: 2, resource: this.reconMotionView as GPUTextureView },
|
|
9787
|
+
{ binding: 3, resource: this.reconPreviousDepthView as GPUTextureView },
|
|
9788
|
+
{ binding: 4, resource: history },
|
|
9789
|
+
{ binding: 5, resource: this.postSampler },
|
|
9790
|
+
{ binding: 6, resource: { buffer: params } },
|
|
9791
|
+
{ binding: 7, resource: target },
|
|
9792
|
+
],
|
|
9793
|
+
});
|
|
9794
|
+
for (let write = 0; write < 2; write += 1) {
|
|
9795
|
+
const read = this.reconHistoryViews[1 - write];
|
|
9796
|
+
const target = this.reconHistoryViews[write];
|
|
9797
|
+
if (read === null || target === null) continue;
|
|
9798
|
+
this.reconGroups[write] = build(`recon.resolve${String(write)}`, read, target);
|
|
9799
|
+
this.reconSharpenGroups[write] = build(
|
|
9800
|
+
`recon.sharpen${String(write)}`,
|
|
9801
|
+
target,
|
|
9802
|
+
this.reconShownView,
|
|
9803
|
+
);
|
|
9804
|
+
}
|
|
9805
|
+
/* The composite's own group, over the resolved picture instead of the scene target. */
|
|
9806
|
+
this.reconRushGroup = this.buildRushBindGroup('recon.rushBindGroup', this.reconShownView);
|
|
9807
|
+
/* And the scene's, because `ensureComposite` built it before this one existed. */
|
|
9808
|
+
this.rebuildRushBindGroup();
|
|
9809
|
+
}
|
|
9810
|
+
|
|
9811
|
+
/**
|
|
9812
|
+
* Remember that this mesh moved, so the motion pass can draw where it was.
|
|
9813
|
+
*
|
|
9814
|
+
* Silent unless a reconstruction is running: a scene that always states its previous transforms
|
|
9815
|
+
* — which is the right thing for a scene to do, the alternative being to know what the renderer
|
|
9816
|
+
* is configured as — must cost nothing at all when nobody is going to read them.
|
|
9817
|
+
*/
|
|
9818
|
+
private recordMotion(
|
|
9819
|
+
mesh: GpuMesh,
|
|
9820
|
+
model: ArrayLike<number>,
|
|
9821
|
+
previousModel: ArrayLike<number>,
|
|
9822
|
+
): void {
|
|
9823
|
+
if (!this.reconstructing) return;
|
|
9824
|
+
const at = this.motionCount;
|
|
9825
|
+
const wanted = (at + 1) * MOTION_DRAW_FLOATS;
|
|
9826
|
+
if (this.motionMatrices.length < wanted) {
|
|
9827
|
+
/* Doubling, so a scene settles on one allocation rather than one a draw. */
|
|
9828
|
+
const grown = new Float32Array(Math.max(wanted, this.motionMatrices.length * 2, 64));
|
|
9829
|
+
grown.set(this.motionMatrices);
|
|
9830
|
+
this.motionMatrices = grown;
|
|
9831
|
+
}
|
|
9832
|
+
this.motionMatrices.set(model as ArrayLike<number> & Iterable<number>, at * MOTION_DRAW_FLOATS);
|
|
9833
|
+
this.motionMatrices.set(
|
|
9834
|
+
previousModel as ArrayLike<number> & Iterable<number>,
|
|
9835
|
+
at * MOTION_DRAW_FLOATS + 16,
|
|
9836
|
+
);
|
|
9837
|
+
this.motionMeshes[at] = mesh;
|
|
9838
|
+
this.motionCount = at + 1;
|
|
9839
|
+
}
|
|
9840
|
+
|
|
9841
|
+
/**
|
|
9842
|
+
* The pipeline the motion pass draws with, built once.
|
|
9843
|
+
*
|
|
9844
|
+
* **The position buffer alone**, because that is all the shader reads and a mesh keeps one buffer
|
|
9845
|
+
* an attribute — so this binds buffer zero and nothing else, whatever else the mesh carries.
|
|
9846
|
+
*/
|
|
9847
|
+
private buildMotionPipeline(): void {
|
|
9848
|
+
if (this.motionLayout !== null) return;
|
|
9849
|
+
const { device } = this.surface;
|
|
9850
|
+
this.motionLayout = device.createBindGroupLayout({
|
|
9851
|
+
label: 'recon.motion.layout',
|
|
9852
|
+
entries: [
|
|
9853
|
+
{ binding: 0, visibility: 0x1, buffer: { type: 'uniform' as const } },
|
|
9854
|
+
{
|
|
9855
|
+
binding: 1,
|
|
9856
|
+
visibility: 0x1,
|
|
9857
|
+
buffer: { type: 'uniform' as const, hasDynamicOffset: true },
|
|
9858
|
+
},
|
|
9859
|
+
],
|
|
9860
|
+
});
|
|
9861
|
+
this.motionModule = device.createShaderModule({
|
|
9862
|
+
label: 'recon.motion',
|
|
9863
|
+
code: RECON_MOTION_WGSL,
|
|
9864
|
+
});
|
|
9865
|
+
this.motionPipelineLayout = device.createPipelineLayout({
|
|
9866
|
+
bindGroupLayouts: [this.motionLayout],
|
|
9867
|
+
});
|
|
9868
|
+
}
|
|
9869
|
+
|
|
9870
|
+
/** The pipeline for one vertex stride, built on first sight of it. */
|
|
9871
|
+
private motionPipelineFor(stride: number): GPURenderPipeline | null {
|
|
9872
|
+
const held = this.motionPipelines.get(stride);
|
|
9873
|
+
if (held !== undefined) return held;
|
|
9874
|
+
const module = this.motionModule;
|
|
9875
|
+
const layout = this.motionPipelineLayout;
|
|
9876
|
+
if (module === null || layout === null) return null;
|
|
9877
|
+
const { device } = this.surface;
|
|
9878
|
+
const pipeline = device.createRenderPipeline({
|
|
9879
|
+
label: `recon.motion.${String(stride)}`,
|
|
9880
|
+
layout,
|
|
9881
|
+
vertex: {
|
|
9882
|
+
module,
|
|
9883
|
+
entryPoint: 'motionVert',
|
|
9884
|
+
/* Positions are the layout's first attribute and are never optional, so they are at offset
|
|
9885
|
+
zero of every stride this can be handed. */
|
|
9886
|
+
buffers: [
|
|
9887
|
+
{
|
|
9888
|
+
arrayStride: stride,
|
|
9889
|
+
attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }],
|
|
9890
|
+
},
|
|
9891
|
+
],
|
|
9892
|
+
},
|
|
9893
|
+
fragment: { module, entryPoint: 'motionFrag', targets: [{ format: MOTION_TARGET_FORMAT }] },
|
|
9894
|
+
/*
|
|
9895
|
+
* **The scene's own winding**, because this draws the scene's geometry through the scene's
|
|
9896
|
+
* clip transform — the product the renderer hands over already carries the negation that
|
|
9897
|
+
* `naga` writes into every generated vertex stage, so the faces come out the way they did
|
|
9898
|
+
* and a `cw` front face here would draw the inside of every mesh. §3 row 57.
|
|
9899
|
+
*/
|
|
9900
|
+
primitive: { topology: 'triangle-list', cullMode: 'back' },
|
|
9901
|
+
depthStencil: {
|
|
9902
|
+
format: DEPTH_FORMAT,
|
|
9903
|
+
/*
|
|
9904
|
+
* **Tested and not written.** The frame's depth is finished; this pass only wants the
|
|
9905
|
+
* pixels the scene actually kept, and `DEPTH_COMPARE_EQUAL` is the comparison that admits
|
|
9906
|
+
* exactly them — the same transform on the same positions gives the same depth, which is
|
|
9907
|
+
* why the rasterisation matrix is handed over whole rather than rebuilt here.
|
|
9908
|
+
*
|
|
9909
|
+
* **And the equality is not fragile here**, which was worth measuring rather than
|
|
9910
|
+
* assuming: this driver contracts multiply-adds (§3 row 107) and two shader modules can
|
|
9911
|
+
* contract a matrix product differently, so an equality across them could have admitted
|
|
9912
|
+
* only some of a surface. Run with `greater-equal` instead, the ghost measured the same
|
|
9913
|
+
* to the pixel — 5,942 either way — so nothing is being dropped.
|
|
9914
|
+
*/
|
|
9915
|
+
depthWriteEnabled: false,
|
|
9916
|
+
depthCompare: DEPTH_COMPARE_EQUAL,
|
|
9917
|
+
},
|
|
9918
|
+
});
|
|
9919
|
+
this.motionPipelines.set(stride, pipeline);
|
|
9920
|
+
return pipeline;
|
|
9921
|
+
}
|
|
9922
|
+
|
|
9923
|
+
/**
|
|
9924
|
+
* Where every mover was last frame, into the motion target.
|
|
9925
|
+
*
|
|
9926
|
+
* **After everything that draws and before the resolve reads it.** The frame's depth has to be
|
|
9927
|
+
* finished for the equality test to mean anything, and the resolve is what consumes the result.
|
|
9928
|
+
*/
|
|
9929
|
+
private runMotion(encoder: GPUCommandEncoder): void {
|
|
9930
|
+
const target = this.reconMotionView;
|
|
9931
|
+
const depth = this.depthView;
|
|
9932
|
+
if (!this.reconstructing || target === null || depth === null) return;
|
|
9933
|
+
const { device } = this.surface;
|
|
9934
|
+
this.buildMotionPipeline();
|
|
9935
|
+
const layout = this.motionLayout;
|
|
9936
|
+
if (layout === null) return;
|
|
9937
|
+
|
|
9938
|
+
/*
|
|
9939
|
+
* **Cleared every frame, even when nothing moved.** A texel left from last frame is a flag
|
|
9940
|
+
* saying "a draw wrote this", and the resolve would take a motion belonging to a surface that
|
|
9941
|
+
* is no longer there. The clear is the pass's own load operation, so an empty queue still runs
|
|
9942
|
+
* it — which is why this is not returned from before the pass is opened.
|
|
9943
|
+
*/
|
|
9944
|
+
const pass = encoder.beginRenderPass({
|
|
9945
|
+
label: 'recon.motion',
|
|
9946
|
+
timestampWrites: this.gpuTimer.writesFor(),
|
|
9947
|
+
colorAttachments: [
|
|
9948
|
+
{ view: target, loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 0] },
|
|
9949
|
+
],
|
|
9950
|
+
depthStencilAttachment: {
|
|
9951
|
+
view: depth,
|
|
9952
|
+
depthLoadOp: 'load',
|
|
9953
|
+
depthStoreOp: 'store',
|
|
9954
|
+
depthReadOnly: false,
|
|
9955
|
+
},
|
|
9956
|
+
});
|
|
9957
|
+
|
|
9958
|
+
const count = this.motionCount;
|
|
9959
|
+
if (count > 0) {
|
|
9960
|
+
const bytes = count * MOTION_DRAW_STRIDE;
|
|
9961
|
+
if (this.motionDrawCapacity < count) {
|
|
9962
|
+
this.motionDrawBuffer?.destroy();
|
|
9963
|
+
this.motionDrawCapacity = Math.max(count, this.motionDrawCapacity * 2, 16);
|
|
9964
|
+
this.motionDrawBuffer = device.createBuffer({
|
|
9965
|
+
label: 'recon.motion.draws',
|
|
9966
|
+
size: this.motionDrawCapacity * MOTION_DRAW_STRIDE,
|
|
9967
|
+
usage: 0x40 | 0x8, // UNIFORM | COPY_DST
|
|
9968
|
+
});
|
|
9969
|
+
this.motionDrawStaging = new Uint8Array(this.motionDrawCapacity * MOTION_DRAW_STRIDE);
|
|
9970
|
+
this.motionGroup = null;
|
|
9971
|
+
}
|
|
9972
|
+
this.motionFrameBuffer ??= device.createBuffer({
|
|
9973
|
+
label: 'recon.motion.frame',
|
|
9974
|
+
size: MOTION_FRAME_FLOATS * 4,
|
|
9975
|
+
usage: 0x40 | 0x8,
|
|
9976
|
+
});
|
|
9977
|
+
this.motionGroup ??= device.createBindGroup({
|
|
9978
|
+
label: 'recon.motion.group',
|
|
9979
|
+
layout,
|
|
9980
|
+
entries: [
|
|
9981
|
+
{ binding: 0, resource: { buffer: this.motionFrameBuffer } },
|
|
9982
|
+
{
|
|
9983
|
+
binding: 1,
|
|
9984
|
+
resource: { buffer: this.motionDrawBuffer as GPUBuffer, size: MOTION_DRAW_STRIDE },
|
|
9985
|
+
},
|
|
9986
|
+
],
|
|
9987
|
+
});
|
|
9988
|
+
|
|
9989
|
+
/* The scene's clip transform, with the generated stages' y negation folded in. */
|
|
9990
|
+
mat4.multiply(this.motionRaster, MOTION_CLIP_FLIP, this.viewProj ?? this.correctedViewProj);
|
|
9991
|
+
this.motionFrameStaging.set(this.motionRaster as Float32Array, 0);
|
|
9992
|
+
this.motionFrameStaging.set(this.frameRawViewProj as Float32Array, 16);
|
|
9993
|
+
this.motionFrameStaging.set(this.reconPreviousRawViewProj as Float32Array, 32);
|
|
9994
|
+
device.queue.writeBuffer(this.motionFrameBuffer, 0, this.motionFrameStaging);
|
|
9995
|
+
|
|
9996
|
+
const draws = new Float32Array(this.motionDrawStaging.buffer);
|
|
9997
|
+
for (let i = 0; i < count; i += 1) {
|
|
9998
|
+
draws.set(
|
|
9999
|
+
this.motionMatrices.subarray(i * MOTION_DRAW_FLOATS, (i + 1) * MOTION_DRAW_FLOATS),
|
|
10000
|
+
(i * MOTION_DRAW_STRIDE) / 4,
|
|
10001
|
+
);
|
|
10002
|
+
}
|
|
10003
|
+
device.queue.writeBuffer(
|
|
10004
|
+
this.motionDrawBuffer as GPUBuffer,
|
|
10005
|
+
0,
|
|
10006
|
+
this.motionDrawStaging,
|
|
10007
|
+
0,
|
|
10008
|
+
bytes,
|
|
10009
|
+
);
|
|
10010
|
+
|
|
10011
|
+
let bound: GPURenderPipeline | null = null;
|
|
10012
|
+
for (let i = 0; i < count; i += 1) {
|
|
10013
|
+
const mesh = this.motionMeshes[i];
|
|
10014
|
+
if (mesh === undefined) continue;
|
|
10015
|
+
const pipeline = this.motionPipelineFor(mesh.vertexStride);
|
|
10016
|
+
if (pipeline === null) continue;
|
|
10017
|
+
if (pipeline !== bound) {
|
|
10018
|
+
pass.setPipeline(pipeline);
|
|
10019
|
+
bound = pipeline;
|
|
10020
|
+
}
|
|
10021
|
+
pass.setBindGroup(0, this.motionGroup, [i * MOTION_DRAW_STRIDE]);
|
|
10022
|
+
pass.setVertexBuffer(0, mesh.vertexBuffers[0] as GPUBuffer);
|
|
10023
|
+
pass.setIndexBuffer(mesh.indexBuffer, 'uint32');
|
|
10024
|
+
pass.drawIndexed(mesh.indexCount);
|
|
10025
|
+
}
|
|
10026
|
+
}
|
|
10027
|
+
pass.end();
|
|
10028
|
+
}
|
|
10029
|
+
|
|
10030
|
+
/**
|
|
10031
|
+
* DriftTR tier 0: the jittered render at the render size into the picture at the output size.
|
|
10032
|
+
*
|
|
10033
|
+
* `recon/resolve.ts` is the reference this is held to and `scripts/recon-parity.mjs` compares the
|
|
10034
|
+
* two; nothing here decides anything the reference does not.
|
|
10035
|
+
*/
|
|
10036
|
+
private runReconstruction(encoder: GPUCommandEncoder): void {
|
|
10037
|
+
if (!this.reconstructing) return;
|
|
10038
|
+
const resolve = this.reconResolvePipeline;
|
|
10039
|
+
const sharpen = this.reconSharpenPipeline;
|
|
10040
|
+
const params = this.reconParams;
|
|
10041
|
+
const write = this.reconWrite;
|
|
10042
|
+
const group = this.reconGroups[write];
|
|
10043
|
+
const sharpenGroup = this.reconSharpenGroups[write];
|
|
10044
|
+
if (resolve === null || sharpen === null || params === null) return;
|
|
10045
|
+
if (group === null || sharpenGroup === null) return;
|
|
10046
|
+
const { device } = this.surface;
|
|
10047
|
+
const outputWidth = Math.max(1, this.surface.canvas.width);
|
|
10048
|
+
const outputHeight = Math.max(1, this.surface.canvas.height);
|
|
10049
|
+
|
|
10050
|
+
/*
|
|
10051
|
+
* **The unjittered matrices**, which is what the reference's own header insists on: the
|
|
10052
|
+
* history stands for the scene as it would have been drawn with no jitter, and the motion the
|
|
10053
|
+
* resolve derives carries none. `frameRawViewProj` is the camera's own view-projection, in
|
|
10054
|
+
* OpenGL's depth convention — which is the convention the shader's accessor converts the
|
|
10055
|
+
* renderer's reversed buffer into.
|
|
10056
|
+
*/
|
|
10057
|
+
mat4.invert(this.reconInverseViewProj, this.frameRawViewProj);
|
|
10058
|
+
const f = this.reconFloats;
|
|
10059
|
+
const u = this.reconInts;
|
|
10060
|
+
u[0] = this.renderWidth;
|
|
10061
|
+
u[1] = this.renderHeight;
|
|
10062
|
+
u[2] = outputWidth;
|
|
10063
|
+
u[3] = outputHeight;
|
|
10064
|
+
f[4] = this.reconJitter[0] as number;
|
|
10065
|
+
f[5] = this.reconJitter[1] as number;
|
|
10066
|
+
f[6] = this.reconPreviousJitter[0] as number;
|
|
10067
|
+
f[7] = this.reconPreviousJitter[1] as number;
|
|
10068
|
+
f.set(this.reconInverseViewProj as Float32Array, 8);
|
|
10069
|
+
f.set(this.reconPreviousRawViewProj as Float32Array, 24);
|
|
10070
|
+
f.set(this.reconPreviousInverseViewProj as Float32Array, 40);
|
|
10071
|
+
f.set(this.reconEye, 56);
|
|
10072
|
+
f.set(this.reconPreviousEye, 60);
|
|
10073
|
+
f[64] = DEFAULT_RECON_QUALITY.alpha;
|
|
10074
|
+
f[65] = DEFAULT_RECON_QUALITY.sharpen;
|
|
10075
|
+
f[66] = DEFAULT_RECON_QUALITY.clampGamma;
|
|
10076
|
+
u[67] = this.reconHasHistory ? 1 : 0;
|
|
10077
|
+
f[68] = DEFAULT_DISOCCLUSION.depthTolerance;
|
|
10078
|
+
f[69] = DEFAULT_DISOCCLUSION.motionScale;
|
|
10079
|
+
f[70] = DEFAULT_DISOCCLUSION.normalFloor;
|
|
10080
|
+
f[71] = DEFAULT_DISOCCLUSION.normalCeiling;
|
|
10081
|
+
device.queue.writeBuffer(params, 0, this.reconStaging);
|
|
10082
|
+
|
|
10083
|
+
const groupsX = Math.ceil(outputWidth / RECON_WORKGROUP);
|
|
10084
|
+
const groupsY = Math.ceil(outputHeight / RECON_WORKGROUP);
|
|
10085
|
+
const pass = encoder.beginComputePass({
|
|
10086
|
+
label: 'recon.resolve',
|
|
10087
|
+
timestampWrites: this.gpuTimer.writesFor(),
|
|
10088
|
+
});
|
|
10089
|
+
pass.setPipeline(resolve);
|
|
10090
|
+
pass.setBindGroup(0, group);
|
|
10091
|
+
pass.dispatchWorkgroups(groupsX, groupsY, 1);
|
|
10092
|
+
/*
|
|
10093
|
+
* **The sharpen in the same pass, and that is legal because it reads a different texture.**
|
|
10094
|
+
* The resolve wrote history `write`; the sharpen reads it and writes the shown picture. Two
|
|
10095
|
+
* dispatches in one compute pass see each other's writes in recording order, and the rule
|
|
10096
|
+
* §3 row 52 is about — one buffer writable and readable in a single synchronisation scope —
|
|
10097
|
+
* does not bite, because no resource here is both.
|
|
10098
|
+
*/
|
|
10099
|
+
pass.setPipeline(sharpen);
|
|
10100
|
+
pass.setBindGroup(0, sharpenGroup);
|
|
10101
|
+
pass.dispatchWorkgroups(groupsX, groupsY, 1);
|
|
10102
|
+
pass.end();
|
|
10103
|
+
|
|
10104
|
+
/* Last frame's depth, for the disocclusion test and the normals derived from it. */
|
|
10105
|
+
const depth = this.resolvedDepth;
|
|
10106
|
+
const previous = this.reconPreviousDepth;
|
|
10107
|
+
if (depth !== null && previous !== null) {
|
|
10108
|
+
encoder.copyTextureToTexture({ texture: depth }, { texture: previous }, [
|
|
10109
|
+
previous.width,
|
|
10110
|
+
previous.height,
|
|
10111
|
+
1,
|
|
10112
|
+
]);
|
|
10113
|
+
}
|
|
10114
|
+
|
|
10115
|
+
this.reconWrite = 1 - write;
|
|
10116
|
+
this.reconHasHistory = true;
|
|
10117
|
+
mat4.copy(this.reconPreviousRawViewProj, this.frameRawViewProj);
|
|
10118
|
+
mat4.copy(this.reconPreviousInverseViewProj, this.reconInverseViewProj);
|
|
10119
|
+
this.reconPreviousEye.set(this.reconEye);
|
|
10120
|
+
}
|
|
10121
|
+
|
|
9165
10122
|
private resolveDepth(encoder: GPUCommandEncoder): void {
|
|
9166
10123
|
const target = this.resolvedDepthView;
|
|
9167
10124
|
const source = this.depthView;
|
|
@@ -9204,6 +10161,12 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9204
10161
|
* must still write, or the history it leaves for the next frame is whatever was in the texture.
|
|
9205
10162
|
*/
|
|
9206
10163
|
private temporalResolve(encoder: GPUCommandEncoder): void {
|
|
10164
|
+
/*
|
|
10165
|
+
* **One guard, not two.** A second test on `reconstructing` here read as belt and braces and
|
|
10166
|
+
* was worse than nothing: it and the `!reconstructing` in `temporalJittering`'s own condition
|
|
10167
|
+
* each covered the other, so breaking either one changed no behaviour and no test could see
|
|
10168
|
+
* it. `temporalJittering` is where the choice is made, and it is made once.
|
|
10169
|
+
*/
|
|
9207
10170
|
if (!this.temporalJittering) return;
|
|
9208
10171
|
const write = this.taaWrite;
|
|
9209
10172
|
const target = this.taaViews[write];
|
|
@@ -9214,8 +10177,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9214
10177
|
const f = this.taaFloats;
|
|
9215
10178
|
const at = (name: string): number => this.postField(TAA_FRAG_FIELDS, name);
|
|
9216
10179
|
f.set(this.reprojection, at('uReprojection'));
|
|
9217
|
-
f[at('uTexel')] = 1 /
|
|
9218
|
-
f[at('uTexel') + 1] = 1 /
|
|
10180
|
+
f[at('uTexel')] = 1 / this.renderWidth;
|
|
10181
|
+
f[at('uTexel') + 1] = 1 / this.renderHeight;
|
|
9219
10182
|
f[at('uHistoryBlend')] = this.temporalHistoryUsable ? TEMPORAL_HISTORY_BLEND : 0;
|
|
9220
10183
|
device.queue.writeBuffer(this.taaUniforms, 0, this.taaStaging);
|
|
9221
10184
|
|
|
@@ -9296,8 +10259,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9296
10259
|
}
|
|
9297
10260
|
if (!this.frameCameraSeen) return;
|
|
9298
10261
|
const { device } = this.surface;
|
|
9299
|
-
|
|
9300
|
-
const
|
|
10262
|
+
/* The scene's size: this marches through the picture's own texels, not the viewer's. */
|
|
10263
|
+
const width = this.renderWidth;
|
|
10264
|
+
const height = this.renderHeight;
|
|
9301
10265
|
const view = this.frameRawViewProj as Float32Array;
|
|
9302
10266
|
|
|
9303
10267
|
mat4.invert(this.ssrDepthToWorld, view);
|
|
@@ -9406,8 +10370,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9406
10370
|
}
|
|
9407
10371
|
if (!this.frameCameraSeen) return;
|
|
9408
10372
|
const { device } = this.surface;
|
|
9409
|
-
|
|
9410
|
-
const
|
|
10373
|
+
/* The scene's size: a mark is projected onto the depth this frame drew, at that depth's size. */
|
|
10374
|
+
const width = this.renderWidth;
|
|
10375
|
+
const height = this.renderHeight;
|
|
9411
10376
|
const view = this.frameRawViewProj as Float32Array;
|
|
9412
10377
|
|
|
9413
10378
|
mat4.invert(this.decalDepthToWorld, view);
|
|
@@ -9493,6 +10458,23 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9493
10458
|
}
|
|
9494
10459
|
const { device } = this.surface;
|
|
9495
10460
|
const options = this.oitReplayOptions;
|
|
10461
|
+
/*
|
|
10462
|
+
* **A refracting pane reads the frame behind it, and here that frame is finished.** So the
|
|
10463
|
+
* copy it reads is taken now, from the resolved scene, once for the whole set — the immediate
|
|
10464
|
+
* path takes it by ending the scene's pass, and that pass has already ended. Only when
|
|
10465
|
+
* something in the set refracts, because a copy of the frame is not free.
|
|
10466
|
+
*/
|
|
10467
|
+
if (this.translucentQueue.refracts && !this.refractSnapshotTaken) {
|
|
10468
|
+
const snapshot = this.refractSnapshot;
|
|
10469
|
+
const source = this.sceneColor;
|
|
10470
|
+
if (snapshot !== null && source !== null) {
|
|
10471
|
+
encoder.copyTextureToTexture({ texture: source }, { texture: snapshot }, [
|
|
10472
|
+
snapshot.width,
|
|
10473
|
+
snapshot.height,
|
|
10474
|
+
]);
|
|
10475
|
+
this.refractSnapshotTaken = true;
|
|
10476
|
+
}
|
|
10477
|
+
}
|
|
9496
10478
|
|
|
9497
10479
|
const buffers = [
|
|
9498
10480
|
{ mode: 'accum' as const, view: accum, clear: [0, 0, 0, 0] as const },
|
|
@@ -9524,6 +10506,12 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9524
10506
|
options.depthWrite = false;
|
|
9525
10507
|
options.depthLayer = draw.depthLayer;
|
|
9526
10508
|
options.tint = draw.tint as unknown as Vec3 | null;
|
|
10509
|
+
/* Replayed too, as the other backend replays it: a pane that refracts under sorted
|
|
10510
|
+
blending and stands clear under this effect reads as a bug in the effect. It did stand
|
|
10511
|
+
clear here until 2026-09-19. */
|
|
10512
|
+
options.refraction = draw.refraction;
|
|
10513
|
+
options.refractTint = draw.refractTint as unknown as Vec3 | undefined;
|
|
10514
|
+
options.thicknessM = draw.thicknessM;
|
|
9527
10515
|
this.drawTranslucentMesh(draw.mesh as GpuMesh, draw.model, draw.opacity, options);
|
|
9528
10516
|
});
|
|
9529
10517
|
this.oitReplaying = false;
|
|
@@ -9980,6 +10968,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
9980
10968
|
private takeRefractSnapshot(): boolean {
|
|
9981
10969
|
if (this.reflectionPassActive || this.probePassActive) return false;
|
|
9982
10970
|
if (this.refractSnapshotTaken) return true;
|
|
10971
|
+
/* The replay of an order-independent set runs after the scene's pass has ended, and `runOit`
|
|
10972
|
+
took the copy for it before it began; there is no pass here to end and reopen. */
|
|
10973
|
+
if (this.oitReplaying) return false;
|
|
9983
10974
|
const snapshot = this.refractSnapshot;
|
|
9984
10975
|
const source = this.sceneColor;
|
|
9985
10976
|
const encoder = this.encoder;
|
|
@@ -10139,7 +11130,22 @@ export class WebGPURenderer implements RendererApi {
|
|
|
10139
11130
|
* recorded so far writes the scene target, so this must come out as exactly one pass;
|
|
10140
11131
|
* anything else means a node declared a write it does not perform.
|
|
10141
11132
|
*/
|
|
10142
|
-
|
|
11133
|
+
/*
|
|
11134
|
+
* **Either scheduler, into the same records**, so nothing below knows which ran. The identifier
|
|
11135
|
+
* graph is the one that can also express the second pipeline's frame; running it here is what
|
|
11136
|
+
* shows it is a generalisation of this one rather than a different renderer — see
|
|
11137
|
+
* `quality.identifierGraph` for the captures that say so.
|
|
11138
|
+
*/
|
|
11139
|
+
const passes =
|
|
11140
|
+
this.flushSchedule === null
|
|
11141
|
+
? schedule(this.arena, this.pendingClearMask, liveOut, this.scheduledPasses)
|
|
11142
|
+
: scheduleFlush(
|
|
11143
|
+
this.arena,
|
|
11144
|
+
this.pendingClearMask,
|
|
11145
|
+
liveOut,
|
|
11146
|
+
this.flushSchedule,
|
|
11147
|
+
this.scheduledPasses,
|
|
11148
|
+
);
|
|
10143
11149
|
this.passesLastFlush = passes;
|
|
10144
11150
|
let discards = 0;
|
|
10145
11151
|
let clears = 0;
|
|
@@ -10261,6 +11267,14 @@ export class WebGPURenderer implements RendererApi {
|
|
|
10261
11267
|
once and ran against a discarded attachment that read zero everywhere — which is why
|
|
10262
11268
|
there is one of these and why this line is in it. */
|
|
10263
11269
|
(this.quality.temporalAa && this.quality.screenEffects) ||
|
|
11270
|
+
/*
|
|
11271
|
+
* **And so does a reconstruction, for the same reason, and it was missing for the same
|
|
11272
|
+
* reason.** Without it the resolve read a discarded attachment as zero — the far plane
|
|
11273
|
+
* reversed — so every surface stood at infinity: a turning camera reprojected correctly,
|
|
11274
|
+
* since a turn moves every depth alike, and a sliding one moved nothing, and the history
|
|
11275
|
+
* trailed every slide by the parallax it never saw.
|
|
11276
|
+
*/
|
|
11277
|
+
this.reconstructing ||
|
|
10264
11278
|
/*
|
|
10265
11279
|
* **The order-independent passes attach the depth and test against it**, so a frame running
|
|
10266
11280
|
* them needs it to survive the main pass. Without this it is discarded, those passes test
|
|
@@ -10614,6 +11628,15 @@ export class WebGPURenderer implements RendererApi {
|
|
|
10614
11628
|
this.flushRings();
|
|
10615
11629
|
framePass.end();
|
|
10616
11630
|
this.pass = null;
|
|
11631
|
+
/*
|
|
11632
|
+
* **The one window in the frame where every declaration is in and no render pass is open.**
|
|
11633
|
+
* A consumer declares its fields wherever in its own frame the objects live, so the earliest
|
|
11634
|
+
* the set is complete is after the last verb; and a compute pass cannot be recorded inside a
|
|
11635
|
+
* render pass, so the latest it can be recorded is before the composite opens its own. The
|
|
11636
|
+
* probes traced from it are read by the next frame's shading, which is what a probe volume
|
|
11637
|
+
* does anyway — it accumulates.
|
|
11638
|
+
*/
|
|
11639
|
+
this.runIndirectLight(this.encoder);
|
|
10617
11640
|
if (this.hasComposite) this.composite(this.encoder);
|
|
10618
11641
|
/* And where there is no composite there is no depth to read, so a frame that submitted marks
|
|
10619
11642
|
is told once rather than losing them quietly. See `refuseDecals`. */
|
|
@@ -10674,7 +11697,17 @@ export class WebGPURenderer implements RendererApi {
|
|
|
10674
11697
|
const width = Math.max(1, this.budgeted.width);
|
|
10675
11698
|
const height = Math.max(1, this.budgeted.height);
|
|
10676
11699
|
this.surface.configure(width, height);
|
|
10677
|
-
|
|
11700
|
+
/*
|
|
11701
|
+
* **The same split `beginFrame` makes, and it has to be made twice.** This path resized the
|
|
11702
|
+
* composite to the drawing buffer while the frame resized it to the render size, so the two
|
|
11703
|
+
* fought every time a canvas changed — and the reconstruction's bind groups, built here
|
|
11704
|
+
* against the previous scene depth, held a view of a texture this call had just destroyed.
|
|
11705
|
+
* The device's words for that are "destroyed texture used in a submit", on the next frame,
|
|
11706
|
+
* after a resize, which is as far from the cause as a message gets.
|
|
11707
|
+
*/
|
|
11708
|
+
const render = this.sceneSize();
|
|
11709
|
+
this.ensureComposite(render.width, render.height);
|
|
11710
|
+
this.ensureReconstruction(render.width, render.height, width, height);
|
|
10678
11711
|
this.ensureReflection(width, height);
|
|
10679
11712
|
}
|
|
10680
11713
|
|
|
@@ -10749,6 +11782,206 @@ export class WebGPURenderer implements RendererApi {
|
|
|
10749
11782
|
this.occlusion?.addOccluder(min as Float32Array, max as Float32Array, model);
|
|
10750
11783
|
}
|
|
10751
11784
|
|
|
11785
|
+
/**
|
|
11786
|
+
* Declare a baked distance field that indirect light may be traced against.
|
|
11787
|
+
*
|
|
11788
|
+
* **The seam is `addOccluder`'s, and for `addOccluder`'s reason.** A distance field is the shape
|
|
11789
|
+
* of a thing it is safe to be *traced against*, and only a consumer knows which of its objects
|
|
11790
|
+
* that is true of — the walls and the terrain, not the leaves, not the water, not the thing it
|
|
11791
|
+
* is about to delete. A renderer that derived the field from whatever was drawn would trace
|
|
11792
|
+
* against the foliage and light the room with a hedge.
|
|
11793
|
+
*
|
|
11794
|
+
* **Declared anywhere in the frame and composed at its end**, which is the one window where
|
|
11795
|
+
* every declaration is in and the encoder has no render pass open. Cleared at `beginFrame` with
|
|
11796
|
+
* the decal and reflection queues, so a consumer redeclares every frame and a field it stops
|
|
11797
|
+
* declaring stops lighting.
|
|
11798
|
+
*
|
|
11799
|
+
* The field is in the model matrix's own space and the matrix must carry uniform scale only —
|
|
11800
|
+
* `composeGlobalField` gives the reason: a distance is not preserved by a non-uniform scale, and
|
|
11801
|
+
* the factor to correct by depends on the direction to the nearest surface, which is exactly
|
|
11802
|
+
* what a distance field does not record.
|
|
11803
|
+
*
|
|
11804
|
+
* **`albedo` is what colour this placement's surface is, and it decides what the bounce carries.**
|
|
11805
|
+
* What leaves a surface is the light arriving times its albedo, so a ray that lands on a wall
|
|
11806
|
+
* and does not know its colour returns the same answer for a red wall and a white one — measured
|
|
11807
|
+
* on `demo/dev/bounce.html` before this argument existed. It is a placement's rather than a
|
|
11808
|
+
* field's, because the same baked shape is placed many times and two pillars cut from one mould
|
|
11809
|
+
* may be painted differently. White where it is left out, so every existing caller is unchanged.
|
|
11810
|
+
*/
|
|
11811
|
+
addDistanceField(field: FieldSource, model: ReadonlyMat4, albedo?: ArrayLike<number>): void {
|
|
11812
|
+
this.distanceFields.record(field, model, albedo);
|
|
11813
|
+
}
|
|
11814
|
+
|
|
11815
|
+
/** Forget the fields declared this frame, for a consumer that rebuilds its list mid-frame. */
|
|
11816
|
+
clearDistanceFields(): void {
|
|
11817
|
+
this.distanceFields.reset();
|
|
11818
|
+
}
|
|
11819
|
+
|
|
11820
|
+
/**
|
|
11821
|
+
* Compose what the frame declared, or nothing at all.
|
|
11822
|
+
*
|
|
11823
|
+
* **Nothing at all is the ordinary case and must cost nothing.** `quality.indirectLight` is off
|
|
11824
|
+
* by default and a frame that declared no fields has nothing to compose, so both are answered
|
|
11825
|
+
* before the composer exists — which is what keeps the eighteen scenes byte-identical with the
|
|
11826
|
+
* flag off rather than merely close.
|
|
11827
|
+
*/
|
|
11828
|
+
private runIndirectLight(encoder: GPUCommandEncoder): void {
|
|
11829
|
+
/*
|
|
11830
|
+
* **The flag is read here and in no other place.** It used to be read again by each of the two
|
|
11831
|
+
* steps, and a perturbation proved neither of those could fail: the bake needs a composed
|
|
11832
|
+
* field, and only the composition makes one. A guard another guard covers is a line no test
|
|
11833
|
+
* can hold — this session has had to remove three of them.
|
|
11834
|
+
*/
|
|
11835
|
+
if (!this.quality.indirectLight) return;
|
|
11836
|
+
this.composeDistanceField(encoder);
|
|
11837
|
+
this.bakeIndirectProbes(encoder);
|
|
11838
|
+
}
|
|
11839
|
+
|
|
11840
|
+
private composeDistanceField(encoder: GPUCommandEncoder): void {
|
|
11841
|
+
/*
|
|
11842
|
+
* **An empty frame still reaches the composer, and a test had to find that.** The early return
|
|
11843
|
+
* used to cover the whole method, so a frame that declared nothing left `composed` true from
|
|
11844
|
+
* the frame before — and the bake traced against a field built out of geometry the consumer
|
|
11845
|
+
* had stopped declaring. What the guard is actually for is not *building* a composer for a
|
|
11846
|
+
* profile that never declares anything, so that is all it now guards.
|
|
11847
|
+
*/
|
|
11848
|
+
if (this.fieldComposer === null) {
|
|
11849
|
+
if (this.distanceFields.length === 0) return;
|
|
11850
|
+
this.fieldComposer = new FieldComposer(this.surface.device, DEFAULT_FIELD_COMPOSE);
|
|
11851
|
+
}
|
|
11852
|
+
this.fieldComposer.compose(encoder, this.distanceFields, this.frameEye);
|
|
11853
|
+
}
|
|
11854
|
+
|
|
11855
|
+
/** The world field this frame composed, or null where none was. For a pass that marches it. */
|
|
11856
|
+
get distanceField(): ComposedField | null {
|
|
11857
|
+
return this.fieldComposer?.field ?? null;
|
|
11858
|
+
}
|
|
11859
|
+
|
|
11860
|
+
/**
|
|
11861
|
+
* Refresh this frame's share of the probe grid from the world field, or do nothing at all.
|
|
11862
|
+
*
|
|
11863
|
+
* **Doing nothing is the ordinary case and must cost nothing**, which is why every condition is
|
|
11864
|
+
* answered before the baker exists: a frame may have composed no field — including a frame that
|
|
11865
|
+
* declared none after a frame that did, where the composer exists and holds nothing — and a
|
|
11866
|
+
* profile may have asked for no probe array. With either false this returns having allocated
|
|
11867
|
+
* nothing and recorded nothing, which is what keeps the scenes byte-identical.
|
|
11868
|
+
*
|
|
11869
|
+
* **The grid is the declared one, or one fitted to the fields themselves.** A consumer that
|
|
11870
|
+
* placed its probes always wins. One that did not gets a grid around whatever it said its light
|
|
11871
|
+
* may be traced against — which is the right extent rather than a convenient one, because a grid
|
|
11872
|
+
* sized to everything drawn would spend its probes on the inside of the sky.
|
|
11873
|
+
*/
|
|
11874
|
+
private bakeIndirectProbes(encoder: GPUCommandEncoder): void {
|
|
11875
|
+
const field = this.fieldComposer?.field;
|
|
11876
|
+
if (field === undefined || field === null) return;
|
|
11877
|
+
|
|
11878
|
+
/*
|
|
11879
|
+
* **The grid is settled before the array is read, and the first version read it first.**
|
|
11880
|
+
* Fitting a grid calls `setProbeGrid`, which reallocates the array for the new layer count —
|
|
11881
|
+
* so a baker built from the array captured a line earlier held the one-layer array the
|
|
11882
|
+
* renderer starts with, and asked it for a view of layer 1. The device refused in those words.
|
|
11883
|
+
* It is the trap `setProbeGrid` already documents for the flat bind groups: a group holds
|
|
11884
|
+
* whatever texture existed when it was built, for ever, with no error anywhere until the
|
|
11885
|
+
* sizes disagree.
|
|
11886
|
+
*/
|
|
11887
|
+
const grid = this.indirectGrid();
|
|
11888
|
+
if (grid === null) return;
|
|
11889
|
+
const array = this.probeArray;
|
|
11890
|
+
const view = this.probeView;
|
|
11891
|
+
if (array === null || view === null) return;
|
|
11892
|
+
|
|
11893
|
+
if (this.probeBaker !== null && this.bakerArray !== array) {
|
|
11894
|
+
this.probeBaker.dispose();
|
|
11895
|
+
this.probeBaker = null;
|
|
11896
|
+
}
|
|
11897
|
+
this.probeBaker ??= new ProbeBaker(this.surface.device, {
|
|
11898
|
+
array,
|
|
11899
|
+
view,
|
|
11900
|
+
sampler: this.probeSampler,
|
|
11901
|
+
edge: this.probeEdge,
|
|
11902
|
+
format: this.pipelines.format,
|
|
11903
|
+
});
|
|
11904
|
+
this.bakerArray = array;
|
|
11905
|
+
/*
|
|
11906
|
+
* **Whether the scene rasterised this grid first**, which decides two things: that the
|
|
11907
|
+
* roughness chain is already valid and must not be overwritten by a diffuse map, and that the
|
|
11908
|
+
* probes hold direct light for the trace to bounce. A grid that was never rasterised is traced
|
|
11909
|
+
* anyway and converges on whatever it started with — see `ProbeBaker`'s header.
|
|
11910
|
+
*/
|
|
11911
|
+
/*
|
|
11912
|
+
* The frame's own sun, taken from the environment `bindMeshPass` was given rather than from a
|
|
11913
|
+
* capture — which is what makes a bounce follow a light that moved. A frame that drew no mesh
|
|
11914
|
+
* has no environment and traces with the sun off, which is the honest answer for a frame that
|
|
11915
|
+
* never said what was lighting it.
|
|
11916
|
+
*/
|
|
11917
|
+
const env = this.frameEnv;
|
|
11918
|
+
this.probeBaker.bake(encoder, field, grid, this.probeBaked, {
|
|
11919
|
+
direction: env?.directionalDir ?? NO_SUN,
|
|
11920
|
+
colour: env?.directionalColor ?? NO_SUN,
|
|
11921
|
+
/* What a ray that left the world finds. The frame's own ambient, not a capture. */
|
|
11922
|
+
sky: env?.ambient ?? NO_SUN,
|
|
11923
|
+
});
|
|
11924
|
+
/*
|
|
11925
|
+
* **The bake is what fills the layers now, so it is what marks them filled.** `probeBaked`
|
|
11926
|
+
* gates the whole array, and a grid nothing rasterised would otherwise never be sampled — the
|
|
11927
|
+
* lit pass would read the hemispheric gradient and the traced light would go nowhere.
|
|
11928
|
+
*/
|
|
11929
|
+
if (this.probeBaker.complete) {
|
|
11930
|
+
for (let layer = 0; layer < grid.layers; layer += 1) this.markProbeFilled(layer);
|
|
11931
|
+
}
|
|
11932
|
+
}
|
|
11933
|
+
|
|
11934
|
+
/** The declared grid, or one fitted to this frame's fields, or null where neither is possible. */
|
|
11935
|
+
private indirectGrid(): ProbeGrid | null {
|
|
11936
|
+
if (this.probes !== null) return this.probes;
|
|
11937
|
+
if (!distanceFieldBounds(this.distanceFields, this.fieldBoundsMin, this.fieldBoundsMax)) {
|
|
11938
|
+
return null;
|
|
11939
|
+
}
|
|
11940
|
+
const fitted = fitProbeGrid(
|
|
11941
|
+
{ min: this.fieldBoundsMin, max: this.fieldBoundsMax },
|
|
11942
|
+
INDIRECT_PROBE_SPACING,
|
|
11943
|
+
MAX_ENV_PROBES,
|
|
11944
|
+
);
|
|
11945
|
+
if (!this.setProbeGrid(fitted)) {
|
|
11946
|
+
if (!this.indirectGridRefused) {
|
|
11947
|
+
this.indirectGridRefused = true;
|
|
11948
|
+
console.warn(
|
|
11949
|
+
'[driftengine] indirect light needs a probe grid and this profile has no probe array, ' +
|
|
11950
|
+
'so nothing is traced. Ask for a quality profile with the environment probe enabled.',
|
|
11951
|
+
);
|
|
11952
|
+
}
|
|
11953
|
+
return null;
|
|
11954
|
+
}
|
|
11955
|
+
return this.probes;
|
|
11956
|
+
}
|
|
11957
|
+
|
|
11958
|
+
/**
|
|
11959
|
+
* The device milliseconds the last composed field took, or null where nothing measured it.
|
|
11960
|
+
*
|
|
11961
|
+
* One frame behind, because a timestamp is read back and a readback that blocked would be the
|
|
11962
|
+
* measurement changing what it measures. Null also where no field has ever been composed, and
|
|
11963
|
+
* where the device was not asked for `timestamp-query`.
|
|
11964
|
+
*/
|
|
11965
|
+
get distanceFieldMs(): number | null {
|
|
11966
|
+
return this.fieldComposer?.composeMs ?? null;
|
|
11967
|
+
}
|
|
11968
|
+
|
|
11969
|
+
/**
|
|
11970
|
+
* The device milliseconds the last traced probe refresh took, or null where nothing measured it.
|
|
11971
|
+
*
|
|
11972
|
+
* **A refresh rather than a grid**, which is `PROBES_PER_FRAME` probes and is what a frame
|
|
11973
|
+
* actually pays. One frame behind, for the reason `distanceFieldMs` gives.
|
|
11974
|
+
*/
|
|
11975
|
+
get indirectBakeMs(): number | null {
|
|
11976
|
+
return this.probeBaker?.bakeMs ?? null;
|
|
11977
|
+
}
|
|
11978
|
+
|
|
11979
|
+
/** Ask for the last frame's figures, once the frame that recorded them has been submitted. */
|
|
11980
|
+
readDistanceFieldTimings(): void {
|
|
11981
|
+
this.fieldComposer?.readTimings();
|
|
11982
|
+
this.probeBaker?.readTimings();
|
|
11983
|
+
}
|
|
11984
|
+
|
|
10752
11985
|
/**
|
|
10753
11986
|
* Whether a mesh is entirely behind the occluders declared this frame.
|
|
10754
11987
|
*
|
|
@@ -10791,11 +12024,13 @@ export class WebGPURenderer implements RendererApi {
|
|
|
10791
12024
|
pass: GPURenderPassEncoder | null;
|
|
10792
12025
|
outputTransform: number;
|
|
10793
12026
|
outputExposure: number;
|
|
12027
|
+
jitter: Float32Array;
|
|
10794
12028
|
} = {
|
|
10795
12029
|
backend: 'webgpu',
|
|
10796
12030
|
pass: null,
|
|
10797
12031
|
outputTransform: 0,
|
|
10798
12032
|
outputExposure: 1,
|
|
12033
|
+
jitter: this.passJitter,
|
|
10799
12034
|
};
|
|
10800
12035
|
|
|
10801
12036
|
/**
|
|
@@ -10823,11 +12058,76 @@ export class WebGPURenderer implements RendererApi {
|
|
|
10823
12058
|
*/
|
|
10824
12059
|
private preparingPasses = 0;
|
|
10825
12060
|
/** Mutated in place, because `beginFrame` is a per-frame path. See `PassDefinition.prepare`. */
|
|
10826
|
-
private readonly prepareContext: {
|
|
12061
|
+
private readonly prepareContext: {
|
|
12062
|
+
backend: 'webgpu';
|
|
12063
|
+
encoder: GPUCommandEncoder | null;
|
|
12064
|
+
environment: PassEnvironment | null;
|
|
12065
|
+
distanceField: ComposedField | null;
|
|
12066
|
+
jitter: Float32Array;
|
|
12067
|
+
} = {
|
|
10827
12068
|
backend: 'webgpu',
|
|
10828
12069
|
encoder: null,
|
|
12070
|
+
environment: null,
|
|
12071
|
+
distanceField: null,
|
|
12072
|
+
jitter: this.passJitter,
|
|
12073
|
+
};
|
|
12074
|
+
/**
|
|
12075
|
+
* The object handed to a pass as `PrepareContext.environment`, rebuilt only when it changes.
|
|
12076
|
+
*
|
|
12077
|
+
* **Mutated in place for the reason the context above is**: this runs once a frame for every
|
|
12078
|
+
* registered pass, and the house rule about per-frame allocation binds the seam as much as the
|
|
12079
|
+
* verbs. The view is the only field that can change identity, and it changes when the grid's
|
|
12080
|
+
* layer count does.
|
|
12081
|
+
*/
|
|
12082
|
+
private readonly passEnvironment: {
|
|
12083
|
+
view: GPUTextureView | null;
|
|
12084
|
+
sampler: GPUSampler | null;
|
|
12085
|
+
edge: number;
|
|
12086
|
+
maxLod: number;
|
|
12087
|
+
irradianceLevel: number;
|
|
12088
|
+
irradiance: boolean;
|
|
12089
|
+
layers: number;
|
|
12090
|
+
} = {
|
|
12091
|
+
view: null,
|
|
12092
|
+
sampler: null,
|
|
12093
|
+
edge: 0,
|
|
12094
|
+
maxLod: 0,
|
|
12095
|
+
irradianceLevel: 0,
|
|
12096
|
+
irradiance: false,
|
|
12097
|
+
layers: 0,
|
|
10829
12098
|
};
|
|
10830
12099
|
|
|
12100
|
+
/**
|
|
12101
|
+
* The frame's reconstruction jitter: one step of the sequence a frame, taken before any pass
|
|
12102
|
+
* prepares.
|
|
12103
|
+
*
|
|
12104
|
+
* **Here and not in `bindMeshPass`, for two reasons that were both defects.** A registered pass
|
|
12105
|
+
* prepares at `beginFrame`, before any mesh pass is bound, so a jitter chosen there could not
|
|
12106
|
+
* reach it — and the GPU-driven pipeline draws its whole world in `prepare`, unjittered, into a
|
|
12107
|
+
* resolve that un-jitters everything. And a frame that bound its mesh pass twice took two steps
|
|
12108
|
+
* of the sequence and resolved against the second.
|
|
12109
|
+
*
|
|
12110
|
+
* The sizes are the frame's by now: `ensureReconstruction` has run.
|
|
12111
|
+
*/
|
|
12112
|
+
private settleReconJitter(): void {
|
|
12113
|
+
if (!this.reconstructionWanted) {
|
|
12114
|
+
this.passJitter[0] = 0;
|
|
12115
|
+
this.passJitter[1] = 0;
|
|
12116
|
+
return;
|
|
12117
|
+
}
|
|
12118
|
+
this.reconFrameIndex += 1;
|
|
12119
|
+
this.reconPreviousJitter.set(this.reconJitter);
|
|
12120
|
+
jitterOffsetFor(
|
|
12121
|
+
this.reconFrameIndex,
|
|
12122
|
+
reconJitterPhases(this.renderWidth, this.surface.canvas.width),
|
|
12123
|
+
this.reconJitter,
|
|
12124
|
+
);
|
|
12125
|
+
/* A render pixel is two over the render's size of the clip square; y upward, as the resolve's
|
|
12126
|
+
rows and the camera's own matrix both run. */
|
|
12127
|
+
this.passJitter[0] = (2 * (this.reconJitter[0] as number)) / this.renderWidth;
|
|
12128
|
+
this.passJitter[1] = (2 * (this.reconJitter[1] as number)) / this.renderHeight;
|
|
12129
|
+
}
|
|
12130
|
+
|
|
10831
12131
|
/**
|
|
10832
12132
|
* Give every pass that owns a target its chance to fill it, before the frame's own opens.
|
|
10833
12133
|
*
|
|
@@ -10841,6 +12141,32 @@ export class WebGPURenderer implements RendererApi {
|
|
|
10841
12141
|
private runPreparePasses(encoder: GPUCommandEncoder): void {
|
|
10842
12142
|
if (this.preparingPasses === 0) return;
|
|
10843
12143
|
this.prepareContext.encoder = encoder;
|
|
12144
|
+
/*
|
|
12145
|
+
* **The same test the lit pass applies to its own binding**: a view exists from the moment the
|
|
12146
|
+
* array is created, and what makes it *readable* is a completed bake that is not the one
|
|
12147
|
+
* currently open. Offering it earlier hands a pass a texture of undefined contents, which is
|
|
12148
|
+
* the failure `probeBaked` was introduced for on the forward path.
|
|
12149
|
+
*/
|
|
12150
|
+
const usable = this.probeView !== null && this.probeBaked && !this.probePassActive;
|
|
12151
|
+
if (usable) {
|
|
12152
|
+
const environment = this.passEnvironment;
|
|
12153
|
+
environment.view = this.probeView;
|
|
12154
|
+
environment.sampler = this.probeSampler;
|
|
12155
|
+
environment.edge = this.probeEdge;
|
|
12156
|
+
environment.maxLod = this.probeMaxLod;
|
|
12157
|
+
environment.irradianceLevel = irradianceLevelFor(this.probeEdge);
|
|
12158
|
+
environment.irradiance = this.probeAmbient;
|
|
12159
|
+
environment.layers = this.probes?.layers ?? 1;
|
|
12160
|
+
this.prepareContext.environment = environment as PassEnvironment;
|
|
12161
|
+
} else {
|
|
12162
|
+
this.prepareContext.environment = null;
|
|
12163
|
+
}
|
|
12164
|
+
/*
|
|
12165
|
+
* The field the *previous* frame composed, because this runs at `beginFrame` and the
|
|
12166
|
+
* composition runs at `endFrame`. `PrepareContext.distanceField` says why that is right rather
|
|
12167
|
+
* than merely what happens.
|
|
12168
|
+
*/
|
|
12169
|
+
this.prepareContext.distanceField = this.fieldComposer?.field ?? null;
|
|
10844
12170
|
for (const definition of this.passes.definitions) {
|
|
10845
12171
|
definition?.prepare?.(this.prepareContext as PrepareContext);
|
|
10846
12172
|
}
|
|
@@ -11060,7 +12386,12 @@ export class WebGPURenderer implements RendererApi {
|
|
|
11060
12386
|
depthFormat: DEPTH_FORMAT,
|
|
11061
12387
|
/* The same four numbers this backend's own verbs are projected through. See `PassDevice`. */
|
|
11062
12388
|
clipCorrection: CLIP_CORRECTION,
|
|
12389
|
+
/* And the one a pass that wrote its own WGSL wants, which is the same without the Y
|
|
12390
|
+
negation — that negation cancels `naga`'s, and a hand-written shader has none. */
|
|
12391
|
+
depthCorrection: DEPTH_CORRECTION,
|
|
11063
12392
|
samples: this.samples,
|
|
12393
|
+
/* Whether a pass drawing the world owes the frame its jitter and its depth. See `PassDevice`. */
|
|
12394
|
+
reconstruction: this.reconstructionWanted,
|
|
11064
12395
|
};
|
|
11065
12396
|
}
|
|
11066
12397
|
|
|
@@ -11113,8 +12444,13 @@ export class WebGPURenderer implements RendererApi {
|
|
|
11113
12444
|
const cssWidth = Math.max(canvas.clientWidth, 1);
|
|
11114
12445
|
const cssHeight = Math.max(canvas.clientHeight, 1);
|
|
11115
12446
|
const origin = canvas.getBoundingClientRect();
|
|
11116
|
-
|
|
11117
|
-
|
|
12447
|
+
/*
|
|
12448
|
+
* **CSS pixels to the scene's pixels, not the viewer's.** A viewport is state on the frame's
|
|
12449
|
+
* own pass, which draws into the render-size targets, so a box measured in the drawing buffer
|
|
12450
|
+
* would be scaled by the reconstruction ratio and land off the side of a smaller attachment.
|
|
12451
|
+
*/
|
|
12452
|
+
const scaleX = this.renderWidth / cssWidth;
|
|
12453
|
+
const scaleY = this.renderHeight / cssHeight;
|
|
11118
12454
|
const left = rect.left - origin.left;
|
|
11119
12455
|
const top = rect.top - origin.top;
|
|
11120
12456
|
const w = Math.max(1, Math.round(rect.width * scaleX));
|
|
@@ -11142,8 +12478,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
11142
12478
|
* edge for the frames it is half-off. That asymmetry is the cost of the clamp, it is
|
|
11143
12479
|
* recorded in the parity ledger, and the alternative was an exception.
|
|
11144
12480
|
*/
|
|
11145
|
-
|
|
11146
|
-
const
|
|
12481
|
+
/* The attachment this pass draws into, which is the scene's size — see `renderWidth`. */
|
|
12482
|
+
const targetWidth = this.renderWidth;
|
|
12483
|
+
const targetHeight = this.renderHeight;
|
|
11147
12484
|
const clampedX = Math.max(0, Math.min(x, targetWidth));
|
|
11148
12485
|
const clampedY = Math.max(0, Math.min(y, targetHeight));
|
|
11149
12486
|
const clampedW = Math.max(0, Math.min(x + w, targetWidth) - clampedX);
|
|
@@ -11220,9 +12557,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
11220
12557
|
if (this.quality.frameGraph) this.flushGraph();
|
|
11221
12558
|
const pass = this.pass;
|
|
11222
12559
|
if (this.surface.lost || pass === null) return;
|
|
11223
|
-
|
|
11224
|
-
pass.setViewport(0, 0,
|
|
11225
|
-
pass.setScissorRect(0, 0,
|
|
12560
|
+
/* The whole of what this pass draws into, which is the scene's size and not the viewer's. */
|
|
12561
|
+
pass.setViewport(0, 0, this.renderWidth, this.renderHeight, 0, 1);
|
|
12562
|
+
pass.setScissorRect(0, 0, this.renderWidth, this.renderHeight);
|
|
11226
12563
|
}
|
|
11227
12564
|
|
|
11228
12565
|
/**
|
|
@@ -11742,12 +13079,28 @@ export class WebGPURenderer implements RendererApi {
|
|
|
11742
13079
|
return this.probes !== null && this.probeFilledCount === this.probes.layers;
|
|
11743
13080
|
}
|
|
11744
13081
|
|
|
11745
|
-
/**
|
|
13082
|
+
/**
|
|
13083
|
+
* Record that one layer now holds a convolution, without counting a rebake twice.
|
|
13084
|
+
*
|
|
13085
|
+
* **And rebuild the flat groups on the last one**, which is the third time this backend has paid
|
|
13086
|
+
* for a bind group holding whatever texture existed when it was built. `flatTextures` resolves
|
|
13087
|
+
* `uEnvironment` to a one-texel **white** stand-in while `probeBaked` is false, so a grid filled
|
|
13088
|
+
* by the trace rather than by `bakeProbeGrid` — which runs before any frame and therefore before
|
|
13089
|
+
* any group — leaves every flat group holding that white for the life of the renderer. The
|
|
13090
|
+
* uniform beside it flips to "use the grid" and the grid the shader reads is pure white.
|
|
13091
|
+
*
|
|
13092
|
+
* Measured on `demo/dev/bounce.html?seed=0`: every surface came back at exactly its own albedo
|
|
13093
|
+
* times 255, with the sun switched off and the trace pinned to a constant. It had been recorded
|
|
13094
|
+
* as undefined memory in the probe array; it is a deliberate white texture, read on purpose.
|
|
13095
|
+
* The refraction snapshot's own note two hundred lines below says the same thing about the same
|
|
13096
|
+
* mistake, and `setProbeGrid` says it about the array being replaced.
|
|
13097
|
+
*/
|
|
11746
13098
|
private markProbeFilled(layer: number): void {
|
|
11747
13099
|
if (layer < 0 || layer >= this.probeFilled.length) return;
|
|
11748
13100
|
if (this.probeFilled[layer] === 1) return;
|
|
11749
13101
|
this.probeFilled[layer] = 1;
|
|
11750
13102
|
this.probeFilledCount++;
|
|
13103
|
+
if (this.probeBaked) this.rebuildFlatGroupsForNewRings();
|
|
11751
13104
|
}
|
|
11752
13105
|
|
|
11753
13106
|
/**
|
|
@@ -11769,6 +13122,47 @@ export class WebGPURenderer implements RendererApi {
|
|
|
11769
13122
|
this.probeView = this.probeArray.createView({ dimension: '2d-array' });
|
|
11770
13123
|
this.probeFilled = new Uint8Array(layers);
|
|
11771
13124
|
this.probeFilledCount = 0;
|
|
13125
|
+
|
|
13126
|
+
/*
|
|
13127
|
+
* **Cleared here, because a texture no pass has written holds undefined contents** — the same
|
|
13128
|
+
* sentence the directional shadow maps are cleared under, arrived at from the other end.
|
|
13129
|
+
*
|
|
13130
|
+
* A traced probe reads the array's own irradiance level back as the light that has already
|
|
13131
|
+
* bounced, so an unwritten array is not "nothing has bounced yet": it is whatever the
|
|
13132
|
+
* allocator left, amplified by every refresh. Measured on `demo/dev/bounce.html` with
|
|
13133
|
+
* `?seed=0`, a grid nothing had rasterised settled on a uniform **209 of 255** in every
|
|
13134
|
+
* channel, which reads as a working ambient term and is memory.
|
|
13135
|
+
*
|
|
13136
|
+
* **Every level, not only the one the bake writes.** The roughness chain is sampled by
|
|
13137
|
+
* reflective surfaces long before any bake has filled it, and a first fill writes the whole
|
|
13138
|
+
* chain only for a layer the scheduler has reached.
|
|
13139
|
+
*/
|
|
13140
|
+
const clear = device.createCommandEncoder({ label: 'probe.clear' });
|
|
13141
|
+
const levels = irradianceLevelFor(this.probeEdge) + 1;
|
|
13142
|
+
for (let layer = 0; layer < layers; layer += 1) {
|
|
13143
|
+
for (let level = 0; level < levels; level += 1) {
|
|
13144
|
+
clear
|
|
13145
|
+
.beginRenderPass({
|
|
13146
|
+
label: 'probe.clear',
|
|
13147
|
+
colorAttachments: [
|
|
13148
|
+
{
|
|
13149
|
+
view: this.probeArray.createView({
|
|
13150
|
+
dimension: '2d',
|
|
13151
|
+
baseArrayLayer: layer,
|
|
13152
|
+
arrayLayerCount: 1,
|
|
13153
|
+
baseMipLevel: level,
|
|
13154
|
+
mipLevelCount: 1,
|
|
13155
|
+
}),
|
|
13156
|
+
clearValue: { r: 0, g: 0, b: 0, a: 1 },
|
|
13157
|
+
loadOp: 'clear',
|
|
13158
|
+
storeOp: 'store',
|
|
13159
|
+
},
|
|
13160
|
+
],
|
|
13161
|
+
})
|
|
13162
|
+
.end();
|
|
13163
|
+
}
|
|
13164
|
+
}
|
|
13165
|
+
device.queue.submit([clear.finish()]);
|
|
11772
13166
|
}
|
|
11773
13167
|
|
|
11774
13168
|
/**
|
|
@@ -11933,7 +13327,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
11933
13327
|
const heldExposure = this.perFrameFloats[this.materialField('uOutputExposure')] ?? 1;
|
|
11934
13328
|
this.perFrameInts[this.materialField('uOutputTransform')] = 0;
|
|
11935
13329
|
this.perFrameFloats[this.materialField('uOutputExposure')] = 1;
|
|
11936
|
-
this.
|
|
13330
|
+
this.materials.dirty();
|
|
11937
13331
|
|
|
11938
13332
|
for (let face = 0; face < PROBE_FACES.length; face++) {
|
|
11939
13333
|
const aim = PROBE_FACES[face];
|
|
@@ -12012,7 +13406,7 @@ export class WebGPURenderer implements RendererApi {
|
|
|
12012
13406
|
this.markProbeFilled(layer);
|
|
12013
13407
|
this.perFrameInts[this.materialField('uOutputTransform')] = heldGrade;
|
|
12014
13408
|
this.perFrameFloats[this.materialField('uOutputExposure')] = heldExposure;
|
|
12015
|
-
this.
|
|
13409
|
+
this.materials.dirty();
|
|
12016
13410
|
this.probePassActive = false;
|
|
12017
13411
|
this.rebuildFlatBindGroup();
|
|
12018
13412
|
return true;
|
|
@@ -12514,7 +13908,13 @@ export class WebGPURenderer implements RendererApi {
|
|
|
12514
13908
|
label: 'post.resolvedDepth',
|
|
12515
13909
|
size: [width, height],
|
|
12516
13910
|
format: RESOLVED_DEPTH_FORMAT,
|
|
12517
|
-
|
|
13911
|
+
/*
|
|
13912
|
+
* **`COPY_SRC` only where a reconstruction is going to keep it**, because a usage is a
|
|
13913
|
+
* promise about the whole life of the texture and every frame that does not reconstruct
|
|
13914
|
+
* would be paying for one it never makes. A reconstruction copies this into its own target
|
|
13915
|
+
* at the end of the frame, which is what its disocclusion tests against next frame.
|
|
13916
|
+
*/
|
|
13917
|
+
usage: this.quality.reconstruction > 0 ? USAGE | 0x1 : USAGE,
|
|
12518
13918
|
});
|
|
12519
13919
|
this.resolvedDepthView = this.resolvedDepth.createView();
|
|
12520
13920
|
/*
|
|
@@ -12983,6 +14383,8 @@ export class WebGPURenderer implements RendererApi {
|
|
|
12983
14383
|
* the 2026-08-13 rule asks for — one decision, bound twice.
|
|
12984
14384
|
*/
|
|
12985
14385
|
dispose(): void {
|
|
14386
|
+
for (const texture of this.retiredTextures) texture.destroy();
|
|
14387
|
+
this.retiredTextures.length = 0;
|
|
12986
14388
|
const passDevice = this.passDevice();
|
|
12987
14389
|
drainRegistry(this.passes, (definition) => definition.dispose?.(passDevice));
|
|
12988
14390
|
this.passReads.clear();
|
|
@@ -12993,6 +14395,9 @@ export class WebGPURenderer implements RendererApi {
|
|
|
12993
14395
|
const computeDevice = this.computeDevice();
|
|
12994
14396
|
drainRegistry(this.computes, (definition) => definition.dispose?.(computeDevice));
|
|
12995
14397
|
|
|
14398
|
+
this.fieldComposer?.dispose();
|
|
14399
|
+
this.fieldComposer = null;
|
|
14400
|
+
|
|
12996
14401
|
this.pass = null;
|
|
12997
14402
|
this.encoder = null;
|
|
12998
14403
|
this.skinPalettes.dispose();
|