@forgeax/engine-render 0.1.21 → 0.1.23
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 +150 -29
- package/dist/assembly/backend-contract.d.ts +13 -0
- package/dist/assembly/backend-contract.d.ts.map +1 -1
- package/dist/assembly/device-feature-admission.d.ts.map +1 -1
- package/dist/assembly/factory.d.ts +14 -6
- 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/authoring.mjs +1 -1
- package/dist/{chunk-PCHBPYML.mjs → chunk-4L3P6DT5.mjs} +2 -2
- package/dist/chunk-4L3P6DT5.mjs.map +1 -0
- package/dist/{chunk-VFPLTKCU.mjs → chunk-4NPUASZR.mjs} +15 -4
- package/dist/chunk-4NPUASZR.mjs.map +1 -0
- package/dist/chunk-DIYW3M5D.mjs +5085 -0
- package/dist/chunk-DIYW3M5D.mjs.map +1 -0
- package/dist/chunk-FBC4R44J.mjs +1003 -0
- package/dist/chunk-FBC4R44J.mjs.map +1 -0
- package/dist/{chunk-FS2KNUQH.mjs → chunk-FF7K3FSO.mjs} +253 -489
- package/dist/chunk-FF7K3FSO.mjs.map +1 -0
- package/dist/chunk-HZUPSYW7.mjs +1104 -0
- package/dist/chunk-HZUPSYW7.mjs.map +1 -0
- package/dist/{chunk-6OPXDQ2E.mjs → chunk-QODA5UKU.mjs} +33 -294
- package/dist/chunk-QODA5UKU.mjs.map +1 -0
- package/dist/chunk-YALPS44A.mjs +296 -0
- package/dist/chunk-YALPS44A.mjs.map +1 -0
- package/dist/{chunk-5YADUCA7.mjs → chunk-ZUWKAZ2I.mjs} +585 -991
- package/dist/chunk-ZUWKAZ2I.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 +10437 -10044
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/device/gpu-residency.d.ts +8 -1
- 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/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 +9 -1
- package/dist/errors/recover.d.ts.map +1 -1
- package/dist/errors/render.d.ts +26 -1
- package/dist/errors/render.d.ts.map +1 -1
- 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/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 +98 -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 +8 -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/index.d.ts +12 -1
- 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 +58 -149
- package/dist/light-buffer-layout.d.ts.map +1 -1
- package/dist/materials.d.ts.map +1 -1
- package/dist/pbr-pipeline.d.ts +26 -12
- 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/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 +50 -10
- package/dist/record/frame-snapshot.d.ts.map +1 -1
- package/dist/record/frame.d.ts +7 -3
- 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 +6 -1
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/main-pass-material.d.ts +3 -1
- 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/mesh-ssbo.d.ts.map +1 -1
- package/dist/record/render-context.d.ts +57 -6
- 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/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 +14 -8
- 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.d.ts +104 -16
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts +12 -1
- 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 +103 -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/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 +13 -1
- package/src/__tests__/direct-light-snapshot.unit.test.ts +59 -3
- package/src/__tests__/environment-generation.integration.test.ts +16 -0
- package/src/__tests__/evidence-identity.ts +13 -0
- package/src/__tests__/evidence-identity.unit.test.ts +24 -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 +33 -0
- 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__/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 +17 -0
- 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__/prepared-gpu-driven-pbr.integration.test.ts +196 -0
- 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__/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 +76 -0
- 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-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 +69 -0
- 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/assembly/backend-contract.ts +13 -0
- package/src/assembly/device-feature-admission.ts +16 -9
- package/src/assembly/factory.ts +547 -93
- package/src/assembly/host-contract.ts +10 -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/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/device/__tests__/extended-lighting-recovery.unit.test.ts +24 -0
- package/src/device/gpu-residency.ts +140 -9
- package/src/device/lifecycle-transaction.ts +12 -0
- package/src/environment/lifecycle.ts +8 -1
- package/src/errors/gpu-driven.ts +92 -0
- package/src/errors/index.ts +1 -0
- package/src/errors/recover.ts +32 -0
- package/src/errors/render.ts +55 -1
- package/src/extract/camera.ts +3 -3
- package/src/extract/gpu-driven.ts +108 -0
- package/src/extract/visibility.ts +0 -23
- 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 +953 -41
- package/src/gpu-driven/view-gpu.ts +411 -29
- package/src/gpu-scene-schema.ts +13 -0
- package/src/gpu-scene.ts +134 -65
- package/src/gpu-usage.ts +0 -1
- package/src/hdrp-buffers.ts +6 -6
- package/src/index.ts +74 -0
- package/src/inspection-types.ts +220 -1
- package/src/internal.ts +38 -0
- package/src/light-buffer-layout.ts +154 -324
- package/src/materials.ts +16 -2
- package/src/pbr-pipeline.ts +151 -45
- 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/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__/gpu-timing.unit.test.ts +177 -0
- package/src/record/__tests__/temporal-payload.unit.test.ts +23 -1
- package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +20 -1
- package/src/record/frame-lighting.ts +252 -53
- package/src/record/frame-snapshot.ts +57 -12
- package/src/record/frame.ts +410 -93
- package/src/record/gpu-timing.ts +108 -44
- package/src/record/helpers.ts +0 -58
- package/src/record/main-pass-geometry.ts +204 -39
- package/src/record/main-pass-material.ts +8 -1
- package/src/record/main-pass-sprite-draws.ts +52 -85
- package/src/record/mesh-ssbo.ts +47 -36
- package/src/record/render-context.ts +65 -4
- package/src/record/shadow-pass.ts +66 -40
- package/src/record/typed-frame-graph.ts +225 -83
- package/src/record/view-ubo.ts +14 -24
- 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 +17 -5
- package/src/render-pipeline.ts +5 -0
- package/src/render-system-extract.ts +493 -134
- package/src/render-system.ts +811 -615
- 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 +848 -183
- 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 +26 -0
- package/src/temporal/frame.ts +14 -1
- package/src/temporal/view.ts +11 -0
- package/src/tilemap-chunk-extract-system.ts +4 -4
- 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.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,165 @@ 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';
|
|
20
23
|
import type { MeshGpuHandles } from '../device/gpu-residency';
|
|
21
24
|
import { GPU_SCENE_WGSL } from '../gpu-scene-schema';
|
|
22
25
|
import {
|
|
23
26
|
GPU_BUFFER_USAGE_COPY_DST,
|
|
24
27
|
GPU_BUFFER_USAGE_INDEX,
|
|
28
|
+
GPU_BUFFER_USAGE_STORAGE,
|
|
29
|
+
GPU_BUFFER_USAGE_UNIFORM,
|
|
25
30
|
GPU_BUFFER_USAGE_VERTEX,
|
|
26
31
|
} from '../gpu-usage';
|
|
32
|
+
import { assembleMaterialWithSkylightEntries } from '../ibl/skylight-bind-group';
|
|
27
33
|
import type { GpuDrivenProductionInspection } from '../inspection-types';
|
|
28
34
|
import type { PipelineBuilderShaderModuleFactory } from '../pipeline-builder';
|
|
29
35
|
import { worldEntityKey } from '../record/frame-snapshot';
|
|
30
36
|
import { geometryRenderStateForTopology } from '../record/main-pass-material';
|
|
37
|
+
import { MESH_SSBO_BYTES } from '../record/mesh-ssbo';
|
|
38
|
+
import type { PipelineState } from '../record/render-context';
|
|
31
39
|
import type { CameraSnapshot } from '../render-contract';
|
|
32
40
|
import type { RenderPipelineFrame, RenderPipelineGpuDrivenProjection } from '../render-pipeline';
|
|
33
41
|
import type { RenderableSnapshot } from '../render-system-extract';
|
|
34
42
|
import type { PersistentGpuDrivenState } from '../scene/render-scene';
|
|
35
|
-
import
|
|
36
|
-
import {
|
|
43
|
+
import { selectLod } from '../scene/visibility/lod-selector';
|
|
44
|
+
import type { OcclusionFrameProjection } from '../scene/visibility/occlusion-runtime';
|
|
45
|
+
import {
|
|
46
|
+
type GpuDrivenBatch,
|
|
47
|
+
type GpuDrivenCandidate,
|
|
48
|
+
projectedHeightForCandidate,
|
|
49
|
+
type SubmissionPlan,
|
|
50
|
+
} from './batch-topology';
|
|
51
|
+
import { type GpuDrivenLodSelectionInspection, GpuDrivenView, selectGpuLodLane } from './view-gpu';
|
|
37
52
|
|
|
38
53
|
const VERTEX_STAGE = 0x1;
|
|
39
54
|
const FRAGMENT_STAGE = 0x2;
|
|
40
55
|
const EMPTY_RESOURCE_CLASS = JSON.stringify({ textures: [], samplers: [], video: [] });
|
|
41
56
|
|
|
57
|
+
export interface StandardPbrSceneCandidate {
|
|
58
|
+
readonly primitiveIndex: number;
|
|
59
|
+
readonly world: Float32Array;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface StandardPbrSceneProjection {
|
|
63
|
+
readonly entryPoint: string;
|
|
64
|
+
readonly layoutIdentity: string;
|
|
65
|
+
readonly receiptGeneration: number;
|
|
66
|
+
/** Packed Mesh rows: mat4 + padded mat3 + previous mat4 + temporal vec4. */
|
|
67
|
+
readonly rows: Float32Array;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Build the producer-owned Standard PBR scene-index projection. The receipt is
|
|
72
|
+
* the only source of the vertex entry and layout identity; callers cannot
|
|
73
|
+
* silently fall back to the legacy GPU-driven shader ABI.
|
|
74
|
+
*/
|
|
75
|
+
export function projectStandardPbrScene(input: {
|
|
76
|
+
readonly artifact: MaterialShaderArtifact;
|
|
77
|
+
readonly candidates: readonly StandardPbrSceneCandidate[];
|
|
78
|
+
}):
|
|
79
|
+
| { readonly ok: true; readonly value: StandardPbrSceneProjection }
|
|
80
|
+
| {
|
|
81
|
+
readonly ok: false;
|
|
82
|
+
readonly error: { readonly code: 'missing-material-receipt' | 'invalid-material-receipt' };
|
|
83
|
+
} {
|
|
84
|
+
const receipt = input.artifact.receipt;
|
|
85
|
+
if (receipt === undefined) return { ok: false, error: { code: 'missing-material-receipt' } };
|
|
86
|
+
if (receipt.reflection.layoutIdentity !== input.artifact.layoutIdentity) {
|
|
87
|
+
return { ok: false, error: { code: 'invalid-material-receipt' } };
|
|
88
|
+
}
|
|
89
|
+
const rows = new Float32Array(input.candidates.length * 48);
|
|
90
|
+
const normal = mat3.create();
|
|
91
|
+
for (let index = 0; index < input.candidates.length; index += 1) {
|
|
92
|
+
const candidate = input.candidates[index];
|
|
93
|
+
if (candidate === undefined) continue;
|
|
94
|
+
const base = index * 48;
|
|
95
|
+
for (let value = 0; value < 16; value += 1) {
|
|
96
|
+
rows[base + value] = candidate.world[value] ?? 0;
|
|
97
|
+
rows[base + 28 + value] = candidate.world[value] ?? 0;
|
|
98
|
+
}
|
|
99
|
+
const normalMatrix = mat3.normalMatrix(normal, candidate.world);
|
|
100
|
+
for (let column = 0; column < 3; column += 1) {
|
|
101
|
+
const source = column * 3;
|
|
102
|
+
const target = base + 16 + column * 4;
|
|
103
|
+
rows[target] = normalMatrix[source] ?? 0;
|
|
104
|
+
rows[target + 1] = normalMatrix[source + 1] ?? 0;
|
|
105
|
+
rows[target + 2] = normalMatrix[source + 2] ?? 0;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
ok: true,
|
|
110
|
+
value: {
|
|
111
|
+
entryPoint: receipt.sceneIndexEntry,
|
|
112
|
+
layoutIdentity: input.artifact.layoutIdentity,
|
|
113
|
+
receiptGeneration: receipt.generation,
|
|
114
|
+
rows,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface StandardPbrFrameResourceBinding {
|
|
120
|
+
readonly entryPoint: string;
|
|
121
|
+
readonly layoutIdentity: string;
|
|
122
|
+
readonly receiptGeneration: number;
|
|
123
|
+
readonly bindGroups: readonly [BindGroup, BindGroup, BindGroup, BindGroup];
|
|
124
|
+
readonly resourceSlots: NonNullable<MaterialShaderArtifact['receipt']>['resourceSlots'];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Adapt the frame's already-created Standard PBR groups to the GPU-driven
|
|
129
|
+
* scene-index draw. This deliberately accepts group handles rather than
|
|
130
|
+
* rebuilding layouts or shader artifacts; the receipt remains the binding
|
|
131
|
+
* contract and the regular frame producers remain the resource owners.
|
|
132
|
+
*/
|
|
133
|
+
export function adaptStandardPbrFrameResources(input: {
|
|
134
|
+
readonly artifact: MaterialShaderArtifact;
|
|
135
|
+
readonly view: BindGroup;
|
|
136
|
+
readonly material: BindGroup;
|
|
137
|
+
readonly mesh: BindGroup;
|
|
138
|
+
readonly instances: BindGroup;
|
|
139
|
+
}):
|
|
140
|
+
| { readonly ok: true; readonly value: StandardPbrFrameResourceBinding }
|
|
141
|
+
| {
|
|
142
|
+
readonly ok: false;
|
|
143
|
+
readonly error: { readonly code: 'missing-material-receipt' | 'invalid-material-receipt' };
|
|
144
|
+
} {
|
|
145
|
+
const receipt = input.artifact.receipt;
|
|
146
|
+
if (receipt === undefined) return { ok: false, error: { code: 'missing-material-receipt' } };
|
|
147
|
+
const reflected = receipt.reflection;
|
|
148
|
+
const sameSlots =
|
|
149
|
+
reflected.resourceSlots.length === receipt.resourceSlots.length &&
|
|
150
|
+
reflected.resourceSlots.every((slot, index) => {
|
|
151
|
+
const expected = receipt.resourceSlots[index];
|
|
152
|
+
return (
|
|
153
|
+
expected !== undefined &&
|
|
154
|
+
slot.name === expected.name &&
|
|
155
|
+
slot.parameter === expected.parameter &&
|
|
156
|
+
slot.kind === expected.kind &&
|
|
157
|
+
slot.group === expected.group &&
|
|
158
|
+
slot.binding === expected.binding
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
if (
|
|
162
|
+
reflected.layoutIdentity !== input.artifact.layoutIdentity ||
|
|
163
|
+
!sameSlots ||
|
|
164
|
+
receipt.resourceSlots.some((slot) => slot.group !== 1)
|
|
165
|
+
) {
|
|
166
|
+
return { ok: false, error: { code: 'invalid-material-receipt' } };
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
ok: true,
|
|
170
|
+
value: {
|
|
171
|
+
entryPoint: receipt.sceneIndexEntry,
|
|
172
|
+
layoutIdentity: input.artifact.layoutIdentity,
|
|
173
|
+
receiptGeneration: receipt.generation,
|
|
174
|
+
bindGroups: [input.view, input.material, input.mesh, input.instances],
|
|
175
|
+
resourceSlots: receipt.resourceSlots,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
42
180
|
export const GPU_DRIVEN_RIGID_UNLIT_WGSL = /* wgsl */ `
|
|
43
181
|
${GPU_SCENE_WGSL}
|
|
44
182
|
|
|
@@ -87,6 +225,11 @@ interface PreparedBatch {
|
|
|
87
225
|
readonly batch: GpuDrivenBatch;
|
|
88
226
|
readonly mesh: MeshGpuHandles;
|
|
89
227
|
readonly renderState: MaterialRenderState | undefined;
|
|
228
|
+
/** Stable World attachment key used to join same-submit GPU counters. */
|
|
229
|
+
readonly worldKey: number;
|
|
230
|
+
/** Stable representative slot identity for this homogeneous batch. */
|
|
231
|
+
readonly primitiveSlot: number;
|
|
232
|
+
readonly slotGeneration: number;
|
|
90
233
|
}
|
|
91
234
|
|
|
92
235
|
interface ProjectedBatch extends PreparedBatch {
|
|
@@ -133,6 +276,22 @@ function activeFrustum(camera: CameraSnapshot): Float32Array {
|
|
|
133
276
|
);
|
|
134
277
|
}
|
|
135
278
|
|
|
279
|
+
/**
|
|
280
|
+
* LOD projected height is a view fact, not a scene-topology fact. Keep the
|
|
281
|
+
* filtered plan cache scene-stable while still rebuilding its candidate
|
|
282
|
+
* heights whenever a camera input used by the selector changes.
|
|
283
|
+
*/
|
|
284
|
+
function lodProjectionFingerprint(camera: CameraSnapshot): string {
|
|
285
|
+
return JSON.stringify({
|
|
286
|
+
position: Array.from(camera.position),
|
|
287
|
+
world: Array.from(camera.world),
|
|
288
|
+
projection: camera.projection,
|
|
289
|
+
fov: camera.fov,
|
|
290
|
+
aspect: camera.aspect,
|
|
291
|
+
orthoHeight: camera.orthoTop - camera.orthoBottom,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
136
295
|
function productionEligible(snapshot: RenderableSnapshot, batch: GpuDrivenBatch): boolean {
|
|
137
296
|
const material = snapshot.materials[batch.key.materialSlot] ?? snapshot.material;
|
|
138
297
|
const shader = material.materialShaderId ?? 'forgeax::default-unlit';
|
|
@@ -144,44 +303,142 @@ function productionEligible(snapshot: RenderableSnapshot, batch: GpuDrivenBatch)
|
|
|
144
303
|
);
|
|
145
304
|
}
|
|
146
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Prepared Standard PBR draws have an explicit receipt identity on their
|
|
308
|
+
* batch key. Unprepared batches stay on the rigid production shader; the
|
|
309
|
+
* presence of a frame-wide PBR artifact is not permission to reinterpret an
|
|
310
|
+
* unlit batch with the PBR bind-group layout.
|
|
311
|
+
*/
|
|
312
|
+
function isStandardPbrBatch(batch: GpuDrivenBatch): boolean {
|
|
313
|
+
return batch.key.preparedIdentity !== undefined;
|
|
314
|
+
}
|
|
315
|
+
|
|
147
316
|
function meshSupportsBatch(mesh: MeshGpuHandles, batch: GpuDrivenBatch): boolean {
|
|
148
|
-
if (
|
|
317
|
+
if (
|
|
318
|
+
batch.candidates.some((candidate) => {
|
|
319
|
+
const lodCount = candidate.lodCoverages?.length ?? 0;
|
|
320
|
+
return (
|
|
321
|
+
lodCount > 1 &&
|
|
322
|
+
(mesh.lodRanges === undefined ||
|
|
323
|
+
mesh.lodRanges.length < lodCount ||
|
|
324
|
+
mesh.lodRanges.some((ranges) => ranges[candidate.drawItemIndex] === undefined))
|
|
325
|
+
);
|
|
326
|
+
})
|
|
327
|
+
) {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
149
330
|
return batch.key.drawKind === 'indexed'
|
|
150
331
|
? mesh.indexed && mesh.indexBuffer !== null
|
|
151
332
|
: !mesh.indexed;
|
|
152
333
|
}
|
|
153
334
|
|
|
335
|
+
function selectedLodLevel(candidate: GpuDrivenCandidate): number {
|
|
336
|
+
const coverages = candidate.lodCoverages;
|
|
337
|
+
if (coverages === undefined || coverages.length <= 1) return 0;
|
|
338
|
+
return selectLod({
|
|
339
|
+
levels: coverages.slice(1).map((screenCoverage) => ({ screenCoverage })),
|
|
340
|
+
projectedHeight: candidate.projectedHeight ?? Number.NaN,
|
|
341
|
+
previousLevel: 0,
|
|
342
|
+
hysteresis: candidate.lodHysteresis ?? 0.08,
|
|
343
|
+
ready: coverages.map(() => true),
|
|
344
|
+
historyValid: false,
|
|
345
|
+
}).level;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function selectedLodRange(
|
|
349
|
+
batch: GpuDrivenBatch,
|
|
350
|
+
candidate: GpuDrivenCandidate,
|
|
351
|
+
): { readonly first: number; readonly count: number; readonly baseVertex: number } {
|
|
352
|
+
const level = selectedLodLevel(candidate);
|
|
353
|
+
return (
|
|
354
|
+
(level === 0 ? undefined : candidate.lodRanges?.[level - 1]) ?? {
|
|
355
|
+
first: batch.key.first,
|
|
356
|
+
count: batch.key.count,
|
|
357
|
+
baseVertex: batch.key.baseVertex,
|
|
358
|
+
}
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* One raster indirect command is emitted for each selector batch that has at
|
|
364
|
+
* least one admitted candidate. Selector-only batches remain in the compute
|
|
365
|
+
* and readback plan, but they must not inflate the raster command count.
|
|
366
|
+
*/
|
|
367
|
+
function admittedRasterBatchCount(plan: SubmissionPlan): number {
|
|
368
|
+
return plan.batches.reduce((count, batch) => count + (batch.visibleCapacity > 0 ? 1 : 0), 0);
|
|
369
|
+
}
|
|
370
|
+
|
|
154
371
|
function filteredPlan(
|
|
155
372
|
source: SubmissionPlan,
|
|
156
373
|
slots: PersistentGpuDrivenState['slots'],
|
|
157
374
|
meshes: ReadonlyMap<number, MeshGpuHandles>,
|
|
375
|
+
camera: CameraSnapshot,
|
|
376
|
+
activeEntityKeys?: ReadonlySet<number>,
|
|
377
|
+
worldKeys?: readonly number[],
|
|
158
378
|
): {
|
|
159
379
|
readonly plan: SubmissionPlan;
|
|
160
380
|
readonly batches: readonly PreparedBatch[];
|
|
161
381
|
readonly entityKeys: ReadonlySet<number>;
|
|
382
|
+
/** No source primitive can enter this lane without a topology/material change. */
|
|
383
|
+
readonly topologyInvariantEmpty: boolean;
|
|
162
384
|
} {
|
|
163
385
|
const slotByPrimitive = new Map(slots.map((slot) => [slot.slot, slot] as const));
|
|
386
|
+
const participatingByPrimitive = new Map<number, Set<GpuDrivenBatch>>();
|
|
387
|
+
for (const batch of source.batches) {
|
|
388
|
+
for (const candidate of batch.candidates) {
|
|
389
|
+
const participating = participatingByPrimitive.get(candidate.primitiveIndex);
|
|
390
|
+
if (participating === undefined) {
|
|
391
|
+
participatingByPrimitive.set(candidate.primitiveIndex, new Set([batch]));
|
|
392
|
+
} else {
|
|
393
|
+
participating.add(batch);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
164
397
|
const entityKeys = new Set<number>();
|
|
398
|
+
const worldKeyFor = (slot: PersistentGpuDrivenState['slots'][number]): number =>
|
|
399
|
+
worldKeys?.[slot.snapshot.worldId] ?? slot.snapshot.worldId;
|
|
400
|
+
const activeKey = (slot: PersistentGpuDrivenState['slots'][number]): number =>
|
|
401
|
+
worldEntityKey(worldKeyFor(slot), slot.snapshot.entityKey);
|
|
165
402
|
const eligiblePrimitives = new Set<number>();
|
|
403
|
+
let topologyEligiblePrimitiveCount = 0;
|
|
166
404
|
for (const slot of slots) {
|
|
167
405
|
const mesh = meshes.get(slot.snapshot.assetHandle);
|
|
168
|
-
const participating =
|
|
169
|
-
|
|
170
|
-
|
|
406
|
+
const participating = participatingByPrimitive.get(slot.slot);
|
|
407
|
+
const participatingBatches = participating === undefined ? [] : [...participating];
|
|
408
|
+
const topologyEligible =
|
|
409
|
+
participatingBatches.length > 0 &&
|
|
410
|
+
participatingBatches.length === (slot.snapshot.gpuDrivenDraws?.length ?? 0) &&
|
|
411
|
+
participatingBatches.every((batch) => productionEligible(slot.snapshot, batch));
|
|
412
|
+
if (topologyEligible) topologyEligiblePrimitiveCount += 1;
|
|
171
413
|
if (
|
|
172
414
|
mesh !== undefined &&
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
(batch) => productionEligible(slot.snapshot, batch) && meshSupportsBatch(mesh, batch),
|
|
176
|
-
)
|
|
415
|
+
topologyEligible &&
|
|
416
|
+
participatingBatches.every((batch) => meshSupportsBatch(mesh, batch))
|
|
177
417
|
) {
|
|
178
418
|
eligiblePrimitives.add(slot.slot);
|
|
179
|
-
|
|
419
|
+
// Record validation still keys extracted renderables by their current
|
|
420
|
+
// stable world/entity key. Suppressed candidates remain in the GPU
|
|
421
|
+
// selector, but only admitted entities are owned by the compacted draw.
|
|
422
|
+
if (activeEntityKeys === undefined || activeEntityKeys.has(activeKey(slot))) {
|
|
423
|
+
entityKeys.add(activeKey(slot));
|
|
424
|
+
}
|
|
180
425
|
}
|
|
181
426
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
427
|
+
// BatchTopology deliberately keeps LOD candidates individually addressable
|
|
428
|
+
// so topology updates never alias a candidate's selector state. Once the
|
|
429
|
+
// camera has projected the candidates, the renderer can safely coalesce
|
|
430
|
+
// candidates that have identical immutable geometry/LOD facts and the same
|
|
431
|
+
// selected range. The GPU selector uses the same no-history inputs below,
|
|
432
|
+
// so one indirect range remains correct for every candidate in a group.
|
|
433
|
+
const groups = new Map<
|
|
434
|
+
string,
|
|
435
|
+
{
|
|
436
|
+
readonly source: GpuDrivenBatch;
|
|
437
|
+
readonly mesh: MeshGpuHandles;
|
|
438
|
+
readonly renderState: MaterialRenderState | undefined;
|
|
439
|
+
readonly candidates: GpuDrivenCandidate[];
|
|
440
|
+
}
|
|
441
|
+
>();
|
|
185
442
|
for (const batch of source.batches) {
|
|
186
443
|
const candidates = batch.candidates.filter((candidate) => {
|
|
187
444
|
const slot = slotByPrimitive.get(candidate.primitiveIndex);
|
|
@@ -190,24 +447,84 @@ function filteredPlan(
|
|
|
190
447
|
return mesh !== undefined && meshSupportsBatch(mesh, batch);
|
|
191
448
|
});
|
|
192
449
|
if (candidates.length === 0) continue;
|
|
450
|
+
for (const candidate of candidates) {
|
|
451
|
+
const slot = slotByPrimitive.get(candidate.primitiveIndex);
|
|
452
|
+
if (slot === undefined) continue;
|
|
453
|
+
const mesh = meshes.get(slot.snapshot.assetHandle);
|
|
454
|
+
if (mesh === undefined) continue;
|
|
455
|
+
const physicalLodRanges = mesh.lodRanges
|
|
456
|
+
?.slice(1)
|
|
457
|
+
.map((ranges) => ranges[candidate.drawItemIndex]);
|
|
458
|
+
const projected = Object.freeze({
|
|
459
|
+
...candidate,
|
|
460
|
+
submitAdmission: activeEntityKeys === undefined || activeEntityKeys.has(activeKey(slot)),
|
|
461
|
+
projectedHeight: projectedHeightForCandidate(slot, camera),
|
|
462
|
+
...(physicalLodRanges?.every((range) => range !== undefined)
|
|
463
|
+
? { lodRanges: physicalLodRanges as NonNullable<typeof candidate.lodRanges> }
|
|
464
|
+
: {}),
|
|
465
|
+
});
|
|
466
|
+
const selectedRange = selectedLodRange(batch, projected);
|
|
467
|
+
const groupKey = JSON.stringify({
|
|
468
|
+
key: batch.key,
|
|
469
|
+
worldKey: worldKeyFor(slot),
|
|
470
|
+
lodCoverages: projected.lodCoverages ?? null,
|
|
471
|
+
lodRanges: projected.lodRanges ?? null,
|
|
472
|
+
selectedRange,
|
|
473
|
+
selectedLevel: selectedLodLevel(projected),
|
|
474
|
+
});
|
|
475
|
+
const existing = groups.get(groupKey);
|
|
476
|
+
if (existing === undefined) {
|
|
477
|
+
const material = slot.snapshot.materials[batch.key.materialSlot] ?? slot.snapshot.material;
|
|
478
|
+
groups.set(groupKey, {
|
|
479
|
+
source: batch,
|
|
480
|
+
mesh,
|
|
481
|
+
renderState: geometryRenderStateForTopology(batch.key.topology, material.renderState),
|
|
482
|
+
candidates: [projected],
|
|
483
|
+
});
|
|
484
|
+
} else {
|
|
485
|
+
existing.candidates.push(projected);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const batches: PreparedBatch[] = [];
|
|
491
|
+
let visibleBase = 0;
|
|
492
|
+
const filteredBatches: GpuDrivenBatch[] = [];
|
|
493
|
+
let batchId = 0;
|
|
494
|
+
for (const group of groups.values()) {
|
|
193
495
|
visibleBase = Math.ceil(visibleBase / 64) * 64;
|
|
496
|
+
const admittedCount = group.candidates.reduce(
|
|
497
|
+
(count, candidate) =>
|
|
498
|
+
count +
|
|
499
|
+
((candidate as GpuDrivenCandidate & { readonly submitAdmission?: boolean })
|
|
500
|
+
.submitAdmission === false
|
|
501
|
+
? 0
|
|
502
|
+
: 1),
|
|
503
|
+
0,
|
|
504
|
+
);
|
|
194
505
|
const filtered: GpuDrivenBatch = Object.freeze({
|
|
195
|
-
...
|
|
196
|
-
|
|
506
|
+
...group.source,
|
|
507
|
+
batchId,
|
|
508
|
+
candidates: Object.freeze(group.candidates),
|
|
197
509
|
visibleBase,
|
|
198
|
-
visibleCapacity:
|
|
510
|
+
visibleCapacity: admittedCount,
|
|
511
|
+
indirectOffset: batchId * 20,
|
|
199
512
|
});
|
|
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;
|
|
513
|
+
visibleBase += admittedCount;
|
|
204
514
|
filteredBatches.push(filtered);
|
|
205
|
-
const
|
|
515
|
+
const firstCandidate = group.candidates[0];
|
|
516
|
+
const firstSlot =
|
|
517
|
+
firstCandidate === undefined ? undefined : slotByPrimitive.get(firstCandidate.primitiveIndex);
|
|
518
|
+
if (firstCandidate === undefined || firstSlot === undefined) continue;
|
|
206
519
|
batches.push({
|
|
207
520
|
batch: filtered,
|
|
208
|
-
mesh,
|
|
209
|
-
renderState:
|
|
521
|
+
mesh: group.mesh,
|
|
522
|
+
renderState: group.renderState,
|
|
523
|
+
worldKey: worldKeyFor(firstSlot),
|
|
524
|
+
primitiveSlot: firstSlot.slot,
|
|
525
|
+
slotGeneration: firstSlot.generation,
|
|
210
526
|
});
|
|
527
|
+
batchId += 1;
|
|
211
528
|
}
|
|
212
529
|
return {
|
|
213
530
|
plan: Object.freeze({
|
|
@@ -218,11 +535,32 @@ function filteredPlan(
|
|
|
218
535
|
}),
|
|
219
536
|
batches: Object.freeze(batches),
|
|
220
537
|
entityKeys,
|
|
538
|
+
topologyInvariantEmpty: topologyEligiblePrimitiveCount === 0,
|
|
221
539
|
};
|
|
222
540
|
}
|
|
223
541
|
|
|
224
542
|
type FilteredProductionPlan = ReturnType<typeof filteredPlan>;
|
|
225
543
|
|
|
544
|
+
export interface GpuDrivenWorldLodSelectionInspection {
|
|
545
|
+
readonly worldKey: number;
|
|
546
|
+
readonly primitiveSlot: number;
|
|
547
|
+
readonly slotGeneration: number;
|
|
548
|
+
readonly candidateCount: number;
|
|
549
|
+
readonly visible: number;
|
|
550
|
+
readonly occluded: number;
|
|
551
|
+
readonly lodHistogram: readonly { readonly level: number; readonly count: number }[];
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function sameEntityKeySet(
|
|
555
|
+
left: ReadonlySet<number> | undefined,
|
|
556
|
+
right: ReadonlySet<number> | undefined,
|
|
557
|
+
): boolean {
|
|
558
|
+
if (left === right) return true;
|
|
559
|
+
if (left === undefined || right === undefined || left.size !== right.size) return false;
|
|
560
|
+
for (const key of left) if (!right.has(key)) return false;
|
|
561
|
+
return true;
|
|
562
|
+
}
|
|
563
|
+
|
|
226
564
|
class GpuDrivenRigidRaster {
|
|
227
565
|
private readonly pipelines = new Map<string, RenderPipeline>();
|
|
228
566
|
|
|
@@ -375,10 +713,212 @@ class GpuDrivenRigidRaster {
|
|
|
375
713
|
}
|
|
376
714
|
}
|
|
377
715
|
|
|
716
|
+
/** The GPU-driven bridge to the existing four-group Standard PBR pipeline. */
|
|
717
|
+
class StandardPbrRasterAdapter {
|
|
718
|
+
private readonly pipelines = new Map<string, RenderPipeline>();
|
|
719
|
+
private readonly projectionBuffers: Buffer[] = [];
|
|
720
|
+
|
|
721
|
+
private constructor(
|
|
722
|
+
private readonly device: RhiDevice,
|
|
723
|
+
private readonly pipelineLayout: import('@forgeax/engine-rhi').PipelineLayout,
|
|
724
|
+
private readonly meshLayout: BindGroupLayout,
|
|
725
|
+
private readonly module: ShaderModule,
|
|
726
|
+
private readonly materialBindGroup: BindGroup,
|
|
727
|
+
private readonly instancesBindGroup: BindGroup,
|
|
728
|
+
private readonly artifact: MaterialShaderArtifact,
|
|
729
|
+
) {}
|
|
730
|
+
|
|
731
|
+
static create(input: {
|
|
732
|
+
readonly device: RhiDevice;
|
|
733
|
+
readonly shaderModuleFactory: PipelineBuilderShaderModuleFactory;
|
|
734
|
+
readonly pipelineState: PipelineState;
|
|
735
|
+
readonly artifact: MaterialShaderArtifact;
|
|
736
|
+
}): Result<StandardPbrRasterAdapter, RhiError> {
|
|
737
|
+
const pipelineLayout = input.pipelineState.pbrPipelineLayout;
|
|
738
|
+
const fallback = input.pipelineState.skylightFallback;
|
|
739
|
+
if (pipelineLayout === null || fallback === null) {
|
|
740
|
+
return err(
|
|
741
|
+
new RhiError({
|
|
742
|
+
code: 'rhi-not-available',
|
|
743
|
+
expected: 'Standard PBR pipeline layout and Skylight fallback',
|
|
744
|
+
hint: 'initialize the Standard PBR pipeline before enabling GPU-driven raster',
|
|
745
|
+
}),
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
const module = input.shaderModuleFactory.createShaderModule({
|
|
749
|
+
label: 'standard-pbr-gpu-driven-scene-index',
|
|
750
|
+
code: input.artifact.wgsl,
|
|
751
|
+
});
|
|
752
|
+
if (!module.ok) return module;
|
|
753
|
+
const materialBuffer = input.device.createBuffer({
|
|
754
|
+
label: 'standard-pbr-gpu-driven-material-row',
|
|
755
|
+
size: 384,
|
|
756
|
+
usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
|
|
757
|
+
});
|
|
758
|
+
if (!materialBuffer.ok) return materialBuffer;
|
|
759
|
+
const materialPayload = new Float32Array(96);
|
|
760
|
+
materialPayload[0] = 1;
|
|
761
|
+
materialPayload[1] = 1;
|
|
762
|
+
materialPayload[2] = 1;
|
|
763
|
+
materialPayload[3] = 1;
|
|
764
|
+
materialPayload[5] = 1;
|
|
765
|
+
materialPayload[6] = 1;
|
|
766
|
+
const materialWrite = input.device.queue.writeBuffer(materialBuffer.value, 0, materialPayload);
|
|
767
|
+
if (!materialWrite.ok) return materialWrite;
|
|
768
|
+
const baseEntries = [
|
|
769
|
+
{ binding: 0, resource: bufferBinding(materialBuffer.value, 0, 384) },
|
|
770
|
+
...Array.from({ length: 8 }, (_, index) => [
|
|
771
|
+
{
|
|
772
|
+
binding: index * 2 + 1,
|
|
773
|
+
resource: { kind: 'sampler' as const, value: fallback.sampler },
|
|
774
|
+
},
|
|
775
|
+
{
|
|
776
|
+
binding: index * 2 + 2,
|
|
777
|
+
resource: {
|
|
778
|
+
kind: 'textureView' as const,
|
|
779
|
+
value:
|
|
780
|
+
index === 2
|
|
781
|
+
? input.pipelineState.defaultNormalTextureView
|
|
782
|
+
: input.pipelineState.defaultWhiteTextureView,
|
|
783
|
+
},
|
|
784
|
+
},
|
|
785
|
+
]).flat(),
|
|
786
|
+
];
|
|
787
|
+
const materialEntries = assembleMaterialWithSkylightEntries(
|
|
788
|
+
baseEntries,
|
|
789
|
+
{
|
|
790
|
+
irradianceView: fallback.irradianceView,
|
|
791
|
+
irradianceSampler: fallback.sampler,
|
|
792
|
+
prefilterView: fallback.prefilterView,
|
|
793
|
+
prefilterSampler: fallback.sampler,
|
|
794
|
+
brdfLutView: fallback.brdfLutView,
|
|
795
|
+
brdfLutSampler: fallback.sampler,
|
|
796
|
+
intensityBuffer: fallback.intensityBuffer,
|
|
797
|
+
},
|
|
798
|
+
{ sampler: fallback.sampler, backdropView: input.pipelineState.defaultWhiteTextureView },
|
|
799
|
+
);
|
|
800
|
+
const materialBindGroup = input.device.createBindGroup({
|
|
801
|
+
label: 'standard-pbr-gpu-driven-material-bg',
|
|
802
|
+
layout: input.pipelineState.materialBindGroupLayout,
|
|
803
|
+
entries: materialEntries,
|
|
804
|
+
});
|
|
805
|
+
if (!materialBindGroup.ok) return materialBindGroup;
|
|
806
|
+
const instancesBindGroup = input.device.createBindGroup({
|
|
807
|
+
label: 'standard-pbr-gpu-driven-identity-instances-bg',
|
|
808
|
+
layout: input.pipelineState.instancesBindGroupLayout,
|
|
809
|
+
entries: [
|
|
810
|
+
{
|
|
811
|
+
binding: 0,
|
|
812
|
+
resource: bufferBinding(input.pipelineState.identityInstanceBuffer),
|
|
813
|
+
},
|
|
814
|
+
],
|
|
815
|
+
});
|
|
816
|
+
if (!instancesBindGroup.ok) return instancesBindGroup;
|
|
817
|
+
return ok(
|
|
818
|
+
new StandardPbrRasterAdapter(
|
|
819
|
+
input.device,
|
|
820
|
+
pipelineLayout,
|
|
821
|
+
input.pipelineState.meshBindGroupLayout,
|
|
822
|
+
module.value,
|
|
823
|
+
materialBindGroup.value,
|
|
824
|
+
instancesBindGroup.value,
|
|
825
|
+
input.artifact,
|
|
826
|
+
),
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
pipeline(
|
|
831
|
+
format: TextureFormat,
|
|
832
|
+
sampleCount: 1 | 4,
|
|
833
|
+
layoutProjection: MeshGpuHandles['layoutProjection'],
|
|
834
|
+
topology: GpuDrivenBatch['key']['topology'],
|
|
835
|
+
stripIndexFormat: 'uint16' | 'uint32' | undefined,
|
|
836
|
+
renderState: MaterialRenderState | undefined,
|
|
837
|
+
): Result<RenderPipeline, RhiError> {
|
|
838
|
+
const projectedLayout = deriveVertexBufferLayoutFromProjection(
|
|
839
|
+
layoutProjection,
|
|
840
|
+
this.artifact.uvSetCount === undefined
|
|
841
|
+
? undefined
|
|
842
|
+
: { shaderUvSetCount: this.artifact.uvSetCount },
|
|
843
|
+
)[0];
|
|
844
|
+
const vertexStride = projectedLayout?.arrayStride ?? layoutProjection.arrayStride;
|
|
845
|
+
const attributes = projectedLayout?.attributes ?? layoutProjection.attributes;
|
|
846
|
+
const key = `${format}|${sampleCount}|${vertexStride}|${topology}|${stripIndexFormat ?? ''}|${JSON.stringify(renderState ?? null)}|${this.artifact.layoutIdentity}`;
|
|
847
|
+
const cached = this.pipelines.get(key);
|
|
848
|
+
if (cached !== undefined) return ok(cached);
|
|
849
|
+
const pipeline = this.device.createRenderPipeline({
|
|
850
|
+
label: `standard-pbr-gpu-driven.${key}`,
|
|
851
|
+
layout: this.pipelineLayout,
|
|
852
|
+
vertex: {
|
|
853
|
+
module: this.module,
|
|
854
|
+
entryPoint: this.artifact.receipt?.sceneIndexEntry ?? 'vs_scene_index',
|
|
855
|
+
buffers: [
|
|
856
|
+
{
|
|
857
|
+
arrayStride: vertexStride,
|
|
858
|
+
stepMode: 'vertex',
|
|
859
|
+
attributes: attributes.map(({ shaderLocation, offset, format }) => ({
|
|
860
|
+
shaderLocation,
|
|
861
|
+
offset,
|
|
862
|
+
format: format as GPUVertexFormat,
|
|
863
|
+
})),
|
|
864
|
+
},
|
|
865
|
+
],
|
|
866
|
+
},
|
|
867
|
+
fragment: {
|
|
868
|
+
module: this.module,
|
|
869
|
+
entryPoint: 'fs_main',
|
|
870
|
+
targets: [{ format }],
|
|
871
|
+
},
|
|
872
|
+
primitive: {
|
|
873
|
+
topology,
|
|
874
|
+
cullMode: renderState?.cullMode ?? 'back',
|
|
875
|
+
frontFace: renderState?.frontFace ?? 'ccw',
|
|
876
|
+
...(stripIndexFormat === undefined ? {} : { stripIndexFormat }),
|
|
877
|
+
},
|
|
878
|
+
depthStencil: {
|
|
879
|
+
format: 'depth24plus-stencil8',
|
|
880
|
+
depthWriteEnabled: renderState?.depthWriteEnabled ?? true,
|
|
881
|
+
depthCompare: renderState?.depthCompare ?? 'less',
|
|
882
|
+
},
|
|
883
|
+
...(sampleCount === 4 ? { multisample: { count: 4 } } : {}),
|
|
884
|
+
});
|
|
885
|
+
if (!pipeline.ok) return pipeline;
|
|
886
|
+
this.pipelines.set(key, pipeline.value);
|
|
887
|
+
return pipeline;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
projectionBindGroup(rows: Float32Array): Result<BindGroup, RhiError> {
|
|
891
|
+
const buffer = this.device.createBuffer({
|
|
892
|
+
label: 'standard-pbr-gpu-driven-scene-meshes',
|
|
893
|
+
size: Math.max(MESH_SSBO_BYTES, rows.byteLength),
|
|
894
|
+
usage: GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST,
|
|
895
|
+
});
|
|
896
|
+
if (!buffer.ok) return buffer;
|
|
897
|
+
const write = this.device.queue.writeBuffer(buffer.value, 0, rows);
|
|
898
|
+
if (!write.ok) return write;
|
|
899
|
+
this.projectionBuffers.push(buffer.value);
|
|
900
|
+
return this.device.createBindGroup({
|
|
901
|
+
label: 'standard-pbr-gpu-driven-scene-mesh-bg',
|
|
902
|
+
layout: this.meshLayout,
|
|
903
|
+
entries: [{ binding: 0, resource: bufferBinding(buffer.value, 0, rows.byteLength) }],
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
dispose(): void {
|
|
908
|
+
for (const buffer of this.projectionBuffers) this.device.destroyBuffer(buffer);
|
|
909
|
+
this.projectionBuffers.length = 0;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
get frameGroups(): readonly [BindGroup, BindGroup] {
|
|
913
|
+
return [this.materialBindGroup, this.instancesBindGroup];
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
378
917
|
export interface PreparedGpuDrivenFrame {
|
|
379
918
|
readonly topologySignature: string;
|
|
380
919
|
readonly entityKeys: ReadonlySet<number>;
|
|
381
920
|
readonly ownsAllRenderables: boolean;
|
|
921
|
+
readonly occlusion?: OcclusionFrameProjection;
|
|
382
922
|
/** @internal Commits a replacement generation only after graph promotion. */
|
|
383
923
|
readonly _commitResourceReplacement: () => void;
|
|
384
924
|
project(
|
|
@@ -391,12 +931,16 @@ export interface PreparedGpuDrivenFrame {
|
|
|
391
931
|
export class GpuDrivenProduction {
|
|
392
932
|
private view: GpuDrivenView | undefined;
|
|
393
933
|
private raster: GpuDrivenRigidRaster | undefined;
|
|
934
|
+
private standardRaster: StandardPbrRasterAdapter | undefined;
|
|
394
935
|
private readonly sceneIdentities = new WeakMap<object, number>();
|
|
395
936
|
private nextSceneIdentity = 1;
|
|
396
937
|
private filteredCache:
|
|
397
938
|
| {
|
|
398
939
|
readonly source: SubmissionPlan;
|
|
399
940
|
readonly meshResidencyEpoch: number;
|
|
941
|
+
readonly lodProjectionFingerprint: string;
|
|
942
|
+
readonly activeEntityRevision?: number;
|
|
943
|
+
readonly activeEntityKeys?: ReadonlySet<number>;
|
|
400
944
|
readonly value: FilteredProductionPlan;
|
|
401
945
|
}
|
|
402
946
|
| undefined;
|
|
@@ -406,6 +950,13 @@ export class GpuDrivenProduction {
|
|
|
406
950
|
private batchBindGroupCreates = 0;
|
|
407
951
|
private validatedGpuOwnedRows = 0;
|
|
408
952
|
private cpuFallbackDrawItems = 0;
|
|
953
|
+
private telemetryPrepared = false;
|
|
954
|
+
private telemetryCandidateCount = 0;
|
|
955
|
+
private telemetryGeometryWork = 0;
|
|
956
|
+
private telemetryRootGeometryWork = 0;
|
|
957
|
+
private indirectDrawCount = 0;
|
|
958
|
+
private sceneRowsRequired = true;
|
|
959
|
+
private standardPbrProjection: StandardPbrSceneProjection | undefined;
|
|
409
960
|
private signatureCache:
|
|
410
961
|
| {
|
|
411
962
|
readonly filtered: FilteredProductionPlan;
|
|
@@ -427,39 +978,131 @@ export class GpuDrivenProduction {
|
|
|
427
978
|
readonly meshes: ReadonlyMap<number, MeshGpuHandles>;
|
|
428
979
|
readonly viewBindGroupLayout: BindGroupLayout;
|
|
429
980
|
readonly meshResidencyEpoch: number;
|
|
981
|
+
readonly hdrp: boolean;
|
|
982
|
+
/** Entity keys admitted by the persistent visibility facet for this view. */
|
|
983
|
+
readonly activeEntityKeys?: ReadonlySet<number>;
|
|
984
|
+
/** Collision-free renderer-owned revision for the active visibility set. */
|
|
985
|
+
readonly activeEntityRevision?: number;
|
|
986
|
+
/** Total extracted candidates, including facet-suppressed rows. */
|
|
987
|
+
readonly telemetryCandidateCount?: number;
|
|
988
|
+
readonly occlusion?: OcclusionFrameProjection;
|
|
989
|
+
/** Cooked producer artifact selected from the frame material catalog. */
|
|
990
|
+
readonly standardPbrArtifact?: MaterialShaderArtifact;
|
|
991
|
+
readonly standardPbrPipelineState?: PipelineState;
|
|
430
992
|
}): Result<PreparedGpuDrivenFrame | undefined, RhiError> {
|
|
431
993
|
this.gpuOwnedSnapshotsMaterialized = 0;
|
|
432
994
|
this.filteredPlanBuilds = 0;
|
|
433
995
|
this.batchBindGroupCreates = 0;
|
|
434
996
|
this.validatedGpuOwnedRows = 0;
|
|
435
997
|
this.cpuFallbackDrawItems = 0;
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
) {
|
|
998
|
+
this.telemetryPrepared = false;
|
|
999
|
+
this.telemetryCandidateCount = input.telemetryCandidateCount ?? 0;
|
|
1000
|
+
this.telemetryGeometryWork = 0;
|
|
1001
|
+
this.telemetryRootGeometryWork = 0;
|
|
1002
|
+
this.indirectDrawCount = 0;
|
|
1003
|
+
if (input.hdrp || input.scene === undefined || selectGpuLodLane(this.device.caps) === 'cpu') {
|
|
442
1004
|
this.gpuOwnedEntityCount = 0;
|
|
1005
|
+
this.sceneRowsRequired = false;
|
|
443
1006
|
return ok(undefined);
|
|
444
1007
|
}
|
|
445
1008
|
const scene = input.scene;
|
|
1009
|
+
const projectionFingerprint = lodProjectionFingerprint(input.camera);
|
|
1010
|
+
// An empty filtered plan means this topology/mesh-residency pair has no
|
|
1011
|
+
// renderables that this production lane can own. Camera and visibility
|
|
1012
|
+
// inputs only affect admitted candidates and LOD selection, so they cannot
|
|
1013
|
+
// turn that empty topology into an eligible one. Keep the negative result
|
|
1014
|
+
// resident instead of rescanning every scene slot as the camera moves.
|
|
1015
|
+
const cachedPlanIsTopologyInvariant = this.filteredCache?.value.topologyInvariantEmpty === true;
|
|
1016
|
+
const cachedPlanIsCameraInvariant = cachedPlanIsTopologyInvariant;
|
|
1017
|
+
const cachedVisibilityMatches =
|
|
1018
|
+
cachedPlanIsCameraInvariant || this.filteredCache === undefined
|
|
1019
|
+
? true
|
|
1020
|
+
: input.activeEntityRevision !== undefined &&
|
|
1021
|
+
this.filteredCache.activeEntityRevision !== undefined
|
|
1022
|
+
? input.activeEntityRevision === this.filteredCache.activeEntityRevision
|
|
1023
|
+
: sameEntityKeySet(this.filteredCache.activeEntityKeys, input.activeEntityKeys);
|
|
446
1024
|
let filtered = this.filteredCache?.value;
|
|
447
1025
|
if (
|
|
448
1026
|
filtered === undefined ||
|
|
449
1027
|
this.filteredCache?.source !== scene.plan ||
|
|
450
|
-
|
|
1028
|
+
(!cachedPlanIsTopologyInvariant &&
|
|
1029
|
+
this.filteredCache.meshResidencyEpoch !== input.meshResidencyEpoch) ||
|
|
1030
|
+
(!cachedPlanIsCameraInvariant &&
|
|
1031
|
+
this.filteredCache.lodProjectionFingerprint !== projectionFingerprint) ||
|
|
1032
|
+
!cachedVisibilityMatches
|
|
451
1033
|
) {
|
|
452
|
-
filtered = filteredPlan(
|
|
1034
|
+
filtered = filteredPlan(
|
|
1035
|
+
scene.plan,
|
|
1036
|
+
scene.slots,
|
|
1037
|
+
input.meshes,
|
|
1038
|
+
input.camera,
|
|
1039
|
+
input.activeEntityKeys,
|
|
1040
|
+
scene.worldKeys,
|
|
1041
|
+
);
|
|
453
1042
|
this.filteredCache = {
|
|
454
1043
|
source: scene.plan,
|
|
455
1044
|
meshResidencyEpoch: input.meshResidencyEpoch,
|
|
1045
|
+
lodProjectionFingerprint: projectionFingerprint,
|
|
1046
|
+
...(input.activeEntityRevision === undefined
|
|
1047
|
+
? {}
|
|
1048
|
+
: { activeEntityRevision: input.activeEntityRevision }),
|
|
1049
|
+
...(input.activeEntityKeys === undefined
|
|
1050
|
+
? {}
|
|
1051
|
+
: { activeEntityKeys: input.activeEntityKeys }),
|
|
456
1052
|
value: filtered,
|
|
457
1053
|
};
|
|
458
1054
|
this.gpuOwnedSnapshotsMaterialized = scene.slots.length;
|
|
459
1055
|
this.filteredPlanBuilds = 1;
|
|
460
1056
|
}
|
|
461
1057
|
this.gpuOwnedEntityCount = filtered.entityKeys.size;
|
|
1058
|
+
this.sceneRowsRequired = !filtered.topologyInvariantEmpty;
|
|
1059
|
+
// Publish the command count from the current filtered plan on every
|
|
1060
|
+
// prepare call. A cached graph does not call project() again on stable
|
|
1061
|
+
// frames, so deriving this only while compiling the raster pass would
|
|
1062
|
+
// transiently report zero after prepare() reset the per-frame counters.
|
|
1063
|
+
this.indirectDrawCount = admittedRasterBatchCount(filtered.plan);
|
|
462
1064
|
if (filtered.batches.length === 0) return ok(undefined);
|
|
1065
|
+
this.standardPbrProjection = undefined;
|
|
1066
|
+
const usesStandardPbrRaster = filtered.batches.every(({ batch }) => isStandardPbrBatch(batch));
|
|
1067
|
+
if (usesStandardPbrRaster && input.standardPbrArtifact !== undefined) {
|
|
1068
|
+
const slotsByPrimitive = new Map(scene.slots.map((slot) => [slot.slot, slot] as const));
|
|
1069
|
+
const projection = projectStandardPbrScene({
|
|
1070
|
+
artifact: input.standardPbrArtifact,
|
|
1071
|
+
candidates: filtered.batches.flatMap(({ batch }) =>
|
|
1072
|
+
batch.candidates.flatMap((candidate) => {
|
|
1073
|
+
const slot = slotsByPrimitive.get(candidate.primitiveIndex);
|
|
1074
|
+
return slot === undefined
|
|
1075
|
+
? []
|
|
1076
|
+
: [
|
|
1077
|
+
{
|
|
1078
|
+
primitiveIndex: candidate.primitiveIndex,
|
|
1079
|
+
world: slot.snapshot.transform.world,
|
|
1080
|
+
},
|
|
1081
|
+
];
|
|
1082
|
+
}),
|
|
1083
|
+
),
|
|
1084
|
+
});
|
|
1085
|
+
if (!projection.ok) {
|
|
1086
|
+
return err(
|
|
1087
|
+
new RhiError({
|
|
1088
|
+
code: 'shader-compile-failed',
|
|
1089
|
+
expected: 'receipt-derived Standard PBR scene projection',
|
|
1090
|
+
hint: 'publish one validated MaterialShaderArtifact receipt before entering the GPU lane',
|
|
1091
|
+
}),
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
this.standardPbrProjection = projection.value;
|
|
1095
|
+
if (input.standardPbrPipelineState !== undefined && this.standardRaster === undefined) {
|
|
1096
|
+
const created = StandardPbrRasterAdapter.create({
|
|
1097
|
+
device: this.device,
|
|
1098
|
+
shaderModuleFactory: this.shaderModuleFactory,
|
|
1099
|
+
pipelineState: input.standardPbrPipelineState,
|
|
1100
|
+
artifact: input.standardPbrArtifact,
|
|
1101
|
+
});
|
|
1102
|
+
if (!created.ok) return created;
|
|
1103
|
+
this.standardRaster = created.value;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
463
1106
|
if (this.view === undefined) {
|
|
464
1107
|
const created = GpuDrivenView.create({
|
|
465
1108
|
device: this.device,
|
|
@@ -479,8 +1122,11 @@ export class GpuDrivenProduction {
|
|
|
479
1122
|
}
|
|
480
1123
|
const updated = this.view.update(filtered.plan, scene.scene, activeFrustum(input.camera));
|
|
481
1124
|
if (!updated.ok) return updated;
|
|
1125
|
+
this.telemetryPrepared = true;
|
|
482
1126
|
const view = this.view;
|
|
483
1127
|
const raster = this.raster;
|
|
1128
|
+
const standardRaster = this.standardRaster;
|
|
1129
|
+
const standardArtifact = input.standardPbrArtifact;
|
|
484
1130
|
const sceneIdentity = this.sceneIdentity(scene.scene);
|
|
485
1131
|
const viewInspection = view.inspect();
|
|
486
1132
|
let signature = this.signatureCache?.value;
|
|
@@ -495,10 +1141,12 @@ export class GpuDrivenProduction {
|
|
|
495
1141
|
sceneIdentity,
|
|
496
1142
|
revision: filtered.plan.revision,
|
|
497
1143
|
meshResidencyEpoch: input.meshResidencyEpoch,
|
|
1144
|
+
standardPbrLayoutIdentity: this.standardPbrProjection?.layoutIdentity,
|
|
498
1145
|
viewResourceGeneration: viewInspection.resourceGeneration,
|
|
499
1146
|
batches: filtered.batches.map(({ batch, mesh }) => [
|
|
500
1147
|
batch.batchId,
|
|
501
1148
|
batch.generation,
|
|
1149
|
+
batch.visibleCapacity,
|
|
502
1150
|
batch.visibleBase,
|
|
503
1151
|
batch.candidates.map((candidate) => [
|
|
504
1152
|
candidate.primitiveIndex,
|
|
@@ -512,6 +1160,7 @@ export class GpuDrivenProduction {
|
|
|
512
1160
|
]),
|
|
513
1161
|
capacities: [
|
|
514
1162
|
viewInspection.candidateCapacity,
|
|
1163
|
+
viewInspection.visibleBufferCapacity,
|
|
515
1164
|
viewInspection.batchCapacity,
|
|
516
1165
|
viewInspection.indirectCapacity,
|
|
517
1166
|
],
|
|
@@ -527,11 +1176,165 @@ export class GpuDrivenProduction {
|
|
|
527
1176
|
return ok({
|
|
528
1177
|
topologySignature: signature,
|
|
529
1178
|
entityKeys: filtered.entityKeys,
|
|
530
|
-
ownsAllRenderables:
|
|
1179
|
+
ownsAllRenderables:
|
|
1180
|
+
input.activeEntityKeys === undefined
|
|
1181
|
+
? filtered.entityKeys.size === scene.slots.length
|
|
1182
|
+
: filtered.entityKeys.size === input.activeEntityKeys.size,
|
|
1183
|
+
...(input.occlusion === undefined ? {} : { occlusion: input.occlusion }),
|
|
531
1184
|
_commitResourceReplacement: () => view._commitResourceReplacement(),
|
|
532
1185
|
project: (graph, format, sampleCount) => {
|
|
1186
|
+
this.indirectDrawCount = admittedRasterBatchCount(filtered.plan);
|
|
533
1187
|
const outputs = view.addPasses(graph);
|
|
534
1188
|
if (!outputs.ok) return outputs;
|
|
1189
|
+
if (
|
|
1190
|
+
usesStandardPbrRaster &&
|
|
1191
|
+
standardRaster !== undefined &&
|
|
1192
|
+
standardArtifact !== undefined
|
|
1193
|
+
) {
|
|
1194
|
+
const standardProjected: Array<{
|
|
1195
|
+
readonly batch: GpuDrivenBatch;
|
|
1196
|
+
readonly mesh: MeshGpuHandles;
|
|
1197
|
+
readonly vertex: GraphBuffer;
|
|
1198
|
+
readonly index?: GraphBuffer;
|
|
1199
|
+
readonly pipeline: RenderPipeline;
|
|
1200
|
+
readonly meshBindGroup: BindGroup;
|
|
1201
|
+
}> = [];
|
|
1202
|
+
const standardAccesses: GraphAccess[] = [
|
|
1203
|
+
{ resource: outputs.value.primitive, usage: 'storage-read' },
|
|
1204
|
+
{ resource: outputs.value.instance, usage: 'storage-read' },
|
|
1205
|
+
{ resource: outputs.value.material, usage: 'storage-read' },
|
|
1206
|
+
{ resource: outputs.value.visible, usage: 'storage-read' },
|
|
1207
|
+
{ resource: outputs.value.transform, usage: 'storage-read' },
|
|
1208
|
+
{ resource: outputs.value.indirect, usage: 'indirect-read' },
|
|
1209
|
+
];
|
|
1210
|
+
const slotsByPrimitive = new Map(scene.slots.map((slot) => [slot.slot, slot] as const));
|
|
1211
|
+
const meshBuffers = new Map<
|
|
1212
|
+
number,
|
|
1213
|
+
{ readonly vertex: GraphBuffer; readonly index?: GraphBuffer }
|
|
1214
|
+
>();
|
|
1215
|
+
for (const prepared of filtered.batches) {
|
|
1216
|
+
const candidates = prepared.batch.candidates.flatMap((candidate) => {
|
|
1217
|
+
const slot = slotsByPrimitive.get(candidate.primitiveIndex);
|
|
1218
|
+
return slot === undefined
|
|
1219
|
+
? []
|
|
1220
|
+
: [
|
|
1221
|
+
{
|
|
1222
|
+
primitiveIndex: candidate.primitiveIndex,
|
|
1223
|
+
world: slot.snapshot.transform.world,
|
|
1224
|
+
},
|
|
1225
|
+
];
|
|
1226
|
+
});
|
|
1227
|
+
const projection = projectStandardPbrScene({
|
|
1228
|
+
artifact: standardArtifact,
|
|
1229
|
+
candidates,
|
|
1230
|
+
});
|
|
1231
|
+
if (!projection.ok) {
|
|
1232
|
+
return err(
|
|
1233
|
+
new RhiError({
|
|
1234
|
+
code: 'shader-compile-failed',
|
|
1235
|
+
expected: 'receipt-derived Standard PBR scene projection',
|
|
1236
|
+
hint: 'keep the frame artifact receipt and projected rows on one identity',
|
|
1237
|
+
}),
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
const meshBindGroup = standardRaster.projectionBindGroup(projection.value.rows);
|
|
1241
|
+
if (!meshBindGroup.ok) return meshBindGroup;
|
|
1242
|
+
let buffers = meshBuffers.get(prepared.batch.key.assetHandle);
|
|
1243
|
+
if (buffers === undefined) {
|
|
1244
|
+
const vertex = graph.importBuffer(
|
|
1245
|
+
`gpu-driven.standard.${prepared.batch.key.assetHandle}.vertex`,
|
|
1246
|
+
{
|
|
1247
|
+
size: prepared.mesh.vboBytes,
|
|
1248
|
+
usage: GPU_BUFFER_USAGE_VERTEX | GPU_BUFFER_USAGE_COPY_DST,
|
|
1249
|
+
},
|
|
1250
|
+
() => prepared.mesh.vertexBuffer.handle,
|
|
1251
|
+
);
|
|
1252
|
+
if (!vertex.ok) return vertex;
|
|
1253
|
+
let index: GraphBuffer | undefined;
|
|
1254
|
+
if (prepared.mesh.indexBuffer !== null) {
|
|
1255
|
+
const indexHandle = prepared.mesh.indexBuffer;
|
|
1256
|
+
const imported = graph.importBuffer(
|
|
1257
|
+
`gpu-driven.standard.${prepared.batch.key.assetHandle}.index`,
|
|
1258
|
+
{
|
|
1259
|
+
size: prepared.mesh.iboBytes,
|
|
1260
|
+
usage: GPU_BUFFER_USAGE_INDEX | GPU_BUFFER_USAGE_COPY_DST,
|
|
1261
|
+
},
|
|
1262
|
+
() => indexHandle.handle,
|
|
1263
|
+
);
|
|
1264
|
+
if (!imported.ok) return imported;
|
|
1265
|
+
index = imported.value;
|
|
1266
|
+
}
|
|
1267
|
+
buffers = { vertex: vertex.value, ...(index === undefined ? {} : { index }) };
|
|
1268
|
+
meshBuffers.set(prepared.batch.key.assetHandle, buffers);
|
|
1269
|
+
standardAccesses.push({ resource: buffers.vertex, usage: 'vertex-read' });
|
|
1270
|
+
if (buffers.index !== undefined) {
|
|
1271
|
+
standardAccesses.push({ resource: buffers.index, usage: 'index-read' });
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
const pipeline = standardRaster.pipeline(
|
|
1275
|
+
format,
|
|
1276
|
+
sampleCount,
|
|
1277
|
+
prepared.mesh.layoutProjection,
|
|
1278
|
+
prepared.batch.key.topology,
|
|
1279
|
+
prepared.batch.key.drawKind === 'indexed' &&
|
|
1280
|
+
(prepared.batch.key.topology === 'line-strip' ||
|
|
1281
|
+
prepared.batch.key.topology === 'triangle-strip')
|
|
1282
|
+
? prepared.mesh.indexFormat
|
|
1283
|
+
: undefined,
|
|
1284
|
+
prepared.renderState,
|
|
1285
|
+
);
|
|
1286
|
+
if (!pipeline.ok) return pipeline;
|
|
1287
|
+
standardProjected.push({
|
|
1288
|
+
batch: prepared.batch,
|
|
1289
|
+
mesh: prepared.mesh,
|
|
1290
|
+
...buffers,
|
|
1291
|
+
pipeline: pipeline.value,
|
|
1292
|
+
meshBindGroup: meshBindGroup.value,
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
const [materialBindGroup, instancesBindGroup] = standardRaster.frameGroups;
|
|
1296
|
+
return ok({
|
|
1297
|
+
accesses: standardAccesses,
|
|
1298
|
+
encode: (viewBindGroup, pass, resources: GraphResourceResolver) => {
|
|
1299
|
+
// pbr-view-bgl has two dynamic uniform bindings: the frame view
|
|
1300
|
+
// slot and the optional Points/Lines style slot. Standard
|
|
1301
|
+
// draws do not use the latter, but WebGPU still requires one
|
|
1302
|
+
// offset per dynamic binding.
|
|
1303
|
+
pass.setBindGroup(0, viewBindGroup, [0, 0]);
|
|
1304
|
+
pass.setBindGroup(1, materialBindGroup, [0]);
|
|
1305
|
+
pass.setBindGroup(3, instancesBindGroup);
|
|
1306
|
+
let currentPipeline: RenderPipeline | undefined;
|
|
1307
|
+
let currentVertex: GraphBuffer | undefined;
|
|
1308
|
+
let currentIndex: GraphBuffer | undefined;
|
|
1309
|
+
for (const batch of standardProjected) {
|
|
1310
|
+
if (currentPipeline !== batch.pipeline) {
|
|
1311
|
+
pass.setPipeline(batch.pipeline);
|
|
1312
|
+
currentPipeline = batch.pipeline;
|
|
1313
|
+
}
|
|
1314
|
+
pass.setBindGroup(2, batch.meshBindGroup, [0]);
|
|
1315
|
+
if (currentVertex !== batch.vertex) {
|
|
1316
|
+
const vertex = resources.buffer(batch.vertex);
|
|
1317
|
+
if (!vertex.ok) throw vertex.error;
|
|
1318
|
+
pass.setVertexBuffer(0, vertex.value);
|
|
1319
|
+
currentVertex = batch.vertex;
|
|
1320
|
+
}
|
|
1321
|
+
if (batch.index !== undefined && currentIndex !== batch.index) {
|
|
1322
|
+
const index = resources.buffer(batch.index);
|
|
1323
|
+
if (!index.ok) throw index.error;
|
|
1324
|
+
pass.setIndexBuffer(index.value, batch.mesh.indexFormat);
|
|
1325
|
+
currentIndex = batch.index;
|
|
1326
|
+
}
|
|
1327
|
+
const indirect = resources.buffer(outputs.value.indirect);
|
|
1328
|
+
if (!indirect.ok) throw indirect.error;
|
|
1329
|
+
if (batch.batch.key.drawKind === 'indexed') {
|
|
1330
|
+
pass.drawIndexedIndirect(indirect.value, batch.batch.indirectOffset);
|
|
1331
|
+
} else {
|
|
1332
|
+
pass.drawIndirect(indirect.value, batch.batch.indirectOffset);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
},
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
535
1338
|
const projected: ProjectedBatch[] = [];
|
|
536
1339
|
const accesses: GraphAccess[] = [
|
|
537
1340
|
{ resource: outputs.value.primitive, usage: 'storage-read' },
|
|
@@ -546,6 +1349,10 @@ export class GpuDrivenProduction {
|
|
|
546
1349
|
{ readonly vertex: GraphBuffer; readonly index?: GraphBuffer }
|
|
547
1350
|
>();
|
|
548
1351
|
for (const prepared of filtered.batches) {
|
|
1352
|
+
// A selector-only batch can have zero admitted candidates. Keep it
|
|
1353
|
+
// in the GPU selector and readback counters, but do not create a
|
|
1354
|
+
// raster bind group or encode a zero-instance indirect draw.
|
|
1355
|
+
if (prepared.batch.visibleCapacity === 0) continue;
|
|
549
1356
|
let buffers = meshBuffers.get(prepared.batch.key.assetHandle);
|
|
550
1357
|
if (buffers === undefined) {
|
|
551
1358
|
const vertex = graph.importBuffer(
|
|
@@ -581,7 +1388,7 @@ export class GpuDrivenProduction {
|
|
|
581
1388
|
const pipeline = raster.pipeline(
|
|
582
1389
|
format,
|
|
583
1390
|
sampleCount,
|
|
584
|
-
|
|
1391
|
+
prepared.mesh.layoutProjection.arrayStride,
|
|
585
1392
|
prepared.batch.key.topology,
|
|
586
1393
|
prepared.batch.key.drawKind === 'indexed' &&
|
|
587
1394
|
(prepared.batch.key.topology === 'line-strip' ||
|
|
@@ -601,6 +1408,7 @@ export class GpuDrivenProduction {
|
|
|
601
1408
|
bindings: bindings.value,
|
|
602
1409
|
});
|
|
603
1410
|
}
|
|
1411
|
+
this.indirectDrawCount = projected.length;
|
|
604
1412
|
return ok({
|
|
605
1413
|
accesses,
|
|
606
1414
|
encode: (viewBindGroup, pass, resources: GraphResourceResolver) => {
|
|
@@ -633,11 +1441,14 @@ export class GpuDrivenProduction {
|
|
|
633
1441
|
}
|
|
634
1442
|
const indirect = resources.buffer(outputs.value.indirect);
|
|
635
1443
|
if (!indirect.ok) throw indirect.error;
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
1444
|
+
const draw = (): void => {
|
|
1445
|
+
if (batch.batch.key.drawKind === 'indexed') {
|
|
1446
|
+
pass.drawIndexedIndirect(indirect.value, batch.batch.indirectOffset);
|
|
1447
|
+
} else {
|
|
1448
|
+
pass.drawIndirect(indirect.value, batch.batch.indirectOffset);
|
|
1449
|
+
}
|
|
1450
|
+
};
|
|
1451
|
+
draw();
|
|
641
1452
|
}
|
|
642
1453
|
},
|
|
643
1454
|
});
|
|
@@ -651,6 +1462,12 @@ export class GpuDrivenProduction {
|
|
|
651
1462
|
this.raster = undefined;
|
|
652
1463
|
this.filteredCache = undefined;
|
|
653
1464
|
this.signatureCache = undefined;
|
|
1465
|
+
this.sceneRowsRequired = true;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
/** Whether the current production topology can consume GPU Scene row updates. */
|
|
1469
|
+
requiresSceneRows(): boolean {
|
|
1470
|
+
return this.sceneRowsRequired;
|
|
654
1471
|
}
|
|
655
1472
|
|
|
656
1473
|
inspect(): GpuDrivenProductionInspection {
|
|
@@ -667,17 +1484,112 @@ export class GpuDrivenProduction {
|
|
|
667
1484
|
topologyRevision: view?.topologyRevision,
|
|
668
1485
|
validatedGpuOwnedRows: this.validatedGpuOwnedRows,
|
|
669
1486
|
cpuFallbackDrawItems: this.cpuFallbackDrawItems,
|
|
1487
|
+
geometryWork: this.telemetryGeometryWork,
|
|
1488
|
+
rootGeometryWork: this.telemetryRootGeometryWork,
|
|
1489
|
+
geometryWorkReduction:
|
|
1490
|
+
this.telemetryRootGeometryWork > 0
|
|
1491
|
+
? 1 - this.telemetryGeometryWork / this.telemetryRootGeometryWork
|
|
1492
|
+
: 0,
|
|
1493
|
+
batchCount: view?.batchCount ?? 0,
|
|
1494
|
+
indirectDrawCount: this.indirectDrawCount,
|
|
670
1495
|
};
|
|
671
1496
|
}
|
|
672
1497
|
|
|
1498
|
+
/** Read renderer-owned LOD counters copied by the last GPU submission. */
|
|
1499
|
+
readLodSelection(): Promise<GpuDrivenLodSelectionInspection | undefined> {
|
|
1500
|
+
if (!this.telemetryPrepared) return Promise.resolve(undefined);
|
|
1501
|
+
return (this.view?.readLodSelection() ?? Promise.resolve(undefined)).then((selection) => {
|
|
1502
|
+
if (selection !== undefined) {
|
|
1503
|
+
this.telemetryGeometryWork = selection.geometryWork;
|
|
1504
|
+
this.telemetryRootGeometryWork = selection.rootGeometryWork;
|
|
1505
|
+
}
|
|
1506
|
+
if (selection === undefined || this.telemetryCandidateCount <= 0) return selection;
|
|
1507
|
+
const filtered = this.filteredCache?.value;
|
|
1508
|
+
if (filtered === undefined) return selection;
|
|
1509
|
+
const preparedByBatchId = new Map(
|
|
1510
|
+
filtered.batches.map((prepared) => [prepared.batch.batchId, prepared] as const),
|
|
1511
|
+
);
|
|
1512
|
+
const worldSelections = new Map<
|
|
1513
|
+
number,
|
|
1514
|
+
{
|
|
1515
|
+
readonly worldKey: number;
|
|
1516
|
+
readonly primitiveSlot: number;
|
|
1517
|
+
readonly slotGeneration: number;
|
|
1518
|
+
candidateCount: number;
|
|
1519
|
+
visible: number;
|
|
1520
|
+
occluded: number;
|
|
1521
|
+
readonly lodHistogram: Map<number, number>;
|
|
1522
|
+
}
|
|
1523
|
+
>();
|
|
1524
|
+
for (const batchSelection of selection.batches) {
|
|
1525
|
+
const prepared = preparedByBatchId.get(batchSelection.batchId);
|
|
1526
|
+
if (prepared === undefined) return selection;
|
|
1527
|
+
let aggregate = worldSelections.get(prepared.worldKey);
|
|
1528
|
+
if (aggregate === undefined) {
|
|
1529
|
+
aggregate = {
|
|
1530
|
+
worldKey: prepared.worldKey,
|
|
1531
|
+
primitiveSlot: prepared.primitiveSlot,
|
|
1532
|
+
slotGeneration: prepared.slotGeneration,
|
|
1533
|
+
candidateCount: 0,
|
|
1534
|
+
visible: 0,
|
|
1535
|
+
occluded: 0,
|
|
1536
|
+
lodHistogram: new Map(),
|
|
1537
|
+
};
|
|
1538
|
+
worldSelections.set(prepared.worldKey, aggregate);
|
|
1539
|
+
}
|
|
1540
|
+
aggregate.candidateCount += batchSelection.candidateCount;
|
|
1541
|
+
aggregate.visible += batchSelection.visible;
|
|
1542
|
+
aggregate.occluded += batchSelection.occluded;
|
|
1543
|
+
for (const row of batchSelection.lodHistogram) {
|
|
1544
|
+
aggregate.lodHistogram.set(
|
|
1545
|
+
row.level,
|
|
1546
|
+
(aggregate.lodHistogram.get(row.level) ?? 0) + row.count,
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
return {
|
|
1551
|
+
...selection,
|
|
1552
|
+
candidateCount: this.telemetryCandidateCount,
|
|
1553
|
+
occluded: Math.max(0, this.telemetryCandidateCount - selection.visible),
|
|
1554
|
+
worldSelections: Object.freeze(
|
|
1555
|
+
[...worldSelections.values()].map((aggregate) =>
|
|
1556
|
+
Object.freeze({
|
|
1557
|
+
worldKey: aggregate.worldKey,
|
|
1558
|
+
primitiveSlot: aggregate.primitiveSlot,
|
|
1559
|
+
slotGeneration: aggregate.slotGeneration,
|
|
1560
|
+
candidateCount: aggregate.candidateCount,
|
|
1561
|
+
visible: aggregate.visible,
|
|
1562
|
+
occluded: aggregate.occluded,
|
|
1563
|
+
lodHistogram: Object.freeze(
|
|
1564
|
+
[...aggregate.lodHistogram.entries()]
|
|
1565
|
+
.sort(([left], [right]) => left - right)
|
|
1566
|
+
.map(([level, count]) => Object.freeze({ level, count })),
|
|
1567
|
+
),
|
|
1568
|
+
} satisfies GpuDrivenWorldLodSelectionInspection),
|
|
1569
|
+
),
|
|
1570
|
+
),
|
|
1571
|
+
};
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
|
|
673
1575
|
recordCpuValidation(
|
|
674
1576
|
validated: readonly { readonly source: RenderableSnapshot }[],
|
|
675
1577
|
gpuOwned: ReadonlySet<number>,
|
|
1578
|
+
worldKeys?: readonly number[],
|
|
676
1579
|
): void {
|
|
677
1580
|
this.cpuFallbackDrawItems = validated.length;
|
|
678
1581
|
let gpuOwnedRows = 0;
|
|
679
1582
|
for (const row of validated) {
|
|
680
|
-
if (
|
|
1583
|
+
if (
|
|
1584
|
+
gpuOwned.has(
|
|
1585
|
+
worldEntityKey(
|
|
1586
|
+
worldKeys?.[row.source.worldId] ?? row.source.worldId,
|
|
1587
|
+
row.source.entityKey,
|
|
1588
|
+
),
|
|
1589
|
+
)
|
|
1590
|
+
) {
|
|
1591
|
+
gpuOwnedRows += 1;
|
|
1592
|
+
}
|
|
681
1593
|
}
|
|
682
1594
|
this.validatedGpuOwnedRows = gpuOwnedRows;
|
|
683
1595
|
}
|