@forgeax/engine-render 0.1.27 → 0.1.29
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 +842 -124
- package/dist/assembly/atmosphere-shader-prewarm.d.ts +6 -0
- package/dist/assembly/atmosphere-shader-prewarm.d.ts.map +1 -0
- package/dist/assembly/dynamic-geometry-host.d.ts +52 -0
- package/dist/assembly/dynamic-geometry-host.d.ts.map +1 -0
- package/dist/assembly/dynamic-geometry-runtime.d.ts +20 -0
- package/dist/assembly/dynamic-geometry-runtime.d.ts.map +1 -0
- package/dist/assembly/factory.d.ts +3 -1
- package/dist/assembly/factory.d.ts.map +1 -1
- package/dist/assembly/gpu-driven-owner.d.ts +10 -0
- package/dist/assembly/gpu-driven-owner.d.ts.map +1 -0
- package/dist/assembly/host-contract.d.ts +19 -0
- package/dist/assembly/host-contract.d.ts.map +1 -1
- package/dist/assembly/material/assembly.d.ts +7 -0
- package/dist/assembly/material/assembly.d.ts.map +1 -1
- package/dist/assembly/material-shader-policy.d.ts +3 -1
- package/dist/assembly/material-shader-policy.d.ts.map +1 -1
- package/dist/assembly/recovery/failure-location.d.ts +12 -0
- package/dist/assembly/recovery/failure-location.d.ts.map +1 -0
- package/dist/assembly/recovery/renderer-recover.d.ts +6 -4
- package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -1
- package/dist/assembly/renderer-facade.d.ts.map +1 -1
- package/dist/assembly/renderer-feature-inspection.d.ts +5 -0
- package/dist/assembly/renderer-feature-inspection.d.ts.map +1 -0
- package/dist/assembly/renderer-frame-transaction.d.ts +19 -0
- package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
- package/dist/assembly/renderer-helpers.d.ts +3 -0
- package/dist/assembly/renderer-helpers.d.ts.map +1 -1
- package/dist/assembly/renderer-inspection.d.ts +4 -0
- package/dist/assembly/renderer-inspection.d.ts.map +1 -1
- package/dist/assembly/shader-prewarm-policy.d.ts +6 -0
- package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -1
- package/dist/assembly/skin-palette-owner.d.ts +8 -0
- package/dist/assembly/skin-palette-owner.d.ts.map +1 -0
- package/dist/assembly/ssr-shader-prewarm.d.ts +6 -0
- package/dist/assembly/ssr-shader-prewarm.d.ts.map +1 -0
- package/dist/assembly/webgpu-device-loss.d.ts +15 -0
- package/dist/assembly/webgpu-device-loss.d.ts.map +1 -0
- package/dist/assembly/webgpu-pbr-ready.d.ts +38 -0
- package/dist/assembly/webgpu-pbr-ready.d.ts.map +1 -0
- package/dist/assembly/webgpu-ready-mipmap.d.ts +11 -0
- package/dist/assembly/webgpu-ready-mipmap.d.ts.map +1 -0
- package/dist/assembly/webgpu-ready.d.ts +7 -3
- package/dist/assembly/webgpu-ready.d.ts.map +1 -1
- package/dist/assembly/webgpu-renderer-bootstrap.d.ts +10 -0
- package/dist/assembly/webgpu-renderer-bootstrap.d.ts.map +1 -0
- package/dist/assembly/webgpu-renderer-contract.d.ts +62 -0
- package/dist/assembly/webgpu-renderer-contract.d.ts.map +1 -0
- package/dist/assembly/webgpu-renderer-guards.d.ts +9 -0
- package/dist/assembly/webgpu-renderer-guards.d.ts.map +1 -0
- package/dist/assembly/webgpu-renderer-observation.d.ts +6 -0
- package/dist/assembly/webgpu-renderer-observation.d.ts.map +1 -0
- package/dist/assembly/webgpu-renderer-recovery-failure.d.ts +12 -0
- package/dist/assembly/webgpu-renderer-recovery-failure.d.ts.map +1 -0
- package/dist/assembly/webgpu-renderer.d.ts +7 -141
- package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
- package/dist/assembly/webgpu-vertex-layouts.d.ts +28 -84
- package/dist/assembly/webgpu-vertex-layouts.d.ts.map +1 -1
- package/dist/authoring.mjs +3 -3
- package/dist/{chunk-ADHHVYLW.mjs → chunk-33XZFTQU.mjs} +245 -39
- package/dist/chunk-33XZFTQU.mjs.map +1 -0
- package/dist/{chunk-GF523LHF.mjs → chunk-BLXGONR2.mjs} +30 -12
- package/dist/chunk-BLXGONR2.mjs.map +1 -0
- package/dist/{chunk-KM2NOX2I.mjs → chunk-BQVQOQJH.mjs} +958 -254
- package/dist/chunk-BQVQOQJH.mjs.map +1 -0
- package/dist/{chunk-HKXTW355.mjs → chunk-BTWH7BP4.mjs} +386 -17
- package/dist/chunk-BTWH7BP4.mjs.map +1 -0
- package/dist/{chunk-TMIASV2N.mjs → chunk-FUODTTIA.mjs} +3934 -676
- package/dist/chunk-FUODTTIA.mjs.map +1 -0
- package/dist/{chunk-FJ6P52EE.mjs → chunk-ILQ5MEZF.mjs} +3 -17
- package/dist/chunk-ILQ5MEZF.mjs.map +1 -0
- package/dist/{chunk-E34VL5VI.mjs → chunk-JQTSEI3Z.mjs} +3 -3
- package/dist/{chunk-E34VL5VI.mjs.map → chunk-JQTSEI3Z.mjs.map} +1 -1
- package/dist/{chunk-GE3SDD72.mjs → chunk-NICXSHAT.mjs} +16 -38
- package/dist/chunk-NICXSHAT.mjs.map +1 -0
- package/dist/{chunk-ZZ4YQ474.mjs → chunk-NRNXBLIJ.mjs} +577 -7
- package/dist/chunk-NRNXBLIJ.mjs.map +1 -0
- package/dist/{chunk-JDWARUOI.mjs → chunk-PA5DRJNI.mjs} +17 -3
- package/dist/chunk-PA5DRJNI.mjs.map +1 -0
- package/dist/{chunk-XDP6XE5V.mjs → chunk-S4DNKQYQ.mjs} +1118 -446
- package/dist/chunk-S4DNKQYQ.mjs.map +1 -0
- package/dist/components/camera.d.ts +62 -2
- package/dist/components/camera.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/instances.d.ts +6 -20
- package/dist/components/instances.d.ts.map +1 -1
- package/dist/components/scene-instance.d.ts +2 -1
- package/dist/components/scene-instance.d.ts.map +1 -1
- package/dist/components/screen-space-reflection.d.ts +14 -0
- package/dist/components/screen-space-reflection.d.ts.map +1 -0
- package/dist/construct-renderer.mjs +13805 -7186
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/device/gpu-residency.d.ts +11 -17
- package/dist/device/gpu-residency.d.ts.map +1 -1
- package/dist/device/mesh-residency-lifetime.d.ts +21 -0
- package/dist/device/mesh-residency-lifetime.d.ts.map +1 -0
- package/dist/dynamic-geometry.d.ts +125 -0
- package/dist/dynamic-geometry.d.ts.map +1 -0
- package/dist/environment/background.d.ts +6 -0
- package/dist/environment/background.d.ts.map +1 -0
- package/dist/errors/gpu-driven.d.ts +6 -3
- package/dist/errors/gpu-driven.d.ts.map +1 -1
- package/dist/errors/render.d.ts +7 -4
- package/dist/errors/render.d.ts.map +1 -1
- package/dist/extract/camera.d.ts.map +1 -1
- package/dist/extract/gpu-driven.d.ts +33 -3
- package/dist/extract/gpu-driven.d.ts.map +1 -1
- package/dist/features/host.d.ts +23 -1
- package/dist/features/host.d.ts.map +1 -1
- package/dist/features/noise-texture.d.ts +17 -0
- package/dist/features/noise-texture.d.ts.map +1 -0
- package/dist/features/plan.d.ts +83 -2
- package/dist/features/plan.d.ts.map +1 -1
- package/dist/features/prepared-gpu-work.d.ts +37 -4
- package/dist/features/prepared-gpu-work.d.ts.map +1 -1
- package/dist/features/prepared-graphics-store.d.ts.map +1 -1
- package/dist/features/prepared-graphics.d.ts +5 -0
- package/dist/features/prepared-graphics.d.ts.map +1 -1
- package/dist/features/render-graph-compute.d.ts +5 -3
- package/dist/features/render-graph-compute.d.ts.map +1 -1
- package/dist/features/render-graph-contribution.d.ts +11 -3
- package/dist/features/render-graph-contribution.d.ts.map +1 -1
- package/dist/features/render-graph-raster.d.ts +19 -3
- package/dist/features/render-graph-raster.d.ts.map +1 -1
- package/dist/features/types.d.ts +11 -0
- package/dist/features/types.d.ts.map +1 -1
- package/dist/fullscreen-post-process-pass.d.ts +2 -0
- package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
- package/dist/gpu-driven/batch-topology.d.ts +34 -1
- package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
- package/dist/gpu-driven/pbr-program.d.ts +10 -0
- package/dist/gpu-driven/pbr-program.d.ts.map +1 -0
- package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
- package/dist/gpu-driven/production-raster.d.ts +144 -23
- package/dist/gpu-driven/production-raster.d.ts.map +1 -1
- package/dist/gpu-driven/shadow-views.d.ts +126 -0
- package/dist/gpu-driven/shadow-views.d.ts.map +1 -0
- package/dist/gpu-driven/view-gpu.d.ts +18 -2
- package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
- package/dist/gpu-scene-schema.d.ts +2 -8
- package/dist/gpu-scene-schema.d.ts.map +1 -1
- package/dist/gpu-scene.d.ts +14 -0
- package/dist/gpu-scene.d.ts.map +1 -1
- package/dist/gpu-texture-usage.d.ts +1 -0
- package/dist/gpu-texture-usage.d.ts.map +1 -1
- package/dist/hdrp-buffers.d.ts +6 -0
- package/dist/hdrp-buffers.d.ts.map +1 -1
- package/dist/ibl/frame-resources.d.ts +8 -0
- package/dist/ibl/frame-resources.d.ts.map +1 -0
- package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
- package/dist/index.d.ts +21 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +8 -7
- package/dist/index.mjs.map +1 -1
- package/dist/inspection-types.d.ts +164 -0
- package/dist/inspection-types.d.ts.map +1 -1
- package/dist/instances-derived-bounds.d.ts +11 -0
- package/dist/instances-derived-bounds.d.ts.map +1 -1
- package/dist/instances.d.ts +89 -44
- package/dist/instances.d.ts.map +1 -1
- package/dist/internal.d.ts +3 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.mjs +118 -10
- package/dist/internal.mjs.map +1 -1
- package/dist/lifecycle.d.ts +2 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/material-row.d.ts +5 -0
- package/dist/material-row.d.ts.map +1 -0
- package/dist/materials.d.ts +15 -0
- package/dist/materials.d.ts.map +1 -1
- package/dist/pbr-pipeline.d.ts +21 -5
- package/dist/pbr-pipeline.d.ts.map +1 -1
- package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
- package/dist/pipeline/standard-output/auto-exposure/capability.d.ts +12 -0
- package/dist/pipeline/standard-output/auto-exposure/capability.d.ts.map +1 -0
- package/dist/pipeline/standard-output/auto-exposure/gpu.d.ts +70 -0
- package/dist/pipeline/standard-output/auto-exposure/gpu.d.ts.map +1 -0
- package/dist/pipeline/standard-output/auto-exposure/graph.d.ts +75 -0
- package/dist/pipeline/standard-output/auto-exposure/graph.d.ts.map +1 -0
- package/dist/pipeline/standard-output/auto-exposure/inspection.d.ts +111 -0
- package/dist/pipeline/standard-output/auto-exposure/inspection.d.ts.map +1 -0
- package/dist/pipeline/standard-output/auto-exposure/oracle.d.ts +16 -0
- package/dist/pipeline/standard-output/auto-exposure/oracle.d.ts.map +1 -0
- package/dist/pipeline/standard-output/auto-exposure/preset.d.ts +14 -0
- package/dist/pipeline/standard-output/auto-exposure/preset.d.ts.map +1 -0
- package/dist/pipeline/standard-output/auto-exposure/state.d.ts +46 -0
- package/dist/pipeline/standard-output/auto-exposure/state.d.ts.map +1 -0
- package/dist/pipeline/standard-output/color-transform.d.ts +27 -0
- package/dist/pipeline/standard-output/color-transform.d.ts.map +1 -0
- package/dist/pipeline/standard-output/graph.d.ts +15 -0
- package/dist/pipeline/standard-output/graph.d.ts.map +1 -0
- package/dist/pipeline/standard-output/lut-admission.d.ts +29 -0
- package/dist/pipeline/standard-output/lut-admission.d.ts.map +1 -0
- package/dist/pipeline/standard-output/lut-gpu.d.ts +48 -0
- package/dist/pipeline/standard-output/lut-gpu.d.ts.map +1 -0
- package/dist/pipeline/standard-output/lut-state.d.ts +68 -0
- package/dist/pipeline/standard-output/lut-state.d.ts.map +1 -0
- package/dist/pipeline/standard-output/resources.d.ts +10 -0
- package/dist/pipeline/standard-output/resources.d.ts.map +1 -0
- package/dist/pipeline/standard-output/types.d.ts +39 -0
- package/dist/pipeline/standard-output/types.d.ts.map +1 -0
- package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
- package/dist/pipeline/standard-post.d.ts +2 -2
- package/dist/pipeline/standard-post.d.ts.map +1 -1
- package/dist/pipeline-builder.d.ts +2 -1
- package/dist/pipeline-builder.d.ts.map +1 -1
- package/dist/pipeline-spec-types.d.ts +4 -0
- package/dist/pipeline-spec-types.d.ts.map +1 -1
- package/dist/pipeline-spec.d.ts +1 -9
- package/dist/pipeline-spec.d.ts.map +1 -1
- package/dist/points-lines/standard-owner.d.ts +1 -1
- package/dist/points-lines/standard-owner.d.ts.map +1 -1
- package/dist/prepare/prepared-graphics-resolver.d.ts +6 -1
- package/dist/prepare/prepared-graphics-resolver.d.ts.map +1 -1
- package/dist/record/dynamic-geometry-consumption.d.ts +15 -0
- package/dist/record/dynamic-geometry-consumption.d.ts.map +1 -0
- package/dist/record/frame-lighting.d.ts +3 -3
- package/dist/record/frame-lighting.d.ts.map +1 -1
- package/dist/record/frame-snapshot.d.ts +106 -5
- package/dist/record/frame-snapshot.d.ts.map +1 -1
- package/dist/record/frame-targets.d.ts.map +1 -1
- package/dist/record/frame.d.ts +39 -4
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/main-pass-geometry.d.ts +7 -8
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/main-pass-material.d.ts +38 -1
- package/dist/record/main-pass-material.d.ts.map +1 -1
- package/dist/record/main-pass-sprite-draws.d.ts +2 -10
- package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
- package/dist/record/main-pass.d.ts +12 -2
- package/dist/record/main-pass.d.ts.map +1 -1
- package/dist/record/mesh-ssbo.d.ts +15 -6
- package/dist/record/mesh-ssbo.d.ts.map +1 -1
- package/dist/record/prepared-material-bindings.d.ts +19 -0
- package/dist/record/prepared-material-bindings.d.ts.map +1 -0
- package/dist/record/recovery-pipeline.d.ts +2 -9
- package/dist/record/recovery-pipeline.d.ts.map +1 -1
- package/dist/record/render-context.d.ts +76 -26
- package/dist/record/render-context.d.ts.map +1 -1
- package/dist/record/shadow-pass.d.ts +62 -7
- package/dist/record/shadow-pass.d.ts.map +1 -1
- package/dist/record/skybox-post-pass.d.ts +1 -1
- package/dist/record/skybox-post-pass.d.ts.map +1 -1
- package/dist/record/typed-frame-graph.d.ts +35 -7
- package/dist/record/typed-frame-graph.d.ts.map +1 -1
- package/dist/record/view-ubo.d.ts +1 -0
- package/dist/record/view-ubo.d.ts.map +1 -1
- package/dist/recovery/render-system-candidate.d.ts +10 -2
- package/dist/recovery/render-system-candidate.d.ts.map +1 -1
- package/dist/recovery/render-system-roots.d.ts +11 -0
- package/dist/recovery/render-system-roots.d.ts.map +1 -0
- package/dist/recovery/types.d.ts +76 -0
- package/dist/recovery/types.d.ts.map +1 -0
- package/dist/reflection/record-owner.d.ts +0 -2
- package/dist/reflection/record-owner.d.ts.map +1 -1
- package/dist/render-contract.d.ts +185 -30
- package/dist/render-contract.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 +64 -5
- package/dist/render-pipeline.d.ts.map +1 -1
- package/dist/render-system-extract-tail.d.ts +7 -5
- package/dist/render-system-extract-tail.d.ts.map +1 -1
- package/dist/render-system-extract.d.ts +101 -41
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system-projections.d.ts +22 -0
- package/dist/render-system-projections.d.ts.map +1 -0
- package/dist/render-system.d.ts +34 -77
- package/dist/render-system.d.ts.map +1 -1
- package/dist/scene/render-scene-types.d.ts +14 -7
- package/dist/scene/render-scene-types.d.ts.map +1 -1
- package/dist/scene/render-scene.d.ts +29 -11
- package/dist/scene/render-scene.d.ts.map +1 -1
- package/dist/scene/shadow-visibility.d.ts +5 -0
- package/dist/scene/shadow-visibility.d.ts.map +1 -0
- package/dist/ssr/admission.d.ts +70 -1
- package/dist/ssr/admission.d.ts.map +1 -1
- package/dist/ssr/compose.d.ts +13 -0
- package/dist/ssr/compose.d.ts.map +1 -0
- package/dist/ssr/errors.d.ts +35 -0
- package/dist/ssr/errors.d.ts.map +1 -1
- package/dist/ssr/graph.d.ts +60 -0
- package/dist/ssr/graph.d.ts.map +1 -0
- package/dist/ssr/history.d.ts +97 -0
- package/dist/ssr/history.d.ts.map +1 -0
- package/dist/ssr/hiz.d.ts +37 -0
- package/dist/ssr/hiz.d.ts.map +1 -0
- package/dist/ssr/inspection.d.ts +42 -0
- package/dist/ssr/inspection.d.ts.map +1 -0
- package/dist/ssr/resources.d.ts +58 -0
- package/dist/ssr/resources.d.ts.map +1 -0
- package/dist/ssr/temporal.d.ts +38 -0
- package/dist/ssr/temporal.d.ts.map +1 -0
- package/dist/systems/skin-palette-allocator.d.ts +32 -10
- package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
- package/dist/systems/skin-palette-types.d.ts +15 -0
- package/dist/systems/skin-palette-types.d.ts.map +1 -1
- package/dist/systems/transparent-dispatch.d.ts +9 -0
- package/dist/systems/transparent-dispatch.d.ts.map +1 -0
- package/dist/targets/material-source.d.ts +7 -0
- package/dist/targets/material-source.d.ts.map +1 -1
- package/dist/temporal/frame-coordinator.d.ts +7 -0
- package/dist/temporal/frame-coordinator.d.ts.map +1 -1
- package/dist/temporal/gpu.d.ts +8 -0
- package/dist/temporal/gpu.d.ts.map +1 -1
- package/dist/temporal/history.d.ts.map +1 -1
- package/dist/temporal/index.mjs +6 -6
- package/dist/temporal/inspection.d.ts.map +1 -1
- package/dist/temporal/standard-scene-data.d.ts +2 -1
- package/dist/temporal/standard-scene-data.d.ts.map +1 -1
- package/dist/temporal/temporal-view.d.ts +2 -0
- package/dist/temporal/temporal-view.d.ts.map +1 -1
- package/dist/typed-render-graph-primitives.d.ts +35 -1
- package/dist/typed-render-graph-primitives.d.ts.map +1 -1
- package/dist/typed-shadow-passes.d.ts +3 -1
- package/dist/typed-shadow-passes.d.ts.map +1 -1
- package/dist/volume/temporal.d.ts +6 -0
- package/dist/volume/temporal.d.ts.map +1 -1
- package/package.json +21 -20
- package/src/__tests__/auto-exposure-gpu-evidence.ts +291 -0
- package/src/__tests__/auto-exposure-inspection.unit.test.ts +80 -0
- package/src/__tests__/auto-exposure-transaction.integration.test.ts +99 -0
- package/src/__tests__/auto-exposure.browser.test.ts +33 -0
- package/src/__tests__/auto-exposure.dawn.test.ts +33 -0
- package/src/__tests__/detached-inspection.integration.test.ts +5 -0
- package/src/__tests__/device-scope-lifecycle.unit.test.ts +18 -0
- package/src/__tests__/device-scope-stale-matrix.unit.test.ts +18 -0
- package/src/__tests__/docs-gate-green.unit.test.ts +3 -3
- package/src/__tests__/dynamic-geometry-host.unit.test.ts +1123 -0
- package/src/__tests__/dynamic-geometry.unit.test.ts +233 -0
- package/src/__tests__/factory-contract.integration.test.ts +73 -22
- package/src/__tests__/fallback-row-stride-surface.unit.test.ts +19 -0
- package/src/__tests__/feature-depth-input.dawn.test.ts +121 -0
- package/src/__tests__/feature-noise.unit.test.ts +38 -0
- package/src/__tests__/fixtures/taa-resolve-pinned-baseline.wgsl +301 -0
- package/src/__tests__/frame-camera-selection.unit.test.ts +131 -0
- package/src/__tests__/frame-plan-contract.unit.test.ts +24 -0
- package/src/__tests__/frame-targets.unit.test.ts +24 -0
- package/src/__tests__/fullscreen-feature-plan.unit.test.ts +3 -0
- package/src/__tests__/gpu-driven-baseline.characterization.test.ts +79 -0
- package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +287 -6
- package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +122 -142
- package/src/__tests__/gpu-driven-indirect-raster.browser.test.ts +10 -1
- package/src/__tests__/gpu-driven-lod-cache.unit.test.ts +264 -0
- package/src/__tests__/gpu-driven-mesh-world-identity.unit.test.ts +349 -0
- package/src/__tests__/gpu-driven-pbr.dawn.test.ts +1417 -0
- package/src/__tests__/gpu-driven-pbr.integration.test.ts +21 -0
- package/src/__tests__/gpu-driven-production-fixture.ts +96 -0
- package/src/__tests__/gpu-driven-production.integration.test.ts +1075 -93
- package/src/__tests__/gpu-driven-scaling.unit.test.ts +1 -3
- package/src/__tests__/gpu-driven-shadow-views.browser.test.ts +63 -0
- package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +294 -0
- package/src/__tests__/gpu-driven-shadow-views.integration.test.ts +97 -0
- package/src/__tests__/gpu-driven-shadow-views.unit.test.ts +168 -0
- package/src/__tests__/gpu-driven-skin-contract.test-d.ts +11 -0
- package/src/__tests__/gpu-driven-view-gpu-evidence.ts +72 -20
- package/src/__tests__/gpu-driven-view-graph.integration.test.ts +30 -9
- package/src/__tests__/gpu-driven-view.unit.test.ts +159 -52
- package/src/__tests__/gpu-pass-timing.browser.test.ts +6 -1
- package/src/__tests__/gpu-resource-store-stride.unit.test.ts +42 -0
- package/src/__tests__/gpu-scene-render-graph-gpu.ts +19 -5
- package/src/__tests__/gpu-scene.dawn.test.ts +40 -13
- package/src/__tests__/gpu-scene.unit.test.ts +559 -33
- package/src/__tests__/instances-culling.integration.test.ts +36 -10
- package/src/__tests__/instances-derived-bounds.unit.test.ts +54 -0
- package/src/__tests__/instances-store.unit.test.ts +242 -39
- package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +6 -0
- package/src/__tests__/lighting-scene-roundtrip.integration.test.ts +13 -16
- package/src/__tests__/lut-lkg-transaction.integration.test.ts +122 -0
- package/src/__tests__/material-contract-inventory.unit.test.ts +5 -6
- package/src/__tests__/material-dispatch-shader-id.unit.test.ts +51 -0
- package/src/__tests__/material-lane-selection.unit.test.ts +60 -0
- package/src/__tests__/material-numeric-upload.unit.test.ts +53 -0
- package/src/__tests__/material-texture-handles.unit.test.ts +18 -0
- package/src/__tests__/material-texture-source.unit.test.ts +87 -0
- package/src/__tests__/materials-authoring-validation.unit.test.ts +41 -0
- package/src/__tests__/materials-standard-contract.unit.test.ts +52 -2
- package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +4 -0
- package/src/__tests__/mesh-submission-lifetime.unit.test.ts +53 -0
- package/src/__tests__/normal-fallback.dawn.test.ts +122 -0
- package/src/__tests__/particle-material-input-layout.unit.test.ts +104 -0
- package/src/__tests__/physical-clearcoat.integration.test.ts +37 -3
- package/src/__tests__/pipeline-texture-specialization.unit.test.ts +138 -0
- package/src/__tests__/point-shadow-inspection.unit.test.ts +2 -0
- package/src/__tests__/prepared-gpu-driven-pbr-contract.test-d.ts +31 -0
- package/src/__tests__/prepared-gpu-driven-pbr-contract.unit.test.ts +39 -0
- package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +35 -4
- package/src/__tests__/prepared-graphics-capability.unit.test.ts +21 -0
- package/src/__tests__/prepared-graphics-pipeline-warmup.unit.test.ts +6 -2
- package/src/__tests__/profiler-phase-catalog.test.ts +5 -0
- package/src/__tests__/public-surface.test-d.ts +3 -0
- package/src/__tests__/recovery-candidate-prepare.contract.test.ts +30 -12
- package/src/__tests__/reflection-fallback-async-publication.unit.test.ts +104 -0
- package/src/__tests__/reflection-probe-record-owner.characterization.unit.test.ts +2 -1
- package/src/__tests__/render-error-code-owner.test-d.ts +4 -0
- package/src/__tests__/render-error-detail-ssot.test-d.ts +22 -0
- package/src/__tests__/render-error-exhaustive.test-d.ts +15 -0
- package/src/__tests__/render-feature-gpu-work.integration.test.ts +285 -2
- package/src/__tests__/render-feature-plan.unit.test.ts +245 -0
- package/src/__tests__/render-feature-shadow-order.unit.test.ts +228 -0
- package/src/__tests__/render-feature-zero-work.unit.test.ts +35 -1
- package/src/__tests__/render-graph-fallback.integration.test.ts +2 -2
- package/src/__tests__/render-owner-cohesion.unit.test.ts +55 -0
- package/src/__tests__/render-scene-mixed-updates.integration.test.ts +426 -0
- package/src/__tests__/render-scene-projection.unit.test.ts +914 -23
- package/src/__tests__/render-scene-resource-updates.integration.test.ts +214 -0
- package/src/__tests__/render-scene-skin-lifecycle.integration.test.ts +173 -0
- package/src/__tests__/render-scene-temporal-retry.unit.test.ts +133 -0
- package/src/__tests__/render-target-material-source.test-d.ts +27 -3
- package/src/__tests__/renderer-factory-material-contract.unit.test.ts +61 -1
- package/src/__tests__/renderer-frame-transaction.integration.test.ts +923 -5
- package/src/__tests__/shader-manifest-fixture.ts +39 -0
- package/src/__tests__/skin-motion-regression.unit.test.ts +0 -33
- package/src/__tests__/skin-palette-persistent.integration.test.ts +28 -0
- package/src/__tests__/skin-palette-persistent.unit.test.ts +247 -0
- package/src/__tests__/skinned-shadow-caster.test.ts +118 -14
- package/src/__tests__/specular-environment-scale.dawn.test.ts +156 -0
- package/src/__tests__/ssr-admission-matrix.integration.test.ts +7 -1
- package/src/__tests__/ssr-confidence-compose.unit.test.ts +84 -0
- package/src/__tests__/ssr-coordinate-oracle.unit.test.ts +28 -0
- package/src/__tests__/ssr-format-probe-gating.integration.test.ts +82 -0
- package/src/__tests__/ssr-gpu-dispatch.browser.test.ts +91 -0
- package/src/__tests__/ssr-gpu-dispatch.dawn.test.ts +112 -0
- package/src/__tests__/ssr-gpu-dispatch.ts +946 -0
- package/src/__tests__/ssr-graph-topology.unit.test.ts +104 -0
- package/src/__tests__/ssr-history-filter.dawn.test.ts +198 -0
- package/src/__tests__/ssr-hit-color.dawn.test.ts +273 -0
- package/src/__tests__/ssr-hiz-odd-reduction.dawn.test.ts +569 -0
- package/src/__tests__/ssr-hiz-rescue.dawn.test.ts +279 -0
- package/src/__tests__/ssr-hiz.browser.test.ts +150 -0
- package/src/__tests__/ssr-hiz.dawn.test.ts +150 -0
- package/src/__tests__/ssr-hiz.unit.test.ts +56 -0
- package/src/__tests__/ssr-inspection.unit.test.ts +105 -0
- package/src/__tests__/ssr-memory-budget.unit.test.ts +45 -0
- package/src/__tests__/ssr-performance-sequence-aggregation.unit.test.ts +15 -0
- package/src/__tests__/ssr-public-surface.test-d.ts +37 -0
- package/src/__tests__/ssr-receiver-coverage.dawn.test.ts +387 -0
- package/src/__tests__/ssr-reflection-mip-reducer.dawn.test.ts +309 -0
- package/src/__tests__/ssr-reprojection-probe.ts +90 -0
- package/src/__tests__/ssr-roughness-mip.dawn.test.ts +163 -0
- package/src/__tests__/ssr-shader-prewarm.unit.test.ts +59 -0
- package/src/__tests__/ssr-spatial.integration.test.ts +141 -0
- package/src/__tests__/ssr-stable-lattice.dawn.test.ts +475 -0
- package/src/__tests__/ssr-temporal-history.unit.test.ts +202 -0
- package/src/__tests__/ssr-texel-crossing.dawn.test.ts +448 -0
- package/src/__tests__/ssr-zero-work.integration.test.ts +87 -0
- package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +44 -12
- package/src/__tests__/standard-clustered-pipeline.unit.test.ts +29 -0
- package/src/__tests__/standard-output-chain-contract.unit.test.ts +99 -0
- package/src/__tests__/standard-output-chain.integration.test.ts +73 -0
- package/src/__tests__/standard-output-gpu-evidence.ts +205 -0
- package/src/__tests__/standard-output.browser.test.ts +15 -0
- package/src/__tests__/standard-output.dawn.test.ts +15 -0
- package/src/__tests__/standard-pbr-artifact-assembly.unit.test.ts +38 -0
- package/src/__tests__/standard-pipeline.integration.test.ts +320 -3
- package/src/__tests__/standard-profile-oracle.unit.test.ts +22 -0
- package/src/__tests__/standard-surface-lighting.unit.test.ts +41 -0
- package/src/__tests__/standard-texture-specialization.dawn.test.ts +141 -0
- package/src/__tests__/surface-adjacent-regressions.integration.test.ts +6 -3
- package/src/__tests__/taa-coverage-edge.dawn.test.ts +193 -0
- package/src/__tests__/taa-history-clipping.dawn.test.ts +328 -0
- package/src/__tests__/taa-neighborhood-equivalence.dawn.test.ts +778 -0
- package/src/__tests__/taa-stability-feedback.browser.test.ts +23 -0
- package/src/__tests__/taa-stability-feedback.dawn.test.ts +68 -0
- package/src/__tests__/taa-stability-feedback.ts +341 -0
- package/src/__tests__/temporal-history-lifecycle.unit.test.ts +49 -7
- package/src/__tests__/temporal-reset-matrix.unit.test.ts +9 -0
- package/src/__tests__/temporal-transaction.integration.test.ts +28 -0
- package/src/__tests__/texture-usage-surface.unit.test.ts +4 -2
- package/src/__tests__/typed-pipeline-topology.unit.test.ts +109 -4
- package/src/__tests__/typed-scene-pass-material-cache.integration.test.ts +126 -0
- package/src/__tests__/vertex-layouts-ssot.unit.test.ts +47 -0
- package/src/__tests__/visibility-extract.perf.test.ts +20 -8
- package/src/__tests__/visibility-instances.integration.test.ts +19 -7
- package/src/__tests__/visibility-producer-matrix.integration.test.ts +20 -9
- package/src/assembly/atmosphere-shader-prewarm.ts +38 -0
- package/src/assembly/dynamic-geometry-host.ts +887 -0
- package/src/assembly/dynamic-geometry-runtime.ts +71 -0
- package/src/assembly/factory.ts +4 -0
- package/src/assembly/gpu-driven-owner.ts +29 -0
- package/src/assembly/host-contract.ts +36 -0
- package/src/assembly/material/assembly.ts +38 -0
- package/src/assembly/material-shader-policy.ts +71 -28
- package/src/assembly/recovery/failure-location.ts +62 -0
- package/src/assembly/recovery/renderer-recover.ts +19 -17
- package/src/assembly/renderer-facade.ts +29 -0
- package/src/assembly/renderer-feature-inspection.ts +13 -0
- package/src/assembly/renderer-frame-transaction.ts +63 -0
- package/src/assembly/renderer-helpers.ts +26 -0
- package/src/assembly/renderer-inspection.ts +6 -0
- package/src/assembly/shader-prewarm-policy.ts +32 -0
- package/src/assembly/skin-palette-owner.ts +21 -0
- package/src/assembly/ssr-shader-prewarm.ts +41 -0
- package/src/assembly/webgpu-device-loss.ts +75 -0
- package/src/assembly/webgpu-pbr-ready.ts +232 -0
- package/src/assembly/webgpu-ready-mipmap.ts +26 -0
- package/src/assembly/webgpu-ready.ts +142 -121
- package/src/assembly/webgpu-renderer-bootstrap.ts +85 -0
- package/src/assembly/webgpu-renderer-contract.ts +69 -0
- package/src/assembly/webgpu-renderer-guards.ts +32 -0
- package/src/assembly/webgpu-renderer-observation.ts +43 -0
- package/src/assembly/webgpu-renderer-recovery-failure.ts +62 -0
- package/src/assembly/webgpu-renderer.ts +675 -614
- package/src/assembly/webgpu-vertex-layouts.ts +233 -87
- package/src/components/__tests__/camera-exposure.contract.unit.test.ts +153 -0
- package/src/components/camera.ts +287 -7
- package/src/components/index.ts +1 -0
- package/src/components/instances.ts +45 -18
- package/src/components/scene-instance.ts +2 -1
- package/src/components/screen-space-reflection.ts +21 -0
- package/src/components/sprite-animation.ts +3 -3
- package/src/components/sprite-instances.ts +1 -1
- package/src/device/gpu-residency.ts +96 -93
- package/src/device/mesh-residency-lifetime.ts +73 -0
- package/src/dynamic-geometry.ts +736 -0
- package/src/environment/background.ts +263 -0
- package/src/errors/gpu-driven.ts +39 -5
- package/src/errors/render.ts +27 -3
- package/src/extract/camera.ts +26 -1
- package/src/extract/gpu-driven.ts +185 -10
- package/src/features/host.ts +237 -19
- package/src/features/noise-texture.ts +91 -0
- package/src/features/plan.ts +553 -24
- package/src/features/prepared-gpu-work.ts +631 -49
- package/src/features/prepared-graphics-store.ts +31 -0
- package/src/features/prepared-graphics.ts +5 -0
- package/src/features/render-graph-compute.ts +41 -8
- package/src/features/render-graph-contribution.ts +141 -3
- package/src/features/render-graph-raster.ts +143 -30
- package/src/features/types.ts +11 -0
- package/src/fullscreen-post-process-pass.ts +3 -0
- package/src/gpu-driven/batch-topology.ts +424 -102
- package/src/gpu-driven/pbr-program.ts +31 -0
- package/src/gpu-driven/prepared-draw.ts +29 -4
- package/src/gpu-driven/production-raster.ts +2267 -694
- package/src/gpu-driven/shadow-views.ts +539 -0
- package/src/gpu-driven/view-gpu.ts +188 -41
- package/src/gpu-scene-schema.ts +52 -11
- package/src/gpu-scene.ts +186 -99
- package/src/gpu-texture-usage.ts +1 -0
- package/src/hdrp-buffers.ts +72 -36
- package/src/ibl/frame-resources.ts +38 -0
- package/src/ibl/skylight-bind-group.ts +5 -4
- package/src/index.ts +125 -1
- package/src/inspection-types.ts +191 -0
- package/src/instances-derived-bounds.ts +258 -34
- package/src/instances.ts +459 -150
- package/src/internal.ts +22 -0
- package/src/lifecycle.ts +2 -0
- package/src/material-row.ts +145 -0
- package/src/materials.ts +44 -8
- package/src/pbr-pipeline.ts +159 -17
- package/src/pipeline/standard-forward-lane.ts +52 -24
- package/src/pipeline/standard-output/__tests__/lut-admission.unit.test.ts +103 -0
- package/src/pipeline/standard-output/auto-exposure/__tests__/capability.unit.test.ts +40 -0
- package/src/pipeline/standard-output/auto-exposure/__tests__/gpu.contract.unit.test.ts +125 -0
- package/src/pipeline/standard-output/auto-exposure/__tests__/graph.contract.unit.test.ts +94 -0
- package/src/pipeline/standard-output/auto-exposure/__tests__/oracle.test.ts +108 -0
- package/src/pipeline/standard-output/auto-exposure/__tests__/preset.test.ts +26 -0
- package/src/pipeline/standard-output/auto-exposure/capability.ts +38 -0
- package/src/pipeline/standard-output/auto-exposure/gpu.ts +567 -0
- package/src/pipeline/standard-output/auto-exposure/graph.ts +221 -0
- package/src/pipeline/standard-output/auto-exposure/inspection.ts +231 -0
- package/src/pipeline/standard-output/auto-exposure/oracle.ts +291 -0
- package/src/pipeline/standard-output/auto-exposure/preset.ts +28 -0
- package/src/pipeline/standard-output/auto-exposure/state.ts +156 -0
- package/src/pipeline/standard-output/color-transform.ts +124 -0
- package/src/pipeline/standard-output/graph.ts +139 -0
- package/src/pipeline/standard-output/lut-admission.ts +121 -0
- package/src/pipeline/standard-output/lut-gpu.ts +399 -0
- package/src/pipeline/standard-output/lut-state.ts +176 -0
- package/src/pipeline/standard-output/resources.ts +121 -0
- package/src/pipeline/standard-output/types.ts +58 -0
- package/src/pipeline/standard-pipeline.ts +192 -59
- package/src/pipeline/standard-post.ts +186 -27
- package/src/pipeline-builder.ts +16 -6
- package/src/pipeline-spec-types.ts +4 -0
- package/src/pipeline-spec.ts +37 -29
- package/src/points-lines/standard-owner.ts +6 -14
- package/src/prepare/prepared-graphics-resolver.ts +9 -2
- package/src/record/__tests__/material-slot-plan.unit.test.ts +48 -0
- package/src/record/__tests__/mesh-ssbo.unit.test.ts +40 -1
- package/src/record/__tests__/recovery-pipeline.unit.test.ts +2 -29
- package/src/record/__tests__/sprite-material-abi.unit.test.ts +36 -5
- package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +45 -0
- package/src/record/__tests__/view-ubo-layout.unit.test.ts +5 -0
- package/src/record/dynamic-geometry-consumption.ts +59 -0
- package/src/record/frame-lighting.ts +12 -11
- package/src/record/frame-snapshot.ts +133 -23
- package/src/record/frame-targets.ts +9 -0
- package/src/record/frame.ts +727 -133
- package/src/record/main-pass-geometry.ts +162 -98
- package/src/record/main-pass-material.ts +115 -26
- package/src/record/main-pass-sprite-draws.ts +18 -107
- package/src/record/main-pass.ts +78 -8
- package/src/record/material-uniforms.ts +1 -1
- package/src/record/mesh-ssbo.ts +59 -15
- package/src/record/prepared-material-bindings.ts +234 -0
- package/src/record/recovery-pipeline.ts +59 -136
- package/src/record/render-context.ts +92 -22
- package/src/record/shadow-pass.ts +1231 -209
- package/src/record/skybox-post-pass.ts +14 -3
- package/src/record/typed-frame-graph.ts +1006 -53
- package/src/record/view-ubo.ts +11 -0
- package/src/recovery/render-system-candidate.ts +90 -66
- package/src/recovery/render-system-roots.ts +113 -0
- package/src/recovery/types.ts +77 -0
- package/src/reflection/record-owner.ts +26 -54
- package/src/render-contract.ts +219 -25
- package/src/render-graph-primitives.ts +6 -2
- package/src/render-pipeline.ts +70 -1
- package/src/render-system-extract-tail.ts +409 -160
- package/src/render-system-extract.ts +376 -102
- package/src/render-system-projections.ts +241 -0
- package/src/render-system.ts +692 -791
- package/src/scene/render-scene-types.ts +13 -4
- package/src/scene/render-scene.ts +569 -355
- package/src/scene/shadow-visibility.ts +21 -0
- package/src/ssr/admission.ts +380 -14
- package/src/ssr/compose.ts +144 -0
- package/src/ssr/errors.ts +48 -0
- package/src/ssr/graph.ts +777 -0
- package/src/ssr/history.ts +436 -0
- package/src/ssr/hiz.ts +124 -0
- package/src/ssr/inspection.ts +124 -0
- package/src/ssr/resources.ts +202 -0
- package/src/ssr/temporal.ts +164 -0
- package/src/systems/skin-palette-allocator.ts +495 -148
- package/src/systems/skin-palette-types.ts +17 -0
- package/src/systems/transparent-dispatch.ts +90 -0
- package/src/targets/material-source.ts +34 -0
- package/src/temporal/coverage.ts +1 -1
- package/src/temporal/frame-coordinator.ts +23 -2
- package/src/temporal/gpu.ts +74 -12
- package/src/temporal/history.ts +3 -1
- package/src/temporal/inspection.ts +2 -1
- package/src/temporal/standard-scene-data.ts +3 -1
- package/src/temporal/temporal-view.ts +3 -0
- package/src/transmission/__tests__/standard-transmission.dawn.test.ts +204 -139
- package/src/typed-render-graph-primitives.ts +266 -74
- package/src/typed-shadow-passes.ts +122 -12
- package/src/volume/temporal.ts +10 -0
- package/dist/chunk-ADHHVYLW.mjs.map +0 -1
- package/dist/chunk-FJ6P52EE.mjs.map +0 -1
- package/dist/chunk-GE3SDD72.mjs.map +0 -1
- package/dist/chunk-GF523LHF.mjs.map +0 -1
- package/dist/chunk-HKXTW355.mjs.map +0 -1
- package/dist/chunk-JDWARUOI.mjs.map +0 -1
- package/dist/chunk-KM2NOX2I.mjs.map +0 -1
- package/dist/chunk-TMIASV2N.mjs.map +0 -1
- package/dist/chunk-XDP6XE5V.mjs.map +0 -1
- package/dist/chunk-ZZ4YQ474.mjs.map +0 -1
- package/src/__tests__/instances-world-ownership.integration.test.ts +0 -62
- package/src/scene/__tests__/changed-block-summary.candidate.test.ts +0 -36
|
@@ -14,7 +14,6 @@ import type {
|
|
|
14
14
|
RenderPipeline,
|
|
15
15
|
Result,
|
|
16
16
|
RhiDevice,
|
|
17
|
-
ShaderModule,
|
|
18
17
|
TextureFormat,
|
|
19
18
|
} from '@forgeax/engine-rhi';
|
|
20
19
|
import { err, ok, RhiError } from '@forgeax/engine-rhi';
|
|
@@ -22,24 +21,43 @@ import type { MaterialShaderArtifact } from '@forgeax/engine-shader';
|
|
|
22
21
|
import type { MaterialRenderState } from '@forgeax/engine-types';
|
|
23
22
|
import type { DeviceScope, LifecycleResourceSpec } from '../device/device-scope';
|
|
24
23
|
import type { MeshGpuHandles } from '../device/gpu-residency';
|
|
25
|
-
import {
|
|
24
|
+
import { GpuDrivenPreparationError } from '../errors/gpu-driven';
|
|
25
|
+
import {
|
|
26
|
+
gpuDrivenDrawKey,
|
|
27
|
+
gpuDrivenShadowDrawKey,
|
|
28
|
+
gpuDrivenSourceDrawItemIndex,
|
|
29
|
+
} from '../extract/gpu-driven';
|
|
26
30
|
import {
|
|
27
31
|
GPU_BUFFER_USAGE_COPY_DST,
|
|
28
32
|
GPU_BUFFER_USAGE_INDEX,
|
|
29
33
|
GPU_BUFFER_USAGE_STORAGE,
|
|
30
|
-
GPU_BUFFER_USAGE_UNIFORM,
|
|
31
34
|
GPU_BUFFER_USAGE_VERTEX,
|
|
32
35
|
} from '../gpu-usage';
|
|
33
|
-
import {
|
|
34
|
-
|
|
36
|
+
import type {
|
|
37
|
+
GpuDrivenLaneReason,
|
|
38
|
+
GpuDrivenLaneSummary,
|
|
39
|
+
GpuDrivenPreparationFailureInspection,
|
|
40
|
+
GpuDrivenProductionInspection,
|
|
41
|
+
GpuDrivenStructureMetrics,
|
|
42
|
+
} from '../inspection-types';
|
|
35
43
|
import type { PipelineBuilderShaderModuleFactory } from '../pipeline-builder';
|
|
44
|
+
import type { ValidatedRenderable } from '../record/frame-snapshot';
|
|
36
45
|
import { worldEntityKey } from '../record/frame-snapshot';
|
|
37
46
|
import { geometryRenderStateForTopology } from '../record/main-pass-material';
|
|
38
|
-
import { MESH_SSBO_BYTES } from '../record/mesh-ssbo';
|
|
47
|
+
import { MESH_SSBO_BYTES, packInstanceStorageBuffer } from '../record/mesh-ssbo';
|
|
39
48
|
import type { PipelineState } from '../record/render-context';
|
|
40
49
|
import type { CameraSnapshot } from '../render-contract';
|
|
41
|
-
import type {
|
|
42
|
-
|
|
50
|
+
import type {
|
|
51
|
+
GpuDrivenStandardPbrFrameResources,
|
|
52
|
+
RenderPipelineFrame,
|
|
53
|
+
RenderPipelineGpuDrivenProjection,
|
|
54
|
+
} from '../render-pipeline';
|
|
55
|
+
import type {
|
|
56
|
+
DispatchEntry,
|
|
57
|
+
MaterialSnapshot,
|
|
58
|
+
RenderableSnapshot,
|
|
59
|
+
ShadowCasterMembership,
|
|
60
|
+
} from '../render-system-extract';
|
|
43
61
|
import type { PersistentGpuDrivenState } from '../scene/render-scene';
|
|
44
62
|
import { selectLod } from '../scene/visibility/lod-selector';
|
|
45
63
|
import type { OcclusionFrameProjection } from '../scene/visibility/occlusion-runtime';
|
|
@@ -49,20 +67,23 @@ import {
|
|
|
49
67
|
projectedHeightForCandidate,
|
|
50
68
|
type SubmissionPlan,
|
|
51
69
|
} from './batch-topology';
|
|
70
|
+
import { type GpuDrivenPbrProgram, resolveStandardPbrProgram } from './pbr-program';
|
|
52
71
|
import {
|
|
53
|
-
|
|
54
|
-
type
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const FRAGMENT_STAGE = 0x2;
|
|
61
|
-
const EMPTY_RESOURCE_CLASS = JSON.stringify({ textures: [], samplers: [], video: [] });
|
|
72
|
+
type ShadowViewIdentity,
|
|
73
|
+
type ShadowViewProjection,
|
|
74
|
+
ShadowViewStatePool,
|
|
75
|
+
type ShadowViewUpdateInput,
|
|
76
|
+
shadowViewIdentityKey,
|
|
77
|
+
} from './shadow-views';
|
|
78
|
+
import { type GpuDrivenLodSelectionInspection, GpuDrivenView, selectGpuLodLane } from './view-gpu';
|
|
62
79
|
|
|
63
80
|
export interface StandardPbrSceneCandidate {
|
|
64
81
|
readonly primitiveIndex: number;
|
|
65
82
|
readonly world: Float32Array;
|
|
83
|
+
/** Optional per-instance local transform selected by BatchTopology. */
|
|
84
|
+
readonly instanceTransform?: Float32Array;
|
|
85
|
+
/** Optional GPU Scene material row carried by the shadow projection. */
|
|
86
|
+
readonly materialIndex?: number;
|
|
66
87
|
}
|
|
67
88
|
|
|
68
89
|
export interface StandardPbrSceneProjection {
|
|
@@ -73,6 +94,17 @@ export interface StandardPbrSceneProjection {
|
|
|
73
94
|
readonly rows: Float32Array;
|
|
74
95
|
}
|
|
75
96
|
|
|
97
|
+
export interface GpuDrivenShadowBatchProjection {
|
|
98
|
+
readonly mesh: MeshGpuHandles;
|
|
99
|
+
readonly meshBindGroup: BindGroup;
|
|
100
|
+
readonly visibleBindGroup: BindGroup;
|
|
101
|
+
readonly deformation: 'rigid' | 'skin';
|
|
102
|
+
/** Material snapshot used to assemble the receipt-owned alpha-mask BG. */
|
|
103
|
+
readonly material: MaterialSnapshot;
|
|
104
|
+
/** Composite entity identity used only for video-resource routing. */
|
|
105
|
+
readonly materialEntityKey: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
76
108
|
/**
|
|
77
109
|
* Build the producer-owned Standard PBR scene-index projection. The receipt is
|
|
78
110
|
* the only source of the vertex entry and layout identity; callers cannot
|
|
@@ -97,12 +129,16 @@ export function projectStandardPbrScene(input: {
|
|
|
97
129
|
for (let index = 0; index < input.candidates.length; index += 1) {
|
|
98
130
|
const candidate = input.candidates[index];
|
|
99
131
|
if (candidate === undefined) continue;
|
|
132
|
+
const world =
|
|
133
|
+
candidate.instanceTransform === undefined
|
|
134
|
+
? candidate.world
|
|
135
|
+
: mat4.multiply(mat4.create(), candidate.world, candidate.instanceTransform);
|
|
100
136
|
const base = index * 48;
|
|
101
137
|
for (let value = 0; value < 16; value += 1) {
|
|
102
|
-
rows[base + value] =
|
|
103
|
-
rows[base + 28 + value] =
|
|
138
|
+
rows[base + value] = world[value] ?? 0;
|
|
139
|
+
rows[base + 28 + value] = world[value] ?? 0;
|
|
104
140
|
}
|
|
105
|
-
const normalMatrix = mat3.normalMatrix(normal,
|
|
141
|
+
const normalMatrix = mat3.normalMatrix(normal, world);
|
|
106
142
|
for (let column = 0; column < 3; column += 1) {
|
|
107
143
|
const source = column * 3;
|
|
108
144
|
const target = base + 16 + column * 4;
|
|
@@ -110,6 +146,11 @@ export function projectStandardPbrScene(input: {
|
|
|
110
146
|
rows[target + 1] = normalMatrix[source + 1] ?? 0;
|
|
111
147
|
rows[target + 2] = normalMatrix[source + 2] ?? 0;
|
|
112
148
|
}
|
|
149
|
+
// The final temporal metadata lane is otherwise unused by the shadow
|
|
150
|
+
// vertex path. Carry the concrete scene-material row there so skinned
|
|
151
|
+
// GPU-driven alpha-mask shadows can keep the palette base in visible.y
|
|
152
|
+
// without introducing a second visible-stream ABI.
|
|
153
|
+
rows[base + 47] = candidate.materialIndex ?? 0;
|
|
113
154
|
}
|
|
114
155
|
return {
|
|
115
156
|
ok: true,
|
|
@@ -183,50 +224,6 @@ export function adaptStandardPbrFrameResources(input: {
|
|
|
183
224
|
};
|
|
184
225
|
}
|
|
185
226
|
|
|
186
|
-
export const GPU_DRIVEN_RIGID_UNLIT_WGSL = /* wgsl */ `
|
|
187
|
-
${GPU_SCENE_WGSL}
|
|
188
|
-
|
|
189
|
-
struct ViewData {
|
|
190
|
-
slots: array<vec4<f32>, 49>,
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
@group(0) @binding(0) var<uniform> view: ViewData;
|
|
194
|
-
@group(1) @binding(0) var<storage, read> primitives: array<GpuScenePrimitive>;
|
|
195
|
-
@group(1) @binding(1) var<storage, read> instances: array<GpuSceneInstance>;
|
|
196
|
-
@group(1) @binding(2) var<storage, read> materials: array<GpuSceneMaterial>;
|
|
197
|
-
@group(1) @binding(3) var<storage, read> visibleItems: array<vec2<u32>>;
|
|
198
|
-
@group(1) @binding(4) var<storage, read> transforms: array<GpuSceneTransform>;
|
|
199
|
-
|
|
200
|
-
struct VertexInput {
|
|
201
|
-
@location(0) position: vec3<f32>,
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
struct VertexOutput {
|
|
205
|
-
@builtin(position) position: vec4<f32>,
|
|
206
|
-
@location(0) color: vec4<f32>,
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
@vertex
|
|
210
|
-
fn vs_main(input: VertexInput, @builtin(instance_index) instanceIndex: u32) -> VertexOutput {
|
|
211
|
-
let viewProjection = mat4x4<f32>(view.slots[0], view.slots[1], view.slots[2], view.slots[3]);
|
|
212
|
-
let visible = visibleItems[instanceIndex];
|
|
213
|
-
let sceneInstance = instances[visible.x];
|
|
214
|
-
let primitiveIndex = sceneInstance.primitiveIndex;
|
|
215
|
-
let primitive = primitives[primitiveIndex];
|
|
216
|
-
let material = materials[visible.y];
|
|
217
|
-
var output: VertexOutput;
|
|
218
|
-
let world = transforms[primitive.transformIndex].currentWorld * transforms[sceneInstance.transformIndex].currentWorld;
|
|
219
|
-
output.position = viewProjection * world * vec4<f32>(input.position, 1.0);
|
|
220
|
-
output.color = material.params0;
|
|
221
|
-
return output;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
@fragment
|
|
225
|
-
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
|
|
226
|
-
return input.color;
|
|
227
|
-
}
|
|
228
|
-
`;
|
|
229
|
-
|
|
230
227
|
interface PreparedBatch {
|
|
231
228
|
readonly batch: GpuDrivenBatch;
|
|
232
229
|
readonly mesh: MeshGpuHandles;
|
|
@@ -238,13 +235,6 @@ interface PreparedBatch {
|
|
|
238
235
|
readonly slotGeneration: number;
|
|
239
236
|
}
|
|
240
237
|
|
|
241
|
-
interface ProjectedBatch extends PreparedBatch {
|
|
242
|
-
readonly bindings: BindGroup;
|
|
243
|
-
readonly pipeline: RenderPipeline;
|
|
244
|
-
readonly vertex: GraphBuffer;
|
|
245
|
-
readonly index?: GraphBuffer;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
238
|
function bufferBinding(
|
|
249
239
|
buffer: import('@forgeax/engine-rhi').Buffer,
|
|
250
240
|
offset?: number,
|
|
@@ -282,46 +272,117 @@ function activeFrustum(camera: CameraSnapshot): Float32Array {
|
|
|
282
272
|
);
|
|
283
273
|
}
|
|
284
274
|
|
|
275
|
+
function hasFiniteOrderedBounds(bounds: Float32Array | undefined): boolean {
|
|
276
|
+
if (bounds === undefined || bounds.length < 6) return false;
|
|
277
|
+
for (let index = 0; index < 6; index += 1) {
|
|
278
|
+
if (!Number.isFinite(bounds[index])) return false;
|
|
279
|
+
}
|
|
280
|
+
const minX = bounds[0];
|
|
281
|
+
const minY = bounds[1];
|
|
282
|
+
const minZ = bounds[2];
|
|
283
|
+
const maxX = bounds[3];
|
|
284
|
+
const maxY = bounds[4];
|
|
285
|
+
const maxZ = bounds[5];
|
|
286
|
+
return (
|
|
287
|
+
minX !== undefined &&
|
|
288
|
+
minY !== undefined &&
|
|
289
|
+
minZ !== undefined &&
|
|
290
|
+
maxX !== undefined &&
|
|
291
|
+
maxY !== undefined &&
|
|
292
|
+
maxZ !== undefined &&
|
|
293
|
+
minX <= maxX &&
|
|
294
|
+
minY <= maxY &&
|
|
295
|
+
minZ <= maxZ
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function skinLaneReason(
|
|
300
|
+
snapshot: RenderableSnapshot,
|
|
301
|
+
material: MaterialSnapshot,
|
|
302
|
+
): 'skin-bounds-missing' | 'skin-address-missing' | undefined {
|
|
303
|
+
const isSkinnedPbr =
|
|
304
|
+
material.materialShaderId === 'forgeax::pbr-skin' ||
|
|
305
|
+
material.materialShaderId === 'forgeax::default-standard-pbr-skin';
|
|
306
|
+
if (!isSkinnedPbr || snapshot.skin === undefined) return undefined;
|
|
307
|
+
if (!hasFiniteOrderedBounds(snapshot.skin.bounds)) return 'skin-bounds-missing';
|
|
308
|
+
if (
|
|
309
|
+
snapshot.skin.storageOrUniform !== 'storage' ||
|
|
310
|
+
!Number.isInteger(snapshot.skin.customDataStart) ||
|
|
311
|
+
snapshot.skin.customDataStart < 0 ||
|
|
312
|
+
snapshot.skin.buffer === undefined
|
|
313
|
+
) {
|
|
314
|
+
return 'skin-address-missing';
|
|
315
|
+
}
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
|
|
285
319
|
/**
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
320
|
+
* Derived projected heights invalidate the prepared plan only when a
|
|
321
|
+
* candidate has more than its implicit root LOD row. No-LOD plans have no
|
|
322
|
+
* view-dependent selection. Root matrix edits and camera edits feed the same
|
|
323
|
+
* height calculation; retaining the empty key for absent LOD avoids rebuilding
|
|
324
|
+
* their candidate and batch uploads on every camera frame.
|
|
289
325
|
*/
|
|
290
|
-
function lodProjectionFingerprint(
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
326
|
+
function lodProjectionFingerprint(
|
|
327
|
+
source: SubmissionPlan,
|
|
328
|
+
camera: CameraSnapshot,
|
|
329
|
+
slots: PersistentGpuDrivenState['slots'],
|
|
330
|
+
): string {
|
|
331
|
+
const primitives = new Set<number>();
|
|
332
|
+
for (const batch of source.batches) {
|
|
333
|
+
for (const candidate of batch.candidates) {
|
|
334
|
+
if ((candidate.lodCoverages?.length ?? 0) > 1) primitives.add(candidate.primitiveIndex);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (primitives.size === 0) return '';
|
|
338
|
+
const heights: number[] = [];
|
|
339
|
+
for (const slot of slots) {
|
|
340
|
+
if (primitives.has(slot.slot))
|
|
341
|
+
heights.push(slot.slot, projectedHeightForCandidate(slot, camera));
|
|
342
|
+
}
|
|
343
|
+
return JSON.stringify(heights);
|
|
299
344
|
}
|
|
300
345
|
|
|
301
|
-
function
|
|
346
|
+
function preparedPbrCandidateEligible(
|
|
347
|
+
snapshot: RenderableSnapshot,
|
|
348
|
+
batch: GpuDrivenBatch,
|
|
349
|
+
candidate: GpuDrivenBatch['candidates'][number],
|
|
350
|
+
skinArtifact?: MaterialShaderArtifact,
|
|
351
|
+
): boolean {
|
|
302
352
|
const material = snapshot.materials[batch.key.materialSlot] ?? snapshot.material;
|
|
303
|
-
const
|
|
353
|
+
const prepared = candidate.prepared;
|
|
354
|
+
if (prepared === undefined) return false;
|
|
355
|
+
const standard =
|
|
356
|
+
prepared.identity.material === 'forgeax::default-standard-pbr' ||
|
|
357
|
+
prepared.identity.material === 'forgeax::pbr-skin' ||
|
|
358
|
+
prepared.identity.material === 'forgeax::default-standard-pbr-skin';
|
|
359
|
+
const alphaBlend = material.transparent === true || material.renderState?.blend !== undefined;
|
|
360
|
+
const cpuOnlyMaterialResources =
|
|
361
|
+
(material.textureSources?.size ?? 0) > 0 || (material.videoTextureFields?.size ?? 0) > 0;
|
|
362
|
+
// Transmission has a separate CPU semantic pass today. Keep it out of the
|
|
363
|
+
// GPU-owned candidate set so the residual transmission draw is not swallowed
|
|
364
|
+
// by the generic Standard PBR lane.
|
|
365
|
+
const transmission =
|
|
366
|
+
typeof material.paramSnapshot?.transmission === 'number' &&
|
|
367
|
+
material.paramSnapshot.transmission > 0;
|
|
368
|
+
const skinReady =
|
|
369
|
+
prepared.identity.deformation !== 'skin' ||
|
|
370
|
+
(skinArtifact?.receipt?.skinPaletteAddress !== undefined &&
|
|
371
|
+
snapshot.skin?.storageOrUniform === 'storage' &&
|
|
372
|
+
snapshot.skin.customDataStart >= 0 &&
|
|
373
|
+
Number.isInteger(snapshot.skin.customDataStart) &&
|
|
374
|
+
snapshot.skin.buffer !== undefined &&
|
|
375
|
+
hasFiniteOrderedBounds(snapshot.skin.bounds));
|
|
304
376
|
return (
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
material.transparent !== true
|
|
377
|
+
standard &&
|
|
378
|
+
!alphaBlend &&
|
|
379
|
+
!cpuOnlyMaterialResources &&
|
|
380
|
+
!transmission &&
|
|
381
|
+
(prepared.identity.deformation === 'rigid' || prepared.identity.deformation === 'skin') &&
|
|
382
|
+
skinReady
|
|
312
383
|
);
|
|
313
384
|
}
|
|
314
385
|
|
|
315
|
-
/**
|
|
316
|
-
* Prepared Standard PBR draws have an explicit receipt identity on their
|
|
317
|
-
* batch key. Unprepared batches stay on the rigid production shader; the
|
|
318
|
-
* presence of a frame-wide PBR artifact is not permission to reinterpret an
|
|
319
|
-
* unlit batch with the PBR bind-group layout.
|
|
320
|
-
*/
|
|
321
|
-
function isStandardPbrBatch(batch: GpuDrivenBatch): boolean {
|
|
322
|
-
return batch.key.preparedIdentity !== undefined;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
386
|
function meshSupportsBatch(mesh: MeshGpuHandles, batch: GpuDrivenBatch): boolean {
|
|
326
387
|
if (
|
|
327
388
|
batch.candidates.some((candidate) => {
|
|
@@ -341,6 +402,132 @@ function meshSupportsBatch(mesh: MeshGpuHandles, batch: GpuDrivenBatch): boolean
|
|
|
341
402
|
: !mesh.indexed;
|
|
342
403
|
}
|
|
343
404
|
|
|
405
|
+
function sceneCandidate(
|
|
406
|
+
slot: PersistentGpuDrivenState['slots'][number],
|
|
407
|
+
candidate: GpuDrivenBatch['candidates'][number],
|
|
408
|
+
materialIndex?: number,
|
|
409
|
+
): StandardPbrSceneCandidate {
|
|
410
|
+
const transforms = slot.snapshot.instances?.transforms;
|
|
411
|
+
return {
|
|
412
|
+
primitiveIndex: candidate.primitiveIndex,
|
|
413
|
+
world: slot.snapshot.transform.world,
|
|
414
|
+
...(transforms === undefined
|
|
415
|
+
? {}
|
|
416
|
+
: {
|
|
417
|
+
instanceTransform: transforms.subarray(
|
|
418
|
+
candidate.instanceOrdinal * 16,
|
|
419
|
+
candidate.instanceOrdinal * 16 + 16,
|
|
420
|
+
),
|
|
421
|
+
}),
|
|
422
|
+
...(materialIndex === undefined ? {} : { materialIndex }),
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function shadowCandidateKey(candidate: GpuDrivenBatch['candidates'][number]): string {
|
|
427
|
+
return `${candidate.primitiveIndex}:${candidate.drawItemIndex}:${candidate.instanceOrdinal}`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function gpuDrivenProjectionKey(
|
|
431
|
+
batch: GpuDrivenBatch,
|
|
432
|
+
deformation: 'rigid' | 'skin',
|
|
433
|
+
artifact: MaterialShaderArtifact,
|
|
434
|
+
): string {
|
|
435
|
+
return `${batch.batchId}:${batch.generation}:${deformation}:${artifact.layoutIdentity}`;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function gpuDrivenShadowProjectionKey(
|
|
439
|
+
viewKey: string,
|
|
440
|
+
batch: GpuDrivenBatch,
|
|
441
|
+
deformation: 'rigid' | 'skin',
|
|
442
|
+
artifact: MaterialShaderArtifact,
|
|
443
|
+
): string {
|
|
444
|
+
return `shadow:${viewKey}:${gpuDrivenProjectionKey(batch, deformation, artifact)}`;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function sourceDrawForCandidate(
|
|
448
|
+
slot: PersistentGpuDrivenState['slots'][number],
|
|
449
|
+
candidate: GpuDrivenBatch['candidates'][number],
|
|
450
|
+
): NonNullable<RenderableSnapshot['gpuDrivenDraws']>[number] | undefined {
|
|
451
|
+
return slot.snapshot.gpuDrivenDraws?.find(
|
|
452
|
+
(draw, compactIndex) =>
|
|
453
|
+
gpuDrivenSourceDrawItemIndex(draw, compactIndex) === candidate.drawItemIndex,
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function shadowKeysForCandidate(
|
|
458
|
+
slot: PersistentGpuDrivenState['slots'][number],
|
|
459
|
+
candidate: GpuDrivenBatch['candidates'][number],
|
|
460
|
+
shadowCasterDrawKeys: ReadonlySet<string> | undefined,
|
|
461
|
+
shadowCasterMembership: readonly ShadowCasterMembership[] | undefined,
|
|
462
|
+
): readonly string[] {
|
|
463
|
+
const draw = sourceDrawForCandidate(slot, candidate);
|
|
464
|
+
if (draw === undefined) return [];
|
|
465
|
+
const material = slot.snapshot.materials[draw.materialSlot] ?? slot.snapshot.material;
|
|
466
|
+
const worldEntity = worldEntityKey(slot.snapshot.worldId, slot.snapshot.entityKey);
|
|
467
|
+
const base = gpuDrivenDrawKey(
|
|
468
|
+
worldEntity,
|
|
469
|
+
material.materialHandle ?? -1,
|
|
470
|
+
candidate.drawItemIndex,
|
|
471
|
+
);
|
|
472
|
+
if (shadowCasterMembership !== undefined) {
|
|
473
|
+
const keys = new Set<string>();
|
|
474
|
+
for (const membership of shadowCasterMembership) {
|
|
475
|
+
if (
|
|
476
|
+
membership.worldEntity === worldEntity &&
|
|
477
|
+
membership.materialHandle === (material.materialHandle ?? -1) &&
|
|
478
|
+
membership.drawItemIndex === candidate.drawItemIndex
|
|
479
|
+
) {
|
|
480
|
+
keys.add(
|
|
481
|
+
gpuDrivenShadowDrawKey(
|
|
482
|
+
membership.worldEntity,
|
|
483
|
+
membership.materialHandle,
|
|
484
|
+
membership.drawItemIndex,
|
|
485
|
+
membership.passIndex,
|
|
486
|
+
),
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return [...keys];
|
|
491
|
+
}
|
|
492
|
+
if (shadowCasterDrawKeys === undefined) return [];
|
|
493
|
+
return [...shadowCasterDrawKeys].filter((key) => {
|
|
494
|
+
const parts = key.split(':');
|
|
495
|
+
return parts.length === 4 && `${parts[0]}:${parts[1]}:${parts[2]}` === base;
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function shadowMembershipIsGpuCompatible(
|
|
500
|
+
slot: PersistentGpuDrivenState['slots'][number],
|
|
501
|
+
candidate: GpuDrivenBatch['candidates'][number],
|
|
502
|
+
shadowCasterDrawKeys: ReadonlySet<string> | undefined,
|
|
503
|
+
shadowCasterMembership: readonly ShadowCasterMembership[] | undefined,
|
|
504
|
+
): boolean {
|
|
505
|
+
const keys = shadowKeysForCandidate(
|
|
506
|
+
slot,
|
|
507
|
+
candidate,
|
|
508
|
+
shadowCasterDrawKeys,
|
|
509
|
+
shadowCasterMembership,
|
|
510
|
+
);
|
|
511
|
+
if (keys.length !== 1) return false;
|
|
512
|
+
if (shadowCasterMembership === undefined) return true;
|
|
513
|
+
const draw = sourceDrawForCandidate(slot, candidate);
|
|
514
|
+
if (draw === undefined) return false;
|
|
515
|
+
const material = slot.snapshot.materials[draw.materialSlot] ?? slot.snapshot.material;
|
|
516
|
+
const worldEntity = worldEntityKey(slot.snapshot.worldId, slot.snapshot.entityKey);
|
|
517
|
+
const matching = shadowCasterMembership.filter(
|
|
518
|
+
(membership) =>
|
|
519
|
+
membership.worldEntity === worldEntity &&
|
|
520
|
+
membership.materialHandle === (material.materialHandle ?? -1) &&
|
|
521
|
+
membership.drawItemIndex === candidate.drawItemIndex,
|
|
522
|
+
);
|
|
523
|
+
if (matching.length !== 1) return false;
|
|
524
|
+
const membership = matching[0];
|
|
525
|
+
if (membership?.cpuReason !== undefined) return false;
|
|
526
|
+
if (membership?.renderState !== undefined) return false;
|
|
527
|
+
const shader = membership?.materialShaderId;
|
|
528
|
+
return shader === undefined || shader === 'forgeax::default-shadow-caster';
|
|
529
|
+
}
|
|
530
|
+
|
|
344
531
|
function selectedLodLevel(candidate: GpuDrivenCandidate): number {
|
|
345
532
|
const coverages = candidate.lodCoverages;
|
|
346
533
|
if (coverages === undefined || coverages.length <= 1) return 0;
|
|
@@ -368,28 +555,38 @@ function selectedLodRange(
|
|
|
368
555
|
);
|
|
369
556
|
}
|
|
370
557
|
|
|
371
|
-
/**
|
|
372
|
-
* One raster indirect command is emitted for each selector batch that has at
|
|
373
|
-
* least one admitted candidate. Selector-only batches remain in the compute
|
|
374
|
-
* and readback plan, but they must not inflate the raster command count.
|
|
375
|
-
*/
|
|
376
558
|
function admittedRasterBatchCount(plan: SubmissionPlan): number {
|
|
377
559
|
return plan.batches.reduce((count, batch) => count + (batch.visibleCapacity > 0 ? 1 : 0), 0);
|
|
378
560
|
}
|
|
379
561
|
|
|
562
|
+
function gpuDrawEntityCount(drawKeys: ReadonlySet<string>): number {
|
|
563
|
+
const entities = new Set<string>();
|
|
564
|
+
for (const key of drawKeys) {
|
|
565
|
+
const separator = key.indexOf(':');
|
|
566
|
+
if (separator > 0) entities.add(key.slice(0, separator));
|
|
567
|
+
}
|
|
568
|
+
return entities.size;
|
|
569
|
+
}
|
|
570
|
+
|
|
380
571
|
function filteredPlan(
|
|
381
572
|
source: SubmissionPlan,
|
|
382
573
|
slots: PersistentGpuDrivenState['slots'],
|
|
383
|
-
|
|
384
|
-
|
|
574
|
+
meshBySlot: ReadonlyMap<number, MeshGpuHandles>,
|
|
575
|
+
shadowCasterDrawKeys?: ReadonlySet<string>,
|
|
576
|
+
skinArtifact?: MaterialShaderArtifact,
|
|
577
|
+
shadowCasterMembership?: readonly ShadowCasterMembership[],
|
|
578
|
+
camera?: CameraSnapshot,
|
|
385
579
|
activeEntityKeys?: ReadonlySet<number>,
|
|
386
580
|
worldKeys?: readonly number[],
|
|
387
581
|
): {
|
|
388
582
|
readonly plan: SubmissionPlan;
|
|
389
583
|
readonly batches: readonly PreparedBatch[];
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
584
|
+
/** True only when every source draw item/instance has a main GPU candidate. */
|
|
585
|
+
readonly ownsAllDrawItems: boolean;
|
|
586
|
+
/** Concrete entity/material/draw identities claimed by this projection. */
|
|
587
|
+
readonly drawKeys: ReadonlySet<string>;
|
|
588
|
+
/** Extracted concrete ShadowCaster keys actually claimed by this plan. */
|
|
589
|
+
readonly shadowDrawKeys: ReadonlySet<string>;
|
|
393
590
|
} {
|
|
394
591
|
const slotByPrimitive = new Map(slots.map((slot) => [slot.slot, slot] as const));
|
|
395
592
|
const participatingByPrimitive = new Map<number, Set<GpuDrivenBatch>>();
|
|
@@ -403,42 +600,62 @@ function filteredPlan(
|
|
|
403
600
|
}
|
|
404
601
|
}
|
|
405
602
|
}
|
|
406
|
-
const
|
|
603
|
+
const eligibleMainCandidates = new Set<string>();
|
|
604
|
+
const drawKeys = new Set<string>();
|
|
605
|
+
const eligibleShadowCandidates = new Set<string>();
|
|
606
|
+
const shadowDrawKeys = new Set<string>();
|
|
607
|
+
const shadowSelectionProvided =
|
|
608
|
+
shadowCasterDrawKeys !== undefined || shadowCasterMembership !== undefined;
|
|
407
609
|
const worldKeyFor = (slot: PersistentGpuDrivenState['slots'][number]): number =>
|
|
408
610
|
worldKeys?.[slot.snapshot.worldId] ?? slot.snapshot.worldId;
|
|
409
611
|
const activeKey = (slot: PersistentGpuDrivenState['slots'][number]): number =>
|
|
410
612
|
worldEntityKey(worldKeyFor(slot), slot.snapshot.entityKey);
|
|
411
|
-
const eligiblePrimitives = new Set<number>();
|
|
412
|
-
let topologyEligiblePrimitiveCount = 0;
|
|
413
613
|
for (const slot of slots) {
|
|
414
|
-
const mesh =
|
|
415
|
-
const participating = participatingByPrimitive.get(slot.slot);
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
614
|
+
const mesh = meshBySlot.get(slot.slot);
|
|
615
|
+
const participating = participatingByPrimitive.get(slot.slot) ?? new Set<GpuDrivenBatch>();
|
|
616
|
+
for (const batch of participating) {
|
|
617
|
+
for (const candidate of batch.candidates) {
|
|
618
|
+
if (candidate.primitiveIndex !== slot.slot) continue;
|
|
619
|
+
const draw = sourceDrawForCandidate(slot, candidate);
|
|
620
|
+
const mainEligible =
|
|
621
|
+
draw !== undefined &&
|
|
622
|
+
preparedPbrCandidateEligible(slot.snapshot, batch, candidate, skinArtifact) &&
|
|
623
|
+
mesh !== undefined &&
|
|
624
|
+
meshSupportsBatch(mesh, batch);
|
|
625
|
+
if (!shadowSelectionProvided && mainEligible && draw !== undefined) {
|
|
626
|
+
eligibleMainCandidates.add(shadowCandidateKey(candidate));
|
|
627
|
+
const material = slot.snapshot.materials[draw.materialSlot] ?? slot.snapshot.material;
|
|
628
|
+
drawKeys.add(
|
|
629
|
+
gpuDrivenDrawKey(
|
|
630
|
+
worldEntityKey(worldKeyFor(slot), slot.snapshot.entityKey),
|
|
631
|
+
material.materialHandle ?? -1,
|
|
632
|
+
candidate.drawItemIndex,
|
|
633
|
+
),
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
if (
|
|
637
|
+
shadowSelectionProvided &&
|
|
638
|
+
mainEligible &&
|
|
639
|
+
shadowMembershipIsGpuCompatible(
|
|
640
|
+
slot,
|
|
641
|
+
candidate,
|
|
642
|
+
shadowCasterDrawKeys,
|
|
643
|
+
shadowCasterMembership,
|
|
644
|
+
)
|
|
645
|
+
) {
|
|
646
|
+
eligibleShadowCandidates.add(shadowCandidateKey(candidate));
|
|
647
|
+
for (const key of shadowKeysForCandidate(
|
|
648
|
+
slot,
|
|
649
|
+
candidate,
|
|
650
|
+
shadowCasterDrawKeys,
|
|
651
|
+
shadowCasterMembership,
|
|
652
|
+
)) {
|
|
653
|
+
shadowDrawKeys.add(key);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
433
656
|
}
|
|
434
657
|
}
|
|
435
658
|
}
|
|
436
|
-
// BatchTopology deliberately keeps LOD candidates individually addressable
|
|
437
|
-
// so topology updates never alias a candidate's selector state. Once the
|
|
438
|
-
// camera has projected the candidates, the renderer can safely coalesce
|
|
439
|
-
// candidates that have identical immutable geometry/LOD facts and the same
|
|
440
|
-
// selected range. The GPU selector uses the same no-history inputs below,
|
|
441
|
-
// so one indirect range remains correct for every candidate in a group.
|
|
442
659
|
const groups = new Map<
|
|
443
660
|
string,
|
|
444
661
|
{
|
|
@@ -451,15 +668,21 @@ function filteredPlan(
|
|
|
451
668
|
for (const batch of source.batches) {
|
|
452
669
|
const candidates = batch.candidates.filter((candidate) => {
|
|
453
670
|
const slot = slotByPrimitive.get(candidate.primitiveIndex);
|
|
454
|
-
if (slot === undefined
|
|
455
|
-
|
|
671
|
+
if (slot === undefined) return false;
|
|
672
|
+
if (
|
|
673
|
+
!shadowSelectionProvided
|
|
674
|
+
? !eligibleMainCandidates.has(shadowCandidateKey(candidate))
|
|
675
|
+
: !eligibleShadowCandidates.has(shadowCandidateKey(candidate))
|
|
676
|
+
)
|
|
677
|
+
return false;
|
|
678
|
+
const mesh = meshBySlot.get(slot.slot);
|
|
456
679
|
return mesh !== undefined && meshSupportsBatch(mesh, batch);
|
|
457
680
|
});
|
|
458
681
|
if (candidates.length === 0) continue;
|
|
459
682
|
for (const candidate of candidates) {
|
|
460
683
|
const slot = slotByPrimitive.get(candidate.primitiveIndex);
|
|
461
684
|
if (slot === undefined) continue;
|
|
462
|
-
const mesh =
|
|
685
|
+
const mesh = meshBySlot.get(slot.slot);
|
|
463
686
|
if (mesh === undefined) continue;
|
|
464
687
|
const physicalLodRanges = mesh.lodRanges
|
|
465
688
|
?.slice(1)
|
|
@@ -467,7 +690,10 @@ function filteredPlan(
|
|
|
467
690
|
const projected = Object.freeze({
|
|
468
691
|
...candidate,
|
|
469
692
|
submitAdmission: activeEntityKeys === undefined || activeEntityKeys.has(activeKey(slot)),
|
|
470
|
-
projectedHeight:
|
|
693
|
+
projectedHeight:
|
|
694
|
+
camera === undefined
|
|
695
|
+
? (candidate.projectedHeight ?? Number.NaN)
|
|
696
|
+
: projectedHeightForCandidate(slot, camera),
|
|
471
697
|
...(physicalLodRanges?.every((range) => range !== undefined)
|
|
472
698
|
? { lodRanges: physicalLodRanges as NonNullable<typeof candidate.lodRanges> }
|
|
473
699
|
: {}),
|
|
@@ -495,7 +721,6 @@ function filteredPlan(
|
|
|
495
721
|
}
|
|
496
722
|
}
|
|
497
723
|
}
|
|
498
|
-
|
|
499
724
|
const batches: PreparedBatch[] = [];
|
|
500
725
|
let visibleBase = 0;
|
|
501
726
|
const filteredBatches: GpuDrivenBatch[] = [];
|
|
@@ -521,7 +746,19 @@ function filteredPlan(
|
|
|
521
746
|
});
|
|
522
747
|
visibleBase += admittedCount;
|
|
523
748
|
filteredBatches.push(filtered);
|
|
524
|
-
|
|
749
|
+
// Keep every selector batch id unique even when no current entity is
|
|
750
|
+
// admitted to raster. The zero-admission batch still feeds LOD telemetry,
|
|
751
|
+
// while the following raster batch must not reuse its indirect slot.
|
|
752
|
+
batchId += 1;
|
|
753
|
+
// A selector batch can retain facet-suppressed candidates for LOD
|
|
754
|
+
// telemetry. Raster ownership, however, must be represented by an
|
|
755
|
+
// admitted candidate; using the first retained row can point material
|
|
756
|
+
// routing at an entity that the main-pass producer intentionally omitted.
|
|
757
|
+
const firstCandidate = group.candidates.find(
|
|
758
|
+
(candidate) =>
|
|
759
|
+
(candidate as GpuDrivenCandidate & { readonly submitAdmission?: boolean })
|
|
760
|
+
.submitAdmission !== false,
|
|
761
|
+
);
|
|
525
762
|
const firstSlot =
|
|
526
763
|
firstCandidate === undefined ? undefined : slotByPrimitive.get(firstCandidate.primitiveIndex);
|
|
527
764
|
if (firstCandidate === undefined || firstSlot === undefined) continue;
|
|
@@ -533,23 +770,142 @@ function filteredPlan(
|
|
|
533
770
|
primitiveSlot: firstSlot.slot,
|
|
534
771
|
slotGeneration: firstSlot.generation,
|
|
535
772
|
});
|
|
536
|
-
batchId += 1;
|
|
537
773
|
}
|
|
774
|
+
const ownershipSlots =
|
|
775
|
+
activeEntityKeys === undefined
|
|
776
|
+
? slots
|
|
777
|
+
: slots.filter((slot) => activeEntityKeys.has(activeKey(slot)));
|
|
778
|
+
const ownsAllDrawItems =
|
|
779
|
+
!shadowSelectionProvided &&
|
|
780
|
+
ownershipSlots.every((slot) => {
|
|
781
|
+
const draws = slot.snapshot.gpuDrivenDraws ?? [];
|
|
782
|
+
if (draws.length === 0) return false;
|
|
783
|
+
const instanceCount = slot.snapshot.instances?.instanceCount ?? 1;
|
|
784
|
+
const candidates = source.batches.flatMap((batch) =>
|
|
785
|
+
batch.candidates.filter((candidate) => candidate.primitiveIndex === slot.slot),
|
|
786
|
+
);
|
|
787
|
+
return (
|
|
788
|
+
candidates.length === draws.length * instanceCount &&
|
|
789
|
+
candidates.every((candidate) => {
|
|
790
|
+
if (!eligibleMainCandidates.has(shadowCandidateKey(candidate))) return false;
|
|
791
|
+
const draw = sourceDrawForCandidate(slot, candidate);
|
|
792
|
+
if (draw === undefined) return false;
|
|
793
|
+
const material = slot.snapshot.materials[draw.materialSlot] ?? slot.snapshot.material;
|
|
794
|
+
return drawKeys.has(
|
|
795
|
+
gpuDrivenDrawKey(
|
|
796
|
+
worldEntityKey(worldKeyFor(slot), slot.snapshot.entityKey),
|
|
797
|
+
material.materialHandle ?? -1,
|
|
798
|
+
candidate.drawItemIndex,
|
|
799
|
+
),
|
|
800
|
+
);
|
|
801
|
+
})
|
|
802
|
+
);
|
|
803
|
+
});
|
|
538
804
|
return {
|
|
539
805
|
plan: Object.freeze({
|
|
540
806
|
revision: source.revision,
|
|
807
|
+
payloadRevision: source.payloadRevision,
|
|
541
808
|
batches: Object.freeze(filteredBatches),
|
|
542
809
|
candidateCount: filteredBatches.reduce((total, batch) => total + batch.candidates.length, 0),
|
|
543
810
|
visibleCapacity: visibleBase,
|
|
544
811
|
}),
|
|
545
812
|
batches: Object.freeze(batches),
|
|
546
|
-
|
|
547
|
-
|
|
813
|
+
ownsAllDrawItems,
|
|
814
|
+
drawKeys,
|
|
815
|
+
shadowDrawKeys,
|
|
548
816
|
};
|
|
549
817
|
}
|
|
550
818
|
|
|
819
|
+
function shadowCasterMembershipSignature(
|
|
820
|
+
drawKeys: ReadonlySet<string> | undefined,
|
|
821
|
+
membership: readonly ShadowCasterMembership[] | undefined,
|
|
822
|
+
): string {
|
|
823
|
+
const keySignature = drawKeys === undefined ? '*' : [...drawKeys].sort().join(',');
|
|
824
|
+
if (membership === undefined) return keySignature;
|
|
825
|
+
const membershipSignature = membership
|
|
826
|
+
.map((entry) =>
|
|
827
|
+
[
|
|
828
|
+
entry.worldEntity,
|
|
829
|
+
entry.renderableIndex,
|
|
830
|
+
entry.drawItemIndex,
|
|
831
|
+
entry.materialHandle,
|
|
832
|
+
entry.passIndex,
|
|
833
|
+
entry.materialShaderId ?? '',
|
|
834
|
+
JSON.stringify(entry.renderState ?? null),
|
|
835
|
+
entry.cpuReason ?? '',
|
|
836
|
+
].join(':'),
|
|
837
|
+
)
|
|
838
|
+
.sort()
|
|
839
|
+
.join(',');
|
|
840
|
+
return `${keySignature}|membership=${membershipSignature}`;
|
|
841
|
+
}
|
|
842
|
+
|
|
551
843
|
type FilteredProductionPlan = ReturnType<typeof filteredPlan>;
|
|
552
844
|
|
|
845
|
+
function sceneRequiresGpuDrivenArtifact(scene: PersistentGpuDrivenState): boolean {
|
|
846
|
+
return scene.plan.batches.some((batch) =>
|
|
847
|
+
batch.candidates.some((candidate) => {
|
|
848
|
+
const material = candidate.prepared?.identity.material;
|
|
849
|
+
return (
|
|
850
|
+
(material === 'forgeax::default-standard-pbr' ||
|
|
851
|
+
material === 'forgeax::pbr-skin' ||
|
|
852
|
+
material === 'forgeax::default-standard-pbr-skin') &&
|
|
853
|
+
(candidate.prepared?.identity.deformation === 'rigid' ||
|
|
854
|
+
candidate.prepared?.identity.deformation === 'skin')
|
|
855
|
+
);
|
|
856
|
+
}),
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function nonCpuPreparationFailure(
|
|
861
|
+
scene: PersistentGpuDrivenState,
|
|
862
|
+
): { readonly error: GpuDrivenPreparationError; readonly drawKeys: readonly string[] } | undefined {
|
|
863
|
+
const drawKeys: string[] = [];
|
|
864
|
+
let first: GpuDrivenPreparationError | undefined;
|
|
865
|
+
for (const slot of scene.slots) {
|
|
866
|
+
const draws = slot.snapshot.gpuDrivenDraws ?? [];
|
|
867
|
+
for (const [compactIndex, draw] of draws.entries()) {
|
|
868
|
+
const error = draw.preparationError;
|
|
869
|
+
if (error === undefined || error.detail.recovery === 'route-cpu-lane') continue;
|
|
870
|
+
const material = slot.snapshot.materials[draw.materialSlot] ?? slot.snapshot.material;
|
|
871
|
+
drawKeys.push(
|
|
872
|
+
gpuDrivenDrawKey(
|
|
873
|
+
worldEntityKey(slot.snapshot.worldId, slot.snapshot.entityKey),
|
|
874
|
+
material.materialHandle ?? -1,
|
|
875
|
+
gpuDrivenSourceDrawItemIndex(draw, compactIndex),
|
|
876
|
+
),
|
|
877
|
+
);
|
|
878
|
+
first ??= error;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
return first === undefined ? undefined : { error: first, drawKeys };
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function preparationFailureInspection(
|
|
885
|
+
error: GpuDrivenPreparationError,
|
|
886
|
+
): GpuDrivenPreparationFailureInspection {
|
|
887
|
+
return Object.freeze({
|
|
888
|
+
code: error.code,
|
|
889
|
+
expected: error.expected,
|
|
890
|
+
hint: error.hint,
|
|
891
|
+
detail: error.detail,
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function shadowOwnershipFailureInspection(
|
|
896
|
+
viewPass: GpuDrivenLaneSummary['viewPass'],
|
|
897
|
+
): GpuDrivenPreparationFailureInspection {
|
|
898
|
+
return preparationFailureInspection(
|
|
899
|
+
new GpuDrivenPreparationError('shadow-ownership', {
|
|
900
|
+
reason: 'shadow-ownership-missing',
|
|
901
|
+
owner: 'shadow',
|
|
902
|
+
expected: 'one GPU-compatible owner for every declared ShadowCaster pass',
|
|
903
|
+
actual: `${viewPass} contains a residual ShadowCaster draw item`,
|
|
904
|
+
recovery: 'route-cpu-lane',
|
|
905
|
+
}),
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
|
|
553
909
|
export interface GpuDrivenWorldLodSelectionInspection {
|
|
554
910
|
readonly worldKey: number;
|
|
555
911
|
readonly primitiveSlot: number;
|
|
@@ -570,89 +926,322 @@ function sameEntityKeySet(
|
|
|
570
926
|
return true;
|
|
571
927
|
}
|
|
572
928
|
|
|
573
|
-
|
|
929
|
+
/** The GPU-driven bridge to the existing four-group Standard PBR pipeline. */
|
|
930
|
+
class StandardPbrRasterAdapter {
|
|
574
931
|
private readonly pipelines = new Map<string, RenderPipeline>();
|
|
932
|
+
private readonly projectionBuffers = new Set<Buffer>();
|
|
933
|
+
private readonly projectionBindings = new Map<
|
|
934
|
+
string,
|
|
935
|
+
{
|
|
936
|
+
readonly buffer: Buffer;
|
|
937
|
+
readonly bindGroup: BindGroup;
|
|
938
|
+
readonly capacityBytes: number;
|
|
939
|
+
rows: Float32Array;
|
|
940
|
+
}
|
|
941
|
+
>();
|
|
942
|
+
/**
|
|
943
|
+
* Projection rows are graph-generation resources. A new topology may stage
|
|
944
|
+
* a replacement before the graph is promoted; keeping staged bindings out
|
|
945
|
+
* of the active map lets an aborted graph leave the last submitted rows
|
|
946
|
+
* intact. Superseded buffers are fence-retired on promotion.
|
|
947
|
+
*/
|
|
948
|
+
private readonly stagedProjectionBindings = new Map<
|
|
949
|
+
string,
|
|
950
|
+
{
|
|
951
|
+
readonly buffer: Buffer;
|
|
952
|
+
readonly bindGroup: BindGroup;
|
|
953
|
+
readonly capacityBytes: number;
|
|
954
|
+
rows: Float32Array;
|
|
955
|
+
}
|
|
956
|
+
>();
|
|
957
|
+
private readonly pendingProjectionRetirements = new Set<Buffer>();
|
|
958
|
+
private readonly destroyedProjectionBuffers = new Set<Buffer>();
|
|
959
|
+
private projectionDisposed = false;
|
|
960
|
+
private stagedProjectionKeys = new Set<string>();
|
|
961
|
+
private readonly activeProjectionRowsAtStage = new Map<string, Float32Array>();
|
|
962
|
+
private readonly visibleWindowBindGroups = new Map<Buffer, Map<string, BindGroup>>();
|
|
575
963
|
|
|
576
964
|
private constructor(
|
|
577
965
|
private readonly device: RhiDevice,
|
|
578
|
-
private readonly layout: BindGroupLayout,
|
|
579
966
|
private readonly pipelineLayout: import('@forgeax/engine-rhi').PipelineLayout,
|
|
580
|
-
private readonly
|
|
967
|
+
private readonly meshLayout: BindGroupLayout,
|
|
968
|
+
private readonly instancesBindGroupLayout: BindGroupLayout,
|
|
969
|
+
private readonly programs: ReadonlyMap<string, GpuDrivenPbrProgram>,
|
|
970
|
+
private instancesBindGroup: BindGroup,
|
|
971
|
+
private readonly instanceBuffer: Buffer,
|
|
972
|
+
private readonly instanceBufferSize: number,
|
|
973
|
+
private visibleBuffer: Buffer,
|
|
974
|
+
private readonly artifact: MaterialShaderArtifact,
|
|
975
|
+
private readonly deformation: 'rigid' | 'skin',
|
|
976
|
+
private paletteBuffer: Buffer | undefined,
|
|
977
|
+
private paletteBindingWindowBytes: number | undefined,
|
|
581
978
|
) {}
|
|
582
979
|
|
|
583
980
|
static create(input: {
|
|
584
981
|
readonly device: RhiDevice;
|
|
585
|
-
readonly
|
|
586
|
-
readonly
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
982
|
+
readonly pipelineState: PipelineState;
|
|
983
|
+
readonly artifact: MaterialShaderArtifact;
|
|
984
|
+
readonly visibleBuffer: Buffer;
|
|
985
|
+
readonly deformation?: 'rigid' | 'skin';
|
|
986
|
+
readonly paletteBuffer?: Buffer;
|
|
987
|
+
readonly paletteBindingWindowBytes?: number;
|
|
988
|
+
}): Result<StandardPbrRasterAdapter, RhiError> {
|
|
989
|
+
const deformation = input.deformation ?? 'rigid';
|
|
990
|
+
const pipelineLayout =
|
|
991
|
+
deformation === 'skin'
|
|
992
|
+
? input.pipelineState.gpuDrivenPbrSkinPipelineLayout
|
|
993
|
+
: input.pipelineState.gpuDrivenPbrPipelineLayout;
|
|
994
|
+
const instancesBindGroupLayout = input.pipelineState.gpuDrivenInstancesBindGroupLayout;
|
|
995
|
+
if (pipelineLayout == null || instancesBindGroupLayout == null) {
|
|
996
|
+
return err(
|
|
997
|
+
new RhiError({
|
|
998
|
+
code: 'rhi-not-available',
|
|
999
|
+
expected: `${deformation === 'skin' ? 'skinned ' : ''}Standard PBR pipeline layout`,
|
|
1000
|
+
hint: 'initialize the producer Standard PBR pipeline before enabling GPU-driven raster',
|
|
1001
|
+
}),
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
const programs = input.pipelineState.gpuDrivenPbrPrograms;
|
|
1005
|
+
if (
|
|
1006
|
+
programs === undefined ||
|
|
1007
|
+
resolveStandardPbrProgram(
|
|
1008
|
+
programs,
|
|
1009
|
+
input.artifact.material,
|
|
1010
|
+
input.artifact.receipt?.vertexInputs.some((attribute) => attribute.semantic === 'color'),
|
|
1011
|
+
) === undefined
|
|
1012
|
+
) {
|
|
1013
|
+
return err(
|
|
1014
|
+
new RhiError({
|
|
1015
|
+
code: 'rhi-not-available',
|
|
1016
|
+
expected: `producer-owned ${deformation === 'skin' ? 'skinned ' : ''}Standard PBR shader module for the current device generation`,
|
|
1017
|
+
hint: 'publish the validated Standard PBR module from buildReadyWebGPU before enabling GPU-driven raster',
|
|
1018
|
+
}),
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
const meshLayout =
|
|
1022
|
+
deformation === 'skin'
|
|
1023
|
+
? input.pipelineState.pbrSkinMeshBindGroupLayout
|
|
1024
|
+
: input.pipelineState.meshBindGroupLayout;
|
|
1025
|
+
if (meshLayout === null) {
|
|
1026
|
+
return err(
|
|
1027
|
+
new RhiError({
|
|
1028
|
+
code: 'rhi-not-available',
|
|
1029
|
+
expected: `${deformation === 'skin' ? 'skinned ' : ''}Standard PBR mesh bind-group layout`,
|
|
1030
|
+
hint: 'publish the exact mesh-array layout selected by the material receipt',
|
|
1031
|
+
}),
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
if (
|
|
1035
|
+
deformation === 'skin' &&
|
|
1036
|
+
(input.paletteBuffer === undefined || input.paletteBindingWindowBytes === undefined)
|
|
1037
|
+
) {
|
|
1038
|
+
return err(
|
|
1039
|
+
new RhiError({
|
|
1040
|
+
code: 'rhi-not-available',
|
|
1041
|
+
expected: 'persistent skin palette buffer and binding window',
|
|
1042
|
+
hint: 'keep the palette receipt and the GPU-driven skinned projection on one frame identity',
|
|
1043
|
+
}),
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
const identity = new Float32Array(16);
|
|
1047
|
+
identity[0] = 1;
|
|
1048
|
+
identity[5] = 1;
|
|
1049
|
+
identity[10] = 1;
|
|
1050
|
+
identity[15] = 1;
|
|
1051
|
+
const instancePayload = packInstanceStorageBuffer(identity);
|
|
1052
|
+
const instanceBuffer = input.device.createBuffer({
|
|
1053
|
+
label: 'standard-pbr-gpu-driven-instance-data',
|
|
1054
|
+
size: instancePayload.byteLength,
|
|
1055
|
+
usage: GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST,
|
|
1056
|
+
});
|
|
1057
|
+
if (!instanceBuffer.ok) return instanceBuffer;
|
|
1058
|
+
const instanceWrite = input.device.queue.writeBuffer(instanceBuffer.value, 0, instancePayload);
|
|
1059
|
+
if (!instanceWrite.ok) return instanceWrite;
|
|
1060
|
+
const instancesBindGroup = input.device.createBindGroup({
|
|
1061
|
+
label: 'standard-pbr-gpu-driven-visible-instances-bg',
|
|
1062
|
+
layout: instancesBindGroupLayout,
|
|
590
1063
|
entries: [
|
|
591
|
-
{
|
|
1064
|
+
{
|
|
1065
|
+
binding: 0,
|
|
1066
|
+
resource: bufferBinding(instanceBuffer.value, 0, instancePayload.byteLength),
|
|
1067
|
+
},
|
|
592
1068
|
{
|
|
593
1069
|
binding: 1,
|
|
594
|
-
|
|
595
|
-
buffer: { type: 'read-only-storage' },
|
|
1070
|
+
resource: bufferBinding(input.visibleBuffer),
|
|
596
1071
|
},
|
|
597
1072
|
{
|
|
598
1073
|
binding: 2,
|
|
599
|
-
|
|
600
|
-
buffer: { type: 'read-only-storage' },
|
|
1074
|
+
resource: bufferBinding(input.visibleBuffer),
|
|
601
1075
|
},
|
|
602
|
-
{ binding: 3, visibility: VERTEX_STAGE, buffer: { type: 'read-only-storage' } },
|
|
603
|
-
{ binding: 4, visibility: VERTEX_STAGE, buffer: { type: 'read-only-storage' } },
|
|
604
1076
|
],
|
|
605
1077
|
});
|
|
606
|
-
if (!
|
|
607
|
-
const pipelineLayout = input.device.createPipelineLayout({
|
|
608
|
-
label: 'gpu-driven-rigid-raster-pl',
|
|
609
|
-
bindGroupLayouts: [input.viewBindGroupLayout, layout.value],
|
|
610
|
-
});
|
|
611
|
-
if (!pipelineLayout.ok) return pipelineLayout;
|
|
612
|
-
const module = input.shaderModuleFactory.createShaderModule({
|
|
613
|
-
label: 'gpu-driven-rigid-unlit',
|
|
614
|
-
code: GPU_DRIVEN_RIGID_UNLIT_WGSL,
|
|
615
|
-
});
|
|
616
|
-
if (!module.ok) return module;
|
|
1078
|
+
if (!instancesBindGroup.ok) return instancesBindGroup;
|
|
617
1079
|
return ok(
|
|
618
|
-
new
|
|
1080
|
+
new StandardPbrRasterAdapter(
|
|
1081
|
+
input.device,
|
|
1082
|
+
pipelineLayout,
|
|
1083
|
+
meshLayout,
|
|
1084
|
+
instancesBindGroupLayout,
|
|
1085
|
+
programs,
|
|
1086
|
+
instancesBindGroup.value,
|
|
1087
|
+
instanceBuffer.value,
|
|
1088
|
+
instancePayload.byteLength,
|
|
1089
|
+
input.visibleBuffer,
|
|
1090
|
+
input.artifact,
|
|
1091
|
+
deformation,
|
|
1092
|
+
input.paletteBuffer,
|
|
1093
|
+
input.paletteBindingWindowBytes,
|
|
1094
|
+
),
|
|
619
1095
|
);
|
|
620
1096
|
}
|
|
621
1097
|
|
|
1098
|
+
ensurePaletteBuffer(
|
|
1099
|
+
paletteBuffer: Buffer | undefined,
|
|
1100
|
+
paletteBindingWindowBytes: number | undefined,
|
|
1101
|
+
): Result<void, RhiError> {
|
|
1102
|
+
if (this.deformation !== 'skin') return ok(undefined);
|
|
1103
|
+
if (paletteBuffer === undefined || paletteBindingWindowBytes === undefined) {
|
|
1104
|
+
return err(
|
|
1105
|
+
new RhiError({
|
|
1106
|
+
code: 'rhi-not-available',
|
|
1107
|
+
expected: 'persistent skin palette buffer and binding window',
|
|
1108
|
+
hint: 'publish a storage-backed SkinPaletteReceipt before the GPU-driven skinned lane',
|
|
1109
|
+
}),
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
this.paletteBuffer = paletteBuffer;
|
|
1113
|
+
this.paletteBindingWindowBytes = paletteBindingWindowBytes;
|
|
1114
|
+
return ok(undefined);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
ensureVisibleBuffer(visibleBuffer: Buffer): Result<void, RhiError> {
|
|
1118
|
+
if (this.visibleBuffer === visibleBuffer) return ok(undefined);
|
|
1119
|
+
const created = this.device.createBindGroup({
|
|
1120
|
+
label: 'standard-pbr-gpu-driven-visible-instances-bg',
|
|
1121
|
+
layout: this.instancesBindGroupLayout,
|
|
1122
|
+
entries: [
|
|
1123
|
+
{
|
|
1124
|
+
binding: 0,
|
|
1125
|
+
resource: bufferBinding(this.instanceBuffer, 0, this.instanceBufferSize),
|
|
1126
|
+
},
|
|
1127
|
+
{ binding: 1, resource: bufferBinding(visibleBuffer) },
|
|
1128
|
+
{ binding: 2, resource: bufferBinding(visibleBuffer) },
|
|
1129
|
+
],
|
|
1130
|
+
});
|
|
1131
|
+
if (!created.ok) return created;
|
|
1132
|
+
this.instancesBindGroup = created.value;
|
|
1133
|
+
this.visibleBuffer = visibleBuffer;
|
|
1134
|
+
return ok(undefined);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
visibleWindowBindGroup(
|
|
1138
|
+
visibleBase: number,
|
|
1139
|
+
visibleCapacity: number,
|
|
1140
|
+
): Result<BindGroup, RhiError> {
|
|
1141
|
+
return this.visibleWindowBindGroupForBuffer(this.visibleBuffer, visibleBase, visibleCapacity);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
visibleWindowBindGroupForBuffer(
|
|
1145
|
+
visibleBuffer: Buffer,
|
|
1146
|
+
visibleBase: number,
|
|
1147
|
+
visibleCapacity: number,
|
|
1148
|
+
): Result<BindGroup, RhiError> {
|
|
1149
|
+
const offset = visibleBase * 8;
|
|
1150
|
+
const size = visibleCapacity * 8;
|
|
1151
|
+
if (offset % 256 !== 0 || size <= 0) {
|
|
1152
|
+
return err(
|
|
1153
|
+
new RhiError({
|
|
1154
|
+
code: 'rhi-descriptor-invalid',
|
|
1155
|
+
expected: '256-byte-aligned non-empty visible window',
|
|
1156
|
+
hint: `allocate each GPU-driven batch visible window on the portable storage-buffer boundary (base=${visibleBase}, capacity=${visibleCapacity}, offset=${offset}, size=${size})`,
|
|
1157
|
+
}),
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
let groups = this.visibleWindowBindGroups.get(visibleBuffer);
|
|
1161
|
+
if (groups === undefined) {
|
|
1162
|
+
groups = new Map();
|
|
1163
|
+
this.visibleWindowBindGroups.set(visibleBuffer, groups);
|
|
1164
|
+
}
|
|
1165
|
+
const key = `${visibleBase}:${visibleCapacity}`;
|
|
1166
|
+
const cached = groups.get(key);
|
|
1167
|
+
if (cached !== undefined) return ok(cached);
|
|
1168
|
+
const created = this.device.createBindGroup({
|
|
1169
|
+
label: `standard-pbr-gpu-driven-visible-window-${visibleBase}`,
|
|
1170
|
+
layout: this.instancesBindGroupLayout,
|
|
1171
|
+
entries: [
|
|
1172
|
+
{
|
|
1173
|
+
binding: 0,
|
|
1174
|
+
resource: bufferBinding(this.instanceBuffer, 0, this.instanceBufferSize),
|
|
1175
|
+
},
|
|
1176
|
+
{ binding: 1, resource: bufferBinding(visibleBuffer, offset, size) },
|
|
1177
|
+
{ binding: 2, resource: bufferBinding(visibleBuffer, offset, size) },
|
|
1178
|
+
],
|
|
1179
|
+
});
|
|
1180
|
+
if (!created.ok) return created;
|
|
1181
|
+
groups.set(key, created.value);
|
|
1182
|
+
return ok(created.value);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
622
1185
|
pipeline(
|
|
623
1186
|
format: TextureFormat,
|
|
624
1187
|
sampleCount: 1 | 4,
|
|
625
|
-
|
|
1188
|
+
layoutProjection: MeshGpuHandles['layoutProjection'],
|
|
1189
|
+
shaderUvSetCount: number,
|
|
626
1190
|
topology: GpuDrivenBatch['key']['topology'],
|
|
627
1191
|
stripIndexFormat: 'uint16' | 'uint32' | undefined,
|
|
628
1192
|
renderState: MaterialRenderState | undefined,
|
|
629
1193
|
): Result<RenderPipeline, RhiError> {
|
|
630
|
-
const
|
|
1194
|
+
const vertexLayout = deriveVertexBufferLayoutFromProjection(layoutProjection, {
|
|
1195
|
+
shaderUvSetCount,
|
|
1196
|
+
})[0];
|
|
1197
|
+
if (vertexLayout === undefined) {
|
|
1198
|
+
return err(
|
|
1199
|
+
new RhiError({
|
|
1200
|
+
code: 'shader-compile-failed',
|
|
1201
|
+
expected: 'non-empty geometry VertexLayoutProjection',
|
|
1202
|
+
hint: 'prepare a receipt-compatible mesh projection before Standard PBR raster',
|
|
1203
|
+
}),
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
const program = resolveStandardPbrProgram(
|
|
1207
|
+
this.programs,
|
|
1208
|
+
this.artifact.material,
|
|
1209
|
+
layoutProjection.attributes.some((attribute) => attribute.key === 'color'),
|
|
1210
|
+
);
|
|
1211
|
+
if (program === undefined) {
|
|
1212
|
+
return err(
|
|
1213
|
+
new RhiError({
|
|
1214
|
+
code: 'rhi-not-available',
|
|
1215
|
+
expected: 'producer-owned Standard PBR program matching the geometry vertex layout',
|
|
1216
|
+
hint: 'publish both vertex-color variants for the current device generation',
|
|
1217
|
+
}),
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
const key = `${format}|${sampleCount}|${vertexLayout.arrayStride}|${topology}|${stripIndexFormat ?? ''}|${JSON.stringify(renderState ?? null)}|${program.artifact.layoutIdentity}|${layoutProjection.digest}|uv${shaderUvSetCount}`;
|
|
631
1221
|
const cached = this.pipelines.get(key);
|
|
632
1222
|
if (cached !== undefined) return ok(cached);
|
|
633
|
-
const
|
|
634
|
-
label: `gpu-driven
|
|
1223
|
+
const pipeline = this.device.createRenderPipeline({
|
|
1224
|
+
label: `standard-pbr-gpu-driven.${key}`,
|
|
635
1225
|
layout: this.pipelineLayout,
|
|
636
1226
|
vertex: {
|
|
637
|
-
module:
|
|
638
|
-
entryPoint: '
|
|
1227
|
+
module: program.module,
|
|
1228
|
+
entryPoint: program.artifact.receipt?.sceneIndexEntry ?? 'vs_scene_index',
|
|
639
1229
|
buffers: [
|
|
640
1230
|
{
|
|
641
|
-
arrayStride:
|
|
1231
|
+
arrayStride: vertexLayout.arrayStride,
|
|
642
1232
|
stepMode: 'vertex',
|
|
643
|
-
attributes:
|
|
1233
|
+
attributes: vertexLayout.attributes.map(({ shaderLocation, offset, format }) => ({
|
|
1234
|
+
shaderLocation,
|
|
1235
|
+
offset,
|
|
1236
|
+
format: format as GPUVertexFormat,
|
|
1237
|
+
})),
|
|
644
1238
|
},
|
|
645
1239
|
],
|
|
646
1240
|
},
|
|
647
1241
|
fragment: {
|
|
648
|
-
module:
|
|
1242
|
+
module: program.module,
|
|
649
1243
|
entryPoint: 'fs_main',
|
|
650
|
-
targets: [
|
|
651
|
-
{
|
|
652
|
-
format,
|
|
653
|
-
...(renderState?.blend === undefined ? {} : { blend: renderState.blend }),
|
|
654
|
-
},
|
|
655
|
-
],
|
|
1244
|
+
targets: [{ format }],
|
|
656
1245
|
},
|
|
657
1246
|
primitive: {
|
|
658
1247
|
topology,
|
|
@@ -664,272 +1253,290 @@ class GpuDrivenRigidRaster {
|
|
|
664
1253
|
format: 'depth24plus-stencil8',
|
|
665
1254
|
depthWriteEnabled: renderState?.depthWriteEnabled ?? true,
|
|
666
1255
|
depthCompare: renderState?.depthCompare ?? 'less',
|
|
667
|
-
...(renderState?.stencil === undefined
|
|
668
|
-
? {}
|
|
669
|
-
: {
|
|
670
|
-
stencilFront: renderState.stencil,
|
|
671
|
-
stencilBack: renderState.stencil,
|
|
672
|
-
stencilReadMask: renderState.stencilReadMask,
|
|
673
|
-
stencilWriteMask: renderState.stencilWriteMask,
|
|
674
|
-
}),
|
|
675
1256
|
},
|
|
676
|
-
...(sampleCount === 4
|
|
677
|
-
? {
|
|
678
|
-
multisample: {
|
|
679
|
-
count: 4,
|
|
680
|
-
alphaToCoverageEnabled: renderState?.alphaToCoverageEnabled ?? false,
|
|
681
|
-
},
|
|
682
|
-
}
|
|
683
|
-
: {}),
|
|
1257
|
+
...(sampleCount === 4 ? { multisample: { count: 4 } } : {}),
|
|
684
1258
|
});
|
|
685
|
-
if (!
|
|
686
|
-
this.pipelines.set(key,
|
|
687
|
-
return
|
|
1259
|
+
if (!pipeline.ok) return pipeline;
|
|
1260
|
+
this.pipelines.set(key, pipeline.value);
|
|
1261
|
+
return pipeline;
|
|
688
1262
|
}
|
|
689
1263
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
1264
|
+
projectionBindGroup(
|
|
1265
|
+
rows: Float32Array,
|
|
1266
|
+
key?: string,
|
|
1267
|
+
capacityRows?: number,
|
|
694
1268
|
): Result<BindGroup, RhiError> {
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
code: 'internal-error',
|
|
700
|
-
expected: 'GPU-driven view buffers exist after a successful view update',
|
|
701
|
-
hint: 'prepare the GPU-driven view before projecting its raster batches',
|
|
702
|
-
}),
|
|
1269
|
+
if (key !== undefined) {
|
|
1270
|
+
const capacityBytes = Math.max(
|
|
1271
|
+
MESH_SSBO_BYTES,
|
|
1272
|
+
(capacityRows ?? Math.ceil(rows.byteLength / MESH_SSBO_BYTES)) * MESH_SSBO_BYTES,
|
|
703
1273
|
);
|
|
1274
|
+
const retained = this.stagedProjectionBindings.get(key) ?? this.projectionBindings.get(key);
|
|
1275
|
+
if (retained !== undefined && retained.capacityBytes >= capacityBytes) {
|
|
1276
|
+
const write = this.writeProjectionRows(key, retained, rows);
|
|
1277
|
+
if (!write.ok) return write;
|
|
1278
|
+
return ok(retained.bindGroup);
|
|
1279
|
+
}
|
|
1280
|
+
const created = this.createProjectionBinding(rows, capacityBytes);
|
|
1281
|
+
if (!created.ok) return created;
|
|
1282
|
+
this.stagedProjectionBindings.set(key, created.value);
|
|
1283
|
+
return ok(created.value.bindGroup);
|
|
704
1284
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
entries: [
|
|
709
|
-
{ binding: 0, resource: bufferBinding(scene.primitiveBuffer) },
|
|
710
|
-
{ binding: 1, resource: bufferBinding(scene.instanceBuffer) },
|
|
711
|
-
{ binding: 2, resource: bufferBinding(scene.materialBuffer) },
|
|
712
|
-
{
|
|
713
|
-
binding: 3,
|
|
714
|
-
resource: bufferBinding(visible, batch.visibleBase * 8, batch.visibleCapacity * 8),
|
|
715
|
-
},
|
|
716
|
-
{
|
|
717
|
-
binding: 4,
|
|
718
|
-
resource: bufferBinding(scene.transformBuffer),
|
|
719
|
-
},
|
|
720
|
-
],
|
|
721
|
-
});
|
|
1285
|
+
const created = this.createProjectionBinding(rows, Math.max(MESH_SSBO_BYTES, rows.byteLength));
|
|
1286
|
+
if (!created.ok) return created;
|
|
1287
|
+
return ok(created.value.bindGroup);
|
|
722
1288
|
}
|
|
723
|
-
}
|
|
724
1289
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
1290
|
+
refreshProjectionRows(key: string, rows: Float32Array): Result<void, RhiError> {
|
|
1291
|
+
const retained = this.stagedProjectionBindings.get(key) ?? this.projectionBindings.get(key);
|
|
1292
|
+
if (retained === undefined) return ok(undefined);
|
|
1293
|
+
if (rows.byteLength > retained.capacityBytes) {
|
|
1294
|
+
const capacityBytes = Math.max(
|
|
1295
|
+
MESH_SSBO_BYTES,
|
|
1296
|
+
Math.ceil(rows.byteLength / MESH_SSBO_BYTES) * MESH_SSBO_BYTES,
|
|
1297
|
+
);
|
|
1298
|
+
const replacement = this.createProjectionBinding(rows, capacityBytes);
|
|
1299
|
+
if (!replacement.ok) return replacement;
|
|
1300
|
+
const previousStaged = this.stagedProjectionBindings.get(key);
|
|
1301
|
+
if (previousStaged !== undefined && previousStaged.buffer !== replacement.value.buffer) {
|
|
1302
|
+
this.retireProjectionBuffer(previousStaged.buffer);
|
|
1303
|
+
}
|
|
1304
|
+
this.stagedProjectionBindings.set(key, replacement.value);
|
|
1305
|
+
return ok(undefined);
|
|
1306
|
+
}
|
|
1307
|
+
return this.writeProjectionRows(key, retained, rows);
|
|
1308
|
+
}
|
|
729
1309
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
private readonly materialBindGroup: BindGroup,
|
|
736
|
-
private readonly instancesBindGroup: BindGroup,
|
|
737
|
-
private readonly artifact: MaterialShaderArtifact,
|
|
738
|
-
) {}
|
|
1310
|
+
/** Stage the projection keys required by the frame being prepared. */
|
|
1311
|
+
stageProjectionKeys(keys: ReadonlySet<string>): void {
|
|
1312
|
+
this.stagedProjectionKeys = new Set(keys);
|
|
1313
|
+
this.activeProjectionRowsAtStage.clear();
|
|
1314
|
+
}
|
|
739
1315
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
)
|
|
1316
|
+
/** Add a shadow projection key discovered while updating a view plan. */
|
|
1317
|
+
noteProjectionKey(key: string): void {
|
|
1318
|
+
this.stagedProjectionKeys.add(key);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
/** Promote staged projection resources after the command buffer submits. */
|
|
1322
|
+
commitProjectionBindings(): void {
|
|
1323
|
+
const keep = new Set(this.stagedProjectionKeys);
|
|
1324
|
+
for (const [key, staged] of this.stagedProjectionBindings) {
|
|
1325
|
+
if (!keep.has(key)) {
|
|
1326
|
+
this.retireProjectionBuffer(staged.buffer);
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
const previous = this.projectionBindings.get(key);
|
|
1330
|
+
this.projectionBindings.set(key, staged);
|
|
1331
|
+
if (previous !== undefined && previous.buffer !== staged.buffer) {
|
|
1332
|
+
this.retireProjectionBuffer(previous.buffer);
|
|
1333
|
+
}
|
|
756
1334
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
});
|
|
767
|
-
if (!materialBuffer.ok) return materialBuffer;
|
|
768
|
-
const materialPayload = new Float32Array(96);
|
|
769
|
-
materialPayload[0] = 1;
|
|
770
|
-
materialPayload[1] = 1;
|
|
771
|
-
materialPayload[2] = 1;
|
|
772
|
-
materialPayload[3] = 1;
|
|
773
|
-
materialPayload[5] = 1;
|
|
774
|
-
materialPayload[6] = 1;
|
|
775
|
-
const materialWrite = input.device.queue.writeBuffer(materialBuffer.value, 0, materialPayload);
|
|
776
|
-
if (!materialWrite.ok) return materialWrite;
|
|
777
|
-
const baseEntries = [
|
|
778
|
-
{ binding: 0, resource: bufferBinding(materialBuffer.value, 0, 384) },
|
|
779
|
-
...Array.from({ length: 8 }, (_, index) => [
|
|
780
|
-
{
|
|
781
|
-
binding: index * 2 + 1,
|
|
782
|
-
resource: { kind: 'sampler' as const, value: fallback.sampler },
|
|
783
|
-
},
|
|
784
|
-
{
|
|
785
|
-
binding: index * 2 + 2,
|
|
786
|
-
resource: {
|
|
787
|
-
kind: 'textureView' as const,
|
|
788
|
-
value:
|
|
789
|
-
index === 2
|
|
790
|
-
? input.pipelineState.defaultNormalTextureView
|
|
791
|
-
: input.pipelineState.defaultWhiteTextureView,
|
|
792
|
-
},
|
|
793
|
-
},
|
|
794
|
-
]).flat(),
|
|
795
|
-
];
|
|
796
|
-
const materialEntries = assembleMaterialWithSkylightEntries(
|
|
797
|
-
baseEntries,
|
|
798
|
-
{
|
|
799
|
-
irradianceView: fallback.irradianceView,
|
|
800
|
-
irradianceSampler: fallback.sampler,
|
|
801
|
-
prefilterView: fallback.prefilterView,
|
|
802
|
-
prefilterSampler: fallback.sampler,
|
|
803
|
-
brdfLutView: fallback.brdfLutView,
|
|
804
|
-
brdfLutSampler: fallback.sampler,
|
|
805
|
-
intensityBuffer: fallback.intensityBuffer,
|
|
806
|
-
},
|
|
807
|
-
{ sampler: fallback.sampler, backdropView: input.pipelineState.defaultWhiteTextureView },
|
|
808
|
-
);
|
|
809
|
-
const materialBindGroup = input.device.createBindGroup({
|
|
810
|
-
label: 'standard-pbr-gpu-driven-material-bg',
|
|
811
|
-
layout: input.pipelineState.materialBindGroupLayout,
|
|
812
|
-
entries: materialEntries,
|
|
813
|
-
});
|
|
814
|
-
if (!materialBindGroup.ok) return materialBindGroup;
|
|
815
|
-
const instancesBindGroup = input.device.createBindGroup({
|
|
816
|
-
label: 'standard-pbr-gpu-driven-identity-instances-bg',
|
|
817
|
-
layout: input.pipelineState.instancesBindGroupLayout,
|
|
818
|
-
entries: [
|
|
819
|
-
{
|
|
820
|
-
binding: 0,
|
|
821
|
-
resource: bufferBinding(input.pipelineState.identityInstanceBuffer),
|
|
822
|
-
},
|
|
823
|
-
],
|
|
824
|
-
});
|
|
825
|
-
if (!instancesBindGroup.ok) return instancesBindGroup;
|
|
826
|
-
return ok(
|
|
827
|
-
new StandardPbrRasterAdapter(
|
|
828
|
-
input.device,
|
|
829
|
-
pipelineLayout,
|
|
830
|
-
input.pipelineState.meshBindGroupLayout,
|
|
831
|
-
module.value,
|
|
832
|
-
materialBindGroup.value,
|
|
833
|
-
instancesBindGroup.value,
|
|
834
|
-
input.artifact,
|
|
835
|
-
),
|
|
836
|
-
);
|
|
1335
|
+
this.stagedProjectionBindings.clear();
|
|
1336
|
+
for (const [key, active] of this.projectionBindings) {
|
|
1337
|
+
if (!keep.has(key)) {
|
|
1338
|
+
this.projectionBindings.delete(key);
|
|
1339
|
+
this.retireProjectionBuffer(active.buffer);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
this.stagedProjectionKeys.clear();
|
|
1343
|
+
this.activeProjectionRowsAtStage.clear();
|
|
837
1344
|
}
|
|
838
1345
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
const
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
)
|
|
853
|
-
const vertexStride = projectedLayout?.arrayStride ?? layoutProjection.arrayStride;
|
|
854
|
-
const attributes = projectedLayout?.attributes ?? layoutProjection.attributes;
|
|
855
|
-
const key = `${format}|${sampleCount}|${vertexStride}|${topology}|${stripIndexFormat ?? ''}|${JSON.stringify(renderState ?? null)}|${this.artifact.layoutIdentity}`;
|
|
856
|
-
const cached = this.pipelines.get(key);
|
|
857
|
-
if (cached !== undefined) return ok(cached);
|
|
858
|
-
const pipeline = this.device.createRenderPipeline({
|
|
859
|
-
label: `standard-pbr-gpu-driven.${key}`,
|
|
860
|
-
layout: this.pipelineLayout,
|
|
861
|
-
vertex: {
|
|
862
|
-
module: this.module,
|
|
863
|
-
entryPoint: this.artifact.receipt?.sceneIndexEntry ?? 'vs_scene_index',
|
|
864
|
-
buffers: [
|
|
865
|
-
{
|
|
866
|
-
arrayStride: vertexStride,
|
|
867
|
-
stepMode: 'vertex',
|
|
868
|
-
attributes: attributes.map(({ shaderLocation, offset, format }) => ({
|
|
869
|
-
shaderLocation,
|
|
870
|
-
offset,
|
|
871
|
-
format: format as GPUVertexFormat,
|
|
872
|
-
})),
|
|
873
|
-
},
|
|
874
|
-
],
|
|
875
|
-
},
|
|
876
|
-
fragment: {
|
|
877
|
-
module: this.module,
|
|
878
|
-
entryPoint: 'fs_main',
|
|
879
|
-
targets: [{ format }],
|
|
880
|
-
},
|
|
881
|
-
primitive: {
|
|
882
|
-
topology,
|
|
883
|
-
cullMode: renderState?.cullMode ?? 'back',
|
|
884
|
-
frontFace: renderState?.frontFace ?? 'ccw',
|
|
885
|
-
...(stripIndexFormat === undefined ? {} : { stripIndexFormat }),
|
|
886
|
-
},
|
|
887
|
-
depthStencil: {
|
|
888
|
-
format: 'depth24plus-stencil8',
|
|
889
|
-
depthWriteEnabled: renderState?.depthWriteEnabled ?? true,
|
|
890
|
-
depthCompare: renderState?.depthCompare ?? 'less',
|
|
891
|
-
},
|
|
892
|
-
...(sampleCount === 4 ? { multisample: { count: 4 } } : {}),
|
|
893
|
-
});
|
|
894
|
-
if (!pipeline.ok) return pipeline;
|
|
895
|
-
this.pipelines.set(key, pipeline.value);
|
|
896
|
-
return pipeline;
|
|
1346
|
+
/** Discard resources created for an aborted graph while retaining LKG rows. */
|
|
1347
|
+
abortProjectionBindings(): void {
|
|
1348
|
+
for (const [key, rows] of this.activeProjectionRowsAtStage) {
|
|
1349
|
+
const active = this.projectionBindings.get(key);
|
|
1350
|
+
if (active === undefined) continue;
|
|
1351
|
+
const restored = this.device.queue.writeBuffer(active.buffer, 0, rows);
|
|
1352
|
+
if (restored.ok) active.rows = new Float32Array(rows);
|
|
1353
|
+
}
|
|
1354
|
+
for (const staged of this.stagedProjectionBindings.values()) {
|
|
1355
|
+
this.retireProjectionBuffer(staged.buffer);
|
|
1356
|
+
}
|
|
1357
|
+
this.stagedProjectionBindings.clear();
|
|
1358
|
+
this.stagedProjectionKeys.clear();
|
|
1359
|
+
this.activeProjectionRowsAtStage.clear();
|
|
897
1360
|
}
|
|
898
1361
|
|
|
899
|
-
|
|
1362
|
+
private createProjectionBinding(
|
|
1363
|
+
rows: Float32Array,
|
|
1364
|
+
capacityBytes: number,
|
|
1365
|
+
): Result<
|
|
1366
|
+
{
|
|
1367
|
+
readonly buffer: Buffer;
|
|
1368
|
+
readonly bindGroup: BindGroup;
|
|
1369
|
+
readonly capacityBytes: number;
|
|
1370
|
+
rows: Float32Array;
|
|
1371
|
+
},
|
|
1372
|
+
RhiError
|
|
1373
|
+
> {
|
|
900
1374
|
const buffer = this.device.createBuffer({
|
|
901
1375
|
label: 'standard-pbr-gpu-driven-scene-meshes',
|
|
902
|
-
size:
|
|
1376
|
+
size: capacityBytes,
|
|
903
1377
|
usage: GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST,
|
|
904
1378
|
});
|
|
905
1379
|
if (!buffer.ok) return buffer;
|
|
906
1380
|
const write = this.device.queue.writeBuffer(buffer.value, 0, rows);
|
|
907
|
-
if (!write.ok)
|
|
908
|
-
|
|
909
|
-
|
|
1381
|
+
if (!write.ok) {
|
|
1382
|
+
this.destroyProjectionBuffer(buffer.value);
|
|
1383
|
+
return write;
|
|
1384
|
+
}
|
|
1385
|
+
this.projectionBuffers.add(buffer.value);
|
|
1386
|
+
const entries = [
|
|
1387
|
+
{ binding: 0, resource: bufferBinding(buffer.value, 0, capacityBytes) },
|
|
1388
|
+
...(this.deformation === 'skin' &&
|
|
1389
|
+
this.paletteBuffer !== undefined &&
|
|
1390
|
+
this.paletteBindingWindowBytes !== undefined
|
|
1391
|
+
? [
|
|
1392
|
+
{
|
|
1393
|
+
binding: 1,
|
|
1394
|
+
// The scene-index second lane is a global palette row. Bind the
|
|
1395
|
+
// complete arena so rows beyond the legacy 255-joint window
|
|
1396
|
+
// remain addressable; the shader performs the row selection.
|
|
1397
|
+
resource: bufferBinding(this.paletteBuffer),
|
|
1398
|
+
},
|
|
1399
|
+
{
|
|
1400
|
+
binding: 2,
|
|
1401
|
+
// The dedicated GPU-driven skin layout keeps the current /
|
|
1402
|
+
// previous palette pair used by the direct skin layout. The
|
|
1403
|
+
// scene-index shader currently reads the current arena, but
|
|
1404
|
+
// the shared layout still requires a complete binding shape.
|
|
1405
|
+
resource: bufferBinding(this.paletteBuffer),
|
|
1406
|
+
},
|
|
1407
|
+
]
|
|
1408
|
+
: []),
|
|
1409
|
+
];
|
|
1410
|
+
const bindGroup = this.device.createBindGroup({
|
|
910
1411
|
label: 'standard-pbr-gpu-driven-scene-mesh-bg',
|
|
911
1412
|
layout: this.meshLayout,
|
|
912
|
-
entries
|
|
1413
|
+
entries,
|
|
1414
|
+
});
|
|
1415
|
+
if (!bindGroup.ok) {
|
|
1416
|
+
this.destroyProjectionBuffer(buffer.value);
|
|
1417
|
+
return bindGroup;
|
|
1418
|
+
}
|
|
1419
|
+
return ok({
|
|
1420
|
+
buffer: buffer.value,
|
|
1421
|
+
bindGroup: bindGroup.value,
|
|
1422
|
+
capacityBytes,
|
|
1423
|
+
rows: new Float32Array(rows),
|
|
913
1424
|
});
|
|
914
1425
|
}
|
|
915
1426
|
|
|
1427
|
+
private writeProjectionRows(
|
|
1428
|
+
key: string,
|
|
1429
|
+
binding: {
|
|
1430
|
+
readonly buffer: Buffer;
|
|
1431
|
+
readonly bindGroup: BindGroup;
|
|
1432
|
+
readonly capacityBytes: number;
|
|
1433
|
+
rows: Float32Array;
|
|
1434
|
+
},
|
|
1435
|
+
rows: Float32Array,
|
|
1436
|
+
): Result<void, RhiError> {
|
|
1437
|
+
// Active bindings are the last-known-good graph resources. Keep one CPU
|
|
1438
|
+
// snapshot so an aborted candidate can restore their exact contents
|
|
1439
|
+
// without allocating a second GPU buffer on every stable frame.
|
|
1440
|
+
if (
|
|
1441
|
+
this.stagedProjectionBindings.get(key) === undefined &&
|
|
1442
|
+
!this.activeProjectionRowsAtStage.has(key)
|
|
1443
|
+
) {
|
|
1444
|
+
this.activeProjectionRowsAtStage.set(key, new Float32Array(binding.rows));
|
|
1445
|
+
}
|
|
1446
|
+
const write = this.device.queue.writeBuffer(binding.buffer, 0, rows);
|
|
1447
|
+
if (!write.ok) return write;
|
|
1448
|
+
binding.rows = new Float32Array(rows);
|
|
1449
|
+
return ok(undefined);
|
|
1450
|
+
}
|
|
1451
|
+
|
|
916
1452
|
dispose(): void {
|
|
917
|
-
|
|
918
|
-
this.
|
|
1453
|
+
this.projectionDisposed = true;
|
|
1454
|
+
for (const binding of this.projectionBindings.values()) {
|
|
1455
|
+
this.destroyProjectionBuffer(binding.buffer);
|
|
1456
|
+
}
|
|
1457
|
+
for (const binding of this.stagedProjectionBindings.values()) {
|
|
1458
|
+
this.destroyProjectionBuffer(binding.buffer);
|
|
1459
|
+
}
|
|
1460
|
+
for (const buffer of this.projectionBuffers) this.destroyProjectionBuffer(buffer);
|
|
1461
|
+
for (const buffer of this.pendingProjectionRetirements) {
|
|
1462
|
+
this.destroyProjectionBuffer(buffer);
|
|
1463
|
+
}
|
|
1464
|
+
this.projectionBindings.clear();
|
|
1465
|
+
this.stagedProjectionBindings.clear();
|
|
1466
|
+
this.activeProjectionRowsAtStage.clear();
|
|
1467
|
+
this.projectionBuffers.clear();
|
|
1468
|
+
this.pendingProjectionRetirements.clear();
|
|
1469
|
+
this.device.destroyBuffer(this.instanceBuffer);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
private retireProjectionBuffer(buffer: Buffer): void {
|
|
1473
|
+
if (this.destroyedProjectionBuffers.has(buffer)) return;
|
|
1474
|
+
this.projectionBuffers.delete(buffer);
|
|
1475
|
+
if (this.projectionDisposed) {
|
|
1476
|
+
this.destroyProjectionBuffer(buffer);
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
if (!this.pendingProjectionRetirements.add(buffer)) return;
|
|
1480
|
+
void this.device.queue.onSubmittedWorkDone().then(
|
|
1481
|
+
() => {
|
|
1482
|
+
this.pendingProjectionRetirements.delete(buffer);
|
|
1483
|
+
this.destroyProjectionBuffer(buffer);
|
|
1484
|
+
},
|
|
1485
|
+
() => {
|
|
1486
|
+
this.pendingProjectionRetirements.delete(buffer);
|
|
1487
|
+
this.destroyProjectionBuffer(buffer);
|
|
1488
|
+
},
|
|
1489
|
+
);
|
|
919
1490
|
}
|
|
920
1491
|
|
|
921
|
-
|
|
922
|
-
|
|
1492
|
+
private destroyProjectionBuffer(buffer: Buffer): void {
|
|
1493
|
+
if (this.destroyedProjectionBuffers.has(buffer)) return;
|
|
1494
|
+
this.destroyedProjectionBuffers.add(buffer);
|
|
1495
|
+
this.projectionBuffers.delete(buffer);
|
|
1496
|
+
this.device.destroyBuffer(buffer);
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
get instancesFrameGroup(): BindGroup {
|
|
1500
|
+
return this.instancesBindGroup;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
get kind(): 'rigid' | 'skin' {
|
|
1504
|
+
return this.deformation;
|
|
923
1505
|
}
|
|
924
1506
|
}
|
|
925
1507
|
|
|
926
1508
|
export interface PreparedGpuDrivenFrame {
|
|
927
1509
|
readonly topologySignature: string;
|
|
928
|
-
|
|
929
|
-
readonly
|
|
1510
|
+
/** True when every source draw item/instance is claimed by the GPU lane. */
|
|
1511
|
+
readonly ownsAllDrawItems: boolean;
|
|
1512
|
+
/** True only when the extracted ShadowCaster channel is fully represented. */
|
|
1513
|
+
readonly ownsAllShadowCasters?: boolean;
|
|
1514
|
+
/** Concrete main-pass draw identities claimed by the GPU projection. */
|
|
1515
|
+
readonly drawKeys?: ReadonlySet<string>;
|
|
1516
|
+
readonly standardPbrFrameResources: GpuDrivenStandardPbrFrameResources;
|
|
930
1517
|
readonly occlusion?: OcclusionFrameProjection;
|
|
931
1518
|
/** @internal Commits a replacement generation only after graph promotion. */
|
|
932
1519
|
readonly _commitResourceReplacement: () => void;
|
|
1520
|
+
/** @internal Retires superseded view buffers when graph compilation promotes them. */
|
|
1521
|
+
readonly _commitGpuResourceReplacement?: () => void;
|
|
1522
|
+
/** @internal Discards staged shadow cache publication after a failed frame. */
|
|
1523
|
+
readonly _abortResourceReplacement?: () => void;
|
|
1524
|
+
/** Concrete ShadowCaster keys claimed by the GPU subset for this frame. */
|
|
1525
|
+
readonly shadowDrawKeys?: ReadonlySet<string>;
|
|
1526
|
+
/** View-scoped ShadowCaster claims consumed by each shadow record pass. */
|
|
1527
|
+
readonly shadowDrawKeysByView?: ReadonlyMap<string, ReadonlySet<string>>;
|
|
1528
|
+
readonly projectShadow?: (
|
|
1529
|
+
graph: RenderGraphBuilder<RenderPipelineFrame>,
|
|
1530
|
+
identity: ShadowViewIdentity,
|
|
1531
|
+
) => Result<ShadowViewProjection | undefined, RenderGraphError>;
|
|
1532
|
+
readonly updateShadowViews?: (
|
|
1533
|
+
views: readonly Omit<ShadowViewUpdateInput, 'sourcePlan' | 'scene'>[],
|
|
1534
|
+
) => Result<void, RhiError>;
|
|
1535
|
+
readonly shadowViewPool?: ShadowViewStatePool;
|
|
1536
|
+
readonly shadowBatchProjections?: ReadonlyMap<
|
|
1537
|
+
string,
|
|
1538
|
+
ReadonlyMap<number, GpuDrivenShadowBatchProjection>
|
|
1539
|
+
>;
|
|
933
1540
|
project(
|
|
934
1541
|
graph: RenderGraphBuilder<RenderPipelineFrame>,
|
|
935
1542
|
format: TextureFormat,
|
|
@@ -937,6 +1544,70 @@ export interface PreparedGpuDrivenFrame {
|
|
|
937
1544
|
): Result<RenderPipelineGpuDrivenProjection, RenderGraphError | RhiError>;
|
|
938
1545
|
}
|
|
939
1546
|
|
|
1547
|
+
/**
|
|
1548
|
+
* Renderer-owned identity token for the validation that resolves renderable
|
|
1549
|
+
* assets to GPU meshes. The token is deliberately narrower than a frame
|
|
1550
|
+
* number: unchanged persistent scene snapshots can reuse the resolved rows,
|
|
1551
|
+
* while a mesh/device/pipeline generation change forces a fresh producer read.
|
|
1552
|
+
*/
|
|
1553
|
+
export interface GpuDrivenResidencyValidationInput {
|
|
1554
|
+
/** Opaque renderer-computed world/source identity and generation fence. */
|
|
1555
|
+
readonly cacheKey: string;
|
|
1556
|
+
readonly renderables: readonly RenderableSnapshot[];
|
|
1557
|
+
readonly transparentDispatch: readonly DispatchEntry[];
|
|
1558
|
+
readonly pipelineState: PipelineState;
|
|
1559
|
+
readonly pipelineHandle: number;
|
|
1560
|
+
readonly meshResidencyEpoch: number;
|
|
1561
|
+
readonly deviceGeneration: number;
|
|
1562
|
+
readonly assetCatalogEpoch: number;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
interface GpuDrivenResidencyValidationCache extends GpuDrivenResidencyValidationInput {
|
|
1566
|
+
readonly value: readonly ValidatedRenderable[];
|
|
1567
|
+
readonly dispatchRenderableIndices: readonly (number | undefined)[];
|
|
1568
|
+
readonly renderableIdentities: readonly RenderableSnapshot[];
|
|
1569
|
+
readonly dispatchIdentities: readonly DispatchEntry[];
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
interface GpuDrivenCpuValidationTelemetryCache {
|
|
1573
|
+
readonly cacheKey: string;
|
|
1574
|
+
readonly validated: readonly { readonly source: RenderableSnapshot }[];
|
|
1575
|
+
readonly gpuOwnedDrawKeys: ReadonlySet<string>;
|
|
1576
|
+
readonly gpuOwnedDrawCount: number;
|
|
1577
|
+
readonly totalDrawItems: number;
|
|
1578
|
+
readonly gpuOwnedRows: number;
|
|
1579
|
+
readonly cpuFallbackDrawItems: number;
|
|
1580
|
+
readonly cpuSemanticFallbackDrawItems: number;
|
|
1581
|
+
readonly cpuDeformationFallbackDrawItems: number;
|
|
1582
|
+
readonly cpuDeformationReasons: ReadonlyMap<
|
|
1583
|
+
'skin-bounds-missing' | 'skin-address-missing' | 'resource-not-ready',
|
|
1584
|
+
number
|
|
1585
|
+
>;
|
|
1586
|
+
readonly blockedDrawItems: number;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
function sameIdentitySequence<T extends object>(
|
|
1590
|
+
expected: readonly T[],
|
|
1591
|
+
actual: readonly T[],
|
|
1592
|
+
): boolean {
|
|
1593
|
+
if (expected.length !== actual.length) return false;
|
|
1594
|
+
for (let index = 0; index < expected.length; index += 1) {
|
|
1595
|
+
if (expected[index] !== actual[index]) return false;
|
|
1596
|
+
}
|
|
1597
|
+
return true;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
function sameDispatchRenderableIndices(
|
|
1601
|
+
expected: readonly (number | undefined)[],
|
|
1602
|
+
actual: readonly DispatchEntry[],
|
|
1603
|
+
): boolean {
|
|
1604
|
+
if (expected.length !== actual.length) return false;
|
|
1605
|
+
for (let index = 0; index < expected.length; index += 1) {
|
|
1606
|
+
if (expected[index] !== actual[index]?.renderableIndex) return false;
|
|
1607
|
+
}
|
|
1608
|
+
return true;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
940
1611
|
export interface RecoveryCapabilityFacts {
|
|
941
1612
|
readonly generation: number;
|
|
942
1613
|
readonly shaderCompilation: boolean;
|
|
@@ -994,17 +1665,34 @@ export function classifyRecoveryCapability(
|
|
|
994
1665
|
export class GpuDrivenProduction {
|
|
995
1666
|
private disposed = false;
|
|
996
1667
|
private view: GpuDrivenView | undefined;
|
|
997
|
-
private raster: GpuDrivenRigidRaster | undefined;
|
|
998
1668
|
private standardRaster: StandardPbrRasterAdapter | undefined;
|
|
1669
|
+
private skinRaster: StandardPbrRasterAdapter | undefined;
|
|
999
1670
|
private readonly sceneIdentities = new WeakMap<object, number>();
|
|
1000
1671
|
private nextSceneIdentity = 1;
|
|
1001
1672
|
private filteredCache:
|
|
1002
1673
|
| {
|
|
1003
1674
|
readonly source: SubmissionPlan;
|
|
1675
|
+
readonly scene: PersistentGpuDrivenState['scene'];
|
|
1676
|
+
readonly slots: PersistentGpuDrivenState['slots'];
|
|
1677
|
+
readonly worldKeys: PersistentGpuDrivenState['worldKeys'];
|
|
1004
1678
|
readonly meshResidencyEpoch: number;
|
|
1005
1679
|
readonly lodProjectionFingerprint: string;
|
|
1006
1680
|
readonly activeEntityRevision?: number;
|
|
1007
1681
|
readonly activeEntityKeys?: ReadonlySet<number>;
|
|
1682
|
+
readonly skinArtifact?: MaterialShaderArtifact;
|
|
1683
|
+
readonly value: FilteredProductionPlan;
|
|
1684
|
+
}
|
|
1685
|
+
| undefined;
|
|
1686
|
+
private shadowFilteredCache:
|
|
1687
|
+
| {
|
|
1688
|
+
readonly source: SubmissionPlan;
|
|
1689
|
+
readonly scene: PersistentGpuDrivenState['scene'];
|
|
1690
|
+
readonly slots: PersistentGpuDrivenState['slots'];
|
|
1691
|
+
readonly worldKeys: PersistentGpuDrivenState['worldKeys'];
|
|
1692
|
+
readonly lodProjectionFingerprint: string;
|
|
1693
|
+
readonly meshResidencyEpoch: number;
|
|
1694
|
+
readonly membership: string;
|
|
1695
|
+
readonly skinArtifact?: MaterialShaderArtifact;
|
|
1008
1696
|
readonly value: FilteredProductionPlan;
|
|
1009
1697
|
}
|
|
1010
1698
|
| undefined;
|
|
@@ -1014,13 +1702,62 @@ export class GpuDrivenProduction {
|
|
|
1014
1702
|
private batchBindGroupCreates = 0;
|
|
1015
1703
|
private validatedGpuOwnedRows = 0;
|
|
1016
1704
|
private cpuFallbackDrawItems = 0;
|
|
1705
|
+
private cpuSemanticFallbackDrawItems = 0;
|
|
1706
|
+
private cpuDeformationFallbackDrawItems = 0;
|
|
1707
|
+
private readonly cpuDeformationReasons = new Map<
|
|
1708
|
+
'skin-bounds-missing' | 'skin-address-missing' | 'resource-not-ready',
|
|
1709
|
+
number
|
|
1710
|
+
>();
|
|
1711
|
+
private blockedDrawItems = 0;
|
|
1712
|
+
private preparationFailure: GpuDrivenPreparationFailureInspection | undefined;
|
|
1713
|
+
private encodedIndirectDraws = 0;
|
|
1017
1714
|
private telemetryPrepared = false;
|
|
1018
1715
|
private telemetryCandidateCount = 0;
|
|
1019
1716
|
private telemetryGeometryWork = 0;
|
|
1020
1717
|
private telemetryRootGeometryWork = 0;
|
|
1718
|
+
private telemetryOverflow = false;
|
|
1021
1719
|
private indirectDrawCount = 0;
|
|
1022
|
-
private
|
|
1720
|
+
private gpuOwnedDrawItems = 0;
|
|
1721
|
+
private mainTotalDrawItems = 0;
|
|
1722
|
+
private lane: 'gpu' | 'cpu-semantic' | 'cpu-deformation' | 'blocked' = 'cpu-semantic';
|
|
1723
|
+
private laneReason: GpuDrivenLaneReason = 'capability';
|
|
1724
|
+
private submissionState: 'idle' | 'pending' | 'submitted' | 'aborted' = 'idle';
|
|
1725
|
+
private retryCount = 0;
|
|
1726
|
+
private lastKnownGoodGeneration: number | undefined;
|
|
1727
|
+
private overflowRecoveryRequired = false;
|
|
1728
|
+
private shadowViews: ShadowViewStatePool | undefined;
|
|
1729
|
+
private shadowSourcePlan: SubmissionPlan | undefined;
|
|
1730
|
+
private shadowScene: PersistentGpuDrivenState['scene'] | undefined;
|
|
1731
|
+
private shadowBatchProjections = new Map<
|
|
1732
|
+
string,
|
|
1733
|
+
ReadonlyMap<number, GpuDrivenShadowBatchProjection>
|
|
1734
|
+
>();
|
|
1735
|
+
private shadowDrawKeysByView = new Map<string, ReadonlySet<string>>();
|
|
1736
|
+
private shadowExpectedDrawKeys = new Set<string>();
|
|
1737
|
+
private shadowBatchProjectionSource: SubmissionPlan | undefined;
|
|
1738
|
+
private skinPaletteBuffer: Buffer | undefined;
|
|
1739
|
+
private committedPaletteContentRevision: number | undefined;
|
|
1023
1740
|
private standardPbrProjection: StandardPbrSceneProjection | undefined;
|
|
1741
|
+
private committedProjectionSource:
|
|
1742
|
+
| {
|
|
1743
|
+
readonly filtered: FilteredProductionPlan;
|
|
1744
|
+
readonly scene: PersistentGpuDrivenState['scene'];
|
|
1745
|
+
readonly contentRevision: number;
|
|
1746
|
+
readonly artifact: MaterialShaderArtifact;
|
|
1747
|
+
readonly skinArtifact: MaterialShaderArtifact | undefined;
|
|
1748
|
+
}
|
|
1749
|
+
| undefined;
|
|
1750
|
+
private validatedResidencyCache: GpuDrivenResidencyValidationCache | undefined;
|
|
1751
|
+
private residencyValidationCacheHits = 0;
|
|
1752
|
+
private residencyValidationScans = 0;
|
|
1753
|
+
private cpuValidationTelemetryCache: GpuDrivenCpuValidationTelemetryCache | undefined;
|
|
1754
|
+
private cpuValidationCacheHits = 0;
|
|
1755
|
+
private cpuValidationScans = 0;
|
|
1756
|
+
private structureMetrics: GpuDrivenStructureMetrics = {
|
|
1757
|
+
worldEntitiesScanned: 0,
|
|
1758
|
+
sceneTableUploadBytes: 0,
|
|
1759
|
+
paletteUploadBytes: 0,
|
|
1760
|
+
};
|
|
1024
1761
|
private signatureCache:
|
|
1025
1762
|
| {
|
|
1026
1763
|
readonly filtered: FilteredProductionPlan;
|
|
@@ -1034,8 +1771,67 @@ export class GpuDrivenProduction {
|
|
|
1034
1771
|
constructor(
|
|
1035
1772
|
private readonly device: RhiDevice,
|
|
1036
1773
|
private readonly shaderModuleFactory: PipelineBuilderShaderModuleFactory,
|
|
1774
|
+
private readonly recoveryCount = 0,
|
|
1037
1775
|
) {}
|
|
1038
1776
|
|
|
1777
|
+
/**
|
|
1778
|
+
* Reuse the last producer-resolved residency rows when all source identity
|
|
1779
|
+
* and generation fences still match. Renderables and dispatch entries are
|
|
1780
|
+
* checked element-by-element as well as by their stable object identities so
|
|
1781
|
+
* an accidental in-place mutation cannot reuse stale renderable indices.
|
|
1782
|
+
*/
|
|
1783
|
+
reuseResidencyValidation(
|
|
1784
|
+
input: GpuDrivenResidencyValidationInput,
|
|
1785
|
+
): readonly ValidatedRenderable[] | undefined {
|
|
1786
|
+
const cached = this.validatedResidencyCache;
|
|
1787
|
+
if (
|
|
1788
|
+
cached === undefined ||
|
|
1789
|
+
cached.cacheKey !== input.cacheKey ||
|
|
1790
|
+
cached.pipelineState !== input.pipelineState ||
|
|
1791
|
+
cached.pipelineState.device !== this.device ||
|
|
1792
|
+
cached.pipelineHandle !== input.pipelineHandle ||
|
|
1793
|
+
cached.meshResidencyEpoch !== input.meshResidencyEpoch ||
|
|
1794
|
+
cached.deviceGeneration !== input.deviceGeneration ||
|
|
1795
|
+
cached.assetCatalogEpoch !== input.assetCatalogEpoch ||
|
|
1796
|
+
!sameIdentitySequence(cached.renderableIdentities, input.renderables) ||
|
|
1797
|
+
!sameIdentitySequence(cached.dispatchIdentities, input.transparentDispatch) ||
|
|
1798
|
+
!sameDispatchRenderableIndices(cached.dispatchRenderableIndices, input.transparentDispatch)
|
|
1799
|
+
) {
|
|
1800
|
+
return undefined;
|
|
1801
|
+
}
|
|
1802
|
+
this.residencyValidationCacheHits += 1;
|
|
1803
|
+
return cached.value;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
/** Publish the producer-owned rows after a completed residency validation. */
|
|
1807
|
+
rememberResidencyValidation(
|
|
1808
|
+
input: GpuDrivenResidencyValidationInput,
|
|
1809
|
+
value: readonly ValidatedRenderable[],
|
|
1810
|
+
): void {
|
|
1811
|
+
this.validatedResidencyCache = {
|
|
1812
|
+
...input,
|
|
1813
|
+
value,
|
|
1814
|
+
renderableIdentities: input.renderables,
|
|
1815
|
+
dispatchIdentities: input.transparentDispatch,
|
|
1816
|
+
dispatchRenderableIndices: input.transparentDispatch.map((entry) => entry.renderableIndex),
|
|
1817
|
+
};
|
|
1818
|
+
this.residencyValidationScans += 1;
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
private applyCpuValidationTelemetry(cache: GpuDrivenCpuValidationTelemetryCache): void {
|
|
1822
|
+
this.gpuOwnedDrawItems = cache.gpuOwnedDrawCount;
|
|
1823
|
+
this.mainTotalDrawItems = cache.totalDrawItems;
|
|
1824
|
+
this.validatedGpuOwnedRows = cache.gpuOwnedRows;
|
|
1825
|
+
this.cpuFallbackDrawItems = cache.cpuFallbackDrawItems;
|
|
1826
|
+
this.cpuSemanticFallbackDrawItems = cache.cpuSemanticFallbackDrawItems;
|
|
1827
|
+
this.cpuDeformationFallbackDrawItems = cache.cpuDeformationFallbackDrawItems;
|
|
1828
|
+
this.cpuDeformationReasons.clear();
|
|
1829
|
+
for (const [reason, count] of cache.cpuDeformationReasons) {
|
|
1830
|
+
this.cpuDeformationReasons.set(reason, count);
|
|
1831
|
+
}
|
|
1832
|
+
this.blockedDrawItems = cache.blockedDrawItems;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1039
1835
|
static forDevice(
|
|
1040
1836
|
device: RhiDevice,
|
|
1041
1837
|
shaderModuleFactory: PipelineBuilderShaderModuleFactory,
|
|
@@ -1043,34 +1839,6 @@ export class GpuDrivenProduction {
|
|
|
1043
1839
|
return new GpuDrivenProduction(device, shaderModuleFactory);
|
|
1044
1840
|
}
|
|
1045
1841
|
|
|
1046
|
-
/** Recovery has no frame retry: await generated programs before preparing the graph. */
|
|
1047
|
-
static async forRecoveryDevice(
|
|
1048
|
-
device: RhiDevice,
|
|
1049
|
-
factory: PipelineBuilderShaderModuleFactory,
|
|
1050
|
-
createShaderModule: (descriptor: {
|
|
1051
|
-
code: string;
|
|
1052
|
-
label?: string;
|
|
1053
|
-
}) => Promise<Result<ShaderModule, RhiError>>,
|
|
1054
|
-
): Promise<Result<GpuDrivenProduction, RhiError>> {
|
|
1055
|
-
const modules = new Map<string, ShaderModule>();
|
|
1056
|
-
for (const descriptor of [
|
|
1057
|
-
{ label: 'gpu-driven-view', code: GPU_DRIVEN_VIEW_WGSL },
|
|
1058
|
-
{ label: 'gpu-driven-rigid-unlit', code: GPU_DRIVEN_RIGID_UNLIT_WGSL },
|
|
1059
|
-
]) {
|
|
1060
|
-
const module = await createShaderModule(descriptor);
|
|
1061
|
-
if (!module.ok) return module;
|
|
1062
|
-
modules.set(descriptor.code, module.value);
|
|
1063
|
-
}
|
|
1064
|
-
return ok(
|
|
1065
|
-
new GpuDrivenProduction(device, {
|
|
1066
|
-
createShaderModule: (descriptor) => {
|
|
1067
|
-
const module = modules.get(descriptor.code);
|
|
1068
|
-
return module === undefined ? factory.createShaderModule(descriptor) : ok(module);
|
|
1069
|
-
},
|
|
1070
|
-
}),
|
|
1071
|
-
);
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
1842
|
createRecoveryRoot(scope: DeviceScope): LifecycleResourceSpec<unknown> {
|
|
1075
1843
|
return {
|
|
1076
1844
|
kind: 'scene-table',
|
|
@@ -1088,10 +1856,20 @@ export class GpuDrivenProduction {
|
|
|
1088
1856
|
prepare(input: {
|
|
1089
1857
|
readonly scene: PersistentGpuDrivenState | undefined;
|
|
1090
1858
|
readonly camera: CameraSnapshot;
|
|
1091
|
-
|
|
1859
|
+
/** GPU meshes projected by retained RenderScene slot, never raw asset handle. */
|
|
1860
|
+
readonly meshBySlot: ReadonlyMap<number, MeshGpuHandles>;
|
|
1092
1861
|
readonly viewBindGroupLayout: BindGroupLayout;
|
|
1093
1862
|
readonly meshResidencyEpoch: number;
|
|
1094
1863
|
readonly hdrp: boolean;
|
|
1864
|
+
/** Cooked producer artifact selected from the frame material catalog. */
|
|
1865
|
+
readonly standardPbrArtifact?: MaterialShaderArtifact;
|
|
1866
|
+
/** Cooked producer artifact for the GPU skinning variant. */
|
|
1867
|
+
readonly standardPbrSkinArtifact?: MaterialShaderArtifact;
|
|
1868
|
+
readonly standardPbrPipelineState?: PipelineState;
|
|
1869
|
+
/** Extracted ShadowCaster ownership keyed by concrete draw item and pass. */
|
|
1870
|
+
readonly shadowCasterDrawKeys?: ReadonlySet<string>;
|
|
1871
|
+
/** Structured ShadowCaster ownership facts for exact pass admission. */
|
|
1872
|
+
readonly shadowCasterMembership?: readonly ShadowCasterMembership[];
|
|
1095
1873
|
/** Entity keys admitted by the persistent visibility facet for this view. */
|
|
1096
1874
|
readonly activeEntityKeys?: ReadonlySet<number>;
|
|
1097
1875
|
/** Collision-free renderer-owned revision for the active visibility set. */
|
|
@@ -1099,20 +1877,37 @@ export class GpuDrivenProduction {
|
|
|
1099
1877
|
/** Total extracted candidates, including facet-suppressed rows. */
|
|
1100
1878
|
readonly telemetryCandidateCount?: number;
|
|
1101
1879
|
readonly occlusion?: OcclusionFrameProjection;
|
|
1102
|
-
/**
|
|
1103
|
-
readonly
|
|
1104
|
-
|
|
1105
|
-
}): Result<PreparedGpuDrivenFrame | undefined, RhiError> {
|
|
1880
|
+
/** Per-frame deltas from the existing RenderScene/GPU Scene/palette owners. */
|
|
1881
|
+
readonly structureMetrics?: GpuDrivenStructureMetrics;
|
|
1882
|
+
}): Result<PreparedGpuDrivenFrame | undefined, RhiError | GpuDrivenPreparationError> {
|
|
1106
1883
|
this.gpuOwnedSnapshotsMaterialized = 0;
|
|
1107
1884
|
this.filteredPlanBuilds = 0;
|
|
1108
1885
|
this.batchBindGroupCreates = 0;
|
|
1109
1886
|
this.validatedGpuOwnedRows = 0;
|
|
1110
1887
|
this.cpuFallbackDrawItems = 0;
|
|
1888
|
+
this.cpuSemanticFallbackDrawItems = 0;
|
|
1889
|
+
this.cpuDeformationFallbackDrawItems = 0;
|
|
1890
|
+
this.cpuDeformationReasons.clear();
|
|
1891
|
+
this.blockedDrawItems = 0;
|
|
1892
|
+
this.preparationFailure = undefined;
|
|
1893
|
+
this.encodedIndirectDraws = 0;
|
|
1111
1894
|
this.telemetryPrepared = false;
|
|
1112
1895
|
this.telemetryCandidateCount = input.telemetryCandidateCount ?? 0;
|
|
1113
1896
|
this.telemetryGeometryWork = 0;
|
|
1114
1897
|
this.telemetryRootGeometryWork = 0;
|
|
1898
|
+
this.telemetryOverflow = false;
|
|
1115
1899
|
this.indirectDrawCount = 0;
|
|
1900
|
+
this.gpuOwnedDrawItems = 0;
|
|
1901
|
+
this.mainTotalDrawItems = 0;
|
|
1902
|
+
this.structureMetrics = input.structureMetrics ?? {
|
|
1903
|
+
worldEntitiesScanned: 0,
|
|
1904
|
+
sceneTableUploadBytes: 0,
|
|
1905
|
+
paletteUploadBytes: 0,
|
|
1906
|
+
};
|
|
1907
|
+
this.lane = 'cpu-semantic';
|
|
1908
|
+
this.laneReason = input.hdrp ? 'unsupported' : 'capability';
|
|
1909
|
+
this.submissionState = 'idle';
|
|
1910
|
+
this.shadowExpectedDrawKeys = new Set();
|
|
1116
1911
|
const capability = classifyRecoveryCapability({
|
|
1117
1912
|
generation: 0,
|
|
1118
1913
|
shaderCompilation: true,
|
|
@@ -1122,26 +1917,79 @@ export class GpuDrivenProduction {
|
|
|
1122
1917
|
multisample: true,
|
|
1123
1918
|
});
|
|
1124
1919
|
if (
|
|
1125
|
-
input.hdrp ||
|
|
1126
1920
|
input.scene === undefined ||
|
|
1127
|
-
|
|
1128
|
-
selectGpuLodLane(this.device.caps) === 'cpu'
|
|
1921
|
+
input.hdrp ||
|
|
1922
|
+
selectGpuLodLane(this.device.caps) === 'cpu' ||
|
|
1923
|
+
capability.status === 'disabled'
|
|
1129
1924
|
) {
|
|
1130
1925
|
this.gpuOwnedEntityCount = 0;
|
|
1131
|
-
this.sceneRowsRequired = false;
|
|
1132
1926
|
return ok(undefined);
|
|
1133
1927
|
}
|
|
1928
|
+
if (this.overflowRecoveryRequired && this.view !== undefined) {
|
|
1929
|
+
const recovered = this.view._recoverFromOverflow();
|
|
1930
|
+
if (!recovered.ok) {
|
|
1931
|
+
this.gpuOwnedEntityCount = 0;
|
|
1932
|
+
this.lane = 'blocked';
|
|
1933
|
+
this.laneReason = 'capacity';
|
|
1934
|
+
return recovered;
|
|
1935
|
+
}
|
|
1936
|
+
this.overflowRecoveryRequired = false;
|
|
1937
|
+
}
|
|
1134
1938
|
const scene = input.scene;
|
|
1135
|
-
const
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1939
|
+
const preparationFailure = nonCpuPreparationFailure(scene);
|
|
1940
|
+
if (preparationFailure !== undefined) {
|
|
1941
|
+
this.gpuOwnedEntityCount = 0;
|
|
1942
|
+
// A capable candidate that cannot be prepared is a stopped promotion,
|
|
1943
|
+
// not a CPU-semantic lane. Keep the concrete rejected draw count in the
|
|
1944
|
+
// detached channel so an inspector can repair the producer and retry the
|
|
1945
|
+
// same view-pass without guessing from an error string.
|
|
1946
|
+
this.blockedDrawItems = preparationFailure.drawKeys.length;
|
|
1947
|
+
this.mainTotalDrawItems = Math.max(
|
|
1948
|
+
this.mainTotalDrawItems,
|
|
1949
|
+
scene.plan.batches.reduce((total, batch) => total + batch.candidates.length, 0),
|
|
1950
|
+
);
|
|
1951
|
+
this.lane = 'blocked';
|
|
1952
|
+
this.laneReason = 'resource-not-ready';
|
|
1953
|
+
this.preparationFailure = preparationFailureInspection(preparationFailure.error);
|
|
1954
|
+
return err(preparationFailure.error);
|
|
1955
|
+
}
|
|
1956
|
+
const standardArtifact = input.standardPbrArtifact;
|
|
1957
|
+
const skinArtifact = input.standardPbrSkinArtifact;
|
|
1958
|
+
const standardPipelineState = input.standardPbrPipelineState;
|
|
1959
|
+
if (standardArtifact === undefined || standardPipelineState === undefined) {
|
|
1960
|
+
this.gpuOwnedEntityCount = 0;
|
|
1961
|
+
this.laneReason = 'resource-not-ready';
|
|
1962
|
+
if (sceneRequiresGpuDrivenArtifact(input.scene)) {
|
|
1963
|
+
this.blockedDrawItems = input.scene.plan.batches.reduce(
|
|
1964
|
+
(total, batch) => total + batch.candidates.length,
|
|
1965
|
+
0,
|
|
1966
|
+
);
|
|
1967
|
+
this.mainTotalDrawItems = Math.max(this.mainTotalDrawItems, this.blockedDrawItems);
|
|
1968
|
+
this.lane = 'blocked';
|
|
1969
|
+
const failure =
|
|
1970
|
+
standardArtifact === undefined
|
|
1971
|
+
? new GpuDrivenPreparationError('missing-material-receipt', {
|
|
1972
|
+
reason: 'material-receipt-missing',
|
|
1973
|
+
owner: 'material',
|
|
1974
|
+
expected: 'producer-owned Standard PBR GPU-driven artifact',
|
|
1975
|
+
})
|
|
1976
|
+
: new GpuDrivenPreparationError('resource-not-ready', {
|
|
1977
|
+
reason: 'material-resource-missing',
|
|
1978
|
+
owner: 'material',
|
|
1979
|
+
expected: 'producer-owned Standard PBR GPU-driven pipeline state',
|
|
1980
|
+
});
|
|
1981
|
+
this.preparationFailure = preparationFailureInspection(failure);
|
|
1982
|
+
return err(failure);
|
|
1983
|
+
}
|
|
1984
|
+
return ok(undefined);
|
|
1985
|
+
}
|
|
1986
|
+
const projectionFingerprint =
|
|
1987
|
+
this.filteredCache?.source === scene.plan &&
|
|
1988
|
+
this.filteredCache.lodProjectionFingerprint === ''
|
|
1989
|
+
? ''
|
|
1990
|
+
: lodProjectionFingerprint(scene.plan, input.camera, scene.slots);
|
|
1143
1991
|
const cachedVisibilityMatches =
|
|
1144
|
-
|
|
1992
|
+
this.filteredCache === undefined
|
|
1145
1993
|
? true
|
|
1146
1994
|
: input.activeEntityRevision !== undefined &&
|
|
1147
1995
|
this.filteredCache.activeEntityRevision !== undefined
|
|
@@ -1151,22 +1999,30 @@ export class GpuDrivenProduction {
|
|
|
1151
1999
|
if (
|
|
1152
2000
|
filtered === undefined ||
|
|
1153
2001
|
this.filteredCache?.source !== scene.plan ||
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
2002
|
+
this.filteredCache?.scene !== scene.scene ||
|
|
2003
|
+
this.filteredCache?.slots !== scene.slots ||
|
|
2004
|
+
this.filteredCache?.worldKeys !== scene.worldKeys ||
|
|
2005
|
+
this.filteredCache.meshResidencyEpoch !== input.meshResidencyEpoch ||
|
|
2006
|
+
this.filteredCache.lodProjectionFingerprint !== projectionFingerprint ||
|
|
2007
|
+
!cachedVisibilityMatches ||
|
|
2008
|
+
this.filteredCache.skinArtifact !== skinArtifact
|
|
1159
2009
|
) {
|
|
1160
2010
|
filtered = filteredPlan(
|
|
1161
2011
|
scene.plan,
|
|
1162
2012
|
scene.slots,
|
|
1163
|
-
input.
|
|
2013
|
+
input.meshBySlot,
|
|
2014
|
+
undefined,
|
|
2015
|
+
skinArtifact,
|
|
2016
|
+
undefined,
|
|
1164
2017
|
input.camera,
|
|
1165
2018
|
input.activeEntityKeys,
|
|
1166
2019
|
scene.worldKeys,
|
|
1167
2020
|
);
|
|
1168
2021
|
this.filteredCache = {
|
|
1169
2022
|
source: scene.plan,
|
|
2023
|
+
scene: scene.scene,
|
|
2024
|
+
slots: scene.slots,
|
|
2025
|
+
worldKeys: scene.worldKeys,
|
|
1170
2026
|
meshResidencyEpoch: input.meshResidencyEpoch,
|
|
1171
2027
|
lodProjectionFingerprint: projectionFingerprint,
|
|
1172
2028
|
...(input.activeEntityRevision === undefined
|
|
@@ -1175,36 +2031,110 @@ export class GpuDrivenProduction {
|
|
|
1175
2031
|
...(input.activeEntityKeys === undefined
|
|
1176
2032
|
? {}
|
|
1177
2033
|
: { activeEntityKeys: input.activeEntityKeys }),
|
|
2034
|
+
...(skinArtifact === undefined ? {} : { skinArtifact }),
|
|
1178
2035
|
value: filtered,
|
|
1179
2036
|
};
|
|
1180
2037
|
this.gpuOwnedSnapshotsMaterialized = scene.slots.length;
|
|
1181
2038
|
this.filteredPlanBuilds = 1;
|
|
1182
2039
|
}
|
|
1183
|
-
this.gpuOwnedEntityCount = filtered.
|
|
1184
|
-
this.sceneRowsRequired = !filtered.topologyInvariantEmpty;
|
|
1185
|
-
// Publish the command count from the current filtered plan on every
|
|
1186
|
-
// prepare call. A cached graph does not call project() again on stable
|
|
1187
|
-
// frames, so deriving this only while compiling the raster pass would
|
|
1188
|
-
// transiently report zero after prepare() reset the per-frame counters.
|
|
2040
|
+
this.gpuOwnedEntityCount = gpuDrawEntityCount(filtered.drawKeys);
|
|
1189
2041
|
this.indirectDrawCount = admittedRasterBatchCount(filtered.plan);
|
|
1190
|
-
if (filtered.batches.length === 0)
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
2042
|
+
if (filtered.batches.length === 0) {
|
|
2043
|
+
this.laneReason = 'unsupported';
|
|
2044
|
+
return ok(undefined);
|
|
2045
|
+
}
|
|
2046
|
+
this.lane = 'gpu';
|
|
2047
|
+
this.laneReason = 'none';
|
|
2048
|
+
this.submissionState = 'pending';
|
|
2049
|
+
const shadowMembership = shadowCasterMembershipSignature(
|
|
2050
|
+
input.shadowCasterDrawKeys,
|
|
2051
|
+
input.shadowCasterMembership,
|
|
2052
|
+
);
|
|
2053
|
+
let shadowFiltered = this.shadowFilteredCache?.value;
|
|
2054
|
+
if (
|
|
2055
|
+
shadowFiltered === undefined ||
|
|
2056
|
+
this.shadowFilteredCache?.source !== scene.plan ||
|
|
2057
|
+
this.shadowFilteredCache.scene !== scene.scene ||
|
|
2058
|
+
this.shadowFilteredCache.slots !== scene.slots ||
|
|
2059
|
+
this.shadowFilteredCache.worldKeys !== scene.worldKeys ||
|
|
2060
|
+
this.shadowFilteredCache.lodProjectionFingerprint !== projectionFingerprint ||
|
|
2061
|
+
this.shadowFilteredCache.meshResidencyEpoch !== input.meshResidencyEpoch ||
|
|
2062
|
+
this.shadowFilteredCache.membership !== shadowMembership ||
|
|
2063
|
+
this.shadowFilteredCache.skinArtifact !== skinArtifact
|
|
2064
|
+
) {
|
|
2065
|
+
const projectedShadowFiltered = filteredPlan(
|
|
2066
|
+
scene.plan,
|
|
2067
|
+
scene.slots,
|
|
2068
|
+
input.meshBySlot,
|
|
2069
|
+
input.shadowCasterDrawKeys,
|
|
2070
|
+
skinArtifact,
|
|
2071
|
+
input.shadowCasterMembership,
|
|
2072
|
+
input.camera,
|
|
2073
|
+
undefined,
|
|
2074
|
+
scene.worldKeys,
|
|
2075
|
+
);
|
|
2076
|
+
shadowFiltered = projectedShadowFiltered;
|
|
2077
|
+
this.shadowFilteredCache = {
|
|
2078
|
+
source: scene.plan,
|
|
2079
|
+
scene: scene.scene,
|
|
2080
|
+
slots: scene.slots,
|
|
2081
|
+
worldKeys: scene.worldKeys,
|
|
2082
|
+
lodProjectionFingerprint: projectionFingerprint,
|
|
2083
|
+
meshResidencyEpoch: input.meshResidencyEpoch,
|
|
2084
|
+
membership: shadowMembership,
|
|
2085
|
+
...(skinArtifact === undefined ? {} : { skinArtifact }),
|
|
2086
|
+
value: shadowFiltered,
|
|
2087
|
+
};
|
|
2088
|
+
}
|
|
2089
|
+
this.shadowSourcePlan = shadowFiltered.plan;
|
|
2090
|
+
this.shadowScene = scene.scene;
|
|
2091
|
+
const shadowCasterKeys =
|
|
2092
|
+
input.shadowCasterMembership === undefined
|
|
2093
|
+
? input.shadowCasterDrawKeys
|
|
2094
|
+
: new Set(
|
|
2095
|
+
input.shadowCasterMembership.map((membership) =>
|
|
2096
|
+
gpuDrivenShadowDrawKey(
|
|
2097
|
+
membership.worldEntity,
|
|
2098
|
+
membership.materialHandle,
|
|
2099
|
+
membership.drawItemIndex,
|
|
2100
|
+
membership.passIndex,
|
|
2101
|
+
),
|
|
2102
|
+
),
|
|
2103
|
+
);
|
|
2104
|
+
this.shadowExpectedDrawKeys = new Set(shadowCasterKeys ?? []);
|
|
2105
|
+
const ownsAllShadowCasters =
|
|
2106
|
+
shadowCasterKeys === undefined ||
|
|
2107
|
+
[...shadowCasterKeys].every((key) => shadowFiltered.shadowDrawKeys.has(key));
|
|
2108
|
+
if (this.shadowViews === undefined) {
|
|
2109
|
+
const created = ShadowViewStatePool.create({
|
|
2110
|
+
device: this.device,
|
|
2111
|
+
shaderModuleFactory: this.shaderModuleFactory,
|
|
2112
|
+
});
|
|
2113
|
+
if (!created.ok) return created;
|
|
2114
|
+
this.shadowViews = created.value;
|
|
2115
|
+
}
|
|
2116
|
+
const projectionSource = {
|
|
2117
|
+
filtered,
|
|
2118
|
+
scene: scene.scene,
|
|
2119
|
+
contentRevision: scene.scene.contentRevision,
|
|
2120
|
+
artifact: standardArtifact,
|
|
2121
|
+
skinArtifact,
|
|
2122
|
+
};
|
|
2123
|
+
const previousSource = this.committedProjectionSource;
|
|
2124
|
+
const reuseProjectionRows =
|
|
2125
|
+
previousSource?.filtered === filtered &&
|
|
2126
|
+
previousSource.scene === scene.scene &&
|
|
2127
|
+
previousSource.contentRevision === scene.scene.contentRevision &&
|
|
2128
|
+
previousSource.artifact === standardArtifact &&
|
|
2129
|
+
previousSource.skinArtifact === skinArtifact;
|
|
2130
|
+
if (!reuseProjectionRows) {
|
|
1194
2131
|
const slotsByPrimitive = new Map(scene.slots.map((slot) => [slot.slot, slot] as const));
|
|
1195
2132
|
const projection = projectStandardPbrScene({
|
|
1196
|
-
artifact:
|
|
2133
|
+
artifact: standardArtifact,
|
|
1197
2134
|
candidates: filtered.batches.flatMap(({ batch }) =>
|
|
1198
2135
|
batch.candidates.flatMap((candidate) => {
|
|
1199
2136
|
const slot = slotsByPrimitive.get(candidate.primitiveIndex);
|
|
1200
|
-
return slot === undefined
|
|
1201
|
-
? []
|
|
1202
|
-
: [
|
|
1203
|
-
{
|
|
1204
|
-
primitiveIndex: candidate.primitiveIndex,
|
|
1205
|
-
world: slot.snapshot.transform.world,
|
|
1206
|
-
},
|
|
1207
|
-
];
|
|
2137
|
+
return slot === undefined ? [] : [sceneCandidate(slot, candidate)];
|
|
1208
2138
|
}),
|
|
1209
2139
|
),
|
|
1210
2140
|
});
|
|
@@ -1218,16 +2148,6 @@ export class GpuDrivenProduction {
|
|
|
1218
2148
|
);
|
|
1219
2149
|
}
|
|
1220
2150
|
this.standardPbrProjection = projection.value;
|
|
1221
|
-
if (input.standardPbrPipelineState !== undefined && this.standardRaster === undefined) {
|
|
1222
|
-
const created = StandardPbrRasterAdapter.create({
|
|
1223
|
-
device: this.device,
|
|
1224
|
-
shaderModuleFactory: this.shaderModuleFactory,
|
|
1225
|
-
pipelineState: input.standardPbrPipelineState,
|
|
1226
|
-
artifact: input.standardPbrArtifact,
|
|
1227
|
-
});
|
|
1228
|
-
if (!created.ok) return created;
|
|
1229
|
-
this.standardRaster = created.value;
|
|
1230
|
-
}
|
|
1231
2151
|
}
|
|
1232
2152
|
if (this.view === undefined) {
|
|
1233
2153
|
const created = GpuDrivenView.create({
|
|
@@ -1237,22 +2157,177 @@ export class GpuDrivenProduction {
|
|
|
1237
2157
|
if (!created.ok) return created;
|
|
1238
2158
|
this.view = created.value;
|
|
1239
2159
|
}
|
|
1240
|
-
if (this.raster === undefined) {
|
|
1241
|
-
const created = GpuDrivenRigidRaster.create({
|
|
1242
|
-
device: this.device,
|
|
1243
|
-
shaderModuleFactory: this.shaderModuleFactory,
|
|
1244
|
-
viewBindGroupLayout: input.viewBindGroupLayout,
|
|
1245
|
-
});
|
|
1246
|
-
if (!created.ok) return created;
|
|
1247
|
-
this.raster = created.value;
|
|
1248
|
-
}
|
|
1249
2160
|
const updated = this.view.update(filtered.plan, scene.scene, activeFrustum(input.camera));
|
|
1250
2161
|
if (!updated.ok) return updated;
|
|
1251
2162
|
this.telemetryPrepared = true;
|
|
1252
2163
|
const view = this.view;
|
|
1253
|
-
const
|
|
2164
|
+
const visibleBuffer = view.visibleBuffer;
|
|
2165
|
+
if (visibleBuffer === undefined) {
|
|
2166
|
+
return err(
|
|
2167
|
+
new RhiError({
|
|
2168
|
+
code: 'internal-error',
|
|
2169
|
+
expected: 'GpuDrivenView visible buffer after update',
|
|
2170
|
+
hint: 'keep the visible-index binding request on the updated GPU scene view',
|
|
2171
|
+
}),
|
|
2172
|
+
);
|
|
2173
|
+
}
|
|
2174
|
+
if (this.standardRaster === undefined) {
|
|
2175
|
+
const created = StandardPbrRasterAdapter.create({
|
|
2176
|
+
device: this.device,
|
|
2177
|
+
pipelineState: standardPipelineState,
|
|
2178
|
+
artifact: standardArtifact,
|
|
2179
|
+
visibleBuffer,
|
|
2180
|
+
});
|
|
2181
|
+
if (!created.ok) return created;
|
|
2182
|
+
this.standardRaster = created.value;
|
|
2183
|
+
} else {
|
|
2184
|
+
const rebound = this.standardRaster.ensureVisibleBuffer(visibleBuffer);
|
|
2185
|
+
if (!rebound.ok) return rebound;
|
|
2186
|
+
}
|
|
1254
2187
|
const standardRaster = this.standardRaster;
|
|
1255
|
-
|
|
2188
|
+
if (standardRaster === undefined) {
|
|
2189
|
+
return err(
|
|
2190
|
+
new RhiError({
|
|
2191
|
+
code: 'internal-error',
|
|
2192
|
+
expected: 'Standard PBR raster adapter after prepared artifact admission',
|
|
2193
|
+
hint: 'keep Standard PBR artifact and PipelineState on one frame identity',
|
|
2194
|
+
}),
|
|
2195
|
+
);
|
|
2196
|
+
}
|
|
2197
|
+
const skinBatch = filtered.batches.find(
|
|
2198
|
+
({ batch }) => batch.candidates[0]?.prepared?.identity.deformation === 'skin',
|
|
2199
|
+
);
|
|
2200
|
+
if (skinBatch !== undefined) {
|
|
2201
|
+
const skinSnapshot = scene.slots.find(
|
|
2202
|
+
(slot) => slot.slot === skinBatch.batch.candidates[0]?.primitiveIndex,
|
|
2203
|
+
)?.snapshot.skin;
|
|
2204
|
+
const skinAllocator = standardPipelineState.skinPaletteAllocator;
|
|
2205
|
+
if (
|
|
2206
|
+
skinArtifact === undefined ||
|
|
2207
|
+
skinSnapshot === undefined ||
|
|
2208
|
+
skinAllocator === null ||
|
|
2209
|
+
skinAllocator === undefined
|
|
2210
|
+
) {
|
|
2211
|
+
return err(
|
|
2212
|
+
new RhiError({
|
|
2213
|
+
code: 'rhi-not-available',
|
|
2214
|
+
expected: 'skinned Standard PBR artifact and persistent palette owner',
|
|
2215
|
+
hint: 'keep missing skin producer resources on the CPU deformation path',
|
|
2216
|
+
}),
|
|
2217
|
+
);
|
|
2218
|
+
}
|
|
2219
|
+
if (this.skinRaster === undefined) {
|
|
2220
|
+
const created = StandardPbrRasterAdapter.create({
|
|
2221
|
+
device: this.device,
|
|
2222
|
+
pipelineState: standardPipelineState,
|
|
2223
|
+
artifact: skinArtifact,
|
|
2224
|
+
visibleBuffer,
|
|
2225
|
+
deformation: 'skin',
|
|
2226
|
+
paletteBuffer: skinSnapshot.buffer,
|
|
2227
|
+
paletteBindingWindowBytes: skinAllocator.bindingWindowBytes,
|
|
2228
|
+
});
|
|
2229
|
+
if (!created.ok) return created;
|
|
2230
|
+
this.skinRaster = created.value;
|
|
2231
|
+
} else {
|
|
2232
|
+
const rebound = this.skinRaster.ensureVisibleBuffer(visibleBuffer);
|
|
2233
|
+
if (!rebound.ok) return rebound;
|
|
2234
|
+
const palette = this.skinRaster.ensurePaletteBuffer(
|
|
2235
|
+
skinSnapshot.buffer,
|
|
2236
|
+
skinAllocator.bindingWindowBytes,
|
|
2237
|
+
);
|
|
2238
|
+
if (!palette.ok) return palette;
|
|
2239
|
+
}
|
|
2240
|
+
if (this.skinPaletteBuffer !== skinSnapshot.buffer) {
|
|
2241
|
+
this.skinPaletteBuffer = skinSnapshot.buffer;
|
|
2242
|
+
this.shadowBatchProjections.clear();
|
|
2243
|
+
this.shadowBatchProjectionSource = undefined;
|
|
2244
|
+
this.shadowViews?.invalidate();
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
const skinRaster = this.skinRaster;
|
|
2248
|
+
const shadowViewPool = this.shadowViews;
|
|
2249
|
+
if (shadowViewPool === undefined) {
|
|
2250
|
+
return err(
|
|
2251
|
+
new RhiError({
|
|
2252
|
+
code: 'internal-error',
|
|
2253
|
+
expected: 'ShadowViewStatePool after GPU-driven preparation',
|
|
2254
|
+
hint: 'publish view-keyed shadow state with the prepared frame',
|
|
2255
|
+
}),
|
|
2256
|
+
);
|
|
2257
|
+
}
|
|
2258
|
+
// A pose can change while conservative bounds, allocation and scene rows
|
|
2259
|
+
// remain unchanged. Read the palette owner, never optional telemetry.
|
|
2260
|
+
const paletteContentRevision = standardPipelineState.skinPaletteAllocator?.contentRevision;
|
|
2261
|
+
if (paletteContentRevision !== this.committedPaletteContentRevision)
|
|
2262
|
+
shadowViewPool.invalidate();
|
|
2263
|
+
const abortProvisionalResources = (): void => {
|
|
2264
|
+
standardRaster.abortProjectionBindings();
|
|
2265
|
+
skinRaster?.abortProjectionBindings();
|
|
2266
|
+
shadowViewPool._abortResourceReplacement();
|
|
2267
|
+
};
|
|
2268
|
+
// The render graph is cached by topology, while transforms are frame
|
|
2269
|
+
// data. Refresh retained scene-row buffers here so a cached graph reads
|
|
2270
|
+
// the current world matrices without forcing a topology rebuild.
|
|
2271
|
+
let slotsByPrimitive: Map<number, PersistentGpuDrivenState['slots'][number]> | undefined;
|
|
2272
|
+
const projectionSlot = (index: number) => {
|
|
2273
|
+
slotsByPrimitive ??= new Map(scene.slots.map((slot) => [slot.slot, slot] as const));
|
|
2274
|
+
return slotsByPrimitive.get(index);
|
|
2275
|
+
};
|
|
2276
|
+
// Start the provisional resource set before any retained row is updated;
|
|
2277
|
+
// an aborted graph can then restore the active rows from its snapshot.
|
|
2278
|
+
standardRaster.stageProjectionKeys(new Set());
|
|
2279
|
+
skinRaster?.stageProjectionKeys(new Set());
|
|
2280
|
+
for (const prepared of filtered.batches) {
|
|
2281
|
+
if (reuseProjectionRows) {
|
|
2282
|
+
const deformation = prepared.batch.candidates[0]?.prepared?.identity.deformation ?? 'rigid';
|
|
2283
|
+
const raster = deformation === 'skin' ? skinRaster : standardRaster;
|
|
2284
|
+
const artifact = deformation === 'skin' ? skinArtifact : standardArtifact;
|
|
2285
|
+
if (raster !== undefined && artifact !== undefined)
|
|
2286
|
+
raster.noteProjectionKey(gpuDrivenProjectionKey(prepared.batch, deformation, artifact));
|
|
2287
|
+
continue;
|
|
2288
|
+
}
|
|
2289
|
+
const admittedCandidates = prepared.batch.candidates.filter(
|
|
2290
|
+
(candidate) =>
|
|
2291
|
+
(candidate as GpuDrivenCandidate & { readonly submitAdmission?: boolean })
|
|
2292
|
+
.submitAdmission !== false,
|
|
2293
|
+
);
|
|
2294
|
+
if (admittedCandidates.length === 0) continue;
|
|
2295
|
+
const deformation = admittedCandidates[0]?.prepared?.identity.deformation ?? 'rigid';
|
|
2296
|
+
const raster = deformation === 'skin' ? skinRaster : standardRaster;
|
|
2297
|
+
const artifact = deformation === 'skin' ? skinArtifact : standardArtifact;
|
|
2298
|
+
if (raster === undefined || artifact === undefined) continue;
|
|
2299
|
+
const projectionKey = gpuDrivenProjectionKey(prepared.batch, deformation, artifact);
|
|
2300
|
+
raster.noteProjectionKey(projectionKey);
|
|
2301
|
+
const candidates = admittedCandidates.flatMap((candidate) => {
|
|
2302
|
+
const slot = projectionSlot(candidate.primitiveIndex);
|
|
2303
|
+
return slot === undefined ? [] : [sceneCandidate(slot, candidate)];
|
|
2304
|
+
});
|
|
2305
|
+
const projection = projectStandardPbrScene({ artifact, candidates });
|
|
2306
|
+
if (!projection.ok) {
|
|
2307
|
+
abortProvisionalResources();
|
|
2308
|
+
return err(
|
|
2309
|
+
new RhiError({
|
|
2310
|
+
code: 'shader-compile-failed',
|
|
2311
|
+
expected: 'receipt-derived Standard PBR scene projection',
|
|
2312
|
+
hint: 'keep the retained projection rows on the current material receipt',
|
|
2313
|
+
}),
|
|
2314
|
+
);
|
|
2315
|
+
}
|
|
2316
|
+
const refreshed = raster.refreshProjectionRows(projectionKey, projection.value.rows);
|
|
2317
|
+
if (!refreshed.ok) {
|
|
2318
|
+
abortProvisionalResources();
|
|
2319
|
+
return refreshed;
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
// Bindings created by the graph projection below are provisional until
|
|
2323
|
+
// the frame submits. A successful commit promotes exactly these keys and
|
|
2324
|
+
// fence-retires any superseded batch generation; an abort keeps the last
|
|
2325
|
+
// known-good active set and drops only staged bindings. Shadow view update
|
|
2326
|
+
// adds its keyed projections to the same staged set.
|
|
2327
|
+
if (this.shadowBatchProjectionSource !== shadowFiltered.plan) {
|
|
2328
|
+
this.shadowBatchProjections.clear();
|
|
2329
|
+
this.shadowBatchProjectionSource = shadowFiltered.plan;
|
|
2330
|
+
}
|
|
1256
2331
|
const sceneIdentity = this.sceneIdentity(scene.scene);
|
|
1257
2332
|
const viewInspection = view.inspect();
|
|
1258
2333
|
let signature = this.signatureCache?.value;
|
|
@@ -1272,13 +2347,24 @@ export class GpuDrivenProduction {
|
|
|
1272
2347
|
batches: filtered.batches.map(({ batch, mesh }) => [
|
|
1273
2348
|
batch.batchId,
|
|
1274
2349
|
batch.generation,
|
|
1275
|
-
batch.visibleCapacity,
|
|
1276
2350
|
batch.visibleBase,
|
|
2351
|
+
batch.visibleCapacity,
|
|
1277
2352
|
batch.candidates.map((candidate) => [
|
|
1278
2353
|
candidate.primitiveIndex,
|
|
1279
2354
|
candidate.generation,
|
|
1280
2355
|
candidate.instanceOrdinal,
|
|
1281
2356
|
]),
|
|
2357
|
+
batch.candidates
|
|
2358
|
+
.filter(
|
|
2359
|
+
(candidate) =>
|
|
2360
|
+
(candidate as GpuDrivenCandidate & { readonly submitAdmission?: boolean })
|
|
2361
|
+
.submitAdmission !== false,
|
|
2362
|
+
)
|
|
2363
|
+
.map((candidate) => [
|
|
2364
|
+
candidate.primitiveIndex,
|
|
2365
|
+
candidate.generation,
|
|
2366
|
+
candidate.instanceOrdinal,
|
|
2367
|
+
]),
|
|
1282
2368
|
mesh.vboBytes,
|
|
1283
2369
|
mesh.iboBytes,
|
|
1284
2370
|
mesh.indexFormat,
|
|
@@ -1286,7 +2372,6 @@ export class GpuDrivenProduction {
|
|
|
1286
2372
|
]),
|
|
1287
2373
|
capacities: [
|
|
1288
2374
|
viewInspection.candidateCapacity,
|
|
1289
|
-
viewInspection.visibleBufferCapacity,
|
|
1290
2375
|
viewInspection.batchCapacity,
|
|
1291
2376
|
viewInspection.indirectCapacity,
|
|
1292
2377
|
],
|
|
@@ -1299,31 +2384,96 @@ export class GpuDrivenProduction {
|
|
|
1299
2384
|
value: signature,
|
|
1300
2385
|
};
|
|
1301
2386
|
}
|
|
2387
|
+
const shadowTopologySignature = () =>
|
|
2388
|
+
this.shadowViews
|
|
2389
|
+
?.inspect()
|
|
2390
|
+
.map(
|
|
2391
|
+
(entry) =>
|
|
2392
|
+
`${shadowViewIdentityKey(entry.identity)}:${entry.cache}:${entry.generation}:${entry.sourceRevision}:${entry.resourceGeneration}`,
|
|
2393
|
+
)
|
|
2394
|
+
.join('|') ?? '';
|
|
2395
|
+
const production = this;
|
|
1302
2396
|
return ok({
|
|
1303
|
-
topologySignature
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
: filtered.entityKeys.size === input.activeEntityKeys.size,
|
|
2397
|
+
get topologySignature() {
|
|
2398
|
+
const shadow = shadowTopologySignature();
|
|
2399
|
+
return shadow.length === 0 ? signature : `${signature}|shadow=${shadow}`;
|
|
2400
|
+
},
|
|
2401
|
+
ownsAllDrawItems: filtered.ownsAllDrawItems,
|
|
1309
2402
|
...(input.occlusion === undefined ? {} : { occlusion: input.occlusion }),
|
|
1310
|
-
|
|
2403
|
+
ownsAllShadowCasters,
|
|
2404
|
+
drawKeys: filtered.drawKeys,
|
|
2405
|
+
shadowDrawKeys: shadowFiltered.shadowDrawKeys,
|
|
2406
|
+
get shadowDrawKeysByView() {
|
|
2407
|
+
return new Map(production.shadowDrawKeysByView);
|
|
2408
|
+
},
|
|
2409
|
+
standardPbrFrameResources: {
|
|
2410
|
+
materialBindGroups: [],
|
|
2411
|
+
materialSlotIndicesByEntity: new Map(),
|
|
2412
|
+
instancesBindGroup: standardRaster.instancesFrameGroup,
|
|
2413
|
+
materialStride: 512,
|
|
2414
|
+
sceneMaterialBuffer: scene.scene.materialBuffer,
|
|
2415
|
+
},
|
|
2416
|
+
_commitGpuResourceReplacement: () => {
|
|
2417
|
+
view._commitResourceReplacement();
|
|
2418
|
+
},
|
|
2419
|
+
_commitResourceReplacement: () => {
|
|
2420
|
+
view._commitResourceReplacement();
|
|
2421
|
+
shadowViewPool._commitResourceReplacement();
|
|
2422
|
+
standardRaster.commitProjectionBindings();
|
|
2423
|
+
skinRaster?.commitProjectionBindings();
|
|
2424
|
+
this.committedProjectionSource = projectionSource;
|
|
2425
|
+
this.committedPaletteContentRevision = paletteContentRevision;
|
|
2426
|
+
this.submissionState = 'submitted';
|
|
2427
|
+
this.lastKnownGoodGeneration = view.inspect().resourceGeneration;
|
|
2428
|
+
},
|
|
2429
|
+
_abortResourceReplacement: () => {
|
|
2430
|
+
shadowViewPool._abortResourceReplacement();
|
|
2431
|
+
standardRaster.abortProjectionBindings();
|
|
2432
|
+
skinRaster?.abortProjectionBindings();
|
|
2433
|
+
this.committedProjectionSource = undefined;
|
|
2434
|
+
this.submissionState = 'aborted';
|
|
2435
|
+
this.retryCount += 1;
|
|
2436
|
+
},
|
|
2437
|
+
projectShadow: (graph, identity) => {
|
|
2438
|
+
if (!shadowViewPool.isActive(identity) || shadowViewPool.submission(identity) === undefined)
|
|
2439
|
+
return ok(undefined);
|
|
2440
|
+
return shadowViewPool.project(graph, identity);
|
|
2441
|
+
},
|
|
2442
|
+
updateShadowViews: (views) =>
|
|
2443
|
+
shadowFiltered.plan.batches.length > 0
|
|
2444
|
+
? this.updateShadowViews(
|
|
2445
|
+
views,
|
|
2446
|
+
standardArtifact,
|
|
2447
|
+
skinArtifact,
|
|
2448
|
+
shadowFiltered,
|
|
2449
|
+
scene,
|
|
2450
|
+
input.shadowCasterDrawKeys,
|
|
2451
|
+
input.shadowCasterMembership,
|
|
2452
|
+
)
|
|
2453
|
+
: this.disableShadowViews(),
|
|
2454
|
+
shadowViewPool,
|
|
2455
|
+
get shadowBatchProjections() {
|
|
2456
|
+
return new Map(
|
|
2457
|
+
[...production.shadowBatchProjections].map(
|
|
2458
|
+
([key, projections]) => [key, new Map(projections)] as const,
|
|
2459
|
+
),
|
|
2460
|
+
);
|
|
2461
|
+
},
|
|
1311
2462
|
project: (graph, format, sampleCount) => {
|
|
1312
2463
|
this.indirectDrawCount = admittedRasterBatchCount(filtered.plan);
|
|
1313
2464
|
const outputs = view.addPasses(graph);
|
|
1314
2465
|
if (!outputs.ok) return outputs;
|
|
1315
|
-
if (
|
|
1316
|
-
usesStandardPbrRaster &&
|
|
1317
|
-
standardRaster !== undefined &&
|
|
1318
|
-
standardArtifact !== undefined
|
|
1319
|
-
) {
|
|
2466
|
+
if (standardRaster !== undefined && standardArtifact !== undefined) {
|
|
1320
2467
|
const standardProjected: Array<{
|
|
1321
2468
|
readonly batch: GpuDrivenBatch;
|
|
1322
2469
|
readonly mesh: MeshGpuHandles;
|
|
2470
|
+
readonly materialEntityKey: number;
|
|
1323
2471
|
readonly vertex: GraphBuffer;
|
|
1324
2472
|
readonly index?: GraphBuffer;
|
|
1325
2473
|
readonly pipeline: RenderPipeline;
|
|
1326
2474
|
readonly meshBindGroup: BindGroup;
|
|
2475
|
+
readonly raster: StandardPbrRasterAdapter;
|
|
2476
|
+
readonly deformation: 'rigid' | 'skin';
|
|
1327
2477
|
}> = [];
|
|
1328
2478
|
const standardAccesses: GraphAccess[] = [
|
|
1329
2479
|
{ resource: outputs.value.primitive, usage: 'storage-read' },
|
|
@@ -1333,25 +2483,41 @@ export class GpuDrivenProduction {
|
|
|
1333
2483
|
{ resource: outputs.value.transform, usage: 'storage-read' },
|
|
1334
2484
|
{ resource: outputs.value.indirect, usage: 'indirect-read' },
|
|
1335
2485
|
];
|
|
1336
|
-
const slotsByPrimitive = new Map(scene.slots.map((slot) => [slot.slot, slot] as const));
|
|
1337
2486
|
const meshBuffers = new Map<
|
|
1338
|
-
|
|
2487
|
+
MeshGpuHandles,
|
|
1339
2488
|
{ readonly vertex: GraphBuffer; readonly index?: GraphBuffer }
|
|
1340
2489
|
>();
|
|
1341
2490
|
for (const prepared of filtered.batches) {
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
2491
|
+
if (prepared.batch.visibleCapacity === 0) continue;
|
|
2492
|
+
const admittedCandidates = prepared.batch.candidates.filter(
|
|
2493
|
+
(candidate) =>
|
|
2494
|
+
(candidate as GpuDrivenCandidate & { readonly submitAdmission?: boolean })
|
|
2495
|
+
.submitAdmission !== false,
|
|
2496
|
+
);
|
|
2497
|
+
if (admittedCandidates.length === 0) continue;
|
|
2498
|
+
const deformation = admittedCandidates[0]?.prepared?.identity.deformation ?? 'rigid';
|
|
2499
|
+
const raster = deformation === 'skin' ? skinRaster : standardRaster;
|
|
2500
|
+
const artifact = deformation === 'skin' ? skinArtifact : standardArtifact;
|
|
2501
|
+
if (raster === undefined || artifact === undefined) {
|
|
2502
|
+
return err(
|
|
2503
|
+
new RhiError({
|
|
2504
|
+
code: 'rhi-not-available',
|
|
2505
|
+
expected: `${deformation === 'skin' ? 'skinned ' : ''}Standard PBR GPU-driven raster owner`,
|
|
2506
|
+
hint: 'keep an unsupported deformation on the CPU fallback lane',
|
|
2507
|
+
}),
|
|
2508
|
+
);
|
|
2509
|
+
}
|
|
2510
|
+
// The visible stream is compacted after the persistent visibility
|
|
2511
|
+
// facet applies its active-entity admission. Project the mesh rows
|
|
2512
|
+
// from that same admitted sequence so visible.x remains the
|
|
2513
|
+
// batch-local row identity even when a suppressed candidate sits
|
|
2514
|
+
// between two submitted candidates.
|
|
2515
|
+
const candidates = admittedCandidates.flatMap((candidate) => {
|
|
2516
|
+
const slot = projectionSlot(candidate.primitiveIndex);
|
|
2517
|
+
return slot === undefined ? [] : [sceneCandidate(slot, candidate)];
|
|
1352
2518
|
});
|
|
1353
2519
|
const projection = projectStandardPbrScene({
|
|
1354
|
-
artifact
|
|
2520
|
+
artifact,
|
|
1355
2521
|
candidates,
|
|
1356
2522
|
});
|
|
1357
2523
|
if (!projection.ok) {
|
|
@@ -1363,12 +2529,26 @@ export class GpuDrivenProduction {
|
|
|
1363
2529
|
}),
|
|
1364
2530
|
);
|
|
1365
2531
|
}
|
|
1366
|
-
const meshBindGroup =
|
|
2532
|
+
const meshBindGroup = raster.projectionBindGroup(
|
|
2533
|
+
projection.value.rows,
|
|
2534
|
+
gpuDrivenProjectionKey(prepared.batch, deformation, artifact),
|
|
2535
|
+
prepared.batch.candidates.length,
|
|
2536
|
+
);
|
|
1367
2537
|
if (!meshBindGroup.ok) return meshBindGroup;
|
|
1368
|
-
|
|
2538
|
+
const materialSlot = projectionSlot(admittedCandidates[0]?.primitiveIndex ?? -1);
|
|
2539
|
+
if (materialSlot === undefined) {
|
|
2540
|
+
return err(
|
|
2541
|
+
new RhiError({
|
|
2542
|
+
code: 'internal-error',
|
|
2543
|
+
expected: 'batch representative entity for global material slot projection',
|
|
2544
|
+
hint: 'preserve the retained scene identity when projecting the GPU batch',
|
|
2545
|
+
}),
|
|
2546
|
+
);
|
|
2547
|
+
}
|
|
2548
|
+
let buffers = meshBuffers.get(prepared.mesh);
|
|
1369
2549
|
if (buffers === undefined) {
|
|
1370
2550
|
const vertex = graph.importBuffer(
|
|
1371
|
-
`gpu-driven.standard.${prepared.batch.key.assetHandle}.vertex`,
|
|
2551
|
+
`gpu-driven.standard.${prepared.worldKey}.${prepared.batch.key.assetHandle}.vertex`,
|
|
1372
2552
|
{
|
|
1373
2553
|
size: prepared.mesh.vboBytes,
|
|
1374
2554
|
usage: GPU_BUFFER_USAGE_VERTEX | GPU_BUFFER_USAGE_COPY_DST,
|
|
@@ -1380,7 +2560,7 @@ export class GpuDrivenProduction {
|
|
|
1380
2560
|
if (prepared.mesh.indexBuffer !== null) {
|
|
1381
2561
|
const indexHandle = prepared.mesh.indexBuffer;
|
|
1382
2562
|
const imported = graph.importBuffer(
|
|
1383
|
-
`gpu-driven.standard.${prepared.batch.key.assetHandle}.index`,
|
|
2563
|
+
`gpu-driven.standard.${prepared.worldKey}.${prepared.batch.key.assetHandle}.index`,
|
|
1384
2564
|
{
|
|
1385
2565
|
size: prepared.mesh.iboBytes,
|
|
1386
2566
|
usage: GPU_BUFFER_USAGE_INDEX | GPU_BUFFER_USAGE_COPY_DST,
|
|
@@ -1391,16 +2571,17 @@ export class GpuDrivenProduction {
|
|
|
1391
2571
|
index = imported.value;
|
|
1392
2572
|
}
|
|
1393
2573
|
buffers = { vertex: vertex.value, ...(index === undefined ? {} : { index }) };
|
|
1394
|
-
meshBuffers.set(prepared.
|
|
2574
|
+
meshBuffers.set(prepared.mesh, buffers);
|
|
1395
2575
|
standardAccesses.push({ resource: buffers.vertex, usage: 'vertex-read' });
|
|
1396
2576
|
if (buffers.index !== undefined) {
|
|
1397
2577
|
standardAccesses.push({ resource: buffers.index, usage: 'index-read' });
|
|
1398
2578
|
}
|
|
1399
2579
|
}
|
|
1400
|
-
const pipeline =
|
|
2580
|
+
const pipeline = raster.pipeline(
|
|
1401
2581
|
format,
|
|
1402
2582
|
sampleCount,
|
|
1403
2583
|
prepared.mesh.layoutProjection,
|
|
2584
|
+
standardPipelineState.standardPbrShaderUvSetCount,
|
|
1404
2585
|
prepared.batch.key.topology,
|
|
1405
2586
|
prepared.batch.key.drawKind === 'indexed' &&
|
|
1406
2587
|
(prepared.batch.key.topology === 'line-strip' ||
|
|
@@ -1413,31 +2594,89 @@ export class GpuDrivenProduction {
|
|
|
1413
2594
|
standardProjected.push({
|
|
1414
2595
|
batch: prepared.batch,
|
|
1415
2596
|
mesh: prepared.mesh,
|
|
2597
|
+
materialEntityKey: worldEntityKey(
|
|
2598
|
+
materialSlot.snapshot.worldId,
|
|
2599
|
+
materialSlot.snapshot.entityKey,
|
|
2600
|
+
),
|
|
1416
2601
|
...buffers,
|
|
1417
2602
|
pipeline: pipeline.value,
|
|
1418
2603
|
meshBindGroup: meshBindGroup.value,
|
|
2604
|
+
raster,
|
|
2605
|
+
deformation,
|
|
1419
2606
|
});
|
|
1420
2607
|
}
|
|
1421
|
-
|
|
2608
|
+
if (
|
|
2609
|
+
standardProjected.length === 0 ||
|
|
2610
|
+
standardProjected.length !== filtered.batches.length
|
|
2611
|
+
) {
|
|
2612
|
+
return err(
|
|
2613
|
+
new RhiError({
|
|
2614
|
+
code: 'internal-error',
|
|
2615
|
+
expected: 'one actual Standard PBR raster dispatch for each eligible batch',
|
|
2616
|
+
hint: 'repair the receipt-backed batch projection before encoding the frame',
|
|
2617
|
+
}),
|
|
2618
|
+
);
|
|
2619
|
+
}
|
|
2620
|
+
const rasterDispatches = Object.freeze(standardProjected);
|
|
1422
2621
|
return ok({
|
|
1423
2622
|
accesses: standardAccesses,
|
|
1424
|
-
encode: (
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
2623
|
+
encode: (
|
|
2624
|
+
viewBindGroup,
|
|
2625
|
+
pass,
|
|
2626
|
+
resources: GraphResourceResolver,
|
|
2627
|
+
frameResources: GpuDrivenStandardPbrFrameResources,
|
|
2628
|
+
) => {
|
|
2629
|
+
// The shared Standard view BGL carries two dynamic buffers:
|
|
2630
|
+
// the frame view UBO and the per-frame shadow/light table. GPU
|
|
2631
|
+
// indirect draws use the same zero offsets as direct draws;
|
|
2632
|
+
// supplying only one offset makes Dawn reject the command
|
|
2633
|
+
// buffer before the indirect work can reach the rasterizer.
|
|
1429
2634
|
pass.setBindGroup(0, viewBindGroup, [0, 0]);
|
|
1430
|
-
pass.setBindGroup(1, materialBindGroup, [0]);
|
|
1431
|
-
pass.setBindGroup(3, instancesBindGroup);
|
|
1432
2635
|
let currentPipeline: RenderPipeline | undefined;
|
|
1433
2636
|
let currentVertex: GraphBuffer | undefined;
|
|
1434
2637
|
let currentIndex: GraphBuffer | undefined;
|
|
1435
|
-
for (const batch of
|
|
2638
|
+
for (const batch of rasterDispatches) {
|
|
1436
2639
|
if (currentPipeline !== batch.pipeline) {
|
|
1437
2640
|
pass.setPipeline(batch.pipeline);
|
|
1438
2641
|
currentPipeline = batch.pipeline;
|
|
1439
2642
|
}
|
|
1440
|
-
|
|
2643
|
+
const globalMaterialSlots = frameResources.materialSlotIndicesByEntity.get(
|
|
2644
|
+
batch.materialEntityKey,
|
|
2645
|
+
);
|
|
2646
|
+
const globalMaterialSlot =
|
|
2647
|
+
globalMaterialSlots?.[batch.batch.key.materialSlot] ??
|
|
2648
|
+
(frameResources.materialSlotIndicesByEntity.size === 0
|
|
2649
|
+
? batch.batch.key.materialSlot
|
|
2650
|
+
: undefined);
|
|
2651
|
+
if (globalMaterialSlot === undefined) {
|
|
2652
|
+
throw new RhiError({
|
|
2653
|
+
code: 'rhi-not-available',
|
|
2654
|
+
expected: `frame-global material slot for entity ${batch.materialEntityKey}`,
|
|
2655
|
+
hint: 'populate materialSlotIndicesByEntity from the main-pass material producer',
|
|
2656
|
+
});
|
|
2657
|
+
}
|
|
2658
|
+
const materialBindGroup = frameResources.materialBindGroups[globalMaterialSlot];
|
|
2659
|
+
if (materialBindGroup === undefined) {
|
|
2660
|
+
throw new RhiError({
|
|
2661
|
+
code: 'rhi-not-available',
|
|
2662
|
+
expected: `material bind group for global slot ${globalMaterialSlot}`,
|
|
2663
|
+
hint: 'keep GPU-driven material selection on the main-pass producer contract',
|
|
2664
|
+
});
|
|
2665
|
+
}
|
|
2666
|
+
pass.setBindGroup(1, materialBindGroup, [
|
|
2667
|
+
globalMaterialSlot * frameResources.materialStride,
|
|
2668
|
+
]);
|
|
2669
|
+
pass.setBindGroup(
|
|
2670
|
+
2,
|
|
2671
|
+
batch.meshBindGroup,
|
|
2672
|
+
batch.deformation === 'skin' ? [0, 0, 0] : [0],
|
|
2673
|
+
);
|
|
2674
|
+
const visibleWindow = batch.raster.visibleWindowBindGroup(
|
|
2675
|
+
batch.batch.visibleBase,
|
|
2676
|
+
batch.batch.visibleCapacity,
|
|
2677
|
+
);
|
|
2678
|
+
if (!visibleWindow.ok) throw visibleWindow.error;
|
|
2679
|
+
pass.setBindGroup(3, visibleWindow.value);
|
|
1441
2680
|
if (currentVertex !== batch.vertex) {
|
|
1442
2681
|
const vertex = resources.buffer(batch.vertex);
|
|
1443
2682
|
if (!vertex.ok) throw vertex.error;
|
|
@@ -1457,129 +2696,222 @@ export class GpuDrivenProduction {
|
|
|
1457
2696
|
} else {
|
|
1458
2697
|
pass.drawIndirect(indirect.value, batch.batch.indirectOffset);
|
|
1459
2698
|
}
|
|
2699
|
+
this.encodedIndirectDraws += 1;
|
|
1460
2700
|
}
|
|
1461
2701
|
},
|
|
1462
2702
|
});
|
|
1463
2703
|
}
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
2704
|
+
return err(
|
|
2705
|
+
new RhiError({
|
|
2706
|
+
code: 'internal-error',
|
|
2707
|
+
expected: 'Standard PBR raster projection branch',
|
|
2708
|
+
hint: 'preserve the artifact-backed Standard PBR branch as the only raster owner',
|
|
2709
|
+
}),
|
|
2710
|
+
);
|
|
2711
|
+
},
|
|
2712
|
+
});
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
updateShadowViews(
|
|
2716
|
+
views: readonly Omit<ShadowViewUpdateInput, 'sourcePlan' | 'scene'>[],
|
|
2717
|
+
artifact: MaterialShaderArtifact,
|
|
2718
|
+
skinArtifact: MaterialShaderArtifact | undefined,
|
|
2719
|
+
filtered: FilteredProductionPlan,
|
|
2720
|
+
scene: PersistentGpuDrivenState,
|
|
2721
|
+
shadowCasterDrawKeys: ReadonlySet<string> | undefined,
|
|
2722
|
+
shadowCasterMembership: readonly ShadowCasterMembership[] | undefined,
|
|
2723
|
+
): Result<void, RhiError> {
|
|
2724
|
+
if (
|
|
2725
|
+
this.shadowViews === undefined ||
|
|
2726
|
+
this.shadowSourcePlan === undefined ||
|
|
2727
|
+
this.shadowScene === undefined
|
|
2728
|
+
) {
|
|
2729
|
+
return ok(undefined);
|
|
2730
|
+
}
|
|
2731
|
+
const nextProjections = new Map<string, ReadonlyMap<number, GpuDrivenShadowBatchProjection>>();
|
|
2732
|
+
const nextShadowDrawKeysByView = new Map<string, ReadonlySet<string>>();
|
|
2733
|
+
const standardRaster = this.standardRaster;
|
|
2734
|
+
if (standardRaster === undefined) {
|
|
2735
|
+
return err(
|
|
2736
|
+
new RhiError({
|
|
2737
|
+
code: 'internal-error',
|
|
2738
|
+
expected: 'Standard PBR raster adapter for shadow projection',
|
|
2739
|
+
hint: 'prepare the artifact-backed raster owner before updating shadow views',
|
|
2740
|
+
}),
|
|
2741
|
+
);
|
|
2742
|
+
}
|
|
2743
|
+
let slotsByPrimitive: Map<number, PersistentGpuDrivenState['slots'][number]> | undefined;
|
|
2744
|
+
const preparedByBatch = new Map(filtered.batches.map((entry) => [entry.batch.batchId, entry]));
|
|
2745
|
+
for (const input of views) {
|
|
2746
|
+
const updated = this.shadowViews.update({
|
|
2747
|
+
...input,
|
|
2748
|
+
sourcePlan: this.shadowSourcePlan,
|
|
2749
|
+
scene: this.shadowScene,
|
|
2750
|
+
});
|
|
2751
|
+
if (!updated.ok) return updated;
|
|
2752
|
+
const identityKey = shadowViewIdentityKey(input.identity);
|
|
2753
|
+
const previousProjections = this.shadowBatchProjections.get(identityKey);
|
|
2754
|
+
const previousClaims = this.shadowDrawKeysByView.get(identityKey);
|
|
2755
|
+
if (
|
|
2756
|
+
updated.value.cache === 'hit' &&
|
|
2757
|
+
previousProjections !== undefined &&
|
|
2758
|
+
previousClaims !== undefined
|
|
2759
|
+
) {
|
|
2760
|
+
nextShadowDrawKeysByView.set(identityKey, previousClaims);
|
|
2761
|
+
for (const batch of updated.value.plan.batches) {
|
|
2762
|
+
const deformation = batch.candidates[0]?.prepared?.identity.deformation ?? 'rigid';
|
|
2763
|
+
const batchArtifact = deformation === 'skin' ? skinArtifact : artifact;
|
|
2764
|
+
const raster = deformation === 'skin' ? this.skinRaster : standardRaster;
|
|
2765
|
+
if (batchArtifact !== undefined && raster !== undefined) {
|
|
2766
|
+
raster.noteProjectionKey(
|
|
2767
|
+
gpuDrivenShadowProjectionKey(identityKey, batch, deformation, batchArtifact),
|
|
1491
2768
|
);
|
|
1492
|
-
if (!vertex.ok) return vertex;
|
|
1493
|
-
let index: GraphBuffer | undefined;
|
|
1494
|
-
const indexHandle = prepared.mesh.indexBuffer;
|
|
1495
|
-
if (indexHandle !== null) {
|
|
1496
|
-
const imported = graph.importBuffer(
|
|
1497
|
-
`gpu-driven.mesh.${prepared.batch.key.assetHandle}.index`,
|
|
1498
|
-
{
|
|
1499
|
-
size: prepared.mesh.iboBytes,
|
|
1500
|
-
usage: GPU_BUFFER_USAGE_INDEX | GPU_BUFFER_USAGE_COPY_DST,
|
|
1501
|
-
},
|
|
1502
|
-
() => indexHandle.handle,
|
|
1503
|
-
);
|
|
1504
|
-
if (!imported.ok) return imported;
|
|
1505
|
-
index = imported.value;
|
|
1506
|
-
}
|
|
1507
|
-
buffers = { vertex: vertex.value, ...(index === undefined ? {} : { index }) };
|
|
1508
|
-
meshBuffers.set(prepared.batch.key.assetHandle, buffers);
|
|
1509
|
-
accesses.push({ resource: buffers.vertex, usage: 'vertex-read' });
|
|
1510
|
-
if (buffers.index !== undefined) {
|
|
1511
|
-
accesses.push({ resource: buffers.index, usage: 'index-read' });
|
|
1512
|
-
}
|
|
1513
2769
|
}
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
2770
|
+
}
|
|
2771
|
+
nextProjections.set(identityKey, previousProjections);
|
|
2772
|
+
continue;
|
|
2773
|
+
}
|
|
2774
|
+
slotsByPrimitive ??= new Map(scene.slots.map((slot) => [slot.slot, slot] as const));
|
|
2775
|
+
const viewClaimedKeys = new Set<string>();
|
|
2776
|
+
for (const batch of updated.value.plan.batches) {
|
|
2777
|
+
for (const candidate of batch.candidates) {
|
|
2778
|
+
const slot = slotsByPrimitive?.get(candidate.primitiveIndex);
|
|
2779
|
+
if (slot === undefined) continue;
|
|
2780
|
+
if (
|
|
2781
|
+
!shadowMembershipIsGpuCompatible(
|
|
2782
|
+
slot,
|
|
2783
|
+
candidate,
|
|
2784
|
+
shadowCasterDrawKeys,
|
|
2785
|
+
shadowCasterMembership,
|
|
2786
|
+
)
|
|
2787
|
+
) {
|
|
2788
|
+
continue;
|
|
2789
|
+
}
|
|
2790
|
+
for (const key of shadowKeysForCandidate(
|
|
2791
|
+
slot,
|
|
2792
|
+
candidate,
|
|
2793
|
+
shadowCasterDrawKeys,
|
|
2794
|
+
shadowCasterMembership,
|
|
2795
|
+
)) {
|
|
2796
|
+
if (filtered.shadowDrawKeys.has(key)) viewClaimedKeys.add(key);
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
nextShadowDrawKeysByView.set(identityKey, Object.freeze(viewClaimedKeys));
|
|
2801
|
+
const submission = this.shadowViews.submission(input.identity);
|
|
2802
|
+
const visibleBuffer = submission?.view.visibleBuffer;
|
|
2803
|
+
if (submission === undefined || visibleBuffer === undefined) {
|
|
2804
|
+
return err(
|
|
2805
|
+
new RhiError({
|
|
2806
|
+
code: 'internal-error',
|
|
2807
|
+
expected: 'shadow view visible buffer after update',
|
|
2808
|
+
hint: 'publish the per-view visible window before recording indirect shadow draws',
|
|
2809
|
+
}),
|
|
2810
|
+
);
|
|
2811
|
+
}
|
|
2812
|
+
const projections = new Map<number, GpuDrivenShadowBatchProjection>();
|
|
2813
|
+
for (const batch of submission.plan.batches) {
|
|
2814
|
+
const prepared = preparedByBatch.get(batch.batchId);
|
|
2815
|
+
if (prepared === undefined) {
|
|
2816
|
+
return err(
|
|
2817
|
+
new RhiError({
|
|
2818
|
+
code: 'internal-error',
|
|
2819
|
+
expected: `prepared shadow mesh batch ${batch.batchId}`,
|
|
2820
|
+
hint: 'keep filtered shadow topology and per-view projection on one frame identity',
|
|
2821
|
+
}),
|
|
1525
2822
|
);
|
|
1526
|
-
if (!pipeline.ok) return pipeline;
|
|
1527
|
-
const bindings = raster.bindGroup(scene.scene, view, prepared.batch);
|
|
1528
|
-
if (!bindings.ok) return bindings;
|
|
1529
|
-
this.batchBindGroupCreates += 1;
|
|
1530
|
-
projected.push({
|
|
1531
|
-
...prepared,
|
|
1532
|
-
...buffers,
|
|
1533
|
-
pipeline: pipeline.value,
|
|
1534
|
-
bindings: bindings.value,
|
|
1535
|
-
});
|
|
1536
2823
|
}
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
const vertex = resources.buffer(batch.vertex);
|
|
1558
|
-
if (!vertex.ok) throw vertex.error;
|
|
1559
|
-
pass.setVertexBuffer(0, vertex.value);
|
|
1560
|
-
currentVertex = batch.vertex;
|
|
1561
|
-
}
|
|
1562
|
-
if (batch.index !== undefined && currentIndex !== batch.index) {
|
|
1563
|
-
const index = resources.buffer(batch.index);
|
|
1564
|
-
if (!index.ok) throw index.error;
|
|
1565
|
-
pass.setIndexBuffer(index.value, batch.mesh.indexFormat);
|
|
1566
|
-
currentIndex = batch.index;
|
|
1567
|
-
}
|
|
1568
|
-
const indirect = resources.buffer(outputs.value.indirect);
|
|
1569
|
-
if (!indirect.ok) throw indirect.error;
|
|
1570
|
-
const draw = (): void => {
|
|
1571
|
-
if (batch.batch.key.drawKind === 'indexed') {
|
|
1572
|
-
pass.drawIndexedIndirect(indirect.value, batch.batch.indirectOffset);
|
|
1573
|
-
} else {
|
|
1574
|
-
pass.drawIndirect(indirect.value, batch.batch.indirectOffset);
|
|
1575
|
-
}
|
|
1576
|
-
};
|
|
1577
|
-
draw();
|
|
1578
|
-
}
|
|
1579
|
-
},
|
|
2824
|
+
const deformation = batch.candidates[0]?.prepared?.identity.deformation ?? 'rigid';
|
|
2825
|
+
const raster = deformation === 'skin' ? this.skinRaster : standardRaster;
|
|
2826
|
+
const batchArtifact = deformation === 'skin' ? skinArtifact : artifact;
|
|
2827
|
+
if (raster === undefined || batchArtifact === undefined) {
|
|
2828
|
+
return err(
|
|
2829
|
+
new RhiError({
|
|
2830
|
+
code: 'rhi-not-available',
|
|
2831
|
+
expected: `${deformation === 'skin' ? 'skinned ' : ''}Standard PBR shadow projection owner`,
|
|
2832
|
+
hint: 'keep a shadow batch on the CPU caster path until its material receipt is ready',
|
|
2833
|
+
}),
|
|
2834
|
+
);
|
|
2835
|
+
}
|
|
2836
|
+
const candidates = batch.candidates.flatMap((candidate) => {
|
|
2837
|
+
const slot = slotsByPrimitive?.get(candidate.primitiveIndex);
|
|
2838
|
+
if (slot === undefined) return [];
|
|
2839
|
+
const materialIndex = scene.scene.materialIndexForSlot(
|
|
2840
|
+
candidate.primitiveIndex,
|
|
2841
|
+
batch.key.materialSlot,
|
|
2842
|
+
);
|
|
2843
|
+
return [sceneCandidate(slot, candidate, materialIndex)];
|
|
1580
2844
|
});
|
|
1581
|
-
|
|
1582
|
-
|
|
2845
|
+
const projected = projectStandardPbrScene({ artifact: batchArtifact, candidates });
|
|
2846
|
+
if (!projected.ok) {
|
|
2847
|
+
return err(
|
|
2848
|
+
new RhiError({
|
|
2849
|
+
code: 'shader-compile-failed',
|
|
2850
|
+
expected: 'receipt-derived shadow scene-index projection',
|
|
2851
|
+
hint: 'publish one validated Standard PBR receipt per shadow batch',
|
|
2852
|
+
}),
|
|
2853
|
+
);
|
|
2854
|
+
}
|
|
2855
|
+
const projectionKey = gpuDrivenShadowProjectionKey(
|
|
2856
|
+
identityKey,
|
|
2857
|
+
batch,
|
|
2858
|
+
deformation,
|
|
2859
|
+
batchArtifact,
|
|
2860
|
+
);
|
|
2861
|
+
raster.noteProjectionKey(projectionKey);
|
|
2862
|
+
const meshBindGroup = raster.projectionBindGroup(
|
|
2863
|
+
projected.value.rows,
|
|
2864
|
+
projectionKey,
|
|
2865
|
+
batch.candidates.length,
|
|
2866
|
+
);
|
|
2867
|
+
if (!meshBindGroup.ok) return meshBindGroup;
|
|
2868
|
+
const visibleBindGroup = raster.visibleWindowBindGroupForBuffer(
|
|
2869
|
+
visibleBuffer,
|
|
2870
|
+
batch.visibleBase,
|
|
2871
|
+
batch.visibleCapacity,
|
|
2872
|
+
);
|
|
2873
|
+
if (!visibleBindGroup.ok) return visibleBindGroup;
|
|
2874
|
+
const firstCandidate = batch.candidates[0];
|
|
2875
|
+
const firstSlot =
|
|
2876
|
+
firstCandidate === undefined
|
|
2877
|
+
? undefined
|
|
2878
|
+
: slotsByPrimitive.get(firstCandidate.primitiveIndex);
|
|
2879
|
+
const material =
|
|
2880
|
+
firstSlot?.snapshot.materials[batch.key.materialSlot] ?? firstSlot?.snapshot.material;
|
|
2881
|
+
if (firstSlot === undefined || material === undefined) {
|
|
2882
|
+
return err(
|
|
2883
|
+
new RhiError({
|
|
2884
|
+
code: 'internal-error',
|
|
2885
|
+
expected: `material snapshot for shadow batch ${batch.batchId}`,
|
|
2886
|
+
hint: 'keep the GPU-driven shadow material projection aligned with the retained slot',
|
|
2887
|
+
}),
|
|
2888
|
+
);
|
|
2889
|
+
}
|
|
2890
|
+
projections.set(batch.batchId, {
|
|
2891
|
+
mesh: prepared.mesh,
|
|
2892
|
+
meshBindGroup: meshBindGroup.value,
|
|
2893
|
+
visibleBindGroup: visibleBindGroup.value,
|
|
2894
|
+
deformation,
|
|
2895
|
+
material,
|
|
2896
|
+
materialEntityKey: worldEntityKey(
|
|
2897
|
+
firstSlot.snapshot.worldId,
|
|
2898
|
+
firstSlot.snapshot.entityKey,
|
|
2899
|
+
),
|
|
2900
|
+
});
|
|
2901
|
+
}
|
|
2902
|
+
nextProjections.set(identityKey, projections);
|
|
2903
|
+
}
|
|
2904
|
+
this.shadowViews.retain(views.map((input) => input.identity));
|
|
2905
|
+
this.shadowBatchProjections = nextProjections;
|
|
2906
|
+
this.shadowDrawKeysByView = nextShadowDrawKeysByView;
|
|
2907
|
+
return ok(undefined);
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
private disableShadowViews(): Result<void, RhiError> {
|
|
2911
|
+
this.shadowViews?.retain([]);
|
|
2912
|
+
this.shadowBatchProjections = new Map();
|
|
2913
|
+
this.shadowDrawKeysByView = new Map();
|
|
2914
|
+
return ok(undefined);
|
|
1583
2915
|
}
|
|
1584
2916
|
|
|
1585
2917
|
dispose(): void {
|
|
@@ -1587,20 +2919,141 @@ export class GpuDrivenProduction {
|
|
|
1587
2919
|
this.disposed = true;
|
|
1588
2920
|
this.view?.dispose();
|
|
1589
2921
|
this.view = undefined;
|
|
1590
|
-
this.
|
|
2922
|
+
this.standardRaster?.dispose();
|
|
2923
|
+
this.standardRaster = undefined;
|
|
2924
|
+
this.committedProjectionSource = undefined;
|
|
2925
|
+
this.skinRaster?.dispose();
|
|
2926
|
+
this.skinRaster = undefined;
|
|
1591
2927
|
this.filteredCache = undefined;
|
|
1592
2928
|
this.signatureCache = undefined;
|
|
1593
|
-
this.
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
2929
|
+
this.validatedResidencyCache = undefined;
|
|
2930
|
+
this.cpuValidationTelemetryCache = undefined;
|
|
2931
|
+
this.shadowViews?.dispose();
|
|
2932
|
+
this.shadowViews = undefined;
|
|
2933
|
+
this.shadowSourcePlan = undefined;
|
|
2934
|
+
this.shadowScene = undefined;
|
|
2935
|
+
this.shadowBatchProjections = new Map();
|
|
2936
|
+
this.shadowDrawKeysByView = new Map();
|
|
2937
|
+
this.shadowExpectedDrawKeys = new Set();
|
|
2938
|
+
this.shadowBatchProjectionSource = undefined;
|
|
2939
|
+
this.skinPaletteBuffer = undefined;
|
|
2940
|
+
this.committedPaletteContentRevision = undefined;
|
|
2941
|
+
this.preparationFailure = undefined;
|
|
2942
|
+
this.overflowRecoveryRequired = false;
|
|
1599
2943
|
}
|
|
1600
2944
|
|
|
1601
2945
|
inspect(): GpuDrivenProductionInspection {
|
|
1602
2946
|
const view = this.view?.inspect();
|
|
2947
|
+
const channels: GpuDrivenLaneSummary[] = [];
|
|
2948
|
+
const mainTotalDrawCount = this.mainTotalDrawItems;
|
|
2949
|
+
const mainClaimedDrawCount = this.telemetryOverflow
|
|
2950
|
+
? 0
|
|
2951
|
+
: Math.min(this.gpuOwnedDrawItems, mainTotalDrawCount);
|
|
2952
|
+
const mainResidualDrawCount = Math.max(0, mainTotalDrawCount - mainClaimedDrawCount);
|
|
2953
|
+
const mainFacts = {
|
|
2954
|
+
totalDrawCount: mainTotalDrawCount,
|
|
2955
|
+
claimedDrawCount: mainClaimedDrawCount,
|
|
2956
|
+
residualDrawCount: mainResidualDrawCount,
|
|
2957
|
+
};
|
|
2958
|
+
if (mainClaimedDrawCount > 0 || (this.lane !== 'gpu' && this.lane !== 'blocked')) {
|
|
2959
|
+
channels.push({
|
|
2960
|
+
viewPass: 'main',
|
|
2961
|
+
viewIndex: 0,
|
|
2962
|
+
lane: this.lane,
|
|
2963
|
+
reason: this.laneReason,
|
|
2964
|
+
drawCount: mainClaimedDrawCount,
|
|
2965
|
+
...mainFacts,
|
|
2966
|
+
});
|
|
2967
|
+
}
|
|
2968
|
+
if (this.cpuSemanticFallbackDrawItems > 0) {
|
|
2969
|
+
channels.push({
|
|
2970
|
+
viewPass: 'main',
|
|
2971
|
+
viewIndex: 0,
|
|
2972
|
+
lane: 'cpu-semantic',
|
|
2973
|
+
reason: 'unsupported',
|
|
2974
|
+
drawCount: this.cpuSemanticFallbackDrawItems,
|
|
2975
|
+
...mainFacts,
|
|
2976
|
+
});
|
|
2977
|
+
}
|
|
2978
|
+
if (this.cpuDeformationFallbackDrawItems > 0) {
|
|
2979
|
+
const reasons =
|
|
2980
|
+
this.cpuDeformationReasons.size === 0
|
|
2981
|
+
? ([['resource-not-ready', this.cpuDeformationFallbackDrawItems]] as const)
|
|
2982
|
+
: this.cpuDeformationReasons.entries();
|
|
2983
|
+
for (const [reason, drawCount] of reasons) {
|
|
2984
|
+
channels.push({
|
|
2985
|
+
viewPass: 'main',
|
|
2986
|
+
viewIndex: 0,
|
|
2987
|
+
lane: 'cpu-deformation',
|
|
2988
|
+
reason,
|
|
2989
|
+
drawCount,
|
|
2990
|
+
...mainFacts,
|
|
2991
|
+
});
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
if (this.blockedDrawItems > 0) {
|
|
2995
|
+
channels.push({
|
|
2996
|
+
viewPass: 'main',
|
|
2997
|
+
viewIndex: 0,
|
|
2998
|
+
lane: 'blocked',
|
|
2999
|
+
reason: 'resource-not-ready',
|
|
3000
|
+
drawCount: this.blockedDrawItems,
|
|
3001
|
+
...mainFacts,
|
|
3002
|
+
...(this.preparationFailure === undefined ? {} : { failure: this.preparationFailure }),
|
|
3003
|
+
});
|
|
3004
|
+
}
|
|
3005
|
+
for (const entry of this.shadowViews?.inspect() ?? []) {
|
|
3006
|
+
// Retained view resources outlive admission. An idle frame has no
|
|
3007
|
+
// current GPU projection and must not publish last frame's channels.
|
|
3008
|
+
if (this.submissionState === 'idle' || channels.length >= 32) break;
|
|
3009
|
+
const key = shadowViewIdentityKey(entry.identity);
|
|
3010
|
+
const claimed = this.shadowDrawKeysByView.get(key)?.size ?? 0;
|
|
3011
|
+
const total = Math.max(this.shadowExpectedDrawKeys.size, claimed);
|
|
3012
|
+
const effectiveClaimed = this.telemetryOverflow ? 0 : Math.min(claimed, total);
|
|
3013
|
+
const residual = Math.max(0, total - effectiveClaimed);
|
|
3014
|
+
if (total === 0) continue;
|
|
3015
|
+
const projected = this.shadowBatchProjections.has(key) && entry.batchCount > 0;
|
|
3016
|
+
const viewPass =
|
|
3017
|
+
entry.identity.kind === 'directional'
|
|
3018
|
+
? 'directional-shadow'
|
|
3019
|
+
: entry.identity.kind === 'point'
|
|
3020
|
+
? 'point-shadow'
|
|
3021
|
+
: 'spot-shadow';
|
|
3022
|
+
if (projected && effectiveClaimed > 0) {
|
|
3023
|
+
channels.push({
|
|
3024
|
+
viewPass,
|
|
3025
|
+
viewIndex: entry.identity.index,
|
|
3026
|
+
...(entry.identity.face === undefined ? {} : { face: entry.identity.face }),
|
|
3027
|
+
lane: this.telemetryOverflow ? 'blocked' : 'gpu',
|
|
3028
|
+
reason: this.telemetryOverflow ? 'overflow' : 'none',
|
|
3029
|
+
drawCount: effectiveClaimed,
|
|
3030
|
+
totalDrawCount: total,
|
|
3031
|
+
claimedDrawCount: effectiveClaimed,
|
|
3032
|
+
residualDrawCount: residual,
|
|
3033
|
+
});
|
|
3034
|
+
}
|
|
3035
|
+
if (residual > 0) {
|
|
3036
|
+
const failure = shadowOwnershipFailureInspection(viewPass);
|
|
3037
|
+
channels.push({
|
|
3038
|
+
viewPass,
|
|
3039
|
+
viewIndex: entry.identity.index,
|
|
3040
|
+
...(entry.identity.face === undefined ? {} : { face: entry.identity.face }),
|
|
3041
|
+
lane: 'blocked',
|
|
3042
|
+
reason: this.telemetryOverflow ? 'overflow' : 'shadow-ownership',
|
|
3043
|
+
drawCount: residual,
|
|
3044
|
+
totalDrawCount: total,
|
|
3045
|
+
claimedDrawCount: effectiveClaimed,
|
|
3046
|
+
residualDrawCount: residual,
|
|
3047
|
+
failure,
|
|
3048
|
+
});
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
1603
3051
|
return {
|
|
3052
|
+
residencyValidationScans: this.residencyValidationScans,
|
|
3053
|
+
residencyValidationCacheHits: this.residencyValidationCacheHits,
|
|
3054
|
+
cpuValidationScans: this.cpuValidationScans,
|
|
3055
|
+
cpuValidationCacheHits: this.cpuValidationCacheHits,
|
|
3056
|
+
...this.structureMetrics,
|
|
1604
3057
|
gpuOwnedSnapshotsMaterialized: this.gpuOwnedSnapshotsMaterialized,
|
|
1605
3058
|
filteredPlanBuilds: this.filteredPlanBuilds,
|
|
1606
3059
|
gpuOwnedEntityCount: this.gpuOwnedEntityCount,
|
|
@@ -1620,19 +3073,54 @@ export class GpuDrivenProduction {
|
|
|
1620
3073
|
: 0,
|
|
1621
3074
|
batchCount: view?.batchCount ?? 0,
|
|
1622
3075
|
indirectDrawCount: this.indirectDrawCount,
|
|
3076
|
+
encodedIndirectDraws: this.encodedIndirectDraws,
|
|
3077
|
+
channels: Object.freeze(channels.map((channel) => Object.freeze(channel))),
|
|
3078
|
+
resourceGeneration: view?.resourceGeneration,
|
|
3079
|
+
candidateCapacity: view?.candidateCapacity ?? 0,
|
|
3080
|
+
batchCapacity: view?.batchCapacity ?? 0,
|
|
3081
|
+
indirectCapacity: view?.indirectCapacity ?? 0,
|
|
3082
|
+
overflow: this.telemetryOverflow,
|
|
3083
|
+
submitted: this.submissionState === 'submitted',
|
|
3084
|
+
recoveryCount: this.recoveryCount,
|
|
3085
|
+
retryCount: this.retryCount,
|
|
3086
|
+
lastKnownGoodGeneration: this.lastKnownGoodGeneration,
|
|
1623
3087
|
};
|
|
1624
3088
|
}
|
|
1625
3089
|
|
|
1626
3090
|
/** Read renderer-owned LOD counters copied by the last GPU submission. */
|
|
1627
3091
|
readLodSelection(): Promise<GpuDrivenLodSelectionInspection | undefined> {
|
|
1628
3092
|
if (!this.telemetryPrepared) return Promise.resolve(undefined);
|
|
1629
|
-
|
|
3093
|
+
const view = this.view;
|
|
3094
|
+
const filtered = this.filteredCache?.value;
|
|
3095
|
+
const telemetryCandidateCount = this.telemetryCandidateCount;
|
|
3096
|
+
if (view === undefined) return Promise.resolve(undefined);
|
|
3097
|
+
return view.readLodSelection().then((selection) => {
|
|
3098
|
+
// The observer is asynchronous and prepare() may have published a new
|
|
3099
|
+
// filtered plan while mapAsync was pending. Do not attach old counters to
|
|
3100
|
+
// the new world/primitive cache; the next real copy will be observed on
|
|
3101
|
+
// its own generation.
|
|
3102
|
+
if (
|
|
3103
|
+
!this.telemetryPrepared ||
|
|
3104
|
+
this.view !== view ||
|
|
3105
|
+
this.filteredCache?.value !== filtered ||
|
|
3106
|
+
this.telemetryCandidateCount !== telemetryCandidateCount
|
|
3107
|
+
) {
|
|
3108
|
+
return undefined;
|
|
3109
|
+
}
|
|
1630
3110
|
if (selection !== undefined) {
|
|
1631
3111
|
this.telemetryGeometryWork = selection.geometryWork;
|
|
1632
3112
|
this.telemetryRootGeometryWork = selection.rootGeometryWork;
|
|
3113
|
+
this.telemetryOverflow = selection.overflow;
|
|
3114
|
+
if (selection.overflow) {
|
|
3115
|
+
if (!this.overflowRecoveryRequired) this.retryCount += 1;
|
|
3116
|
+
this.overflowRecoveryRequired = true;
|
|
3117
|
+
this.submissionState = 'aborted';
|
|
3118
|
+
this.lastKnownGoodGeneration = undefined;
|
|
3119
|
+
this.lane = 'blocked';
|
|
3120
|
+
this.laneReason = 'overflow';
|
|
3121
|
+
}
|
|
1633
3122
|
}
|
|
1634
|
-
if (selection === undefined ||
|
|
1635
|
-
const filtered = this.filteredCache?.value;
|
|
3123
|
+
if (selection === undefined || telemetryCandidateCount <= 0) return selection;
|
|
1636
3124
|
if (filtered === undefined) return selection;
|
|
1637
3125
|
const preparedByBatchId = new Map(
|
|
1638
3126
|
filtered.batches.map((prepared) => [prepared.batch.batchId, prepared] as const),
|
|
@@ -1677,8 +3165,8 @@ export class GpuDrivenProduction {
|
|
|
1677
3165
|
}
|
|
1678
3166
|
return {
|
|
1679
3167
|
...selection,
|
|
1680
|
-
candidateCount:
|
|
1681
|
-
occluded: Math.max(0,
|
|
3168
|
+
candidateCount: telemetryCandidateCount,
|
|
3169
|
+
occluded: Math.max(0, telemetryCandidateCount - selection.visible),
|
|
1682
3170
|
worldSelections: Object.freeze(
|
|
1683
3171
|
[...worldSelections.values()].map((aggregate) =>
|
|
1684
3172
|
Object.freeze({
|
|
@@ -1702,24 +3190,109 @@ export class GpuDrivenProduction {
|
|
|
1702
3190
|
|
|
1703
3191
|
recordCpuValidation(
|
|
1704
3192
|
validated: readonly { readonly source: RenderableSnapshot }[],
|
|
1705
|
-
|
|
1706
|
-
|
|
3193
|
+
gpuOwnedDrawKeys: ReadonlySet<string>,
|
|
3194
|
+
cacheKey?: string,
|
|
1707
3195
|
): void {
|
|
1708
|
-
|
|
3196
|
+
const cached = this.cpuValidationTelemetryCache;
|
|
3197
|
+
if (
|
|
3198
|
+
cacheKey !== undefined &&
|
|
3199
|
+
cached !== undefined &&
|
|
3200
|
+
cached.cacheKey === cacheKey &&
|
|
3201
|
+
cached.validated === validated &&
|
|
3202
|
+
cached.gpuOwnedDrawKeys === gpuOwnedDrawKeys
|
|
3203
|
+
) {
|
|
3204
|
+
this.applyCpuValidationTelemetry(cached);
|
|
3205
|
+
this.cpuValidationCacheHits += 1;
|
|
3206
|
+
return;
|
|
3207
|
+
}
|
|
3208
|
+
this.cpuFallbackDrawItems = 0;
|
|
3209
|
+
this.cpuSemanticFallbackDrawItems = 0;
|
|
3210
|
+
this.cpuDeformationFallbackDrawItems = 0;
|
|
3211
|
+
this.cpuDeformationReasons.clear();
|
|
3212
|
+
this.blockedDrawItems = 0;
|
|
1709
3213
|
let gpuOwnedRows = 0;
|
|
3214
|
+
let totalDrawItems = 0;
|
|
3215
|
+
const addDeformationReason = (
|
|
3216
|
+
reason: 'skin-bounds-missing' | 'skin-address-missing' | 'resource-not-ready',
|
|
3217
|
+
): void => {
|
|
3218
|
+
this.cpuDeformationReasons.set(reason, (this.cpuDeformationReasons.get(reason) ?? 0) + 1);
|
|
3219
|
+
};
|
|
1710
3220
|
for (const row of validated) {
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
)
|
|
1718
|
-
|
|
1719
|
-
|
|
3221
|
+
const draws = row.source.gpuDrivenDraws ?? [];
|
|
3222
|
+
if (draws.length === 0) {
|
|
3223
|
+
totalDrawItems += 1;
|
|
3224
|
+
this.cpuFallbackDrawItems += 1;
|
|
3225
|
+
const material = row.source.material;
|
|
3226
|
+
const skinReason = skinLaneReason(row.source, material);
|
|
3227
|
+
if (skinReason !== undefined) {
|
|
3228
|
+
this.cpuDeformationFallbackDrawItems += 1;
|
|
3229
|
+
addDeformationReason(skinReason);
|
|
3230
|
+
} else {
|
|
3231
|
+
this.cpuSemanticFallbackDrawItems += 1;
|
|
3232
|
+
}
|
|
3233
|
+
continue;
|
|
1720
3234
|
}
|
|
3235
|
+
totalDrawItems += draws.length;
|
|
3236
|
+
let gpuOwned = false;
|
|
3237
|
+
let cpuResidual = false;
|
|
3238
|
+
for (const [compactIndex, draw] of draws.entries()) {
|
|
3239
|
+
const material = row.source.materials[draw.materialSlot] ?? row.source.material;
|
|
3240
|
+
const key = gpuDrivenDrawKey(
|
|
3241
|
+
worldEntityKey(row.source.worldId, row.source.entityKey),
|
|
3242
|
+
material.materialHandle ?? -1,
|
|
3243
|
+
gpuDrivenSourceDrawItemIndex(draw, compactIndex),
|
|
3244
|
+
);
|
|
3245
|
+
if (gpuOwnedDrawKeys.has(key)) {
|
|
3246
|
+
gpuOwned = true;
|
|
3247
|
+
continue;
|
|
3248
|
+
}
|
|
3249
|
+
this.cpuFallbackDrawItems += 1;
|
|
3250
|
+
cpuResidual = true;
|
|
3251
|
+
const skinReason = skinLaneReason(row.source, material);
|
|
3252
|
+
if (skinReason !== undefined) {
|
|
3253
|
+
this.cpuDeformationFallbackDrawItems += 1;
|
|
3254
|
+
addDeformationReason(skinReason);
|
|
3255
|
+
} else if (draw.preparationError?.detail.recovery === 'route-cpu-lane') {
|
|
3256
|
+
// Geometry/UV incompatibility is an explicit semantic CPU lane,
|
|
3257
|
+
// not a failed capable submission.
|
|
3258
|
+
this.cpuSemanticFallbackDrawItems += 1;
|
|
3259
|
+
} else if (draw.preparationError?.detail.owner === 'skin') {
|
|
3260
|
+
this.cpuDeformationFallbackDrawItems += 1;
|
|
3261
|
+
addDeformationReason(
|
|
3262
|
+
draw.preparationError.detail.reason === 'skin-address-missing'
|
|
3263
|
+
? 'skin-address-missing'
|
|
3264
|
+
: 'resource-not-ready',
|
|
3265
|
+
);
|
|
3266
|
+
} else if (draw.preparationError !== undefined) {
|
|
3267
|
+
this.blockedDrawItems += 1;
|
|
3268
|
+
} else {
|
|
3269
|
+
this.cpuSemanticFallbackDrawItems += 1;
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
// A row is only CPU-validated when it still has a CPU residual. GPU-only
|
|
3273
|
+
// rows are already admitted by the concrete draw-key set and must not
|
|
3274
|
+
// inflate the per-frame CPU validation budget.
|
|
3275
|
+
if (gpuOwned && cpuResidual) gpuOwnedRows += 1;
|
|
1721
3276
|
}
|
|
3277
|
+
this.gpuOwnedDrawItems = gpuOwnedDrawKeys.size;
|
|
3278
|
+
this.mainTotalDrawItems = totalDrawItems;
|
|
1722
3279
|
this.validatedGpuOwnedRows = gpuOwnedRows;
|
|
3280
|
+
this.cpuValidationScans += 1;
|
|
3281
|
+
if (cacheKey !== undefined) {
|
|
3282
|
+
this.cpuValidationTelemetryCache = {
|
|
3283
|
+
cacheKey,
|
|
3284
|
+
validated,
|
|
3285
|
+
gpuOwnedDrawKeys,
|
|
3286
|
+
gpuOwnedDrawCount: this.gpuOwnedDrawItems,
|
|
3287
|
+
totalDrawItems: this.mainTotalDrawItems,
|
|
3288
|
+
gpuOwnedRows: this.validatedGpuOwnedRows,
|
|
3289
|
+
cpuFallbackDrawItems: this.cpuFallbackDrawItems,
|
|
3290
|
+
cpuSemanticFallbackDrawItems: this.cpuSemanticFallbackDrawItems,
|
|
3291
|
+
cpuDeformationFallbackDrawItems: this.cpuDeformationFallbackDrawItems,
|
|
3292
|
+
cpuDeformationReasons: new Map(this.cpuDeformationReasons),
|
|
3293
|
+
blockedDrawItems: this.blockedDrawItems,
|
|
3294
|
+
};
|
|
3295
|
+
}
|
|
1723
3296
|
}
|
|
1724
3297
|
|
|
1725
3298
|
private sceneIdentity(scene: object): number {
|