@forgeax/engine-render 0.1.21 → 0.1.24
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 +281 -38
- package/dist/assembly/backend-contract.d.ts +13 -0
- package/dist/assembly/backend-contract.d.ts.map +1 -1
- package/dist/assembly/bundler-contract.d.ts +9 -0
- package/dist/assembly/bundler-contract.d.ts.map +1 -0
- package/dist/assembly/device-feature-admission.d.ts +8 -0
- package/dist/assembly/device-feature-admission.d.ts.map +1 -1
- package/dist/assembly/extended-lighting-fallback.d.ts +13 -0
- package/dist/assembly/extended-lighting-fallback.d.ts.map +1 -0
- package/dist/assembly/factory.d.ts +7 -235
- package/dist/assembly/factory.d.ts.map +1 -1
- package/dist/assembly/host-contract.d.ts +10 -0
- package/dist/assembly/host-contract.d.ts.map +1 -1
- package/dist/assembly/material/assembly.d.ts +3 -2
- package/dist/assembly/material/assembly.d.ts.map +1 -1
- package/dist/assembly/material/standard-layer-projection.d.ts +10 -0
- package/dist/assembly/material/standard-layer-projection.d.ts.map +1 -0
- package/dist/assembly/material-shader-policy.d.ts +168 -0
- package/dist/assembly/material-shader-policy.d.ts.map +1 -0
- package/dist/assembly/material-shader-prewarm.d.ts +21 -0
- package/dist/assembly/material-shader-prewarm.d.ts.map +1 -0
- package/dist/assembly/mesh-ssbo-grow.d.ts +88 -0
- package/dist/assembly/mesh-ssbo-grow.d.ts.map +1 -0
- package/dist/assembly/recovery/device-loss-fanout.d.ts +14 -0
- package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -0
- package/dist/assembly/recovery/generation.d.ts +46 -0
- package/dist/assembly/recovery/generation.d.ts.map +1 -0
- package/dist/assembly/recovery/recovery-attempt.d.ts +63 -0
- package/dist/assembly/recovery/recovery-attempt.d.ts.map +1 -0
- package/dist/assembly/recovery/renderer-recover.d.ts +115 -0
- package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -0
- package/dist/assembly/renderer-facade.d.ts +10 -0
- package/dist/assembly/renderer-facade.d.ts.map +1 -0
- package/dist/assembly/renderer-frame-transaction.d.ts +17 -0
- package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
- package/dist/assembly/renderer-helpers.d.ts +19 -0
- package/dist/assembly/renderer-helpers.d.ts.map +1 -0
- package/dist/assembly/renderer-lifecycle.d.ts +34 -0
- package/dist/assembly/renderer-lifecycle.d.ts.map +1 -1
- package/dist/assembly/shader-prewarm-policy.d.ts +15 -0
- package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -0
- package/dist/assembly/webgpu-ready-contract.d.ts +9 -0
- package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -0
- package/dist/assembly/webgpu-ready.d.ts +55 -0
- package/dist/assembly/webgpu-ready.d.ts.map +1 -0
- package/dist/assembly/webgpu-renderer.d.ts +152 -0
- package/dist/assembly/webgpu-renderer.d.ts.map +1 -0
- package/dist/authoring.mjs +1 -1
- package/dist/{chunk-PCHBPYML.mjs → chunk-GE3SDD72.mjs} +37 -5
- package/dist/chunk-GE3SDD72.mjs.map +1 -0
- package/dist/chunk-KZFTK6DO.mjs +1191 -0
- package/dist/chunk-KZFTK6DO.mjs.map +1 -0
- package/dist/{chunk-FS2KNUQH.mjs → chunk-Q752SHI5.mjs} +332 -568
- package/dist/chunk-Q752SHI5.mjs.map +1 -0
- package/dist/{chunk-6OPXDQ2E.mjs → chunk-RR5L4SRS.mjs} +33 -294
- package/dist/chunk-RR5L4SRS.mjs.map +1 -0
- package/dist/chunk-TFH5I7JJ.mjs +296 -0
- package/dist/chunk-TFH5I7JJ.mjs.map +1 -0
- package/dist/chunk-UQYS7FRM.mjs +1003 -0
- package/dist/chunk-UQYS7FRM.mjs.map +1 -0
- package/dist/chunk-YKB5DQV7.mjs +345 -0
- package/dist/chunk-YKB5DQV7.mjs.map +1 -0
- package/dist/{chunk-5YADUCA7.mjs → chunk-YRUYGA4A.mjs} +1189 -1217
- package/dist/chunk-YRUYGA4A.mjs.map +1 -0
- package/dist/chunk-Z3YYH2MB.mjs +5082 -0
- package/dist/chunk-Z3YYH2MB.mjs.map +1 -0
- package/dist/cluster-binner.d.ts +5 -0
- package/dist/cluster-binner.d.ts.map +1 -1
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/light-helpers.d.ts +4 -0
- package/dist/components/light-helpers.d.ts.map +1 -1
- package/dist/components/light-probe.d.ts +9 -0
- package/dist/components/light-probe.d.ts.map +1 -0
- package/dist/components/rect-area-light.d.ts +12 -0
- package/dist/components/rect-area-light.d.ts.map +1 -0
- package/dist/components/scene-instance.d.ts +4 -0
- package/dist/components/scene-instance.d.ts.map +1 -1
- package/dist/components/spot-light.d.ts +3 -0
- package/dist/components/spot-light.d.ts.map +1 -1
- package/dist/construct-renderer.mjs +27276 -23232
- 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 +32 -5
- package/dist/device/gpu-residency.d.ts.map +1 -1
- package/dist/device/lifecycle-transaction.d.ts +2 -0
- package/dist/device/lifecycle-transaction.d.ts.map +1 -0
- package/dist/environment/frame.d.ts +5 -0
- package/dist/environment/frame.d.ts.map +1 -1
- package/dist/environment/inspection.d.ts +7 -0
- package/dist/environment/inspection.d.ts.map +1 -1
- package/dist/environment/lifecycle.d.ts +20 -2
- package/dist/environment/lifecycle.d.ts.map +1 -1
- package/dist/errors/gpu-driven.d.ts +26 -0
- package/dist/errors/gpu-driven.d.ts.map +1 -0
- package/dist/errors/index.d.ts +1 -0
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/recover.d.ts +67 -3
- package/dist/errors/recover.d.ts.map +1 -1
- package/dist/errors/render.d.ts +39 -1
- package/dist/errors/render.d.ts.map +1 -1
- package/dist/extract/contracts.d.ts +899 -0
- package/dist/extract/contracts.d.ts.map +1 -0
- package/dist/extract/gpu-driven.d.ts +50 -0
- package/dist/extract/gpu-driven.d.ts.map +1 -0
- package/dist/extract/visibility.d.ts.map +1 -1
- package/dist/features/host.d.ts +2 -0
- package/dist/features/host.d.ts.map +1 -1
- package/dist/features/prepared-gpu-work.d.ts +3 -0
- package/dist/features/prepared-gpu-work.d.ts.map +1 -1
- package/dist/features/prepared-graphics-store.d.ts +2 -0
- package/dist/features/prepared-graphics-store.d.ts.map +1 -1
- package/dist/gpu-driven/batch-topology.d.ts +34 -0
- package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
- package/dist/gpu-driven/prepared-draw.d.ts +63 -0
- package/dist/gpu-driven/prepared-draw.d.ts.map +1 -0
- package/dist/gpu-driven/production-raster.d.ts +126 -2
- package/dist/gpu-driven/production-raster.d.ts.map +1 -1
- package/dist/gpu-driven/view-gpu.d.ts +64 -1
- package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
- package/dist/gpu-scene-schema.d.ts +14 -0
- package/dist/gpu-scene-schema.d.ts.map +1 -1
- package/dist/gpu-scene.d.ts +9 -1
- package/dist/gpu-scene.d.ts.map +1 -1
- package/dist/gpu-usage.d.ts +0 -1
- package/dist/gpu-usage.d.ts.map +1 -1
- package/dist/hdrp-buffers.d.ts +3 -3
- package/dist/hdrp-buffers.d.ts.map +1 -1
- package/dist/ibl/IblPipelineCache.d.ts +7 -6
- package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
- package/dist/ibl/skylight-bind-group.d.ts +7 -1
- package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
- package/dist/index.d.ts +13 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +90 -5
- package/dist/index.mjs.map +1 -1
- package/dist/inspection-types.d.ts +163 -1
- package/dist/inspection-types.d.ts.map +1 -1
- package/dist/internal.d.ts +9 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.mjs +23 -2
- package/dist/internal.mjs.map +1 -1
- package/dist/light-buffer-layout.d.ts +60 -148
- package/dist/light-buffer-layout.d.ts.map +1 -1
- package/dist/materials.d.ts +34 -10
- package/dist/materials.d.ts.map +1 -1
- package/dist/mesh-material-bindings.d.ts +56 -0
- package/dist/mesh-material-bindings.d.ts.map +1 -1
- package/dist/occlusion-proxy.d.ts +4 -0
- package/dist/occlusion-proxy.d.ts.map +1 -0
- package/dist/pbr-pipeline.d.ts +58 -21
- package/dist/pbr-pipeline.d.ts.map +1 -1
- package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
- package/dist/pipeline/standard-lighting/graph.d.ts.map +1 -1
- package/dist/pipeline/standard-lighting/light-frame.d.ts +3 -3
- package/dist/pipeline/standard-lighting/light-frame.d.ts.map +1 -1
- package/dist/pipeline/standard-lighting/topology.d.ts +1 -1
- package/dist/pipeline/standard-lighting/topology.d.ts.map +1 -1
- package/dist/pipeline/standard-lighting/transport.d.ts.map +1 -1
- package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
- package/dist/pipeline-builder.d.ts +2 -0
- package/dist/pipeline-builder.d.ts.map +1 -1
- package/dist/pipeline-spec.d.ts +1 -1
- package/dist/pipeline-spec.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/points-lines/prepare.d.ts +3 -0
- package/dist/points-lines/prepare.d.ts.map +1 -1
- package/dist/points-lines/standard-owner.d.ts +33 -0
- package/dist/points-lines/standard-owner.d.ts.map +1 -0
- package/dist/prepare/extended-lighting/ltc-resources.d.ts +31 -0
- package/dist/prepare/extended-lighting/ltc-resources.d.ts.map +1 -0
- package/dist/prepare/extended-lighting/resources.d.ts +72 -0
- package/dist/prepare/extended-lighting/resources.d.ts.map +1 -0
- package/dist/prepare/extended-lighting/spot-modifiers.d.ts +42 -0
- package/dist/prepare/extended-lighting/spot-modifiers.d.ts.map +1 -0
- package/dist/prepare/extended-lighting/state.d.ts +24 -0
- package/dist/prepare/extended-lighting/state.d.ts.map +1 -0
- package/dist/record/frame-lighting.d.ts +8 -6
- package/dist/record/frame-lighting.d.ts.map +1 -1
- package/dist/record/frame-snapshot.d.ts +55 -10
- 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 +21 -5
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/gpu-timing.d.ts +19 -7
- package/dist/record/gpu-timing.d.ts.map +1 -1
- package/dist/record/helpers.d.ts +0 -20
- package/dist/record/helpers.d.ts.map +1 -1
- package/dist/record/main-pass-geometry.d.ts +20 -2
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/main-pass-material.d.ts +10 -18
- package/dist/record/main-pass-material.d.ts.map +1 -1
- package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
- package/dist/record/main-pass.d.ts.map +1 -1
- package/dist/record/mesh-ssbo.d.ts +11 -3
- package/dist/record/mesh-ssbo.d.ts.map +1 -1
- package/dist/record/recovery-pipeline.d.ts +30 -0
- package/dist/record/recovery-pipeline.d.ts.map +1 -0
- package/dist/record/render-context.d.ts +82 -9
- package/dist/record/render-context.d.ts.map +1 -1
- package/dist/record/shadow-pass.d.ts.map +1 -1
- package/dist/record/typed-frame-graph.d.ts +49 -2
- package/dist/record/typed-frame-graph.d.ts.map +1 -1
- package/dist/record/view-ubo.d.ts +1 -2
- package/dist/record/view-ubo.d.ts.map +1 -1
- package/dist/recovery/render-system-candidate.d.ts +65 -0
- package/dist/recovery/render-system-candidate.d.ts.map +1 -0
- package/dist/reflection/inspection.d.ts +10 -0
- package/dist/reflection/inspection.d.ts.map +1 -0
- package/dist/reflection/projection.d.ts +49 -0
- package/dist/reflection/projection.d.ts.map +1 -1
- package/dist/reflection/record-owner.d.ts +60 -0
- package/dist/reflection/record-owner.d.ts.map +1 -0
- package/dist/render-contract.d.ts +78 -15
- package/dist/render-contract.d.ts.map +1 -1
- package/dist/render-pipeline.d.ts +7 -0
- package/dist/render-pipeline.d.ts.map +1 -1
- package/dist/render-system-extract-tail.d.ts +12 -0
- package/dist/render-system-extract-tail.d.ts.map +1 -0
- package/dist/render-system-extract.d.ts +252 -30
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts +140 -5
- package/dist/render-system.d.ts.map +1 -1
- package/dist/scene/probe-blend-record.d.ts +22 -0
- package/dist/scene/probe-blend-record.d.ts.map +1 -0
- package/dist/scene/probe-blend.d.ts +142 -0
- package/dist/scene/probe-blend.d.ts.map +1 -0
- package/dist/scene/render-scene-types.d.ts +0 -6
- package/dist/scene/render-scene-types.d.ts.map +1 -1
- package/dist/scene/render-scene.d.ts +122 -9
- package/dist/scene/render-scene.d.ts.map +1 -1
- package/dist/scene/visibility/bounds.d.ts +20 -0
- package/dist/scene/visibility/bounds.d.ts.map +1 -0
- package/dist/scene/visibility/budget.d.ts +16 -0
- package/dist/scene/visibility/budget.d.ts.map +1 -0
- package/dist/scene/visibility/facet.d.ts +73 -0
- package/dist/scene/visibility/facet.d.ts.map +1 -0
- package/dist/scene/visibility/gpu-frame-timing.d.ts +41 -0
- package/dist/scene/visibility/gpu-frame-timing.d.ts.map +1 -0
- package/dist/scene/visibility/gpu-lod.d.ts +46 -0
- package/dist/scene/visibility/gpu-lod.d.ts.map +1 -0
- package/dist/scene/visibility/inspection.d.ts +132 -0
- package/dist/scene/visibility/inspection.d.ts.map +1 -0
- package/dist/scene/visibility/lod-selector.d.ts +25 -0
- package/dist/scene/visibility/lod-selector.d.ts.map +1 -0
- package/dist/scene/visibility/occlusion-confidence.d.ts +81 -0
- package/dist/scene/visibility/occlusion-confidence.d.ts.map +1 -0
- package/dist/scene/visibility/occlusion-pass.d.ts +57 -0
- package/dist/scene/visibility/occlusion-pass.d.ts.map +1 -0
- package/dist/scene/visibility/occlusion-query-pool.d.ts +57 -0
- package/dist/scene/visibility/occlusion-query-pool.d.ts.map +1 -0
- package/dist/scene/visibility/occlusion-runtime.d.ts +96 -0
- package/dist/scene/visibility/occlusion-runtime.d.ts.map +1 -0
- package/dist/scene/visibility/types.d.ts +34 -0
- package/dist/scene/visibility/types.d.ts.map +1 -0
- package/dist/ssr/admission.d.ts +122 -0
- package/dist/ssr/admission.d.ts.map +1 -0
- package/dist/ssr/composition.d.ts +32 -0
- package/dist/ssr/composition.d.ts.map +1 -0
- package/dist/ssr/errors.d.ts +39 -0
- package/dist/ssr/errors.d.ts.map +1 -0
- package/dist/ssr/identity.d.ts +13 -0
- package/dist/ssr/identity.d.ts.map +1 -0
- package/dist/systems/skin-palette-allocator.d.ts +1 -1
- package/dist/temporal/frame-coordinator.d.ts +13 -0
- package/dist/temporal/frame-coordinator.d.ts.map +1 -1
- package/dist/temporal/frame.d.ts +2 -0
- package/dist/temporal/frame.d.ts.map +1 -1
- package/dist/temporal/index.mjs +6 -5
- package/dist/temporal/taa-history-store.d.ts +15 -1
- package/dist/temporal/taa-history-store.d.ts.map +1 -1
- package/dist/temporal/transaction.d.ts +5 -0
- package/dist/temporal/transaction.d.ts.map +1 -1
- package/dist/temporal/view.d.ts +7 -0
- package/dist/temporal/view.d.ts.map +1 -1
- package/dist/tilemap-chunk-extract-system.d.ts +1 -1
- package/dist/typed-render-graph-primitives.d.ts +4 -0
- package/dist/typed-render-graph-primitives.d.ts.map +1 -1
- package/dist/volume/passes.d.ts +8 -4
- package/dist/volume/passes.d.ts.map +1 -1
- package/package.json +20 -20
- package/src/__tests__/cluster-grid-default-surface.unit.test.ts +5 -0
- package/src/__tests__/detached-inspection.integration.test.ts +6 -0
- package/src/__tests__/device-feature-admission.unit.test.ts +19 -1
- package/src/__tests__/device-loss-fanout.unit.test.ts +107 -0
- package/src/__tests__/device-scope-lifecycle.unit.test.ts +21 -0
- package/src/__tests__/direct-light-snapshot.unit.test.ts +59 -3
- package/src/__tests__/environment-generation.integration.test.ts +39 -0
- package/src/__tests__/evidence-identity.ts +13 -0
- package/src/__tests__/evidence-identity.unit.test.ts +24 -0
- package/src/__tests__/expose-renderer-errors.unit.test.ts +34 -0
- package/src/__tests__/extended-lighting-recovery.browser.test.ts +14 -0
- package/src/__tests__/extended-lighting-recovery.dawn.test.ts +14 -0
- package/src/__tests__/extended-lighting-recovery.integration.test.ts +81 -0
- package/src/__tests__/extended-lighting-resources.unit.test.ts +51 -0
- package/src/__tests__/extended-lighting-zero-feature.integration.test.ts +52 -0
- package/src/__tests__/factory-contract.integration.test.ts +154 -6
- package/src/__tests__/fallback-row-stride-surface.unit.test.ts +4 -1
- package/src/__tests__/frame-observation.contract.test.ts +15 -0
- package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +146 -0
- package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +1 -0
- package/src/__tests__/gpu-driven-indirect-raster.dawn.test.ts +1 -0
- package/src/__tests__/gpu-driven-production.integration.test.ts +572 -9
- package/src/__tests__/gpu-driven-scaling.unit.test.ts +66 -2
- package/src/__tests__/gpu-driven-view-gpu-evidence.ts +26 -1
- package/src/__tests__/gpu-driven-view-graph.integration.test.ts +89 -2
- package/src/__tests__/gpu-driven-view.browser.test.ts +3 -3
- package/src/__tests__/gpu-driven-view.dawn.test.ts +3 -3
- package/src/__tests__/gpu-driven-view.unit.test.ts +50 -0
- package/src/__tests__/gpu-lod-evidence.ts +188 -0
- package/src/__tests__/gpu-lod-fallback.integration.test.ts +10 -0
- package/src/__tests__/gpu-lod-indirect.integration.test.ts +40 -0
- package/src/__tests__/gpu-lod-parity.dawn.test.ts +29 -0
- package/src/__tests__/gpu-lod-readiness.integration.test.ts +52 -0
- package/src/__tests__/gpu-lod-schema.unit.test.ts +66 -0
- package/src/__tests__/gpu-lod-selector.unit.test.ts +28 -0
- package/src/__tests__/gpu-lod-silhouette.browser.test.ts +13 -0
- package/src/__tests__/gpu-resource-store-stride.unit.test.ts +66 -1
- package/src/__tests__/gpu-scene.dawn.test.ts +8 -1
- package/src/__tests__/gpu-scene.unit.test.ts +49 -3
- package/src/__tests__/ibl/face-uniforms.unit.test.ts +2 -0
- package/src/__tests__/ibl-residency-publication.unit.test.ts +113 -0
- package/src/__tests__/light-buffer-layout.unit.test.ts +113 -3
- package/src/__tests__/light-inspection.unit.test.ts +55 -0
- package/src/__tests__/light-resource-error.unit.test.ts +43 -0
- package/src/__tests__/lighting-scene-roundtrip.integration.test.ts +109 -0
- package/src/__tests__/lod-bounds.unit.test.ts +42 -0
- package/src/__tests__/lod-cpu-lane.integration.test.ts +55 -0
- package/src/__tests__/lod-cpu-no-change.perf.test.ts +19 -0
- package/src/__tests__/lod-gpu-frame-timing.unit.test.ts +49 -0
- package/src/__tests__/lod-multi-view.integration.test.ts +63 -0
- package/src/__tests__/lod-occlusion-device-loss.integration.test.ts +27 -0
- package/src/__tests__/lod-occlusion-final-decision.integration.test.ts +47 -0
- package/src/__tests__/lod-occlusion-inspection.integration.test.ts +217 -0
- package/src/__tests__/lod-occlusion-inspection.schema.test-d.ts +14 -0
- package/src/__tests__/lod-occlusion-inspection.unit.test.ts +72 -0
- package/src/__tests__/lod-occlusion-visual-evidence.ts +91 -0
- package/src/__tests__/lod-occlusion.browser.test.ts +15 -0
- package/src/__tests__/lod-occlusion.dawn.test.ts +20 -0
- package/src/__tests__/lod-selector.profile.test.ts +35 -0
- package/src/__tests__/lod-selector.unit.test.ts +68 -0
- package/src/__tests__/lod-visibility-identity.unit.test.ts +198 -0
- package/src/__tests__/lod-visibility-lifecycle.integration.test.ts +132 -0
- package/src/__tests__/ltc-resources.unit.test.ts +32 -0
- package/src/__tests__/material-contract-inventory.unit.test.ts +41 -9
- package/src/__tests__/material-cooked-projection.unit.test.ts +19 -1
- package/src/__tests__/material-static-dynamic-route.unit.test.ts +29 -0
- package/src/__tests__/material-stride-surface.unit.test.ts +7 -3
- package/src/__tests__/materials-standard-contract.unit.test.ts +104 -0
- package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +1 -1
- package/src/__tests__/occlusion-confidence.unit.test.ts +168 -0
- package/src/__tests__/occlusion-invalidation.integration.test.ts +37 -0
- package/src/__tests__/occlusion-pass-routing.unit.test.ts +16 -0
- package/src/__tests__/occlusion-pass.dawn.test.ts +20 -0
- package/src/__tests__/occlusion-pass.integration.test.ts +43 -0
- package/src/__tests__/occlusion-pass.rhi-null.test.ts +22 -0
- package/src/__tests__/occlusion-query-pool.rhi-null.test.ts +21 -0
- package/src/__tests__/occlusion-query-pool.unit.test.ts +95 -0
- package/src/__tests__/occlusion-query-ticket.integration.test.ts +47 -0
- package/src/__tests__/occlusion-query.dawn.test.ts +16 -0
- package/src/__tests__/occlusion-query.evidence.ts +178 -0
- package/src/__tests__/occlusion-query.rhi-null.test.ts +31 -0
- package/src/__tests__/occlusion-runtime.unit.test.ts +120 -0
- package/src/__tests__/physical-clearcoat.integration.test.ts +89 -0
- package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +196 -0
- package/src/__tests__/prepared-graphics-pipeline-warmup.unit.test.ts +1 -1
- package/src/__tests__/probe-bindings.unit.test.ts +200 -0
- package/src/__tests__/probe-blend-numeric.unit.test.ts +67 -0
- package/src/__tests__/probe-blend-oracle.unit.test.ts +136 -0
- package/src/__tests__/probe-capacity.integration.test.ts +97 -0
- package/src/__tests__/probe-recovery.integration.test.ts +104 -0
- package/src/__tests__/probe-scene-dirty.integration.test.ts +211 -0
- package/src/__tests__/profiler-phase-catalog.test.ts +4 -1
- package/src/__tests__/profiler-phase-token.test.ts +1 -0
- package/src/__tests__/recover-error-policy-owner.unit.test.ts +6 -3
- package/src/__tests__/recovery-candidate-prepare.contract.test.ts +140 -0
- package/src/__tests__/rect-area-boundary.unit.test.ts +24 -0
- package/src/__tests__/reflection-fallback-graph.unit.test.ts +67 -0
- package/src/__tests__/reflection-fallback-inspection.unit.test.ts +77 -0
- package/src/__tests__/reflection-fallback-lifecycle.integration.test.ts +21 -0
- package/src/__tests__/reflection-fallback-projection.unit.test.ts +90 -0
- package/src/__tests__/reflection-fallback-source.unit.test.ts +32 -0
- package/src/__tests__/reflection-probe-owner-placement.unit.test.ts +39 -0
- package/src/__tests__/reflection-probe-record-owner.characterization.unit.test.ts +120 -0
- package/src/__tests__/render-error-code-owner.test-d.ts +1 -0
- package/src/__tests__/render-feature-docs.unit.test.ts +12 -7
- package/src/__tests__/render-graph-fallback.integration.test.ts +24 -0
- package/src/__tests__/render-perf/extended-lighting.bench.test.ts +66 -0
- package/src/__tests__/render-scene-projection.unit.test.ts +447 -18
- package/src/__tests__/renderer-factory-material-contract.unit.test.ts +160 -17
- package/src/__tests__/renderer-frame-generation-fence.integration.test.ts +47 -0
- package/src/__tests__/renderer-generation-assembly.unit.test.ts +77 -0
- package/src/__tests__/renderer-generation-failure.unit.test.ts +108 -0
- package/src/__tests__/renderer-producer-root-matrix.integration.test.ts +33 -0
- package/src/__tests__/renderer-recovery-capability-matrix.unit.test.ts +59 -0
- package/src/__tests__/renderer-recovery-contract.unit.test.ts +73 -0
- package/src/__tests__/renderer-recovery-lifecycle.unit.test.ts +216 -0
- package/src/__tests__/renderer-target-history-recovery.integration.test.ts +80 -0
- package/src/__tests__/shader-manifest-fixture.ts +13 -16
- package/src/__tests__/shader-stage-surface.unit.test.ts +1 -1
- package/src/__tests__/skin-motion-regression.unit.test.ts +22 -0
- package/src/__tests__/skinned-shadow-caster.test.ts +2 -2
- package/src/__tests__/spot-modifier-projection.unit.test.ts +70 -0
- package/src/__tests__/ssr-admission-matrix.integration.test.ts +206 -0
- package/src/__tests__/ssr-composition.unit.test.ts +46 -0
- package/src/__tests__/ssr-fallback-zero-work.bench.test.ts +94 -0
- package/src/__tests__/ssr-recovery-errors.unit.test.ts +58 -0
- package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +67 -0
- package/src/__tests__/standard-clustered-pipeline.unit.test.ts +7 -0
- package/src/__tests__/standard-layer-projection.unit.test.ts +23 -0
- package/src/__tests__/standard-lighting-inspection.integration.test.ts +1 -1
- package/src/__tests__/standard-lighting-write.unit.test.ts +66 -2
- package/src/__tests__/temporal-fallback-generation.integration.test.ts +16 -0
- package/src/__tests__/temporal-view.unit.test.ts +3 -3
- package/src/__tests__/transmission-prewarm.unit.test.ts +111 -32
- package/src/__tests__/visibility-budget.integration.test.ts +140 -0
- package/src/__tests__/visibility-budget.unit.test.ts +70 -0
- package/src/__tests__/visibility-extract.integration.test.ts +11 -6
- package/src/__tests__/volumetric-fog-point-spot-accumulation.integration.test.ts +6 -3
- package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +31 -28
- package/src/__tests__/volumetric-fog-topology.unit.test.ts +3 -1
- package/src/assembly/backend-contract.ts +13 -0
- package/src/assembly/bundler-contract.ts +9 -0
- package/src/assembly/device-feature-admission.ts +26 -9
- package/src/assembly/extended-lighting-fallback.ts +216 -0
- package/src/assembly/factory.ts +81 -9634
- package/src/assembly/host-contract.ts +10 -0
- package/src/assembly/material/assembly.ts +18 -2
- package/src/assembly/material/standard-layer-projection.ts +16 -0
- package/src/assembly/material-shader-policy.ts +854 -0
- package/src/assembly/material-shader-prewarm.ts +88 -0
- package/src/assembly/mesh-ssbo-grow.ts +270 -0
- package/src/assembly/recovery/device-loss-fanout.ts +89 -0
- package/src/assembly/recovery/generation.ts +141 -0
- package/src/assembly/recovery/recovery-attempt.ts +800 -0
- package/src/assembly/recovery/renderer-recover.ts +906 -0
- package/src/assembly/renderer-facade.ts +342 -0
- package/src/assembly/renderer-frame-transaction.ts +45 -0
- package/src/assembly/renderer-helpers.ts +99 -0
- package/src/assembly/renderer-lifecycle.ts +159 -0
- package/src/assembly/shader-prewarm-policy.ts +157 -0
- package/src/assembly/webgpu-ready-contract.ts +14 -0
- package/src/assembly/webgpu-ready.ts +4079 -0
- package/src/assembly/webgpu-renderer.ts +4095 -0
- package/src/cluster-binner.ts +31 -0
- package/src/components/__tests__/lighting-authoring.unit.test.ts +39 -0
- package/src/components/__tests__/rect-area-light.unit.test.ts +33 -0
- package/src/components/camera.ts +1 -1
- package/src/components/index.ts +3 -0
- package/src/components/light-helpers.ts +52 -1
- package/src/components/light-probe.ts +13 -0
- package/src/components/rect-area-light.ts +15 -0
- package/src/components/scene-instance.ts +6 -0
- package/src/components/spot-light.ts +3 -0
- package/src/debug-draw-glue.ts +1 -1
- package/src/device/__tests__/extended-lighting-recovery.unit.test.ts +24 -0
- package/src/device/device-scope.ts +35 -6
- package/src/device/gpu-residency.ts +334 -35
- package/src/device/lifecycle-transaction.ts +12 -0
- package/src/environment/frame.ts +6 -0
- package/src/environment/inspection.ts +10 -0
- package/src/environment/lifecycle.ts +122 -9
- package/src/errors/gpu-driven.ts +92 -0
- package/src/errors/index.ts +1 -0
- package/src/errors/recover.ts +108 -5
- package/src/errors/render.ts +72 -1
- package/src/extract/camera.ts +3 -3
- package/src/extract/contracts.ts +974 -0
- package/src/extract/gpu-driven.ts +108 -0
- package/src/extract/visibility.ts +0 -23
- package/src/features/host.ts +16 -0
- package/src/features/prepared-gpu-work.ts +20 -2
- package/src/features/prepared-graphics-store.ts +17 -0
- package/src/gpu-driven/batch-topology.ts +206 -28
- package/src/gpu-driven/prepared-draw.ts +208 -0
- package/src/gpu-driven/production-raster.ts +1046 -39
- package/src/gpu-driven/view-gpu.ts +411 -29
- package/src/gpu-scene-schema.ts +13 -0
- package/src/gpu-scene.ts +137 -67
- package/src/gpu-usage.ts +0 -1
- package/src/hdrp-buffers.ts +6 -6
- package/src/ibl/IblPipelineCache.ts +134 -50
- package/src/ibl/skylight-bind-group.ts +26 -0
- package/src/index.ts +83 -1
- package/src/inspection-types.ts +220 -1
- package/src/internal.ts +38 -0
- package/src/light-buffer-layout.ts +157 -324
- package/src/materials.ts +278 -47
- package/src/mesh-material-bindings.ts +59 -0
- package/src/occlusion-proxy.ts +64 -0
- package/src/pbr-pipeline.ts +290 -60
- package/src/pipeline/standard-forward-lane.ts +47 -0
- package/src/pipeline/standard-lighting/graph.ts +2 -1
- package/src/pipeline/standard-lighting/light-frame.ts +3 -2
- package/src/pipeline/standard-lighting/topology.ts +1 -1
- package/src/pipeline/standard-lighting/transport.ts +2 -1
- package/src/pipeline/standard-pipeline.ts +2 -0
- package/src/pipeline-builder.ts +5 -2
- package/src/pipeline-spec.ts +12 -1
- package/src/plugin.ts +4 -0
- package/src/points-lines/__tests__/cache-recovery.unit.test.ts +22 -0
- package/src/points-lines/prepare.ts +9 -0
- package/src/points-lines/standard-owner.ts +403 -0
- package/src/prepare/__tests__/extended-lighting-capability.unit.test.ts +56 -0
- package/src/prepare/extended-lighting/ltc-resources.ts +136 -0
- package/src/prepare/extended-lighting/resources.ts +389 -0
- package/src/prepare/extended-lighting/spot-modifiers.ts +194 -0
- package/src/prepare/extended-lighting/state.ts +92 -0
- package/src/record/__tests__/frame-targets-recovery.unit.test.ts +60 -0
- package/src/record/__tests__/gpu-timing.unit.test.ts +177 -0
- package/src/record/__tests__/main-pass-geometry.unit.test.ts +26 -1
- package/src/record/__tests__/mesh-ssbo.unit.test.ts +38 -0
- package/src/record/__tests__/recovery-pipeline.unit.test.ts +124 -0
- package/src/record/__tests__/sprite-material-abi.unit.test.ts +28 -13
- package/src/record/__tests__/standard-pbr-ubo-layout.unit.test.ts +47 -4
- package/src/record/__tests__/temporal-payload.unit.test.ts +29 -28
- package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +21 -2
- package/src/record/frame-lighting.ts +252 -53
- package/src/record/frame-snapshot.ts +72 -12
- package/src/record/frame-targets.ts +11 -1
- package/src/record/frame.ts +416 -95
- package/src/record/gpu-timing.ts +108 -44
- package/src/record/helpers.ts +1 -59
- package/src/record/main-pass-geometry.ts +434 -44
- package/src/record/main-pass-material.ts +324 -210
- package/src/record/main-pass-sprite-draws.ts +58 -92
- package/src/record/main-pass.ts +94 -6
- package/src/record/mesh-ssbo.ts +79 -46
- package/src/record/recovery-pipeline.ts +695 -0
- package/src/record/render-context.ts +99 -7
- package/src/record/shadow-pass.ts +66 -40
- package/src/record/typed-frame-graph.ts +270 -84
- package/src/record/view-ubo.ts +17 -27
- package/src/recovery/render-system-candidate.ts +839 -0
- package/src/reflection/inspection.ts +67 -0
- package/src/reflection/projection.ts +196 -1
- package/src/reflection/record-owner.ts +1283 -0
- package/src/render-contract.ts +82 -12
- package/src/render-pipeline.ts +5 -0
- package/src/render-system-extract-tail.ts +2983 -0
- package/src/render-system-extract.ts +290 -2784
- package/src/render-system.ts +1597 -1192
- package/src/scene/__tests__/changed-block-summary.candidate.test.ts +36 -0
- package/src/scene/probe-blend-record.ts +54 -0
- package/src/scene/probe-blend.ts +813 -0
- package/src/scene/render-scene-types.ts +0 -7
- package/src/scene/render-scene.ts +972 -184
- package/src/scene/visibility/bounds.ts +57 -0
- package/src/scene/visibility/budget.ts +40 -0
- package/src/scene/visibility/facet.ts +400 -0
- package/src/scene/visibility/gpu-frame-timing.ts +101 -0
- package/src/scene/visibility/gpu-lod.ts +119 -0
- package/src/scene/visibility/inspection.ts +313 -0
- package/src/scene/visibility/lod-selector.ts +84 -0
- package/src/scene/visibility/occlusion-confidence.ts +478 -0
- package/src/scene/visibility/occlusion-pass.ts +165 -0
- package/src/scene/visibility/occlusion-query-pool.ts +178 -0
- package/src/scene/visibility/occlusion-runtime.ts +672 -0
- package/src/scene/visibility/types.ts +50 -0
- package/src/ssr/admission.ts +451 -0
- package/src/ssr/composition.ts +73 -0
- package/src/ssr/errors.ts +76 -0
- package/src/ssr/identity.ts +12 -0
- package/src/systems/skin-palette-allocator.ts +1 -1
- package/src/temporal/frame-coordinator.ts +30 -1
- package/src/temporal/frame.ts +14 -1
- package/src/temporal/taa-history-store.ts +46 -2
- package/src/temporal/temporal-view.ts +1 -1
- package/src/temporal/transaction.ts +9 -0
- package/src/temporal/view.ts +11 -0
- package/src/tilemap-chunk-extract-system.ts +5 -5
- package/src/transmission/__tests__/standard-transmission.dawn.test.ts +19 -12
- package/src/typed-render-graph-primitives.ts +48 -0
- package/src/volume/passes.ts +46 -49
- package/dist/.tsbuildinfo +0 -1
- package/dist/chunk-5YADUCA7.mjs.map +0 -1
- package/dist/chunk-6OPXDQ2E.mjs.map +0 -1
- package/dist/chunk-BXBQQKFQ.mjs +0 -67
- package/dist/chunk-BXBQQKFQ.mjs.map +0 -1
- package/dist/chunk-FS2KNUQH.mjs.map +0 -1
- package/dist/chunk-PCHBPYML.mjs.map +0 -1
- package/dist/chunk-VFPLTKCU.mjs +0 -192
- package/dist/chunk-VFPLTKCU.mjs.map +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { deriveVertexBufferLayoutFromProjection } from '@forgeax/engine-geometry';
|
|
2
|
+
import { frustum, mat3, mat4 } from '@forgeax/engine-math';
|
|
2
3
|
import type {
|
|
3
4
|
GraphAccess,
|
|
4
5
|
GraphBuffer,
|
|
@@ -9,6 +10,7 @@ import type {
|
|
|
9
10
|
import type {
|
|
10
11
|
BindGroup,
|
|
11
12
|
BindGroupLayout,
|
|
13
|
+
Buffer,
|
|
12
14
|
RenderPipeline,
|
|
13
15
|
Result,
|
|
14
16
|
RhiDevice,
|
|
@@ -16,29 +18,166 @@ import type {
|
|
|
16
18
|
TextureFormat,
|
|
17
19
|
} from '@forgeax/engine-rhi';
|
|
18
20
|
import { err, ok, RhiError } from '@forgeax/engine-rhi';
|
|
21
|
+
import type { MaterialShaderArtifact } from '@forgeax/engine-shader';
|
|
19
22
|
import type { MaterialRenderState } from '@forgeax/engine-types';
|
|
23
|
+
import type { DeviceScope, LifecycleResourceSpec } from '../device/device-scope';
|
|
20
24
|
import type { MeshGpuHandles } from '../device/gpu-residency';
|
|
21
25
|
import { GPU_SCENE_WGSL } from '../gpu-scene-schema';
|
|
22
26
|
import {
|
|
23
27
|
GPU_BUFFER_USAGE_COPY_DST,
|
|
24
28
|
GPU_BUFFER_USAGE_INDEX,
|
|
29
|
+
GPU_BUFFER_USAGE_STORAGE,
|
|
30
|
+
GPU_BUFFER_USAGE_UNIFORM,
|
|
25
31
|
GPU_BUFFER_USAGE_VERTEX,
|
|
26
32
|
} from '../gpu-usage';
|
|
33
|
+
import { assembleMaterialWithSkylightEntries } from '../ibl/skylight-bind-group';
|
|
27
34
|
import type { GpuDrivenProductionInspection } from '../inspection-types';
|
|
28
35
|
import type { PipelineBuilderShaderModuleFactory } from '../pipeline-builder';
|
|
29
36
|
import { worldEntityKey } from '../record/frame-snapshot';
|
|
30
37
|
import { geometryRenderStateForTopology } from '../record/main-pass-material';
|
|
38
|
+
import { MESH_SSBO_BYTES } from '../record/mesh-ssbo';
|
|
39
|
+
import type { PipelineState } from '../record/render-context';
|
|
31
40
|
import type { CameraSnapshot } from '../render-contract';
|
|
32
41
|
import type { RenderPipelineFrame, RenderPipelineGpuDrivenProjection } from '../render-pipeline';
|
|
33
42
|
import type { RenderableSnapshot } from '../render-system-extract';
|
|
34
43
|
import type { PersistentGpuDrivenState } from '../scene/render-scene';
|
|
35
|
-
import
|
|
36
|
-
import {
|
|
44
|
+
import { selectLod } from '../scene/visibility/lod-selector';
|
|
45
|
+
import type { OcclusionFrameProjection } from '../scene/visibility/occlusion-runtime';
|
|
46
|
+
import {
|
|
47
|
+
type GpuDrivenBatch,
|
|
48
|
+
type GpuDrivenCandidate,
|
|
49
|
+
projectedHeightForCandidate,
|
|
50
|
+
type SubmissionPlan,
|
|
51
|
+
} from './batch-topology';
|
|
52
|
+
import { type GpuDrivenLodSelectionInspection, GpuDrivenView, selectGpuLodLane } from './view-gpu';
|
|
37
53
|
|
|
38
54
|
const VERTEX_STAGE = 0x1;
|
|
39
55
|
const FRAGMENT_STAGE = 0x2;
|
|
40
56
|
const EMPTY_RESOURCE_CLASS = JSON.stringify({ textures: [], samplers: [], video: [] });
|
|
41
57
|
|
|
58
|
+
export interface StandardPbrSceneCandidate {
|
|
59
|
+
readonly primitiveIndex: number;
|
|
60
|
+
readonly world: Float32Array;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface StandardPbrSceneProjection {
|
|
64
|
+
readonly entryPoint: string;
|
|
65
|
+
readonly layoutIdentity: string;
|
|
66
|
+
readonly receiptGeneration: number;
|
|
67
|
+
/** Packed Mesh rows: mat4 + padded mat3 + previous mat4 + temporal vec4. */
|
|
68
|
+
readonly rows: Float32Array;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Build the producer-owned Standard PBR scene-index projection. The receipt is
|
|
73
|
+
* the only source of the vertex entry and layout identity; callers cannot
|
|
74
|
+
* silently fall back to the legacy GPU-driven shader ABI.
|
|
75
|
+
*/
|
|
76
|
+
export function projectStandardPbrScene(input: {
|
|
77
|
+
readonly artifact: MaterialShaderArtifact;
|
|
78
|
+
readonly candidates: readonly StandardPbrSceneCandidate[];
|
|
79
|
+
}):
|
|
80
|
+
| { readonly ok: true; readonly value: StandardPbrSceneProjection }
|
|
81
|
+
| {
|
|
82
|
+
readonly ok: false;
|
|
83
|
+
readonly error: { readonly code: 'missing-material-receipt' | 'invalid-material-receipt' };
|
|
84
|
+
} {
|
|
85
|
+
const receipt = input.artifact.receipt;
|
|
86
|
+
if (receipt === undefined) return { ok: false, error: { code: 'missing-material-receipt' } };
|
|
87
|
+
if (receipt.reflection.layoutIdentity !== input.artifact.layoutIdentity) {
|
|
88
|
+
return { ok: false, error: { code: 'invalid-material-receipt' } };
|
|
89
|
+
}
|
|
90
|
+
const rows = new Float32Array(input.candidates.length * 48);
|
|
91
|
+
const normal = mat3.create();
|
|
92
|
+
for (let index = 0; index < input.candidates.length; index += 1) {
|
|
93
|
+
const candidate = input.candidates[index];
|
|
94
|
+
if (candidate === undefined) continue;
|
|
95
|
+
const base = index * 48;
|
|
96
|
+
for (let value = 0; value < 16; value += 1) {
|
|
97
|
+
rows[base + value] = candidate.world[value] ?? 0;
|
|
98
|
+
rows[base + 28 + value] = candidate.world[value] ?? 0;
|
|
99
|
+
}
|
|
100
|
+
const normalMatrix = mat3.normalMatrix(normal, candidate.world);
|
|
101
|
+
for (let column = 0; column < 3; column += 1) {
|
|
102
|
+
const source = column * 3;
|
|
103
|
+
const target = base + 16 + column * 4;
|
|
104
|
+
rows[target] = normalMatrix[source] ?? 0;
|
|
105
|
+
rows[target + 1] = normalMatrix[source + 1] ?? 0;
|
|
106
|
+
rows[target + 2] = normalMatrix[source + 2] ?? 0;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
ok: true,
|
|
111
|
+
value: {
|
|
112
|
+
entryPoint: receipt.sceneIndexEntry,
|
|
113
|
+
layoutIdentity: input.artifact.layoutIdentity,
|
|
114
|
+
receiptGeneration: receipt.generation,
|
|
115
|
+
rows,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface StandardPbrFrameResourceBinding {
|
|
121
|
+
readonly entryPoint: string;
|
|
122
|
+
readonly layoutIdentity: string;
|
|
123
|
+
readonly receiptGeneration: number;
|
|
124
|
+
readonly bindGroups: readonly [BindGroup, BindGroup, BindGroup, BindGroup];
|
|
125
|
+
readonly resourceSlots: NonNullable<MaterialShaderArtifact['receipt']>['resourceSlots'];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Adapt the frame's already-created Standard PBR groups to the GPU-driven
|
|
130
|
+
* scene-index draw. This deliberately accepts group handles rather than
|
|
131
|
+
* rebuilding layouts or shader artifacts; the receipt remains the binding
|
|
132
|
+
* contract and the regular frame producers remain the resource owners.
|
|
133
|
+
*/
|
|
134
|
+
export function adaptStandardPbrFrameResources(input: {
|
|
135
|
+
readonly artifact: MaterialShaderArtifact;
|
|
136
|
+
readonly view: BindGroup;
|
|
137
|
+
readonly material: BindGroup;
|
|
138
|
+
readonly mesh: BindGroup;
|
|
139
|
+
readonly instances: BindGroup;
|
|
140
|
+
}):
|
|
141
|
+
| { readonly ok: true; readonly value: StandardPbrFrameResourceBinding }
|
|
142
|
+
| {
|
|
143
|
+
readonly ok: false;
|
|
144
|
+
readonly error: { readonly code: 'missing-material-receipt' | 'invalid-material-receipt' };
|
|
145
|
+
} {
|
|
146
|
+
const receipt = input.artifact.receipt;
|
|
147
|
+
if (receipt === undefined) return { ok: false, error: { code: 'missing-material-receipt' } };
|
|
148
|
+
const reflected = receipt.reflection;
|
|
149
|
+
const sameSlots =
|
|
150
|
+
reflected.resourceSlots.length === receipt.resourceSlots.length &&
|
|
151
|
+
reflected.resourceSlots.every((slot, index) => {
|
|
152
|
+
const expected = receipt.resourceSlots[index];
|
|
153
|
+
return (
|
|
154
|
+
expected !== undefined &&
|
|
155
|
+
slot.name === expected.name &&
|
|
156
|
+
slot.parameter === expected.parameter &&
|
|
157
|
+
slot.kind === expected.kind &&
|
|
158
|
+
slot.group === expected.group &&
|
|
159
|
+
slot.binding === expected.binding
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
if (
|
|
163
|
+
reflected.layoutIdentity !== input.artifact.layoutIdentity ||
|
|
164
|
+
!sameSlots ||
|
|
165
|
+
receipt.resourceSlots.some((slot) => slot.group !== 1)
|
|
166
|
+
) {
|
|
167
|
+
return { ok: false, error: { code: 'invalid-material-receipt' } };
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
ok: true,
|
|
171
|
+
value: {
|
|
172
|
+
entryPoint: receipt.sceneIndexEntry,
|
|
173
|
+
layoutIdentity: input.artifact.layoutIdentity,
|
|
174
|
+
receiptGeneration: receipt.generation,
|
|
175
|
+
bindGroups: [input.view, input.material, input.mesh, input.instances],
|
|
176
|
+
resourceSlots: receipt.resourceSlots,
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
42
181
|
export const GPU_DRIVEN_RIGID_UNLIT_WGSL = /* wgsl */ `
|
|
43
182
|
${GPU_SCENE_WGSL}
|
|
44
183
|
|
|
@@ -87,6 +226,11 @@ interface PreparedBatch {
|
|
|
87
226
|
readonly batch: GpuDrivenBatch;
|
|
88
227
|
readonly mesh: MeshGpuHandles;
|
|
89
228
|
readonly renderState: MaterialRenderState | undefined;
|
|
229
|
+
/** Stable World attachment key used to join same-submit GPU counters. */
|
|
230
|
+
readonly worldKey: number;
|
|
231
|
+
/** Stable representative slot identity for this homogeneous batch. */
|
|
232
|
+
readonly primitiveSlot: number;
|
|
233
|
+
readonly slotGeneration: number;
|
|
90
234
|
}
|
|
91
235
|
|
|
92
236
|
interface ProjectedBatch extends PreparedBatch {
|
|
@@ -118,8 +262,8 @@ function activeFrustum(camera: CameraSnapshot): Float32Array {
|
|
|
118
262
|
projection,
|
|
119
263
|
camera.orthoLeft,
|
|
120
264
|
camera.orthoRight,
|
|
121
|
-
camera.orthoBottom,
|
|
122
265
|
camera.orthoTop,
|
|
266
|
+
camera.orthoBottom,
|
|
123
267
|
camera.near,
|
|
124
268
|
camera.far,
|
|
125
269
|
);
|
|
@@ -133,55 +277,172 @@ function activeFrustum(camera: CameraSnapshot): Float32Array {
|
|
|
133
277
|
);
|
|
134
278
|
}
|
|
135
279
|
|
|
280
|
+
/**
|
|
281
|
+
* LOD projected height is a view fact, not a scene-topology fact. Keep the
|
|
282
|
+
* filtered plan cache scene-stable while still rebuilding its candidate
|
|
283
|
+
* heights whenever a camera input used by the selector changes.
|
|
284
|
+
*/
|
|
285
|
+
function lodProjectionFingerprint(camera: CameraSnapshot): string {
|
|
286
|
+
return JSON.stringify({
|
|
287
|
+
position: Array.from(camera.position),
|
|
288
|
+
world: Array.from(camera.world),
|
|
289
|
+
projection: camera.projection,
|
|
290
|
+
fov: camera.fov,
|
|
291
|
+
aspect: camera.aspect,
|
|
292
|
+
orthoHeight: camera.orthoTop - camera.orthoBottom,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
136
296
|
function productionEligible(snapshot: RenderableSnapshot, batch: GpuDrivenBatch): boolean {
|
|
137
297
|
const material = snapshot.materials[batch.key.materialSlot] ?? snapshot.material;
|
|
138
298
|
const shader = material.materialShaderId ?? 'forgeax::default-unlit';
|
|
139
299
|
return (
|
|
140
300
|
shader === 'forgeax::default-unlit' &&
|
|
301
|
+
// The production rigid shader owns triangle-list raster only. Lines and
|
|
302
|
+
// points stay with their CPU/specialized topology owners.
|
|
303
|
+
batch.key.topology === 'triangle-list' &&
|
|
141
304
|
batch.key.materialResourceClass === EMPTY_RESOURCE_CLASS &&
|
|
142
305
|
material.baseColorTexture === undefined &&
|
|
143
306
|
material.transparent !== true
|
|
144
307
|
);
|
|
145
308
|
}
|
|
146
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Prepared Standard PBR draws have an explicit receipt identity on their
|
|
312
|
+
* batch key. Unprepared batches stay on the rigid production shader; the
|
|
313
|
+
* presence of a frame-wide PBR artifact is not permission to reinterpret an
|
|
314
|
+
* unlit batch with the PBR bind-group layout.
|
|
315
|
+
*/
|
|
316
|
+
function isStandardPbrBatch(batch: GpuDrivenBatch): boolean {
|
|
317
|
+
return batch.key.preparedIdentity !== undefined;
|
|
318
|
+
}
|
|
319
|
+
|
|
147
320
|
function meshSupportsBatch(mesh: MeshGpuHandles, batch: GpuDrivenBatch): boolean {
|
|
148
|
-
if (
|
|
321
|
+
if (
|
|
322
|
+
batch.candidates.some((candidate) => {
|
|
323
|
+
const lodCount = candidate.lodCoverages?.length ?? 0;
|
|
324
|
+
return (
|
|
325
|
+
lodCount > 1 &&
|
|
326
|
+
(mesh.lodRanges === undefined ||
|
|
327
|
+
mesh.lodRanges.length < lodCount ||
|
|
328
|
+
mesh.lodRanges.some((ranges) => ranges[candidate.drawItemIndex] === undefined))
|
|
329
|
+
);
|
|
330
|
+
})
|
|
331
|
+
) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
149
334
|
return batch.key.drawKind === 'indexed'
|
|
150
335
|
? mesh.indexed && mesh.indexBuffer !== null
|
|
151
336
|
: !mesh.indexed;
|
|
152
337
|
}
|
|
153
338
|
|
|
339
|
+
function selectedLodLevel(candidate: GpuDrivenCandidate): number {
|
|
340
|
+
const coverages = candidate.lodCoverages;
|
|
341
|
+
if (coverages === undefined || coverages.length <= 1) return 0;
|
|
342
|
+
return selectLod({
|
|
343
|
+
levels: coverages.slice(1).map((screenCoverage) => ({ screenCoverage })),
|
|
344
|
+
projectedHeight: candidate.projectedHeight ?? Number.NaN,
|
|
345
|
+
previousLevel: 0,
|
|
346
|
+
hysteresis: candidate.lodHysteresis ?? 0.08,
|
|
347
|
+
ready: coverages.map(() => true),
|
|
348
|
+
historyValid: false,
|
|
349
|
+
}).level;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function selectedLodRange(
|
|
353
|
+
batch: GpuDrivenBatch,
|
|
354
|
+
candidate: GpuDrivenCandidate,
|
|
355
|
+
): { readonly first: number; readonly count: number; readonly baseVertex: number } {
|
|
356
|
+
const level = selectedLodLevel(candidate);
|
|
357
|
+
return (
|
|
358
|
+
(level === 0 ? undefined : candidate.lodRanges?.[level - 1]) ?? {
|
|
359
|
+
first: batch.key.first,
|
|
360
|
+
count: batch.key.count,
|
|
361
|
+
baseVertex: batch.key.baseVertex,
|
|
362
|
+
}
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* One raster indirect command is emitted for each selector batch that has at
|
|
368
|
+
* least one admitted candidate. Selector-only batches remain in the compute
|
|
369
|
+
* and readback plan, but they must not inflate the raster command count.
|
|
370
|
+
*/
|
|
371
|
+
function admittedRasterBatchCount(plan: SubmissionPlan): number {
|
|
372
|
+
return plan.batches.reduce((count, batch) => count + (batch.visibleCapacity > 0 ? 1 : 0), 0);
|
|
373
|
+
}
|
|
374
|
+
|
|
154
375
|
function filteredPlan(
|
|
155
376
|
source: SubmissionPlan,
|
|
156
377
|
slots: PersistentGpuDrivenState['slots'],
|
|
157
378
|
meshes: ReadonlyMap<number, MeshGpuHandles>,
|
|
379
|
+
camera: CameraSnapshot,
|
|
380
|
+
activeEntityKeys?: ReadonlySet<number>,
|
|
381
|
+
worldKeys?: readonly number[],
|
|
158
382
|
): {
|
|
159
383
|
readonly plan: SubmissionPlan;
|
|
160
384
|
readonly batches: readonly PreparedBatch[];
|
|
161
385
|
readonly entityKeys: ReadonlySet<number>;
|
|
386
|
+
/** No source primitive can enter this lane without a topology/material change. */
|
|
387
|
+
readonly topologyInvariantEmpty: boolean;
|
|
162
388
|
} {
|
|
163
389
|
const slotByPrimitive = new Map(slots.map((slot) => [slot.slot, slot] as const));
|
|
390
|
+
const participatingByPrimitive = new Map<number, Set<GpuDrivenBatch>>();
|
|
391
|
+
for (const batch of source.batches) {
|
|
392
|
+
for (const candidate of batch.candidates) {
|
|
393
|
+
const participating = participatingByPrimitive.get(candidate.primitiveIndex);
|
|
394
|
+
if (participating === undefined) {
|
|
395
|
+
participatingByPrimitive.set(candidate.primitiveIndex, new Set([batch]));
|
|
396
|
+
} else {
|
|
397
|
+
participating.add(batch);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
164
401
|
const entityKeys = new Set<number>();
|
|
402
|
+
const worldKeyFor = (slot: PersistentGpuDrivenState['slots'][number]): number =>
|
|
403
|
+
worldKeys?.[slot.snapshot.worldId] ?? slot.snapshot.worldId;
|
|
404
|
+
const activeKey = (slot: PersistentGpuDrivenState['slots'][number]): number =>
|
|
405
|
+
worldEntityKey(worldKeyFor(slot), slot.snapshot.entityKey);
|
|
165
406
|
const eligiblePrimitives = new Set<number>();
|
|
407
|
+
let topologyEligiblePrimitiveCount = 0;
|
|
166
408
|
for (const slot of slots) {
|
|
167
409
|
const mesh = meshes.get(slot.snapshot.assetHandle);
|
|
168
|
-
const participating =
|
|
169
|
-
|
|
170
|
-
|
|
410
|
+
const participating = participatingByPrimitive.get(slot.slot);
|
|
411
|
+
const participatingBatches = participating === undefined ? [] : [...participating];
|
|
412
|
+
const topologyEligible =
|
|
413
|
+
participatingBatches.length > 0 &&
|
|
414
|
+
participatingBatches.length === (slot.snapshot.gpuDrivenDraws?.length ?? 0) &&
|
|
415
|
+
participatingBatches.every((batch) => productionEligible(slot.snapshot, batch));
|
|
416
|
+
if (topologyEligible) topologyEligiblePrimitiveCount += 1;
|
|
171
417
|
if (
|
|
172
418
|
mesh !== undefined &&
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
(batch) => productionEligible(slot.snapshot, batch) && meshSupportsBatch(mesh, batch),
|
|
176
|
-
)
|
|
419
|
+
topologyEligible &&
|
|
420
|
+
participatingBatches.every((batch) => meshSupportsBatch(mesh, batch))
|
|
177
421
|
) {
|
|
178
422
|
eligiblePrimitives.add(slot.slot);
|
|
179
|
-
|
|
423
|
+
// Record validation still keys extracted renderables by their current
|
|
424
|
+
// stable world/entity key. Suppressed candidates remain in the GPU
|
|
425
|
+
// selector, but only admitted entities are owned by the compacted draw.
|
|
426
|
+
if (activeEntityKeys === undefined || activeEntityKeys.has(activeKey(slot))) {
|
|
427
|
+
entityKeys.add(activeKey(slot));
|
|
428
|
+
}
|
|
180
429
|
}
|
|
181
430
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
431
|
+
// BatchTopology deliberately keeps LOD candidates individually addressable
|
|
432
|
+
// so topology updates never alias a candidate's selector state. Once the
|
|
433
|
+
// camera has projected the candidates, the renderer can safely coalesce
|
|
434
|
+
// candidates that have identical immutable geometry/LOD facts and the same
|
|
435
|
+
// selected range. The GPU selector uses the same no-history inputs below,
|
|
436
|
+
// so one indirect range remains correct for every candidate in a group.
|
|
437
|
+
const groups = new Map<
|
|
438
|
+
string,
|
|
439
|
+
{
|
|
440
|
+
readonly source: GpuDrivenBatch;
|
|
441
|
+
readonly mesh: MeshGpuHandles;
|
|
442
|
+
readonly renderState: MaterialRenderState | undefined;
|
|
443
|
+
readonly candidates: GpuDrivenCandidate[];
|
|
444
|
+
}
|
|
445
|
+
>();
|
|
185
446
|
for (const batch of source.batches) {
|
|
186
447
|
const candidates = batch.candidates.filter((candidate) => {
|
|
187
448
|
const slot = slotByPrimitive.get(candidate.primitiveIndex);
|
|
@@ -190,24 +451,84 @@ function filteredPlan(
|
|
|
190
451
|
return mesh !== undefined && meshSupportsBatch(mesh, batch);
|
|
191
452
|
});
|
|
192
453
|
if (candidates.length === 0) continue;
|
|
454
|
+
for (const candidate of candidates) {
|
|
455
|
+
const slot = slotByPrimitive.get(candidate.primitiveIndex);
|
|
456
|
+
if (slot === undefined) continue;
|
|
457
|
+
const mesh = meshes.get(slot.snapshot.assetHandle);
|
|
458
|
+
if (mesh === undefined) continue;
|
|
459
|
+
const physicalLodRanges = mesh.lodRanges
|
|
460
|
+
?.slice(1)
|
|
461
|
+
.map((ranges) => ranges[candidate.drawItemIndex]);
|
|
462
|
+
const projected = Object.freeze({
|
|
463
|
+
...candidate,
|
|
464
|
+
submitAdmission: activeEntityKeys === undefined || activeEntityKeys.has(activeKey(slot)),
|
|
465
|
+
projectedHeight: projectedHeightForCandidate(slot, camera),
|
|
466
|
+
...(physicalLodRanges?.every((range) => range !== undefined)
|
|
467
|
+
? { lodRanges: physicalLodRanges as NonNullable<typeof candidate.lodRanges> }
|
|
468
|
+
: {}),
|
|
469
|
+
});
|
|
470
|
+
const selectedRange = selectedLodRange(batch, projected);
|
|
471
|
+
const groupKey = JSON.stringify({
|
|
472
|
+
key: batch.key,
|
|
473
|
+
worldKey: worldKeyFor(slot),
|
|
474
|
+
lodCoverages: projected.lodCoverages ?? null,
|
|
475
|
+
lodRanges: projected.lodRanges ?? null,
|
|
476
|
+
selectedRange,
|
|
477
|
+
selectedLevel: selectedLodLevel(projected),
|
|
478
|
+
});
|
|
479
|
+
const existing = groups.get(groupKey);
|
|
480
|
+
if (existing === undefined) {
|
|
481
|
+
const material = slot.snapshot.materials[batch.key.materialSlot] ?? slot.snapshot.material;
|
|
482
|
+
groups.set(groupKey, {
|
|
483
|
+
source: batch,
|
|
484
|
+
mesh,
|
|
485
|
+
renderState: geometryRenderStateForTopology(batch.key.topology, material.renderState),
|
|
486
|
+
candidates: [projected],
|
|
487
|
+
});
|
|
488
|
+
} else {
|
|
489
|
+
existing.candidates.push(projected);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const batches: PreparedBatch[] = [];
|
|
495
|
+
let visibleBase = 0;
|
|
496
|
+
const filteredBatches: GpuDrivenBatch[] = [];
|
|
497
|
+
let batchId = 0;
|
|
498
|
+
for (const group of groups.values()) {
|
|
193
499
|
visibleBase = Math.ceil(visibleBase / 64) * 64;
|
|
500
|
+
const admittedCount = group.candidates.reduce(
|
|
501
|
+
(count, candidate) =>
|
|
502
|
+
count +
|
|
503
|
+
((candidate as GpuDrivenCandidate & { readonly submitAdmission?: boolean })
|
|
504
|
+
.submitAdmission === false
|
|
505
|
+
? 0
|
|
506
|
+
: 1),
|
|
507
|
+
0,
|
|
508
|
+
);
|
|
194
509
|
const filtered: GpuDrivenBatch = Object.freeze({
|
|
195
|
-
...
|
|
196
|
-
|
|
510
|
+
...group.source,
|
|
511
|
+
batchId,
|
|
512
|
+
candidates: Object.freeze(group.candidates),
|
|
197
513
|
visibleBase,
|
|
198
|
-
visibleCapacity:
|
|
514
|
+
visibleCapacity: admittedCount,
|
|
515
|
+
indirectOffset: batchId * 20,
|
|
199
516
|
});
|
|
200
|
-
visibleBase +=
|
|
201
|
-
const first = slotByPrimitive.get(candidates[0]?.primitiveIndex ?? -1);
|
|
202
|
-
const mesh = first === undefined ? undefined : meshes.get(first.snapshot.assetHandle);
|
|
203
|
-
if (mesh === undefined || first === undefined) continue;
|
|
517
|
+
visibleBase += admittedCount;
|
|
204
518
|
filteredBatches.push(filtered);
|
|
205
|
-
const
|
|
519
|
+
const firstCandidate = group.candidates[0];
|
|
520
|
+
const firstSlot =
|
|
521
|
+
firstCandidate === undefined ? undefined : slotByPrimitive.get(firstCandidate.primitiveIndex);
|
|
522
|
+
if (firstCandidate === undefined || firstSlot === undefined) continue;
|
|
206
523
|
batches.push({
|
|
207
524
|
batch: filtered,
|
|
208
|
-
mesh,
|
|
209
|
-
renderState:
|
|
525
|
+
mesh: group.mesh,
|
|
526
|
+
renderState: group.renderState,
|
|
527
|
+
worldKey: worldKeyFor(firstSlot),
|
|
528
|
+
primitiveSlot: firstSlot.slot,
|
|
529
|
+
slotGeneration: firstSlot.generation,
|
|
210
530
|
});
|
|
531
|
+
batchId += 1;
|
|
211
532
|
}
|
|
212
533
|
return {
|
|
213
534
|
plan: Object.freeze({
|
|
@@ -218,11 +539,32 @@ function filteredPlan(
|
|
|
218
539
|
}),
|
|
219
540
|
batches: Object.freeze(batches),
|
|
220
541
|
entityKeys,
|
|
542
|
+
topologyInvariantEmpty: topologyEligiblePrimitiveCount === 0,
|
|
221
543
|
};
|
|
222
544
|
}
|
|
223
545
|
|
|
224
546
|
type FilteredProductionPlan = ReturnType<typeof filteredPlan>;
|
|
225
547
|
|
|
548
|
+
export interface GpuDrivenWorldLodSelectionInspection {
|
|
549
|
+
readonly worldKey: number;
|
|
550
|
+
readonly primitiveSlot: number;
|
|
551
|
+
readonly slotGeneration: number;
|
|
552
|
+
readonly candidateCount: number;
|
|
553
|
+
readonly visible: number;
|
|
554
|
+
readonly occluded: number;
|
|
555
|
+
readonly lodHistogram: readonly { readonly level: number; readonly count: number }[];
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function sameEntityKeySet(
|
|
559
|
+
left: ReadonlySet<number> | undefined,
|
|
560
|
+
right: ReadonlySet<number> | undefined,
|
|
561
|
+
): boolean {
|
|
562
|
+
if (left === right) return true;
|
|
563
|
+
if (left === undefined || right === undefined || left.size !== right.size) return false;
|
|
564
|
+
for (const key of left) if (!right.has(key)) return false;
|
|
565
|
+
return true;
|
|
566
|
+
}
|
|
567
|
+
|
|
226
568
|
class GpuDrivenRigidRaster {
|
|
227
569
|
private readonly pipelines = new Map<string, RenderPipeline>();
|
|
228
570
|
|
|
@@ -375,10 +717,212 @@ class GpuDrivenRigidRaster {
|
|
|
375
717
|
}
|
|
376
718
|
}
|
|
377
719
|
|
|
720
|
+
/** The GPU-driven bridge to the existing four-group Standard PBR pipeline. */
|
|
721
|
+
class StandardPbrRasterAdapter {
|
|
722
|
+
private readonly pipelines = new Map<string, RenderPipeline>();
|
|
723
|
+
private readonly projectionBuffers: Buffer[] = [];
|
|
724
|
+
|
|
725
|
+
private constructor(
|
|
726
|
+
private readonly device: RhiDevice,
|
|
727
|
+
private readonly pipelineLayout: import('@forgeax/engine-rhi').PipelineLayout,
|
|
728
|
+
private readonly meshLayout: BindGroupLayout,
|
|
729
|
+
private readonly module: ShaderModule,
|
|
730
|
+
private readonly materialBindGroup: BindGroup,
|
|
731
|
+
private readonly instancesBindGroup: BindGroup,
|
|
732
|
+
private readonly artifact: MaterialShaderArtifact,
|
|
733
|
+
) {}
|
|
734
|
+
|
|
735
|
+
static create(input: {
|
|
736
|
+
readonly device: RhiDevice;
|
|
737
|
+
readonly shaderModuleFactory: PipelineBuilderShaderModuleFactory;
|
|
738
|
+
readonly pipelineState: PipelineState;
|
|
739
|
+
readonly artifact: MaterialShaderArtifact;
|
|
740
|
+
}): Result<StandardPbrRasterAdapter, RhiError> {
|
|
741
|
+
const pipelineLayout = input.pipelineState.pbrPipelineLayout;
|
|
742
|
+
const fallback = input.pipelineState.skylightFallback;
|
|
743
|
+
if (pipelineLayout === null || fallback === null) {
|
|
744
|
+
return err(
|
|
745
|
+
new RhiError({
|
|
746
|
+
code: 'rhi-not-available',
|
|
747
|
+
expected: 'Standard PBR pipeline layout and Skylight fallback',
|
|
748
|
+
hint: 'initialize the Standard PBR pipeline before enabling GPU-driven raster',
|
|
749
|
+
}),
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
const module = input.shaderModuleFactory.createShaderModule({
|
|
753
|
+
label: 'standard-pbr-gpu-driven-scene-index',
|
|
754
|
+
code: input.artifact.wgsl,
|
|
755
|
+
});
|
|
756
|
+
if (!module.ok) return module;
|
|
757
|
+
const materialBuffer = input.device.createBuffer({
|
|
758
|
+
label: 'standard-pbr-gpu-driven-material-row',
|
|
759
|
+
size: 384,
|
|
760
|
+
usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
|
|
761
|
+
});
|
|
762
|
+
if (!materialBuffer.ok) return materialBuffer;
|
|
763
|
+
const materialPayload = new Float32Array(96);
|
|
764
|
+
materialPayload[0] = 1;
|
|
765
|
+
materialPayload[1] = 1;
|
|
766
|
+
materialPayload[2] = 1;
|
|
767
|
+
materialPayload[3] = 1;
|
|
768
|
+
materialPayload[5] = 1;
|
|
769
|
+
materialPayload[6] = 1;
|
|
770
|
+
const materialWrite = input.device.queue.writeBuffer(materialBuffer.value, 0, materialPayload);
|
|
771
|
+
if (!materialWrite.ok) return materialWrite;
|
|
772
|
+
const baseEntries = [
|
|
773
|
+
{ binding: 0, resource: bufferBinding(materialBuffer.value, 0, 384) },
|
|
774
|
+
...Array.from({ length: 8 }, (_, index) => [
|
|
775
|
+
{
|
|
776
|
+
binding: index * 2 + 1,
|
|
777
|
+
resource: { kind: 'sampler' as const, value: fallback.sampler },
|
|
778
|
+
},
|
|
779
|
+
{
|
|
780
|
+
binding: index * 2 + 2,
|
|
781
|
+
resource: {
|
|
782
|
+
kind: 'textureView' as const,
|
|
783
|
+
value:
|
|
784
|
+
index === 2
|
|
785
|
+
? input.pipelineState.defaultNormalTextureView
|
|
786
|
+
: input.pipelineState.defaultWhiteTextureView,
|
|
787
|
+
},
|
|
788
|
+
},
|
|
789
|
+
]).flat(),
|
|
790
|
+
];
|
|
791
|
+
const materialEntries = assembleMaterialWithSkylightEntries(
|
|
792
|
+
baseEntries,
|
|
793
|
+
{
|
|
794
|
+
irradianceView: fallback.irradianceView,
|
|
795
|
+
irradianceSampler: fallback.sampler,
|
|
796
|
+
prefilterView: fallback.prefilterView,
|
|
797
|
+
prefilterSampler: fallback.sampler,
|
|
798
|
+
brdfLutView: fallback.brdfLutView,
|
|
799
|
+
brdfLutSampler: fallback.sampler,
|
|
800
|
+
intensityBuffer: fallback.intensityBuffer,
|
|
801
|
+
},
|
|
802
|
+
{ sampler: fallback.sampler, backdropView: input.pipelineState.defaultWhiteTextureView },
|
|
803
|
+
);
|
|
804
|
+
const materialBindGroup = input.device.createBindGroup({
|
|
805
|
+
label: 'standard-pbr-gpu-driven-material-bg',
|
|
806
|
+
layout: input.pipelineState.materialBindGroupLayout,
|
|
807
|
+
entries: materialEntries,
|
|
808
|
+
});
|
|
809
|
+
if (!materialBindGroup.ok) return materialBindGroup;
|
|
810
|
+
const instancesBindGroup = input.device.createBindGroup({
|
|
811
|
+
label: 'standard-pbr-gpu-driven-identity-instances-bg',
|
|
812
|
+
layout: input.pipelineState.instancesBindGroupLayout,
|
|
813
|
+
entries: [
|
|
814
|
+
{
|
|
815
|
+
binding: 0,
|
|
816
|
+
resource: bufferBinding(input.pipelineState.identityInstanceBuffer),
|
|
817
|
+
},
|
|
818
|
+
],
|
|
819
|
+
});
|
|
820
|
+
if (!instancesBindGroup.ok) return instancesBindGroup;
|
|
821
|
+
return ok(
|
|
822
|
+
new StandardPbrRasterAdapter(
|
|
823
|
+
input.device,
|
|
824
|
+
pipelineLayout,
|
|
825
|
+
input.pipelineState.meshBindGroupLayout,
|
|
826
|
+
module.value,
|
|
827
|
+
materialBindGroup.value,
|
|
828
|
+
instancesBindGroup.value,
|
|
829
|
+
input.artifact,
|
|
830
|
+
),
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
pipeline(
|
|
835
|
+
format: TextureFormat,
|
|
836
|
+
sampleCount: 1 | 4,
|
|
837
|
+
layoutProjection: MeshGpuHandles['layoutProjection'],
|
|
838
|
+
topology: GpuDrivenBatch['key']['topology'],
|
|
839
|
+
stripIndexFormat: 'uint16' | 'uint32' | undefined,
|
|
840
|
+
renderState: MaterialRenderState | undefined,
|
|
841
|
+
): Result<RenderPipeline, RhiError> {
|
|
842
|
+
const projectedLayout = deriveVertexBufferLayoutFromProjection(
|
|
843
|
+
layoutProjection,
|
|
844
|
+
this.artifact.uvSetCount === undefined
|
|
845
|
+
? undefined
|
|
846
|
+
: { shaderUvSetCount: this.artifact.uvSetCount },
|
|
847
|
+
)[0];
|
|
848
|
+
const vertexStride = projectedLayout?.arrayStride ?? layoutProjection.arrayStride;
|
|
849
|
+
const attributes = projectedLayout?.attributes ?? layoutProjection.attributes;
|
|
850
|
+
const key = `${format}|${sampleCount}|${vertexStride}|${topology}|${stripIndexFormat ?? ''}|${JSON.stringify(renderState ?? null)}|${this.artifact.layoutIdentity}`;
|
|
851
|
+
const cached = this.pipelines.get(key);
|
|
852
|
+
if (cached !== undefined) return ok(cached);
|
|
853
|
+
const pipeline = this.device.createRenderPipeline({
|
|
854
|
+
label: `standard-pbr-gpu-driven.${key}`,
|
|
855
|
+
layout: this.pipelineLayout,
|
|
856
|
+
vertex: {
|
|
857
|
+
module: this.module,
|
|
858
|
+
entryPoint: this.artifact.receipt?.sceneIndexEntry ?? 'vs_scene_index',
|
|
859
|
+
buffers: [
|
|
860
|
+
{
|
|
861
|
+
arrayStride: vertexStride,
|
|
862
|
+
stepMode: 'vertex',
|
|
863
|
+
attributes: attributes.map(({ shaderLocation, offset, format }) => ({
|
|
864
|
+
shaderLocation,
|
|
865
|
+
offset,
|
|
866
|
+
format: format as GPUVertexFormat,
|
|
867
|
+
})),
|
|
868
|
+
},
|
|
869
|
+
],
|
|
870
|
+
},
|
|
871
|
+
fragment: {
|
|
872
|
+
module: this.module,
|
|
873
|
+
entryPoint: 'fs_main',
|
|
874
|
+
targets: [{ format }],
|
|
875
|
+
},
|
|
876
|
+
primitive: {
|
|
877
|
+
topology,
|
|
878
|
+
cullMode: renderState?.cullMode ?? 'back',
|
|
879
|
+
frontFace: renderState?.frontFace ?? 'ccw',
|
|
880
|
+
...(stripIndexFormat === undefined ? {} : { stripIndexFormat }),
|
|
881
|
+
},
|
|
882
|
+
depthStencil: {
|
|
883
|
+
format: 'depth24plus-stencil8',
|
|
884
|
+
depthWriteEnabled: renderState?.depthWriteEnabled ?? true,
|
|
885
|
+
depthCompare: renderState?.depthCompare ?? 'less',
|
|
886
|
+
},
|
|
887
|
+
...(sampleCount === 4 ? { multisample: { count: 4 } } : {}),
|
|
888
|
+
});
|
|
889
|
+
if (!pipeline.ok) return pipeline;
|
|
890
|
+
this.pipelines.set(key, pipeline.value);
|
|
891
|
+
return pipeline;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
projectionBindGroup(rows: Float32Array): Result<BindGroup, RhiError> {
|
|
895
|
+
const buffer = this.device.createBuffer({
|
|
896
|
+
label: 'standard-pbr-gpu-driven-scene-meshes',
|
|
897
|
+
size: Math.max(MESH_SSBO_BYTES, rows.byteLength),
|
|
898
|
+
usage: GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST,
|
|
899
|
+
});
|
|
900
|
+
if (!buffer.ok) return buffer;
|
|
901
|
+
const write = this.device.queue.writeBuffer(buffer.value, 0, rows);
|
|
902
|
+
if (!write.ok) return write;
|
|
903
|
+
this.projectionBuffers.push(buffer.value);
|
|
904
|
+
return this.device.createBindGroup({
|
|
905
|
+
label: 'standard-pbr-gpu-driven-scene-mesh-bg',
|
|
906
|
+
layout: this.meshLayout,
|
|
907
|
+
entries: [{ binding: 0, resource: bufferBinding(buffer.value, 0, rows.byteLength) }],
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
dispose(): void {
|
|
912
|
+
for (const buffer of this.projectionBuffers) this.device.destroyBuffer(buffer);
|
|
913
|
+
this.projectionBuffers.length = 0;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
get frameGroups(): readonly [BindGroup, BindGroup] {
|
|
917
|
+
return [this.materialBindGroup, this.instancesBindGroup];
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
378
921
|
export interface PreparedGpuDrivenFrame {
|
|
379
922
|
readonly topologySignature: string;
|
|
380
923
|
readonly entityKeys: ReadonlySet<number>;
|
|
381
924
|
readonly ownsAllRenderables: boolean;
|
|
925
|
+
readonly occlusion?: OcclusionFrameProjection;
|
|
382
926
|
/** @internal Commits a replacement generation only after graph promotion. */
|
|
383
927
|
readonly _commitResourceReplacement: () => void;
|
|
384
928
|
project(
|
|
@@ -388,15 +932,74 @@ export interface PreparedGpuDrivenFrame {
|
|
|
388
932
|
): Result<RenderPipelineGpuDrivenProjection, RenderGraphError | RhiError>;
|
|
389
933
|
}
|
|
390
934
|
|
|
935
|
+
export interface RecoveryCapabilityFacts {
|
|
936
|
+
readonly generation: number;
|
|
937
|
+
readonly shaderCompilation: boolean;
|
|
938
|
+
readonly compute: boolean;
|
|
939
|
+
readonly storageBuffer: boolean;
|
|
940
|
+
readonly indirectDrawing: boolean;
|
|
941
|
+
readonly multisample: boolean;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
export type RecoveryCapabilityOutcome =
|
|
945
|
+
| {
|
|
946
|
+
readonly status: 'ready' | 'fallback';
|
|
947
|
+
readonly generation: number;
|
|
948
|
+
readonly disabled: readonly string[];
|
|
949
|
+
}
|
|
950
|
+
| {
|
|
951
|
+
readonly status: 'disabled';
|
|
952
|
+
readonly generation: number;
|
|
953
|
+
readonly disabled: readonly string[];
|
|
954
|
+
readonly allocations: 0;
|
|
955
|
+
}
|
|
956
|
+
| {
|
|
957
|
+
readonly status: 'refused';
|
|
958
|
+
readonly generation: number;
|
|
959
|
+
readonly failedOwner: 'shader-material-pipeline';
|
|
960
|
+
readonly resourceKind: 'pipeline';
|
|
961
|
+
};
|
|
962
|
+
|
|
963
|
+
export function classifyRecoveryCapability(
|
|
964
|
+
facts: RecoveryCapabilityFacts,
|
|
965
|
+
): RecoveryCapabilityOutcome {
|
|
966
|
+
if (!facts.shaderCompilation) {
|
|
967
|
+
return {
|
|
968
|
+
status: 'refused',
|
|
969
|
+
generation: facts.generation,
|
|
970
|
+
failedOwner: 'shader-material-pipeline',
|
|
971
|
+
resourceKind: 'pipeline',
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
if (!facts.compute || !facts.storageBuffer || !facts.indirectDrawing) {
|
|
975
|
+
return {
|
|
976
|
+
status: 'disabled',
|
|
977
|
+
generation: facts.generation,
|
|
978
|
+
disabled: ['gpu-driven'],
|
|
979
|
+
allocations: 0,
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
return {
|
|
983
|
+
status: facts.multisample ? 'ready' : 'fallback',
|
|
984
|
+
generation: facts.generation,
|
|
985
|
+
disabled: [],
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
|
|
391
989
|
export class GpuDrivenProduction {
|
|
990
|
+
private disposed = false;
|
|
392
991
|
private view: GpuDrivenView | undefined;
|
|
393
992
|
private raster: GpuDrivenRigidRaster | undefined;
|
|
993
|
+
private standardRaster: StandardPbrRasterAdapter | undefined;
|
|
394
994
|
private readonly sceneIdentities = new WeakMap<object, number>();
|
|
395
995
|
private nextSceneIdentity = 1;
|
|
396
996
|
private filteredCache:
|
|
397
997
|
| {
|
|
398
998
|
readonly source: SubmissionPlan;
|
|
399
999
|
readonly meshResidencyEpoch: number;
|
|
1000
|
+
readonly lodProjectionFingerprint: string;
|
|
1001
|
+
readonly activeEntityRevision?: number;
|
|
1002
|
+
readonly activeEntityKeys?: ReadonlySet<number>;
|
|
400
1003
|
readonly value: FilteredProductionPlan;
|
|
401
1004
|
}
|
|
402
1005
|
| undefined;
|
|
@@ -406,6 +1009,13 @@ export class GpuDrivenProduction {
|
|
|
406
1009
|
private batchBindGroupCreates = 0;
|
|
407
1010
|
private validatedGpuOwnedRows = 0;
|
|
408
1011
|
private cpuFallbackDrawItems = 0;
|
|
1012
|
+
private telemetryPrepared = false;
|
|
1013
|
+
private telemetryCandidateCount = 0;
|
|
1014
|
+
private telemetryGeometryWork = 0;
|
|
1015
|
+
private telemetryRootGeometryWork = 0;
|
|
1016
|
+
private indirectDrawCount = 0;
|
|
1017
|
+
private sceneRowsRequired = true;
|
|
1018
|
+
private standardPbrProjection: StandardPbrSceneProjection | undefined;
|
|
409
1019
|
private signatureCache:
|
|
410
1020
|
| {
|
|
411
1021
|
readonly filtered: FilteredProductionPlan;
|
|
@@ -421,45 +1031,171 @@ export class GpuDrivenProduction {
|
|
|
421
1031
|
private readonly shaderModuleFactory: PipelineBuilderShaderModuleFactory,
|
|
422
1032
|
) {}
|
|
423
1033
|
|
|
1034
|
+
static forDevice(
|
|
1035
|
+
device: RhiDevice,
|
|
1036
|
+
shaderModuleFactory: PipelineBuilderShaderModuleFactory,
|
|
1037
|
+
): GpuDrivenProduction {
|
|
1038
|
+
return new GpuDrivenProduction(device, shaderModuleFactory);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
createRecoveryRoot(scope: DeviceScope): LifecycleResourceSpec<unknown> {
|
|
1042
|
+
return {
|
|
1043
|
+
kind: 'scene-table',
|
|
1044
|
+
create: () => {
|
|
1045
|
+
if (!scope.isAlive()) throw new Error('GPU-driven candidate scope is not active.');
|
|
1046
|
+
// Keep the actual detached production owner in the generation
|
|
1047
|
+
// aggregate. The recovery transaction, not a marker object, is the
|
|
1048
|
+
// owner that is published or discarded exactly once.
|
|
1049
|
+
return this;
|
|
1050
|
+
},
|
|
1051
|
+
cleanup: () => undefined,
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
|
|
424
1055
|
prepare(input: {
|
|
425
1056
|
readonly scene: PersistentGpuDrivenState | undefined;
|
|
426
1057
|
readonly camera: CameraSnapshot;
|
|
427
1058
|
readonly meshes: ReadonlyMap<number, MeshGpuHandles>;
|
|
428
1059
|
readonly viewBindGroupLayout: BindGroupLayout;
|
|
429
1060
|
readonly meshResidencyEpoch: number;
|
|
1061
|
+
readonly hdrp: boolean;
|
|
1062
|
+
/** Entity keys admitted by the persistent visibility facet for this view. */
|
|
1063
|
+
readonly activeEntityKeys?: ReadonlySet<number>;
|
|
1064
|
+
/** Collision-free renderer-owned revision for the active visibility set. */
|
|
1065
|
+
readonly activeEntityRevision?: number;
|
|
1066
|
+
/** Total extracted candidates, including facet-suppressed rows. */
|
|
1067
|
+
readonly telemetryCandidateCount?: number;
|
|
1068
|
+
readonly occlusion?: OcclusionFrameProjection;
|
|
1069
|
+
/** Cooked producer artifact selected from the frame material catalog. */
|
|
1070
|
+
readonly standardPbrArtifact?: MaterialShaderArtifact;
|
|
1071
|
+
readonly standardPbrPipelineState?: PipelineState;
|
|
430
1072
|
}): Result<PreparedGpuDrivenFrame | undefined, RhiError> {
|
|
431
1073
|
this.gpuOwnedSnapshotsMaterialized = 0;
|
|
432
1074
|
this.filteredPlanBuilds = 0;
|
|
433
1075
|
this.batchBindGroupCreates = 0;
|
|
434
1076
|
this.validatedGpuOwnedRows = 0;
|
|
435
1077
|
this.cpuFallbackDrawItems = 0;
|
|
1078
|
+
this.telemetryPrepared = false;
|
|
1079
|
+
this.telemetryCandidateCount = input.telemetryCandidateCount ?? 0;
|
|
1080
|
+
this.telemetryGeometryWork = 0;
|
|
1081
|
+
this.telemetryRootGeometryWork = 0;
|
|
1082
|
+
this.indirectDrawCount = 0;
|
|
1083
|
+
const capability = classifyRecoveryCapability({
|
|
1084
|
+
generation: 0,
|
|
1085
|
+
shaderCompilation: true,
|
|
1086
|
+
compute: this.device.caps.compute,
|
|
1087
|
+
storageBuffer: this.device.caps.storageBuffer,
|
|
1088
|
+
indirectDrawing: this.device.caps.indirectDrawing,
|
|
1089
|
+
multisample: true,
|
|
1090
|
+
});
|
|
436
1091
|
if (
|
|
1092
|
+
input.hdrp ||
|
|
437
1093
|
input.scene === undefined ||
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
!this.device.caps.indirectDrawing
|
|
1094
|
+
capability.status === 'disabled' ||
|
|
1095
|
+
selectGpuLodLane(this.device.caps) === 'cpu'
|
|
441
1096
|
) {
|
|
442
1097
|
this.gpuOwnedEntityCount = 0;
|
|
1098
|
+
this.sceneRowsRequired = false;
|
|
443
1099
|
return ok(undefined);
|
|
444
1100
|
}
|
|
445
1101
|
const scene = input.scene;
|
|
1102
|
+
const projectionFingerprint = lodProjectionFingerprint(input.camera);
|
|
1103
|
+
// An empty filtered plan means this topology/mesh-residency pair has no
|
|
1104
|
+
// renderables that this production lane can own. Camera and visibility
|
|
1105
|
+
// inputs only affect admitted candidates and LOD selection, so they cannot
|
|
1106
|
+
// turn that empty topology into an eligible one. Keep the negative result
|
|
1107
|
+
// resident instead of rescanning every scene slot as the camera moves.
|
|
1108
|
+
const cachedPlanIsTopologyInvariant = this.filteredCache?.value.topologyInvariantEmpty === true;
|
|
1109
|
+
const cachedPlanIsCameraInvariant = cachedPlanIsTopologyInvariant;
|
|
1110
|
+
const cachedVisibilityMatches =
|
|
1111
|
+
cachedPlanIsCameraInvariant || this.filteredCache === undefined
|
|
1112
|
+
? true
|
|
1113
|
+
: input.activeEntityRevision !== undefined &&
|
|
1114
|
+
this.filteredCache.activeEntityRevision !== undefined
|
|
1115
|
+
? input.activeEntityRevision === this.filteredCache.activeEntityRevision
|
|
1116
|
+
: sameEntityKeySet(this.filteredCache.activeEntityKeys, input.activeEntityKeys);
|
|
446
1117
|
let filtered = this.filteredCache?.value;
|
|
447
1118
|
if (
|
|
448
1119
|
filtered === undefined ||
|
|
449
1120
|
this.filteredCache?.source !== scene.plan ||
|
|
450
|
-
|
|
1121
|
+
(!cachedPlanIsTopologyInvariant &&
|
|
1122
|
+
this.filteredCache.meshResidencyEpoch !== input.meshResidencyEpoch) ||
|
|
1123
|
+
(!cachedPlanIsCameraInvariant &&
|
|
1124
|
+
this.filteredCache.lodProjectionFingerprint !== projectionFingerprint) ||
|
|
1125
|
+
!cachedVisibilityMatches
|
|
451
1126
|
) {
|
|
452
|
-
filtered = filteredPlan(
|
|
1127
|
+
filtered = filteredPlan(
|
|
1128
|
+
scene.plan,
|
|
1129
|
+
scene.slots,
|
|
1130
|
+
input.meshes,
|
|
1131
|
+
input.camera,
|
|
1132
|
+
input.activeEntityKeys,
|
|
1133
|
+
scene.worldKeys,
|
|
1134
|
+
);
|
|
453
1135
|
this.filteredCache = {
|
|
454
1136
|
source: scene.plan,
|
|
455
1137
|
meshResidencyEpoch: input.meshResidencyEpoch,
|
|
1138
|
+
lodProjectionFingerprint: projectionFingerprint,
|
|
1139
|
+
...(input.activeEntityRevision === undefined
|
|
1140
|
+
? {}
|
|
1141
|
+
: { activeEntityRevision: input.activeEntityRevision }),
|
|
1142
|
+
...(input.activeEntityKeys === undefined
|
|
1143
|
+
? {}
|
|
1144
|
+
: { activeEntityKeys: input.activeEntityKeys }),
|
|
456
1145
|
value: filtered,
|
|
457
1146
|
};
|
|
458
1147
|
this.gpuOwnedSnapshotsMaterialized = scene.slots.length;
|
|
459
1148
|
this.filteredPlanBuilds = 1;
|
|
460
1149
|
}
|
|
461
1150
|
this.gpuOwnedEntityCount = filtered.entityKeys.size;
|
|
1151
|
+
this.sceneRowsRequired = !filtered.topologyInvariantEmpty;
|
|
1152
|
+
// Publish the command count from the current filtered plan on every
|
|
1153
|
+
// prepare call. A cached graph does not call project() again on stable
|
|
1154
|
+
// frames, so deriving this only while compiling the raster pass would
|
|
1155
|
+
// transiently report zero after prepare() reset the per-frame counters.
|
|
1156
|
+
this.indirectDrawCount = admittedRasterBatchCount(filtered.plan);
|
|
462
1157
|
if (filtered.batches.length === 0) return ok(undefined);
|
|
1158
|
+
this.standardPbrProjection = undefined;
|
|
1159
|
+
const usesStandardPbrRaster = filtered.batches.every(({ batch }) => isStandardPbrBatch(batch));
|
|
1160
|
+
if (usesStandardPbrRaster && input.standardPbrArtifact !== undefined) {
|
|
1161
|
+
const slotsByPrimitive = new Map(scene.slots.map((slot) => [slot.slot, slot] as const));
|
|
1162
|
+
const projection = projectStandardPbrScene({
|
|
1163
|
+
artifact: input.standardPbrArtifact,
|
|
1164
|
+
candidates: filtered.batches.flatMap(({ batch }) =>
|
|
1165
|
+
batch.candidates.flatMap((candidate) => {
|
|
1166
|
+
const slot = slotsByPrimitive.get(candidate.primitiveIndex);
|
|
1167
|
+
return slot === undefined
|
|
1168
|
+
? []
|
|
1169
|
+
: [
|
|
1170
|
+
{
|
|
1171
|
+
primitiveIndex: candidate.primitiveIndex,
|
|
1172
|
+
world: slot.snapshot.transform.world,
|
|
1173
|
+
},
|
|
1174
|
+
];
|
|
1175
|
+
}),
|
|
1176
|
+
),
|
|
1177
|
+
});
|
|
1178
|
+
if (!projection.ok) {
|
|
1179
|
+
return err(
|
|
1180
|
+
new RhiError({
|
|
1181
|
+
code: 'shader-compile-failed',
|
|
1182
|
+
expected: 'receipt-derived Standard PBR scene projection',
|
|
1183
|
+
hint: 'publish one validated MaterialShaderArtifact receipt before entering the GPU lane',
|
|
1184
|
+
}),
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
this.standardPbrProjection = projection.value;
|
|
1188
|
+
if (input.standardPbrPipelineState !== undefined && this.standardRaster === undefined) {
|
|
1189
|
+
const created = StandardPbrRasterAdapter.create({
|
|
1190
|
+
device: this.device,
|
|
1191
|
+
shaderModuleFactory: this.shaderModuleFactory,
|
|
1192
|
+
pipelineState: input.standardPbrPipelineState,
|
|
1193
|
+
artifact: input.standardPbrArtifact,
|
|
1194
|
+
});
|
|
1195
|
+
if (!created.ok) return created;
|
|
1196
|
+
this.standardRaster = created.value;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
463
1199
|
if (this.view === undefined) {
|
|
464
1200
|
const created = GpuDrivenView.create({
|
|
465
1201
|
device: this.device,
|
|
@@ -479,8 +1215,11 @@ export class GpuDrivenProduction {
|
|
|
479
1215
|
}
|
|
480
1216
|
const updated = this.view.update(filtered.plan, scene.scene, activeFrustum(input.camera));
|
|
481
1217
|
if (!updated.ok) return updated;
|
|
1218
|
+
this.telemetryPrepared = true;
|
|
482
1219
|
const view = this.view;
|
|
483
1220
|
const raster = this.raster;
|
|
1221
|
+
const standardRaster = this.standardRaster;
|
|
1222
|
+
const standardArtifact = input.standardPbrArtifact;
|
|
484
1223
|
const sceneIdentity = this.sceneIdentity(scene.scene);
|
|
485
1224
|
const viewInspection = view.inspect();
|
|
486
1225
|
let signature = this.signatureCache?.value;
|
|
@@ -495,10 +1234,12 @@ export class GpuDrivenProduction {
|
|
|
495
1234
|
sceneIdentity,
|
|
496
1235
|
revision: filtered.plan.revision,
|
|
497
1236
|
meshResidencyEpoch: input.meshResidencyEpoch,
|
|
1237
|
+
standardPbrLayoutIdentity: this.standardPbrProjection?.layoutIdentity,
|
|
498
1238
|
viewResourceGeneration: viewInspection.resourceGeneration,
|
|
499
1239
|
batches: filtered.batches.map(({ batch, mesh }) => [
|
|
500
1240
|
batch.batchId,
|
|
501
1241
|
batch.generation,
|
|
1242
|
+
batch.visibleCapacity,
|
|
502
1243
|
batch.visibleBase,
|
|
503
1244
|
batch.candidates.map((candidate) => [
|
|
504
1245
|
candidate.primitiveIndex,
|
|
@@ -512,6 +1253,7 @@ export class GpuDrivenProduction {
|
|
|
512
1253
|
]),
|
|
513
1254
|
capacities: [
|
|
514
1255
|
viewInspection.candidateCapacity,
|
|
1256
|
+
viewInspection.visibleBufferCapacity,
|
|
515
1257
|
viewInspection.batchCapacity,
|
|
516
1258
|
viewInspection.indirectCapacity,
|
|
517
1259
|
],
|
|
@@ -527,11 +1269,165 @@ export class GpuDrivenProduction {
|
|
|
527
1269
|
return ok({
|
|
528
1270
|
topologySignature: signature,
|
|
529
1271
|
entityKeys: filtered.entityKeys,
|
|
530
|
-
ownsAllRenderables:
|
|
1272
|
+
ownsAllRenderables:
|
|
1273
|
+
input.activeEntityKeys === undefined
|
|
1274
|
+
? filtered.entityKeys.size === scene.slots.length
|
|
1275
|
+
: filtered.entityKeys.size === input.activeEntityKeys.size,
|
|
1276
|
+
...(input.occlusion === undefined ? {} : { occlusion: input.occlusion }),
|
|
531
1277
|
_commitResourceReplacement: () => view._commitResourceReplacement(),
|
|
532
1278
|
project: (graph, format, sampleCount) => {
|
|
1279
|
+
this.indirectDrawCount = admittedRasterBatchCount(filtered.plan);
|
|
533
1280
|
const outputs = view.addPasses(graph);
|
|
534
1281
|
if (!outputs.ok) return outputs;
|
|
1282
|
+
if (
|
|
1283
|
+
usesStandardPbrRaster &&
|
|
1284
|
+
standardRaster !== undefined &&
|
|
1285
|
+
standardArtifact !== undefined
|
|
1286
|
+
) {
|
|
1287
|
+
const standardProjected: Array<{
|
|
1288
|
+
readonly batch: GpuDrivenBatch;
|
|
1289
|
+
readonly mesh: MeshGpuHandles;
|
|
1290
|
+
readonly vertex: GraphBuffer;
|
|
1291
|
+
readonly index?: GraphBuffer;
|
|
1292
|
+
readonly pipeline: RenderPipeline;
|
|
1293
|
+
readonly meshBindGroup: BindGroup;
|
|
1294
|
+
}> = [];
|
|
1295
|
+
const standardAccesses: GraphAccess[] = [
|
|
1296
|
+
{ resource: outputs.value.primitive, usage: 'storage-read' },
|
|
1297
|
+
{ resource: outputs.value.instance, usage: 'storage-read' },
|
|
1298
|
+
{ resource: outputs.value.material, usage: 'storage-read' },
|
|
1299
|
+
{ resource: outputs.value.visible, usage: 'storage-read' },
|
|
1300
|
+
{ resource: outputs.value.transform, usage: 'storage-read' },
|
|
1301
|
+
{ resource: outputs.value.indirect, usage: 'indirect-read' },
|
|
1302
|
+
];
|
|
1303
|
+
const slotsByPrimitive = new Map(scene.slots.map((slot) => [slot.slot, slot] as const));
|
|
1304
|
+
const meshBuffers = new Map<
|
|
1305
|
+
number,
|
|
1306
|
+
{ readonly vertex: GraphBuffer; readonly index?: GraphBuffer }
|
|
1307
|
+
>();
|
|
1308
|
+
for (const prepared of filtered.batches) {
|
|
1309
|
+
const candidates = prepared.batch.candidates.flatMap((candidate) => {
|
|
1310
|
+
const slot = slotsByPrimitive.get(candidate.primitiveIndex);
|
|
1311
|
+
return slot === undefined
|
|
1312
|
+
? []
|
|
1313
|
+
: [
|
|
1314
|
+
{
|
|
1315
|
+
primitiveIndex: candidate.primitiveIndex,
|
|
1316
|
+
world: slot.snapshot.transform.world,
|
|
1317
|
+
},
|
|
1318
|
+
];
|
|
1319
|
+
});
|
|
1320
|
+
const projection = projectStandardPbrScene({
|
|
1321
|
+
artifact: standardArtifact,
|
|
1322
|
+
candidates,
|
|
1323
|
+
});
|
|
1324
|
+
if (!projection.ok) {
|
|
1325
|
+
return err(
|
|
1326
|
+
new RhiError({
|
|
1327
|
+
code: 'shader-compile-failed',
|
|
1328
|
+
expected: 'receipt-derived Standard PBR scene projection',
|
|
1329
|
+
hint: 'keep the frame artifact receipt and projected rows on one identity',
|
|
1330
|
+
}),
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
const meshBindGroup = standardRaster.projectionBindGroup(projection.value.rows);
|
|
1334
|
+
if (!meshBindGroup.ok) return meshBindGroup;
|
|
1335
|
+
let buffers = meshBuffers.get(prepared.batch.key.assetHandle);
|
|
1336
|
+
if (buffers === undefined) {
|
|
1337
|
+
const vertex = graph.importBuffer(
|
|
1338
|
+
`gpu-driven.standard.${prepared.batch.key.assetHandle}.vertex`,
|
|
1339
|
+
{
|
|
1340
|
+
size: prepared.mesh.vboBytes,
|
|
1341
|
+
usage: GPU_BUFFER_USAGE_VERTEX | GPU_BUFFER_USAGE_COPY_DST,
|
|
1342
|
+
},
|
|
1343
|
+
() => prepared.mesh.vertexBuffer.handle,
|
|
1344
|
+
);
|
|
1345
|
+
if (!vertex.ok) return vertex;
|
|
1346
|
+
let index: GraphBuffer | undefined;
|
|
1347
|
+
if (prepared.mesh.indexBuffer !== null) {
|
|
1348
|
+
const indexHandle = prepared.mesh.indexBuffer;
|
|
1349
|
+
const imported = graph.importBuffer(
|
|
1350
|
+
`gpu-driven.standard.${prepared.batch.key.assetHandle}.index`,
|
|
1351
|
+
{
|
|
1352
|
+
size: prepared.mesh.iboBytes,
|
|
1353
|
+
usage: GPU_BUFFER_USAGE_INDEX | GPU_BUFFER_USAGE_COPY_DST,
|
|
1354
|
+
},
|
|
1355
|
+
() => indexHandle.handle,
|
|
1356
|
+
);
|
|
1357
|
+
if (!imported.ok) return imported;
|
|
1358
|
+
index = imported.value;
|
|
1359
|
+
}
|
|
1360
|
+
buffers = { vertex: vertex.value, ...(index === undefined ? {} : { index }) };
|
|
1361
|
+
meshBuffers.set(prepared.batch.key.assetHandle, buffers);
|
|
1362
|
+
standardAccesses.push({ resource: buffers.vertex, usage: 'vertex-read' });
|
|
1363
|
+
if (buffers.index !== undefined) {
|
|
1364
|
+
standardAccesses.push({ resource: buffers.index, usage: 'index-read' });
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
const pipeline = standardRaster.pipeline(
|
|
1368
|
+
format,
|
|
1369
|
+
sampleCount,
|
|
1370
|
+
prepared.mesh.layoutProjection,
|
|
1371
|
+
prepared.batch.key.topology,
|
|
1372
|
+
prepared.batch.key.drawKind === 'indexed' &&
|
|
1373
|
+
(prepared.batch.key.topology === 'line-strip' ||
|
|
1374
|
+
prepared.batch.key.topology === 'triangle-strip')
|
|
1375
|
+
? prepared.mesh.indexFormat
|
|
1376
|
+
: undefined,
|
|
1377
|
+
prepared.renderState,
|
|
1378
|
+
);
|
|
1379
|
+
if (!pipeline.ok) return pipeline;
|
|
1380
|
+
standardProjected.push({
|
|
1381
|
+
batch: prepared.batch,
|
|
1382
|
+
mesh: prepared.mesh,
|
|
1383
|
+
...buffers,
|
|
1384
|
+
pipeline: pipeline.value,
|
|
1385
|
+
meshBindGroup: meshBindGroup.value,
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
const [materialBindGroup, instancesBindGroup] = standardRaster.frameGroups;
|
|
1389
|
+
return ok({
|
|
1390
|
+
accesses: standardAccesses,
|
|
1391
|
+
encode: (viewBindGroup, pass, resources: GraphResourceResolver) => {
|
|
1392
|
+
// pbr-view-bgl has two dynamic uniform bindings: the frame view
|
|
1393
|
+
// slot and the optional Points/Lines style slot. Standard
|
|
1394
|
+
// draws do not use the latter, but WebGPU still requires one
|
|
1395
|
+
// offset per dynamic binding.
|
|
1396
|
+
pass.setBindGroup(0, viewBindGroup, [0, 0]);
|
|
1397
|
+
pass.setBindGroup(1, materialBindGroup, [0]);
|
|
1398
|
+
pass.setBindGroup(3, instancesBindGroup);
|
|
1399
|
+
let currentPipeline: RenderPipeline | undefined;
|
|
1400
|
+
let currentVertex: GraphBuffer | undefined;
|
|
1401
|
+
let currentIndex: GraphBuffer | undefined;
|
|
1402
|
+
for (const batch of standardProjected) {
|
|
1403
|
+
if (currentPipeline !== batch.pipeline) {
|
|
1404
|
+
pass.setPipeline(batch.pipeline);
|
|
1405
|
+
currentPipeline = batch.pipeline;
|
|
1406
|
+
}
|
|
1407
|
+
pass.setBindGroup(2, batch.meshBindGroup, [0]);
|
|
1408
|
+
if (currentVertex !== batch.vertex) {
|
|
1409
|
+
const vertex = resources.buffer(batch.vertex);
|
|
1410
|
+
if (!vertex.ok) throw vertex.error;
|
|
1411
|
+
pass.setVertexBuffer(0, vertex.value);
|
|
1412
|
+
currentVertex = batch.vertex;
|
|
1413
|
+
}
|
|
1414
|
+
if (batch.index !== undefined && currentIndex !== batch.index) {
|
|
1415
|
+
const index = resources.buffer(batch.index);
|
|
1416
|
+
if (!index.ok) throw index.error;
|
|
1417
|
+
pass.setIndexBuffer(index.value, batch.mesh.indexFormat);
|
|
1418
|
+
currentIndex = batch.index;
|
|
1419
|
+
}
|
|
1420
|
+
const indirect = resources.buffer(outputs.value.indirect);
|
|
1421
|
+
if (!indirect.ok) throw indirect.error;
|
|
1422
|
+
if (batch.batch.key.drawKind === 'indexed') {
|
|
1423
|
+
pass.drawIndexedIndirect(indirect.value, batch.batch.indirectOffset);
|
|
1424
|
+
} else {
|
|
1425
|
+
pass.drawIndirect(indirect.value, batch.batch.indirectOffset);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
},
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
535
1431
|
const projected: ProjectedBatch[] = [];
|
|
536
1432
|
const accesses: GraphAccess[] = [
|
|
537
1433
|
{ resource: outputs.value.primitive, usage: 'storage-read' },
|
|
@@ -546,6 +1442,10 @@ export class GpuDrivenProduction {
|
|
|
546
1442
|
{ readonly vertex: GraphBuffer; readonly index?: GraphBuffer }
|
|
547
1443
|
>();
|
|
548
1444
|
for (const prepared of filtered.batches) {
|
|
1445
|
+
// A selector-only batch can have zero admitted candidates. Keep it
|
|
1446
|
+
// in the GPU selector and readback counters, but do not create a
|
|
1447
|
+
// raster bind group or encode a zero-instance indirect draw.
|
|
1448
|
+
if (prepared.batch.visibleCapacity === 0) continue;
|
|
549
1449
|
let buffers = meshBuffers.get(prepared.batch.key.assetHandle);
|
|
550
1450
|
if (buffers === undefined) {
|
|
551
1451
|
const vertex = graph.importBuffer(
|
|
@@ -581,7 +1481,7 @@ export class GpuDrivenProduction {
|
|
|
581
1481
|
const pipeline = raster.pipeline(
|
|
582
1482
|
format,
|
|
583
1483
|
sampleCount,
|
|
584
|
-
|
|
1484
|
+
prepared.mesh.layoutProjection.arrayStride,
|
|
585
1485
|
prepared.batch.key.topology,
|
|
586
1486
|
prepared.batch.key.drawKind === 'indexed' &&
|
|
587
1487
|
(prepared.batch.key.topology === 'line-strip' ||
|
|
@@ -601,6 +1501,7 @@ export class GpuDrivenProduction {
|
|
|
601
1501
|
bindings: bindings.value,
|
|
602
1502
|
});
|
|
603
1503
|
}
|
|
1504
|
+
this.indirectDrawCount = projected.length;
|
|
604
1505
|
return ok({
|
|
605
1506
|
accesses,
|
|
606
1507
|
encode: (viewBindGroup, pass, resources: GraphResourceResolver) => {
|
|
@@ -633,11 +1534,14 @@ export class GpuDrivenProduction {
|
|
|
633
1534
|
}
|
|
634
1535
|
const indirect = resources.buffer(outputs.value.indirect);
|
|
635
1536
|
if (!indirect.ok) throw indirect.error;
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
1537
|
+
const draw = (): void => {
|
|
1538
|
+
if (batch.batch.key.drawKind === 'indexed') {
|
|
1539
|
+
pass.drawIndexedIndirect(indirect.value, batch.batch.indirectOffset);
|
|
1540
|
+
} else {
|
|
1541
|
+
pass.drawIndirect(indirect.value, batch.batch.indirectOffset);
|
|
1542
|
+
}
|
|
1543
|
+
};
|
|
1544
|
+
draw();
|
|
641
1545
|
}
|
|
642
1546
|
},
|
|
643
1547
|
});
|
|
@@ -646,11 +1550,19 @@ export class GpuDrivenProduction {
|
|
|
646
1550
|
}
|
|
647
1551
|
|
|
648
1552
|
dispose(): void {
|
|
1553
|
+
if (this.disposed) return;
|
|
1554
|
+
this.disposed = true;
|
|
649
1555
|
this.view?.dispose();
|
|
650
1556
|
this.view = undefined;
|
|
651
1557
|
this.raster = undefined;
|
|
652
1558
|
this.filteredCache = undefined;
|
|
653
1559
|
this.signatureCache = undefined;
|
|
1560
|
+
this.sceneRowsRequired = true;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
/** Whether the current production topology can consume GPU Scene row updates. */
|
|
1564
|
+
requiresSceneRows(): boolean {
|
|
1565
|
+
return this.sceneRowsRequired;
|
|
654
1566
|
}
|
|
655
1567
|
|
|
656
1568
|
inspect(): GpuDrivenProductionInspection {
|
|
@@ -667,17 +1579,112 @@ export class GpuDrivenProduction {
|
|
|
667
1579
|
topologyRevision: view?.topologyRevision,
|
|
668
1580
|
validatedGpuOwnedRows: this.validatedGpuOwnedRows,
|
|
669
1581
|
cpuFallbackDrawItems: this.cpuFallbackDrawItems,
|
|
1582
|
+
geometryWork: this.telemetryGeometryWork,
|
|
1583
|
+
rootGeometryWork: this.telemetryRootGeometryWork,
|
|
1584
|
+
geometryWorkReduction:
|
|
1585
|
+
this.telemetryRootGeometryWork > 0
|
|
1586
|
+
? 1 - this.telemetryGeometryWork / this.telemetryRootGeometryWork
|
|
1587
|
+
: 0,
|
|
1588
|
+
batchCount: view?.batchCount ?? 0,
|
|
1589
|
+
indirectDrawCount: this.indirectDrawCount,
|
|
670
1590
|
};
|
|
671
1591
|
}
|
|
672
1592
|
|
|
1593
|
+
/** Read renderer-owned LOD counters copied by the last GPU submission. */
|
|
1594
|
+
readLodSelection(): Promise<GpuDrivenLodSelectionInspection | undefined> {
|
|
1595
|
+
if (!this.telemetryPrepared) return Promise.resolve(undefined);
|
|
1596
|
+
return (this.view?.readLodSelection() ?? Promise.resolve(undefined)).then((selection) => {
|
|
1597
|
+
if (selection !== undefined) {
|
|
1598
|
+
this.telemetryGeometryWork = selection.geometryWork;
|
|
1599
|
+
this.telemetryRootGeometryWork = selection.rootGeometryWork;
|
|
1600
|
+
}
|
|
1601
|
+
if (selection === undefined || this.telemetryCandidateCount <= 0) return selection;
|
|
1602
|
+
const filtered = this.filteredCache?.value;
|
|
1603
|
+
if (filtered === undefined) return selection;
|
|
1604
|
+
const preparedByBatchId = new Map(
|
|
1605
|
+
filtered.batches.map((prepared) => [prepared.batch.batchId, prepared] as const),
|
|
1606
|
+
);
|
|
1607
|
+
const worldSelections = new Map<
|
|
1608
|
+
number,
|
|
1609
|
+
{
|
|
1610
|
+
readonly worldKey: number;
|
|
1611
|
+
readonly primitiveSlot: number;
|
|
1612
|
+
readonly slotGeneration: number;
|
|
1613
|
+
candidateCount: number;
|
|
1614
|
+
visible: number;
|
|
1615
|
+
occluded: number;
|
|
1616
|
+
readonly lodHistogram: Map<number, number>;
|
|
1617
|
+
}
|
|
1618
|
+
>();
|
|
1619
|
+
for (const batchSelection of selection.batches) {
|
|
1620
|
+
const prepared = preparedByBatchId.get(batchSelection.batchId);
|
|
1621
|
+
if (prepared === undefined) return selection;
|
|
1622
|
+
let aggregate = worldSelections.get(prepared.worldKey);
|
|
1623
|
+
if (aggregate === undefined) {
|
|
1624
|
+
aggregate = {
|
|
1625
|
+
worldKey: prepared.worldKey,
|
|
1626
|
+
primitiveSlot: prepared.primitiveSlot,
|
|
1627
|
+
slotGeneration: prepared.slotGeneration,
|
|
1628
|
+
candidateCount: 0,
|
|
1629
|
+
visible: 0,
|
|
1630
|
+
occluded: 0,
|
|
1631
|
+
lodHistogram: new Map(),
|
|
1632
|
+
};
|
|
1633
|
+
worldSelections.set(prepared.worldKey, aggregate);
|
|
1634
|
+
}
|
|
1635
|
+
aggregate.candidateCount += batchSelection.candidateCount;
|
|
1636
|
+
aggregate.visible += batchSelection.visible;
|
|
1637
|
+
aggregate.occluded += batchSelection.occluded;
|
|
1638
|
+
for (const row of batchSelection.lodHistogram) {
|
|
1639
|
+
aggregate.lodHistogram.set(
|
|
1640
|
+
row.level,
|
|
1641
|
+
(aggregate.lodHistogram.get(row.level) ?? 0) + row.count,
|
|
1642
|
+
);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
return {
|
|
1646
|
+
...selection,
|
|
1647
|
+
candidateCount: this.telemetryCandidateCount,
|
|
1648
|
+
occluded: Math.max(0, this.telemetryCandidateCount - selection.visible),
|
|
1649
|
+
worldSelections: Object.freeze(
|
|
1650
|
+
[...worldSelections.values()].map((aggregate) =>
|
|
1651
|
+
Object.freeze({
|
|
1652
|
+
worldKey: aggregate.worldKey,
|
|
1653
|
+
primitiveSlot: aggregate.primitiveSlot,
|
|
1654
|
+
slotGeneration: aggregate.slotGeneration,
|
|
1655
|
+
candidateCount: aggregate.candidateCount,
|
|
1656
|
+
visible: aggregate.visible,
|
|
1657
|
+
occluded: aggregate.occluded,
|
|
1658
|
+
lodHistogram: Object.freeze(
|
|
1659
|
+
[...aggregate.lodHistogram.entries()]
|
|
1660
|
+
.sort(([left], [right]) => left - right)
|
|
1661
|
+
.map(([level, count]) => Object.freeze({ level, count })),
|
|
1662
|
+
),
|
|
1663
|
+
} satisfies GpuDrivenWorldLodSelectionInspection),
|
|
1664
|
+
),
|
|
1665
|
+
),
|
|
1666
|
+
};
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
|
|
673
1670
|
recordCpuValidation(
|
|
674
1671
|
validated: readonly { readonly source: RenderableSnapshot }[],
|
|
675
1672
|
gpuOwned: ReadonlySet<number>,
|
|
1673
|
+
worldKeys?: readonly number[],
|
|
676
1674
|
): void {
|
|
677
1675
|
this.cpuFallbackDrawItems = validated.length;
|
|
678
1676
|
let gpuOwnedRows = 0;
|
|
679
1677
|
for (const row of validated) {
|
|
680
|
-
if (
|
|
1678
|
+
if (
|
|
1679
|
+
gpuOwned.has(
|
|
1680
|
+
worldEntityKey(
|
|
1681
|
+
worldKeys?.[row.source.worldId] ?? row.source.worldId,
|
|
1682
|
+
row.source.entityKey,
|
|
1683
|
+
),
|
|
1684
|
+
)
|
|
1685
|
+
) {
|
|
1686
|
+
gpuOwnedRows += 1;
|
|
1687
|
+
}
|
|
681
1688
|
}
|
|
682
1689
|
this.validatedGpuOwnedRows = gpuOwnedRows;
|
|
683
1690
|
}
|