@forgeax/engine-render 0.1.19 → 0.1.21
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/README.md +215 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/assembly/device-feature-admission.d.ts +18 -0
- package/dist/assembly/device-feature-admission.d.ts.map +1 -0
- package/dist/assembly/directional-shadow-inspection.d.ts +38 -0
- package/dist/assembly/directional-shadow-inspection.d.ts.map +1 -0
- package/dist/assembly/factory.d.ts +34 -20
- package/dist/assembly/factory.d.ts.map +1 -1
- package/dist/assembly/host-contract.d.ts +9 -0
- package/dist/assembly/host-contract.d.ts.map +1 -1
- package/dist/assembly/render-target-host.d.ts +46 -0
- package/dist/assembly/render-target-host.d.ts.map +1 -0
- package/dist/assets/render-asset-port.d.ts.map +1 -1
- package/dist/authoring.mjs +3 -0
- package/dist/authoring.mjs.map +1 -1
- package/dist/capture/cube-views.d.ts +21 -0
- package/dist/capture/cube-views.d.ts.map +1 -0
- package/dist/capture/scheduler.d.ts +54 -0
- package/dist/capture/scheduler.d.ts.map +1 -0
- package/dist/{chunk-RJRUS55L.mjs → chunk-5YADUCA7.mjs} +1896 -1624
- package/dist/chunk-5YADUCA7.mjs.map +1 -0
- package/dist/{chunk-LOSO4CTV.mjs → chunk-6OPXDQ2E.mjs} +423 -46
- package/dist/chunk-6OPXDQ2E.mjs.map +1 -0
- package/dist/{chunk-IPXCABEP.mjs → chunk-BXBQQKFQ.mjs} +3 -9
- package/dist/chunk-BXBQQKFQ.mjs.map +1 -0
- package/dist/chunk-FS2KNUQH.mjs +1294 -0
- package/dist/chunk-FS2KNUQH.mjs.map +1 -0
- package/dist/components/camera.d.ts +1 -0
- package/dist/components/camera.d.ts.map +1 -1
- package/dist/components/cube-camera.d.ts +20 -0
- package/dist/components/cube-camera.d.ts.map +1 -0
- package/dist/components/directional-light.d.ts +11 -2
- package/dist/components/directional-light.d.ts.map +1 -1
- package/dist/components/directional-shadow-filter.d.ts +25 -0
- package/dist/components/directional-shadow-filter.d.ts.map +1 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/light-helpers.d.ts.map +1 -1
- package/dist/components/reflection-probe.d.ts +17 -0
- package/dist/components/reflection-probe.d.ts.map +1 -0
- package/dist/components/spot-light.d.ts +12 -0
- package/dist/components/spot-light.d.ts.map +1 -1
- package/dist/construct-renderer.mjs +8552 -1612
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/device/gpu-residency.d.ts +19 -2
- package/dist/device/gpu-residency.d.ts.map +1 -1
- package/dist/environment/frame.d.ts.map +1 -1
- package/dist/environment/lifecycle.d.ts +5 -0
- package/dist/environment/lifecycle.d.ts.map +1 -1
- package/dist/errors/render.d.ts +209 -93
- package/dist/errors/render.d.ts.map +1 -1
- package/dist/extract/directional-shadow-projection.d.ts +23 -0
- package/dist/extract/directional-shadow-projection.d.ts.map +1 -0
- package/dist/extract/environment.d.ts +14 -0
- package/dist/extract/environment.d.ts.map +1 -1
- package/dist/extract/visibility.d.ts.map +1 -1
- package/dist/features/fullscreen.d.ts.map +1 -1
- package/dist/features/targets.d.ts.map +1 -1
- package/dist/features/types.d.ts +10 -0
- package/dist/features/types.d.ts.map +1 -1
- package/dist/fullscreen-post-process-pass.d.ts +13 -2
- package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
- package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
- package/dist/gpu-driven/production-raster.d.ts +0 -1
- package/dist/gpu-driven/production-raster.d.ts.map +1 -1
- package/dist/gpu-scene.d.ts.map +1 -1
- package/dist/gpu-usage.d.ts +2 -0
- package/dist/gpu-usage.d.ts.map +1 -1
- package/dist/hdrp-buffers.d.ts +19 -17
- package/dist/hdrp-buffers.d.ts.map +1 -1
- package/dist/ibl/IblPipelineCache.d.ts +4 -0
- package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
- package/dist/ibl/kernel-cache.d.ts +36 -0
- package/dist/ibl/kernel-cache.d.ts.map +1 -0
- package/dist/index.d.ts +35 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +4 -4
- package/dist/index.mjs.map +1 -1
- package/dist/inspection-types.d.ts +51 -0
- package/dist/inspection-types.d.ts.map +1 -1
- package/dist/instances-derived-bounds.d.ts +50 -0
- package/dist/instances-derived-bounds.d.ts.map +1 -0
- package/dist/internal.mjs +1 -1
- package/dist/light-buffer-layout.d.ts +30 -13
- package/dist/light-buffer-layout.d.ts.map +1 -1
- package/dist/mesh-material-bindings.d.ts +54 -0
- package/dist/mesh-material-bindings.d.ts.map +1 -1
- package/dist/pbr-pipeline.d.ts +20 -7
- package/dist/pbr-pipeline.d.ts.map +1 -1
- package/dist/pipeline/standard-forward-lane.d.ts +2 -1
- package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
- package/dist/pipeline/standard-lighting/errors.d.ts +4 -0
- package/dist/pipeline/standard-lighting/errors.d.ts.map +1 -0
- package/dist/pipeline/standard-lighting/graph.d.ts +24 -0
- package/dist/pipeline/standard-lighting/graph.d.ts.map +1 -0
- package/dist/pipeline/standard-lighting/inspection.d.ts +38 -0
- package/dist/pipeline/standard-lighting/inspection.d.ts.map +1 -0
- package/dist/pipeline/standard-lighting/layout.d.ts +28 -0
- package/dist/pipeline/standard-lighting/layout.d.ts.map +1 -0
- package/dist/pipeline/standard-lighting/light-frame.d.ts +33 -0
- package/dist/pipeline/standard-lighting/light-frame.d.ts.map +1 -0
- package/dist/pipeline/standard-lighting/membership.d.ts +14 -0
- package/dist/pipeline/standard-lighting/membership.d.ts.map +1 -0
- package/dist/pipeline/standard-lighting/prepare.d.ts +16 -0
- package/dist/pipeline/standard-lighting/prepare.d.ts.map +1 -0
- package/dist/pipeline/standard-lighting/topology.d.ts +45 -0
- package/dist/pipeline/standard-lighting/topology.d.ts.map +1 -0
- package/dist/pipeline/standard-lighting/transport.d.ts +36 -0
- package/dist/pipeline/standard-lighting/transport.d.ts.map +1 -0
- package/dist/pipeline/standard-pipeline.d.ts +4 -13
- package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
- package/dist/pipeline/standard-post.d.ts.map +1 -1
- package/dist/pipeline/standard-profile.d.ts +12 -20
- package/dist/pipeline/standard-profile.d.ts.map +1 -1
- package/dist/pipeline-spec.d.ts +40 -7
- package/dist/pipeline-spec.d.ts.map +1 -1
- package/dist/point-shadow-inspection.d.ts +21 -0
- package/dist/point-shadow-inspection.d.ts.map +1 -0
- package/dist/record/frame-lighting.d.ts +29 -32
- package/dist/record/frame-lighting.d.ts.map +1 -1
- package/dist/record/frame-snapshot.d.ts +65 -15
- package/dist/record/frame-snapshot.d.ts.map +1 -1
- package/dist/record/frame-targets.d.ts +5 -0
- package/dist/record/frame-targets.d.ts.map +1 -1
- package/dist/record/frame.d.ts +12 -5
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/gpu-pass-timing/contract.d.ts +96 -0
- package/dist/record/gpu-pass-timing/contract.d.ts.map +1 -0
- package/dist/record/gpu-pass-timing/errors.d.ts +18 -0
- package/dist/record/gpu-pass-timing/errors.d.ts.map +1 -0
- package/dist/record/gpu-pass-timing/index.d.ts +8 -0
- package/dist/record/gpu-pass-timing/index.d.ts.map +1 -0
- package/dist/record/gpu-pass-timing/parser.d.ts +11 -0
- package/dist/record/gpu-pass-timing/parser.d.ts.map +1 -0
- package/dist/record/gpu-pass-timing/session.d.ts +43 -0
- package/dist/record/gpu-pass-timing/session.d.ts.map +1 -0
- package/dist/record/gpu-timing.d.ts +37 -0
- package/dist/record/gpu-timing.d.ts.map +1 -0
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/main-pass-material.d.ts +23 -2
- package/dist/record/main-pass-material.d.ts.map +1 -1
- package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
- package/dist/record/main-pass.d.ts +2 -0
- package/dist/record/main-pass.d.ts.map +1 -1
- package/dist/record/render-context.d.ts +48 -3
- package/dist/record/render-context.d.ts.map +1 -1
- package/dist/record/shadow-pass.d.ts.map +1 -1
- package/dist/record/skybox-post-pass.d.ts.map +1 -1
- package/dist/record/typed-frame-graph.d.ts +81 -5
- package/dist/record/typed-frame-graph.d.ts.map +1 -1
- package/dist/record/view-ubo.d.ts +7 -4
- package/dist/record/view-ubo.d.ts.map +1 -1
- package/dist/reflection/filter.d.ts +22 -0
- package/dist/reflection/filter.d.ts.map +1 -0
- package/dist/reflection/gpu-table.d.ts +25 -0
- package/dist/reflection/gpu-table.d.ts.map +1 -0
- package/dist/reflection/projection.d.ts +91 -0
- package/dist/reflection/projection.d.ts.map +1 -0
- package/dist/render-contract.d.ts +114 -7
- package/dist/render-contract.d.ts.map +1 -1
- package/dist/render-data.d.ts +10 -6
- package/dist/render-data.d.ts.map +1 -1
- package/dist/render-graph-primitives.d.ts +1 -0
- package/dist/render-graph-primitives.d.ts.map +1 -1
- package/dist/render-pipeline.d.ts +78 -2
- package/dist/render-pipeline.d.ts.map +1 -1
- package/dist/render-system-extract.d.ts +113 -20
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts +24 -3
- package/dist/render-system.d.ts.map +1 -1
- package/dist/renderer-inspect.d.ts +29 -0
- package/dist/renderer-inspect.d.ts.map +1 -0
- package/dist/renderer.d.ts +43 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/scene/render-scene.d.ts +14 -0
- package/dist/scene/render-scene.d.ts.map +1 -1
- package/dist/shadow-atlas.d.ts +3 -3
- package/dist/shadow-atlas.d.ts.map +1 -1
- package/dist/ssao-buffers.d.ts +3 -0
- package/dist/ssao-buffers.d.ts.map +1 -1
- package/dist/targets/contracts.d.ts +66 -0
- package/dist/targets/contracts.d.ts.map +1 -0
- package/dist/targets/graph-projection.d.ts +29 -0
- package/dist/targets/graph-projection.d.ts.map +1 -0
- package/dist/targets/material-source.d.ts +34 -0
- package/dist/targets/material-source.d.ts.map +1 -0
- package/dist/targets/owner.d.ts +32 -0
- package/dist/targets/owner.d.ts.map +1 -0
- package/dist/targets/physical.d.ts +18 -0
- package/dist/targets/physical.d.ts.map +1 -0
- package/dist/targets/readback.d.ts +39 -0
- package/dist/targets/readback.d.ts.map +1 -0
- package/dist/temporal/frame.d.ts +44 -0
- package/dist/temporal/frame.d.ts.map +1 -0
- package/dist/temporal/index.mjs +3 -3
- package/dist/typed-render-graph-primitives.d.ts +6 -1
- package/dist/typed-render-graph-primitives.d.ts.map +1 -1
- package/dist/typed-shadow-atlas.d.ts +10 -0
- package/dist/typed-shadow-atlas.d.ts.map +1 -0
- package/dist/typed-shadow-passes.d.ts +1 -1
- package/dist/typed-shadow-passes.d.ts.map +1 -1
- package/dist/volume/capability.d.ts +57 -0
- package/dist/volume/capability.d.ts.map +1 -0
- package/dist/volume/component.d.ts +61 -0
- package/dist/volume/component.d.ts.map +1 -0
- package/dist/volume/extract.d.ts +11 -0
- package/dist/volume/extract.d.ts.map +1 -0
- package/dist/volume/inspection.d.ts +38 -0
- package/dist/volume/inspection.d.ts.map +1 -0
- package/dist/volume/passes.d.ts +38 -0
- package/dist/volume/passes.d.ts.map +1 -0
- package/dist/volume/recovery.d.ts +38 -0
- package/dist/volume/recovery.d.ts.map +1 -0
- package/dist/volume/resources.d.ts +57 -0
- package/dist/volume/resources.d.ts.map +1 -0
- package/dist/volume/temporal.d.ts +35 -0
- package/dist/volume/temporal.d.ts.map +1 -0
- package/package.json +20 -20
- package/src/__tests__/bloom-exact-zero.unit.test.ts +48 -19
- package/src/__tests__/bloom-lifecycle.integration.test.ts +41 -13
- package/src/__tests__/camera-target-consumption.unit.test.ts +53 -0
- package/src/__tests__/cluster-binner.unit.test.ts +35 -0
- package/src/__tests__/cluster-grid-default-surface.unit.test.ts +7 -2
- package/src/__tests__/cube-camera-scheduler.unit.test.ts +70 -0
- package/src/__tests__/cube-camera-single-submit.perf.test.ts +75 -0
- package/src/__tests__/cube-camera-view-matrices.unit.test.ts +41 -0
- package/src/__tests__/cube-camera-vocabulary.unit.test.ts +47 -0
- package/src/__tests__/cube-camera.integration.test.ts +42 -0
- package/src/__tests__/device-feature-admission.unit.test.ts +73 -0
- package/src/__tests__/direct-light-snapshot.unit.test.ts +3 -3
- package/src/__tests__/docs-gate-green.unit.test.ts +38 -0
- package/src/__tests__/factory-contract.integration.test.ts +255 -29
- package/src/__tests__/fog-component.unit.test.ts +67 -0
- package/src/__tests__/fog.integration.test.ts +86 -0
- package/src/__tests__/fullscreen-feature-plan.unit.test.ts +37 -0
- package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +10 -9
- package/src/__tests__/gpu-driven-indirect-raster.dawn.test.ts +17 -1
- package/src/__tests__/gpu-driven-production.integration.test.ts +16 -17
- package/src/__tests__/gpu-driven-view-graph.integration.test.ts +12 -4
- package/src/__tests__/gpu-pass-timing-browser-runner.ts +281 -0
- package/src/__tests__/gpu-pass-timing-coexistence.integration.test.ts +80 -0
- package/src/__tests__/gpu-pass-timing-contract.unit.test.ts +243 -0
- package/src/__tests__/gpu-pass-timing-exact-zero.unit.test.ts +131 -0
- package/src/__tests__/gpu-pass-timing-lifecycle.integration.test.ts +230 -0
- package/src/__tests__/gpu-pass-timing-observation.unit.test.ts +233 -0
- package/src/__tests__/gpu-pass-timing-session.unit.test.ts +438 -0
- package/src/__tests__/gpu-pass-timing.browser.test.ts +26 -0
- package/src/__tests__/gpu-pass-timing.rhi-null.unit.test.ts +125 -0
- package/src/__tests__/hdrp-group2-visibility.unit.test.ts +24 -0
- package/src/__tests__/instances-culling.integration.test.ts +94 -0
- package/src/__tests__/instances-derived-bounds.unit.test.ts +118 -0
- package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +14 -14
- package/src/__tests__/material-observation.integration.test.ts +101 -0
- package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +1 -1
- package/src/__tests__/pbr-view-layout.unit.test.ts +16 -0
- package/src/__tests__/point-shadow-inspection.unit.test.ts +60 -0
- package/src/__tests__/public-surface.test-d.ts +10 -0
- package/src/__tests__/reflection-probe-authoring.unit.test.ts +71 -0
- package/src/__tests__/reflection-probe-filter.unit.test.ts +54 -0
- package/src/__tests__/reflection-probe-ibl-topology.unit.test.ts +45 -0
- package/src/__tests__/reflection-probe-pass-encode.integration.test.ts +101 -0
- package/src/__tests__/reflection-probe-performance.perf.test.ts +60 -0
- package/src/__tests__/reflection-probe-recovery.integration.test.ts +55 -0
- package/src/__tests__/reflection-probe-selection.unit.test.ts +46 -0
- package/src/__tests__/render-error-code-owner.test-d.ts +23 -3
- package/src/__tests__/render-feature-docs.unit.test.ts +1 -1
- package/src/__tests__/render-target-admission.unit.test.ts +80 -0
- package/src/__tests__/render-target-graph-projection.unit.test.ts +102 -0
- package/src/__tests__/render-target-inspection.unit.test.ts +52 -0
- package/src/__tests__/render-target-material-source.test-d.ts +30 -0
- package/src/__tests__/render-target-material-source.unit.test.ts +97 -0
- package/src/__tests__/render-target-owner-placement.unit.test.ts +203 -0
- package/src/__tests__/render-target-owner.unit.test.ts +122 -0
- package/src/__tests__/render-target-probe-soak.perf.test.ts +94 -0
- package/src/__tests__/render-target-public-schema.json +107 -0
- package/src/__tests__/render-target-public-schema.unit.test.ts +91 -0
- package/src/__tests__/render-target-public-surface.test-d.ts +83 -0
- package/src/__tests__/render-target-readback.integration.test.ts +90 -0
- package/src/__tests__/render-target-recovery.integration.test.ts +85 -0
- package/src/__tests__/renderer-factory-material-contract.unit.test.ts +76 -0
- package/src/__tests__/renderer-frame-transaction.integration.test.ts +1 -1
- package/src/__tests__/renderer-lifecycle.integration.test.ts +14 -0
- package/src/__tests__/shader-stage-surface.unit.test.ts +19 -4
- package/src/__tests__/skin-motion-regression.unit.test.ts +132 -0
- package/src/__tests__/skinned-shadow-caster.test.ts +32 -2
- package/src/__tests__/standard-cluster-capability.integration.test.ts +63 -0
- package/src/__tests__/standard-cluster-transport.unit.test.ts +121 -0
- package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +232 -0
- package/src/__tests__/standard-clustered-pipeline.unit.test.ts +8 -5
- package/src/__tests__/standard-lighting-inspection.integration.test.ts +138 -0
- package/src/__tests__/standard-lighting-ssot.unit.test.ts +18 -0
- package/src/__tests__/standard-lighting-write.unit.test.ts +377 -0
- package/src/__tests__/standard-lighting.integration.test.ts +67 -0
- package/src/__tests__/standard-pipeline.dawn.test.ts +206 -5
- package/src/__tests__/standard-pipeline.integration.test.ts +181 -150
- package/src/__tests__/standard-probe-binding.unit.test.ts +25 -0
- package/src/__tests__/standard-profile-oracle.unit.test.ts +23 -36
- package/src/__tests__/typed-pipeline-topology.unit.test.ts +395 -14
- package/src/__tests__/urp-pipeline-targets.unit.test.ts +2 -2
- package/src/__tests__/visibility-resolver.unit.test.ts +15 -0
- package/src/__tests__/volume-optics-oracle.unit.test.ts +85 -0
- package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +655 -0
- package/src/__tests__/volumetric-fog-component.unit.test.ts +71 -0
- package/src/__tests__/volumetric-fog-composite-stage-readback.dawn.test.ts +376 -0
- package/src/__tests__/volumetric-fog-errors.unit.test.ts +51 -0
- package/src/__tests__/volumetric-fog-inspection.unit.test.ts +77 -0
- package/src/__tests__/volumetric-fog-light-capability.unit.test.ts +19 -0
- package/src/__tests__/volumetric-fog-light-consumers.integration.test.ts +18 -0
- package/src/__tests__/volumetric-fog-light-errors.unit.test.ts +17 -0
- package/src/__tests__/volumetric-fog-light-topology.unit.test.ts +21 -0
- package/src/__tests__/volumetric-fog-light-transaction.integration.test.ts +53 -0
- package/src/__tests__/volumetric-fog-m9-watcher.integration.test.ts +27 -0
- package/src/__tests__/volumetric-fog-point-light-owner.integration.test.ts +44 -0
- package/src/__tests__/volumetric-fog-point-spot-accumulation.integration.test.ts +49 -0
- package/src/__tests__/volumetric-fog-point-spot-contract.unit.test.ts +28 -0
- package/src/__tests__/volumetric-fog-point-spot-lkg.integration.test.ts +65 -0
- package/src/__tests__/volumetric-fog-point-spot-selection.integration.test.ts +49 -0
- package/src/__tests__/volumetric-fog-point-spot-temporal.unit.test.ts +55 -0
- package/src/__tests__/volumetric-fog-projector-contract.unit.test.ts +22 -0
- package/src/__tests__/volumetric-fog-projector-inspection.unit.test.ts +12 -0
- package/src/__tests__/volumetric-fog-projector-surface-volume.integration.test.ts +11 -0
- package/src/__tests__/volumetric-fog-projector-transaction.integration.test.ts +26 -0
- package/src/__tests__/volumetric-fog-recovery.integration.test.ts +47 -0
- package/src/__tests__/volumetric-fog-selected-light-resolution.integration.test.ts +63 -0
- package/src/__tests__/volumetric-fog-spot-closure.integration.test.ts +26 -0
- package/src/__tests__/volumetric-fog-spot-contract.test-d.ts +21 -0
- package/src/__tests__/volumetric-fog-spot-contract.unit.test.ts +27 -0
- package/src/__tests__/volumetric-fog-spot-frame-optics.integration.test.ts +14 -0
- package/src/__tests__/volumetric-fog-spot-optics.integration.test.ts +29 -0
- package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +550 -0
- package/src/__tests__/volumetric-fog-temporal-selected-light.unit.test.ts +33 -0
- package/src/__tests__/volumetric-fog-temporal.unit.test.ts +45 -0
- package/src/__tests__/volumetric-fog-topology.unit.test.ts +237 -0
- package/src/__tests__/volumetric-fog-transparent.integration.test.ts +27 -0
- package/src/assembly/device-feature-admission.ts +73 -0
- package/src/assembly/directional-shadow-inspection.ts +122 -0
- package/src/assembly/factory.ts +1093 -246
- package/src/assembly/host-contract.ts +25 -0
- package/src/assembly/render-target-host.ts +847 -0
- package/src/assets/asset-decoders.ts +1 -1
- package/src/assets/render-asset-port.ts +7 -1
- package/src/capture/cube-views.ts +69 -0
- package/src/capture/scheduler.ts +265 -0
- package/src/components/__tests__/schema-contract.unit.test.ts +12 -0
- package/src/components/__tests__/schema-parity.unit.test.ts +9 -0
- package/src/components/camera.ts +4 -0
- package/src/components/cube-camera.ts +52 -0
- package/src/components/directional-light.ts +22 -2
- package/src/components/directional-shadow-filter.ts +57 -0
- package/src/components/index.ts +2 -0
- package/src/components/light-helpers.ts +85 -7
- package/src/components/reflection-probe.ts +49 -0
- package/src/components/spot-light.ts +22 -3
- package/src/device/gpu-residency.ts +168 -58
- package/src/environment/frame.ts +6 -24
- package/src/environment/lifecycle.ts +12 -0
- package/src/errors/render.ts +405 -143
- package/src/extract/directional-shadow-projection.ts +61 -0
- package/src/extract/environment.ts +57 -0
- package/src/extract/visibility.ts +25 -1
- package/src/features/fullscreen.ts +3 -0
- package/src/features/targets.ts +4 -1
- package/src/features/types.ts +10 -0
- package/src/fullscreen-post-process-pass.ts +43 -2
- package/src/gpu-driven/batch-topology.ts +1 -0
- package/src/gpu-driven/production-raster.ts +1 -3
- package/src/gpu-scene.ts +8 -1
- package/src/gpu-usage.ts +2 -0
- package/src/hdrp-buffers.ts +165 -59
- package/src/ibl/IblPipelineCache.ts +11 -0
- package/src/ibl/kernel-cache.ts +88 -0
- package/src/index.ts +186 -1
- package/src/inspection-types.ts +56 -0
- package/src/instances-derived-bounds.ts +171 -0
- package/src/light-buffer-layout.ts +74 -26
- package/src/mesh-material-bindings.ts +132 -0
- package/src/pbr-pipeline.ts +139 -27
- package/src/pipeline/standard-forward-lane.ts +65 -15
- package/src/pipeline/standard-lighting/errors.ts +6 -0
- package/src/pipeline/standard-lighting/graph.ts +157 -0
- package/src/pipeline/standard-lighting/inspection.ts +88 -0
- package/src/pipeline/standard-lighting/layout.ts +31 -0
- package/src/pipeline/standard-lighting/light-frame.ts +34 -0
- package/src/pipeline/standard-lighting/membership.ts +43 -0
- package/src/pipeline/standard-lighting/prepare.ts +37 -0
- package/src/pipeline/standard-lighting/topology.ts +120 -0
- package/src/pipeline/standard-lighting/transport.ts +77 -0
- package/src/pipeline/standard-pipeline.ts +141 -192
- package/src/pipeline/standard-post.ts +26 -7
- package/src/pipeline/standard-profile.ts +23 -30
- package/src/pipeline-spec.ts +84 -21
- package/src/point-shadow-inspection.ts +55 -0
- package/src/record/__tests__/temporal-payload.unit.test.ts +42 -1
- package/src/record/__tests__/transparent-group2-selection.unit.test.ts +73 -0
- package/src/record/__tests__/view-ubo-layout.unit.test.ts +40 -0
- package/src/record/frame-lighting.ts +470 -516
- package/src/record/frame-snapshot.ts +76 -28
- package/src/record/frame-targets.ts +23 -2
- package/src/record/frame.ts +1169 -79
- package/src/record/gpu-pass-timing/contract.ts +180 -0
- package/src/record/gpu-pass-timing/errors.ts +34 -0
- package/src/record/gpu-pass-timing/index.ts +37 -0
- package/src/record/gpu-pass-timing/parser.ts +97 -0
- package/src/record/gpu-pass-timing/session.ts +938 -0
- package/src/record/gpu-timing.ts +233 -0
- package/src/record/main-pass-geometry.ts +120 -80
- package/src/record/main-pass-material.ts +101 -11
- package/src/record/main-pass-sprite-draws.ts +54 -25
- package/src/record/main-pass.ts +71 -15
- package/src/record/render-context.ts +77 -0
- package/src/record/shadow-pass.ts +104 -66
- package/src/record/skybox-post-pass.ts +20 -4
- package/src/record/typed-frame-graph.ts +1016 -32
- package/src/record/view-ubo.ts +63 -28
- package/src/reflection/filter.ts +81 -0
- package/src/reflection/gpu-table.ts +44 -0
- package/src/reflection/projection.ts +281 -0
- package/src/render-contract.ts +147 -4
- package/src/render-data.ts +50 -22
- package/src/render-graph-primitives.ts +9 -4
- package/src/render-pipeline.ts +161 -2
- package/src/render-system-extract.ts +886 -103
- package/src/render-system.ts +1174 -66
- package/src/renderer-inspect.ts +66 -0
- package/src/renderer.ts +222 -0
- package/src/scene/render-scene.ts +117 -9
- package/src/shadow-atlas.ts +5 -5
- package/src/ssao-buffers.ts +9 -2
- package/src/targets/contracts.ts +182 -0
- package/src/targets/graph-projection.ts +169 -0
- package/src/targets/material-source.ts +143 -0
- package/src/targets/owner.ts +214 -0
- package/src/targets/physical.ts +18 -0
- package/src/targets/readback.ts +153 -0
- package/src/temporal/frame.ts +128 -0
- package/src/transmission/__tests__/standard-transmission.dawn.test.ts +61 -23
- package/src/typed-render-graph-primitives.ts +34 -2
- package/src/typed-shadow-atlas.ts +17 -0
- package/src/typed-shadow-passes.ts +47 -40
- package/src/volume/capability.ts +173 -0
- package/src/volume/component.ts +110 -0
- package/src/volume/extract.ts +21 -0
- package/src/volume/inspection.ts +78 -0
- package/src/volume/passes.ts +686 -0
- package/src/volume/recovery.ts +86 -0
- package/src/volume/resources.ts +236 -0
- package/src/volume/temporal.ts +89 -0
- package/dist/chunk-IPXCABEP.mjs.map +0 -1
- package/dist/chunk-LOSO4CTV.mjs.map +0 -1
- package/dist/chunk-RJRUS55L.mjs.map +0 -1
- package/dist/chunk-YTQ5C7YQ.mjs +0 -584
- package/dist/chunk-YTQ5C7YQ.mjs.map +0 -1
package/src/record/frame.ts
CHANGED
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
import { HANDLE_NINESLICE_QUAD, resolveAssetHandle } from '@forgeax/engine-assets-runtime';
|
|
5
5
|
import type { World } from '@forgeax/engine-ecs';
|
|
6
6
|
import type { RenderReadLease } from '@forgeax/engine-ecs/projection';
|
|
7
|
-
import { mat4 } from '@forgeax/engine-math';
|
|
7
|
+
import { mat4, vec3 } from '@forgeax/engine-math';
|
|
8
8
|
import {
|
|
9
9
|
type CurrentFrameObservationLease,
|
|
10
10
|
createCurrentFrameObservationLease,
|
|
11
11
|
} from '@forgeax/engine-render-graph';
|
|
12
12
|
import {
|
|
13
|
+
type Buffer,
|
|
13
14
|
err,
|
|
14
15
|
ok,
|
|
15
16
|
type Result,
|
|
@@ -20,36 +21,59 @@ import {
|
|
|
20
21
|
} from '@forgeax/engine-rhi';
|
|
21
22
|
import type { MaterialRenderState, MeshAsset } from '@forgeax/engine-types';
|
|
22
23
|
import { handleSlot, toShared } from '@forgeax/engine-types';
|
|
24
|
+
import { CUBE_CAMERA_FACE_ORDER } from '../components/cube-camera';
|
|
25
|
+
import type { DirectionalShadowQuality } from '../components/directional-shadow-filter';
|
|
23
26
|
import type { MeshGpuHandles } from '../device/gpu-residency';
|
|
24
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
ObservationUnavailableError,
|
|
29
|
+
type ObservationUnavailableReason,
|
|
30
|
+
RendererOperationError,
|
|
31
|
+
} from '../errors/render';
|
|
25
32
|
import type { FogFrame } from '../extract/environment';
|
|
26
33
|
import type { GpuDrivenProduction, PreparedGpuDrivenFrame } from '../gpu-driven/production-raster';
|
|
27
34
|
import { GpuBuffer } from '../gpu-resource';
|
|
28
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
GPU_BUFFER_USAGE_COPY_DST,
|
|
37
|
+
GPU_BUFFER_USAGE_UNIFORM,
|
|
38
|
+
GPU_BUFFER_USAGE_VERTEX,
|
|
39
|
+
} from '../gpu-usage';
|
|
29
40
|
import { disposeTransientInstanceBuffers } from '../instance-buffer-cache';
|
|
30
|
-
import
|
|
41
|
+
import { inspectStandardLighting } from '../pipeline/standard-lighting/inspection';
|
|
42
|
+
import { standardLightingTopologySignature } from '../pipeline/standard-lighting/topology';
|
|
43
|
+
import { inspectPointShadow } from '../point-shadow-inspection';
|
|
44
|
+
import type {
|
|
45
|
+
CameraSnapshot,
|
|
46
|
+
CubeCameraSnapshot,
|
|
47
|
+
RenderRecordPhase,
|
|
48
|
+
SpotLightProjectorFrameContext,
|
|
49
|
+
VolumetricFogFrameContext,
|
|
50
|
+
} from '../render-contract';
|
|
31
51
|
import type {
|
|
32
52
|
DispatchEntry,
|
|
33
53
|
ExtractedLights,
|
|
54
|
+
ExtractedVolumetricFog,
|
|
34
55
|
MaterialSnapshot,
|
|
35
56
|
RenderableSnapshot,
|
|
36
57
|
SkyboxSnapshot,
|
|
37
58
|
SkylightSnapshot,
|
|
38
59
|
} from '../render-system-extract';
|
|
39
60
|
import type { PersistentGpuDrivenState } from '../scene/render-scene';
|
|
61
|
+
import { SHADOW_ATLAS_DEFAULT_LAYERS } from '../shadow-atlas';
|
|
40
62
|
import {
|
|
41
63
|
getTransparentSortConfig,
|
|
42
64
|
TRANSPARENT_SORT_MODE_LAYER_Y,
|
|
43
65
|
TRANSPARENT_SORT_MODE_LAYER_Z,
|
|
44
66
|
} from '../systems/transparent-sort-config';
|
|
45
67
|
import type { TransmissionDemand } from '../transmission/projection';
|
|
68
|
+
import { hasVolumetricFogCapability } from '../volume/capability';
|
|
69
|
+
import { inspectVolumetricFogResources, VOLUMETRIC_FOG_PARAMS_BYTES } from '../volume/resources';
|
|
70
|
+
import { resolveVolumeTemporalReset, type VolumeTemporalSignature } from '../volume/temporal';
|
|
46
71
|
import {
|
|
47
72
|
buildPerFrameBindGroups,
|
|
48
73
|
prepareFrameLighting,
|
|
49
74
|
resolveSkyboxActive,
|
|
50
75
|
warnZeroLightStandard,
|
|
51
76
|
writeHdrpClusterAndSsaoBuffers,
|
|
52
|
-
writePointSpotLightBuffers,
|
|
53
77
|
writeShadowParamsBuffer,
|
|
54
78
|
} from './frame-lighting';
|
|
55
79
|
import {
|
|
@@ -188,12 +212,14 @@ export async function observeCurrentFrame(
|
|
|
188
212
|
}
|
|
189
213
|
|
|
190
214
|
import { createTemporalView, resolveTemporalReset } from '../temporal/view';
|
|
215
|
+
import { inspectVolumetricFog } from '../volume/inspection';
|
|
191
216
|
import {
|
|
192
217
|
acquireSwapChainTarget,
|
|
193
218
|
graphExecutionPhase,
|
|
194
219
|
resolveShadowMapSize,
|
|
195
220
|
resolveSpotShadowMapSize,
|
|
196
221
|
} from './frame-targets';
|
|
222
|
+
import type { GpuTimingCapture } from './gpu-timing';
|
|
197
223
|
import {
|
|
198
224
|
computeProjectionMatrix,
|
|
199
225
|
computeViewMatrix,
|
|
@@ -207,11 +233,18 @@ import { computeSplitLdrSprite } from './main-pass-sprite-draws';
|
|
|
207
233
|
import { cleanPerEntityCache, ensureMeshSsboCapacity, uploadMeshSsboBatch } from './mesh-ssbo';
|
|
208
234
|
import { writeShadowCasterUniforms } from './shadow-pass';
|
|
209
235
|
import {
|
|
236
|
+
type CubeCaptureGraphState,
|
|
210
237
|
ensureCompiledFrameGraph,
|
|
211
238
|
executeCompiledFrameGraph,
|
|
212
239
|
type RenderFeatureGraphCandidate,
|
|
240
|
+
settleVolumetricFogGraphCandidate,
|
|
213
241
|
} from './typed-frame-graph';
|
|
214
|
-
import {
|
|
242
|
+
import {
|
|
243
|
+
CUBE_CAPTURE_VIEW_SLOT_BASE,
|
|
244
|
+
VIEW_UNIFORM_SLOT_STRIDE,
|
|
245
|
+
writePointsLinesViewUbo,
|
|
246
|
+
writeViewUbo,
|
|
247
|
+
} from './view-ubo';
|
|
215
248
|
|
|
216
249
|
/**
|
|
217
250
|
* A frame-local material table. Repeated snapshot objects share one UBO slot;
|
|
@@ -284,6 +317,67 @@ export function materialSlotCountForPrefix(
|
|
|
284
317
|
|
|
285
318
|
export type RecordProfileRunner = <T>(phase: RenderRecordPhase, action: () => T) => T;
|
|
286
319
|
|
|
320
|
+
export interface CubeCaptureFrameInput {
|
|
321
|
+
readonly snapshots: readonly CubeCameraSnapshot[];
|
|
322
|
+
readonly scheduler: import('../capture/scheduler').CubeCaptureScheduler;
|
|
323
|
+
readonly state: CubeCaptureGraphState;
|
|
324
|
+
readonly reflectionProbes?: import('./render-context').ReflectionProbeRecordState;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function prepareCubeCapture(
|
|
328
|
+
internals: RenderSystemInternals,
|
|
329
|
+
input: CubeCaptureFrameInput | undefined,
|
|
330
|
+
displayCamera: CameraSnapshot,
|
|
331
|
+
): readonly import('./typed-frame-graph').CubeCaptureGraphWork[] {
|
|
332
|
+
if (input === undefined) return [];
|
|
333
|
+
if (input.reflectionProbes === undefined) delete input.state.reflectionProbes;
|
|
334
|
+
else input.state.reflectionProbes = input.reflectionProbes.graph;
|
|
335
|
+
input.scheduler.beginFrame();
|
|
336
|
+
for (const snapshot of input.snapshots) {
|
|
337
|
+
const inspection = input.scheduler.inspect(snapshot.target);
|
|
338
|
+
if (inspection.candidateGeneration !== undefined) continue;
|
|
339
|
+
const requested = input.scheduler.request({
|
|
340
|
+
target: snapshot.target,
|
|
341
|
+
position: snapshot.position,
|
|
342
|
+
near: snapshot.near,
|
|
343
|
+
far: snapshot.far,
|
|
344
|
+
updateIntent: snapshot.updateIntent,
|
|
345
|
+
requestVersion: snapshot.requestVersion,
|
|
346
|
+
faceBudget: snapshot.faceBudget,
|
|
347
|
+
});
|
|
348
|
+
if (!requested.ok) internals.errorRegistry.fire(requested.error);
|
|
349
|
+
}
|
|
350
|
+
const work = input.scheduler.nextWork();
|
|
351
|
+
const physicalWork: import('./typed-frame-graph').CubeCaptureGraphWork[] = [];
|
|
352
|
+
for (const item of work) {
|
|
353
|
+
const physical = internals.getRenderTargetPhysical?.(item.target);
|
|
354
|
+
if (physical === undefined) continue;
|
|
355
|
+
const faceIndex = CUBE_CAMERA_FACE_ORDER.indexOf(item.face);
|
|
356
|
+
if (faceIndex < 0) continue;
|
|
357
|
+
const faceWorld = mat4.create();
|
|
358
|
+
mat4.invert(faceWorld, item.view);
|
|
359
|
+
physicalWork.push({
|
|
360
|
+
target: item.target,
|
|
361
|
+
faceIndex,
|
|
362
|
+
physical,
|
|
363
|
+
faceCamera: {
|
|
364
|
+
...displayCamera,
|
|
365
|
+
position: vec3.create(item.position[0] ?? 0, item.position[1] ?? 0, item.position[2] ?? 0),
|
|
366
|
+
world: faceWorld,
|
|
367
|
+
fov: Math.PI / 2,
|
|
368
|
+
aspect: 1,
|
|
369
|
+
near: item.near,
|
|
370
|
+
far: item.far,
|
|
371
|
+
projection: 'perspective',
|
|
372
|
+
},
|
|
373
|
+
viewBindGroupDynamicOffset:
|
|
374
|
+
(CUBE_CAPTURE_VIEW_SLOT_BASE + physicalWork.length) * VIEW_UNIFORM_SLOT_STRIDE,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
input.state.work = physicalWork;
|
|
378
|
+
return physicalWork;
|
|
379
|
+
}
|
|
380
|
+
|
|
287
381
|
const EMPTY_ENTITY_KEYS: ReadonlySet<number> = new Set<number>();
|
|
288
382
|
|
|
289
383
|
function runRecordProfilePhase<T>(
|
|
@@ -294,6 +388,705 @@ function runRecordProfilePhase<T>(
|
|
|
294
388
|
return runner === undefined ? action() : runner(phase, action);
|
|
295
389
|
}
|
|
296
390
|
|
|
391
|
+
const VOLUMETRIC_FOG_PASS_COUNT = 4;
|
|
392
|
+
|
|
393
|
+
type VolumetricFogParamsSlot = 0 | 1;
|
|
394
|
+
|
|
395
|
+
function nextVolumetricFogParamsSlot(frameState: RenderFrameState): VolumetricFogParamsSlot {
|
|
396
|
+
const accepted = frameState.volumetricFogParamsAcceptedSlot;
|
|
397
|
+
const pending = frameState.volumetricFogParamsPendingSlot;
|
|
398
|
+
if (pending !== null && pending !== accepted) return pending;
|
|
399
|
+
return accepted === null ? 0 : accepted === 0 ? 1 : 0;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function stageVolumetricFogParams(
|
|
403
|
+
internals: RenderSystemInternals,
|
|
404
|
+
frameState: RenderFrameState,
|
|
405
|
+
params: Float32Array,
|
|
406
|
+
): Buffer | undefined {
|
|
407
|
+
const slot = nextVolumetricFogParamsSlot(frameState);
|
|
408
|
+
let buffer = frameState.volumetricFogParamsBuffers[slot];
|
|
409
|
+
if (buffer === null) {
|
|
410
|
+
const created = internals.device.createBuffer({
|
|
411
|
+
label: `volumetric-fog-params-${slot}`,
|
|
412
|
+
size: VOLUMETRIC_FOG_PARAMS_BYTES,
|
|
413
|
+
usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
|
|
414
|
+
mappedAtCreation: false,
|
|
415
|
+
});
|
|
416
|
+
if (!created.ok) {
|
|
417
|
+
internals.errorRegistry.fire(created.error);
|
|
418
|
+
return undefined;
|
|
419
|
+
}
|
|
420
|
+
buffer = created.value;
|
|
421
|
+
frameState.volumetricFogParamsBuffers[slot] = buffer;
|
|
422
|
+
}
|
|
423
|
+
const written = internals.device.queue.writeBuffer(buffer, 0, params);
|
|
424
|
+
if (!written.ok) {
|
|
425
|
+
internals.errorRegistry.fire(written.error);
|
|
426
|
+
return undefined;
|
|
427
|
+
}
|
|
428
|
+
frameState.volumetricFogParamsPendingSlot = slot;
|
|
429
|
+
frameState.volumetricFogPendingParams = new Float32Array(params);
|
|
430
|
+
return buffer;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** Retire parameter buffers only after the submit that stopped using them. */
|
|
434
|
+
function retireVolumetricFogParams(
|
|
435
|
+
internals: RenderSystemInternals,
|
|
436
|
+
frameState: RenderFrameState,
|
|
437
|
+
): void {
|
|
438
|
+
const buffers = frameState.volumetricFogParamsBuffers;
|
|
439
|
+
frameState.volumetricFogParamsBuffers = [null, null];
|
|
440
|
+
frameState.volumetricFogParamsPendingSlot = null;
|
|
441
|
+
frameState.volumetricFogParamsAcceptedSlot = null;
|
|
442
|
+
frameState.volumetricFogAcceptedParams = undefined;
|
|
443
|
+
frameState.volumetricFogPendingParams = undefined;
|
|
444
|
+
const live = buffers.filter((buffer): buffer is Buffer => buffer !== null);
|
|
445
|
+
if (live.length === 0) return;
|
|
446
|
+
const device = internals.device;
|
|
447
|
+
const release = (): void => {
|
|
448
|
+
for (const buffer of live) {
|
|
449
|
+
const destroyed = device.destroyBuffer(buffer);
|
|
450
|
+
if (!destroyed.ok) internals.errorRegistry.fire(destroyed.error);
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
try {
|
|
454
|
+
void device.queue.onSubmittedWorkDone().then(release, release);
|
|
455
|
+
} catch {
|
|
456
|
+
release();
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function volumeRevision(values: readonly number[]): number {
|
|
461
|
+
let hash = 2166136261;
|
|
462
|
+
for (const value of values) {
|
|
463
|
+
const quantized = Number.isFinite(value) ? Math.round(value * 1_000_000) : 0;
|
|
464
|
+
hash = Math.imul(hash ^ quantized, 16777619) >>> 0;
|
|
465
|
+
}
|
|
466
|
+
return hash;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function volumeTemporalSignature(
|
|
470
|
+
fog: ExtractedVolumetricFog,
|
|
471
|
+
prepared: VolumetricFogFrameContext,
|
|
472
|
+
camera: CameraSnapshot,
|
|
473
|
+
lights: ExtractedLights,
|
|
474
|
+
width: number,
|
|
475
|
+
height: number,
|
|
476
|
+
): VolumeTemporalSignature {
|
|
477
|
+
const authored = fog.fog;
|
|
478
|
+
const directional = lights.directional;
|
|
479
|
+
const cameraRevision = volumeRevision([
|
|
480
|
+
...camera.world,
|
|
481
|
+
camera.fov,
|
|
482
|
+
camera.aspect,
|
|
483
|
+
camera.near,
|
|
484
|
+
camera.far,
|
|
485
|
+
camera.orthoLeft,
|
|
486
|
+
camera.orthoRight,
|
|
487
|
+
camera.orthoBottom,
|
|
488
|
+
camera.orthoTop,
|
|
489
|
+
]);
|
|
490
|
+
const fogRevision =
|
|
491
|
+
authored === undefined
|
|
492
|
+
? 0
|
|
493
|
+
: volumeRevision([
|
|
494
|
+
...authored.bounds.min,
|
|
495
|
+
...authored.bounds.max,
|
|
496
|
+
...authored.extinction,
|
|
497
|
+
...authored.albedo,
|
|
498
|
+
...authored.emission,
|
|
499
|
+
authored.anisotropy,
|
|
500
|
+
authored.maxDistance,
|
|
501
|
+
]);
|
|
502
|
+
const selectedSpot =
|
|
503
|
+
fog.spotLightEntity === undefined
|
|
504
|
+
? fog.lightKind === 'spot'
|
|
505
|
+
? lights.spot.find((spot) => spot.entity === fog.lightEntity)
|
|
506
|
+
: undefined
|
|
507
|
+
: lights.spot.find((spot) => spot.entity === fog.spotLightEntity);
|
|
508
|
+
const selectedPoint =
|
|
509
|
+
fog.pointLightEntity === undefined
|
|
510
|
+
? fog.lightKind === 'point'
|
|
511
|
+
? lights.point.find((point) => point.entity === fog.lightEntity)
|
|
512
|
+
: undefined
|
|
513
|
+
: lights.point.find((point) => point.entity === fog.pointLightEntity);
|
|
514
|
+
const selectedDirectional =
|
|
515
|
+
fog.lightKind === 'directional' && directional?.entity === fog.lightEntity
|
|
516
|
+
? directional
|
|
517
|
+
: undefined;
|
|
518
|
+
const lightRevision =
|
|
519
|
+
selectedSpot === undefined && selectedPoint === undefined && selectedDirectional === undefined
|
|
520
|
+
? 0
|
|
521
|
+
: volumeRevision(
|
|
522
|
+
selectedSpot === undefined && selectedPoint === undefined
|
|
523
|
+
? selectedDirectional === undefined
|
|
524
|
+
? []
|
|
525
|
+
: [
|
|
526
|
+
...selectedDirectional.direction,
|
|
527
|
+
...selectedDirectional.color,
|
|
528
|
+
selectedDirectional.intensity,
|
|
529
|
+
...(lights.lightViewProj?.flatMap((matrix) => [...matrix]) ?? []),
|
|
530
|
+
]
|
|
531
|
+
: [
|
|
532
|
+
...(selectedPoint === undefined
|
|
533
|
+
? []
|
|
534
|
+
: [
|
|
535
|
+
...selectedPoint.position,
|
|
536
|
+
...selectedPoint.color,
|
|
537
|
+
selectedPoint.intensity,
|
|
538
|
+
selectedPoint.invRangeSquared,
|
|
539
|
+
selectedPoint.shadowAtlasLayer ?? -1,
|
|
540
|
+
selectedPoint.shadowNear ?? 0,
|
|
541
|
+
selectedPoint.shadowFar ?? 0,
|
|
542
|
+
]),
|
|
543
|
+
...(selectedSpot === undefined
|
|
544
|
+
? []
|
|
545
|
+
: [
|
|
546
|
+
...selectedSpot.position,
|
|
547
|
+
...selectedSpot.direction,
|
|
548
|
+
...selectedSpot.color,
|
|
549
|
+
selectedSpot.intensity,
|
|
550
|
+
selectedSpot.invRangeSquared,
|
|
551
|
+
selectedSpot.cosInner,
|
|
552
|
+
selectedSpot.cosOuter,
|
|
553
|
+
selectedSpot.depthBias ?? 0.005,
|
|
554
|
+
selectedSpot.normalBias ?? 0.05,
|
|
555
|
+
selectedSpot.pcfKernelSize ?? 3,
|
|
556
|
+
selectedSpot.shadowAtlasTile,
|
|
557
|
+
...(selectedSpot.lightViewProj === undefined
|
|
558
|
+
? []
|
|
559
|
+
: [...selectedSpot.lightViewProj]),
|
|
560
|
+
]),
|
|
561
|
+
],
|
|
562
|
+
);
|
|
563
|
+
return {
|
|
564
|
+
cameraRevision,
|
|
565
|
+
fogRevision,
|
|
566
|
+
lightRevision,
|
|
567
|
+
densityGeneration: prepared.densityGeneration,
|
|
568
|
+
width,
|
|
569
|
+
height,
|
|
570
|
+
...(fog.lightKind === undefined ? {} : { lightKind: fog.lightKind }),
|
|
571
|
+
...(fog.lightEntity === undefined ? {} : { lightEntity: fog.lightEntity }),
|
|
572
|
+
...(fog.pointLightEntity === undefined ? {} : { pointLightEntity: fog.pointLightEntity }),
|
|
573
|
+
...(fog.spotLightEntity === undefined ? {} : { spotLightEntity: fog.spotLightEntity }),
|
|
574
|
+
...(fog.projector === undefined ? {} : { projector: fog.projector }),
|
|
575
|
+
lightShadowRevision: lightRevision,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function resolveVolumetricFogHistoryContext(
|
|
580
|
+
frameState: RenderFrameState,
|
|
581
|
+
graph: import('@forgeax/engine-render-graph').CompiledRenderGraph<
|
|
582
|
+
import('../render-pipeline').RenderPipelineFrame
|
|
583
|
+
>,
|
|
584
|
+
fog: ExtractedVolumetricFog,
|
|
585
|
+
prepared: VolumetricFogFrameContext,
|
|
586
|
+
camera: CameraSnapshot,
|
|
587
|
+
lights: ExtractedLights,
|
|
588
|
+
width: number,
|
|
589
|
+
height: number,
|
|
590
|
+
): { readonly context: VolumetricFogFrameContext; readonly signature: VolumeTemporalSignature } {
|
|
591
|
+
const signature = volumeTemporalSignature(fog, prepared, camera, lights, width, height);
|
|
592
|
+
const acceptedGraph = frameState.volumetricFogHistoryGraph;
|
|
593
|
+
const acceptedSlot = frameState.volumetricFogHistorySlot;
|
|
594
|
+
const previousSignature = frameState.volumetricFogHistorySignature;
|
|
595
|
+
let reset = acceptedGraph !== graph || acceptedSlot === null || previousSignature === null;
|
|
596
|
+
if (!reset && previousSignature !== null) {
|
|
597
|
+
const temporal = frameState.temporalFrame;
|
|
598
|
+
const previousResetReason = temporal?.resetReason;
|
|
599
|
+
const reprojection =
|
|
600
|
+
previousResetReason === 'out-of-screen'
|
|
601
|
+
? 'out-of-screen'
|
|
602
|
+
: previousResetReason === 'depth-discontinuity'
|
|
603
|
+
? 'depth-discontinuity'
|
|
604
|
+
: undefined;
|
|
605
|
+
reset = resolveVolumeTemporalReset(previousSignature, signature, {
|
|
606
|
+
taaEnabled: camera.antialias === 'taa',
|
|
607
|
+
...(reprojection === undefined ? {} : { reprojection }),
|
|
608
|
+
}).reset;
|
|
609
|
+
}
|
|
610
|
+
const historyReadSlot = reset ? null : acceptedSlot;
|
|
611
|
+
const historyWriteSlot = reset || acceptedSlot === null ? 0 : acceptedSlot === 0 ? 1 : 0;
|
|
612
|
+
return {
|
|
613
|
+
context: {
|
|
614
|
+
...prepared,
|
|
615
|
+
historyReadSlot,
|
|
616
|
+
historyWriteSlot,
|
|
617
|
+
historyValid: historyReadSlot !== null,
|
|
618
|
+
},
|
|
619
|
+
signature,
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Resolve the authored density POD into the renderer-owned GPU resources used
|
|
625
|
+
* by the graph. This is deliberately a pull operation: a missing/stale
|
|
626
|
+
* handle or failed upload produces a structured error and leaves the graph
|
|
627
|
+
* candidate disabled for this frame rather than binding a synthetic density.
|
|
628
|
+
*/
|
|
629
|
+
function prepareVolumetricFogFrame(
|
|
630
|
+
internals: RenderSystemInternals,
|
|
631
|
+
frameState: RenderFrameState,
|
|
632
|
+
world: World,
|
|
633
|
+
worlds: readonly World[],
|
|
634
|
+
fog: ExtractedVolumetricFog | undefined,
|
|
635
|
+
lights: ExtractedLights,
|
|
636
|
+
volumeCapability: boolean,
|
|
637
|
+
projector: SpotLightProjectorFrameContext | undefined,
|
|
638
|
+
): VolumetricFogFrameContext | undefined {
|
|
639
|
+
if (
|
|
640
|
+
!volumeCapability ||
|
|
641
|
+
fog?.status !== 'available' ||
|
|
642
|
+
fog.fog === undefined ||
|
|
643
|
+
fog.densityHandle === undefined ||
|
|
644
|
+
fog.densityAsset === undefined ||
|
|
645
|
+
(fog.lightKind === 'spot'
|
|
646
|
+
? lights.spot.find((light) => light.entity === fog.lightEntity) === undefined
|
|
647
|
+
: fog.lightKind === 'point'
|
|
648
|
+
? lights.point.find((light) => light.entity === fog.lightEntity) === undefined
|
|
649
|
+
: lights.directional === undefined)
|
|
650
|
+
) {
|
|
651
|
+
return undefined;
|
|
652
|
+
}
|
|
653
|
+
const ownerWorld = worlds[fog.worldId ?? 0] ?? world;
|
|
654
|
+
const resident = internals.gpuStore.ensureResident(
|
|
655
|
+
fog.densityHandle,
|
|
656
|
+
fog.densityAsset,
|
|
657
|
+
ownerWorld,
|
|
658
|
+
);
|
|
659
|
+
if (!resident.ok || !('texture' in resident.value)) {
|
|
660
|
+
if (!resident.ok) {
|
|
661
|
+
internals.errorRegistry.fire(
|
|
662
|
+
resident.error instanceof RhiError
|
|
663
|
+
? resident.error
|
|
664
|
+
: new RhiError({
|
|
665
|
+
code: 'webgpu-runtime-error',
|
|
666
|
+
expected: 'volumetric density residency to resolve a GPU texture',
|
|
667
|
+
hint: 'repair the authored TextureAsset payload and retry the next frame',
|
|
668
|
+
detail: { error: resident.error },
|
|
669
|
+
}),
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
return undefined;
|
|
673
|
+
}
|
|
674
|
+
const params = new Float32Array(32);
|
|
675
|
+
params.set(fog.fog.bounds.min, 0);
|
|
676
|
+
params.set(fog.fog.bounds.max, 4);
|
|
677
|
+
params.set(fog.fog.extinction, 8);
|
|
678
|
+
params.set(fog.fog.albedo, 12);
|
|
679
|
+
params.set(fog.fog.emission, 16);
|
|
680
|
+
const selectedPoint =
|
|
681
|
+
fog.pointLightEntity === undefined
|
|
682
|
+
? undefined
|
|
683
|
+
: lights.point.find((light) => light.entity === fog.pointLightEntity);
|
|
684
|
+
const selectedSpot =
|
|
685
|
+
fog.spotLightEntity === undefined
|
|
686
|
+
? fog.lightKind === 'spot'
|
|
687
|
+
? lights.spot.find((light) => light.entity === fog.lightEntity)
|
|
688
|
+
: undefined
|
|
689
|
+
: lights.spot.find((light) => light.entity === fog.spotLightEntity);
|
|
690
|
+
const lightDirection =
|
|
691
|
+
selectedSpot?.direction ?? lights.directional?.direction ?? vec3.create(0, -1, 0);
|
|
692
|
+
const lightColor = selectedSpot?.color ?? lights.directional?.color ?? vec3.create(0, 0, 0);
|
|
693
|
+
if (lightDirection === undefined || lightColor === undefined) return undefined;
|
|
694
|
+
params.set(lightDirection, 20);
|
|
695
|
+
// Reuse the vec4 padding lane as the renderer-owned frame phase for
|
|
696
|
+
// sub-froxel jitter. The temporal resolver consumes it without growing the
|
|
697
|
+
// public fog component or allocating another uniform buffer.
|
|
698
|
+
params[23] = frameState.frameNumber % 1024;
|
|
699
|
+
const selectedSpotIndex =
|
|
700
|
+
selectedSpot === undefined
|
|
701
|
+
? -1
|
|
702
|
+
: lights.spot.findIndex((light) => light.entity === selectedSpot.entity);
|
|
703
|
+
params.set(lightColor, 24);
|
|
704
|
+
params[19] =
|
|
705
|
+
selectedPoint === undefined
|
|
706
|
+
? -1
|
|
707
|
+
: lights.point.findIndex((light) => light.entity === selectedPoint.entity);
|
|
708
|
+
params[27] = selectedSpotIndex;
|
|
709
|
+
params[28] = fog.fog.maxDistance;
|
|
710
|
+
params[29] = fog.fog.anisotropy;
|
|
711
|
+
params[30] =
|
|
712
|
+
selectedPoint !== undefined && selectedSpot !== undefined
|
|
713
|
+
? 3
|
|
714
|
+
: selectedPoint !== undefined
|
|
715
|
+
? 1
|
|
716
|
+
: selectedSpot !== undefined
|
|
717
|
+
? 2
|
|
718
|
+
: 0;
|
|
719
|
+
// The temporal shader reads this lane after the record stage resolves the
|
|
720
|
+
// accepted graph/slot. It is overwritten on the next queue-visible frame;
|
|
721
|
+
// the first candidate deliberately starts with history disabled.
|
|
722
|
+
params[31] =
|
|
723
|
+
frameState.temporalFrame !== undefined && frameState.temporalFrame.resetReason === 'none'
|
|
724
|
+
? 1
|
|
725
|
+
: 0;
|
|
726
|
+
|
|
727
|
+
const paramsBuffer = stageVolumetricFogParams(internals, frameState, params);
|
|
728
|
+
if (paramsBuffer === undefined) return undefined;
|
|
729
|
+
return {
|
|
730
|
+
densityTexture: resident.value.texture.handle,
|
|
731
|
+
densityView: resident.value.view,
|
|
732
|
+
paramsBuffer,
|
|
733
|
+
densityGeneration: resident.value.receipt.generation,
|
|
734
|
+
historyReadSlot: null,
|
|
735
|
+
historyWriteSlot: 0,
|
|
736
|
+
historyValid: false,
|
|
737
|
+
...(projector === undefined
|
|
738
|
+
? {}
|
|
739
|
+
: {
|
|
740
|
+
projectorTexture: projector.texture,
|
|
741
|
+
projectorView: projector.view,
|
|
742
|
+
projectorSampler: projector.sampler,
|
|
743
|
+
}),
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Resolve the SpotLight-owned cookie independently of volumetric fog. Surface
|
|
749
|
+
* lighting must keep the authored projector when the fog component is absent;
|
|
750
|
+
* otherwise the view bind group silently falls back to white and changes the
|
|
751
|
+
* direct-light energy on the floor. A fog-selected projector is preferred so
|
|
752
|
+
* the surface and volume lanes consume one identity, with the first authored
|
|
753
|
+
* projector as the deterministic scene fallback.
|
|
754
|
+
*/
|
|
755
|
+
function prepareSpotLightProjectorFrame(
|
|
756
|
+
internals: RenderSystemInternals,
|
|
757
|
+
world: World,
|
|
758
|
+
worlds: readonly World[],
|
|
759
|
+
lights: ExtractedLights,
|
|
760
|
+
fog: ExtractedVolumetricFog | undefined,
|
|
761
|
+
): SpotLightProjectorFrameContext | undefined {
|
|
762
|
+
const preferred =
|
|
763
|
+
fog?.projectorHandle === undefined
|
|
764
|
+
? undefined
|
|
765
|
+
: lights.spot.find((light) => light.projectorHandle === fog.projectorHandle);
|
|
766
|
+
const source =
|
|
767
|
+
preferred ??
|
|
768
|
+
lights.spot.find(
|
|
769
|
+
(light) =>
|
|
770
|
+
light.projectorHandle !== undefined &&
|
|
771
|
+
light.projectorAsset !== undefined &&
|
|
772
|
+
light.lightViewProj !== undefined,
|
|
773
|
+
);
|
|
774
|
+
if (
|
|
775
|
+
source === undefined ||
|
|
776
|
+
source.projectorHandle === undefined ||
|
|
777
|
+
source.projectorAsset === undefined
|
|
778
|
+
) {
|
|
779
|
+
return undefined;
|
|
780
|
+
}
|
|
781
|
+
const ownerWorld = worlds[fog?.worldId ?? 0] ?? world;
|
|
782
|
+
const resident = internals.gpuStore.ensureResident(
|
|
783
|
+
source.projectorHandle,
|
|
784
|
+
source.projectorAsset,
|
|
785
|
+
ownerWorld,
|
|
786
|
+
);
|
|
787
|
+
if (!resident.ok || !('texture' in resident.value)) {
|
|
788
|
+
if (!resident.ok) {
|
|
789
|
+
internals.errorRegistry.fire(
|
|
790
|
+
resident.error instanceof RhiError
|
|
791
|
+
? resident.error
|
|
792
|
+
: new RhiError({
|
|
793
|
+
code: 'webgpu-runtime-error',
|
|
794
|
+
expected: 'SpotLight projector residency to resolve a GPU texture',
|
|
795
|
+
hint: 'repair the authored projector TextureAsset payload and retry the next frame',
|
|
796
|
+
detail: { error: resident.error },
|
|
797
|
+
}),
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
return undefined;
|
|
801
|
+
}
|
|
802
|
+
const sampler = internals.device.createSampler({
|
|
803
|
+
label: 'spot-projector-linear-clamp',
|
|
804
|
+
minFilter: 'linear',
|
|
805
|
+
magFilter: 'linear',
|
|
806
|
+
mipmapFilter: 'linear',
|
|
807
|
+
addressModeU: 'clamp-to-edge',
|
|
808
|
+
addressModeV: 'clamp-to-edge',
|
|
809
|
+
addressModeW: 'clamp-to-edge',
|
|
810
|
+
});
|
|
811
|
+
if (!sampler.ok) {
|
|
812
|
+
internals.errorRegistry.fire(sampler.error);
|
|
813
|
+
return undefined;
|
|
814
|
+
}
|
|
815
|
+
return {
|
|
816
|
+
texture: resident.value.texture.handle,
|
|
817
|
+
view: resident.value.view,
|
|
818
|
+
sampler: sampler.value,
|
|
819
|
+
spotIndex: lights.spot.indexOf(source),
|
|
820
|
+
lightSlotIndex: lights.point.length + lights.spot.indexOf(source),
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/** Stage the accepted parameter payload into the writable slot for a degraded
|
|
825
|
+
* source frame. This keeps the accepted GPU buffer immutable until submit
|
|
826
|
+
* succeeds while still allowing the LKG graph to update its history flag. */
|
|
827
|
+
function stageAcceptedVolumetricFogFrame(
|
|
828
|
+
internals: RenderSystemInternals,
|
|
829
|
+
frameState: RenderFrameState,
|
|
830
|
+
accepted: VolumetricFogFrameContext | undefined,
|
|
831
|
+
): VolumetricFogFrameContext | undefined {
|
|
832
|
+
if (accepted === undefined) return undefined;
|
|
833
|
+
const params = frameState.volumetricFogAcceptedParams;
|
|
834
|
+
if (params === undefined) return accepted;
|
|
835
|
+
const paramsBuffer = stageVolumetricFogParams(internals, frameState, new Float32Array(params));
|
|
836
|
+
if (paramsBuffer === undefined) return accepted;
|
|
837
|
+
return { ...accepted, paramsBuffer };
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function updateVolumetricFogInspection(
|
|
841
|
+
frameState: RenderFrameState,
|
|
842
|
+
fog: ExtractedVolumetricFog | undefined,
|
|
843
|
+
lights: ExtractedLights,
|
|
844
|
+
prepared: VolumetricFogFrameContext | undefined,
|
|
845
|
+
submitted: boolean,
|
|
846
|
+
volumeCapability: boolean,
|
|
847
|
+
volumeSubmissionAttempted: boolean,
|
|
848
|
+
): void {
|
|
849
|
+
const resourceFacts =
|
|
850
|
+
frameState.compiledFrameGraph === undefined || frameState.compiledFrameGraph === null
|
|
851
|
+
? undefined
|
|
852
|
+
: inspectVolumetricFogResources(frameState.compiledFrameGraph.inspect(), {
|
|
853
|
+
parameterBufferCount: frameState.volumetricFogParamsBuffers.filter(
|
|
854
|
+
(buffer) => buffer !== null,
|
|
855
|
+
).length,
|
|
856
|
+
});
|
|
857
|
+
const graphSampleCount = resourceFacts?.sampleCount ?? 0;
|
|
858
|
+
const graphMemoryBytes = resourceFacts?.totalBytes ?? 0;
|
|
859
|
+
const previous = frameState.volumetricFogInspection;
|
|
860
|
+
const accepted = frameState.volumetricFogAccepted;
|
|
861
|
+
const acceptedLkg =
|
|
862
|
+
accepted === undefined
|
|
863
|
+
? undefined
|
|
864
|
+
: {
|
|
865
|
+
guid: accepted.guid ?? previous.guid,
|
|
866
|
+
generation: accepted.generation ?? previous.generation,
|
|
867
|
+
digest: accepted.digest ?? previous.digest,
|
|
868
|
+
deviceEpoch: previous.deviceEpoch,
|
|
869
|
+
format: previous.format ?? accepted.densityAsset?.format ?? fog?.densityAsset?.format,
|
|
870
|
+
passCount: previous.passCount > 0 ? previous.passCount : VOLUMETRIC_FOG_PASS_COUNT,
|
|
871
|
+
sampleCount: previous.sampleCount > 0 ? previous.sampleCount : graphSampleCount,
|
|
872
|
+
memoryBytes: previous.memoryBytes > 0 ? previous.memoryBytes : graphMemoryBytes,
|
|
873
|
+
resourceFacts: previous.resourceFacts ?? resourceFacts,
|
|
874
|
+
lightEntity: accepted.lightEntity,
|
|
875
|
+
lightKind: accepted.lightKind,
|
|
876
|
+
pointLightEntity: accepted.pointLightEntity,
|
|
877
|
+
spotLightEntity: accepted.spotLightEntity,
|
|
878
|
+
projector: accepted.projector,
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
const candidateIdentity =
|
|
882
|
+
fog?.guid === undefined || fog.generation === undefined
|
|
883
|
+
? undefined
|
|
884
|
+
: {
|
|
885
|
+
guid: fog.guid,
|
|
886
|
+
generation: fog.generation,
|
|
887
|
+
digest: fog.digest,
|
|
888
|
+
};
|
|
889
|
+
const selectedLightAvailable =
|
|
890
|
+
fog?.lightKind === 'spot'
|
|
891
|
+
? lights.spot.some((light) => light.entity === fog.lightEntity)
|
|
892
|
+
: fog?.lightKind === 'point'
|
|
893
|
+
? lights.point.some(
|
|
894
|
+
(light) => light.entity === (fog.pointLightEntity ?? fog.lightEntity),
|
|
895
|
+
) &&
|
|
896
|
+
(fog.spotLightEntity === undefined ||
|
|
897
|
+
lights.spot.some((light) => light.entity === fog.spotLightEntity))
|
|
898
|
+
: fog?.lightKind === 'directional'
|
|
899
|
+
? lights.directional?.entity === fog.lightEntity
|
|
900
|
+
: lights.directional !== undefined;
|
|
901
|
+
const capability = volumeCapability && selectedLightAvailable ? 'available' : 'unavailable';
|
|
902
|
+
|
|
903
|
+
const inspectAccepted = (status: 'accepted' | 'degraded', candidateFailure?: 'submit-failed') => {
|
|
904
|
+
if (
|
|
905
|
+
acceptedLkg === undefined ||
|
|
906
|
+
acceptedLkg.guid === undefined ||
|
|
907
|
+
acceptedLkg.generation === undefined
|
|
908
|
+
) {
|
|
909
|
+
return false;
|
|
910
|
+
}
|
|
911
|
+
const recovery = {
|
|
912
|
+
guid: acceptedLkg.guid,
|
|
913
|
+
generation: acceptedLkg.generation,
|
|
914
|
+
deviceEpoch: acceptedLkg.deviceEpoch,
|
|
915
|
+
status,
|
|
916
|
+
...(status === 'degraded' ? { lkgGeneration: acceptedLkg.generation } : {}),
|
|
917
|
+
...(candidateIdentity === undefined
|
|
918
|
+
? {}
|
|
919
|
+
: {
|
|
920
|
+
candidateGeneration: candidateIdentity.generation,
|
|
921
|
+
...(candidateIdentity.digest === undefined
|
|
922
|
+
? {}
|
|
923
|
+
: { candidateDigest: candidateIdentity.digest }),
|
|
924
|
+
}),
|
|
925
|
+
...(acceptedLkg.lightEntity === undefined || acceptedLkg.lightKind === undefined
|
|
926
|
+
? {}
|
|
927
|
+
: {
|
|
928
|
+
selectedLight: {
|
|
929
|
+
entity: acceptedLkg.lightEntity,
|
|
930
|
+
kind: acceptedLkg.lightKind,
|
|
931
|
+
revision: 0,
|
|
932
|
+
shadowTile: -1,
|
|
933
|
+
...(acceptedLkg.pointLightEntity === undefined
|
|
934
|
+
? {}
|
|
935
|
+
: { pointLightEntity: acceptedLkg.pointLightEntity }),
|
|
936
|
+
...(acceptedLkg.spotLightEntity === undefined
|
|
937
|
+
? {}
|
|
938
|
+
: { spotLightEntity: acceptedLkg.spotLightEntity }),
|
|
939
|
+
...(acceptedLkg.projector === undefined ? {} : { projector: acceptedLkg.projector }),
|
|
940
|
+
},
|
|
941
|
+
}),
|
|
942
|
+
...(candidateFailure === undefined ? {} : { candidateFailure }),
|
|
943
|
+
} satisfies import('../volume/recovery').VolumeRecoveryState;
|
|
944
|
+
frameState.volumetricFogInspection = inspectVolumetricFog({
|
|
945
|
+
authored: true,
|
|
946
|
+
capability: 'available',
|
|
947
|
+
degraded: status === 'degraded',
|
|
948
|
+
recovery,
|
|
949
|
+
...(acceptedLkg.digest === undefined ? {} : { acceptedDigest: acceptedLkg.digest }),
|
|
950
|
+
...(acceptedLkg.format === undefined ? {} : { format: acceptedLkg.format }),
|
|
951
|
+
passCount: acceptedLkg.passCount,
|
|
952
|
+
sampleCount: acceptedLkg.sampleCount,
|
|
953
|
+
memoryBytes: acceptedLkg.memoryBytes,
|
|
954
|
+
...(acceptedLkg.resourceFacts === undefined
|
|
955
|
+
? {}
|
|
956
|
+
: { resourceFacts: acceptedLkg.resourceFacts }),
|
|
957
|
+
});
|
|
958
|
+
return true;
|
|
959
|
+
};
|
|
960
|
+
|
|
961
|
+
if (fog === undefined || fog.status === 'off') {
|
|
962
|
+
// A failed fog-off candidate must not erase the inspection/LKG that is
|
|
963
|
+
// still being rendered. The accepted projection is retired only after a
|
|
964
|
+
// successful no-volume submission below.
|
|
965
|
+
if (!submitted && volumeSubmissionAttempted && inspectAccepted('degraded', 'submit-failed')) {
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
if (!submitted && volumeSubmissionAttempted) return;
|
|
969
|
+
if (!submitted) return;
|
|
970
|
+
frameState.volumetricFogInspection = inspectVolumetricFog({
|
|
971
|
+
authored: false,
|
|
972
|
+
capability: 'available',
|
|
973
|
+
});
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const authored = fog.status === 'available' || fog.status === 'degraded';
|
|
978
|
+
|
|
979
|
+
// A successfully submitted available candidate is the only path that may
|
|
980
|
+
// become the new accepted projection. This call runs after graph settlement
|
|
981
|
+
// so the inspection and the graph/context observe the same transaction.
|
|
982
|
+
if (
|
|
983
|
+
submitted &&
|
|
984
|
+
fog.status === 'available' &&
|
|
985
|
+
prepared !== undefined &&
|
|
986
|
+
capability === 'available'
|
|
987
|
+
) {
|
|
988
|
+
const recovery =
|
|
989
|
+
candidateIdentity === undefined
|
|
990
|
+
? undefined
|
|
991
|
+
: {
|
|
992
|
+
guid: candidateIdentity.guid,
|
|
993
|
+
generation: candidateIdentity.generation,
|
|
994
|
+
deviceEpoch: previous.deviceEpoch,
|
|
995
|
+
status: 'accepted' as const,
|
|
996
|
+
...(fog.lightEntity === undefined || fog.lightKind === undefined
|
|
997
|
+
? {}
|
|
998
|
+
: {
|
|
999
|
+
selectedLight: {
|
|
1000
|
+
entity: fog.lightEntity,
|
|
1001
|
+
kind: fog.lightKind,
|
|
1002
|
+
revision: 0,
|
|
1003
|
+
shadowTile:
|
|
1004
|
+
lights.spot.find((spot) => spot.entity === fog.spotLightEntity)
|
|
1005
|
+
?.shadowAtlasTile ??
|
|
1006
|
+
lights.spot.find((spot) => spot.entity === fog.lightEntity)
|
|
1007
|
+
?.shadowAtlasTile ??
|
|
1008
|
+
-1,
|
|
1009
|
+
...(fog.pointLightEntity === undefined
|
|
1010
|
+
? {}
|
|
1011
|
+
: { pointLightEntity: fog.pointLightEntity }),
|
|
1012
|
+
...(fog.spotLightEntity === undefined
|
|
1013
|
+
? {}
|
|
1014
|
+
: { spotLightEntity: fog.spotLightEntity }),
|
|
1015
|
+
...(fog.projector === undefined ? {} : { projector: fog.projector }),
|
|
1016
|
+
},
|
|
1017
|
+
}),
|
|
1018
|
+
};
|
|
1019
|
+
frameState.volumetricFogInspection = inspectVolumetricFog({
|
|
1020
|
+
authored: true,
|
|
1021
|
+
capability,
|
|
1022
|
+
...(recovery === undefined ? {} : { recovery }),
|
|
1023
|
+
...(candidateIdentity?.digest === undefined
|
|
1024
|
+
? {}
|
|
1025
|
+
: { acceptedDigest: candidateIdentity.digest }),
|
|
1026
|
+
...(fog.densityAsset?.format === undefined ? {} : { format: fog.densityAsset.format }),
|
|
1027
|
+
passCount: VOLUMETRIC_FOG_PASS_COUNT,
|
|
1028
|
+
sampleCount: graphSampleCount,
|
|
1029
|
+
memoryBytes: graphMemoryBytes,
|
|
1030
|
+
...(resourceFacts === undefined ? {} : { resourceFacts }),
|
|
1031
|
+
});
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// A submitted degraded source (or an available source whose residency
|
|
1036
|
+
// preparation failed) is rendered through the previously accepted graph.
|
|
1037
|
+
// Keep that LKG resource fact visible instead of projecting a zero-resource
|
|
1038
|
+
// candidate merely because the current source could not be prepared.
|
|
1039
|
+
if (
|
|
1040
|
+
submitted &&
|
|
1041
|
+
acceptedLkg !== undefined &&
|
|
1042
|
+
(fog.status === 'degraded' || prepared === undefined)
|
|
1043
|
+
) {
|
|
1044
|
+
if (inspectAccepted('degraded')) return;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Any candidate that reached encode/submit but failed must project the LKG,
|
|
1048
|
+
// never the unsubmitted candidate. The candidate identity remains visible so
|
|
1049
|
+
// recovery tooling can distinguish the rejected bytes from the frame being
|
|
1050
|
+
// rendered.
|
|
1051
|
+
if (!submitted && volumeSubmissionAttempted && inspectAccepted('degraded', 'submit-failed')) {
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// A degraded source that has no LKG is still a diagnostic, but it must not
|
|
1056
|
+
// claim accepted resources. Keep its identity in the candidate stage.
|
|
1057
|
+
const candidateRecovery =
|
|
1058
|
+
candidateIdentity === undefined
|
|
1059
|
+
? undefined
|
|
1060
|
+
: {
|
|
1061
|
+
guid: candidateIdentity.guid,
|
|
1062
|
+
generation: candidateIdentity.generation,
|
|
1063
|
+
deviceEpoch: previous.deviceEpoch,
|
|
1064
|
+
status: 'candidate' as const,
|
|
1065
|
+
candidateGeneration: candidateIdentity.generation,
|
|
1066
|
+
...(candidateIdentity.digest === undefined
|
|
1067
|
+
? {}
|
|
1068
|
+
: { candidateDigest: candidateIdentity.digest }),
|
|
1069
|
+
...(!submitted && volumeSubmissionAttempted
|
|
1070
|
+
? { candidateFailure: 'submit-failed' as const }
|
|
1071
|
+
: {}),
|
|
1072
|
+
};
|
|
1073
|
+
frameState.volumetricFogInspection = inspectVolumetricFog({
|
|
1074
|
+
authored,
|
|
1075
|
+
capability,
|
|
1076
|
+
degraded: fog.status === 'degraded' || capability === 'unavailable',
|
|
1077
|
+
...(candidateRecovery === undefined ? {} : { recovery: candidateRecovery }),
|
|
1078
|
+
...(acceptedLkg?.digest === undefined ? {} : { acceptedDigest: acceptedLkg.digest }),
|
|
1079
|
+
...(fog.densityAsset?.format === undefined
|
|
1080
|
+
? acceptedLkg?.format === undefined
|
|
1081
|
+
? {}
|
|
1082
|
+
: { format: acceptedLkg.format }
|
|
1083
|
+
: { format: fog.densityAsset.format }),
|
|
1084
|
+
passCount: 0,
|
|
1085
|
+
sampleCount: 0,
|
|
1086
|
+
memoryBytes: 0,
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
|
|
297
1090
|
export function recordFrame(
|
|
298
1091
|
internals: RenderSystemInternals,
|
|
299
1092
|
world: World,
|
|
@@ -322,15 +1115,18 @@ export function recordFrame(
|
|
|
322
1115
|
gpuDriven?: {
|
|
323
1116
|
readonly owner: GpuDrivenProduction;
|
|
324
1117
|
readonly scene: PersistentGpuDrivenState | undefined;
|
|
325
|
-
readonly onCoverage: (ownsAll: boolean) => void;
|
|
1118
|
+
readonly onCoverage: (ownsAll: boolean, clusteredLighting: boolean) => void;
|
|
326
1119
|
},
|
|
327
1120
|
renderReadLeases?: readonly RenderReadLease[],
|
|
328
1121
|
featureGraphCandidate?: RenderFeatureGraphCandidate,
|
|
329
1122
|
pointsLinesOwner?: import('./render-context').PointsLinesRecordOwner,
|
|
1123
|
+
cubeCapture?: CubeCaptureFrameInput,
|
|
330
1124
|
environmentSignature = '',
|
|
331
1125
|
fogSignature = '',
|
|
332
1126
|
environmentReady = true,
|
|
333
1127
|
transmissionDemand?: TransmissionDemand,
|
|
1128
|
+
volumetricFog?: ExtractedVolumetricFog,
|
|
1129
|
+
timingCapture?: GpuTimingCapture,
|
|
334
1130
|
): boolean {
|
|
335
1131
|
// The `try / finally` wrapper advances `frameState.frameNumber` exactly
|
|
336
1132
|
// once per `recordFrame` invocation regardless of which early-return
|
|
@@ -361,6 +1157,11 @@ export function recordFrame(
|
|
|
361
1157
|
);
|
|
362
1158
|
activeCameras = [makeZeroCameraFallbackSnapshot()];
|
|
363
1159
|
}
|
|
1160
|
+
let submitted = false;
|
|
1161
|
+
let volumeInspectionReady = false;
|
|
1162
|
+
let volumeSubmissionAttempted = false;
|
|
1163
|
+
let preparedVolumetricFogForInspection: VolumetricFogFrameContext | undefined;
|
|
1164
|
+
let volumeCapabilityForInspection = false;
|
|
364
1165
|
try {
|
|
365
1166
|
// Case D: N>1 Camera => fire onError, use first hit (D-S7).
|
|
366
1167
|
if (activeCameras.length > 1) {
|
|
@@ -376,6 +1177,23 @@ export function recordFrame(
|
|
|
376
1177
|
if (!camera) return false;
|
|
377
1178
|
const pipelineState = internals.getPipelineState();
|
|
378
1179
|
if (pipelineState === null) return false;
|
|
1180
|
+
if (pipelineState.device !== internals.device) {
|
|
1181
|
+
internals.errorRegistry.fire(
|
|
1182
|
+
new RendererOperationError('device-operation-failed', {
|
|
1183
|
+
operation: 'draw',
|
|
1184
|
+
cause: {
|
|
1185
|
+
code: 'pipeline-device-mismatch',
|
|
1186
|
+
expected: 'pipelineState.device === runtime.device',
|
|
1187
|
+
hint: 'discard stale pipeline state before recording the next frame',
|
|
1188
|
+
detail: {
|
|
1189
|
+
expected: 'pipelineState.device === runtime.device',
|
|
1190
|
+
hint: 'discard stale pipeline state before recording the next frame',
|
|
1191
|
+
},
|
|
1192
|
+
},
|
|
1193
|
+
}),
|
|
1194
|
+
);
|
|
1195
|
+
return false;
|
|
1196
|
+
}
|
|
379
1197
|
frameState.currentFrameObservationSource = undefined;
|
|
380
1198
|
frameState.currentDirectionalShadowView = null;
|
|
381
1199
|
frameState.currentSpotShadowView = null;
|
|
@@ -383,7 +1201,7 @@ export function recordFrame(
|
|
|
383
1201
|
// Record-stage fold operator linear scan (groups transparent-sort entries
|
|
384
1202
|
// into fold buckets + records the fold-eligible count metric). Extracted to
|
|
385
1203
|
// computeFoldBuckets (M3/w18).
|
|
386
|
-
const
|
|
1204
|
+
const lightingPreparation = runRecordProfilePhase(profilePhase, 'record/scene-state', () => {
|
|
387
1205
|
const foldBuckets = runRecordProfilePhase(
|
|
388
1206
|
profilePhase,
|
|
389
1207
|
'record/scene-state/fold-buckets',
|
|
@@ -394,11 +1212,11 @@ export function recordFrame(
|
|
|
394
1212
|
// ensure + ExtractedLights three-arm destructure (directional fallback,
|
|
395
1213
|
// point/spot arrays, totalLightCount). Extracted to prepareFrameLighting
|
|
396
1214
|
// (M3/w18) so recordFrame stays a skeleton.
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
'record/scene-state/lighting-prep',
|
|
400
|
-
() => prepareFrameLighting(internals, frameState, lights),
|
|
1215
|
+
const lighting = runRecordProfilePhase(profilePhase, 'record/scene-state/lighting-prep', () =>
|
|
1216
|
+
prepareFrameLighting(internals, frameState, lights, camera, pipelineState),
|
|
401
1217
|
);
|
|
1218
|
+
if (!lighting.ok) return lighting;
|
|
1219
|
+
const { light, totalLightCount, standard } = lighting.value;
|
|
402
1220
|
|
|
403
1221
|
runRecordProfilePhase(profilePhase, 'record/scene-state/ambient-resolution', () => {
|
|
404
1222
|
// feat-20260520-skylight-ibl-cubemap M4 / t27 (AC-10 + F-4 nit):
|
|
@@ -445,31 +1263,17 @@ export function recordFrame(
|
|
|
445
1263
|
}
|
|
446
1264
|
});
|
|
447
1265
|
|
|
448
|
-
// feat-20260608-cluster-lighting M5 / w21 + M6 / w23: HDRP per-frame CPU
|
|
449
|
-
// cluster binning + light-data / cluster-grid / SSAO buffer uploads. Runs
|
|
450
|
-
// only when the HDRP pipeline is active and there is at least one punctual
|
|
451
|
-
// light. Extracted to a sub-function (M3/w18) so recordFrame stays a
|
|
452
|
-
// skeleton; fail-soft error semantics are preserved verbatim.
|
|
453
|
-
runRecordProfilePhase(profilePhase, 'record/scene-state/hdrp-cluster', () => {
|
|
454
|
-
writeHdrpClusterAndSsaoBuffers(
|
|
455
|
-
internals,
|
|
456
|
-
frameState,
|
|
457
|
-
camera,
|
|
458
|
-
pointLights,
|
|
459
|
-
spotLights,
|
|
460
|
-
profilePhase,
|
|
461
|
-
pipelineState.hdrpClusterMembershipPipeline !== null &&
|
|
462
|
-
internals.device.caps?.compute === true,
|
|
463
|
-
pipelineState.hdrpClusterMembershipBindGroupLayout,
|
|
464
|
-
);
|
|
465
|
-
});
|
|
466
|
-
|
|
467
1266
|
// Zero-light standard-material once-warn (no Skylight + 0 direct light +
|
|
468
1267
|
// >=1 lit material -> black). Extracted to warnZeroLightStandard (M3/w18).
|
|
469
1268
|
warnZeroLightStandard(frameState, renderables, skylight, totalLightCount);
|
|
470
1269
|
|
|
471
|
-
return { foldBuckets, light };
|
|
1270
|
+
return { ok: true as const, value: { foldBuckets, light, standard } };
|
|
472
1271
|
});
|
|
1272
|
+
if (!lightingPreparation.ok) {
|
|
1273
|
+
internals.errorRegistry.fire(lightingPreparation.error);
|
|
1274
|
+
return false;
|
|
1275
|
+
}
|
|
1276
|
+
const { foldBuckets, light, standard } = lightingPreparation.value;
|
|
473
1277
|
|
|
474
1278
|
// Case E (this commit): 0 renderables = legitimate scene (LO §1.1
|
|
475
1279
|
// hello-window minimum semantic). Mirrors the Case C softening for
|
|
@@ -494,6 +1298,64 @@ export function recordFrame(
|
|
|
494
1298
|
const effectiveShadowMapSize = resolveShadowMapSize(internals, lights);
|
|
495
1299
|
const effectiveSpotShadowMapSize = resolveSpotShadowMapSize(internals, lights);
|
|
496
1300
|
const graphShadowMapSize = effectiveShadowMapSize ?? effectiveSpotShadowMapSize;
|
|
1301
|
+
const volumeCapability = hasVolumetricFogCapability(
|
|
1302
|
+
internals.device.caps,
|
|
1303
|
+
internals.volumetricFogShaders,
|
|
1304
|
+
);
|
|
1305
|
+
volumeInspectionReady = true;
|
|
1306
|
+
volumeCapabilityForInspection = volumeCapability;
|
|
1307
|
+
|
|
1308
|
+
const preparedSpotLightProjector = prepareSpotLightProjectorFrame(
|
|
1309
|
+
internals,
|
|
1310
|
+
world,
|
|
1311
|
+
worlds,
|
|
1312
|
+
lights,
|
|
1313
|
+
volumetricFog,
|
|
1314
|
+
);
|
|
1315
|
+
|
|
1316
|
+
const preparedVolumetricFog = prepareVolumetricFogFrame(
|
|
1317
|
+
internals,
|
|
1318
|
+
frameState,
|
|
1319
|
+
world,
|
|
1320
|
+
worlds,
|
|
1321
|
+
volumetricFog,
|
|
1322
|
+
lights,
|
|
1323
|
+
volumeCapability,
|
|
1324
|
+
preparedSpotLightProjector,
|
|
1325
|
+
);
|
|
1326
|
+
preparedVolumetricFogForInspection = preparedVolumetricFog;
|
|
1327
|
+
// A degraded authored snapshot must keep consuming the last accepted GPU
|
|
1328
|
+
// projection. The current source is still surfaced as a diagnostic, but
|
|
1329
|
+
// it must not replace the graph/context that produced the last good frame.
|
|
1330
|
+
// An absent component is different: a successful no-volume frame retires
|
|
1331
|
+
// the accepted projection and compiles the normal graph.
|
|
1332
|
+
const acceptedFallback =
|
|
1333
|
+
volumeCapability && volumetricFog !== undefined && preparedVolumetricFog === undefined
|
|
1334
|
+
? frameState.volumetricFogAccepted
|
|
1335
|
+
: undefined;
|
|
1336
|
+
const acceptedFallbackContext =
|
|
1337
|
+
acceptedFallback === undefined
|
|
1338
|
+
? undefined
|
|
1339
|
+
: stageAcceptedVolumetricFogFrame(
|
|
1340
|
+
internals,
|
|
1341
|
+
frameState,
|
|
1342
|
+
frameState.volumetricFogAcceptedContext,
|
|
1343
|
+
);
|
|
1344
|
+
const graphVolumetricFog = volumeCapability
|
|
1345
|
+
? preparedVolumetricFog === undefined
|
|
1346
|
+
? acceptedFallback
|
|
1347
|
+
: volumetricFog
|
|
1348
|
+
: undefined;
|
|
1349
|
+
const baseVolumetricFogContext = preparedVolumetricFog ?? acceptedFallbackContext;
|
|
1350
|
+
// A volume graph replacement is provisional whenever it can affect the
|
|
1351
|
+
// accepted volume projection. This includes authoring degradation (which
|
|
1352
|
+
// consumes the LKG graph), an authored replacement, and an explicit
|
|
1353
|
+
// fog-off transition. Non-volume frames retain the legacy immediate graph
|
|
1354
|
+
// retirement path.
|
|
1355
|
+
const volumeTopologyCandidate =
|
|
1356
|
+
(graphVolumetricFog?.status === 'available' && baseVolumetricFogContext !== undefined) ||
|
|
1357
|
+
(graphVolumetricFog === undefined && frameState.volumetricFogAccepted !== undefined) ||
|
|
1358
|
+
(volumetricFog?.status === 'degraded' && acceptedFallback !== undefined);
|
|
497
1359
|
|
|
498
1360
|
let preparedGpuDriven: PreparedGpuDrivenFrame | undefined;
|
|
499
1361
|
if (gpuDriven !== undefined) {
|
|
@@ -503,7 +1365,6 @@ export function recordFrame(
|
|
|
503
1365
|
meshes: pipelineState.meshes,
|
|
504
1366
|
viewBindGroupLayout: pipelineState.viewBindGroupLayout,
|
|
505
1367
|
meshResidencyEpoch: internals.gpuStore.meshResidencyEpoch,
|
|
506
|
-
hdrp: frameState.isHdrpActive,
|
|
507
1368
|
});
|
|
508
1369
|
if (prepared.ok) preparedGpuDriven = prepared.value;
|
|
509
1370
|
else if (prepared.error.code !== 'rhi-not-available')
|
|
@@ -517,7 +1378,36 @@ export function recordFrame(
|
|
|
517
1378
|
lights.pointShadow.length > 0 ||
|
|
518
1379
|
lights.spot.some((spot) => spot.castShadow);
|
|
519
1380
|
const gpuOwnsAllPrimaryRaster = preparedGpuDriven?.ownsAllRenderables === true;
|
|
520
|
-
gpuDriven?.onCoverage(
|
|
1381
|
+
gpuDriven?.onCoverage(
|
|
1382
|
+
gpuOwnsAllPrimaryRaster && !cpuShadowCastersActive,
|
|
1383
|
+
standard.kind === 'clustered',
|
|
1384
|
+
);
|
|
1385
|
+
|
|
1386
|
+
const cubeCaptureWork = prepareCubeCapture(internals, cubeCapture, camera);
|
|
1387
|
+
|
|
1388
|
+
// Validate renderables before graph admission. Besides producing the draw
|
|
1389
|
+
// plan, this is the owner of per-frame cache eviction; running it before a
|
|
1390
|
+
// graph candidate can fail guarantees despawned instance buffers are
|
|
1391
|
+
// released even when the frame cannot be submitted.
|
|
1392
|
+
const gpuOwnedEntityKeys = preparedGpuDriven?.entityKeys ?? EMPTY_ENTITY_KEYS;
|
|
1393
|
+
const validated = runRecordProfilePhase(profilePhase, 'record/validation', () =>
|
|
1394
|
+
gpuOwnsAllPrimaryRaster && !cpuShadowCastersActive && cubeCaptureWork.length === 0
|
|
1395
|
+
? []
|
|
1396
|
+
: validateRenderables(
|
|
1397
|
+
internals,
|
|
1398
|
+
world,
|
|
1399
|
+
worlds,
|
|
1400
|
+
pipelineState,
|
|
1401
|
+
frameState,
|
|
1402
|
+
renderables,
|
|
1403
|
+
transparentDispatch,
|
|
1404
|
+
cpuShadowCastersActive || cubeCaptureWork.length > 0
|
|
1405
|
+
? EMPTY_ENTITY_KEYS
|
|
1406
|
+
: gpuOwnedEntityKeys,
|
|
1407
|
+
),
|
|
1408
|
+
);
|
|
1409
|
+
gpuDriven?.owner.recordCpuValidation(validated, gpuOwnedEntityKeys);
|
|
1410
|
+
cleanPerFrameCaches(internals, frameState, validated, gpuOwnedEntityKeys);
|
|
521
1411
|
|
|
522
1412
|
const preflightGraph = runRecordProfilePhase(profilePhase, 'record/render-graph', () =>
|
|
523
1413
|
ensureCompiledFrameGraph(
|
|
@@ -531,12 +1421,60 @@ export function recordFrame(
|
|
|
531
1421
|
graphShadowMapSize,
|
|
532
1422
|
preparedGpuDriven,
|
|
533
1423
|
featureGraphCandidate,
|
|
1424
|
+
cubeCapture?.state,
|
|
534
1425
|
cameras.length === 0,
|
|
535
1426
|
transmissionDemand,
|
|
1427
|
+
graphVolumetricFog,
|
|
1428
|
+
volumeTopologyCandidate,
|
|
1429
|
+
standard,
|
|
536
1430
|
),
|
|
537
1431
|
);
|
|
538
1432
|
if (preflightGraph === null) return false;
|
|
539
1433
|
|
|
1434
|
+
// The compiled graph is the acceptance boundary for the prepared
|
|
1435
|
+
// Standard lighting declaration. Upload the same declaration only after
|
|
1436
|
+
// that graph is current; a failed candidate therefore cannot leave an old
|
|
1437
|
+
// graph consuming a new buffer generation.
|
|
1438
|
+
if (frameState.standardLightingGraphSignature !== standardLightingTopologySignature(standard)) {
|
|
1439
|
+
internals.errorRegistry.fire(
|
|
1440
|
+
new RendererOperationError('device-operation-failed', {
|
|
1441
|
+
operation: 'draw',
|
|
1442
|
+
cause: {
|
|
1443
|
+
code: 'standard-graph-generation-mismatch',
|
|
1444
|
+
expected: 'compiled graph and prepared Standard lighting share one topology signature',
|
|
1445
|
+
hint: 'retry the frame after the Standard graph candidate is accepted',
|
|
1446
|
+
detail: {
|
|
1447
|
+
expected: standardLightingTopologySignature(standard),
|
|
1448
|
+
actual: frameState.standardLightingGraphSignature,
|
|
1449
|
+
},
|
|
1450
|
+
},
|
|
1451
|
+
}),
|
|
1452
|
+
);
|
|
1453
|
+
return false;
|
|
1454
|
+
}
|
|
1455
|
+
if (standard.kind === 'clustered') {
|
|
1456
|
+
const clusterWrite = runRecordProfilePhase(
|
|
1457
|
+
profilePhase,
|
|
1458
|
+
'record/scene-state/hdrp-cluster',
|
|
1459
|
+
() =>
|
|
1460
|
+
writeHdrpClusterAndSsaoBuffers(
|
|
1461
|
+
internals,
|
|
1462
|
+
frameState,
|
|
1463
|
+
camera,
|
|
1464
|
+
standard.prepared,
|
|
1465
|
+
standard.transport,
|
|
1466
|
+
profilePhase,
|
|
1467
|
+
pipelineState.hdrpClusterMembershipPipeline !== null,
|
|
1468
|
+
pipelineState.hdrpClusterMembershipBindGroupLayout,
|
|
1469
|
+
preparedSpotLightProjector?.lightSlotIndex,
|
|
1470
|
+
),
|
|
1471
|
+
);
|
|
1472
|
+
if (!clusterWrite.ok) {
|
|
1473
|
+
internals.errorRegistry.fire(clusterWrite.error);
|
|
1474
|
+
return false;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
|
|
540
1478
|
// Acquire the swap-chain texture + colour view + target dimensions (with
|
|
541
1479
|
// one reconfigure-and-retry on surface-outdated). Extracted to
|
|
542
1480
|
// acquireSwapChainTarget (M3/w18); returns null on unrecoverable failure
|
|
@@ -623,6 +1561,35 @@ export function recordFrame(
|
|
|
623
1561
|
: { kind: 'taa', view: temporalView };
|
|
624
1562
|
const recordCamera: CameraSnapshot =
|
|
625
1563
|
temporalView === undefined ? camera : { ...camera, temporal: temporalView };
|
|
1564
|
+
let activeVolumetricFogContext: VolumetricFogFrameContext | undefined;
|
|
1565
|
+
let activeVolumetricFogSignature: VolumeTemporalSignature | undefined;
|
|
1566
|
+
if (graphVolumetricFog?.status === 'available' && baseVolumetricFogContext !== undefined) {
|
|
1567
|
+
const resolvedHistory = resolveVolumetricFogHistoryContext(
|
|
1568
|
+
frameState,
|
|
1569
|
+
preflightGraph,
|
|
1570
|
+
graphVolumetricFog,
|
|
1571
|
+
baseVolumetricFogContext,
|
|
1572
|
+
camera,
|
|
1573
|
+
lights,
|
|
1574
|
+
targetW,
|
|
1575
|
+
targetH,
|
|
1576
|
+
);
|
|
1577
|
+
activeVolumetricFogContext = resolvedHistory.context;
|
|
1578
|
+
activeVolumetricFogSignature = resolvedHistory.signature;
|
|
1579
|
+
const historyFlag = new Float32Array([activeVolumetricFogContext.historyValid ? 1 : 0]);
|
|
1580
|
+
if (frameState.volumetricFogPendingParams !== undefined) {
|
|
1581
|
+
frameState.volumetricFogPendingParams[31] = historyFlag[0] ?? 0;
|
|
1582
|
+
}
|
|
1583
|
+
const historyFlagWritten = internals.device.queue.writeBuffer(
|
|
1584
|
+
activeVolumetricFogContext.paramsBuffer,
|
|
1585
|
+
31 * Float32Array.BYTES_PER_ELEMENT,
|
|
1586
|
+
historyFlag,
|
|
1587
|
+
);
|
|
1588
|
+
if (!historyFlagWritten.ok) {
|
|
1589
|
+
internals.errorRegistry.fire(historyFlagWritten.error);
|
|
1590
|
+
return false;
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
626
1593
|
|
|
627
1594
|
const depthView: TextureView | null = null;
|
|
628
1595
|
// feat-20260709 M3 / D-3: clear-color read from the active CameraSnapshot's
|
|
@@ -691,41 +1658,6 @@ export function recordFrame(
|
|
|
691
1658
|
const geometryColorResolveView: TextureView | null = null;
|
|
692
1659
|
const ldrSpriteColorView: TextureView | null = null;
|
|
693
1660
|
|
|
694
|
-
// Validate renderable handles + collect render plan. Empty `renderables`
|
|
695
|
-
// input or all-unregistered handles both produce `validatedOrdered.length === 0`,
|
|
696
|
-
// which is the Case E (clear-pass-only) path.
|
|
697
|
-
//
|
|
698
|
-
// feat-20260520-2d-sprite-layer-mvp M-3 / w25 (@new-surface): per-entry
|
|
699
|
-
// `renderableIndex` tracks the position into the original renderables[]
|
|
700
|
-
// array so the bucket-aware reordering below can correlate
|
|
701
|
-
// transparent-sort output (TransparentEntry.renderableIndex) back to
|
|
702
|
-
// the validated row.
|
|
703
|
-
// bug-20260527-renderstate-pipeline-dispatch-gap D-4:
|
|
704
|
-
// build a renderableIndex -> renderState map from dispatch entries
|
|
705
|
-
// so each ValidatedRenderable carries its per-material renderState
|
|
706
|
-
// override without an O(n^2) back-scan in the draw loop.
|
|
707
|
-
const gpuOwnedEntityKeys = preparedGpuDriven?.entityKeys ?? EMPTY_ENTITY_KEYS;
|
|
708
|
-
const validated = runRecordProfilePhase(profilePhase, 'record/validation', () =>
|
|
709
|
-
gpuOwnsAllPrimaryRaster && !cpuShadowCastersActive
|
|
710
|
-
? []
|
|
711
|
-
: validateRenderables(
|
|
712
|
-
internals,
|
|
713
|
-
world,
|
|
714
|
-
worlds,
|
|
715
|
-
pipelineState,
|
|
716
|
-
frameState,
|
|
717
|
-
renderables,
|
|
718
|
-
transparentDispatch,
|
|
719
|
-
cpuShadowCastersActive ? EMPTY_ENTITY_KEYS : gpuOwnedEntityKeys,
|
|
720
|
-
),
|
|
721
|
-
);
|
|
722
|
-
gpuDriven?.owner.recordCpuValidation(validated, gpuOwnedEntityKeys);
|
|
723
|
-
|
|
724
|
-
// Per-frame cache clean-up: drop per-entity BG cache entries + instance
|
|
725
|
-
// buffers whose entityKey is absent from the current validated set (despawn
|
|
726
|
-
// eviction). Extracted to cleanPerFrameCaches (M3/w18).
|
|
727
|
-
cleanPerFrameCaches(internals, frameState, validated, gpuOwnedEntityKeys);
|
|
728
|
-
|
|
729
1661
|
// Dispatch-ordered render plan: reorder validated renderables to dispatch
|
|
730
1662
|
// order, run the mesh-SSBO capacity gate (graceful truncation), and build
|
|
731
1663
|
// the fold dispatch plan. Extracted to buildDispatchPlan (M3/w18).
|
|
@@ -772,7 +1704,18 @@ export function recordFrame(
|
|
|
772
1704
|
// feat-20260531-skybox-env-background M2 / w6: gate relaxed from
|
|
773
1705
|
// `validatedOrdered.length > 0` to include skybox-only frames
|
|
774
1706
|
// (plan-strategy D-3, R-3).
|
|
775
|
-
|
|
1707
|
+
const currentViewProjection = mat4.create();
|
|
1708
|
+
mat4.multiply(
|
|
1709
|
+
currentViewProjection,
|
|
1710
|
+
computeProjectionMatrix(camera),
|
|
1711
|
+
computeViewMatrix(camera),
|
|
1712
|
+
);
|
|
1713
|
+
if (
|
|
1714
|
+
validatedOrdered.length > 0 ||
|
|
1715
|
+
skyboxActive ||
|
|
1716
|
+
activeVolumetricFogContext !== undefined ||
|
|
1717
|
+
preparedVolumetricFog !== undefined
|
|
1718
|
+
) {
|
|
776
1719
|
runRecordProfilePhase(profilePhase, 'record/uploads', () => {
|
|
777
1720
|
// View UBO + CSM/spot-shadow matrix pack: assembled + uploaded in one
|
|
778
1721
|
// queue.writeBuffer round-trip. Extracted to view-ubo.ts (M3/w18) so
|
|
@@ -786,6 +1729,7 @@ export function recordFrame(
|
|
|
786
1729
|
frameState.spotShadowSnapshots,
|
|
787
1730
|
fog,
|
|
788
1731
|
temporalView,
|
|
1732
|
+
preparedSpotLightProjector?.spotIndex,
|
|
789
1733
|
);
|
|
790
1734
|
if (pipelineState.pointsLinesViewBuffer !== undefined) {
|
|
791
1735
|
writePointsLinesViewUbo(
|
|
@@ -802,11 +1746,6 @@ export function recordFrame(
|
|
|
802
1746
|
lights,
|
|
803
1747
|
);
|
|
804
1748
|
|
|
805
|
-
// Per-frame full rewrite of the PointLight + SpotLight std430 storage
|
|
806
|
-
// buffers (header + first-slice cap N=4 slots). Extracted to
|
|
807
|
-
// writePointSpotLightBuffers (M3/w18).
|
|
808
|
-
writePointSpotLightBuffers(internals, pipelineState, lights);
|
|
809
|
-
|
|
810
1749
|
// Per-renderable entity_world upload (batched): all N mat4+normalMatrix
|
|
811
1750
|
// slots assembled into a single contiguous scratch buffer, then flushed
|
|
812
1751
|
// as one writeBuffer call. Extracted to mesh-ssbo.ts (M3/w21) so its
|
|
@@ -855,10 +1794,40 @@ export function recordFrame(
|
|
|
855
1794
|
validated.length > 0 || preparedGpuDriven !== undefined,
|
|
856
1795
|
bindGroupCounts,
|
|
857
1796
|
undefined,
|
|
858
|
-
|
|
1797
|
+
true,
|
|
1798
|
+
standard,
|
|
859
1799
|
),
|
|
860
1800
|
);
|
|
861
1801
|
|
|
1802
|
+
for (const capture of cubeCaptureWork) {
|
|
1803
|
+
writeViewUbo(
|
|
1804
|
+
internals.device.queue,
|
|
1805
|
+
pipelineState.viewUniformBuffer,
|
|
1806
|
+
capture.faceCamera,
|
|
1807
|
+
light,
|
|
1808
|
+
lights,
|
|
1809
|
+
frameState.spotShadowSnapshots,
|
|
1810
|
+
capture.viewBindGroupDynamicOffset,
|
|
1811
|
+
undefined,
|
|
1812
|
+
preparedSpotLightProjector?.spotIndex,
|
|
1813
|
+
);
|
|
1814
|
+
}
|
|
1815
|
+
for (const probe of cubeCapture?.reflectionProbes?.graph.work ?? []) {
|
|
1816
|
+
if (probe.faceCamera === undefined || probe.viewBindGroupDynamicOffset === undefined)
|
|
1817
|
+
continue;
|
|
1818
|
+
writeViewUbo(
|
|
1819
|
+
internals.device.queue,
|
|
1820
|
+
pipelineState.viewUniformBuffer,
|
|
1821
|
+
probe.faceCamera,
|
|
1822
|
+
light,
|
|
1823
|
+
lights,
|
|
1824
|
+
frameState.spotShadowSnapshots,
|
|
1825
|
+
probe.viewBindGroupDynamicOffset,
|
|
1826
|
+
undefined,
|
|
1827
|
+
preparedSpotLightProjector?.spotIndex,
|
|
1828
|
+
);
|
|
1829
|
+
}
|
|
1830
|
+
|
|
862
1831
|
// ── feat-20260529-rendergraph-pass-abstraction M4 / w13b ───────────
|
|
863
1832
|
// The compiled typed graph is the sole per-frame pass owner.
|
|
864
1833
|
//
|
|
@@ -922,20 +1891,48 @@ export function recordFrame(
|
|
|
922
1891
|
geometryColorResolveView,
|
|
923
1892
|
ldrSpriteColorView,
|
|
924
1893
|
postProcessParams: framePostProcessParams,
|
|
1894
|
+
...(internals.volumetricFogShaders === undefined
|
|
1895
|
+
? {}
|
|
1896
|
+
: { volumetricFogShaders: internals.volumetricFogShaders }),
|
|
1897
|
+
...(activeVolumetricFogContext === undefined
|
|
1898
|
+
? {}
|
|
1899
|
+
: { volumetricFog: activeVolumetricFogContext }),
|
|
1900
|
+
...(preparedSpotLightProjector === undefined
|
|
1901
|
+
? {}
|
|
1902
|
+
: { spotLightProjector: preparedSpotLightProjector }),
|
|
925
1903
|
dispatch: transparentDispatch,
|
|
926
1904
|
hdrpClusterBindGroup,
|
|
927
1905
|
hdrpClusterMembershipBindGroup,
|
|
1906
|
+
standardLighting: standard,
|
|
928
1907
|
foldDispatchPlan,
|
|
929
1908
|
materialSlotIndices,
|
|
930
1909
|
materialSlots,
|
|
931
1910
|
materialSlotOwners,
|
|
932
1911
|
materialSlotCount,
|
|
1912
|
+
...(cubeCapture?.reflectionProbes === undefined
|
|
1913
|
+
? {}
|
|
1914
|
+
: { reflectionProbes: cubeCapture.reflectionProbes }),
|
|
933
1915
|
pointsLines: pointsLinesOwner,
|
|
934
1916
|
materialBgAssemblyCache: frameState.materialBgAssemblyCache,
|
|
935
1917
|
directionalShadowCacheReuse: directionalShadowCache.reuse,
|
|
936
1918
|
...(profilePhase !== undefined ? { profilePhase } : {}),
|
|
937
1919
|
};
|
|
938
|
-
|
|
1920
|
+
let completion: Promise<unknown> | undefined;
|
|
1921
|
+
const stagedTemporalFrame = {
|
|
1922
|
+
frameId: frameState.frameNumber,
|
|
1923
|
+
currentViewProjection: new Float32Array(currentViewProjection),
|
|
1924
|
+
jitter: [0, 0] as [number, number],
|
|
1925
|
+
viewport: { width: targetW, height: targetH },
|
|
1926
|
+
cameraPosition: [
|
|
1927
|
+
camera.position[0] ?? 0,
|
|
1928
|
+
camera.position[1] ?? 0,
|
|
1929
|
+
camera.position[2] ?? 0,
|
|
1930
|
+
] as [number, number, number],
|
|
1931
|
+
};
|
|
1932
|
+
frameState.temporalFrameInput = stagedTemporalFrame;
|
|
1933
|
+
frameState.temporalFrameTransaction.stage(stagedTemporalFrame);
|
|
1934
|
+
volumeSubmissionAttempted = volumeTopologyCandidate || activeVolumetricFogContext !== undefined;
|
|
1935
|
+
submitted = runRecordProfilePhase(profilePhase, 'record/graph-execute', () =>
|
|
939
1936
|
executeCompiledFrameGraph(
|
|
940
1937
|
internals,
|
|
941
1938
|
frameState,
|
|
@@ -944,19 +1941,92 @@ export function recordFrame(
|
|
|
944
1941
|
profilePhase === undefined
|
|
945
1942
|
? undefined
|
|
946
1943
|
: (pass, encode) => profilePhase(graphExecutionPhase(pass.name), encode),
|
|
1944
|
+
cubeCaptureWork.length === 0 ? undefined : cubeCaptureWork.map((work) => work.faceIndex),
|
|
1945
|
+
(done) => {
|
|
1946
|
+
completion = done;
|
|
1947
|
+
},
|
|
1948
|
+
timingCapture,
|
|
947
1949
|
),
|
|
948
1950
|
);
|
|
1951
|
+
if (cubeCapture !== undefined && cubeCaptureWork.length > 0) {
|
|
1952
|
+
const completed = cubeCapture.scheduler.completeSubmission(submitted, completion);
|
|
1953
|
+
if (!completed.ok) internals.errorRegistry.fire(completed.error);
|
|
1954
|
+
}
|
|
1955
|
+
cubeCapture?.reflectionProbes?.completeSubmission(submitted, completion);
|
|
949
1956
|
if (submitted && !directionalShadowCache.reuse) {
|
|
950
1957
|
frameState.directionalShadowCache = frameState.directionalShadowCacheRecorded
|
|
951
1958
|
? directionalShadowCache.next
|
|
952
1959
|
: null;
|
|
953
1960
|
}
|
|
1961
|
+
if (submitted) {
|
|
1962
|
+
if (graphVolumetricFog === undefined) {
|
|
1963
|
+
frameState.volumetricFogAccepted = undefined;
|
|
1964
|
+
frameState.volumetricFogAcceptedContext = undefined;
|
|
1965
|
+
frameState.volumetricFogHistoryGraph = null;
|
|
1966
|
+
frameState.volumetricFogHistorySlot = null;
|
|
1967
|
+
frameState.volumetricFogHistorySignature = null;
|
|
1968
|
+
retireVolumetricFogParams(internals, frameState);
|
|
1969
|
+
} else if (
|
|
1970
|
+
activeVolumetricFogContext !== undefined &&
|
|
1971
|
+
activeVolumetricFogSignature !== undefined
|
|
1972
|
+
) {
|
|
1973
|
+
// Temporal history is graph-owned state. Promote the write slot only
|
|
1974
|
+
// after queue submission succeeds; a failed candidate keeps the
|
|
1975
|
+
// accepted graph/context/slot and retries into the same pending slot.
|
|
1976
|
+
frameState.volumetricFogHistoryGraph = preflightGraph;
|
|
1977
|
+
frameState.volumetricFogHistorySlot = activeVolumetricFogContext.historyWriteSlot;
|
|
1978
|
+
frameState.volumetricFogHistorySignature = activeVolumetricFogSignature;
|
|
1979
|
+
if (frameState.volumetricFogParamsPendingSlot !== null) {
|
|
1980
|
+
frameState.volumetricFogParamsAcceptedSlot = frameState.volumetricFogParamsPendingSlot;
|
|
1981
|
+
frameState.volumetricFogAcceptedParams =
|
|
1982
|
+
frameState.volumetricFogPendingParams === undefined
|
|
1983
|
+
? undefined
|
|
1984
|
+
: new Float32Array(frameState.volumetricFogPendingParams);
|
|
1985
|
+
frameState.volumetricFogParamsPendingSlot = null;
|
|
1986
|
+
frameState.volumetricFogPendingParams = undefined;
|
|
1987
|
+
}
|
|
1988
|
+
if (volumetricFog?.status === 'available' && preparedVolumetricFog !== undefined) {
|
|
1989
|
+
frameState.volumetricFogAccepted = volumetricFog;
|
|
1990
|
+
frameState.volumetricFogAcceptedContext = activeVolumetricFogContext;
|
|
1991
|
+
} else if (acceptedFallback !== undefined) {
|
|
1992
|
+
// The current authored source is degraded, so keep the old accepted
|
|
1993
|
+
// POD while advancing the LKG graph's ping-pong slot.
|
|
1994
|
+
frameState.volumetricFogAcceptedContext = activeVolumetricFogContext;
|
|
1995
|
+
}
|
|
1996
|
+
} else if (volumetricFog?.status === 'available' && preparedVolumetricFog !== undefined) {
|
|
1997
|
+
// Defensive fallback for a volume graph that was accepted before the
|
|
1998
|
+
// temporal context could be resolved; do not manufacture history.
|
|
1999
|
+
frameState.volumetricFogAccepted = volumetricFog;
|
|
2000
|
+
frameState.volumetricFogAcceptedContext = preparedVolumetricFog;
|
|
2001
|
+
}
|
|
2002
|
+
frameState.standardLightingInspection = inspectStandardLighting(standard);
|
|
2003
|
+
frameState.pointShadowInspection = inspectPointShadow(
|
|
2004
|
+
lights.pointShadow,
|
|
2005
|
+
SHADOW_ATLAS_DEFAULT_LAYERS,
|
|
2006
|
+
);
|
|
2007
|
+
}
|
|
954
2008
|
return submitted;
|
|
955
2009
|
} finally {
|
|
2010
|
+
// Volume topology replacements are provisional until the queue submission
|
|
2011
|
+
// succeeds. This runs for every early-return path as well, so a swapchain,
|
|
2012
|
+
// encoder, or graph failure cannot leave a non-submitted candidate as the
|
|
2013
|
+
// next frame's accepted graph.
|
|
2014
|
+
settleVolumetricFogGraphCandidate(frameState, submitted);
|
|
2015
|
+
if (volumeInspectionReady && (submitted || volumeSubmissionAttempted)) {
|
|
2016
|
+
updateVolumetricFogInspection(
|
|
2017
|
+
frameState,
|
|
2018
|
+
volumetricFog,
|
|
2019
|
+
lights,
|
|
2020
|
+
preparedVolumetricFogForInspection,
|
|
2021
|
+
submitted,
|
|
2022
|
+
volumeCapabilityForInspection,
|
|
2023
|
+
volumeSubmissionAttempted,
|
|
2024
|
+
);
|
|
2025
|
+
}
|
|
956
2026
|
frameState.frameNumber += 1;
|
|
957
|
-
// feat-20260608-cluster-lighting M5 / w22: clear
|
|
2027
|
+
// feat-20260608-cluster-lighting M5 / w22: clear Standard once-per-frame fired
|
|
958
2028
|
// set so the next frame re-fires if the condition persists.
|
|
959
|
-
frameState.
|
|
2029
|
+
frameState.standardOncePerFrameFired.clear();
|
|
960
2030
|
}
|
|
961
2031
|
}
|
|
962
2032
|
|
|
@@ -1024,6 +2094,20 @@ function sameLightViewProj(
|
|
|
1024
2094
|
return true;
|
|
1025
2095
|
}
|
|
1026
2096
|
|
|
2097
|
+
function sameDirectionalShadowQuality(
|
|
2098
|
+
previous: DirectionalShadowQuality,
|
|
2099
|
+
next: DirectionalShadowQuality,
|
|
2100
|
+
): boolean {
|
|
2101
|
+
if (previous.kind !== next.kind) return false;
|
|
2102
|
+
if (previous.kind === 'pcf' && next.kind === 'pcf') return previous.kernel === next.kernel;
|
|
2103
|
+
if (previous.kind !== 'pcss' || next.kind !== 'pcss') return false;
|
|
2104
|
+
return (
|
|
2105
|
+
previous.preset === next.preset &&
|
|
2106
|
+
previous.angularRadiusRadians === next.angularRadiusRadians &&
|
|
2107
|
+
previous.maxPenumbraTexels === next.maxPenumbraTexels
|
|
2108
|
+
);
|
|
2109
|
+
}
|
|
2110
|
+
|
|
1027
2111
|
function prepareDirectionalShadowCache(
|
|
1028
2112
|
internals: RenderSystemInternals,
|
|
1029
2113
|
frameState: RenderFrameState,
|
|
@@ -1043,6 +2127,7 @@ function prepareDirectionalShadowCache(
|
|
|
1043
2127
|
cascadeCount === undefined ||
|
|
1044
2128
|
cascadeCount <= 0 ||
|
|
1045
2129
|
lightViewProj === undefined ||
|
|
2130
|
+
lights.directionalShadowQuality === undefined ||
|
|
1046
2131
|
validatedOrdered.length === 0
|
|
1047
2132
|
) {
|
|
1048
2133
|
return { reuse: false, next: null };
|
|
@@ -1055,6 +2140,10 @@ function prepareDirectionalShadowCache(
|
|
|
1055
2140
|
cached !== null &&
|
|
1056
2141
|
cached.shadowMapSize === shadowMapSize &&
|
|
1057
2142
|
cached.cascadeCount === cascadeCount &&
|
|
2143
|
+
sameDirectionalShadowQuality(
|
|
2144
|
+
cached.directionalShadowQuality,
|
|
2145
|
+
lights.directionalShadowQuality,
|
|
2146
|
+
) &&
|
|
1058
2147
|
cached.pipelineHandle === frameState.installedPipelineHandle &&
|
|
1059
2148
|
cached.graphTopologyKey === frameState.compiledFrameGraphTopologyKey &&
|
|
1060
2149
|
cached.assetCatalogEpoch === assetCatalogEpoch &&
|
|
@@ -1083,6 +2172,7 @@ function prepareDirectionalShadowCache(
|
|
|
1083
2172
|
graphTopologyKey: frameState.compiledFrameGraphTopologyKey,
|
|
1084
2173
|
shadowMapSize,
|
|
1085
2174
|
cascadeCount,
|
|
2175
|
+
directionalShadowQuality: lights.directionalShadowQuality,
|
|
1086
2176
|
lightViewProj: lightViewProj.map((matrix) => new Float32Array(matrix)),
|
|
1087
2177
|
meshResidencyEpoch,
|
|
1088
2178
|
},
|