@forgeax/engine-render 0.1.28 → 0.1.30
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 +856 -126
- package/dist/assembly/atmosphere-shader-prewarm.d.ts +6 -0
- package/dist/assembly/atmosphere-shader-prewarm.d.ts.map +1 -0
- package/dist/assembly/device-feature-admission.d.ts +4 -3
- package/dist/assembly/device-feature-admission.d.ts.map +1 -1
- package/dist/assembly/dynamic-geometry-host.d.ts +27 -0
- package/dist/assembly/dynamic-geometry-host.d.ts.map +1 -1
- 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 +14 -0
- package/dist/assembly/host-contract.d.ts.map +1 -1
- package/dist/assembly/material/assembly.d.ts +31 -1
- package/dist/assembly/material/assembly.d.ts.map +1 -1
- package/dist/assembly/material/pipeline-helpers.d.ts +24 -0
- package/dist/assembly/material/pipeline-helpers.d.ts.map +1 -0
- 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/device-loss-fanout.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/recovery-attempt.d.ts +1 -6
- package/dist/assembly/recovery/recovery-attempt.d.ts.map +1 -1
- package/dist/assembly/recovery/renderer-recover.d.ts +7 -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 +7 -1
- 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 +2 -2
- package/dist/capture/scene-projection.d.ts +41 -0
- package/dist/capture/scene-projection.d.ts.map +1 -0
- package/dist/{chunk-J52NMQKR.mjs → chunk-7MQ3XBSP.mjs} +1098 -844
- package/dist/chunk-7MQ3XBSP.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-VSL23LZE.mjs → chunk-C3DDSXYK.mjs} +1188 -463
- package/dist/chunk-C3DDSXYK.mjs.map +1 -0
- package/dist/{chunk-IQMKJLNU.mjs → chunk-FUUO4UND.mjs} +443 -30
- package/dist/chunk-FUUO4UND.mjs.map +1 -0
- package/dist/{chunk-J6KACNOH.mjs → chunk-JRBDO2KB.mjs} +617 -7
- package/dist/chunk-JRBDO2KB.mjs.map +1 -0
- package/dist/{chunk-GE3SDD72.mjs → chunk-MDQJPY3T.mjs} +20 -40
- package/dist/chunk-MDQJPY3T.mjs.map +1 -0
- package/dist/{chunk-MYJ5RPJO.mjs → chunk-NO4WZCAH.mjs} +3 -3
- package/dist/chunk-NO4WZCAH.mjs.map +1 -0
- package/dist/{chunk-RBDBZ5R7.mjs → chunk-PA5DRJNI.mjs} +2 -2
- package/dist/chunk-PA5DRJNI.mjs.map +1 -0
- package/dist/{chunk-MBU4ZVE7.mjs → chunk-PSEQ6QPH.mjs} +4655 -892
- package/dist/chunk-PSEQ6QPH.mjs.map +1 -0
- package/dist/{chunk-JAASCMXA.mjs → chunk-S5UE2T5P.mjs} +289 -41
- package/dist/chunk-S5UE2T5P.mjs.map +1 -0
- package/dist/components/camera.d.ts +62 -2
- package/dist/components/camera.d.ts.map +1 -1
- package/dist/components/directional-light.d.ts +2 -2
- package/dist/components/dynamic-resolution.d.ts +15 -0
- package/dist/components/dynamic-resolution.d.ts.map +1 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/instances.d.ts +6 -20
- package/dist/components/instances.d.ts.map +1 -1
- package/dist/components/light-helpers.d.ts.map +1 -1
- package/dist/components/reflection-probe.d.ts +1 -0
- package/dist/components/reflection-probe.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/components/skylight.d.ts +4 -1
- package/dist/components/skylight.d.ts.map +1 -1
- package/dist/construct-renderer.mjs +14809 -7570
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/device/device-scope.d.ts +2 -0
- package/dist/device/device-scope.d.ts.map +1 -1
- package/dist/device/gpu-residency.d.ts +3 -14
- package/dist/device/gpu-residency.d.ts.map +1 -1
- package/dist/dynamic-geometry.d.ts.map +1 -1
- package/dist/environment/background.d.ts +7 -0
- package/dist/environment/background.d.ts.map +1 -0
- package/dist/environment/ibl.d.ts +11 -0
- package/dist/environment/ibl.d.ts.map +1 -0
- package/dist/environment/storage.d.ts +14 -0
- package/dist/environment/storage.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 +41 -4
- package/dist/errors/render.d.ts.map +1 -1
- package/dist/extract/camera.d.ts.map +1 -1
- package/dist/extract/contracts.d.ts +7 -0
- package/dist/extract/contracts.d.ts.map +1 -1
- package/dist/extract/gpu-driven.d.ts +50 -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/material-artifacts.d.ts +24 -0
- package/dist/gpu-driven/material-artifacts.d.ts.map +1 -0
- package/dist/gpu-driven/material-bindings.d.ts +7 -0
- package/dist/gpu-driven/material-bindings.d.ts.map +1 -0
- 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 +2 -0
- package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
- package/dist/gpu-driven/production-raster.d.ts +169 -27
- 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-resource.d.ts +5 -2
- package/dist/gpu-resource.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 +9 -0
- package/dist/hdrp-buffers.d.ts.map +1 -1
- package/dist/ibl/IblPipelineCache.d.ts +2 -0
- package/dist/ibl/IblPipelineCache.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 +3 -1
- package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
- package/dist/index.d.ts +87 -75
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +10 -8
- package/dist/index.mjs.map +1 -1
- package/dist/inspection-types.d.ts +180 -1
- 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 +117 -9
- 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 +15 -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 +23 -5
- package/dist/pbr-pipeline.d.ts.map +1 -1
- package/dist/pipeline/render-extent.d.ts +51 -0
- package/dist/pipeline/render-extent.d.ts.map +1 -0
- package/dist/pipeline/standard-build-context.d.ts +7 -0
- package/dist/pipeline/standard-build-context.d.ts.map +1 -0
- package/dist/pipeline/standard-forward-lane.d.ts +3 -2
- 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/standard-profile.d.ts +2 -1
- package/dist/pipeline/standard-profile.d.ts.map +1 -1
- package/dist/pipeline-builder.d.ts +5 -2
- 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/plugin.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/post-process-errors.d.ts +9 -2
- package/dist/post-process-errors.d.ts.map +1 -1
- package/dist/prepare/extended-lighting/resources.d.ts +3 -3
- 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 +109 -13
- 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 +41 -5
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/main-pass-geometry.d.ts +13 -9
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/main-pass-material.d.ts +43 -3
- 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 +14 -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 +1 -0
- package/dist/record/recovery-pipeline.d.ts.map +1 -1
- package/dist/record/render-context.d.ts +101 -22
- 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 +39 -8
- 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/background.d.ts +14 -0
- package/dist/reflection/background.d.ts.map +1 -0
- package/dist/reflection/filter.d.ts.map +1 -1
- package/dist/reflection/projection.d.ts +1 -0
- package/dist/reflection/projection.d.ts.map +1 -1
- package/dist/reflection/record-owner.d.ts +4 -4
- package/dist/reflection/record-owner.d.ts.map +1 -1
- package/dist/render-contract.d.ts +211 -53
- 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 +81 -6
- package/dist/render-pipeline.d.ts.map +1 -1
- package/dist/render-system-extract-tail.d.ts +15 -6
- package/dist/render-system-extract-tail.d.ts.map +1 -1
- package/dist/render-system-extract.d.ts +128 -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 +27 -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 +31 -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/ssao-config.d.ts +8 -6
- package/dist/ssao-config.d.ts.map +1 -1
- 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 +4 -2
- package/dist/temporal/standard-scene-data.d.ts.map +1 -1
- package/dist/temporal/target-coverage-attachment.d.ts +15 -0
- package/dist/temporal/target-coverage-attachment.d.ts.map +1 -0
- package/dist/temporal/temporal-view.d.ts +6 -0
- package/dist/temporal/temporal-view.d.ts.map +1 -1
- package/dist/temporal/view.d.ts +6 -0
- package/dist/temporal/view.d.ts.map +1 -1
- package/dist/typed-render-graph-primitives.d.ts +41 -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/component.d.ts +1 -0
- package/dist/volume/component.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__/capture-scene-projection.unit.test.ts +24 -0
- package/src/__tests__/detached-inspection.integration.test.ts +5 -0
- package/src/__tests__/device-feature-admission.unit.test.ts +15 -24
- package/src/__tests__/device-loss-fanout.unit.test.ts +43 -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-batch.unit.test.ts +157 -0
- package/src/__tests__/dynamic-geometry.unit.test.ts +36 -0
- package/src/__tests__/dynamic-resolution-plugin.integration.test.ts +81 -0
- package/src/__tests__/factory-contract.integration.test.ts +231 -9
- 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-observation.contract.test.ts +21 -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 +307 -7
- 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 +261 -0
- package/src/__tests__/gpu-driven-mesh-world-identity.unit.test.ts +346 -0
- package/src/__tests__/gpu-driven-pbr.dawn.test.ts +1422 -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 +1076 -106
- 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 +160 -52
- package/src/__tests__/gpu-pass-timing-browser-runner.ts +15 -0
- package/src/__tests__/gpu-pass-timing.browser.test.ts +20 -2
- package/src/__tests__/gpu-resource-scope.unit.test.ts +71 -0
- 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 +613 -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 +57 -3
- package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +4 -0
- package/src/__tests__/mesh-submission-lifetime.unit.test.ts +8 -0
- package/src/__tests__/morph-neutral-admission.unit.test.ts +19 -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 +42 -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__/post-process-error-policy-owner.unit.test.ts +6 -5
- 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 +251 -9
- 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 +171 -0
- package/src/__tests__/reflection-probe-authoring.unit.test.ts +12 -10
- package/src/__tests__/reflection-probe-box.dawn.test.ts +71 -0
- package/src/__tests__/reflection-probe-filter.unit.test.ts +9 -0
- package/src/__tests__/reflection-probe-orientation.dawn.test.ts +294 -0
- package/src/__tests__/reflection-probe-record-owner.characterization.unit.test.ts +5 -14
- package/src/__tests__/reflection-probe-updates.unit.test.ts +129 -0
- package/src/__tests__/render-error-code-owner.test-d.ts +7 -0
- package/src/__tests__/render-error-detail-ssot.test-d.ts +27 -1
- package/src/__tests__/render-error-exhaustive.test-d.ts +15 -0
- package/src/__tests__/render-extent.unit.test.ts +63 -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 +915 -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 +233 -9
- package/src/__tests__/renderer-frame-transaction.integration.test.ts +923 -5
- package/src/__tests__/shader-manifest-fixture.ts +45 -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 +125 -14
- package/src/__tests__/specular-environment-scale.dawn.test.ts +158 -0
- package/src/__tests__/ssao-config.unit.test.ts +7 -0
- package/src/__tests__/ssao-surface.dawn.test.ts +252 -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-cube-silhouette.dawn.test.ts +270 -0
- package/src/__tests__/ssr-format-probe-gating.integration.test.ts +82 -0
- package/src/__tests__/ssr-gpu-dispatch.browser.test.ts +99 -0
- package/src/__tests__/ssr-gpu-dispatch.dawn.test.ts +116 -0
- package/src/__tests__/ssr-gpu-dispatch.ts +951 -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 +84 -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 +275 -0
- package/src/__tests__/standard-pipeline.integration.test.ts +320 -3
- package/src/__tests__/standard-profile-oracle.unit.test.ts +49 -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 +199 -0
- package/src/__tests__/taa-history-clipping.dawn.test.ts +328 -0
- package/src/__tests__/taa-neighborhood-equivalence.dawn.test.ts +782 -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 +342 -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__/temporal-view.unit.test.ts +17 -0
- package/src/__tests__/texture-usage-surface.unit.test.ts +4 -2
- package/src/__tests__/transmission-prewarm.unit.test.ts +57 -0
- 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/__tests__/volumetric-fog-stage-readback.dawn.test.ts +62 -13
- package/src/__tests__/volumetric-fog-topology.unit.test.ts +7 -7
- package/src/assembly/atmosphere-shader-prewarm.ts +39 -0
- package/src/assembly/device-feature-admission.ts +9 -4
- package/src/assembly/dynamic-geometry-host.ts +284 -163
- 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 +19 -0
- package/src/assembly/material/assembly.ts +383 -1
- package/src/assembly/material/pipeline-helpers.ts +100 -0
- package/src/assembly/material-shader-policy.ts +110 -46
- package/src/assembly/recovery/device-loss-fanout.ts +5 -0
- package/src/assembly/recovery/failure-location.ts +62 -0
- package/src/assembly/recovery/recovery-attempt.ts +2 -29
- package/src/assembly/recovery/renderer-recover.ts +16 -17
- package/src/assembly/renderer-facade.ts +34 -1
- 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 +46 -1
- 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 +161 -131
- 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 +744 -730
- package/src/assembly/webgpu-vertex-layouts.ts +233 -87
- package/src/capture/scene-projection.ts +37 -0
- package/src/components/__tests__/camera-exposure.contract.unit.test.ts +153 -0
- package/src/components/__tests__/dynamic-resolution-schema.unit.test.ts +65 -0
- package/src/components/__tests__/light-radius.integration.test.ts +59 -0
- package/src/components/camera.ts +287 -7
- package/src/components/directional-light.ts +3 -3
- package/src/components/dynamic-resolution.ts +67 -0
- package/src/components/index.ts +2 -0
- package/src/components/instances.ts +45 -18
- package/src/components/light-helpers.ts +6 -1
- package/src/components/reflection-probe.ts +1 -0
- package/src/components/scene-instance.ts +2 -1
- package/src/components/screen-space-reflection.ts +21 -0
- package/src/components/skylight.ts +4 -1
- package/src/components/sprite-animation.ts +2 -2
- package/src/components/sprite-instances.ts +1 -1
- package/src/device/device-scope.ts +13 -7
- package/src/device/gpu-residency.ts +45 -42
- package/src/dynamic-geometry.ts +13 -8
- package/src/environment/background.ts +281 -0
- package/src/environment/ibl.ts +211 -0
- package/src/environment/storage.ts +63 -0
- package/src/errors/gpu-driven.ts +49 -8
- package/src/errors/render.ts +91 -3
- package/src/extract/camera.ts +37 -1
- package/src/extract/contracts.ts +10 -0
- package/src/extract/gpu-driven.ts +240 -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 +452 -108
- package/src/gpu-driven/material-artifacts.ts +179 -0
- package/src/gpu-driven/material-bindings.ts +41 -0
- package/src/gpu-driven/pbr-program.ts +31 -0
- package/src/gpu-driven/prepared-draw.ts +42 -6
- package/src/gpu-driven/production-raster.ts +2806 -716
- package/src/gpu-driven/shadow-views.ts +539 -0
- package/src/gpu-driven/view-gpu.ts +188 -41
- package/src/gpu-resource.ts +21 -2
- package/src/gpu-scene-schema.ts +61 -11
- package/src/gpu-scene.ts +197 -99
- package/src/gpu-texture-usage.ts +1 -0
- package/src/hdrp-buffers.ts +141 -36
- package/src/ibl/IblPipelineCache.ts +5 -0
- package/src/ibl/frame-resources.ts +38 -0
- package/src/ibl/skylight-bind-group.ts +32 -21
- package/src/index.ts +194 -75
- package/src/inspection-types.ts +207 -1
- 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 +194 -0
- package/src/materials.ts +44 -8
- package/src/pbr-pipeline.ts +172 -19
- package/src/pipeline/render-extent.ts +111 -0
- package/src/pipeline/standard-build-context.ts +7 -0
- package/src/pipeline/standard-forward-lane.ts +74 -29
- 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 +232 -70
- package/src/pipeline/standard-post.ts +190 -27
- package/src/pipeline/standard-profile.ts +2 -1
- package/src/pipeline-builder.ts +28 -9
- package/src/pipeline-spec-types.ts +4 -0
- package/src/pipeline-spec.ts +37 -29
- package/src/plugin.ts +4 -0
- package/src/points-lines/standard-owner.ts +6 -14
- package/src/post-process-errors.ts +26 -18
- package/src/prepare/__tests__/extended-lighting-capability.unit.test.ts +2 -2
- package/src/prepare/extended-lighting/resources.ts +3 -3
- package/src/prepare/prepared-graphics-resolver.ts +9 -2
- package/src/record/__tests__/main-pass-geometry.unit.test.ts +68 -0
- package/src/record/__tests__/material-bg-cache.unit.test.ts +45 -0
- package/src/record/__tests__/material-slot-plan.unit.test.ts +57 -0
- package/src/record/__tests__/mesh-ssbo.unit.test.ts +40 -1
- package/src/record/__tests__/recovery-pipeline.unit.test.ts +20 -1
- package/src/record/__tests__/sprite-material-abi.unit.test.ts +41 -6
- package/src/record/__tests__/temporal-payload.unit.test.ts +2 -0
- 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 +15 -12
- package/src/record/frame-snapshot.ts +146 -34
- package/src/record/frame-targets.ts +9 -0
- package/src/record/frame.ts +838 -170
- package/src/record/main-pass-geometry.ts +209 -103
- package/src/record/main-pass-material.ts +157 -40
- package/src/record/main-pass-sprite-draws.ts +16 -107
- package/src/record/main-pass.ts +141 -12
- package/src/record/material-uniforms.ts +1 -1
- package/src/record/mesh-ssbo.ts +59 -15
- package/src/record/prepared-material-bindings.ts +238 -0
- package/src/record/recovery-pipeline.ts +69 -90
- package/src/record/render-context.ts +123 -24
- package/src/record/shadow-pass.ts +1389 -230
- package/src/record/skybox-post-pass.ts +14 -3
- package/src/record/typed-frame-graph.ts +1064 -47
- package/src/record/view-ubo.ts +12 -1
- package/src/recovery/render-system-candidate.ts +135 -68
- package/src/recovery/render-system-roots.ts +113 -0
- package/src/recovery/types.ts +77 -0
- package/src/reflection/background.ts +151 -0
- package/src/reflection/filter.ts +3 -0
- package/src/reflection/projection.ts +16 -6
- package/src/reflection/record-owner.ts +308 -140
- package/src/render-contract.ts +237 -49
- package/src/render-graph-primitives.ts +34 -5
- package/src/render-pipeline.ts +90 -2
- package/src/render-system-extract-tail.ts +431 -162
- package/src/render-system-extract.ts +478 -119
- package/src/render-system-projections.ts +241 -0
- package/src/render-system.ts +715 -828
- package/src/scene/render-scene-types.ts +13 -4
- package/src/scene/render-scene.ts +577 -355
- package/src/scene/shadow-visibility.ts +21 -0
- package/src/ssao-config.ts +25 -8
- 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 +797 -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 +79 -12
- package/src/temporal/history.ts +3 -1
- package/src/temporal/inspection.ts +2 -1
- package/src/temporal/standard-scene-data.ts +10 -2
- package/src/temporal/target-coverage-attachment.ts +72 -0
- package/src/temporal/temporal-view.ts +11 -2
- package/src/temporal/view.ts +12 -2
- package/src/transmission/__tests__/standard-transmission.dawn.test.ts +219 -139
- package/src/typed-render-graph-primitives.ts +304 -74
- package/src/typed-shadow-passes.ts +122 -12
- package/src/volume/component.ts +1 -0
- package/src/volume/passes.ts +3 -3
- package/src/volume/temporal.ts +10 -0
- package/dist/chunk-GE3SDD72.mjs.map +0 -1
- package/dist/chunk-GF523LHF.mjs.map +0 -1
- package/dist/chunk-IQMKJLNU.mjs.map +0 -1
- package/dist/chunk-J52NMQKR.mjs.map +0 -1
- package/dist/chunk-J6KACNOH.mjs.map +0 -1
- package/dist/chunk-JAASCMXA.mjs.map +0 -1
- package/dist/chunk-MBU4ZVE7.mjs.map +0 -1
- package/dist/chunk-MYJ5RPJO.mjs.map +0 -1
- package/dist/chunk-RBDBZ5R7.mjs.map +0 -1
- package/dist/chunk-VSL23LZE.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,51 @@ 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
|
+
gpuDrivenMaterialArtifactKey,
|
|
28
|
+
gpuDrivenShadowDrawKey,
|
|
29
|
+
gpuDrivenSourceDrawItemIndex,
|
|
30
|
+
} from '../extract/gpu-driven';
|
|
31
|
+
import { GPU_SCENE_LAYOUTS } from '../gpu-scene-schema';
|
|
26
32
|
import {
|
|
27
33
|
GPU_BUFFER_USAGE_COPY_DST,
|
|
28
34
|
GPU_BUFFER_USAGE_INDEX,
|
|
29
35
|
GPU_BUFFER_USAGE_STORAGE,
|
|
30
|
-
GPU_BUFFER_USAGE_UNIFORM,
|
|
31
36
|
GPU_BUFFER_USAGE_VERTEX,
|
|
32
37
|
} from '../gpu-usage';
|
|
33
|
-
import {
|
|
34
|
-
|
|
38
|
+
import type {
|
|
39
|
+
GpuDrivenLaneReason,
|
|
40
|
+
GpuDrivenLaneSummary,
|
|
41
|
+
GpuDrivenPreparationFailureInspection,
|
|
42
|
+
GpuDrivenProductionInspection,
|
|
43
|
+
GpuDrivenStructureMetrics,
|
|
44
|
+
} from '../inspection-types';
|
|
45
|
+
import { isStandardPbrMaterialShader, resolvePipelineGroup2Contract } from '../pbr-pipeline';
|
|
35
46
|
import type { PipelineBuilderShaderModuleFactory } from '../pipeline-builder';
|
|
47
|
+
import type { ValidatedRenderable } from '../record/frame-snapshot';
|
|
36
48
|
import { worldEntityKey } from '../record/frame-snapshot';
|
|
37
49
|
import { geometryRenderStateForTopology } from '../record/main-pass-material';
|
|
38
|
-
import { MESH_SSBO_BYTES } from '../record/mesh-ssbo';
|
|
39
|
-
import
|
|
50
|
+
import { MESH_SSBO_BYTES, packInstanceStorageBuffer } from '../record/mesh-ssbo';
|
|
51
|
+
import {
|
|
52
|
+
MATERIAL_PER_ENTITY_STRIDE,
|
|
53
|
+
type PipelineState,
|
|
54
|
+
type RenderSystemInternals,
|
|
55
|
+
} from '../record/render-context';
|
|
40
56
|
import type { CameraSnapshot } from '../render-contract';
|
|
41
|
-
import type {
|
|
42
|
-
|
|
57
|
+
import type {
|
|
58
|
+
GpuDrivenSceneMeshBinding,
|
|
59
|
+
GpuDrivenStandardPbrFrameResources,
|
|
60
|
+
RenderPipelineFrame,
|
|
61
|
+
RenderPipelineGpuDrivenProjection,
|
|
62
|
+
} from '../render-pipeline';
|
|
63
|
+
import type {
|
|
64
|
+
DispatchEntry,
|
|
65
|
+
MaterialSnapshot,
|
|
66
|
+
RenderableSnapshot,
|
|
67
|
+
ShadowCasterMembership,
|
|
68
|
+
} from '../render-system-extract';
|
|
43
69
|
import type { PersistentGpuDrivenState } from '../scene/render-scene';
|
|
44
70
|
import { selectLod } from '../scene/visibility/lod-selector';
|
|
45
71
|
import type { OcclusionFrameProjection } from '../scene/visibility/occlusion-runtime';
|
|
@@ -49,20 +75,24 @@ import {
|
|
|
49
75
|
projectedHeightForCandidate,
|
|
50
76
|
type SubmissionPlan,
|
|
51
77
|
} from './batch-topology';
|
|
78
|
+
import { materialBindingKey } from './material-bindings';
|
|
79
|
+
import { type GpuDrivenPbrProgram, resolveStandardPbrProgram } from './pbr-program';
|
|
52
80
|
import {
|
|
53
|
-
|
|
54
|
-
type
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const FRAGMENT_STAGE = 0x2;
|
|
61
|
-
const EMPTY_RESOURCE_CLASS = JSON.stringify({ textures: [], samplers: [], video: [] });
|
|
81
|
+
type ShadowViewIdentity,
|
|
82
|
+
type ShadowViewProjection,
|
|
83
|
+
ShadowViewStatePool,
|
|
84
|
+
type ShadowViewUpdateInput,
|
|
85
|
+
shadowViewIdentityKey,
|
|
86
|
+
} from './shadow-views';
|
|
87
|
+
import { type GpuDrivenLodSelectionInspection, GpuDrivenView, selectGpuLodLane } from './view-gpu';
|
|
62
88
|
|
|
63
89
|
export interface StandardPbrSceneCandidate {
|
|
64
90
|
readonly primitiveIndex: number;
|
|
65
91
|
readonly world: Float32Array;
|
|
92
|
+
/** Optional per-instance local transform selected by BatchTopology. */
|
|
93
|
+
readonly instanceTransform?: Float32Array;
|
|
94
|
+
/** Optional GPU Scene material row carried by the shadow projection. */
|
|
95
|
+
readonly materialIndex?: number;
|
|
66
96
|
}
|
|
67
97
|
|
|
68
98
|
export interface StandardPbrSceneProjection {
|
|
@@ -73,6 +103,24 @@ export interface StandardPbrSceneProjection {
|
|
|
73
103
|
readonly rows: Float32Array;
|
|
74
104
|
}
|
|
75
105
|
|
|
106
|
+
export interface GpuDrivenShadowBatchProjection {
|
|
107
|
+
readonly mesh: MeshGpuHandles;
|
|
108
|
+
readonly meshBindGroup: BindGroup;
|
|
109
|
+
readonly visibleBindGroup: BindGroup;
|
|
110
|
+
readonly deformation: 'rigid' | 'skin';
|
|
111
|
+
/** Actual producer-selected shadow-pass artifact for this batch. */
|
|
112
|
+
readonly shadowArtifact: MaterialShaderArtifact;
|
|
113
|
+
/** Pass entry points selected by the authored ShadowCaster dispatch. */
|
|
114
|
+
readonly shadowVertexEntry?: string;
|
|
115
|
+
readonly shadowFragmentEntry?: string;
|
|
116
|
+
/** Material snapshot used to assemble the receipt-owned alpha-mask BG. */
|
|
117
|
+
readonly material: MaterialSnapshot;
|
|
118
|
+
/** Composite entity identity used only for video-resource routing. */
|
|
119
|
+
readonly materialEntityKey: number;
|
|
120
|
+
/** Geometry-owned COLOR_0 fact shared by main and shadow ABI variants. */
|
|
121
|
+
readonly vertexColorAvailable: boolean;
|
|
122
|
+
}
|
|
123
|
+
|
|
76
124
|
/**
|
|
77
125
|
* Build the producer-owned Standard PBR scene-index projection. The receipt is
|
|
78
126
|
* the only source of the vertex entry and layout identity; callers cannot
|
|
@@ -97,12 +145,16 @@ export function projectStandardPbrScene(input: {
|
|
|
97
145
|
for (let index = 0; index < input.candidates.length; index += 1) {
|
|
98
146
|
const candidate = input.candidates[index];
|
|
99
147
|
if (candidate === undefined) continue;
|
|
148
|
+
const world =
|
|
149
|
+
candidate.instanceTransform === undefined
|
|
150
|
+
? candidate.world
|
|
151
|
+
: mat4.multiply(mat4.create(), candidate.world, candidate.instanceTransform);
|
|
100
152
|
const base = index * 48;
|
|
101
153
|
for (let value = 0; value < 16; value += 1) {
|
|
102
|
-
rows[base + value] =
|
|
103
|
-
rows[base + 28 + value] =
|
|
154
|
+
rows[base + value] = world[value] ?? 0;
|
|
155
|
+
rows[base + 28 + value] = world[value] ?? 0;
|
|
104
156
|
}
|
|
105
|
-
const normalMatrix = mat3.normalMatrix(normal,
|
|
157
|
+
const normalMatrix = mat3.normalMatrix(normal, world);
|
|
106
158
|
for (let column = 0; column < 3; column += 1) {
|
|
107
159
|
const source = column * 3;
|
|
108
160
|
const target = base + 16 + column * 4;
|
|
@@ -110,6 +162,11 @@ export function projectStandardPbrScene(input: {
|
|
|
110
162
|
rows[target + 1] = normalMatrix[source + 1] ?? 0;
|
|
111
163
|
rows[target + 2] = normalMatrix[source + 2] ?? 0;
|
|
112
164
|
}
|
|
165
|
+
// The final temporal metadata lane is otherwise unused by the shadow
|
|
166
|
+
// vertex path. Carry the concrete scene-material row there so skinned
|
|
167
|
+
// GPU-driven alpha-mask shadows can keep the palette base in visible.y
|
|
168
|
+
// without introducing a second visible-stream ABI.
|
|
169
|
+
rows[base + 47] = candidate.materialIndex ?? 0;
|
|
113
170
|
}
|
|
114
171
|
return {
|
|
115
172
|
ok: true,
|
|
@@ -183,53 +240,11 @@ export function adaptStandardPbrFrameResources(input: {
|
|
|
183
240
|
};
|
|
184
241
|
}
|
|
185
242
|
|
|
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
243
|
interface PreparedBatch {
|
|
231
244
|
readonly batch: GpuDrivenBatch;
|
|
232
245
|
readonly mesh: MeshGpuHandles;
|
|
246
|
+
/** Exact producer artifact selected by every draw in this homogeneous batch. */
|
|
247
|
+
readonly artifact: MaterialShaderArtifact;
|
|
233
248
|
readonly renderState: MaterialRenderState | undefined;
|
|
234
249
|
/** Stable World attachment key used to join same-submit GPU counters. */
|
|
235
250
|
readonly worldKey: number;
|
|
@@ -238,13 +253,6 @@ interface PreparedBatch {
|
|
|
238
253
|
readonly slotGeneration: number;
|
|
239
254
|
}
|
|
240
255
|
|
|
241
|
-
interface ProjectedBatch extends PreparedBatch {
|
|
242
|
-
readonly bindings: BindGroup;
|
|
243
|
-
readonly pipeline: RenderPipeline;
|
|
244
|
-
readonly vertex: GraphBuffer;
|
|
245
|
-
readonly index?: GraphBuffer;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
256
|
function bufferBinding(
|
|
249
257
|
buffer: import('@forgeax/engine-rhi').Buffer,
|
|
250
258
|
offset?: number,
|
|
@@ -282,46 +290,118 @@ function activeFrustum(camera: CameraSnapshot): Float32Array {
|
|
|
282
290
|
);
|
|
283
291
|
}
|
|
284
292
|
|
|
293
|
+
function hasFiniteOrderedBounds(bounds: Float32Array | undefined): boolean {
|
|
294
|
+
if (bounds === undefined || bounds.length < 6) return false;
|
|
295
|
+
for (let index = 0; index < 6; index += 1) {
|
|
296
|
+
if (!Number.isFinite(bounds[index])) return false;
|
|
297
|
+
}
|
|
298
|
+
const minX = bounds[0];
|
|
299
|
+
const minY = bounds[1];
|
|
300
|
+
const minZ = bounds[2];
|
|
301
|
+
const maxX = bounds[3];
|
|
302
|
+
const maxY = bounds[4];
|
|
303
|
+
const maxZ = bounds[5];
|
|
304
|
+
return (
|
|
305
|
+
minX !== undefined &&
|
|
306
|
+
minY !== undefined &&
|
|
307
|
+
minZ !== undefined &&
|
|
308
|
+
maxX !== undefined &&
|
|
309
|
+
maxY !== undefined &&
|
|
310
|
+
maxZ !== undefined &&
|
|
311
|
+
minX <= maxX &&
|
|
312
|
+
minY <= maxY &&
|
|
313
|
+
minZ <= maxZ
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function skinLaneReason(
|
|
318
|
+
snapshot: RenderableSnapshot,
|
|
319
|
+
): 'skin-bounds-missing' | 'skin-address-missing' | undefined {
|
|
320
|
+
if (snapshot.skin === undefined) return undefined;
|
|
321
|
+
if (!hasFiniteOrderedBounds(snapshot.skin.bounds)) return 'skin-bounds-missing';
|
|
322
|
+
if (
|
|
323
|
+
snapshot.skin.storageOrUniform !== 'storage' ||
|
|
324
|
+
!Number.isInteger(snapshot.skin.customDataStart) ||
|
|
325
|
+
snapshot.skin.customDataStart < 0 ||
|
|
326
|
+
snapshot.skin.buffer === undefined
|
|
327
|
+
) {
|
|
328
|
+
return 'skin-address-missing';
|
|
329
|
+
}
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
|
|
285
333
|
/**
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
334
|
+
* Derived projected heights invalidate the prepared plan only when a
|
|
335
|
+
* candidate has more than its implicit root LOD row. No-LOD plans have no
|
|
336
|
+
* view-dependent selection. Root matrix edits and camera edits feed the same
|
|
337
|
+
* height calculation; retaining the empty key for absent LOD avoids rebuilding
|
|
338
|
+
* their candidate and batch uploads on every camera frame.
|
|
289
339
|
*/
|
|
290
|
-
function lodProjectionFingerprint(
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
340
|
+
function lodProjectionFingerprint(
|
|
341
|
+
source: SubmissionPlan,
|
|
342
|
+
camera: CameraSnapshot,
|
|
343
|
+
slots: PersistentGpuDrivenState['slots'],
|
|
344
|
+
): string {
|
|
345
|
+
const primitives = new Set<number>();
|
|
346
|
+
for (const batch of source.batches) {
|
|
347
|
+
for (const candidate of batch.candidates) {
|
|
348
|
+
if ((candidate.lodCoverages?.length ?? 0) > 1) primitives.add(candidate.primitiveIndex);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (primitives.size === 0) return '';
|
|
352
|
+
const heights: number[] = [];
|
|
353
|
+
for (const slot of slots) {
|
|
354
|
+
if (primitives.has(slot.slot))
|
|
355
|
+
heights.push(slot.slot, projectedHeightForCandidate(slot, camera));
|
|
356
|
+
}
|
|
357
|
+
return JSON.stringify(heights);
|
|
299
358
|
}
|
|
300
359
|
|
|
301
|
-
function
|
|
360
|
+
function preparedPbrCandidateEligible(
|
|
361
|
+
snapshot: RenderableSnapshot,
|
|
362
|
+
batch: GpuDrivenBatch,
|
|
363
|
+
candidate: GpuDrivenBatch['candidates'][number],
|
|
364
|
+
artifact: MaterialShaderArtifact | undefined,
|
|
365
|
+
): boolean {
|
|
302
366
|
const material = snapshot.materials[batch.key.materialSlot] ?? snapshot.material;
|
|
303
|
-
const
|
|
367
|
+
const prepared = candidate.prepared;
|
|
368
|
+
if (prepared === undefined) return false;
|
|
369
|
+
const receipt = artifact?.receipt;
|
|
370
|
+
const publishedAbiMatches =
|
|
371
|
+
receipt !== undefined &&
|
|
372
|
+
artifact?.material === prepared.identity.material &&
|
|
373
|
+
(prepared.receiptIdentity === undefined ||
|
|
374
|
+
(prepared.receiptIdentity === receipt.receiptIdentity &&
|
|
375
|
+
prepared.receiptGeneration === receipt.generation)) &&
|
|
376
|
+
receipt.reflection.layoutIdentity === artifact?.layoutIdentity;
|
|
377
|
+
const alphaBlend = material.transparent === true || material.renderState?.blend !== undefined;
|
|
378
|
+
const cpuOnlyMaterialResources =
|
|
379
|
+
(material.textureSources?.size ?? 0) > 0 || (material.videoTextureFields?.size ?? 0) > 0;
|
|
380
|
+
// Transmission has a separate CPU semantic pass today. Keep it out of the
|
|
381
|
+
// GPU-owned candidate set so the residual transmission draw is not swallowed
|
|
382
|
+
// by the generic Standard PBR lane.
|
|
383
|
+
const transmission =
|
|
384
|
+
isStandardPbrMaterialShader(prepared.identity.material) &&
|
|
385
|
+
typeof material.paramSnapshot?.transmission === 'number' &&
|
|
386
|
+
material.paramSnapshot.transmission > 0;
|
|
387
|
+
const skinReady =
|
|
388
|
+
prepared.identity.deformation !== 'skin' ||
|
|
389
|
+
(artifact?.receipt?.skinPaletteAddress !== undefined &&
|
|
390
|
+
snapshot.skin?.storageOrUniform === 'storage' &&
|
|
391
|
+
snapshot.skin.customDataStart >= 0 &&
|
|
392
|
+
Number.isInteger(snapshot.skin.customDataStart) &&
|
|
393
|
+
snapshot.skin.buffer !== undefined &&
|
|
394
|
+
hasFiniteOrderedBounds(snapshot.skin.bounds));
|
|
304
395
|
return (
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
material.transparent !== true
|
|
396
|
+
publishedAbiMatches &&
|
|
397
|
+
!alphaBlend &&
|
|
398
|
+
!cpuOnlyMaterialResources &&
|
|
399
|
+
!transmission &&
|
|
400
|
+
(prepared.identity.deformation === 'rigid' || prepared.identity.deformation === 'skin') &&
|
|
401
|
+
skinReady
|
|
312
402
|
);
|
|
313
403
|
}
|
|
314
404
|
|
|
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
405
|
function meshSupportsBatch(mesh: MeshGpuHandles, batch: GpuDrivenBatch): boolean {
|
|
326
406
|
if (
|
|
327
407
|
batch.candidates.some((candidate) => {
|
|
@@ -341,6 +421,160 @@ function meshSupportsBatch(mesh: MeshGpuHandles, batch: GpuDrivenBatch): boolean
|
|
|
341
421
|
: !mesh.indexed;
|
|
342
422
|
}
|
|
343
423
|
|
|
424
|
+
function sceneCandidate(
|
|
425
|
+
slot: PersistentGpuDrivenState['slots'][number],
|
|
426
|
+
candidate: GpuDrivenBatch['candidates'][number],
|
|
427
|
+
materialIndex?: number,
|
|
428
|
+
): StandardPbrSceneCandidate {
|
|
429
|
+
const transforms = slot.snapshot.instances?.transforms;
|
|
430
|
+
return {
|
|
431
|
+
primitiveIndex: candidate.primitiveIndex,
|
|
432
|
+
world: slot.snapshot.transform.world,
|
|
433
|
+
...(transforms === undefined
|
|
434
|
+
? {}
|
|
435
|
+
: {
|
|
436
|
+
instanceTransform: transforms.subarray(
|
|
437
|
+
candidate.instanceOrdinal * 16,
|
|
438
|
+
candidate.instanceOrdinal * 16 + 16,
|
|
439
|
+
),
|
|
440
|
+
}),
|
|
441
|
+
...(materialIndex === undefined ? {} : { materialIndex }),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function shadowCandidateKey(candidate: GpuDrivenBatch['candidates'][number]): string {
|
|
446
|
+
return `${candidate.primitiveIndex}:${candidate.drawItemIndex}:${candidate.instanceOrdinal}`;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function gpuDrivenProjectionKey(
|
|
450
|
+
batch: GpuDrivenBatch,
|
|
451
|
+
deformation: 'rigid' | 'skin',
|
|
452
|
+
artifact: MaterialShaderArtifact,
|
|
453
|
+
): string {
|
|
454
|
+
const receipt = artifact.receipt;
|
|
455
|
+
return `${batch.batchId}:${batch.generation}:${gpuDrivenMaterialArtifactKey({
|
|
456
|
+
material: artifact.material,
|
|
457
|
+
deformation,
|
|
458
|
+
receiptIdentity: receipt?.receiptIdentity,
|
|
459
|
+
receiptGeneration: receipt?.generation ?? 0,
|
|
460
|
+
})}:${artifact.variantSet ?? ''}:${materialArtifactProgramIdentity(artifact)}:${artifact.vertexEntry ?? receipt?.sceneIndexEntry ?? ''}:${artifact.fragmentEntry ?? ''}`;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function gpuDrivenShadowProjectionKey(
|
|
464
|
+
viewKey: string,
|
|
465
|
+
batch: GpuDrivenBatch,
|
|
466
|
+
deformation: 'rigid' | 'skin',
|
|
467
|
+
artifact: MaterialShaderArtifact,
|
|
468
|
+
): string {
|
|
469
|
+
return `shadow:${viewKey}:${gpuDrivenProjectionKey(batch, deformation, artifact)}`;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function sourceDrawForCandidate(
|
|
473
|
+
slot: PersistentGpuDrivenState['slots'][number],
|
|
474
|
+
candidate: GpuDrivenBatch['candidates'][number],
|
|
475
|
+
): NonNullable<RenderableSnapshot['gpuDrivenDraws']>[number] | undefined {
|
|
476
|
+
return slot.snapshot.gpuDrivenDraws?.find(
|
|
477
|
+
(draw, compactIndex) =>
|
|
478
|
+
gpuDrivenSourceDrawItemIndex(draw, compactIndex) === candidate.drawItemIndex,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const materialArtifactProgramIdentities = new WeakMap<
|
|
483
|
+
MaterialShaderArtifact,
|
|
484
|
+
{ readonly source: string; readonly identity: string }
|
|
485
|
+
>();
|
|
486
|
+
|
|
487
|
+
/** Hash a publication once; replacement source invalidates the same artifact key. */
|
|
488
|
+
function materialArtifactProgramIdentity(artifact: MaterialShaderArtifact): string {
|
|
489
|
+
const source = artifact.specializationKey ?? artifact.wgsl;
|
|
490
|
+
const cached = materialArtifactProgramIdentities.get(artifact);
|
|
491
|
+
if (cached?.source === source) return cached.identity;
|
|
492
|
+
let hash = 2166136261;
|
|
493
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
494
|
+
hash ^= source.charCodeAt(index);
|
|
495
|
+
hash = Math.imul(hash, 16777619);
|
|
496
|
+
}
|
|
497
|
+
const identity = `${source.length}:${hash >>> 0}`;
|
|
498
|
+
materialArtifactProgramIdentities.set(artifact, { source, identity });
|
|
499
|
+
return identity;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function shadowKeysForCandidate(
|
|
503
|
+
slot: PersistentGpuDrivenState['slots'][number],
|
|
504
|
+
candidate: GpuDrivenBatch['candidates'][number],
|
|
505
|
+
shadowCasterDrawKeys: ReadonlySet<string> | undefined,
|
|
506
|
+
shadowCasterMembership: readonly ShadowCasterMembership[] | undefined,
|
|
507
|
+
): readonly string[] {
|
|
508
|
+
const draw = sourceDrawForCandidate(slot, candidate);
|
|
509
|
+
if (draw === undefined) return [];
|
|
510
|
+
const material = slot.snapshot.materials[draw.materialSlot] ?? slot.snapshot.material;
|
|
511
|
+
const worldEntity = worldEntityKey(slot.snapshot.worldId, slot.snapshot.entityKey);
|
|
512
|
+
const base = gpuDrivenDrawKey(
|
|
513
|
+
worldEntity,
|
|
514
|
+
material.materialHandle ?? -1,
|
|
515
|
+
candidate.drawItemIndex,
|
|
516
|
+
);
|
|
517
|
+
if (shadowCasterMembership !== undefined) {
|
|
518
|
+
const keys = new Set<string>();
|
|
519
|
+
for (const membership of shadowCasterMembership) {
|
|
520
|
+
if (
|
|
521
|
+
membership.worldEntity === worldEntity &&
|
|
522
|
+
membership.materialHandle === (material.materialHandle ?? -1) &&
|
|
523
|
+
membership.drawItemIndex === candidate.drawItemIndex
|
|
524
|
+
) {
|
|
525
|
+
keys.add(
|
|
526
|
+
gpuDrivenShadowDrawKey(
|
|
527
|
+
membership.worldEntity,
|
|
528
|
+
membership.materialHandle,
|
|
529
|
+
membership.drawItemIndex,
|
|
530
|
+
membership.passIndex,
|
|
531
|
+
),
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return [...keys];
|
|
536
|
+
}
|
|
537
|
+
if (shadowCasterDrawKeys === undefined) return [];
|
|
538
|
+
return [...shadowCasterDrawKeys].filter((key) => {
|
|
539
|
+
const parts = key.split(':');
|
|
540
|
+
return parts.length === 4 && `${parts[0]}:${parts[1]}:${parts[2]}` === base;
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function shadowMembershipIsGpuCompatible(
|
|
545
|
+
slot: PersistentGpuDrivenState['slots'][number],
|
|
546
|
+
candidate: GpuDrivenBatch['candidates'][number],
|
|
547
|
+
shadowCasterDrawKeys: ReadonlySet<string> | undefined,
|
|
548
|
+
shadowCasterMembership: readonly ShadowCasterMembership[] | undefined,
|
|
549
|
+
): boolean {
|
|
550
|
+
const keys = shadowKeysForCandidate(
|
|
551
|
+
slot,
|
|
552
|
+
candidate,
|
|
553
|
+
shadowCasterDrawKeys,
|
|
554
|
+
shadowCasterMembership,
|
|
555
|
+
);
|
|
556
|
+
if (keys.length !== 1) return false;
|
|
557
|
+
if (shadowCasterMembership === undefined) return true;
|
|
558
|
+
const draw = sourceDrawForCandidate(slot, candidate);
|
|
559
|
+
if (draw === undefined) return false;
|
|
560
|
+
const material = slot.snapshot.materials[draw.materialSlot] ?? slot.snapshot.material;
|
|
561
|
+
const worldEntity = worldEntityKey(slot.snapshot.worldId, slot.snapshot.entityKey);
|
|
562
|
+
const matching = shadowCasterMembership.filter(
|
|
563
|
+
(membership) =>
|
|
564
|
+
membership.worldEntity === worldEntity &&
|
|
565
|
+
membership.materialHandle === (material.materialHandle ?? -1) &&
|
|
566
|
+
membership.drawItemIndex === candidate.drawItemIndex,
|
|
567
|
+
);
|
|
568
|
+
if (matching.length !== 1) return false;
|
|
569
|
+
const membership = matching[0];
|
|
570
|
+
if (membership?.cpuReason !== undefined) return false;
|
|
571
|
+
if (membership?.renderState !== undefined) return false;
|
|
572
|
+
// Older synthetic membership fixtures omit the producer flag and remain
|
|
573
|
+
// compatible; extracted records always carry it. A false flag is an
|
|
574
|
+
// authored/producer decision and must keep the draw on its CPU lane.
|
|
575
|
+
return membership?.gpuDrivenEligible !== false;
|
|
576
|
+
}
|
|
577
|
+
|
|
344
578
|
function selectedLodLevel(candidate: GpuDrivenCandidate): number {
|
|
345
579
|
const coverages = candidate.lodCoverages;
|
|
346
580
|
if (coverages === undefined || coverages.length <= 1) return 0;
|
|
@@ -368,28 +602,41 @@ function selectedLodRange(
|
|
|
368
602
|
);
|
|
369
603
|
}
|
|
370
604
|
|
|
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
605
|
function admittedRasterBatchCount(plan: SubmissionPlan): number {
|
|
377
606
|
return plan.batches.reduce((count, batch) => count + (batch.visibleCapacity > 0 ? 1 : 0), 0);
|
|
378
607
|
}
|
|
379
608
|
|
|
609
|
+
function gpuDrawEntityCount(drawKeys: ReadonlySet<string>): number {
|
|
610
|
+
const entities = new Set<string>();
|
|
611
|
+
for (const key of drawKeys) {
|
|
612
|
+
const separator = key.indexOf(':');
|
|
613
|
+
if (separator > 0) entities.add(key.slice(0, separator));
|
|
614
|
+
}
|
|
615
|
+
return entities.size;
|
|
616
|
+
}
|
|
617
|
+
|
|
380
618
|
function filteredPlan(
|
|
381
619
|
source: SubmissionPlan,
|
|
382
620
|
slots: PersistentGpuDrivenState['slots'],
|
|
383
|
-
|
|
384
|
-
|
|
621
|
+
meshBySlot: ReadonlyMap<number, MeshGpuHandles>,
|
|
622
|
+
shadowCasterDrawKeys?: ReadonlySet<string>,
|
|
623
|
+
skinArtifact?: MaterialShaderArtifact,
|
|
624
|
+
shadowCasterMembership?: readonly ShadowCasterMembership[],
|
|
625
|
+
camera?: CameraSnapshot,
|
|
385
626
|
activeEntityKeys?: ReadonlySet<number>,
|
|
386
627
|
worldKeys?: readonly number[],
|
|
628
|
+
materialArtifact?: MaterialShaderArtifact,
|
|
629
|
+
materialArtifacts?: ReadonlyMap<string, MaterialShaderArtifact>,
|
|
630
|
+
materialBindingClasses?: ReadonlyMap<string, string>,
|
|
387
631
|
): {
|
|
388
632
|
readonly plan: SubmissionPlan;
|
|
389
633
|
readonly batches: readonly PreparedBatch[];
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
634
|
+
/** True only when every source draw item/instance has a main GPU candidate. */
|
|
635
|
+
readonly ownsAllDrawItems: boolean;
|
|
636
|
+
/** Concrete entity/material/draw identities claimed by this projection. */
|
|
637
|
+
readonly drawKeys: ReadonlySet<string>;
|
|
638
|
+
/** Extracted concrete ShadowCaster keys actually claimed by this plan. */
|
|
639
|
+
readonly shadowDrawKeys: ReadonlySet<string>;
|
|
393
640
|
} {
|
|
394
641
|
const slotByPrimitive = new Map(slots.map((slot) => [slot.slot, slot] as const));
|
|
395
642
|
const participatingByPrimitive = new Map<number, Set<GpuDrivenBatch>>();
|
|
@@ -403,47 +650,79 @@ function filteredPlan(
|
|
|
403
650
|
}
|
|
404
651
|
}
|
|
405
652
|
}
|
|
406
|
-
const
|
|
653
|
+
const eligibleMainCandidates = new Set<string>();
|
|
654
|
+
const eligibleArtifacts = new Map<string, MaterialShaderArtifact>();
|
|
655
|
+
const drawKeys = new Set<string>();
|
|
656
|
+
const eligibleShadowCandidates = new Set<string>();
|
|
657
|
+
const shadowDrawKeys = new Set<string>();
|
|
658
|
+
const shadowSelectionProvided =
|
|
659
|
+
shadowCasterDrawKeys !== undefined || shadowCasterMembership !== undefined;
|
|
407
660
|
const worldKeyFor = (slot: PersistentGpuDrivenState['slots'][number]): number =>
|
|
408
661
|
worldKeys?.[slot.snapshot.worldId] ?? slot.snapshot.worldId;
|
|
409
662
|
const activeKey = (slot: PersistentGpuDrivenState['slots'][number]): number =>
|
|
410
663
|
worldEntityKey(worldKeyFor(slot), slot.snapshot.entityKey);
|
|
411
|
-
const eligiblePrimitives = new Set<number>();
|
|
412
|
-
let topologyEligiblePrimitiveCount = 0;
|
|
413
664
|
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
|
-
|
|
665
|
+
const mesh = meshBySlot.get(slot.slot);
|
|
666
|
+
const participating = participatingByPrimitive.get(slot.slot) ?? new Set<GpuDrivenBatch>();
|
|
667
|
+
for (const batch of participating) {
|
|
668
|
+
for (const candidate of batch.candidates) {
|
|
669
|
+
if (candidate.primitiveIndex !== slot.slot) continue;
|
|
670
|
+
const draw = sourceDrawForCandidate(slot, candidate);
|
|
671
|
+
const selectedArtifact = artifactForPreparedCandidate(
|
|
672
|
+
candidate,
|
|
673
|
+
materialArtifacts,
|
|
674
|
+
materialArtifact,
|
|
675
|
+
skinArtifact,
|
|
676
|
+
);
|
|
677
|
+
const mainEligible =
|
|
678
|
+
draw !== undefined &&
|
|
679
|
+
preparedPbrCandidateEligible(slot.snapshot, batch, candidate, selectedArtifact) &&
|
|
680
|
+
mesh !== undefined &&
|
|
681
|
+
meshSupportsBatch(mesh, batch);
|
|
682
|
+
const candidateKey = shadowCandidateKey(candidate);
|
|
683
|
+
if (mainEligible && selectedArtifact !== undefined) {
|
|
684
|
+
eligibleArtifacts.set(candidateKey, selectedArtifact);
|
|
685
|
+
}
|
|
686
|
+
if (!shadowSelectionProvided && mainEligible && draw !== undefined) {
|
|
687
|
+
eligibleMainCandidates.add(candidateKey);
|
|
688
|
+
const material = slot.snapshot.materials[draw.materialSlot] ?? slot.snapshot.material;
|
|
689
|
+
drawKeys.add(
|
|
690
|
+
gpuDrivenDrawKey(
|
|
691
|
+
worldEntityKey(worldKeyFor(slot), slot.snapshot.entityKey),
|
|
692
|
+
material.materialHandle ?? -1,
|
|
693
|
+
candidate.drawItemIndex,
|
|
694
|
+
),
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
if (
|
|
698
|
+
shadowSelectionProvided &&
|
|
699
|
+
mainEligible &&
|
|
700
|
+
shadowMembershipIsGpuCompatible(
|
|
701
|
+
slot,
|
|
702
|
+
candidate,
|
|
703
|
+
shadowCasterDrawKeys,
|
|
704
|
+
shadowCasterMembership,
|
|
705
|
+
)
|
|
706
|
+
) {
|
|
707
|
+
eligibleShadowCandidates.add(shadowCandidateKey(candidate));
|
|
708
|
+
for (const key of shadowKeysForCandidate(
|
|
709
|
+
slot,
|
|
710
|
+
candidate,
|
|
711
|
+
shadowCasterDrawKeys,
|
|
712
|
+
shadowCasterMembership,
|
|
713
|
+
)) {
|
|
714
|
+
shadowDrawKeys.add(key);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
433
717
|
}
|
|
434
718
|
}
|
|
435
719
|
}
|
|
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
720
|
const groups = new Map<
|
|
443
721
|
string,
|
|
444
722
|
{
|
|
445
723
|
readonly source: GpuDrivenBatch;
|
|
446
724
|
readonly mesh: MeshGpuHandles;
|
|
725
|
+
readonly artifact: MaterialShaderArtifact;
|
|
447
726
|
readonly renderState: MaterialRenderState | undefined;
|
|
448
727
|
readonly candidates: GpuDrivenCandidate[];
|
|
449
728
|
}
|
|
@@ -451,23 +730,34 @@ function filteredPlan(
|
|
|
451
730
|
for (const batch of source.batches) {
|
|
452
731
|
const candidates = batch.candidates.filter((candidate) => {
|
|
453
732
|
const slot = slotByPrimitive.get(candidate.primitiveIndex);
|
|
454
|
-
if (slot === undefined
|
|
455
|
-
|
|
733
|
+
if (slot === undefined) return false;
|
|
734
|
+
if (
|
|
735
|
+
!shadowSelectionProvided
|
|
736
|
+
? !eligibleMainCandidates.has(shadowCandidateKey(candidate))
|
|
737
|
+
: !eligibleShadowCandidates.has(shadowCandidateKey(candidate))
|
|
738
|
+
)
|
|
739
|
+
return false;
|
|
740
|
+
const mesh = meshBySlot.get(slot.slot);
|
|
456
741
|
return mesh !== undefined && meshSupportsBatch(mesh, batch);
|
|
457
742
|
});
|
|
458
743
|
if (candidates.length === 0) continue;
|
|
459
744
|
for (const candidate of candidates) {
|
|
460
745
|
const slot = slotByPrimitive.get(candidate.primitiveIndex);
|
|
461
746
|
if (slot === undefined) continue;
|
|
462
|
-
const mesh =
|
|
747
|
+
const mesh = meshBySlot.get(slot.slot);
|
|
463
748
|
if (mesh === undefined) continue;
|
|
749
|
+
const artifact = eligibleArtifacts.get(shadowCandidateKey(candidate));
|
|
750
|
+
if (artifact === undefined) continue;
|
|
464
751
|
const physicalLodRanges = mesh.lodRanges
|
|
465
752
|
?.slice(1)
|
|
466
753
|
.map((ranges) => ranges[candidate.drawItemIndex]);
|
|
467
754
|
const projected = Object.freeze({
|
|
468
755
|
...candidate,
|
|
469
756
|
submitAdmission: activeEntityKeys === undefined || activeEntityKeys.has(activeKey(slot)),
|
|
470
|
-
projectedHeight:
|
|
757
|
+
projectedHeight:
|
|
758
|
+
camera === undefined
|
|
759
|
+
? (candidate.projectedHeight ?? Number.NaN)
|
|
760
|
+
: projectedHeightForCandidate(slot, camera),
|
|
471
761
|
...(physicalLodRanges?.every((range) => range !== undefined)
|
|
472
762
|
? { lodRanges: physicalLodRanges as NonNullable<typeof candidate.lodRanges> }
|
|
473
763
|
: {}),
|
|
@@ -475,7 +765,23 @@ function filteredPlan(
|
|
|
475
765
|
const selectedRange = selectedLodRange(batch, projected);
|
|
476
766
|
const groupKey = JSON.stringify({
|
|
477
767
|
key: batch.key,
|
|
768
|
+
artifact: [
|
|
769
|
+
artifact.material,
|
|
770
|
+
artifact.layoutIdentity,
|
|
771
|
+
artifact.receipt?.receiptIdentity ?? '',
|
|
772
|
+
artifact.receipt?.generation ?? 0,
|
|
773
|
+
artifact.variantSet ?? '',
|
|
774
|
+
materialArtifactProgramIdentity(artifact),
|
|
775
|
+
],
|
|
478
776
|
worldKey: worldKeyFor(slot),
|
|
777
|
+
bindingClass:
|
|
778
|
+
materialBindingClasses?.get(
|
|
779
|
+
materialBindingKey(
|
|
780
|
+
slot.snapshot,
|
|
781
|
+
(slot.snapshot.materials[batch.key.materialSlot] ?? slot.snapshot.material)
|
|
782
|
+
.materialHandle ?? -1,
|
|
783
|
+
),
|
|
784
|
+
) ?? '',
|
|
479
785
|
lodCoverages: projected.lodCoverages ?? null,
|
|
480
786
|
lodRanges: projected.lodRanges ?? null,
|
|
481
787
|
selectedRange,
|
|
@@ -487,6 +793,7 @@ function filteredPlan(
|
|
|
487
793
|
groups.set(groupKey, {
|
|
488
794
|
source: batch,
|
|
489
795
|
mesh,
|
|
796
|
+
artifact,
|
|
490
797
|
renderState: geometryRenderStateForTopology(batch.key.topology, material.renderState),
|
|
491
798
|
candidates: [projected],
|
|
492
799
|
});
|
|
@@ -495,7 +802,6 @@ function filteredPlan(
|
|
|
495
802
|
}
|
|
496
803
|
}
|
|
497
804
|
}
|
|
498
|
-
|
|
499
805
|
const batches: PreparedBatch[] = [];
|
|
500
806
|
let visibleBase = 0;
|
|
501
807
|
const filteredBatches: GpuDrivenBatch[] = [];
|
|
@@ -521,35 +827,190 @@ function filteredPlan(
|
|
|
521
827
|
});
|
|
522
828
|
visibleBase += admittedCount;
|
|
523
829
|
filteredBatches.push(filtered);
|
|
524
|
-
|
|
830
|
+
// Keep every selector batch id unique even when no current entity is
|
|
831
|
+
// admitted to raster. The zero-admission batch still feeds LOD telemetry,
|
|
832
|
+
// while the following raster batch must not reuse its indirect slot.
|
|
833
|
+
batchId += 1;
|
|
834
|
+
// A selector batch can retain facet-suppressed candidates for LOD
|
|
835
|
+
// telemetry. Raster ownership, however, must be represented by an
|
|
836
|
+
// admitted candidate; using the first retained row can point material
|
|
837
|
+
// routing at an entity that the main-pass producer intentionally omitted.
|
|
838
|
+
const firstCandidate = group.candidates.find(
|
|
839
|
+
(candidate) =>
|
|
840
|
+
(candidate as GpuDrivenCandidate & { readonly submitAdmission?: boolean })
|
|
841
|
+
.submitAdmission !== false,
|
|
842
|
+
);
|
|
525
843
|
const firstSlot =
|
|
526
844
|
firstCandidate === undefined ? undefined : slotByPrimitive.get(firstCandidate.primitiveIndex);
|
|
527
845
|
if (firstCandidate === undefined || firstSlot === undefined) continue;
|
|
528
846
|
batches.push({
|
|
529
847
|
batch: filtered,
|
|
530
848
|
mesh: group.mesh,
|
|
849
|
+
artifact: group.artifact,
|
|
531
850
|
renderState: group.renderState,
|
|
532
851
|
worldKey: worldKeyFor(firstSlot),
|
|
533
852
|
primitiveSlot: firstSlot.slot,
|
|
534
853
|
slotGeneration: firstSlot.generation,
|
|
535
854
|
});
|
|
536
|
-
batchId += 1;
|
|
537
855
|
}
|
|
856
|
+
const ownershipSlots =
|
|
857
|
+
activeEntityKeys === undefined
|
|
858
|
+
? slots
|
|
859
|
+
: slots.filter((slot) => activeEntityKeys.has(activeKey(slot)));
|
|
860
|
+
const ownsAllDrawItems =
|
|
861
|
+
!shadowSelectionProvided &&
|
|
862
|
+
ownershipSlots.every((slot) => {
|
|
863
|
+
const draws = slot.snapshot.gpuDrivenDraws ?? [];
|
|
864
|
+
if (draws.length === 0) return false;
|
|
865
|
+
const instanceCount = slot.snapshot.instances?.instanceCount ?? 1;
|
|
866
|
+
const candidates = source.batches.flatMap((batch) =>
|
|
867
|
+
batch.candidates.filter((candidate) => candidate.primitiveIndex === slot.slot),
|
|
868
|
+
);
|
|
869
|
+
return (
|
|
870
|
+
candidates.length === draws.length * instanceCount &&
|
|
871
|
+
candidates.every((candidate) => {
|
|
872
|
+
if (!eligibleMainCandidates.has(shadowCandidateKey(candidate))) return false;
|
|
873
|
+
const draw = sourceDrawForCandidate(slot, candidate);
|
|
874
|
+
if (draw === undefined) return false;
|
|
875
|
+
const material = slot.snapshot.materials[draw.materialSlot] ?? slot.snapshot.material;
|
|
876
|
+
return drawKeys.has(
|
|
877
|
+
gpuDrivenDrawKey(
|
|
878
|
+
worldEntityKey(worldKeyFor(slot), slot.snapshot.entityKey),
|
|
879
|
+
material.materialHandle ?? -1,
|
|
880
|
+
candidate.drawItemIndex,
|
|
881
|
+
),
|
|
882
|
+
);
|
|
883
|
+
})
|
|
884
|
+
);
|
|
885
|
+
});
|
|
538
886
|
return {
|
|
539
887
|
plan: Object.freeze({
|
|
540
888
|
revision: source.revision,
|
|
889
|
+
payloadRevision: source.payloadRevision,
|
|
541
890
|
batches: Object.freeze(filteredBatches),
|
|
542
891
|
candidateCount: filteredBatches.reduce((total, batch) => total + batch.candidates.length, 0),
|
|
543
892
|
visibleCapacity: visibleBase,
|
|
544
893
|
}),
|
|
545
894
|
batches: Object.freeze(batches),
|
|
546
|
-
|
|
547
|
-
|
|
895
|
+
ownsAllDrawItems,
|
|
896
|
+
drawKeys,
|
|
897
|
+
shadowDrawKeys,
|
|
548
898
|
};
|
|
549
899
|
}
|
|
550
900
|
|
|
901
|
+
function shadowCasterMembershipSignature(
|
|
902
|
+
drawKeys: ReadonlySet<string> | undefined,
|
|
903
|
+
membership: readonly ShadowCasterMembership[] | undefined,
|
|
904
|
+
): string {
|
|
905
|
+
const keySignature = drawKeys === undefined ? '*' : [...drawKeys].sort().join(',');
|
|
906
|
+
if (membership === undefined) return keySignature;
|
|
907
|
+
const membershipSignature = membership
|
|
908
|
+
.map((entry) =>
|
|
909
|
+
[
|
|
910
|
+
entry.worldEntity,
|
|
911
|
+
entry.renderableIndex,
|
|
912
|
+
entry.drawItemIndex,
|
|
913
|
+
entry.materialHandle,
|
|
914
|
+
entry.passIndex,
|
|
915
|
+
entry.materialShaderId ?? '',
|
|
916
|
+
JSON.stringify(entry.renderState ?? null),
|
|
917
|
+
entry.cpuReason ?? '',
|
|
918
|
+
entry.gpuDrivenEligible === undefined ? '' : String(entry.gpuDrivenEligible),
|
|
919
|
+
].join(':'),
|
|
920
|
+
)
|
|
921
|
+
.sort()
|
|
922
|
+
.join(',');
|
|
923
|
+
return `${keySignature}|membership=${membershipSignature}`;
|
|
924
|
+
}
|
|
925
|
+
|
|
551
926
|
type FilteredProductionPlan = ReturnType<typeof filteredPlan>;
|
|
552
927
|
|
|
928
|
+
function sceneRequiresGpuDrivenArtifact(scene: PersistentGpuDrivenState): boolean {
|
|
929
|
+
return scene.plan.batches.some((batch) =>
|
|
930
|
+
batch.candidates.some((candidate) => {
|
|
931
|
+
return (
|
|
932
|
+
candidate.prepared?.receiptIdentity !== undefined &&
|
|
933
|
+
(candidate.prepared?.identity.deformation === 'rigid' ||
|
|
934
|
+
candidate.prepared?.identity.deformation === 'skin')
|
|
935
|
+
);
|
|
936
|
+
}),
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function missingMaterialArtifact(
|
|
941
|
+
scene: PersistentGpuDrivenState,
|
|
942
|
+
artifacts: ReadonlyMap<string, MaterialShaderArtifact> | undefined,
|
|
943
|
+
rigidFallback: MaterialShaderArtifact | undefined,
|
|
944
|
+
skinFallback: MaterialShaderArtifact | undefined,
|
|
945
|
+
): GpuDrivenPreparationError | undefined {
|
|
946
|
+
for (const batch of scene.plan.batches) {
|
|
947
|
+
for (const candidate of batch.candidates) {
|
|
948
|
+
const prepared = candidate.prepared;
|
|
949
|
+
if (prepared?.receiptIdentity === undefined) continue;
|
|
950
|
+
if (
|
|
951
|
+
artifactForPreparedCandidate(candidate, artifacts, rigidFallback, skinFallback) !==
|
|
952
|
+
undefined
|
|
953
|
+
)
|
|
954
|
+
continue;
|
|
955
|
+
return new GpuDrivenPreparationError('missing-material-receipt', {
|
|
956
|
+
reason: 'material-receipt-missing',
|
|
957
|
+
owner: 'material',
|
|
958
|
+
expected: `published artifact for ${prepared.identity.material} (${prepared.receiptIdentity}@${prepared.receiptGeneration})`,
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
return undefined;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function nonCpuPreparationFailure(
|
|
966
|
+
scene: PersistentGpuDrivenState,
|
|
967
|
+
): { readonly error: GpuDrivenPreparationError; readonly drawKeys: readonly string[] } | undefined {
|
|
968
|
+
const drawKeys: string[] = [];
|
|
969
|
+
let first: GpuDrivenPreparationError | undefined;
|
|
970
|
+
for (const slot of scene.slots) {
|
|
971
|
+
const draws = slot.snapshot.gpuDrivenDraws ?? [];
|
|
972
|
+
for (const [compactIndex, draw] of draws.entries()) {
|
|
973
|
+
const error = draw.preparationError;
|
|
974
|
+
if (error === undefined || error.detail.recovery === 'route-cpu-lane') continue;
|
|
975
|
+
const material = slot.snapshot.materials[draw.materialSlot] ?? slot.snapshot.material;
|
|
976
|
+
drawKeys.push(
|
|
977
|
+
gpuDrivenDrawKey(
|
|
978
|
+
worldEntityKey(slot.snapshot.worldId, slot.snapshot.entityKey),
|
|
979
|
+
material.materialHandle ?? -1,
|
|
980
|
+
gpuDrivenSourceDrawItemIndex(draw, compactIndex),
|
|
981
|
+
),
|
|
982
|
+
);
|
|
983
|
+
first ??= error;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
return first === undefined ? undefined : { error: first, drawKeys };
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function preparationFailureInspection(
|
|
990
|
+
error: GpuDrivenPreparationError,
|
|
991
|
+
): GpuDrivenPreparationFailureInspection {
|
|
992
|
+
return Object.freeze({
|
|
993
|
+
code: error.code,
|
|
994
|
+
expected: error.expected,
|
|
995
|
+
hint: error.hint,
|
|
996
|
+
detail: error.detail,
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function shadowOwnershipFailureInspection(
|
|
1001
|
+
viewPass: GpuDrivenLaneSummary['viewPass'],
|
|
1002
|
+
): GpuDrivenPreparationFailureInspection {
|
|
1003
|
+
return preparationFailureInspection(
|
|
1004
|
+
new GpuDrivenPreparationError('shadow-ownership', {
|
|
1005
|
+
reason: 'shadow-ownership-missing',
|
|
1006
|
+
owner: 'shadow',
|
|
1007
|
+
expected: 'one GPU-compatible owner for every declared ShadowCaster pass',
|
|
1008
|
+
actual: `${viewPass} contains a residual ShadowCaster draw item`,
|
|
1009
|
+
recovery: 'route-cpu-lane',
|
|
1010
|
+
}),
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
553
1014
|
export interface GpuDrivenWorldLodSelectionInspection {
|
|
554
1015
|
readonly worldKey: number;
|
|
555
1016
|
readonly primitiveSlot: number;
|
|
@@ -570,302 +1031,537 @@ function sameEntityKeySet(
|
|
|
570
1031
|
return true;
|
|
571
1032
|
}
|
|
572
1033
|
|
|
573
|
-
|
|
574
|
-
private readonly pipelines = new Map<string, RenderPipeline>();
|
|
575
|
-
|
|
576
|
-
private constructor(
|
|
577
|
-
private readonly device: RhiDevice,
|
|
578
|
-
private readonly layout: BindGroupLayout,
|
|
579
|
-
private readonly pipelineLayout: import('@forgeax/engine-rhi').PipelineLayout,
|
|
580
|
-
private readonly module: ShaderModule,
|
|
581
|
-
) {}
|
|
1034
|
+
type MaterialPipelineFactory = NonNullable<RenderSystemInternals['getMaterialShaderPipelineEntry']>;
|
|
582
1035
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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;
|
|
617
|
-
return ok(
|
|
618
|
-
new GpuDrivenRigidRaster(input.device, layout.value, pipelineLayout.value, module.value),
|
|
619
|
-
);
|
|
620
|
-
}
|
|
1036
|
+
function artifactForPreparedCandidate(
|
|
1037
|
+
candidate: GpuDrivenBatch['candidates'][number],
|
|
1038
|
+
artifacts: ReadonlyMap<string, MaterialShaderArtifact> | undefined,
|
|
1039
|
+
rigidFallback: MaterialShaderArtifact | undefined,
|
|
1040
|
+
skinFallback: MaterialShaderArtifact | undefined,
|
|
1041
|
+
): MaterialShaderArtifact | undefined {
|
|
1042
|
+
const prepared = candidate.prepared;
|
|
1043
|
+
if (prepared === undefined) return undefined;
|
|
1044
|
+
const exact = artifacts?.get(
|
|
1045
|
+
gpuDrivenMaterialArtifactKey({
|
|
1046
|
+
material: prepared.identity.material,
|
|
1047
|
+
deformation: prepared.identity.deformation,
|
|
1048
|
+
receiptIdentity: prepared.receiptIdentity,
|
|
1049
|
+
receiptGeneration: prepared.receiptGeneration,
|
|
1050
|
+
}),
|
|
1051
|
+
);
|
|
1052
|
+
if (exact !== undefined) return exact;
|
|
1053
|
+
// A map supplied by the real record path is authoritative. A receipt-backed
|
|
1054
|
+
// draw missing from it is a producer/load failure, not permission to pair
|
|
1055
|
+
// the draw with another material's artifact.
|
|
1056
|
+
if (artifacts !== undefined && prepared.receiptIdentity !== undefined) return undefined;
|
|
1057
|
+
const fallback = prepared.identity.deformation === 'skin' ? skinFallback : rigidFallback;
|
|
1058
|
+
return fallback?.material === prepared.identity.material ? fallback : undefined;
|
|
1059
|
+
}
|
|
621
1060
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
},
|
|
645
|
-
],
|
|
646
|
-
},
|
|
647
|
-
fragment: {
|
|
648
|
-
module: this.module,
|
|
649
|
-
entryPoint: 'fs_main',
|
|
650
|
-
targets: [
|
|
651
|
-
{
|
|
652
|
-
format,
|
|
653
|
-
...(renderState?.blend === undefined ? {} : { blend: renderState.blend }),
|
|
654
|
-
},
|
|
655
|
-
],
|
|
656
|
-
},
|
|
657
|
-
primitive: {
|
|
658
|
-
topology,
|
|
659
|
-
cullMode: renderState?.cullMode ?? 'back',
|
|
660
|
-
frontFace: renderState?.frontFace ?? 'ccw',
|
|
661
|
-
...(stripIndexFormat === undefined ? {} : { stripIndexFormat }),
|
|
662
|
-
},
|
|
663
|
-
depthStencil: {
|
|
664
|
-
format: 'depth24plus-stencil8',
|
|
665
|
-
depthWriteEnabled: renderState?.depthWriteEnabled ?? true,
|
|
666
|
-
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
|
-
},
|
|
676
|
-
...(sampleCount === 4
|
|
677
|
-
? {
|
|
678
|
-
multisample: {
|
|
679
|
-
count: 4,
|
|
680
|
-
alphaToCoverageEnabled: renderState?.alphaToCoverageEnabled ?? false,
|
|
681
|
-
},
|
|
682
|
-
}
|
|
683
|
-
: {}),
|
|
684
|
-
});
|
|
685
|
-
if (!created.ok) return created;
|
|
686
|
-
this.pipelines.set(key, created.value);
|
|
687
|
-
return created;
|
|
688
|
-
}
|
|
1061
|
+
function shadowArtifactForPreparedCandidate(
|
|
1062
|
+
candidate: GpuDrivenBatch['candidates'][number],
|
|
1063
|
+
artifacts: ReadonlyMap<string, MaterialShaderArtifact> | undefined,
|
|
1064
|
+
fallback: MaterialShaderArtifact | undefined,
|
|
1065
|
+
): MaterialShaderArtifact | undefined {
|
|
1066
|
+
const prepared = candidate.prepared;
|
|
1067
|
+
if (prepared === undefined) return undefined;
|
|
1068
|
+
const exact = artifacts?.get(
|
|
1069
|
+
gpuDrivenMaterialArtifactKey({
|
|
1070
|
+
material: prepared.identity.material,
|
|
1071
|
+
deformation: prepared.identity.deformation,
|
|
1072
|
+
receiptIdentity: prepared.receiptIdentity,
|
|
1073
|
+
receiptGeneration: prepared.receiptGeneration,
|
|
1074
|
+
}),
|
|
1075
|
+
);
|
|
1076
|
+
if (exact !== undefined) return exact;
|
|
1077
|
+
// Direct production fixtures predate the separate shadow publication map.
|
|
1078
|
+
// They are allowed to reuse the already validated artifact; a real frame
|
|
1079
|
+
// passes the authoritative map and therefore fails closed on a missing
|
|
1080
|
+
// shadow program instead of silently selecting the forward program.
|
|
1081
|
+
return artifacts === undefined ? fallback : undefined;
|
|
1082
|
+
}
|
|
689
1083
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
1084
|
+
function materialArtifactSignature(
|
|
1085
|
+
artifacts: ReadonlyMap<string, MaterialShaderArtifact> | undefined,
|
|
1086
|
+
rigidFallback: MaterialShaderArtifact | undefined,
|
|
1087
|
+
skinFallback: MaterialShaderArtifact | undefined,
|
|
1088
|
+
): string {
|
|
1089
|
+
const rows =
|
|
1090
|
+
artifacts === undefined
|
|
1091
|
+
? []
|
|
1092
|
+
: [...artifacts.entries()]
|
|
1093
|
+
.map(([key, artifact]) => [
|
|
1094
|
+
key,
|
|
1095
|
+
artifact.material,
|
|
1096
|
+
artifact.layoutIdentity,
|
|
1097
|
+
artifact.receipt?.receiptIdentity ?? '',
|
|
1098
|
+
artifact.receipt?.generation ?? 0,
|
|
1099
|
+
artifact.variantSet ?? '',
|
|
1100
|
+
materialArtifactProgramIdentity(artifact),
|
|
1101
|
+
artifact.vertexEntry ?? artifact.receipt?.sceneIndexEntry ?? '',
|
|
1102
|
+
artifact.fragmentEntry ?? '',
|
|
1103
|
+
])
|
|
1104
|
+
.sort((left, right) => String(left[0] ?? '').localeCompare(String(right[0] ?? '')));
|
|
1105
|
+
return JSON.stringify({
|
|
1106
|
+
rows,
|
|
1107
|
+
rigid:
|
|
1108
|
+
rigidFallback === undefined
|
|
1109
|
+
? undefined
|
|
1110
|
+
: [
|
|
1111
|
+
rigidFallback.material,
|
|
1112
|
+
rigidFallback.layoutIdentity,
|
|
1113
|
+
rigidFallback.receipt?.receiptIdentity ?? '',
|
|
1114
|
+
rigidFallback.receipt?.generation ?? 0,
|
|
1115
|
+
rigidFallback.variantSet ?? '',
|
|
1116
|
+
materialArtifactProgramIdentity(rigidFallback),
|
|
1117
|
+
rigidFallback.vertexEntry ?? rigidFallback.receipt?.sceneIndexEntry ?? '',
|
|
1118
|
+
rigidFallback.fragmentEntry ?? '',
|
|
1119
|
+
],
|
|
1120
|
+
skin:
|
|
1121
|
+
skinFallback === undefined
|
|
1122
|
+
? undefined
|
|
1123
|
+
: [
|
|
1124
|
+
skinFallback.material,
|
|
1125
|
+
skinFallback.layoutIdentity,
|
|
1126
|
+
skinFallback.receipt?.receiptIdentity ?? '',
|
|
1127
|
+
skinFallback.receipt?.generation ?? 0,
|
|
1128
|
+
skinFallback.variantSet ?? '',
|
|
1129
|
+
materialArtifactProgramIdentity(skinFallback),
|
|
1130
|
+
skinFallback.vertexEntry ?? skinFallback.receipt?.sceneIndexEntry ?? '',
|
|
1131
|
+
skinFallback.fragmentEntry ?? '',
|
|
1132
|
+
],
|
|
1133
|
+
});
|
|
723
1134
|
}
|
|
724
1135
|
|
|
725
1136
|
/** The GPU-driven bridge to the existing four-group Standard PBR pipeline. */
|
|
726
|
-
class
|
|
1137
|
+
class MaterialAbiRasterAdapter {
|
|
727
1138
|
private readonly pipelines = new Map<string, RenderPipeline>();
|
|
728
|
-
private readonly projectionBuffers
|
|
1139
|
+
private readonly projectionBuffers = new Set<Buffer>();
|
|
1140
|
+
private readonly projectionBindings = new Map<
|
|
1141
|
+
string,
|
|
1142
|
+
{
|
|
1143
|
+
readonly buffer: Buffer;
|
|
1144
|
+
readonly bindGroup: BindGroup;
|
|
1145
|
+
readonly capacityBytes: number;
|
|
1146
|
+
rows: Float32Array;
|
|
1147
|
+
}
|
|
1148
|
+
>();
|
|
1149
|
+
/**
|
|
1150
|
+
* Projection rows are graph-generation resources. A new topology may stage
|
|
1151
|
+
* a replacement before the graph is promoted; keeping staged bindings out
|
|
1152
|
+
* of the active map lets an aborted graph leave the last submitted rows
|
|
1153
|
+
* intact. Superseded buffers are fence-retired on promotion.
|
|
1154
|
+
*/
|
|
1155
|
+
private readonly stagedProjectionBindings = new Map<
|
|
1156
|
+
string,
|
|
1157
|
+
{
|
|
1158
|
+
readonly buffer: Buffer;
|
|
1159
|
+
readonly bindGroup: BindGroup;
|
|
1160
|
+
readonly capacityBytes: number;
|
|
1161
|
+
rows: Float32Array;
|
|
1162
|
+
}
|
|
1163
|
+
>();
|
|
1164
|
+
private readonly pendingProjectionRetirements = new Set<Buffer>();
|
|
1165
|
+
private readonly destroyedProjectionBuffers = new Set<Buffer>();
|
|
1166
|
+
private projectionDisposed = false;
|
|
1167
|
+
private stagedProjectionKeys = new Set<string>();
|
|
1168
|
+
private readonly activeProjectionRowsAtStage = new Map<string, Float32Array>();
|
|
1169
|
+
private readonly visibleWindowBindGroups = new Map<Buffer, Map<string, BindGroup>>();
|
|
729
1170
|
|
|
730
1171
|
private constructor(
|
|
731
1172
|
private readonly device: RhiDevice,
|
|
732
1173
|
private readonly pipelineLayout: import('@forgeax/engine-rhi').PipelineLayout,
|
|
733
1174
|
private readonly meshLayout: BindGroupLayout,
|
|
734
|
-
private readonly
|
|
735
|
-
private readonly
|
|
736
|
-
private
|
|
1175
|
+
private readonly instancesBindGroupLayout: BindGroupLayout,
|
|
1176
|
+
private readonly programs: ReadonlyMap<string, GpuDrivenPbrProgram> | undefined,
|
|
1177
|
+
private instancesBindGroup: BindGroup,
|
|
1178
|
+
private readonly instanceBuffer: Buffer,
|
|
1179
|
+
private readonly instanceBufferSize: number,
|
|
1180
|
+
private visibleBuffer: Buffer,
|
|
737
1181
|
private readonly artifact: MaterialShaderArtifact,
|
|
1182
|
+
private readonly deformation: 'rigid' | 'skin',
|
|
1183
|
+
private readonly pipelineFactory: MaterialPipelineFactory | undefined,
|
|
1184
|
+
private paletteBuffer: Buffer | undefined,
|
|
1185
|
+
private paletteBindingWindowBytes: number | undefined,
|
|
738
1186
|
) {}
|
|
739
1187
|
|
|
740
1188
|
static create(input: {
|
|
741
1189
|
readonly device: RhiDevice;
|
|
742
|
-
readonly shaderModuleFactory: PipelineBuilderShaderModuleFactory;
|
|
743
1190
|
readonly pipelineState: PipelineState;
|
|
744
1191
|
readonly artifact: MaterialShaderArtifact;
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
1192
|
+
readonly visibleBuffer: Buffer;
|
|
1193
|
+
readonly deformation?: 'rigid' | 'skin';
|
|
1194
|
+
readonly paletteBuffer?: Buffer;
|
|
1195
|
+
readonly paletteBindingWindowBytes?: number;
|
|
1196
|
+
/** Renderer-owned factory for the selected artifact's actual PSO. */
|
|
1197
|
+
readonly pipelineFactory?: MaterialPipelineFactory;
|
|
1198
|
+
}): Result<MaterialAbiRasterAdapter, RhiError | GpuDrivenPreparationError> {
|
|
1199
|
+
const deformation = input.deformation ?? 'rigid';
|
|
1200
|
+
const receipt = input.artifact.receipt;
|
|
1201
|
+
if (receipt === undefined) {
|
|
749
1202
|
return err(
|
|
750
1203
|
new RhiError({
|
|
751
1204
|
code: 'rhi-not-available',
|
|
752
|
-
expected: '
|
|
753
|
-
hint: '
|
|
1205
|
+
expected: 'published MaterialProgramAbi receipt for the selected material artifact',
|
|
1206
|
+
hint: 'recook and publish the material before creating a GPU-driven raster owner',
|
|
754
1207
|
}),
|
|
755
1208
|
);
|
|
756
1209
|
}
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
1210
|
+
const materialRowMatchesSceneTable =
|
|
1211
|
+
receipt.materialRow.byteLength === GPU_SCENE_LAYOUTS.material.stride &&
|
|
1212
|
+
new Set(receipt.materialRow.fields).size === receipt.materialRow.fields.length;
|
|
1213
|
+
if (!materialRowMatchesSceneTable) {
|
|
1214
|
+
return err(
|
|
1215
|
+
new GpuDrivenPreparationError('material-abi-not-executable', {
|
|
1216
|
+
reason: 'material-abi-not-executable',
|
|
1217
|
+
owner: 'material',
|
|
1218
|
+
expected: `scene-index material row ${GPU_SCENE_LAYOUTS.material.stride} bytes with unique producer fields`,
|
|
1219
|
+
actual: `row ${receipt.materialRow.byteLength} bytes with fields ${receipt.materialRow.fields.join(',')}`,
|
|
1220
|
+
}),
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
if (
|
|
1224
|
+
receipt.reflection.layoutIdentity !== input.artifact.layoutIdentity ||
|
|
1225
|
+
receipt.directEntry.length === 0 ||
|
|
1226
|
+
receipt.sceneIndexEntry.length === 0 ||
|
|
1227
|
+
receipt.resourceSlots.some((slot) => slot.group !== 1)
|
|
1228
|
+
) {
|
|
1229
|
+
return err(
|
|
1230
|
+
new RhiError({
|
|
1231
|
+
code: 'rhi-descriptor-invalid',
|
|
1232
|
+
expected: `complete selected MaterialProgramAbi reflection, scene row stride ${GPU_SCENE_LAYOUTS.material.stride}, and resource slots`,
|
|
1233
|
+
hint: 'repair the producer publication or use the owner for its non-scene material layout',
|
|
1234
|
+
}),
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
// Scene-index shaders declare the dedicated slot-3 ABI. Keep the legacy
|
|
1238
|
+
// fields as a compatibility fallback for synthetic fixtures; the real
|
|
1239
|
+
// factory publishes the dedicated handles whenever the capable storage
|
|
1240
|
+
// lane is enabled, so production draws never bind the direct-draw BGL.
|
|
1241
|
+
const pipelineLayout =
|
|
1242
|
+
deformation === 'skin'
|
|
1243
|
+
? input.pipelineState.gpuDrivenPbrSkinPipelineLayout
|
|
1244
|
+
: input.pipelineState.gpuDrivenPbrPipelineLayout;
|
|
1245
|
+
const instancesBindGroupLayout = input.pipelineState.gpuDrivenInstancesBindGroupLayout;
|
|
1246
|
+
if (pipelineLayout == null || instancesBindGroupLayout == null) {
|
|
1247
|
+
return err(
|
|
1248
|
+
new RhiError({
|
|
1249
|
+
code: 'rhi-not-available',
|
|
1250
|
+
expected: `${deformation === 'skin' ? 'skinned ' : ''}published material pipeline layout`,
|
|
1251
|
+
hint: 'initialize the producer material pipeline before enabling GPU-driven raster',
|
|
1252
|
+
}),
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
const programs = input.pipelineState.gpuDrivenPbrPrograms;
|
|
1256
|
+
if (
|
|
1257
|
+
input.pipelineFactory === undefined &&
|
|
1258
|
+
(programs === undefined ||
|
|
1259
|
+
resolveStandardPbrProgram(
|
|
1260
|
+
programs,
|
|
1261
|
+
input.artifact.material,
|
|
1262
|
+
input.artifact.receipt?.vertexInputs.some((attribute) => attribute.semantic === 'color'),
|
|
1263
|
+
) === undefined)
|
|
1264
|
+
) {
|
|
1265
|
+
return err(
|
|
1266
|
+
new RhiError({
|
|
1267
|
+
code: 'rhi-not-available',
|
|
1268
|
+
expected: `producer-owned ${deformation === 'skin' ? 'skinned ' : ''}material shader module for the current device generation`,
|
|
1269
|
+
hint: 'publish the selected material module from buildReadyWebGPU before enabling GPU-driven raster',
|
|
1270
|
+
}),
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
const meshLayout =
|
|
1274
|
+
deformation === 'skin'
|
|
1275
|
+
? input.pipelineState.pbrSkinMeshBindGroupLayout
|
|
1276
|
+
: input.pipelineState.meshBindGroupLayout;
|
|
1277
|
+
if (meshLayout === null) {
|
|
1278
|
+
return err(
|
|
1279
|
+
new RhiError({
|
|
1280
|
+
code: 'rhi-not-available',
|
|
1281
|
+
expected: `${deformation === 'skin' ? 'skinned ' : ''}material mesh bind-group layout`,
|
|
1282
|
+
hint: 'publish the exact mesh-array layout selected by the material receipt',
|
|
1283
|
+
}),
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
if (
|
|
1287
|
+
deformation === 'skin' &&
|
|
1288
|
+
(input.paletteBuffer === undefined || input.paletteBindingWindowBytes === undefined)
|
|
1289
|
+
) {
|
|
1290
|
+
return err(
|
|
1291
|
+
new RhiError({
|
|
1292
|
+
code: 'rhi-not-available',
|
|
1293
|
+
expected: 'persistent skin palette buffer and binding window',
|
|
1294
|
+
hint: 'keep the palette receipt and the GPU-driven skinned projection on one frame identity',
|
|
1295
|
+
}),
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
const identity = new Float32Array(16);
|
|
1299
|
+
identity[0] = 1;
|
|
1300
|
+
identity[5] = 1;
|
|
1301
|
+
identity[10] = 1;
|
|
1302
|
+
identity[15] = 1;
|
|
1303
|
+
const instancePayload = packInstanceStorageBuffer(identity);
|
|
1304
|
+
const instanceBuffer = input.device.createBuffer({
|
|
1305
|
+
label: 'standard-pbr-gpu-driven-instance-data',
|
|
1306
|
+
size: instancePayload.byteLength,
|
|
1307
|
+
usage: GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST,
|
|
1308
|
+
});
|
|
1309
|
+
if (!instanceBuffer.ok) return instanceBuffer;
|
|
1310
|
+
const instanceWrite = input.device.queue.writeBuffer(instanceBuffer.value, 0, instancePayload);
|
|
1311
|
+
if (!instanceWrite.ok) return instanceWrite;
|
|
1312
|
+
const instancesBindGroup = input.device.createBindGroup({
|
|
1313
|
+
label: 'standard-pbr-gpu-driven-visible-instances-bg',
|
|
1314
|
+
layout: instancesBindGroupLayout,
|
|
1315
|
+
entries: [
|
|
1316
|
+
{
|
|
1317
|
+
binding: 0,
|
|
1318
|
+
resource: bufferBinding(instanceBuffer.value, 0, instancePayload.byteLength),
|
|
1319
|
+
},
|
|
1320
|
+
{
|
|
1321
|
+
binding: 1,
|
|
1322
|
+
resource: bufferBinding(input.visibleBuffer),
|
|
1323
|
+
},
|
|
1324
|
+
{
|
|
1325
|
+
binding: 2,
|
|
1326
|
+
resource: bufferBinding(input.visibleBuffer),
|
|
1327
|
+
},
|
|
1328
|
+
],
|
|
1329
|
+
});
|
|
1330
|
+
if (!instancesBindGroup.ok) return instancesBindGroup;
|
|
1331
|
+
return ok(
|
|
1332
|
+
new MaterialAbiRasterAdapter(
|
|
1333
|
+
input.device,
|
|
1334
|
+
pipelineLayout,
|
|
1335
|
+
meshLayout,
|
|
1336
|
+
instancesBindGroupLayout,
|
|
1337
|
+
programs,
|
|
833
1338
|
instancesBindGroup.value,
|
|
1339
|
+
instanceBuffer.value,
|
|
1340
|
+
instancePayload.byteLength,
|
|
1341
|
+
input.visibleBuffer,
|
|
834
1342
|
input.artifact,
|
|
1343
|
+
deformation,
|
|
1344
|
+
input.pipelineFactory,
|
|
1345
|
+
input.paletteBuffer,
|
|
1346
|
+
input.paletteBindingWindowBytes,
|
|
835
1347
|
),
|
|
836
1348
|
);
|
|
837
1349
|
}
|
|
838
1350
|
|
|
1351
|
+
ensurePaletteBuffer(
|
|
1352
|
+
paletteBuffer: Buffer | undefined,
|
|
1353
|
+
paletteBindingWindowBytes: number | undefined,
|
|
1354
|
+
): Result<void, RhiError> {
|
|
1355
|
+
if (this.deformation !== 'skin') return ok(undefined);
|
|
1356
|
+
if (paletteBuffer === undefined || paletteBindingWindowBytes === undefined) {
|
|
1357
|
+
return err(
|
|
1358
|
+
new RhiError({
|
|
1359
|
+
code: 'rhi-not-available',
|
|
1360
|
+
expected: 'persistent skin palette buffer and binding window',
|
|
1361
|
+
hint: 'publish a storage-backed SkinPaletteReceipt before the GPU-driven skinned lane',
|
|
1362
|
+
}),
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
this.paletteBuffer = paletteBuffer;
|
|
1366
|
+
this.paletteBindingWindowBytes = paletteBindingWindowBytes;
|
|
1367
|
+
return ok(undefined);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
ensureVisibleBuffer(visibleBuffer: Buffer): Result<void, RhiError> {
|
|
1371
|
+
if (this.visibleBuffer === visibleBuffer) return ok(undefined);
|
|
1372
|
+
const created = this.device.createBindGroup({
|
|
1373
|
+
label: 'standard-pbr-gpu-driven-visible-instances-bg',
|
|
1374
|
+
layout: this.instancesBindGroupLayout,
|
|
1375
|
+
entries: [
|
|
1376
|
+
{
|
|
1377
|
+
binding: 0,
|
|
1378
|
+
resource: bufferBinding(this.instanceBuffer, 0, this.instanceBufferSize),
|
|
1379
|
+
},
|
|
1380
|
+
{ binding: 1, resource: bufferBinding(visibleBuffer) },
|
|
1381
|
+
{ binding: 2, resource: bufferBinding(visibleBuffer) },
|
|
1382
|
+
],
|
|
1383
|
+
});
|
|
1384
|
+
if (!created.ok) return created;
|
|
1385
|
+
this.instancesBindGroup = created.value;
|
|
1386
|
+
this.visibleBuffer = visibleBuffer;
|
|
1387
|
+
return ok(undefined);
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
visibleWindowBindGroup(
|
|
1391
|
+
visibleBase: number,
|
|
1392
|
+
visibleCapacity: number,
|
|
1393
|
+
): Result<BindGroup, RhiError> {
|
|
1394
|
+
return this.visibleWindowBindGroupForBuffer(this.visibleBuffer, visibleBase, visibleCapacity);
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
visibleWindowBindGroupForBuffer(
|
|
1398
|
+
visibleBuffer: Buffer,
|
|
1399
|
+
visibleBase: number,
|
|
1400
|
+
visibleCapacity: number,
|
|
1401
|
+
): Result<BindGroup, RhiError> {
|
|
1402
|
+
const offset = visibleBase * 8;
|
|
1403
|
+
const size = visibleCapacity * 8;
|
|
1404
|
+
if (offset % 256 !== 0 || size <= 0) {
|
|
1405
|
+
return err(
|
|
1406
|
+
new RhiError({
|
|
1407
|
+
code: 'rhi-descriptor-invalid',
|
|
1408
|
+
expected: '256-byte-aligned non-empty visible window',
|
|
1409
|
+
hint: `allocate each GPU-driven batch visible window on the portable storage-buffer boundary (base=${visibleBase}, capacity=${visibleCapacity}, offset=${offset}, size=${size})`,
|
|
1410
|
+
}),
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
let groups = this.visibleWindowBindGroups.get(visibleBuffer);
|
|
1414
|
+
if (groups === undefined) {
|
|
1415
|
+
groups = new Map();
|
|
1416
|
+
this.visibleWindowBindGroups.set(visibleBuffer, groups);
|
|
1417
|
+
}
|
|
1418
|
+
const key = `${visibleBase}:${visibleCapacity}`;
|
|
1419
|
+
const cached = groups.get(key);
|
|
1420
|
+
if (cached !== undefined) return ok(cached);
|
|
1421
|
+
const created = this.device.createBindGroup({
|
|
1422
|
+
label: `standard-pbr-gpu-driven-visible-window-${visibleBase}`,
|
|
1423
|
+
layout: this.instancesBindGroupLayout,
|
|
1424
|
+
entries: [
|
|
1425
|
+
{
|
|
1426
|
+
binding: 0,
|
|
1427
|
+
resource: bufferBinding(this.instanceBuffer, 0, this.instanceBufferSize),
|
|
1428
|
+
},
|
|
1429
|
+
{ binding: 1, resource: bufferBinding(visibleBuffer, offset, size) },
|
|
1430
|
+
{ binding: 2, resource: bufferBinding(visibleBuffer, offset, size) },
|
|
1431
|
+
],
|
|
1432
|
+
});
|
|
1433
|
+
if (!created.ok) return created;
|
|
1434
|
+
groups.set(key, created.value);
|
|
1435
|
+
return ok(created.value);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
839
1438
|
pipeline(
|
|
840
1439
|
format: TextureFormat,
|
|
841
1440
|
sampleCount: 1 | 4,
|
|
842
1441
|
layoutProjection: MeshGpuHandles['layoutProjection'],
|
|
1442
|
+
shaderUvSetCount: number,
|
|
843
1443
|
topology: GpuDrivenBatch['key']['topology'],
|
|
844
1444
|
stripIndexFormat: 'uint16' | 'uint32' | undefined,
|
|
845
1445
|
renderState: MaterialRenderState | undefined,
|
|
1446
|
+
selectedArtifact: MaterialShaderArtifact = this.artifact,
|
|
1447
|
+
additionalColorFormats: readonly TextureFormat[] = [],
|
|
846
1448
|
): Result<RenderPipeline, RhiError> {
|
|
847
|
-
const
|
|
848
|
-
|
|
849
|
-
|
|
1449
|
+
const vertexLayout = deriveVertexBufferLayoutFromProjection(layoutProjection, {
|
|
1450
|
+
shaderUvSetCount,
|
|
1451
|
+
})[0];
|
|
1452
|
+
if (vertexLayout === undefined) {
|
|
1453
|
+
return err(
|
|
1454
|
+
new RhiError({
|
|
1455
|
+
code: 'shader-compile-failed',
|
|
1456
|
+
expected: 'non-empty geometry VertexLayoutProjection',
|
|
1457
|
+
hint: 'prepare a receipt-compatible mesh projection before Standard PBR raster',
|
|
1458
|
+
}),
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
const receipt = selectedArtifact.receipt;
|
|
1462
|
+
if (receipt === undefined) {
|
|
1463
|
+
return err(
|
|
1464
|
+
new RhiError({
|
|
1465
|
+
code: 'rhi-not-available',
|
|
1466
|
+
expected: 'selected MaterialProgramAbi receipt for GPU-driven raster',
|
|
1467
|
+
hint: 'keep the draw on the producer recovery path until its ABI is published',
|
|
1468
|
+
}),
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
const program =
|
|
1472
|
+
this.programs === undefined
|
|
850
1473
|
? undefined
|
|
851
|
-
:
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1474
|
+
: resolveStandardPbrProgram(
|
|
1475
|
+
this.programs,
|
|
1476
|
+
this.artifact.material,
|
|
1477
|
+
layoutProjection.attributes.some((attribute) => attribute.key === 'color'),
|
|
1478
|
+
);
|
|
1479
|
+
if (this.pipelineFactory === undefined && program === undefined) {
|
|
1480
|
+
return err(
|
|
1481
|
+
new RhiError({
|
|
1482
|
+
code: 'rhi-not-available',
|
|
1483
|
+
expected: 'producer-owned Standard PBR program matching the geometry vertex layout',
|
|
1484
|
+
hint: 'publish both vertex-color variants for the current device generation',
|
|
1485
|
+
}),
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
const key = `${format}:${additionalColorFormats.join(',')}|${sampleCount}|${vertexLayout.arrayStride}|${topology}|${stripIndexFormat ?? ''}|${JSON.stringify(renderState ?? null)}|${selectedArtifact.material}|${selectedArtifact.layoutIdentity}|${receipt.receiptIdentity}|${receipt.generation}|${selectedArtifact.variantSet ?? ''}|${materialArtifactProgramIdentity(selectedArtifact)}|${selectedArtifact.vertexEntry ?? receipt.sceneIndexEntry}|${selectedArtifact.fragmentEntry ?? ''}|${layoutProjection.digest}|uv${shaderUvSetCount}`;
|
|
856
1489
|
const cached = this.pipelines.get(key);
|
|
857
1490
|
if (cached !== undefined) return ok(cached);
|
|
1491
|
+
if (this.pipelineFactory !== undefined) {
|
|
1492
|
+
const pipelineEntry = this.pipelineFactory(
|
|
1493
|
+
selectedArtifact.specializationKey ?? selectedArtifact.material,
|
|
1494
|
+
false,
|
|
1495
|
+
renderState,
|
|
1496
|
+
topology,
|
|
1497
|
+
stripIndexFormat,
|
|
1498
|
+
selectedArtifact.variantSet,
|
|
1499
|
+
'forward',
|
|
1500
|
+
undefined,
|
|
1501
|
+
sampleCount,
|
|
1502
|
+
format,
|
|
1503
|
+
selectedArtifact.uvSetCount ?? shaderUvSetCount,
|
|
1504
|
+
'depth24plus-stencil8',
|
|
1505
|
+
undefined,
|
|
1506
|
+
layoutProjection,
|
|
1507
|
+
// GPU-driven receipts are already producer-validated. Use the
|
|
1508
|
+
// synchronous shader-module adapter for this submission seam so a
|
|
1509
|
+
// newly loaded authored material cannot lose its first draw to the
|
|
1510
|
+
// ordinary async pre-bake warm-up window.
|
|
1511
|
+
'immediate',
|
|
1512
|
+
this.deformation === 'skin' ? 'gpu-driven-skin' : 'gpu-driven-pbr',
|
|
1513
|
+
receipt.sceneIndexEntry,
|
|
1514
|
+
additionalColorFormats,
|
|
1515
|
+
selectedArtifact.vertexEntry,
|
|
1516
|
+
selectedArtifact.fragmentEntry,
|
|
1517
|
+
);
|
|
1518
|
+
if (pipelineEntry === null) {
|
|
1519
|
+
return err(
|
|
1520
|
+
new RhiError({
|
|
1521
|
+
code: 'rhi-not-available',
|
|
1522
|
+
expected: `producer-owned scene-index pipeline for material '${selectedArtifact.material}'`,
|
|
1523
|
+
hint: 'publish a matching program, entry, layout and resource contract before GPU admission',
|
|
1524
|
+
}),
|
|
1525
|
+
);
|
|
1526
|
+
}
|
|
1527
|
+
const expectedGroup2Contract = resolvePipelineGroup2Contract(selectedArtifact.wgsl);
|
|
1528
|
+
if (pipelineEntry.group2Contract !== expectedGroup2Contract) {
|
|
1529
|
+
return err(
|
|
1530
|
+
new RhiError({
|
|
1531
|
+
code: 'rhi-descriptor-invalid',
|
|
1532
|
+
expected: `GPU-driven ${this.deformation === 'skin' ? 'skinned ' : ''}material pipeline group(2) ${expectedGroup2Contract} contract`,
|
|
1533
|
+
hint: 'custom attachment or incompatible group(2) layouts remain on their specialized lane',
|
|
1534
|
+
}),
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
this.pipelines.set(key, pipelineEntry.pipeline);
|
|
1538
|
+
return ok(pipelineEntry.pipeline);
|
|
1539
|
+
}
|
|
1540
|
+
if (
|
|
1541
|
+
program === undefined ||
|
|
1542
|
+
selectedArtifact.material !== this.artifact.material ||
|
|
1543
|
+
selectedArtifact.layoutIdentity !== this.artifact.layoutIdentity ||
|
|
1544
|
+
selectedArtifact.receipt?.receiptIdentity !== this.artifact.receipt?.receiptIdentity
|
|
1545
|
+
) {
|
|
1546
|
+
return err(
|
|
1547
|
+
new RhiError({
|
|
1548
|
+
code: 'rhi-not-available',
|
|
1549
|
+
expected: 'selected material pipeline factory for the requested ABI artifact',
|
|
1550
|
+
hint: 'do not pair a receipt-backed custom or color variant with the legacy fixed module',
|
|
1551
|
+
}),
|
|
1552
|
+
);
|
|
1553
|
+
}
|
|
858
1554
|
const pipeline = this.device.createRenderPipeline({
|
|
859
1555
|
label: `standard-pbr-gpu-driven.${key}`,
|
|
860
1556
|
layout: this.pipelineLayout,
|
|
861
1557
|
vertex: {
|
|
862
|
-
module:
|
|
863
|
-
entryPoint:
|
|
1558
|
+
module: program.module,
|
|
1559
|
+
entryPoint: selectedArtifact.vertexEntry ?? receipt.sceneIndexEntry,
|
|
864
1560
|
buffers: [
|
|
865
1561
|
{
|
|
866
|
-
arrayStride:
|
|
1562
|
+
arrayStride: vertexLayout.arrayStride,
|
|
867
1563
|
stepMode: 'vertex',
|
|
868
|
-
attributes: attributes.map(({ shaderLocation, offset, format }) => ({
|
|
1564
|
+
attributes: vertexLayout.attributes.map(({ shaderLocation, offset, format }) => ({
|
|
869
1565
|
shaderLocation,
|
|
870
1566
|
offset,
|
|
871
1567
|
format: format as GPUVertexFormat,
|
|
@@ -874,9 +1570,9 @@ class StandardPbrRasterAdapter {
|
|
|
874
1570
|
],
|
|
875
1571
|
},
|
|
876
1572
|
fragment: {
|
|
877
|
-
module:
|
|
1573
|
+
module: program.module,
|
|
878
1574
|
entryPoint: 'fs_main',
|
|
879
|
-
targets: [{ format }
|
|
1575
|
+
targets: [format, ...additionalColorFormats].map((format) => ({ format })),
|
|
880
1576
|
},
|
|
881
1577
|
primitive: {
|
|
882
1578
|
topology,
|
|
@@ -896,47 +1592,380 @@ class StandardPbrRasterAdapter {
|
|
|
896
1592
|
return pipeline;
|
|
897
1593
|
}
|
|
898
1594
|
|
|
899
|
-
|
|
1595
|
+
/**
|
|
1596
|
+
* Drop only program-derived caches when a publication switches. Resource
|
|
1597
|
+
* buffers stay alive until their normal fence retirement, while the next
|
|
1598
|
+
* draw must obtain a PSO for the new artifact rather than reuse the old one.
|
|
1599
|
+
*/
|
|
1600
|
+
invalidatePipelineCache(): void {
|
|
1601
|
+
this.pipelines.clear();
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
projectionBindGroup(
|
|
1605
|
+
rows: Float32Array,
|
|
1606
|
+
key?: string,
|
|
1607
|
+
capacityRows?: number,
|
|
1608
|
+
): Result<BindGroup, RhiError> {
|
|
1609
|
+
if (key !== undefined) {
|
|
1610
|
+
const capacityBytes = Math.max(
|
|
1611
|
+
MESH_SSBO_BYTES,
|
|
1612
|
+
(capacityRows ?? Math.ceil(rows.byteLength / MESH_SSBO_BYTES)) * MESH_SSBO_BYTES,
|
|
1613
|
+
);
|
|
1614
|
+
const retained = this.stagedProjectionBindings.get(key) ?? this.projectionBindings.get(key);
|
|
1615
|
+
if (retained !== undefined && retained.capacityBytes >= capacityBytes) {
|
|
1616
|
+
const write = this.writeProjectionRows(key, retained, rows);
|
|
1617
|
+
if (!write.ok) return write;
|
|
1618
|
+
return ok(retained.bindGroup);
|
|
1619
|
+
}
|
|
1620
|
+
const created = this.createProjectionBinding(rows, capacityBytes);
|
|
1621
|
+
if (!created.ok) return created;
|
|
1622
|
+
this.stagedProjectionBindings.set(key, created.value);
|
|
1623
|
+
return ok(created.value.bindGroup);
|
|
1624
|
+
}
|
|
1625
|
+
const created = this.createProjectionBinding(rows, Math.max(MESH_SSBO_BYTES, rows.byteLength));
|
|
1626
|
+
if (!created.ok) return created;
|
|
1627
|
+
return ok(created.value.bindGroup);
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
refreshProjectionRows(key: string, rows: Float32Array): Result<void, RhiError> {
|
|
1631
|
+
const retained = this.stagedProjectionBindings.get(key) ?? this.projectionBindings.get(key);
|
|
1632
|
+
if (retained === undefined) return ok(undefined);
|
|
1633
|
+
if (rows.byteLength > retained.capacityBytes) {
|
|
1634
|
+
const capacityBytes = Math.max(
|
|
1635
|
+
MESH_SSBO_BYTES,
|
|
1636
|
+
Math.ceil(rows.byteLength / MESH_SSBO_BYTES) * MESH_SSBO_BYTES,
|
|
1637
|
+
);
|
|
1638
|
+
const replacement = this.createProjectionBinding(rows, capacityBytes);
|
|
1639
|
+
if (!replacement.ok) return replacement;
|
|
1640
|
+
const previousStaged = this.stagedProjectionBindings.get(key);
|
|
1641
|
+
if (previousStaged !== undefined && previousStaged.buffer !== replacement.value.buffer) {
|
|
1642
|
+
this.retireProjectionBuffer(previousStaged.buffer);
|
|
1643
|
+
}
|
|
1644
|
+
this.stagedProjectionBindings.set(key, replacement.value);
|
|
1645
|
+
return ok(undefined);
|
|
1646
|
+
}
|
|
1647
|
+
return this.writeProjectionRows(key, retained, rows);
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
sceneMeshBinding(key: string): GpuDrivenSceneMeshBinding {
|
|
1651
|
+
const binding = this.stagedProjectionBindings.get(key) ?? this.projectionBindings.get(key);
|
|
1652
|
+
if (binding === undefined)
|
|
1653
|
+
throw new RhiError({
|
|
1654
|
+
code: 'rhi-not-available',
|
|
1655
|
+
expected: 'retained scene mesh projection',
|
|
1656
|
+
hint: 'prepare this batch projection before binding its lighting resources',
|
|
1657
|
+
});
|
|
1658
|
+
return {
|
|
1659
|
+
meshBuffer: binding.buffer,
|
|
1660
|
+
meshBytes: binding.capacityBytes,
|
|
1661
|
+
...(this.deformation === 'skin' && this.paletteBuffer !== undefined
|
|
1662
|
+
? { paletteBuffer: this.paletteBuffer }
|
|
1663
|
+
: {}),
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
/** Stage the projection keys required by the frame being prepared. */
|
|
1668
|
+
stageProjectionKeys(keys: ReadonlySet<string>): void {
|
|
1669
|
+
this.stagedProjectionKeys = new Set(keys);
|
|
1670
|
+
this.activeProjectionRowsAtStage.clear();
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
/** Add a shadow projection key discovered while updating a view plan. */
|
|
1674
|
+
noteProjectionKey(key: string): void {
|
|
1675
|
+
this.stagedProjectionKeys.add(key);
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
/** Promote staged projection resources after the command buffer submits. */
|
|
1679
|
+
commitProjectionBindings(): void {
|
|
1680
|
+
const keep = new Set(this.stagedProjectionKeys);
|
|
1681
|
+
for (const [key, staged] of this.stagedProjectionBindings) {
|
|
1682
|
+
if (!keep.has(key)) {
|
|
1683
|
+
this.retireProjectionBuffer(staged.buffer);
|
|
1684
|
+
continue;
|
|
1685
|
+
}
|
|
1686
|
+
const previous = this.projectionBindings.get(key);
|
|
1687
|
+
this.projectionBindings.set(key, staged);
|
|
1688
|
+
if (previous !== undefined && previous.buffer !== staged.buffer) {
|
|
1689
|
+
this.retireProjectionBuffer(previous.buffer);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
this.stagedProjectionBindings.clear();
|
|
1693
|
+
for (const [key, active] of this.projectionBindings) {
|
|
1694
|
+
if (!keep.has(key)) {
|
|
1695
|
+
this.projectionBindings.delete(key);
|
|
1696
|
+
this.retireProjectionBuffer(active.buffer);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
this.stagedProjectionKeys.clear();
|
|
1700
|
+
this.activeProjectionRowsAtStage.clear();
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
/** Discard resources created for an aborted graph while retaining LKG rows. */
|
|
1704
|
+
abortProjectionBindings(): void {
|
|
1705
|
+
for (const [key, rows] of this.activeProjectionRowsAtStage) {
|
|
1706
|
+
const active = this.projectionBindings.get(key);
|
|
1707
|
+
if (active === undefined) continue;
|
|
1708
|
+
const restored = this.device.queue.writeBuffer(active.buffer, 0, rows);
|
|
1709
|
+
if (restored.ok) active.rows = new Float32Array(rows);
|
|
1710
|
+
}
|
|
1711
|
+
for (const staged of this.stagedProjectionBindings.values()) {
|
|
1712
|
+
this.retireProjectionBuffer(staged.buffer);
|
|
1713
|
+
}
|
|
1714
|
+
this.stagedProjectionBindings.clear();
|
|
1715
|
+
this.stagedProjectionKeys.clear();
|
|
1716
|
+
this.activeProjectionRowsAtStage.clear();
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
private createProjectionBinding(
|
|
1720
|
+
rows: Float32Array,
|
|
1721
|
+
capacityBytes: number,
|
|
1722
|
+
): Result<
|
|
1723
|
+
{
|
|
1724
|
+
readonly buffer: Buffer;
|
|
1725
|
+
readonly bindGroup: BindGroup;
|
|
1726
|
+
readonly capacityBytes: number;
|
|
1727
|
+
rows: Float32Array;
|
|
1728
|
+
},
|
|
1729
|
+
RhiError
|
|
1730
|
+
> {
|
|
900
1731
|
const buffer = this.device.createBuffer({
|
|
901
1732
|
label: 'standard-pbr-gpu-driven-scene-meshes',
|
|
902
|
-
size:
|
|
1733
|
+
size: capacityBytes,
|
|
903
1734
|
usage: GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST,
|
|
904
1735
|
});
|
|
905
1736
|
if (!buffer.ok) return buffer;
|
|
906
1737
|
const write = this.device.queue.writeBuffer(buffer.value, 0, rows);
|
|
907
|
-
if (!write.ok)
|
|
908
|
-
|
|
909
|
-
|
|
1738
|
+
if (!write.ok) {
|
|
1739
|
+
this.destroyProjectionBuffer(buffer.value);
|
|
1740
|
+
return write;
|
|
1741
|
+
}
|
|
1742
|
+
this.projectionBuffers.add(buffer.value);
|
|
1743
|
+
const entries = [
|
|
1744
|
+
{ binding: 0, resource: bufferBinding(buffer.value, 0, capacityBytes) },
|
|
1745
|
+
...(this.deformation === 'skin' &&
|
|
1746
|
+
this.paletteBuffer !== undefined &&
|
|
1747
|
+
this.paletteBindingWindowBytes !== undefined
|
|
1748
|
+
? [
|
|
1749
|
+
{
|
|
1750
|
+
binding: 1,
|
|
1751
|
+
// The scene-index second lane is a global palette row. Bind the
|
|
1752
|
+
// complete arena so rows beyond the legacy 255-joint window
|
|
1753
|
+
// remain addressable; the shader performs the row selection.
|
|
1754
|
+
resource: bufferBinding(this.paletteBuffer),
|
|
1755
|
+
},
|
|
1756
|
+
{
|
|
1757
|
+
binding: 2,
|
|
1758
|
+
// The dedicated GPU-driven skin layout keeps the current /
|
|
1759
|
+
// previous palette pair used by the direct skin layout. The
|
|
1760
|
+
// scene-index shader currently reads the current arena, but
|
|
1761
|
+
// the shared layout still requires a complete binding shape.
|
|
1762
|
+
resource: bufferBinding(this.paletteBuffer),
|
|
1763
|
+
},
|
|
1764
|
+
]
|
|
1765
|
+
: []),
|
|
1766
|
+
];
|
|
1767
|
+
const bindGroup = this.device.createBindGroup({
|
|
910
1768
|
label: 'standard-pbr-gpu-driven-scene-mesh-bg',
|
|
911
1769
|
layout: this.meshLayout,
|
|
912
|
-
entries
|
|
1770
|
+
entries,
|
|
913
1771
|
});
|
|
1772
|
+
if (!bindGroup.ok) {
|
|
1773
|
+
this.destroyProjectionBuffer(buffer.value);
|
|
1774
|
+
return bindGroup;
|
|
1775
|
+
}
|
|
1776
|
+
return ok({
|
|
1777
|
+
buffer: buffer.value,
|
|
1778
|
+
bindGroup: bindGroup.value,
|
|
1779
|
+
capacityBytes,
|
|
1780
|
+
rows: new Float32Array(rows),
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
private writeProjectionRows(
|
|
1785
|
+
key: string,
|
|
1786
|
+
binding: {
|
|
1787
|
+
readonly buffer: Buffer;
|
|
1788
|
+
readonly bindGroup: BindGroup;
|
|
1789
|
+
readonly capacityBytes: number;
|
|
1790
|
+
rows: Float32Array;
|
|
1791
|
+
},
|
|
1792
|
+
rows: Float32Array,
|
|
1793
|
+
): Result<void, RhiError> {
|
|
1794
|
+
// Active bindings are the last-known-good graph resources. Keep one CPU
|
|
1795
|
+
// snapshot so an aborted candidate can restore their exact contents
|
|
1796
|
+
// without allocating a second GPU buffer on every stable frame.
|
|
1797
|
+
if (
|
|
1798
|
+
this.stagedProjectionBindings.get(key) === undefined &&
|
|
1799
|
+
!this.activeProjectionRowsAtStage.has(key)
|
|
1800
|
+
) {
|
|
1801
|
+
this.activeProjectionRowsAtStage.set(key, new Float32Array(binding.rows));
|
|
1802
|
+
}
|
|
1803
|
+
const write = this.device.queue.writeBuffer(binding.buffer, 0, rows);
|
|
1804
|
+
if (!write.ok) return write;
|
|
1805
|
+
binding.rows = new Float32Array(rows);
|
|
1806
|
+
return ok(undefined);
|
|
914
1807
|
}
|
|
915
1808
|
|
|
916
1809
|
dispose(): void {
|
|
917
|
-
|
|
918
|
-
this.
|
|
1810
|
+
this.projectionDisposed = true;
|
|
1811
|
+
for (const binding of this.projectionBindings.values()) {
|
|
1812
|
+
this.destroyProjectionBuffer(binding.buffer);
|
|
1813
|
+
}
|
|
1814
|
+
for (const binding of this.stagedProjectionBindings.values()) {
|
|
1815
|
+
this.destroyProjectionBuffer(binding.buffer);
|
|
1816
|
+
}
|
|
1817
|
+
for (const buffer of this.projectionBuffers) this.destroyProjectionBuffer(buffer);
|
|
1818
|
+
for (const buffer of this.pendingProjectionRetirements) {
|
|
1819
|
+
this.destroyProjectionBuffer(buffer);
|
|
1820
|
+
}
|
|
1821
|
+
this.projectionBindings.clear();
|
|
1822
|
+
this.stagedProjectionBindings.clear();
|
|
1823
|
+
this.activeProjectionRowsAtStage.clear();
|
|
1824
|
+
this.projectionBuffers.clear();
|
|
1825
|
+
this.pendingProjectionRetirements.clear();
|
|
1826
|
+
this.device.destroyBuffer(this.instanceBuffer);
|
|
919
1827
|
}
|
|
920
1828
|
|
|
921
|
-
|
|
922
|
-
|
|
1829
|
+
private retireProjectionBuffer(buffer: Buffer): void {
|
|
1830
|
+
if (this.destroyedProjectionBuffers.has(buffer)) return;
|
|
1831
|
+
this.projectionBuffers.delete(buffer);
|
|
1832
|
+
if (this.projectionDisposed) {
|
|
1833
|
+
this.destroyProjectionBuffer(buffer);
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
if (!this.pendingProjectionRetirements.add(buffer)) return;
|
|
1837
|
+
void this.device.queue.onSubmittedWorkDone().then(
|
|
1838
|
+
() => {
|
|
1839
|
+
this.pendingProjectionRetirements.delete(buffer);
|
|
1840
|
+
this.destroyProjectionBuffer(buffer);
|
|
1841
|
+
},
|
|
1842
|
+
() => {
|
|
1843
|
+
this.pendingProjectionRetirements.delete(buffer);
|
|
1844
|
+
this.destroyProjectionBuffer(buffer);
|
|
1845
|
+
},
|
|
1846
|
+
);
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
private destroyProjectionBuffer(buffer: Buffer): void {
|
|
1850
|
+
if (this.destroyedProjectionBuffers.has(buffer)) return;
|
|
1851
|
+
this.destroyedProjectionBuffers.add(buffer);
|
|
1852
|
+
this.projectionBuffers.delete(buffer);
|
|
1853
|
+
this.device.destroyBuffer(buffer);
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
get instancesFrameGroup(): BindGroup {
|
|
1857
|
+
return this.instancesBindGroup;
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
get kind(): 'rigid' | 'skin' {
|
|
1861
|
+
return this.deformation;
|
|
923
1862
|
}
|
|
924
1863
|
}
|
|
925
1864
|
|
|
926
1865
|
export interface PreparedGpuDrivenFrame {
|
|
927
1866
|
readonly topologySignature: string;
|
|
928
|
-
|
|
929
|
-
readonly
|
|
1867
|
+
/** True when every source draw item/instance is claimed by the GPU lane. */
|
|
1868
|
+
readonly ownsAllDrawItems: boolean;
|
|
1869
|
+
/** True only when the extracted ShadowCaster channel is fully represented. */
|
|
1870
|
+
readonly ownsAllShadowCasters?: boolean;
|
|
1871
|
+
/** Concrete main-pass draw identities claimed by the GPU projection. */
|
|
1872
|
+
readonly drawKeys?: ReadonlySet<string>;
|
|
1873
|
+
readonly standardPbrFrameResources: GpuDrivenStandardPbrFrameResources;
|
|
930
1874
|
readonly occlusion?: OcclusionFrameProjection;
|
|
931
1875
|
/** @internal Commits a replacement generation only after graph promotion. */
|
|
932
1876
|
readonly _commitResourceReplacement: () => void;
|
|
1877
|
+
/** @internal Retires superseded view buffers when graph compilation promotes them. */
|
|
1878
|
+
readonly _commitGpuResourceReplacement?: () => void;
|
|
1879
|
+
/** @internal Discards staged shadow cache publication after a failed frame. */
|
|
1880
|
+
readonly _abortResourceReplacement?: () => void;
|
|
1881
|
+
/** Concrete ShadowCaster keys claimed by the GPU subset for this frame. */
|
|
1882
|
+
readonly shadowDrawKeys?: ReadonlySet<string>;
|
|
1883
|
+
/** View-scoped ShadowCaster claims consumed by each shadow record pass. */
|
|
1884
|
+
readonly shadowDrawKeysByView?: ReadonlyMap<string, ReadonlySet<string>>;
|
|
1885
|
+
readonly projectShadow?: (
|
|
1886
|
+
graph: RenderGraphBuilder<RenderPipelineFrame>,
|
|
1887
|
+
identity: ShadowViewIdentity,
|
|
1888
|
+
) => Result<ShadowViewProjection | undefined, RenderGraphError>;
|
|
1889
|
+
readonly updateShadowViews?: (
|
|
1890
|
+
views: readonly Omit<ShadowViewUpdateInput, 'sourcePlan' | 'scene'>[],
|
|
1891
|
+
) => Result<void, RhiError>;
|
|
1892
|
+
readonly shadowViewPool?: ShadowViewStatePool;
|
|
1893
|
+
readonly shadowBatchProjections?: ReadonlyMap<
|
|
1894
|
+
string,
|
|
1895
|
+
ReadonlyMap<number, GpuDrivenShadowBatchProjection>
|
|
1896
|
+
>;
|
|
933
1897
|
project(
|
|
934
1898
|
graph: RenderGraphBuilder<RenderPipelineFrame>,
|
|
935
1899
|
format: TextureFormat,
|
|
936
1900
|
sampleCount: 1 | 4,
|
|
1901
|
+
additionalColorFormats?: readonly TextureFormat[],
|
|
937
1902
|
): Result<RenderPipelineGpuDrivenProjection, RenderGraphError | RhiError>;
|
|
938
1903
|
}
|
|
939
1904
|
|
|
1905
|
+
/**
|
|
1906
|
+
* Renderer-owned identity token for the validation that resolves renderable
|
|
1907
|
+
* assets to GPU meshes. The token is deliberately narrower than a frame
|
|
1908
|
+
* number: unchanged persistent scene snapshots can reuse the resolved rows,
|
|
1909
|
+
* while a mesh/device/pipeline generation change forces a fresh producer read.
|
|
1910
|
+
*/
|
|
1911
|
+
export interface GpuDrivenResidencyValidationInput {
|
|
1912
|
+
/** Opaque renderer-computed world/source identity and generation fence. */
|
|
1913
|
+
readonly cacheKey: string;
|
|
1914
|
+
readonly renderables: readonly RenderableSnapshot[];
|
|
1915
|
+
readonly transparentDispatch: readonly DispatchEntry[];
|
|
1916
|
+
readonly pipelineState: PipelineState;
|
|
1917
|
+
readonly pipelineHandle: number;
|
|
1918
|
+
readonly meshResidencyEpoch: number;
|
|
1919
|
+
readonly deviceGeneration: number;
|
|
1920
|
+
readonly assetCatalogEpoch: number;
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
interface GpuDrivenResidencyValidationCache extends GpuDrivenResidencyValidationInput {
|
|
1924
|
+
readonly value: readonly ValidatedRenderable[];
|
|
1925
|
+
readonly dispatchRenderableIndices: readonly (number | undefined)[];
|
|
1926
|
+
readonly renderableIdentities: readonly RenderableSnapshot[];
|
|
1927
|
+
readonly dispatchIdentities: readonly DispatchEntry[];
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
interface GpuDrivenCpuValidationTelemetryCache {
|
|
1931
|
+
readonly cacheKey: string;
|
|
1932
|
+
readonly validated: readonly { readonly source: RenderableSnapshot }[];
|
|
1933
|
+
readonly gpuOwnedDrawKeys: ReadonlySet<string>;
|
|
1934
|
+
readonly gpuOwnedDrawCount: number;
|
|
1935
|
+
readonly totalDrawItems: number;
|
|
1936
|
+
readonly gpuOwnedRows: number;
|
|
1937
|
+
readonly cpuFallbackDrawItems: number;
|
|
1938
|
+
readonly cpuSemanticFallbackDrawItems: number;
|
|
1939
|
+
readonly cpuDeformationFallbackDrawItems: number;
|
|
1940
|
+
readonly cpuDeformationReasons: ReadonlyMap<
|
|
1941
|
+
'skin-bounds-missing' | 'skin-address-missing' | 'resource-not-ready',
|
|
1942
|
+
number
|
|
1943
|
+
>;
|
|
1944
|
+
readonly blockedDrawItems: number;
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
function sameIdentitySequence<T extends object>(
|
|
1948
|
+
expected: readonly T[],
|
|
1949
|
+
actual: readonly T[],
|
|
1950
|
+
): boolean {
|
|
1951
|
+
if (expected.length !== actual.length) return false;
|
|
1952
|
+
for (let index = 0; index < expected.length; index += 1) {
|
|
1953
|
+
if (expected[index] !== actual[index]) return false;
|
|
1954
|
+
}
|
|
1955
|
+
return true;
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
function sameDispatchRenderableIndices(
|
|
1959
|
+
expected: readonly (number | undefined)[],
|
|
1960
|
+
actual: readonly DispatchEntry[],
|
|
1961
|
+
): boolean {
|
|
1962
|
+
if (expected.length !== actual.length) return false;
|
|
1963
|
+
for (let index = 0; index < expected.length; index += 1) {
|
|
1964
|
+
if (expected[index] !== actual[index]?.renderableIndex) return false;
|
|
1965
|
+
}
|
|
1966
|
+
return true;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
940
1969
|
export interface RecoveryCapabilityFacts {
|
|
941
1970
|
readonly generation: number;
|
|
942
1971
|
readonly shaderCompilation: boolean;
|
|
@@ -994,17 +2023,36 @@ export function classifyRecoveryCapability(
|
|
|
994
2023
|
export class GpuDrivenProduction {
|
|
995
2024
|
private disposed = false;
|
|
996
2025
|
private view: GpuDrivenView | undefined;
|
|
997
|
-
private
|
|
998
|
-
private
|
|
2026
|
+
private materialRaster: MaterialAbiRasterAdapter | undefined;
|
|
2027
|
+
private skinRaster: MaterialAbiRasterAdapter | undefined;
|
|
999
2028
|
private readonly sceneIdentities = new WeakMap<object, number>();
|
|
1000
2029
|
private nextSceneIdentity = 1;
|
|
1001
2030
|
private filteredCache:
|
|
1002
2031
|
| {
|
|
1003
2032
|
readonly source: SubmissionPlan;
|
|
2033
|
+
readonly scene: PersistentGpuDrivenState['scene'];
|
|
2034
|
+
readonly slots: PersistentGpuDrivenState['slots'];
|
|
2035
|
+
readonly worldKeys: PersistentGpuDrivenState['worldKeys'];
|
|
1004
2036
|
readonly meshResidencyEpoch: number;
|
|
1005
2037
|
readonly lodProjectionFingerprint: string;
|
|
1006
2038
|
readonly activeEntityRevision?: number;
|
|
1007
2039
|
readonly activeEntityKeys?: ReadonlySet<number>;
|
|
2040
|
+
readonly skinArtifact?: MaterialShaderArtifact;
|
|
2041
|
+
readonly materialArtifactSignature: string;
|
|
2042
|
+
readonly value: FilteredProductionPlan;
|
|
2043
|
+
}
|
|
2044
|
+
| undefined;
|
|
2045
|
+
private shadowFilteredCache:
|
|
2046
|
+
| {
|
|
2047
|
+
readonly source: SubmissionPlan;
|
|
2048
|
+
readonly scene: PersistentGpuDrivenState['scene'];
|
|
2049
|
+
readonly slots: PersistentGpuDrivenState['slots'];
|
|
2050
|
+
readonly worldKeys: PersistentGpuDrivenState['worldKeys'];
|
|
2051
|
+
readonly lodProjectionFingerprint: string;
|
|
2052
|
+
readonly meshResidencyEpoch: number;
|
|
2053
|
+
readonly membership: string;
|
|
2054
|
+
readonly skinArtifact?: MaterialShaderArtifact;
|
|
2055
|
+
readonly materialArtifactSignature: string;
|
|
1008
2056
|
readonly value: FilteredProductionPlan;
|
|
1009
2057
|
}
|
|
1010
2058
|
| undefined;
|
|
@@ -1014,13 +2062,61 @@ export class GpuDrivenProduction {
|
|
|
1014
2062
|
private batchBindGroupCreates = 0;
|
|
1015
2063
|
private validatedGpuOwnedRows = 0;
|
|
1016
2064
|
private cpuFallbackDrawItems = 0;
|
|
2065
|
+
private cpuSemanticFallbackDrawItems = 0;
|
|
2066
|
+
private cpuDeformationFallbackDrawItems = 0;
|
|
2067
|
+
private readonly cpuDeformationReasons = new Map<
|
|
2068
|
+
'skin-bounds-missing' | 'skin-address-missing' | 'resource-not-ready',
|
|
2069
|
+
number
|
|
2070
|
+
>();
|
|
2071
|
+
private blockedDrawItems = 0;
|
|
2072
|
+
private preparationFailure: GpuDrivenPreparationFailureInspection | undefined;
|
|
2073
|
+
private encodedIndirectDraws = 0;
|
|
1017
2074
|
private telemetryPrepared = false;
|
|
1018
2075
|
private telemetryCandidateCount = 0;
|
|
1019
2076
|
private telemetryGeometryWork = 0;
|
|
1020
2077
|
private telemetryRootGeometryWork = 0;
|
|
2078
|
+
private telemetryOverflow = false;
|
|
1021
2079
|
private indirectDrawCount = 0;
|
|
1022
|
-
private
|
|
2080
|
+
private gpuOwnedDrawItems = 0;
|
|
2081
|
+
private mainTotalDrawItems = 0;
|
|
2082
|
+
private lane: 'gpu' | 'cpu-semantic' | 'cpu-deformation' | 'blocked' = 'cpu-semantic';
|
|
2083
|
+
private laneReason: GpuDrivenLaneReason = 'capability';
|
|
2084
|
+
private submissionState: 'idle' | 'pending' | 'submitted' | 'aborted' = 'idle';
|
|
2085
|
+
private retryCount = 0;
|
|
2086
|
+
private lastKnownGoodGeneration: number | undefined;
|
|
2087
|
+
private overflowRecoveryRequired = false;
|
|
2088
|
+
private shadowViews: ShadowViewStatePool | undefined;
|
|
2089
|
+
private shadowSourcePlan: SubmissionPlan | undefined;
|
|
2090
|
+
private shadowScene: PersistentGpuDrivenState['scene'] | undefined;
|
|
2091
|
+
private shadowBatchProjections = new Map<
|
|
2092
|
+
string,
|
|
2093
|
+
ReadonlyMap<number, GpuDrivenShadowBatchProjection>
|
|
2094
|
+
>();
|
|
2095
|
+
private shadowDrawKeysByView = new Map<string, ReadonlySet<string>>();
|
|
2096
|
+
private shadowExpectedDrawKeys = new Set<string>();
|
|
2097
|
+
private shadowBatchProjectionSource: SubmissionPlan | undefined;
|
|
2098
|
+
private skinPaletteBuffer: Buffer | undefined;
|
|
2099
|
+
private committedPaletteContentRevision: number | undefined;
|
|
1023
2100
|
private standardPbrProjection: StandardPbrSceneProjection | undefined;
|
|
2101
|
+
private committedProjectionSource:
|
|
2102
|
+
| {
|
|
2103
|
+
readonly filtered: FilteredProductionPlan;
|
|
2104
|
+
readonly scene: PersistentGpuDrivenState['scene'];
|
|
2105
|
+
readonly contentRevision: number;
|
|
2106
|
+
readonly materialArtifactSignature: string;
|
|
2107
|
+
}
|
|
2108
|
+
| undefined;
|
|
2109
|
+
private validatedResidencyCache: GpuDrivenResidencyValidationCache | undefined;
|
|
2110
|
+
private residencyValidationCacheHits = 0;
|
|
2111
|
+
private residencyValidationScans = 0;
|
|
2112
|
+
private cpuValidationTelemetryCache: GpuDrivenCpuValidationTelemetryCache | undefined;
|
|
2113
|
+
private cpuValidationCacheHits = 0;
|
|
2114
|
+
private cpuValidationScans = 0;
|
|
2115
|
+
private structureMetrics: GpuDrivenStructureMetrics = {
|
|
2116
|
+
worldEntitiesScanned: 0,
|
|
2117
|
+
sceneTableUploadBytes: 0,
|
|
2118
|
+
paletteUploadBytes: 0,
|
|
2119
|
+
};
|
|
1024
2120
|
private signatureCache:
|
|
1025
2121
|
| {
|
|
1026
2122
|
readonly filtered: FilteredProductionPlan;
|
|
@@ -1034,8 +2130,67 @@ export class GpuDrivenProduction {
|
|
|
1034
2130
|
constructor(
|
|
1035
2131
|
private readonly device: RhiDevice,
|
|
1036
2132
|
private readonly shaderModuleFactory: PipelineBuilderShaderModuleFactory,
|
|
2133
|
+
private readonly recoveryCount = 0,
|
|
1037
2134
|
) {}
|
|
1038
2135
|
|
|
2136
|
+
/**
|
|
2137
|
+
* Reuse the last producer-resolved residency rows when all source identity
|
|
2138
|
+
* and generation fences still match. Renderables and dispatch entries are
|
|
2139
|
+
* checked element-by-element as well as by their stable object identities so
|
|
2140
|
+
* an accidental in-place mutation cannot reuse stale renderable indices.
|
|
2141
|
+
*/
|
|
2142
|
+
reuseResidencyValidation(
|
|
2143
|
+
input: GpuDrivenResidencyValidationInput,
|
|
2144
|
+
): readonly ValidatedRenderable[] | undefined {
|
|
2145
|
+
const cached = this.validatedResidencyCache;
|
|
2146
|
+
if (
|
|
2147
|
+
cached === undefined ||
|
|
2148
|
+
cached.cacheKey !== input.cacheKey ||
|
|
2149
|
+
cached.pipelineState !== input.pipelineState ||
|
|
2150
|
+
cached.pipelineState.device !== this.device ||
|
|
2151
|
+
cached.pipelineHandle !== input.pipelineHandle ||
|
|
2152
|
+
cached.meshResidencyEpoch !== input.meshResidencyEpoch ||
|
|
2153
|
+
cached.deviceGeneration !== input.deviceGeneration ||
|
|
2154
|
+
cached.assetCatalogEpoch !== input.assetCatalogEpoch ||
|
|
2155
|
+
!sameIdentitySequence(cached.renderableIdentities, input.renderables) ||
|
|
2156
|
+
!sameIdentitySequence(cached.dispatchIdentities, input.transparentDispatch) ||
|
|
2157
|
+
!sameDispatchRenderableIndices(cached.dispatchRenderableIndices, input.transparentDispatch)
|
|
2158
|
+
) {
|
|
2159
|
+
return undefined;
|
|
2160
|
+
}
|
|
2161
|
+
this.residencyValidationCacheHits += 1;
|
|
2162
|
+
return cached.value;
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
/** Publish the producer-owned rows after a completed residency validation. */
|
|
2166
|
+
rememberResidencyValidation(
|
|
2167
|
+
input: GpuDrivenResidencyValidationInput,
|
|
2168
|
+
value: readonly ValidatedRenderable[],
|
|
2169
|
+
): void {
|
|
2170
|
+
this.validatedResidencyCache = {
|
|
2171
|
+
...input,
|
|
2172
|
+
value,
|
|
2173
|
+
renderableIdentities: input.renderables,
|
|
2174
|
+
dispatchIdentities: input.transparentDispatch,
|
|
2175
|
+
dispatchRenderableIndices: input.transparentDispatch.map((entry) => entry.renderableIndex),
|
|
2176
|
+
};
|
|
2177
|
+
this.residencyValidationScans += 1;
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
private applyCpuValidationTelemetry(cache: GpuDrivenCpuValidationTelemetryCache): void {
|
|
2181
|
+
this.gpuOwnedDrawItems = cache.gpuOwnedDrawCount;
|
|
2182
|
+
this.mainTotalDrawItems = cache.totalDrawItems;
|
|
2183
|
+
this.validatedGpuOwnedRows = cache.gpuOwnedRows;
|
|
2184
|
+
this.cpuFallbackDrawItems = cache.cpuFallbackDrawItems;
|
|
2185
|
+
this.cpuSemanticFallbackDrawItems = cache.cpuSemanticFallbackDrawItems;
|
|
2186
|
+
this.cpuDeformationFallbackDrawItems = cache.cpuDeformationFallbackDrawItems;
|
|
2187
|
+
this.cpuDeformationReasons.clear();
|
|
2188
|
+
for (const [reason, count] of cache.cpuDeformationReasons) {
|
|
2189
|
+
this.cpuDeformationReasons.set(reason, count);
|
|
2190
|
+
}
|
|
2191
|
+
this.blockedDrawItems = cache.blockedDrawItems;
|
|
2192
|
+
}
|
|
2193
|
+
|
|
1039
2194
|
static forDevice(
|
|
1040
2195
|
device: RhiDevice,
|
|
1041
2196
|
shaderModuleFactory: PipelineBuilderShaderModuleFactory,
|
|
@@ -1043,34 +2198,6 @@ export class GpuDrivenProduction {
|
|
|
1043
2198
|
return new GpuDrivenProduction(device, shaderModuleFactory);
|
|
1044
2199
|
}
|
|
1045
2200
|
|
|
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
2201
|
createRecoveryRoot(scope: DeviceScope): LifecycleResourceSpec<unknown> {
|
|
1075
2202
|
return {
|
|
1076
2203
|
kind: 'scene-table',
|
|
@@ -1088,10 +2215,31 @@ export class GpuDrivenProduction {
|
|
|
1088
2215
|
prepare(input: {
|
|
1089
2216
|
readonly scene: PersistentGpuDrivenState | undefined;
|
|
1090
2217
|
readonly camera: CameraSnapshot;
|
|
1091
|
-
|
|
2218
|
+
/** GPU meshes projected by retained RenderScene slot, never raw asset handle. */
|
|
2219
|
+
readonly meshBySlot: ReadonlyMap<number, MeshGpuHandles>;
|
|
1092
2220
|
readonly viewBindGroupLayout: BindGroupLayout;
|
|
1093
2221
|
readonly meshResidencyEpoch: number;
|
|
1094
|
-
readonly
|
|
2222
|
+
readonly materialBindingClasses?: ReadonlyMap<string, string>;
|
|
2223
|
+
/** Cooked producer artifact selected from the frame material catalog. */
|
|
2224
|
+
readonly materialArtifact?: MaterialShaderArtifact;
|
|
2225
|
+
/** Compatibility input for callers that have not migrated their name. */
|
|
2226
|
+
readonly standardPbrArtifact?: MaterialShaderArtifact;
|
|
2227
|
+
/** Cooked producer artifact for the GPU skinning variant. */
|
|
2228
|
+
readonly materialSkinArtifact?: MaterialShaderArtifact;
|
|
2229
|
+
/** Compatibility input for callers that have not migrated their name. */
|
|
2230
|
+
readonly standardPbrSkinArtifact?: MaterialShaderArtifact;
|
|
2231
|
+
/** Per-draw producer artifacts keyed by prepared identity and receipt. */
|
|
2232
|
+
readonly materialArtifacts?: ReadonlyMap<string, MaterialShaderArtifact>;
|
|
2233
|
+
/** Per-draw shadow-pass artifacts keyed by the same prepared identity. */
|
|
2234
|
+
readonly shadowMaterialArtifacts?: ReadonlyMap<string, MaterialShaderArtifact>;
|
|
2235
|
+
readonly materialPipelineState?: PipelineState;
|
|
2236
|
+
readonly standardPbrPipelineState?: PipelineState;
|
|
2237
|
+
/** Existing renderer owner that builds the selected artifact PSO/BGL. */
|
|
2238
|
+
readonly materialPipelineFactory?: MaterialPipelineFactory;
|
|
2239
|
+
/** Extracted ShadowCaster ownership keyed by concrete draw item and pass. */
|
|
2240
|
+
readonly shadowCasterDrawKeys?: ReadonlySet<string>;
|
|
2241
|
+
/** Structured ShadowCaster ownership facts for exact pass admission. */
|
|
2242
|
+
readonly shadowCasterMembership?: readonly ShadowCasterMembership[];
|
|
1095
2243
|
/** Entity keys admitted by the persistent visibility facet for this view. */
|
|
1096
2244
|
readonly activeEntityKeys?: ReadonlySet<number>;
|
|
1097
2245
|
/** Collision-free renderer-owned revision for the active visibility set. */
|
|
@@ -1099,20 +2247,37 @@ export class GpuDrivenProduction {
|
|
|
1099
2247
|
/** Total extracted candidates, including facet-suppressed rows. */
|
|
1100
2248
|
readonly telemetryCandidateCount?: number;
|
|
1101
2249
|
readonly occlusion?: OcclusionFrameProjection;
|
|
1102
|
-
/**
|
|
1103
|
-
readonly
|
|
1104
|
-
|
|
1105
|
-
}): Result<PreparedGpuDrivenFrame | undefined, RhiError> {
|
|
2250
|
+
/** Per-frame deltas from the existing RenderScene/GPU Scene/palette owners. */
|
|
2251
|
+
readonly structureMetrics?: GpuDrivenStructureMetrics;
|
|
2252
|
+
}): Result<PreparedGpuDrivenFrame | undefined, RhiError | GpuDrivenPreparationError> {
|
|
1106
2253
|
this.gpuOwnedSnapshotsMaterialized = 0;
|
|
1107
2254
|
this.filteredPlanBuilds = 0;
|
|
1108
2255
|
this.batchBindGroupCreates = 0;
|
|
1109
2256
|
this.validatedGpuOwnedRows = 0;
|
|
1110
2257
|
this.cpuFallbackDrawItems = 0;
|
|
2258
|
+
this.cpuSemanticFallbackDrawItems = 0;
|
|
2259
|
+
this.cpuDeformationFallbackDrawItems = 0;
|
|
2260
|
+
this.cpuDeformationReasons.clear();
|
|
2261
|
+
this.blockedDrawItems = 0;
|
|
2262
|
+
this.preparationFailure = undefined;
|
|
2263
|
+
this.encodedIndirectDraws = 0;
|
|
1111
2264
|
this.telemetryPrepared = false;
|
|
1112
2265
|
this.telemetryCandidateCount = input.telemetryCandidateCount ?? 0;
|
|
1113
2266
|
this.telemetryGeometryWork = 0;
|
|
1114
2267
|
this.telemetryRootGeometryWork = 0;
|
|
2268
|
+
this.telemetryOverflow = false;
|
|
1115
2269
|
this.indirectDrawCount = 0;
|
|
2270
|
+
this.gpuOwnedDrawItems = 0;
|
|
2271
|
+
this.mainTotalDrawItems = 0;
|
|
2272
|
+
this.structureMetrics = input.structureMetrics ?? {
|
|
2273
|
+
worldEntitiesScanned: 0,
|
|
2274
|
+
sceneTableUploadBytes: 0,
|
|
2275
|
+
paletteUploadBytes: 0,
|
|
2276
|
+
};
|
|
2277
|
+
this.lane = 'cpu-semantic';
|
|
2278
|
+
this.laneReason = 'capability';
|
|
2279
|
+
this.submissionState = 'idle';
|
|
2280
|
+
this.shadowExpectedDrawKeys = new Set();
|
|
1116
2281
|
const capability = classifyRecoveryCapability({
|
|
1117
2282
|
generation: 0,
|
|
1118
2283
|
shaderCompilation: true,
|
|
@@ -1122,26 +2287,103 @@ export class GpuDrivenProduction {
|
|
|
1122
2287
|
multisample: true,
|
|
1123
2288
|
});
|
|
1124
2289
|
if (
|
|
1125
|
-
input.hdrp ||
|
|
1126
2290
|
input.scene === undefined ||
|
|
1127
|
-
|
|
1128
|
-
|
|
2291
|
+
selectGpuLodLane(this.device.caps) === 'cpu' ||
|
|
2292
|
+
capability.status === 'disabled'
|
|
1129
2293
|
) {
|
|
1130
2294
|
this.gpuOwnedEntityCount = 0;
|
|
1131
|
-
this.sceneRowsRequired = false;
|
|
1132
2295
|
return ok(undefined);
|
|
1133
2296
|
}
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
2297
|
+
if (this.overflowRecoveryRequired && this.view !== undefined) {
|
|
2298
|
+
const recovered = this.view._recoverFromOverflow();
|
|
2299
|
+
if (!recovered.ok) {
|
|
2300
|
+
this.gpuOwnedEntityCount = 0;
|
|
2301
|
+
this.lane = 'blocked';
|
|
2302
|
+
this.laneReason = 'capacity';
|
|
2303
|
+
return recovered;
|
|
2304
|
+
}
|
|
2305
|
+
this.overflowRecoveryRequired = false;
|
|
2306
|
+
}
|
|
2307
|
+
const scene = input.scene;
|
|
2308
|
+
const preparationFailure = nonCpuPreparationFailure(scene);
|
|
2309
|
+
if (preparationFailure !== undefined) {
|
|
2310
|
+
this.gpuOwnedEntityCount = 0;
|
|
2311
|
+
// A capable candidate that cannot be prepared is a stopped promotion,
|
|
2312
|
+
// not a CPU-semantic lane. Keep the concrete rejected draw count in the
|
|
2313
|
+
// detached channel so an inspector can repair the producer and retry the
|
|
2314
|
+
// same view-pass without guessing from an error string.
|
|
2315
|
+
this.blockedDrawItems = preparationFailure.drawKeys.length;
|
|
2316
|
+
this.mainTotalDrawItems = Math.max(
|
|
2317
|
+
this.mainTotalDrawItems,
|
|
2318
|
+
scene.plan.batches.reduce((total, batch) => total + batch.candidates.length, 0),
|
|
2319
|
+
);
|
|
2320
|
+
this.lane = 'blocked';
|
|
2321
|
+
this.laneReason = 'resource-not-ready';
|
|
2322
|
+
this.preparationFailure = preparationFailureInspection(preparationFailure.error);
|
|
2323
|
+
return err(preparationFailure.error);
|
|
2324
|
+
}
|
|
2325
|
+
const materialArtifact = input.materialArtifact ?? input.standardPbrArtifact;
|
|
2326
|
+
const skinArtifact = input.materialSkinArtifact ?? input.standardPbrSkinArtifact;
|
|
2327
|
+
const materialPipelineState = input.materialPipelineState ?? input.standardPbrPipelineState;
|
|
2328
|
+
const artifactSignature = `${materialArtifactSignature(
|
|
2329
|
+
input.materialArtifacts,
|
|
2330
|
+
materialArtifact,
|
|
2331
|
+
skinArtifact,
|
|
2332
|
+
)}|shadow=${materialArtifactSignature(input.shadowMaterialArtifacts, undefined, undefined)}|bindings=${JSON.stringify([...(input.materialBindingClasses ?? [])])}`;
|
|
2333
|
+
if (
|
|
2334
|
+
(materialArtifact === undefined && input.materialArtifacts === undefined) ||
|
|
2335
|
+
materialPipelineState === undefined
|
|
2336
|
+
) {
|
|
2337
|
+
this.gpuOwnedEntityCount = 0;
|
|
2338
|
+
this.laneReason = 'resource-not-ready';
|
|
2339
|
+
if (sceneRequiresGpuDrivenArtifact(input.scene)) {
|
|
2340
|
+
this.blockedDrawItems = input.scene.plan.batches.reduce(
|
|
2341
|
+
(total, batch) => total + batch.candidates.length,
|
|
2342
|
+
0,
|
|
2343
|
+
);
|
|
2344
|
+
this.mainTotalDrawItems = Math.max(this.mainTotalDrawItems, this.blockedDrawItems);
|
|
2345
|
+
this.lane = 'blocked';
|
|
2346
|
+
const failure =
|
|
2347
|
+
materialArtifact === undefined && input.materialArtifacts === undefined
|
|
2348
|
+
? new GpuDrivenPreparationError('missing-material-receipt', {
|
|
2349
|
+
reason: 'material-receipt-missing',
|
|
2350
|
+
owner: 'material',
|
|
2351
|
+
expected: 'producer-owned MaterialProgramAbi GPU-driven artifact',
|
|
2352
|
+
})
|
|
2353
|
+
: new GpuDrivenPreparationError('resource-not-ready', {
|
|
2354
|
+
reason: 'material-resource-missing',
|
|
2355
|
+
owner: 'material',
|
|
2356
|
+
expected: 'producer-owned GPU-driven material pipeline state',
|
|
2357
|
+
});
|
|
2358
|
+
this.preparationFailure = preparationFailureInspection(failure);
|
|
2359
|
+
return err(failure);
|
|
2360
|
+
}
|
|
2361
|
+
return ok(undefined);
|
|
2362
|
+
}
|
|
2363
|
+
const missingArtifact = missingMaterialArtifact(
|
|
2364
|
+
scene,
|
|
2365
|
+
input.materialArtifacts,
|
|
2366
|
+
materialArtifact,
|
|
2367
|
+
skinArtifact,
|
|
2368
|
+
);
|
|
2369
|
+
if (missingArtifact !== undefined) {
|
|
2370
|
+
this.gpuOwnedEntityCount = 0;
|
|
2371
|
+
this.lane = 'blocked';
|
|
2372
|
+
this.laneReason = 'resource-not-ready';
|
|
2373
|
+
this.blockedDrawItems = scene.plan.batches.reduce(
|
|
2374
|
+
(total, batch) => total + batch.candidates.length,
|
|
2375
|
+
0,
|
|
2376
|
+
);
|
|
2377
|
+
this.preparationFailure = preparationFailureInspection(missingArtifact);
|
|
2378
|
+
return err(missingArtifact);
|
|
2379
|
+
}
|
|
2380
|
+
const projectionFingerprint =
|
|
2381
|
+
this.filteredCache?.source === scene.plan &&
|
|
2382
|
+
this.filteredCache.lodProjectionFingerprint === ''
|
|
2383
|
+
? ''
|
|
2384
|
+
: lodProjectionFingerprint(scene.plan, input.camera, scene.slots);
|
|
1143
2385
|
const cachedVisibilityMatches =
|
|
1144
|
-
|
|
2386
|
+
this.filteredCache === undefined
|
|
1145
2387
|
? true
|
|
1146
2388
|
: input.activeEntityRevision !== undefined &&
|
|
1147
2389
|
this.filteredCache.activeEntityRevision !== undefined
|
|
@@ -1151,22 +2393,34 @@ export class GpuDrivenProduction {
|
|
|
1151
2393
|
if (
|
|
1152
2394
|
filtered === undefined ||
|
|
1153
2395
|
this.filteredCache?.source !== scene.plan ||
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
2396
|
+
this.filteredCache?.scene !== scene.scene ||
|
|
2397
|
+
this.filteredCache?.slots !== scene.slots ||
|
|
2398
|
+
this.filteredCache?.worldKeys !== scene.worldKeys ||
|
|
2399
|
+
this.filteredCache.meshResidencyEpoch !== input.meshResidencyEpoch ||
|
|
2400
|
+
this.filteredCache.lodProjectionFingerprint !== projectionFingerprint ||
|
|
2401
|
+
!cachedVisibilityMatches ||
|
|
2402
|
+
this.filteredCache.skinArtifact !== skinArtifact ||
|
|
2403
|
+
this.filteredCache.materialArtifactSignature !== artifactSignature
|
|
1159
2404
|
) {
|
|
1160
2405
|
filtered = filteredPlan(
|
|
1161
2406
|
scene.plan,
|
|
1162
2407
|
scene.slots,
|
|
1163
|
-
input.
|
|
2408
|
+
input.meshBySlot,
|
|
2409
|
+
undefined,
|
|
2410
|
+
skinArtifact,
|
|
2411
|
+
undefined,
|
|
1164
2412
|
input.camera,
|
|
1165
2413
|
input.activeEntityKeys,
|
|
1166
2414
|
scene.worldKeys,
|
|
2415
|
+
materialArtifact,
|
|
2416
|
+
input.materialArtifacts,
|
|
2417
|
+
input.materialBindingClasses,
|
|
1167
2418
|
);
|
|
1168
2419
|
this.filteredCache = {
|
|
1169
2420
|
source: scene.plan,
|
|
2421
|
+
scene: scene.scene,
|
|
2422
|
+
slots: scene.slots,
|
|
2423
|
+
worldKeys: scene.worldKeys,
|
|
1170
2424
|
meshResidencyEpoch: input.meshResidencyEpoch,
|
|
1171
2425
|
lodProjectionFingerprint: projectionFingerprint,
|
|
1172
2426
|
...(input.activeEntityRevision === undefined
|
|
@@ -1175,59 +2429,126 @@ export class GpuDrivenProduction {
|
|
|
1175
2429
|
...(input.activeEntityKeys === undefined
|
|
1176
2430
|
? {}
|
|
1177
2431
|
: { activeEntityKeys: input.activeEntityKeys }),
|
|
2432
|
+
...(skinArtifact === undefined ? {} : { skinArtifact }),
|
|
2433
|
+
materialArtifactSignature: artifactSignature,
|
|
1178
2434
|
value: filtered,
|
|
1179
2435
|
};
|
|
1180
2436
|
this.gpuOwnedSnapshotsMaterialized = scene.slots.length;
|
|
1181
2437
|
this.filteredPlanBuilds = 1;
|
|
1182
2438
|
}
|
|
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.
|
|
2439
|
+
this.gpuOwnedEntityCount = gpuDrawEntityCount(filtered.drawKeys);
|
|
1189
2440
|
this.indirectDrawCount = admittedRasterBatchCount(filtered.plan);
|
|
1190
|
-
if (filtered.batches.length === 0)
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
2441
|
+
if (filtered.batches.length === 0) {
|
|
2442
|
+
this.laneReason = 'unsupported';
|
|
2443
|
+
return ok(undefined);
|
|
2444
|
+
}
|
|
2445
|
+
this.lane = 'gpu';
|
|
2446
|
+
this.laneReason = 'none';
|
|
2447
|
+
this.submissionState = 'pending';
|
|
2448
|
+
const shadowMembership = shadowCasterMembershipSignature(
|
|
2449
|
+
input.shadowCasterDrawKeys,
|
|
2450
|
+
input.shadowCasterMembership,
|
|
2451
|
+
);
|
|
2452
|
+
let shadowFiltered = this.shadowFilteredCache?.value;
|
|
2453
|
+
if (
|
|
2454
|
+
shadowFiltered === undefined ||
|
|
2455
|
+
this.shadowFilteredCache?.source !== scene.plan ||
|
|
2456
|
+
this.shadowFilteredCache.scene !== scene.scene ||
|
|
2457
|
+
this.shadowFilteredCache.slots !== scene.slots ||
|
|
2458
|
+
this.shadowFilteredCache.worldKeys !== scene.worldKeys ||
|
|
2459
|
+
this.shadowFilteredCache.lodProjectionFingerprint !== projectionFingerprint ||
|
|
2460
|
+
this.shadowFilteredCache.meshResidencyEpoch !== input.meshResidencyEpoch ||
|
|
2461
|
+
this.shadowFilteredCache.membership !== shadowMembership ||
|
|
2462
|
+
this.shadowFilteredCache.skinArtifact !== skinArtifact ||
|
|
2463
|
+
this.shadowFilteredCache.materialArtifactSignature !== artifactSignature
|
|
2464
|
+
) {
|
|
2465
|
+
const projectedShadowFiltered = filteredPlan(
|
|
2466
|
+
scene.plan,
|
|
2467
|
+
scene.slots,
|
|
2468
|
+
input.meshBySlot,
|
|
2469
|
+
input.shadowCasterDrawKeys,
|
|
2470
|
+
skinArtifact,
|
|
2471
|
+
input.shadowCasterMembership,
|
|
2472
|
+
input.camera,
|
|
2473
|
+
undefined,
|
|
2474
|
+
scene.worldKeys,
|
|
2475
|
+
materialArtifact,
|
|
2476
|
+
input.materialArtifacts,
|
|
2477
|
+
);
|
|
2478
|
+
shadowFiltered = projectedShadowFiltered;
|
|
2479
|
+
this.shadowFilteredCache = {
|
|
2480
|
+
source: scene.plan,
|
|
2481
|
+
scene: scene.scene,
|
|
2482
|
+
slots: scene.slots,
|
|
2483
|
+
worldKeys: scene.worldKeys,
|
|
2484
|
+
lodProjectionFingerprint: projectionFingerprint,
|
|
2485
|
+
meshResidencyEpoch: input.meshResidencyEpoch,
|
|
2486
|
+
membership: shadowMembership,
|
|
2487
|
+
...(skinArtifact === undefined ? {} : { skinArtifact }),
|
|
2488
|
+
materialArtifactSignature: artifactSignature,
|
|
2489
|
+
value: shadowFiltered,
|
|
2490
|
+
};
|
|
2491
|
+
}
|
|
2492
|
+
this.shadowSourcePlan = shadowFiltered.plan;
|
|
2493
|
+
this.shadowScene = scene.scene;
|
|
2494
|
+
const shadowCasterKeys =
|
|
2495
|
+
input.shadowCasterMembership === undefined
|
|
2496
|
+
? input.shadowCasterDrawKeys
|
|
2497
|
+
: new Set(
|
|
2498
|
+
input.shadowCasterMembership.map((membership) =>
|
|
2499
|
+
gpuDrivenShadowDrawKey(
|
|
2500
|
+
membership.worldEntity,
|
|
2501
|
+
membership.materialHandle,
|
|
2502
|
+
membership.drawItemIndex,
|
|
2503
|
+
membership.passIndex,
|
|
2504
|
+
),
|
|
2505
|
+
),
|
|
2506
|
+
);
|
|
2507
|
+
this.shadowExpectedDrawKeys = new Set(shadowCasterKeys ?? []);
|
|
2508
|
+
const ownsAllShadowCasters =
|
|
2509
|
+
shadowCasterKeys === undefined ||
|
|
2510
|
+
[...shadowCasterKeys].every((key) => shadowFiltered.shadowDrawKeys.has(key));
|
|
2511
|
+
if (this.shadowViews === undefined) {
|
|
2512
|
+
const created = ShadowViewStatePool.create({
|
|
2513
|
+
device: this.device,
|
|
2514
|
+
shaderModuleFactory: this.shaderModuleFactory,
|
|
2515
|
+
});
|
|
2516
|
+
if (!created.ok) return created;
|
|
2517
|
+
this.shadowViews = created.value;
|
|
2518
|
+
}
|
|
2519
|
+
const projectionSource = {
|
|
2520
|
+
filtered,
|
|
2521
|
+
scene: scene.scene,
|
|
2522
|
+
contentRevision: scene.scene.contentRevision,
|
|
2523
|
+
materialArtifactSignature: artifactSignature,
|
|
2524
|
+
};
|
|
2525
|
+
const previousSource = this.committedProjectionSource;
|
|
2526
|
+
const reuseProjectionRows =
|
|
2527
|
+
previousSource?.filtered === filtered &&
|
|
2528
|
+
previousSource.scene === scene.scene &&
|
|
2529
|
+
previousSource.contentRevision === scene.scene.contentRevision &&
|
|
2530
|
+
previousSource.materialArtifactSignature === artifactSignature;
|
|
2531
|
+
if (!reuseProjectionRows) {
|
|
1194
2532
|
const slotsByPrimitive = new Map(scene.slots.map((slot) => [slot.slot, slot] as const));
|
|
2533
|
+
const representative = filtered.batches[0];
|
|
2534
|
+
if (representative === undefined) return ok(undefined);
|
|
1195
2535
|
const projection = projectStandardPbrScene({
|
|
1196
|
-
artifact:
|
|
1197
|
-
candidates:
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
? []
|
|
1202
|
-
: [
|
|
1203
|
-
{
|
|
1204
|
-
primitiveIndex: candidate.primitiveIndex,
|
|
1205
|
-
world: slot.snapshot.transform.world,
|
|
1206
|
-
},
|
|
1207
|
-
];
|
|
1208
|
-
}),
|
|
1209
|
-
),
|
|
2536
|
+
artifact: representative.artifact,
|
|
2537
|
+
candidates: representative.batch.candidates.flatMap((candidate) => {
|
|
2538
|
+
const slot = slotsByPrimitive.get(candidate.primitiveIndex);
|
|
2539
|
+
return slot === undefined ? [] : [sceneCandidate(slot, candidate)];
|
|
2540
|
+
}),
|
|
1210
2541
|
});
|
|
1211
2542
|
if (!projection.ok) {
|
|
1212
2543
|
return err(
|
|
1213
2544
|
new RhiError({
|
|
1214
2545
|
code: 'shader-compile-failed',
|
|
1215
|
-
expected: 'receipt-derived
|
|
2546
|
+
expected: 'receipt-derived material scene projection',
|
|
1216
2547
|
hint: 'publish one validated MaterialShaderArtifact receipt before entering the GPU lane',
|
|
1217
2548
|
}),
|
|
1218
2549
|
);
|
|
1219
2550
|
}
|
|
1220
2551
|
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
2552
|
}
|
|
1232
2553
|
if (this.view === undefined) {
|
|
1233
2554
|
const created = GpuDrivenView.create({
|
|
@@ -1237,22 +2558,210 @@ export class GpuDrivenProduction {
|
|
|
1237
2558
|
if (!created.ok) return created;
|
|
1238
2559
|
this.view = created.value;
|
|
1239
2560
|
}
|
|
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
2561
|
const updated = this.view.update(filtered.plan, scene.scene, activeFrustum(input.camera));
|
|
1250
2562
|
if (!updated.ok) return updated;
|
|
1251
2563
|
this.telemetryPrepared = true;
|
|
1252
2564
|
const view = this.view;
|
|
1253
|
-
const
|
|
1254
|
-
|
|
1255
|
-
|
|
2565
|
+
const visibleBuffer = view.visibleBuffer;
|
|
2566
|
+
if (visibleBuffer === undefined) {
|
|
2567
|
+
return err(
|
|
2568
|
+
new RhiError({
|
|
2569
|
+
code: 'internal-error',
|
|
2570
|
+
expected: 'GpuDrivenView visible buffer after update',
|
|
2571
|
+
hint: 'keep the visible-index binding request on the updated GPU scene view',
|
|
2572
|
+
}),
|
|
2573
|
+
);
|
|
2574
|
+
}
|
|
2575
|
+
const representativeArtifact = materialArtifact ?? filtered.batches[0]?.artifact;
|
|
2576
|
+
if (representativeArtifact === undefined) {
|
|
2577
|
+
return err(
|
|
2578
|
+
new RhiError({
|
|
2579
|
+
code: 'rhi-not-available',
|
|
2580
|
+
expected: 'one selected MaterialProgramAbi artifact for GPU-driven batches',
|
|
2581
|
+
hint: 'keep receipt-backed draws blocked until Catalog/load publishes their artifact',
|
|
2582
|
+
}),
|
|
2583
|
+
);
|
|
2584
|
+
}
|
|
2585
|
+
const committedArtifactChanged =
|
|
2586
|
+
this.committedProjectionSource !== undefined &&
|
|
2587
|
+
this.committedProjectionSource.materialArtifactSignature !== artifactSignature;
|
|
2588
|
+
if (committedArtifactChanged) {
|
|
2589
|
+
// ShadowViewStatePool's cache token is light/view scoped and cannot see
|
|
2590
|
+
// a material publication replacement. Drop the retained projections at
|
|
2591
|
+
// the same boundary as the main PSO cache so a cache hit cannot replay
|
|
2592
|
+
// the previous shadow program or BGL with the new receipt.
|
|
2593
|
+
this.shadowBatchProjections.clear();
|
|
2594
|
+
this.shadowBatchProjectionSource = undefined;
|
|
2595
|
+
this.shadowViews?.invalidate();
|
|
2596
|
+
}
|
|
2597
|
+
if (this.committedProjectionSource?.materialArtifactSignature !== artifactSignature) {
|
|
2598
|
+
this.materialRaster?.invalidatePipelineCache();
|
|
2599
|
+
this.skinRaster?.invalidatePipelineCache();
|
|
2600
|
+
}
|
|
2601
|
+
if (this.materialRaster === undefined) {
|
|
2602
|
+
const created = MaterialAbiRasterAdapter.create({
|
|
2603
|
+
device: this.device,
|
|
2604
|
+
pipelineState: materialPipelineState,
|
|
2605
|
+
artifact: representativeArtifact,
|
|
2606
|
+
visibleBuffer,
|
|
2607
|
+
...(input.materialPipelineFactory === undefined
|
|
2608
|
+
? {}
|
|
2609
|
+
: { pipelineFactory: input.materialPipelineFactory }),
|
|
2610
|
+
});
|
|
2611
|
+
if (!created.ok) return created;
|
|
2612
|
+
this.materialRaster = created.value;
|
|
2613
|
+
} else {
|
|
2614
|
+
const rebound = this.materialRaster.ensureVisibleBuffer(visibleBuffer);
|
|
2615
|
+
if (!rebound.ok) return rebound;
|
|
2616
|
+
}
|
|
2617
|
+
const materialRaster = this.materialRaster;
|
|
2618
|
+
if (materialRaster === undefined) {
|
|
2619
|
+
return err(
|
|
2620
|
+
new RhiError({
|
|
2621
|
+
code: 'internal-error',
|
|
2622
|
+
expected: 'material ABI raster adapter after prepared artifact admission',
|
|
2623
|
+
hint: 'keep the selected material artifact and PipelineState on one frame identity',
|
|
2624
|
+
}),
|
|
2625
|
+
);
|
|
2626
|
+
}
|
|
2627
|
+
const skinBatch = filtered.batches.find(
|
|
2628
|
+
({ batch }) => batch.candidates[0]?.prepared?.identity.deformation === 'skin',
|
|
2629
|
+
);
|
|
2630
|
+
if (skinBatch !== undefined) {
|
|
2631
|
+
const skinSnapshot = scene.slots.find(
|
|
2632
|
+
(slot) => slot.slot === skinBatch.batch.candidates[0]?.primitiveIndex,
|
|
2633
|
+
)?.snapshot.skin;
|
|
2634
|
+
const skinAllocator = materialPipelineState.skinPaletteAllocator;
|
|
2635
|
+
const selectedSkinArtifact = skinArtifact ?? skinBatch.artifact;
|
|
2636
|
+
if (
|
|
2637
|
+
selectedSkinArtifact === undefined ||
|
|
2638
|
+
skinSnapshot === undefined ||
|
|
2639
|
+
skinAllocator === null ||
|
|
2640
|
+
skinAllocator === undefined
|
|
2641
|
+
) {
|
|
2642
|
+
return err(
|
|
2643
|
+
new RhiError({
|
|
2644
|
+
code: 'rhi-not-available',
|
|
2645
|
+
expected: 'skinned material artifact and persistent palette owner',
|
|
2646
|
+
hint: 'keep missing skin producer resources on the CPU deformation path',
|
|
2647
|
+
}),
|
|
2648
|
+
);
|
|
2649
|
+
}
|
|
2650
|
+
if (this.skinRaster === undefined) {
|
|
2651
|
+
const created = MaterialAbiRasterAdapter.create({
|
|
2652
|
+
device: this.device,
|
|
2653
|
+
pipelineState: materialPipelineState,
|
|
2654
|
+
artifact: selectedSkinArtifact,
|
|
2655
|
+
visibleBuffer,
|
|
2656
|
+
deformation: 'skin',
|
|
2657
|
+
paletteBuffer: skinSnapshot.buffer,
|
|
2658
|
+
paletteBindingWindowBytes: skinAllocator.bindingWindowBytes,
|
|
2659
|
+
...(input.materialPipelineFactory === undefined
|
|
2660
|
+
? {}
|
|
2661
|
+
: { pipelineFactory: input.materialPipelineFactory }),
|
|
2662
|
+
});
|
|
2663
|
+
if (!created.ok) return created;
|
|
2664
|
+
this.skinRaster = created.value;
|
|
2665
|
+
} else {
|
|
2666
|
+
const rebound = this.skinRaster.ensureVisibleBuffer(visibleBuffer);
|
|
2667
|
+
if (!rebound.ok) return rebound;
|
|
2668
|
+
const palette = this.skinRaster.ensurePaletteBuffer(
|
|
2669
|
+
skinSnapshot.buffer,
|
|
2670
|
+
skinAllocator.bindingWindowBytes,
|
|
2671
|
+
);
|
|
2672
|
+
if (!palette.ok) return palette;
|
|
2673
|
+
}
|
|
2674
|
+
if (this.skinPaletteBuffer !== skinSnapshot.buffer) {
|
|
2675
|
+
this.skinPaletteBuffer = skinSnapshot.buffer;
|
|
2676
|
+
this.shadowBatchProjections.clear();
|
|
2677
|
+
this.shadowBatchProjectionSource = undefined;
|
|
2678
|
+
this.shadowViews?.invalidate();
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
const skinRaster = this.skinRaster;
|
|
2682
|
+
const shadowViewPool = this.shadowViews;
|
|
2683
|
+
if (shadowViewPool === undefined) {
|
|
2684
|
+
return err(
|
|
2685
|
+
new RhiError({
|
|
2686
|
+
code: 'internal-error',
|
|
2687
|
+
expected: 'ShadowViewStatePool after GPU-driven preparation',
|
|
2688
|
+
hint: 'publish view-keyed shadow state with the prepared frame',
|
|
2689
|
+
}),
|
|
2690
|
+
);
|
|
2691
|
+
}
|
|
2692
|
+
// A pose can change while conservative bounds, allocation and scene rows
|
|
2693
|
+
// remain unchanged. Read the palette owner, never optional telemetry.
|
|
2694
|
+
const paletteContentRevision = materialPipelineState.skinPaletteAllocator?.contentRevision;
|
|
2695
|
+
if (paletteContentRevision !== this.committedPaletteContentRevision)
|
|
2696
|
+
shadowViewPool.invalidate();
|
|
2697
|
+
const abortProvisionalResources = (): void => {
|
|
2698
|
+
materialRaster.abortProjectionBindings();
|
|
2699
|
+
skinRaster?.abortProjectionBindings();
|
|
2700
|
+
shadowViewPool._abortResourceReplacement();
|
|
2701
|
+
};
|
|
2702
|
+
// The render graph is cached by topology, while transforms are frame
|
|
2703
|
+
// data. Refresh retained scene-row buffers here so a cached graph reads
|
|
2704
|
+
// the current world matrices without forcing a topology rebuild.
|
|
2705
|
+
let slotsByPrimitive: Map<number, PersistentGpuDrivenState['slots'][number]> | undefined;
|
|
2706
|
+
const projectionSlot = (index: number) => {
|
|
2707
|
+
slotsByPrimitive ??= new Map(scene.slots.map((slot) => [slot.slot, slot] as const));
|
|
2708
|
+
return slotsByPrimitive.get(index);
|
|
2709
|
+
};
|
|
2710
|
+
// Start the provisional resource set before any retained row is updated;
|
|
2711
|
+
// an aborted graph can then restore the active rows from its snapshot.
|
|
2712
|
+
materialRaster.stageProjectionKeys(new Set());
|
|
2713
|
+
skinRaster?.stageProjectionKeys(new Set());
|
|
2714
|
+
for (const prepared of filtered.batches) {
|
|
2715
|
+
if (reuseProjectionRows) {
|
|
2716
|
+
const deformation = prepared.batch.candidates[0]?.prepared?.identity.deformation ?? 'rigid';
|
|
2717
|
+
const raster = deformation === 'skin' ? skinRaster : materialRaster;
|
|
2718
|
+
const artifact = prepared.artifact;
|
|
2719
|
+
if (raster !== undefined && artifact !== undefined)
|
|
2720
|
+
raster.noteProjectionKey(gpuDrivenProjectionKey(prepared.batch, deformation, artifact));
|
|
2721
|
+
continue;
|
|
2722
|
+
}
|
|
2723
|
+
const admittedCandidates = prepared.batch.candidates.filter(
|
|
2724
|
+
(candidate) =>
|
|
2725
|
+
(candidate as GpuDrivenCandidate & { readonly submitAdmission?: boolean })
|
|
2726
|
+
.submitAdmission !== false,
|
|
2727
|
+
);
|
|
2728
|
+
if (admittedCandidates.length === 0) continue;
|
|
2729
|
+
const deformation = admittedCandidates[0]?.prepared?.identity.deformation ?? 'rigid';
|
|
2730
|
+
const raster = deformation === 'skin' ? skinRaster : materialRaster;
|
|
2731
|
+
const artifact = prepared.artifact;
|
|
2732
|
+
if (raster === undefined) continue;
|
|
2733
|
+
const projectionKey = gpuDrivenProjectionKey(prepared.batch, deformation, artifact);
|
|
2734
|
+
raster.noteProjectionKey(projectionKey);
|
|
2735
|
+
const candidates = admittedCandidates.flatMap((candidate) => {
|
|
2736
|
+
const slot = projectionSlot(candidate.primitiveIndex);
|
|
2737
|
+
return slot === undefined ? [] : [sceneCandidate(slot, candidate)];
|
|
2738
|
+
});
|
|
2739
|
+
const projection = projectStandardPbrScene({ artifact, candidates });
|
|
2740
|
+
if (!projection.ok) {
|
|
2741
|
+
abortProvisionalResources();
|
|
2742
|
+
return err(
|
|
2743
|
+
new RhiError({
|
|
2744
|
+
code: 'shader-compile-failed',
|
|
2745
|
+
expected: 'receipt-derived material scene projection',
|
|
2746
|
+
hint: 'keep the retained projection rows on the current material receipt',
|
|
2747
|
+
}),
|
|
2748
|
+
);
|
|
2749
|
+
}
|
|
2750
|
+
const refreshed = raster.refreshProjectionRows(projectionKey, projection.value.rows);
|
|
2751
|
+
if (!refreshed.ok) {
|
|
2752
|
+
abortProvisionalResources();
|
|
2753
|
+
return refreshed;
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
// Bindings created by the graph projection below are provisional until
|
|
2757
|
+
// the frame submits. A successful commit promotes exactly these keys and
|
|
2758
|
+
// fence-retires any superseded batch generation; an abort keeps the last
|
|
2759
|
+
// known-good active set and drops only staged bindings. Shadow view update
|
|
2760
|
+
// adds its keyed projections to the same staged set.
|
|
2761
|
+
if (this.shadowBatchProjectionSource !== shadowFiltered.plan) {
|
|
2762
|
+
this.shadowBatchProjections.clear();
|
|
2763
|
+
this.shadowBatchProjectionSource = shadowFiltered.plan;
|
|
2764
|
+
}
|
|
1256
2765
|
const sceneIdentity = this.sceneIdentity(scene.scene);
|
|
1257
2766
|
const viewInspection = view.inspect();
|
|
1258
2767
|
let signature = this.signatureCache?.value;
|
|
@@ -1267,26 +2776,43 @@ export class GpuDrivenProduction {
|
|
|
1267
2776
|
sceneIdentity,
|
|
1268
2777
|
revision: filtered.plan.revision,
|
|
1269
2778
|
meshResidencyEpoch: input.meshResidencyEpoch,
|
|
2779
|
+
materialArtifactSignature: artifactSignature,
|
|
1270
2780
|
standardPbrLayoutIdentity: this.standardPbrProjection?.layoutIdentity,
|
|
1271
2781
|
viewResourceGeneration: viewInspection.resourceGeneration,
|
|
1272
|
-
batches: filtered.batches.map(({ batch, mesh }) => [
|
|
2782
|
+
batches: filtered.batches.map(({ batch, mesh, artifact }) => [
|
|
1273
2783
|
batch.batchId,
|
|
1274
2784
|
batch.generation,
|
|
1275
|
-
batch.visibleCapacity,
|
|
1276
2785
|
batch.visibleBase,
|
|
2786
|
+
batch.visibleCapacity,
|
|
1277
2787
|
batch.candidates.map((candidate) => [
|
|
1278
2788
|
candidate.primitiveIndex,
|
|
1279
2789
|
candidate.generation,
|
|
1280
2790
|
candidate.instanceOrdinal,
|
|
1281
2791
|
]),
|
|
2792
|
+
batch.candidates
|
|
2793
|
+
.filter(
|
|
2794
|
+
(candidate) =>
|
|
2795
|
+
(candidate as GpuDrivenCandidate & { readonly submitAdmission?: boolean })
|
|
2796
|
+
.submitAdmission !== false,
|
|
2797
|
+
)
|
|
2798
|
+
.map((candidate) => [
|
|
2799
|
+
candidate.primitiveIndex,
|
|
2800
|
+
candidate.generation,
|
|
2801
|
+
candidate.instanceOrdinal,
|
|
2802
|
+
]),
|
|
1282
2803
|
mesh.vboBytes,
|
|
1283
2804
|
mesh.iboBytes,
|
|
1284
2805
|
mesh.indexFormat,
|
|
1285
2806
|
mesh.uvSetCount,
|
|
2807
|
+
artifact.material,
|
|
2808
|
+
artifact.layoutIdentity,
|
|
2809
|
+
artifact.receipt?.receiptIdentity,
|
|
2810
|
+
artifact.receipt?.generation,
|
|
2811
|
+
artifact.variantSet,
|
|
2812
|
+
artifact.wgsl.length,
|
|
1286
2813
|
]),
|
|
1287
2814
|
capacities: [
|
|
1288
2815
|
viewInspection.candidateCapacity,
|
|
1289
|
-
viewInspection.visibleBufferCapacity,
|
|
1290
2816
|
viewInspection.batchCapacity,
|
|
1291
2817
|
viewInspection.indirectCapacity,
|
|
1292
2818
|
],
|
|
@@ -1299,31 +2825,102 @@ export class GpuDrivenProduction {
|
|
|
1299
2825
|
value: signature,
|
|
1300
2826
|
};
|
|
1301
2827
|
}
|
|
2828
|
+
const shadowTopologySignature = () =>
|
|
2829
|
+
this.shadowViews
|
|
2830
|
+
?.inspect()
|
|
2831
|
+
.map(
|
|
2832
|
+
(entry) =>
|
|
2833
|
+
`${shadowViewIdentityKey(entry.identity)}:${entry.cache}:${entry.generation}:${entry.sourceRevision}:${entry.resourceGeneration}`,
|
|
2834
|
+
)
|
|
2835
|
+
.join('|') ?? '';
|
|
2836
|
+
const production = this;
|
|
1302
2837
|
return ok({
|
|
1303
|
-
topologySignature
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
: filtered.entityKeys.size === input.activeEntityKeys.size,
|
|
2838
|
+
get topologySignature() {
|
|
2839
|
+
const shadow = shadowTopologySignature();
|
|
2840
|
+
return shadow.length === 0 ? signature : `${signature}|shadow=${shadow}`;
|
|
2841
|
+
},
|
|
2842
|
+
ownsAllDrawItems: filtered.ownsAllDrawItems,
|
|
1309
2843
|
...(input.occlusion === undefined ? {} : { occlusion: input.occlusion }),
|
|
1310
|
-
|
|
1311
|
-
|
|
2844
|
+
ownsAllShadowCasters,
|
|
2845
|
+
drawKeys: filtered.drawKeys,
|
|
2846
|
+
shadowDrawKeys: shadowFiltered.shadowDrawKeys,
|
|
2847
|
+
get shadowDrawKeysByView() {
|
|
2848
|
+
return new Map(production.shadowDrawKeysByView);
|
|
2849
|
+
},
|
|
2850
|
+
standardPbrFrameResources: {
|
|
2851
|
+
materialBindGroups: [],
|
|
2852
|
+
materialSlotIndicesByEntity: new Map(),
|
|
2853
|
+
instancesBindGroup: materialRaster.instancesFrameGroup,
|
|
2854
|
+
materialStride: MATERIAL_PER_ENTITY_STRIDE,
|
|
2855
|
+
sceneMaterialBuffer: scene.scene.materialBuffer,
|
|
2856
|
+
},
|
|
2857
|
+
_commitGpuResourceReplacement: () => {
|
|
2858
|
+
view._commitResourceReplacement();
|
|
2859
|
+
},
|
|
2860
|
+
_commitResourceReplacement: () => {
|
|
2861
|
+
view._commitResourceReplacement();
|
|
2862
|
+
shadowViewPool._commitResourceReplacement();
|
|
2863
|
+
materialRaster.commitProjectionBindings();
|
|
2864
|
+
skinRaster?.commitProjectionBindings();
|
|
2865
|
+
this.committedProjectionSource = projectionSource;
|
|
2866
|
+
this.committedPaletteContentRevision = paletteContentRevision;
|
|
2867
|
+
this.submissionState = 'submitted';
|
|
2868
|
+
this.lastKnownGoodGeneration = view.inspect().resourceGeneration;
|
|
2869
|
+
},
|
|
2870
|
+
_abortResourceReplacement: () => {
|
|
2871
|
+
shadowViewPool._abortResourceReplacement();
|
|
2872
|
+
materialRaster.abortProjectionBindings();
|
|
2873
|
+
skinRaster?.abortProjectionBindings();
|
|
2874
|
+
this.committedProjectionSource = undefined;
|
|
2875
|
+
this.submissionState = 'aborted';
|
|
2876
|
+
this.retryCount += 1;
|
|
2877
|
+
},
|
|
2878
|
+
projectShadow: (graph, identity) => {
|
|
2879
|
+
if (!shadowViewPool.isActive(identity) || shadowViewPool.submission(identity) === undefined)
|
|
2880
|
+
return ok(undefined);
|
|
2881
|
+
return shadowViewPool.project(graph, identity);
|
|
2882
|
+
},
|
|
2883
|
+
updateShadowViews: (views) =>
|
|
2884
|
+
shadowFiltered.plan.batches.length > 0
|
|
2885
|
+
? this.updateShadowViews(
|
|
2886
|
+
views,
|
|
2887
|
+
materialArtifact,
|
|
2888
|
+
skinArtifact,
|
|
2889
|
+
input.materialArtifacts,
|
|
2890
|
+
input.shadowMaterialArtifacts,
|
|
2891
|
+
shadowFiltered,
|
|
2892
|
+
scene,
|
|
2893
|
+
input.shadowCasterDrawKeys,
|
|
2894
|
+
input.shadowCasterMembership,
|
|
2895
|
+
)
|
|
2896
|
+
: this.disableShadowViews(),
|
|
2897
|
+
shadowViewPool,
|
|
2898
|
+
get shadowBatchProjections() {
|
|
2899
|
+
return new Map(
|
|
2900
|
+
[...production.shadowBatchProjections].map(
|
|
2901
|
+
([key, projections]) => [key, new Map(projections)] as const,
|
|
2902
|
+
),
|
|
2903
|
+
);
|
|
2904
|
+
},
|
|
2905
|
+
project: (graph, format, sampleCount, additionalColorFormats) => {
|
|
1312
2906
|
this.indirectDrawCount = admittedRasterBatchCount(filtered.plan);
|
|
1313
2907
|
const outputs = view.addPasses(graph);
|
|
1314
2908
|
if (!outputs.ok) return outputs;
|
|
1315
|
-
if (
|
|
1316
|
-
usesStandardPbrRaster &&
|
|
1317
|
-
standardRaster !== undefined &&
|
|
1318
|
-
standardArtifact !== undefined
|
|
1319
|
-
) {
|
|
2909
|
+
if (materialRaster !== undefined && filtered.batches.length > 0) {
|
|
1320
2910
|
const standardProjected: Array<{
|
|
1321
2911
|
readonly batch: GpuDrivenBatch;
|
|
1322
2912
|
readonly mesh: MeshGpuHandles;
|
|
2913
|
+
readonly materialEntityKey: number;
|
|
1323
2914
|
readonly vertex: GraphBuffer;
|
|
1324
2915
|
readonly index?: GraphBuffer;
|
|
1325
|
-
readonly
|
|
2916
|
+
readonly pipelineFor: (
|
|
2917
|
+
formats: readonly TextureFormat[],
|
|
2918
|
+
) => Result<RenderPipeline, RhiError>;
|
|
1326
2919
|
readonly meshBindGroup: BindGroup;
|
|
2920
|
+
readonly raster: MaterialAbiRasterAdapter;
|
|
2921
|
+
readonly deformation: 'rigid' | 'skin';
|
|
2922
|
+
readonly clustered: boolean;
|
|
2923
|
+
readonly projectionKey: string;
|
|
1327
2924
|
}> = [];
|
|
1328
2925
|
const standardAccesses: GraphAccess[] = [
|
|
1329
2926
|
{ resource: outputs.value.primitive, usage: 'storage-read' },
|
|
@@ -1333,42 +2930,72 @@ export class GpuDrivenProduction {
|
|
|
1333
2930
|
{ resource: outputs.value.transform, usage: 'storage-read' },
|
|
1334
2931
|
{ resource: outputs.value.indirect, usage: 'indirect-read' },
|
|
1335
2932
|
];
|
|
1336
|
-
const slotsByPrimitive = new Map(scene.slots.map((slot) => [slot.slot, slot] as const));
|
|
1337
2933
|
const meshBuffers = new Map<
|
|
1338
|
-
|
|
2934
|
+
MeshGpuHandles,
|
|
1339
2935
|
{ readonly vertex: GraphBuffer; readonly index?: GraphBuffer }
|
|
1340
2936
|
>();
|
|
1341
2937
|
for (const prepared of filtered.batches) {
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
2938
|
+
if (prepared.batch.visibleCapacity === 0) continue;
|
|
2939
|
+
const admittedCandidates = prepared.batch.candidates.filter(
|
|
2940
|
+
(candidate) =>
|
|
2941
|
+
(candidate as GpuDrivenCandidate & { readonly submitAdmission?: boolean })
|
|
2942
|
+
.submitAdmission !== false,
|
|
2943
|
+
);
|
|
2944
|
+
if (admittedCandidates.length === 0) continue;
|
|
2945
|
+
const deformation = admittedCandidates[0]?.prepared?.identity.deformation ?? 'rigid';
|
|
2946
|
+
const raster = deformation === 'skin' ? skinRaster : materialRaster;
|
|
2947
|
+
const artifact = prepared.artifact;
|
|
2948
|
+
if (raster === undefined) {
|
|
2949
|
+
return err(
|
|
2950
|
+
new RhiError({
|
|
2951
|
+
code: 'rhi-not-available',
|
|
2952
|
+
expected: `${deformation === 'skin' ? 'skinned ' : ''}published material ABI raster owner`,
|
|
2953
|
+
hint: 'publish the selected material artifact before entering the GPU lane',
|
|
2954
|
+
}),
|
|
2955
|
+
);
|
|
2956
|
+
}
|
|
2957
|
+
// The visible stream is compacted after the persistent visibility
|
|
2958
|
+
// facet applies its active-entity admission. Project the mesh rows
|
|
2959
|
+
// from that same admitted sequence so visible.x remains the
|
|
2960
|
+
// batch-local row identity even when a suppressed candidate sits
|
|
2961
|
+
// between two submitted candidates.
|
|
2962
|
+
const candidates = admittedCandidates.flatMap((candidate) => {
|
|
2963
|
+
const slot = projectionSlot(candidate.primitiveIndex);
|
|
2964
|
+
return slot === undefined ? [] : [sceneCandidate(slot, candidate)];
|
|
1352
2965
|
});
|
|
1353
2966
|
const projection = projectStandardPbrScene({
|
|
1354
|
-
artifact
|
|
2967
|
+
artifact,
|
|
1355
2968
|
candidates,
|
|
1356
2969
|
});
|
|
1357
2970
|
if (!projection.ok) {
|
|
1358
2971
|
return err(
|
|
1359
2972
|
new RhiError({
|
|
1360
2973
|
code: 'shader-compile-failed',
|
|
1361
|
-
expected: 'receipt-derived
|
|
2974
|
+
expected: 'receipt-derived material scene projection',
|
|
1362
2975
|
hint: 'keep the frame artifact receipt and projected rows on one identity',
|
|
1363
2976
|
}),
|
|
1364
2977
|
);
|
|
1365
2978
|
}
|
|
1366
|
-
const meshBindGroup =
|
|
2979
|
+
const meshBindGroup = raster.projectionBindGroup(
|
|
2980
|
+
projection.value.rows,
|
|
2981
|
+
gpuDrivenProjectionKey(prepared.batch, deformation, artifact),
|
|
2982
|
+
prepared.batch.candidates.length,
|
|
2983
|
+
);
|
|
1367
2984
|
if (!meshBindGroup.ok) return meshBindGroup;
|
|
1368
|
-
|
|
2985
|
+
const materialSlot = projectionSlot(admittedCandidates[0]?.primitiveIndex ?? -1);
|
|
2986
|
+
if (materialSlot === undefined) {
|
|
2987
|
+
return err(
|
|
2988
|
+
new RhiError({
|
|
2989
|
+
code: 'internal-error',
|
|
2990
|
+
expected: 'batch representative entity for global material slot projection',
|
|
2991
|
+
hint: 'preserve the retained scene identity when projecting the GPU batch',
|
|
2992
|
+
}),
|
|
2993
|
+
);
|
|
2994
|
+
}
|
|
2995
|
+
let buffers = meshBuffers.get(prepared.mesh);
|
|
1369
2996
|
if (buffers === undefined) {
|
|
1370
2997
|
const vertex = graph.importBuffer(
|
|
1371
|
-
`gpu-driven.standard.${prepared.batch.key.assetHandle}.vertex`,
|
|
2998
|
+
`gpu-driven.standard.${prepared.worldKey}.${prepared.batch.key.assetHandle}.vertex`,
|
|
1372
2999
|
{
|
|
1373
3000
|
size: prepared.mesh.vboBytes,
|
|
1374
3001
|
usage: GPU_BUFFER_USAGE_VERTEX | GPU_BUFFER_USAGE_COPY_DST,
|
|
@@ -1380,7 +3007,7 @@ export class GpuDrivenProduction {
|
|
|
1380
3007
|
if (prepared.mesh.indexBuffer !== null) {
|
|
1381
3008
|
const indexHandle = prepared.mesh.indexBuffer;
|
|
1382
3009
|
const imported = graph.importBuffer(
|
|
1383
|
-
`gpu-driven.standard.${prepared.batch.key.assetHandle}.index`,
|
|
3010
|
+
`gpu-driven.standard.${prepared.worldKey}.${prepared.batch.key.assetHandle}.index`,
|
|
1384
3011
|
{
|
|
1385
3012
|
size: prepared.mesh.iboBytes,
|
|
1386
3013
|
usage: GPU_BUFFER_USAGE_INDEX | GPU_BUFFER_USAGE_COPY_DST,
|
|
@@ -1391,53 +3018,144 @@ export class GpuDrivenProduction {
|
|
|
1391
3018
|
index = imported.value;
|
|
1392
3019
|
}
|
|
1393
3020
|
buffers = { vertex: vertex.value, ...(index === undefined ? {} : { index }) };
|
|
1394
|
-
meshBuffers.set(prepared.
|
|
3021
|
+
meshBuffers.set(prepared.mesh, buffers);
|
|
1395
3022
|
standardAccesses.push({ resource: buffers.vertex, usage: 'vertex-read' });
|
|
1396
3023
|
if (buffers.index !== undefined) {
|
|
1397
3024
|
standardAccesses.push({ resource: buffers.index, usage: 'index-read' });
|
|
1398
3025
|
}
|
|
1399
3026
|
}
|
|
1400
|
-
const
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
3027
|
+
const pipelineFor = (
|
|
3028
|
+
formats: readonly TextureFormat[],
|
|
3029
|
+
): Result<RenderPipeline, RhiError> => {
|
|
3030
|
+
return raster.pipeline(
|
|
3031
|
+
formats[0] ?? format,
|
|
3032
|
+
sampleCount,
|
|
3033
|
+
prepared.mesh.layoutProjection,
|
|
3034
|
+
// The producer receipt owns the selected shader's UV ABI. The
|
|
3035
|
+
// Standard count is only a fallback for legacy synthetic
|
|
3036
|
+
// fixtures; using it for a custom program would add aliases
|
|
3037
|
+
// the shader never declares (or omit a declared custom set).
|
|
3038
|
+
artifact.uvSetCount ?? materialPipelineState.standardPbrShaderUvSetCount,
|
|
3039
|
+
prepared.batch.key.topology,
|
|
3040
|
+
prepared.batch.key.drawKind === 'indexed' &&
|
|
3041
|
+
(prepared.batch.key.topology === 'line-strip' ||
|
|
3042
|
+
prepared.batch.key.topology === 'triangle-strip')
|
|
3043
|
+
? prepared.mesh.indexFormat
|
|
3044
|
+
: undefined,
|
|
3045
|
+
prepared.renderState,
|
|
3046
|
+
artifact,
|
|
3047
|
+
formats.slice(1),
|
|
3048
|
+
);
|
|
3049
|
+
};
|
|
3050
|
+
const initialPipeline = pipelineFor([format, ...(additionalColorFormats ?? [])]);
|
|
3051
|
+
if (!initialPipeline.ok) return initialPipeline;
|
|
1413
3052
|
standardProjected.push({
|
|
1414
3053
|
batch: prepared.batch,
|
|
1415
3054
|
mesh: prepared.mesh,
|
|
3055
|
+
materialEntityKey: worldEntityKey(
|
|
3056
|
+
materialSlot.snapshot.worldId,
|
|
3057
|
+
materialSlot.snapshot.entityKey,
|
|
3058
|
+
),
|
|
1416
3059
|
...buffers,
|
|
1417
|
-
|
|
3060
|
+
pipelineFor,
|
|
1418
3061
|
meshBindGroup: meshBindGroup.value,
|
|
3062
|
+
raster,
|
|
3063
|
+
deformation,
|
|
3064
|
+
clustered: ['cluster', 'skin-cluster'].includes(
|
|
3065
|
+
resolvePipelineGroup2Contract(artifact.wgsl),
|
|
3066
|
+
),
|
|
3067
|
+
projectionKey: gpuDrivenProjectionKey(prepared.batch, deformation, artifact),
|
|
1419
3068
|
});
|
|
1420
3069
|
}
|
|
1421
|
-
|
|
3070
|
+
if (
|
|
3071
|
+
standardProjected.length === 0 ||
|
|
3072
|
+
standardProjected.length !== filtered.batches.length
|
|
3073
|
+
) {
|
|
3074
|
+
return err(
|
|
3075
|
+
new RhiError({
|
|
3076
|
+
code: 'internal-error',
|
|
3077
|
+
expected: 'one actual material ABI raster dispatch for each eligible batch',
|
|
3078
|
+
hint: 'repair the receipt-backed batch projection before encoding the frame',
|
|
3079
|
+
}),
|
|
3080
|
+
);
|
|
3081
|
+
}
|
|
3082
|
+
const rasterDispatches = Object.freeze(standardProjected);
|
|
1422
3083
|
return ok({
|
|
1423
3084
|
accesses: standardAccesses,
|
|
1424
|
-
encode: (
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
3085
|
+
encode: (
|
|
3086
|
+
viewBindGroup,
|
|
3087
|
+
pass,
|
|
3088
|
+
resources: GraphResourceResolver,
|
|
3089
|
+
frameResources: GpuDrivenStandardPbrFrameResources,
|
|
3090
|
+
) => {
|
|
3091
|
+
// The shared Standard view BGL carries two dynamic buffers:
|
|
3092
|
+
// the frame view UBO and the per-frame shadow/light table. GPU
|
|
3093
|
+
// indirect draws use the same zero offsets as direct draws;
|
|
3094
|
+
// supplying only one offset makes Dawn reject the command
|
|
3095
|
+
// buffer before the indirect work can reach the rasterizer.
|
|
1429
3096
|
pass.setBindGroup(0, viewBindGroup, [0, 0]);
|
|
1430
|
-
pass.setBindGroup(1, materialBindGroup, [0]);
|
|
1431
|
-
pass.setBindGroup(3, instancesBindGroup);
|
|
1432
3097
|
let currentPipeline: RenderPipeline | undefined;
|
|
1433
3098
|
let currentVertex: GraphBuffer | undefined;
|
|
1434
3099
|
let currentIndex: GraphBuffer | undefined;
|
|
1435
|
-
for (const batch of
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
3100
|
+
for (const batch of rasterDispatches) {
|
|
3101
|
+
const globalMaterialSlots = frameResources.materialSlotIndicesByEntity.get(
|
|
3102
|
+
batch.materialEntityKey,
|
|
3103
|
+
);
|
|
3104
|
+
const globalMaterialSlot =
|
|
3105
|
+
globalMaterialSlots?.[batch.batch.key.materialSlot] ??
|
|
3106
|
+
(frameResources.materialSlotIndicesByEntity.size === 0
|
|
3107
|
+
? batch.batch.key.materialSlot
|
|
3108
|
+
: undefined);
|
|
3109
|
+
if (globalMaterialSlot === undefined) {
|
|
3110
|
+
throw new RhiError({
|
|
3111
|
+
code: 'rhi-not-available',
|
|
3112
|
+
expected: `frame-global material slot for entity ${batch.materialEntityKey}`,
|
|
3113
|
+
hint: 'populate materialSlotIndicesByEntity from the main-pass material producer',
|
|
3114
|
+
});
|
|
3115
|
+
}
|
|
3116
|
+
if (
|
|
3117
|
+
frameResources.selectedMaterialSlots !== undefined &&
|
|
3118
|
+
!frameResources.selectedMaterialSlots.has(globalMaterialSlot)
|
|
3119
|
+
)
|
|
3120
|
+
continue;
|
|
3121
|
+
const selectedPipeline = batch.pipelineFor(
|
|
3122
|
+
frameResources.colorFormats ?? [format, ...(additionalColorFormats ?? [])],
|
|
3123
|
+
);
|
|
3124
|
+
if (!selectedPipeline.ok) throw selectedPipeline.error;
|
|
3125
|
+
const pipeline = selectedPipeline.value;
|
|
3126
|
+
if (currentPipeline !== pipeline) {
|
|
3127
|
+
pass.setPipeline(pipeline);
|
|
3128
|
+
currentPipeline = pipeline;
|
|
1439
3129
|
}
|
|
1440
|
-
|
|
3130
|
+
const materialBindGroup = frameResources.materialBindGroups[globalMaterialSlot];
|
|
3131
|
+
if (materialBindGroup === undefined) {
|
|
3132
|
+
throw new RhiError({
|
|
3133
|
+
code: 'rhi-not-available',
|
|
3134
|
+
expected: `material bind group for global slot ${globalMaterialSlot}`,
|
|
3135
|
+
hint: 'keep GPU-driven material selection on the main-pass producer contract',
|
|
3136
|
+
});
|
|
3137
|
+
}
|
|
3138
|
+
pass.setBindGroup(1, materialBindGroup, [
|
|
3139
|
+
globalMaterialSlot * frameResources.materialStride,
|
|
3140
|
+
]);
|
|
3141
|
+
const meshBindGroup = batch.clustered
|
|
3142
|
+
? frameResources.resolveClusteredMeshBindGroup?.(
|
|
3143
|
+
batch.raster.sceneMeshBinding(batch.projectionKey),
|
|
3144
|
+
)
|
|
3145
|
+
: batch.meshBindGroup;
|
|
3146
|
+
if (meshBindGroup === undefined)
|
|
3147
|
+
throw new RhiError({
|
|
3148
|
+
code: 'rhi-not-available',
|
|
3149
|
+
expected: 'current scene-index lighting binding',
|
|
3150
|
+
hint: 'resolve clustered surface resources during the material pass',
|
|
3151
|
+
});
|
|
3152
|
+
pass.setBindGroup(2, meshBindGroup, batch.deformation === 'skin' ? [0, 0, 0] : [0]);
|
|
3153
|
+
const visibleWindow = batch.raster.visibleWindowBindGroup(
|
|
3154
|
+
batch.batch.visibleBase,
|
|
3155
|
+
batch.batch.visibleCapacity,
|
|
3156
|
+
);
|
|
3157
|
+
if (!visibleWindow.ok) throw visibleWindow.error;
|
|
3158
|
+
pass.setBindGroup(3, visibleWindow.value);
|
|
1441
3159
|
if (currentVertex !== batch.vertex) {
|
|
1442
3160
|
const vertex = resources.buffer(batch.vertex);
|
|
1443
3161
|
if (!vertex.ok) throw vertex.error;
|
|
@@ -1457,129 +3175,261 @@ export class GpuDrivenProduction {
|
|
|
1457
3175
|
} else {
|
|
1458
3176
|
pass.drawIndirect(indirect.value, batch.batch.indirectOffset);
|
|
1459
3177
|
}
|
|
3178
|
+
this.encodedIndirectDraws += 1;
|
|
1460
3179
|
}
|
|
1461
3180
|
},
|
|
1462
3181
|
});
|
|
1463
3182
|
}
|
|
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
|
-
|
|
3183
|
+
return err(
|
|
3184
|
+
new RhiError({
|
|
3185
|
+
code: 'internal-error',
|
|
3186
|
+
expected: 'material ABI raster projection branch',
|
|
3187
|
+
hint: 'preserve the selected-artifact branch as the only raster owner',
|
|
3188
|
+
}),
|
|
3189
|
+
);
|
|
3190
|
+
},
|
|
3191
|
+
});
|
|
3192
|
+
}
|
|
3193
|
+
|
|
3194
|
+
updateShadowViews(
|
|
3195
|
+
views: readonly Omit<ShadowViewUpdateInput, 'sourcePlan' | 'scene'>[],
|
|
3196
|
+
artifact: MaterialShaderArtifact | undefined,
|
|
3197
|
+
skinArtifact: MaterialShaderArtifact | undefined,
|
|
3198
|
+
materialArtifacts: ReadonlyMap<string, MaterialShaderArtifact> | undefined,
|
|
3199
|
+
shadowMaterialArtifacts: ReadonlyMap<string, MaterialShaderArtifact> | undefined,
|
|
3200
|
+
filtered: FilteredProductionPlan,
|
|
3201
|
+
scene: PersistentGpuDrivenState,
|
|
3202
|
+
shadowCasterDrawKeys: ReadonlySet<string> | undefined,
|
|
3203
|
+
shadowCasterMembership: readonly ShadowCasterMembership[] | undefined,
|
|
3204
|
+
): Result<void, RhiError> {
|
|
3205
|
+
if (
|
|
3206
|
+
this.shadowViews === undefined ||
|
|
3207
|
+
this.shadowSourcePlan === undefined ||
|
|
3208
|
+
this.shadowScene === undefined
|
|
3209
|
+
) {
|
|
3210
|
+
return ok(undefined);
|
|
3211
|
+
}
|
|
3212
|
+
const nextProjections = new Map<string, ReadonlyMap<number, GpuDrivenShadowBatchProjection>>();
|
|
3213
|
+
const nextShadowDrawKeysByView = new Map<string, ReadonlySet<string>>();
|
|
3214
|
+
const materialRaster = this.materialRaster;
|
|
3215
|
+
if (materialRaster === undefined) {
|
|
3216
|
+
return err(
|
|
3217
|
+
new RhiError({
|
|
3218
|
+
code: 'internal-error',
|
|
3219
|
+
expected: 'material ABI raster adapter for shadow projection',
|
|
3220
|
+
hint: 'prepare the selected-artifact raster owner before updating shadow views',
|
|
3221
|
+
}),
|
|
3222
|
+
);
|
|
3223
|
+
}
|
|
3224
|
+
let slotsByPrimitive: Map<number, PersistentGpuDrivenState['slots'][number]> | undefined;
|
|
3225
|
+
const preparedByBatch = new Map(filtered.batches.map((entry) => [entry.batch.batchId, entry]));
|
|
3226
|
+
for (const input of views) {
|
|
3227
|
+
const updated = this.shadowViews.update({
|
|
3228
|
+
...input,
|
|
3229
|
+
sourcePlan: this.shadowSourcePlan,
|
|
3230
|
+
scene: this.shadowScene,
|
|
3231
|
+
});
|
|
3232
|
+
if (!updated.ok) return updated;
|
|
3233
|
+
const identityKey = shadowViewIdentityKey(input.identity);
|
|
3234
|
+
const previousProjections = this.shadowBatchProjections.get(identityKey);
|
|
3235
|
+
const previousClaims = this.shadowDrawKeysByView.get(identityKey);
|
|
3236
|
+
if (
|
|
3237
|
+
updated.value.cache === 'hit' &&
|
|
3238
|
+
previousProjections !== undefined &&
|
|
3239
|
+
previousClaims !== undefined
|
|
3240
|
+
) {
|
|
3241
|
+
nextShadowDrawKeysByView.set(identityKey, previousClaims);
|
|
3242
|
+
for (const batch of updated.value.plan.batches) {
|
|
3243
|
+
const deformation = batch.candidates[0]?.prepared?.identity.deformation ?? 'rigid';
|
|
3244
|
+
const batchArtifact =
|
|
3245
|
+
preparedByBatch.get(batch.batchId)?.artifact ??
|
|
3246
|
+
artifactForPreparedCandidate(
|
|
3247
|
+
batch.candidates[0] as GpuDrivenBatch['candidates'][number],
|
|
3248
|
+
materialArtifacts,
|
|
3249
|
+
artifact,
|
|
3250
|
+
skinArtifact,
|
|
1491
3251
|
);
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
3252
|
+
const shadowArtifact = shadowArtifactForPreparedCandidate(
|
|
3253
|
+
batch.candidates[0] as GpuDrivenBatch['candidates'][number],
|
|
3254
|
+
shadowMaterialArtifacts,
|
|
3255
|
+
batchArtifact,
|
|
3256
|
+
);
|
|
3257
|
+
const raster = deformation === 'skin' ? this.skinRaster : materialRaster;
|
|
3258
|
+
if (shadowArtifact !== undefined && raster !== undefined) {
|
|
3259
|
+
raster.noteProjectionKey(
|
|
3260
|
+
gpuDrivenShadowProjectionKey(identityKey, batch, deformation, shadowArtifact),
|
|
3261
|
+
);
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
nextProjections.set(identityKey, previousProjections);
|
|
3265
|
+
continue;
|
|
3266
|
+
}
|
|
3267
|
+
slotsByPrimitive ??= new Map(scene.slots.map((slot) => [slot.slot, slot] as const));
|
|
3268
|
+
const viewClaimedKeys = new Set<string>();
|
|
3269
|
+
for (const batch of updated.value.plan.batches) {
|
|
3270
|
+
for (const candidate of batch.candidates) {
|
|
3271
|
+
const slot = slotsByPrimitive?.get(candidate.primitiveIndex);
|
|
3272
|
+
if (slot === undefined) continue;
|
|
3273
|
+
if (
|
|
3274
|
+
!shadowMembershipIsGpuCompatible(
|
|
3275
|
+
slot,
|
|
3276
|
+
candidate,
|
|
3277
|
+
shadowCasterDrawKeys,
|
|
3278
|
+
shadowCasterMembership,
|
|
3279
|
+
)
|
|
3280
|
+
) {
|
|
3281
|
+
continue;
|
|
3282
|
+
}
|
|
3283
|
+
for (const key of shadowKeysForCandidate(
|
|
3284
|
+
slot,
|
|
3285
|
+
candidate,
|
|
3286
|
+
shadowCasterDrawKeys,
|
|
3287
|
+
shadowCasterMembership,
|
|
3288
|
+
)) {
|
|
3289
|
+
if (filtered.shadowDrawKeys.has(key)) viewClaimedKeys.add(key);
|
|
1513
3290
|
}
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
nextShadowDrawKeysByView.set(identityKey, Object.freeze(viewClaimedKeys));
|
|
3294
|
+
const submission = this.shadowViews.submission(input.identity);
|
|
3295
|
+
const visibleBuffer = submission?.view.visibleBuffer;
|
|
3296
|
+
if (submission === undefined || visibleBuffer === undefined) {
|
|
3297
|
+
return err(
|
|
3298
|
+
new RhiError({
|
|
3299
|
+
code: 'internal-error',
|
|
3300
|
+
expected: 'shadow view visible buffer after update',
|
|
3301
|
+
hint: 'publish the per-view visible window before recording indirect shadow draws',
|
|
3302
|
+
}),
|
|
3303
|
+
);
|
|
3304
|
+
}
|
|
3305
|
+
const projections = new Map<number, GpuDrivenShadowBatchProjection>();
|
|
3306
|
+
for (const batch of submission.plan.batches) {
|
|
3307
|
+
const prepared = preparedByBatch.get(batch.batchId);
|
|
3308
|
+
if (prepared === undefined) {
|
|
3309
|
+
return err(
|
|
3310
|
+
new RhiError({
|
|
3311
|
+
code: 'internal-error',
|
|
3312
|
+
expected: `prepared shadow mesh batch ${batch.batchId}`,
|
|
3313
|
+
hint: 'keep filtered shadow topology and per-view projection on one frame identity',
|
|
3314
|
+
}),
|
|
1525
3315
|
);
|
|
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
3316
|
}
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
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
|
-
},
|
|
3317
|
+
const deformation = batch.candidates[0]?.prepared?.identity.deformation ?? 'rigid';
|
|
3318
|
+
const raster = deformation === 'skin' ? this.skinRaster : materialRaster;
|
|
3319
|
+
const batchArtifact = prepared.artifact;
|
|
3320
|
+
const shadowArtifact = shadowArtifactForPreparedCandidate(
|
|
3321
|
+
batch.candidates[0] as GpuDrivenBatch['candidates'][number],
|
|
3322
|
+
shadowMaterialArtifacts,
|
|
3323
|
+
batchArtifact,
|
|
3324
|
+
);
|
|
3325
|
+
if (raster === undefined || batchArtifact === undefined || shadowArtifact === undefined) {
|
|
3326
|
+
return err(
|
|
3327
|
+
new RhiError({
|
|
3328
|
+
code: 'rhi-not-available',
|
|
3329
|
+
expected: `${deformation === 'skin' ? 'skinned ' : ''}selected shadow MaterialProgramAbi artifact`,
|
|
3330
|
+
hint: 'publish a matching shadow-pass program before entering the capable shadow lane',
|
|
3331
|
+
}),
|
|
3332
|
+
);
|
|
3333
|
+
}
|
|
3334
|
+
const candidates = batch.candidates.flatMap((candidate) => {
|
|
3335
|
+
const slot = slotsByPrimitive?.get(candidate.primitiveIndex);
|
|
3336
|
+
if (slot === undefined) return [];
|
|
3337
|
+
const materialIndex = scene.scene.materialIndexForSlot(
|
|
3338
|
+
candidate.primitiveIndex,
|
|
3339
|
+
batch.key.materialSlot,
|
|
3340
|
+
);
|
|
3341
|
+
return [sceneCandidate(slot, candidate, materialIndex)];
|
|
1580
3342
|
});
|
|
1581
|
-
|
|
1582
|
-
|
|
3343
|
+
const projected = projectStandardPbrScene({ artifact: shadowArtifact, candidates });
|
|
3344
|
+
if (!projected.ok) {
|
|
3345
|
+
return err(
|
|
3346
|
+
new RhiError({
|
|
3347
|
+
code: 'shader-compile-failed',
|
|
3348
|
+
expected: 'receipt-derived shadow scene-index projection',
|
|
3349
|
+
hint: 'publish one validated material receipt per shadow batch',
|
|
3350
|
+
}),
|
|
3351
|
+
);
|
|
3352
|
+
}
|
|
3353
|
+
const projectionKey = gpuDrivenShadowProjectionKey(
|
|
3354
|
+
identityKey,
|
|
3355
|
+
batch,
|
|
3356
|
+
deformation,
|
|
3357
|
+
shadowArtifact,
|
|
3358
|
+
);
|
|
3359
|
+
raster.noteProjectionKey(projectionKey);
|
|
3360
|
+
const meshBindGroup = raster.projectionBindGroup(
|
|
3361
|
+
projected.value.rows,
|
|
3362
|
+
projectionKey,
|
|
3363
|
+
batch.candidates.length,
|
|
3364
|
+
);
|
|
3365
|
+
if (!meshBindGroup.ok) return meshBindGroup;
|
|
3366
|
+
const visibleBindGroup = raster.visibleWindowBindGroupForBuffer(
|
|
3367
|
+
visibleBuffer,
|
|
3368
|
+
batch.visibleBase,
|
|
3369
|
+
batch.visibleCapacity,
|
|
3370
|
+
);
|
|
3371
|
+
if (!visibleBindGroup.ok) return visibleBindGroup;
|
|
3372
|
+
const firstCandidate = batch.candidates[0];
|
|
3373
|
+
const firstSlot =
|
|
3374
|
+
firstCandidate === undefined
|
|
3375
|
+
? undefined
|
|
3376
|
+
: slotsByPrimitive.get(firstCandidate.primitiveIndex);
|
|
3377
|
+
const material =
|
|
3378
|
+
firstSlot?.snapshot.materials[batch.key.materialSlot] ?? firstSlot?.snapshot.material;
|
|
3379
|
+
if (firstSlot === undefined || material === undefined) {
|
|
3380
|
+
return err(
|
|
3381
|
+
new RhiError({
|
|
3382
|
+
code: 'internal-error',
|
|
3383
|
+
expected: `material snapshot for shadow batch ${batch.batchId}`,
|
|
3384
|
+
hint: 'keep the GPU-driven shadow material projection aligned with the retained slot',
|
|
3385
|
+
}),
|
|
3386
|
+
);
|
|
3387
|
+
}
|
|
3388
|
+
const firstWorldEntity = worldEntityKey(
|
|
3389
|
+
firstSlot.snapshot.worldId,
|
|
3390
|
+
firstSlot.snapshot.entityKey,
|
|
3391
|
+
);
|
|
3392
|
+
const shadowEntry = shadowCasterMembership?.find(
|
|
3393
|
+
(membership) =>
|
|
3394
|
+
membership.worldEntity === firstWorldEntity &&
|
|
3395
|
+
membership.materialHandle === (material.materialHandle ?? -1) &&
|
|
3396
|
+
membership.drawItemIndex === firstCandidate?.drawItemIndex,
|
|
3397
|
+
);
|
|
3398
|
+
projections.set(batch.batchId, {
|
|
3399
|
+
mesh: prepared.mesh,
|
|
3400
|
+
meshBindGroup: meshBindGroup.value,
|
|
3401
|
+
visibleBindGroup: visibleBindGroup.value,
|
|
3402
|
+
deformation,
|
|
3403
|
+
shadowArtifact,
|
|
3404
|
+
...(shadowEntry?.vertexEntry === undefined
|
|
3405
|
+
? {}
|
|
3406
|
+
: { shadowVertexEntry: shadowEntry.vertexEntry }),
|
|
3407
|
+
...(shadowEntry?.fragmentEntry === undefined
|
|
3408
|
+
? {}
|
|
3409
|
+
: { shadowFragmentEntry: shadowEntry.fragmentEntry }),
|
|
3410
|
+
material,
|
|
3411
|
+
materialEntityKey: worldEntityKey(
|
|
3412
|
+
firstSlot.snapshot.worldId,
|
|
3413
|
+
firstSlot.snapshot.entityKey,
|
|
3414
|
+
),
|
|
3415
|
+
vertexColorAvailable: prepared.mesh.layoutProjection.attributes.some(
|
|
3416
|
+
(attribute) => attribute.key === 'color',
|
|
3417
|
+
),
|
|
3418
|
+
});
|
|
3419
|
+
}
|
|
3420
|
+
nextProjections.set(identityKey, projections);
|
|
3421
|
+
}
|
|
3422
|
+
this.shadowViews.retain(views.map((input) => input.identity));
|
|
3423
|
+
this.shadowBatchProjections = nextProjections;
|
|
3424
|
+
this.shadowDrawKeysByView = nextShadowDrawKeysByView;
|
|
3425
|
+
return ok(undefined);
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3428
|
+
private disableShadowViews(): Result<void, RhiError> {
|
|
3429
|
+
this.shadowViews?.retain([]);
|
|
3430
|
+
this.shadowBatchProjections = new Map();
|
|
3431
|
+
this.shadowDrawKeysByView = new Map();
|
|
3432
|
+
return ok(undefined);
|
|
1583
3433
|
}
|
|
1584
3434
|
|
|
1585
3435
|
dispose(): void {
|
|
@@ -1587,20 +3437,141 @@ export class GpuDrivenProduction {
|
|
|
1587
3437
|
this.disposed = true;
|
|
1588
3438
|
this.view?.dispose();
|
|
1589
3439
|
this.view = undefined;
|
|
1590
|
-
this.
|
|
3440
|
+
this.materialRaster?.dispose();
|
|
3441
|
+
this.materialRaster = undefined;
|
|
3442
|
+
this.committedProjectionSource = undefined;
|
|
3443
|
+
this.skinRaster?.dispose();
|
|
3444
|
+
this.skinRaster = undefined;
|
|
1591
3445
|
this.filteredCache = undefined;
|
|
1592
3446
|
this.signatureCache = undefined;
|
|
1593
|
-
this.
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
3447
|
+
this.validatedResidencyCache = undefined;
|
|
3448
|
+
this.cpuValidationTelemetryCache = undefined;
|
|
3449
|
+
this.shadowViews?.dispose();
|
|
3450
|
+
this.shadowViews = undefined;
|
|
3451
|
+
this.shadowSourcePlan = undefined;
|
|
3452
|
+
this.shadowScene = undefined;
|
|
3453
|
+
this.shadowBatchProjections = new Map();
|
|
3454
|
+
this.shadowDrawKeysByView = new Map();
|
|
3455
|
+
this.shadowExpectedDrawKeys = new Set();
|
|
3456
|
+
this.shadowBatchProjectionSource = undefined;
|
|
3457
|
+
this.skinPaletteBuffer = undefined;
|
|
3458
|
+
this.committedPaletteContentRevision = undefined;
|
|
3459
|
+
this.preparationFailure = undefined;
|
|
3460
|
+
this.overflowRecoveryRequired = false;
|
|
1599
3461
|
}
|
|
1600
3462
|
|
|
1601
3463
|
inspect(): GpuDrivenProductionInspection {
|
|
1602
3464
|
const view = this.view?.inspect();
|
|
3465
|
+
const channels: GpuDrivenLaneSummary[] = [];
|
|
3466
|
+
const mainTotalDrawCount = this.mainTotalDrawItems;
|
|
3467
|
+
const mainClaimedDrawCount = this.telemetryOverflow
|
|
3468
|
+
? 0
|
|
3469
|
+
: Math.min(this.gpuOwnedDrawItems, mainTotalDrawCount);
|
|
3470
|
+
const mainResidualDrawCount = Math.max(0, mainTotalDrawCount - mainClaimedDrawCount);
|
|
3471
|
+
const mainFacts = {
|
|
3472
|
+
totalDrawCount: mainTotalDrawCount,
|
|
3473
|
+
claimedDrawCount: mainClaimedDrawCount,
|
|
3474
|
+
residualDrawCount: mainResidualDrawCount,
|
|
3475
|
+
};
|
|
3476
|
+
if (mainClaimedDrawCount > 0 || (this.lane !== 'gpu' && this.lane !== 'blocked')) {
|
|
3477
|
+
channels.push({
|
|
3478
|
+
viewPass: 'main',
|
|
3479
|
+
viewIndex: 0,
|
|
3480
|
+
lane: this.lane,
|
|
3481
|
+
reason: this.laneReason,
|
|
3482
|
+
drawCount: mainClaimedDrawCount,
|
|
3483
|
+
...mainFacts,
|
|
3484
|
+
});
|
|
3485
|
+
}
|
|
3486
|
+
if (this.cpuSemanticFallbackDrawItems > 0) {
|
|
3487
|
+
channels.push({
|
|
3488
|
+
viewPass: 'main',
|
|
3489
|
+
viewIndex: 0,
|
|
3490
|
+
lane: 'cpu-semantic',
|
|
3491
|
+
reason: 'unsupported',
|
|
3492
|
+
drawCount: this.cpuSemanticFallbackDrawItems,
|
|
3493
|
+
...mainFacts,
|
|
3494
|
+
});
|
|
3495
|
+
}
|
|
3496
|
+
if (this.cpuDeformationFallbackDrawItems > 0) {
|
|
3497
|
+
const reasons =
|
|
3498
|
+
this.cpuDeformationReasons.size === 0
|
|
3499
|
+
? ([['resource-not-ready', this.cpuDeformationFallbackDrawItems]] as const)
|
|
3500
|
+
: this.cpuDeformationReasons.entries();
|
|
3501
|
+
for (const [reason, drawCount] of reasons) {
|
|
3502
|
+
channels.push({
|
|
3503
|
+
viewPass: 'main',
|
|
3504
|
+
viewIndex: 0,
|
|
3505
|
+
lane: 'cpu-deformation',
|
|
3506
|
+
reason,
|
|
3507
|
+
drawCount,
|
|
3508
|
+
...mainFacts,
|
|
3509
|
+
});
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
if (this.blockedDrawItems > 0) {
|
|
3513
|
+
channels.push({
|
|
3514
|
+
viewPass: 'main',
|
|
3515
|
+
viewIndex: 0,
|
|
3516
|
+
lane: 'blocked',
|
|
3517
|
+
reason: 'resource-not-ready',
|
|
3518
|
+
drawCount: this.blockedDrawItems,
|
|
3519
|
+
...mainFacts,
|
|
3520
|
+
...(this.preparationFailure === undefined ? {} : { failure: this.preparationFailure }),
|
|
3521
|
+
});
|
|
3522
|
+
}
|
|
3523
|
+
for (const entry of this.shadowViews?.inspect() ?? []) {
|
|
3524
|
+
// Retained view resources outlive admission. An idle frame has no
|
|
3525
|
+
// current GPU projection and must not publish last frame's channels.
|
|
3526
|
+
if (this.submissionState === 'idle' || channels.length >= 32) break;
|
|
3527
|
+
const key = shadowViewIdentityKey(entry.identity);
|
|
3528
|
+
const claimed = this.shadowDrawKeysByView.get(key)?.size ?? 0;
|
|
3529
|
+
const total = Math.max(this.shadowExpectedDrawKeys.size, claimed);
|
|
3530
|
+
const effectiveClaimed = this.telemetryOverflow ? 0 : Math.min(claimed, total);
|
|
3531
|
+
const residual = Math.max(0, total - effectiveClaimed);
|
|
3532
|
+
if (total === 0) continue;
|
|
3533
|
+
const projected = this.shadowBatchProjections.has(key) && entry.batchCount > 0;
|
|
3534
|
+
const viewPass =
|
|
3535
|
+
entry.identity.kind === 'directional'
|
|
3536
|
+
? 'directional-shadow'
|
|
3537
|
+
: entry.identity.kind === 'point'
|
|
3538
|
+
? 'point-shadow'
|
|
3539
|
+
: 'spot-shadow';
|
|
3540
|
+
if (projected && effectiveClaimed > 0) {
|
|
3541
|
+
channels.push({
|
|
3542
|
+
viewPass,
|
|
3543
|
+
viewIndex: entry.identity.index,
|
|
3544
|
+
...(entry.identity.face === undefined ? {} : { face: entry.identity.face }),
|
|
3545
|
+
lane: this.telemetryOverflow ? 'blocked' : 'gpu',
|
|
3546
|
+
reason: this.telemetryOverflow ? 'overflow' : 'none',
|
|
3547
|
+
drawCount: effectiveClaimed,
|
|
3548
|
+
totalDrawCount: total,
|
|
3549
|
+
claimedDrawCount: effectiveClaimed,
|
|
3550
|
+
residualDrawCount: residual,
|
|
3551
|
+
});
|
|
3552
|
+
}
|
|
3553
|
+
if (residual > 0) {
|
|
3554
|
+
const failure = shadowOwnershipFailureInspection(viewPass);
|
|
3555
|
+
channels.push({
|
|
3556
|
+
viewPass,
|
|
3557
|
+
viewIndex: entry.identity.index,
|
|
3558
|
+
...(entry.identity.face === undefined ? {} : { face: entry.identity.face }),
|
|
3559
|
+
lane: 'blocked',
|
|
3560
|
+
reason: this.telemetryOverflow ? 'overflow' : 'shadow-ownership',
|
|
3561
|
+
drawCount: residual,
|
|
3562
|
+
totalDrawCount: total,
|
|
3563
|
+
claimedDrawCount: effectiveClaimed,
|
|
3564
|
+
residualDrawCount: residual,
|
|
3565
|
+
failure,
|
|
3566
|
+
});
|
|
3567
|
+
}
|
|
3568
|
+
}
|
|
1603
3569
|
return {
|
|
3570
|
+
residencyValidationScans: this.residencyValidationScans,
|
|
3571
|
+
residencyValidationCacheHits: this.residencyValidationCacheHits,
|
|
3572
|
+
cpuValidationScans: this.cpuValidationScans,
|
|
3573
|
+
cpuValidationCacheHits: this.cpuValidationCacheHits,
|
|
3574
|
+
...this.structureMetrics,
|
|
1604
3575
|
gpuOwnedSnapshotsMaterialized: this.gpuOwnedSnapshotsMaterialized,
|
|
1605
3576
|
filteredPlanBuilds: this.filteredPlanBuilds,
|
|
1606
3577
|
gpuOwnedEntityCount: this.gpuOwnedEntityCount,
|
|
@@ -1620,19 +3591,54 @@ export class GpuDrivenProduction {
|
|
|
1620
3591
|
: 0,
|
|
1621
3592
|
batchCount: view?.batchCount ?? 0,
|
|
1622
3593
|
indirectDrawCount: this.indirectDrawCount,
|
|
3594
|
+
encodedIndirectDraws: this.encodedIndirectDraws,
|
|
3595
|
+
channels: Object.freeze(channels.map((channel) => Object.freeze(channel))),
|
|
3596
|
+
resourceGeneration: view?.resourceGeneration,
|
|
3597
|
+
candidateCapacity: view?.candidateCapacity ?? 0,
|
|
3598
|
+
batchCapacity: view?.batchCapacity ?? 0,
|
|
3599
|
+
indirectCapacity: view?.indirectCapacity ?? 0,
|
|
3600
|
+
overflow: this.telemetryOverflow,
|
|
3601
|
+
submitted: this.submissionState === 'submitted',
|
|
3602
|
+
recoveryCount: this.recoveryCount,
|
|
3603
|
+
retryCount: this.retryCount,
|
|
3604
|
+
lastKnownGoodGeneration: this.lastKnownGoodGeneration,
|
|
1623
3605
|
};
|
|
1624
3606
|
}
|
|
1625
3607
|
|
|
1626
3608
|
/** Read renderer-owned LOD counters copied by the last GPU submission. */
|
|
1627
3609
|
readLodSelection(): Promise<GpuDrivenLodSelectionInspection | undefined> {
|
|
1628
3610
|
if (!this.telemetryPrepared) return Promise.resolve(undefined);
|
|
1629
|
-
|
|
3611
|
+
const view = this.view;
|
|
3612
|
+
const filtered = this.filteredCache?.value;
|
|
3613
|
+
const telemetryCandidateCount = this.telemetryCandidateCount;
|
|
3614
|
+
if (view === undefined) return Promise.resolve(undefined);
|
|
3615
|
+
return view.readLodSelection().then((selection) => {
|
|
3616
|
+
// The observer is asynchronous and prepare() may have published a new
|
|
3617
|
+
// filtered plan while mapAsync was pending. Do not attach old counters to
|
|
3618
|
+
// the new world/primitive cache; the next real copy will be observed on
|
|
3619
|
+
// its own generation.
|
|
3620
|
+
if (
|
|
3621
|
+
!this.telemetryPrepared ||
|
|
3622
|
+
this.view !== view ||
|
|
3623
|
+
this.filteredCache?.value !== filtered ||
|
|
3624
|
+
this.telemetryCandidateCount !== telemetryCandidateCount
|
|
3625
|
+
) {
|
|
3626
|
+
return undefined;
|
|
3627
|
+
}
|
|
1630
3628
|
if (selection !== undefined) {
|
|
1631
3629
|
this.telemetryGeometryWork = selection.geometryWork;
|
|
1632
3630
|
this.telemetryRootGeometryWork = selection.rootGeometryWork;
|
|
3631
|
+
this.telemetryOverflow = selection.overflow;
|
|
3632
|
+
if (selection.overflow) {
|
|
3633
|
+
if (!this.overflowRecoveryRequired) this.retryCount += 1;
|
|
3634
|
+
this.overflowRecoveryRequired = true;
|
|
3635
|
+
this.submissionState = 'aborted';
|
|
3636
|
+
this.lastKnownGoodGeneration = undefined;
|
|
3637
|
+
this.lane = 'blocked';
|
|
3638
|
+
this.laneReason = 'overflow';
|
|
3639
|
+
}
|
|
1633
3640
|
}
|
|
1634
|
-
if (selection === undefined ||
|
|
1635
|
-
const filtered = this.filteredCache?.value;
|
|
3641
|
+
if (selection === undefined || telemetryCandidateCount <= 0) return selection;
|
|
1636
3642
|
if (filtered === undefined) return selection;
|
|
1637
3643
|
const preparedByBatchId = new Map(
|
|
1638
3644
|
filtered.batches.map((prepared) => [prepared.batch.batchId, prepared] as const),
|
|
@@ -1677,8 +3683,8 @@ export class GpuDrivenProduction {
|
|
|
1677
3683
|
}
|
|
1678
3684
|
return {
|
|
1679
3685
|
...selection,
|
|
1680
|
-
candidateCount:
|
|
1681
|
-
occluded: Math.max(0,
|
|
3686
|
+
candidateCount: telemetryCandidateCount,
|
|
3687
|
+
occluded: Math.max(0, telemetryCandidateCount - selection.visible),
|
|
1682
3688
|
worldSelections: Object.freeze(
|
|
1683
3689
|
[...worldSelections.values()].map((aggregate) =>
|
|
1684
3690
|
Object.freeze({
|
|
@@ -1702,24 +3708,108 @@ export class GpuDrivenProduction {
|
|
|
1702
3708
|
|
|
1703
3709
|
recordCpuValidation(
|
|
1704
3710
|
validated: readonly { readonly source: RenderableSnapshot }[],
|
|
1705
|
-
|
|
1706
|
-
|
|
3711
|
+
gpuOwnedDrawKeys: ReadonlySet<string>,
|
|
3712
|
+
cacheKey?: string,
|
|
1707
3713
|
): void {
|
|
1708
|
-
|
|
3714
|
+
const cached = this.cpuValidationTelemetryCache;
|
|
3715
|
+
if (
|
|
3716
|
+
cacheKey !== undefined &&
|
|
3717
|
+
cached !== undefined &&
|
|
3718
|
+
cached.cacheKey === cacheKey &&
|
|
3719
|
+
cached.validated === validated &&
|
|
3720
|
+
cached.gpuOwnedDrawKeys === gpuOwnedDrawKeys
|
|
3721
|
+
) {
|
|
3722
|
+
this.applyCpuValidationTelemetry(cached);
|
|
3723
|
+
this.cpuValidationCacheHits += 1;
|
|
3724
|
+
return;
|
|
3725
|
+
}
|
|
3726
|
+
this.cpuFallbackDrawItems = 0;
|
|
3727
|
+
this.cpuSemanticFallbackDrawItems = 0;
|
|
3728
|
+
this.cpuDeformationFallbackDrawItems = 0;
|
|
3729
|
+
this.cpuDeformationReasons.clear();
|
|
3730
|
+
this.blockedDrawItems = 0;
|
|
1709
3731
|
let gpuOwnedRows = 0;
|
|
3732
|
+
let totalDrawItems = 0;
|
|
3733
|
+
const addDeformationReason = (
|
|
3734
|
+
reason: 'skin-bounds-missing' | 'skin-address-missing' | 'resource-not-ready',
|
|
3735
|
+
): void => {
|
|
3736
|
+
this.cpuDeformationReasons.set(reason, (this.cpuDeformationReasons.get(reason) ?? 0) + 1);
|
|
3737
|
+
};
|
|
1710
3738
|
for (const row of validated) {
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
3739
|
+
const draws = row.source.gpuDrivenDraws ?? [];
|
|
3740
|
+
if (draws.length === 0) {
|
|
3741
|
+
totalDrawItems += 1;
|
|
3742
|
+
this.cpuFallbackDrawItems += 1;
|
|
3743
|
+
const skinReason = skinLaneReason(row.source);
|
|
3744
|
+
if (skinReason !== undefined) {
|
|
3745
|
+
this.cpuDeformationFallbackDrawItems += 1;
|
|
3746
|
+
addDeformationReason(skinReason);
|
|
3747
|
+
} else {
|
|
3748
|
+
this.cpuSemanticFallbackDrawItems += 1;
|
|
3749
|
+
}
|
|
3750
|
+
continue;
|
|
3751
|
+
}
|
|
3752
|
+
totalDrawItems += draws.length;
|
|
3753
|
+
let gpuOwned = false;
|
|
3754
|
+
let cpuResidual = false;
|
|
3755
|
+
for (const [compactIndex, draw] of draws.entries()) {
|
|
3756
|
+
const material = row.source.materials[draw.materialSlot] ?? row.source.material;
|
|
3757
|
+
const key = gpuDrivenDrawKey(
|
|
3758
|
+
worldEntityKey(row.source.worldId, row.source.entityKey),
|
|
3759
|
+
material.materialHandle ?? -1,
|
|
3760
|
+
gpuDrivenSourceDrawItemIndex(draw, compactIndex),
|
|
3761
|
+
);
|
|
3762
|
+
if (gpuOwnedDrawKeys.has(key)) {
|
|
3763
|
+
gpuOwned = true;
|
|
3764
|
+
continue;
|
|
3765
|
+
}
|
|
3766
|
+
this.cpuFallbackDrawItems += 1;
|
|
3767
|
+
cpuResidual = true;
|
|
3768
|
+
const skinReason = skinLaneReason(row.source);
|
|
3769
|
+
if (skinReason !== undefined) {
|
|
3770
|
+
this.cpuDeformationFallbackDrawItems += 1;
|
|
3771
|
+
addDeformationReason(skinReason);
|
|
3772
|
+
} else if (draw.preparationError?.detail.recovery === 'route-cpu-lane') {
|
|
3773
|
+
// Geometry/UV incompatibility is an explicit semantic CPU lane,
|
|
3774
|
+
// not a failed capable submission.
|
|
3775
|
+
this.cpuSemanticFallbackDrawItems += 1;
|
|
3776
|
+
} else if (draw.preparationError?.detail.owner === 'skin') {
|
|
3777
|
+
this.cpuDeformationFallbackDrawItems += 1;
|
|
3778
|
+
addDeformationReason(
|
|
3779
|
+
draw.preparationError.detail.reason === 'skin-address-missing'
|
|
3780
|
+
? 'skin-address-missing'
|
|
3781
|
+
: 'resource-not-ready',
|
|
3782
|
+
);
|
|
3783
|
+
} else if (draw.preparationError !== undefined) {
|
|
3784
|
+
this.blockedDrawItems += 1;
|
|
3785
|
+
} else {
|
|
3786
|
+
this.cpuSemanticFallbackDrawItems += 1;
|
|
3787
|
+
}
|
|
1720
3788
|
}
|
|
3789
|
+
// A row is only CPU-validated when it still has a CPU residual. GPU-only
|
|
3790
|
+
// rows are already admitted by the concrete draw-key set and must not
|
|
3791
|
+
// inflate the per-frame CPU validation budget.
|
|
3792
|
+
if (gpuOwned && cpuResidual) gpuOwnedRows += 1;
|
|
1721
3793
|
}
|
|
3794
|
+
this.gpuOwnedDrawItems = gpuOwnedDrawKeys.size;
|
|
3795
|
+
this.mainTotalDrawItems = totalDrawItems;
|
|
1722
3796
|
this.validatedGpuOwnedRows = gpuOwnedRows;
|
|
3797
|
+
this.cpuValidationScans += 1;
|
|
3798
|
+
if (cacheKey !== undefined) {
|
|
3799
|
+
this.cpuValidationTelemetryCache = {
|
|
3800
|
+
cacheKey,
|
|
3801
|
+
validated,
|
|
3802
|
+
gpuOwnedDrawKeys,
|
|
3803
|
+
gpuOwnedDrawCount: this.gpuOwnedDrawItems,
|
|
3804
|
+
totalDrawItems: this.mainTotalDrawItems,
|
|
3805
|
+
gpuOwnedRows: this.validatedGpuOwnedRows,
|
|
3806
|
+
cpuFallbackDrawItems: this.cpuFallbackDrawItems,
|
|
3807
|
+
cpuSemanticFallbackDrawItems: this.cpuSemanticFallbackDrawItems,
|
|
3808
|
+
cpuDeformationFallbackDrawItems: this.cpuDeformationFallbackDrawItems,
|
|
3809
|
+
cpuDeformationReasons: new Map(this.cpuDeformationReasons),
|
|
3810
|
+
blockedDrawItems: this.blockedDrawItems,
|
|
3811
|
+
};
|
|
3812
|
+
}
|
|
1723
3813
|
}
|
|
1724
3814
|
|
|
1725
3815
|
private sceneIdentity(scene: object): number {
|