@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,9 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Query, World } from '@forgeax/engine-ecs';
|
|
2
2
|
import { componentId } from '@forgeax/engine-ecs/internal';
|
|
3
|
-
import type { RenderReadLease } from '@forgeax/engine-ecs/projection';
|
|
3
|
+
import type { RenderReadLease, RenderReadVersion } from '@forgeax/engine-ecs/projection';
|
|
4
4
|
import { box3, frustum, mat4 } from '@forgeax/engine-math';
|
|
5
5
|
import { type RhiDevice, RhiError } from '@forgeax/engine-rhi';
|
|
6
|
-
import { ChildOf, Transform } from '@forgeax/engine-scene';
|
|
6
|
+
import { ChildOf, GlobalTransform, Transform } from '@forgeax/engine-scene';
|
|
7
7
|
import { Skin } from '@forgeax/engine-skinning';
|
|
8
8
|
import { err, ok, type Result } from '@forgeax/engine-types';
|
|
9
9
|
import {
|
|
@@ -11,8 +11,10 @@ import {
|
|
|
11
11
|
DirectionalLight,
|
|
12
12
|
Instances,
|
|
13
13
|
Layer,
|
|
14
|
+
LightProbe,
|
|
14
15
|
MeshFilter,
|
|
15
16
|
MeshRenderer,
|
|
17
|
+
MotionBlur,
|
|
16
18
|
PointLight,
|
|
17
19
|
PointLightShadow,
|
|
18
20
|
PostProcessParams,
|
|
@@ -34,6 +36,7 @@ import type { PersistentRenderSceneInspection } from '../inspection-types';
|
|
|
34
36
|
import { fingerprintNumericArray, InstanceBoundsCache } from '../instances-derived-bounds';
|
|
35
37
|
import type { PointsLinesInspection } from '../points-lines/inspection';
|
|
36
38
|
import type { PointsLinesRetainedSnapshot } from '../points-lines/snapshot';
|
|
39
|
+
import { worldEntityKey } from '../record/frame-snapshot';
|
|
37
40
|
import {
|
|
38
41
|
type ReflectionProbeFact,
|
|
39
42
|
ReflectionProbeProjection,
|
|
@@ -55,21 +58,30 @@ import {
|
|
|
55
58
|
type TransmissionDemandOperation,
|
|
56
59
|
} from '../transmission/projection';
|
|
57
60
|
import { VolumetricFog } from '../volume/component';
|
|
61
|
+
import { ProbeBlendSceneProjection, type ProbeSceneObjectInput } from './probe-blend';
|
|
58
62
|
import type {
|
|
59
63
|
RenderSceneApplyResult,
|
|
60
64
|
RenderSceneBounds,
|
|
61
|
-
RenderSceneDelta,
|
|
62
65
|
RenderSceneInspection,
|
|
63
66
|
RenderSceneOperation,
|
|
64
67
|
RenderSceneRecord,
|
|
65
68
|
RenderSceneResyncReason,
|
|
66
69
|
RenderSceneSlot,
|
|
67
70
|
} from './render-scene-types';
|
|
71
|
+
import { createVisibilityBudget, type VisibilityBudget } from './visibility/budget';
|
|
72
|
+
import {
|
|
73
|
+
primitiveKey,
|
|
74
|
+
primitiveKeyId,
|
|
75
|
+
VisibilityFacetStore,
|
|
76
|
+
viewKey,
|
|
77
|
+
viewKeyId,
|
|
78
|
+
} from './visibility/facet';
|
|
79
|
+
import { decideVisibility } from './visibility/occlusion-confidence';
|
|
80
|
+
import type { PrimitiveKey, ViewKey, VisibilityCandidate } from './visibility/types';
|
|
68
81
|
|
|
69
82
|
export type {
|
|
70
83
|
RenderSceneApplyResult,
|
|
71
84
|
RenderSceneBounds,
|
|
72
|
-
RenderSceneDelta,
|
|
73
85
|
RenderSceneIdentity,
|
|
74
86
|
RenderSceneInspection,
|
|
75
87
|
RenderSceneOperation,
|
|
@@ -86,6 +98,14 @@ interface PendingIdentity {
|
|
|
86
98
|
removed: boolean;
|
|
87
99
|
}
|
|
88
100
|
|
|
101
|
+
interface WorldTransformSpan {
|
|
102
|
+
readonly worldId: number;
|
|
103
|
+
readonly entities: Readonly<Uint32Array>;
|
|
104
|
+
readonly worlds: Readonly<Float32Array>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
type GlobalTransformChangeQuery = Query<readonly [typeof GlobalTransform]>;
|
|
108
|
+
|
|
89
109
|
export interface RenderSceneSubmissionCapture {
|
|
90
110
|
readonly revision: number;
|
|
91
111
|
readonly slots: readonly {
|
|
@@ -200,31 +220,55 @@ function intersects(left: RenderSceneBounds, right: RenderSceneBounds): boolean
|
|
|
200
220
|
);
|
|
201
221
|
}
|
|
202
222
|
|
|
203
|
-
function worldBounds(
|
|
223
|
+
function worldBounds(
|
|
224
|
+
snapshot: RenderableSnapshot,
|
|
225
|
+
target?: RenderSceneBounds,
|
|
226
|
+
): RenderSceneBounds | undefined {
|
|
204
227
|
const local = snapshot.localAabb;
|
|
205
228
|
if (local === undefined || local.length < 6) return undefined;
|
|
206
229
|
const world = snapshot.transform.world;
|
|
207
|
-
const min
|
|
208
|
-
const max
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
230
|
+
const min = (target?.min ?? [0, 0, 0]) as [number, number, number];
|
|
231
|
+
const max = (target?.max ?? [0, 0, 0]) as [number, number, number];
|
|
232
|
+
const centerX = ((local[0] ?? 0) + (local[3] ?? 0)) * 0.5;
|
|
233
|
+
const centerY = ((local[1] ?? 0) + (local[4] ?? 0)) * 0.5;
|
|
234
|
+
const centerZ = ((local[2] ?? 0) + (local[5] ?? 0)) * 0.5;
|
|
235
|
+
const extentX = ((local[3] ?? 0) - (local[0] ?? 0)) * 0.5;
|
|
236
|
+
const extentY = ((local[4] ?? 0) - (local[1] ?? 0)) * 0.5;
|
|
237
|
+
const extentZ = ((local[5] ?? 0) - (local[2] ?? 0)) * 0.5;
|
|
238
|
+
const worldCenterX =
|
|
239
|
+
(world[0] ?? 0) * centerX +
|
|
240
|
+
(world[4] ?? 0) * centerY +
|
|
241
|
+
(world[8] ?? 0) * centerZ +
|
|
242
|
+
(world[12] ?? 0);
|
|
243
|
+
const worldCenterY =
|
|
244
|
+
(world[1] ?? 0) * centerX +
|
|
245
|
+
(world[5] ?? 0) * centerY +
|
|
246
|
+
(world[9] ?? 0) * centerZ +
|
|
247
|
+
(world[13] ?? 0);
|
|
248
|
+
const worldCenterZ =
|
|
249
|
+
(world[2] ?? 0) * centerX +
|
|
250
|
+
(world[6] ?? 0) * centerY +
|
|
251
|
+
(world[10] ?? 0) * centerZ +
|
|
252
|
+
(world[14] ?? 0);
|
|
253
|
+
const worldExtentX =
|
|
254
|
+
Math.abs(world[0] ?? 0) * extentX +
|
|
255
|
+
Math.abs(world[4] ?? 0) * extentY +
|
|
256
|
+
Math.abs(world[8] ?? 0) * extentZ;
|
|
257
|
+
const worldExtentY =
|
|
258
|
+
Math.abs(world[1] ?? 0) * extentX +
|
|
259
|
+
Math.abs(world[5] ?? 0) * extentY +
|
|
260
|
+
Math.abs(world[9] ?? 0) * extentZ;
|
|
261
|
+
const worldExtentZ =
|
|
262
|
+
Math.abs(world[2] ?? 0) * extentX +
|
|
263
|
+
Math.abs(world[6] ?? 0) * extentY +
|
|
264
|
+
Math.abs(world[10] ?? 0) * extentZ;
|
|
265
|
+
min[0] = worldCenterX - worldExtentX;
|
|
266
|
+
min[1] = worldCenterY - worldExtentY;
|
|
267
|
+
min[2] = worldCenterZ - worldExtentZ;
|
|
268
|
+
max[0] = worldCenterX + worldExtentX;
|
|
269
|
+
max[1] = worldCenterY + worldExtentY;
|
|
270
|
+
max[2] = worldCenterZ + worldExtentZ;
|
|
271
|
+
return target ?? { min, max };
|
|
228
272
|
}
|
|
229
273
|
|
|
230
274
|
function boundsFromArray(bounds: ArrayLike<number>): RenderSceneBounds | undefined {
|
|
@@ -253,20 +297,23 @@ function boundsFromArray(bounds: ArrayLike<number>): RenderSceneBounds | undefin
|
|
|
253
297
|
export class RenderScene {
|
|
254
298
|
private readonly slots: Array<RenderSceneSlot | undefined> = [];
|
|
255
299
|
private readonly lastSubmittedSlots: Array<RenderSceneSlot | undefined> = [];
|
|
300
|
+
private readonly previousWorldBySlot: Array<Float32Array | undefined> = [];
|
|
256
301
|
private readonly generations: number[] = [];
|
|
257
302
|
private readonly freeSlots: number[] = [];
|
|
258
303
|
private readonly slotsByWorld = new Map<number, Map<number, number>>();
|
|
259
304
|
private readonly slotsByMaterial = new Map<number, Set<number>>();
|
|
260
305
|
private readonly dirtySinceSubmitted = new Set<number>();
|
|
306
|
+
private readonly transformUpdatedSlots: RenderSceneSlot[] = [];
|
|
307
|
+
private temporalTracking = false;
|
|
261
308
|
private lastSubmittedVisible = new Set<string>();
|
|
262
309
|
private submittedEpoch = 0;
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
private readonly
|
|
310
|
+
// Bounds are a derived column of the stable render slot, not metadata of a
|
|
311
|
+
// transient snapshot wrapper. Undefined means dirty; null means that the
|
|
312
|
+
// retained row has no usable bounds and must be conservatively visible.
|
|
313
|
+
private readonly worldBoundsBySlot: Array<RenderSceneBounds | null | undefined> = [];
|
|
267
314
|
private readonly instanceBoundsCache = new InstanceBoundsCache();
|
|
268
315
|
private orderedSlots: number[] = [];
|
|
269
|
-
private materialized:
|
|
316
|
+
private materialized: RenderableSnapshot[] | undefined;
|
|
270
317
|
private slotSnapshot: readonly RenderSceneSlot[] | undefined;
|
|
271
318
|
private revision = 0;
|
|
272
319
|
private noChangeFrames = 0;
|
|
@@ -374,6 +421,7 @@ export class RenderScene {
|
|
|
374
421
|
};
|
|
375
422
|
this.unindexMaterials(pending.initial);
|
|
376
423
|
this.slots[pending.initial.slot] = updatedSlot;
|
|
424
|
+
this.worldBoundsBySlot[updatedSlot.slot] = undefined;
|
|
377
425
|
this.indexMaterials(updatedSlot);
|
|
378
426
|
this.dirtySinceSubmitted.add(updatedSlot.slot);
|
|
379
427
|
updatedSlots.push(updatedSlot);
|
|
@@ -399,19 +447,55 @@ export class RenderScene {
|
|
|
399
447
|
};
|
|
400
448
|
}
|
|
401
449
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
450
|
+
/** Apply version-selected GlobalTransform columns without allocating per-entity operations. */
|
|
451
|
+
applyTransformSpans(spans: readonly WorldTransformSpan[]): RenderSceneApplyResult {
|
|
452
|
+
this.deltaFrames += 1;
|
|
453
|
+
let scanned = 0;
|
|
454
|
+
let updated = 0;
|
|
455
|
+
let ignoredLateUpdates = 0;
|
|
456
|
+
const updatedSlots = this.transformUpdatedSlots;
|
|
457
|
+
updatedSlots.length = 0;
|
|
458
|
+
for (const span of spans) {
|
|
459
|
+
scanned += span.entities.length;
|
|
460
|
+
for (let row = 0; row < span.entities.length; row += 1) {
|
|
461
|
+
const entity = span.entities[row];
|
|
462
|
+
if (entity === undefined) continue;
|
|
463
|
+
const slot = this.lookup(span.worldId, entity);
|
|
464
|
+
if (slot === undefined) {
|
|
465
|
+
ignoredLateUpdates += 1;
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
const target = slot.snapshot.transform.world;
|
|
469
|
+
const sourceStart = row * 16;
|
|
470
|
+
for (let index = 0; index < 16; index += 1) {
|
|
471
|
+
target[index] = span.worlds[sourceStart + index] ?? target[index] ?? 0;
|
|
472
|
+
}
|
|
473
|
+
if (slot.snapshot.instances === undefined) {
|
|
474
|
+
const cached = this.worldBoundsBySlot[slot.slot];
|
|
475
|
+
const bounds = worldBounds(slot.snapshot, cached ?? undefined);
|
|
476
|
+
this.worldBoundsBySlot[slot.slot] = bounds ?? null;
|
|
477
|
+
} else {
|
|
478
|
+
this.worldBoundsBySlot[slot.slot] = undefined;
|
|
479
|
+
}
|
|
480
|
+
if (this.temporalTracking) this.dirtySinceSubmitted.add(slot.slot);
|
|
481
|
+
updatedSlots.push(slot);
|
|
482
|
+
updated += 1;
|
|
483
|
+
}
|
|
413
484
|
}
|
|
414
|
-
|
|
485
|
+
this.renderableScans = scanned;
|
|
486
|
+
if (updated > 0) this.revision += 1;
|
|
487
|
+
return {
|
|
488
|
+
created: 0,
|
|
489
|
+
updated,
|
|
490
|
+
removed: 0,
|
|
491
|
+
recreated: 0,
|
|
492
|
+
ignoredLateUpdates,
|
|
493
|
+
createdSlots: [],
|
|
494
|
+
updatedSlots,
|
|
495
|
+
removedSlots: [],
|
|
496
|
+
recreatedSlots: [],
|
|
497
|
+
resynced: 0,
|
|
498
|
+
};
|
|
415
499
|
}
|
|
416
500
|
|
|
417
501
|
/** Reconcile the current identity set while retaining slots for survivors. */
|
|
@@ -446,6 +530,7 @@ export class RenderScene {
|
|
|
446
530
|
};
|
|
447
531
|
this.unindexMaterials(existing);
|
|
448
532
|
this.slots[existing.slot] = updated;
|
|
533
|
+
this.worldBoundsBySlot[updated.slot] = undefined;
|
|
449
534
|
this.indexMaterials(updated);
|
|
450
535
|
this.dirtySinceSubmitted.add(updated.slot);
|
|
451
536
|
this.orderedSlots.push(existing.slot);
|
|
@@ -460,14 +545,25 @@ export class RenderScene {
|
|
|
460
545
|
this.reset(snapshots);
|
|
461
546
|
}
|
|
462
547
|
|
|
463
|
-
|
|
548
|
+
/** Enable previous-frame publication only while a temporal feature consumes it. */
|
|
549
|
+
setTemporalTracking(enabled: boolean): void {
|
|
550
|
+
if (enabled === this.temporalTracking) return;
|
|
551
|
+
this.temporalTracking = enabled;
|
|
552
|
+
if (!enabled) {
|
|
553
|
+
this.dirtySinceSubmitted.clear();
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
for (const slot of this.orderedSlots) this.dirtySinceSubmitted.add(slot);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
materialize(): RenderableSnapshot[] {
|
|
464
560
|
if (this.materialized !== undefined) return this.materialized;
|
|
465
561
|
const snapshots: RenderableSnapshot[] = [];
|
|
466
562
|
for (const slot of this.orderedSlots) {
|
|
467
563
|
const record = this.slots[slot];
|
|
468
564
|
if (record !== undefined) snapshots.push(record.snapshot);
|
|
469
565
|
}
|
|
470
|
-
this.materialized =
|
|
566
|
+
this.materialized = snapshots;
|
|
471
567
|
return this.materialized;
|
|
472
568
|
}
|
|
473
569
|
|
|
@@ -513,6 +609,15 @@ export class RenderScene {
|
|
|
513
609
|
}
|
|
514
610
|
for (const update of capture.slots) {
|
|
515
611
|
this.lastSubmittedSlots[update.slot] = update.current;
|
|
612
|
+
const currentWorld = update.current?.snapshot.transform.world;
|
|
613
|
+
if (currentWorld !== undefined) {
|
|
614
|
+
let previousWorld = this.previousWorldBySlot[update.slot];
|
|
615
|
+
if (previousWorld === undefined) {
|
|
616
|
+
previousWorld = new Float32Array(16);
|
|
617
|
+
this.previousWorldBySlot[update.slot] = previousWorld;
|
|
618
|
+
}
|
|
619
|
+
previousWorld.set(currentWorld);
|
|
620
|
+
}
|
|
516
621
|
this.dirtySinceSubmitted.delete(update.slot);
|
|
517
622
|
}
|
|
518
623
|
this.lastSubmittedVisible = new Set(capture.visible);
|
|
@@ -575,8 +680,7 @@ export class RenderScene {
|
|
|
575
680
|
for (const slot of this.orderedSlots) {
|
|
576
681
|
const record = this.slots[slot];
|
|
577
682
|
if (record === undefined) continue;
|
|
578
|
-
const
|
|
579
|
-
const candidate = this.cullingWorldBounds(snapshot);
|
|
683
|
+
const candidate = this.cullingWorldBoundsAt(record);
|
|
580
684
|
if (candidate === null || candidate === undefined) continue;
|
|
581
685
|
if (intersects(candidate, bounds)) records.push(record);
|
|
582
686
|
}
|
|
@@ -634,8 +738,15 @@ export class RenderScene {
|
|
|
634
738
|
entityKey: snapshot.entityKey,
|
|
635
739
|
snapshot: ownSnapshot(snapshot),
|
|
636
740
|
};
|
|
741
|
+
let previousWorld = this.previousWorldBySlot[slot];
|
|
742
|
+
if (previousWorld === undefined) {
|
|
743
|
+
previousWorld = new Float32Array(16);
|
|
744
|
+
this.previousWorldBySlot[slot] = previousWorld;
|
|
745
|
+
}
|
|
746
|
+
previousWorld.set(record.snapshot.transform.world);
|
|
637
747
|
this.generations[slot] = generation;
|
|
638
748
|
this.slots[slot] = record;
|
|
749
|
+
this.worldBoundsBySlot[slot] = undefined;
|
|
639
750
|
let entities = this.slotsByWorld.get(snapshot.worldId);
|
|
640
751
|
if (entities === undefined) {
|
|
641
752
|
entities = new Map<number, number>();
|
|
@@ -649,6 +760,7 @@ export class RenderScene {
|
|
|
649
760
|
|
|
650
761
|
private release(record: RenderSceneSlot): void {
|
|
651
762
|
this.slots[record.slot] = undefined;
|
|
763
|
+
this.worldBoundsBySlot[record.slot] = undefined;
|
|
652
764
|
this.instanceBoundsCache.invalidate(record.entityKey, record.worldId);
|
|
653
765
|
this.unindexMaterials(record);
|
|
654
766
|
const entities = this.slotsByWorld.get(record.worldId);
|
|
@@ -711,12 +823,16 @@ export class RenderScene {
|
|
|
711
823
|
reasons.includes('geometry-revision') ||
|
|
712
824
|
reasons.includes('material-revision');
|
|
713
825
|
const previous = seedCurrent || submitted === undefined ? slot.snapshot : submitted.snapshot;
|
|
826
|
+
const previousWorld = this.previousWorldBySlot[slot.slot];
|
|
714
827
|
return {
|
|
715
828
|
previousEpoch: seedCurrent ? undefined : this.submittedEpoch,
|
|
716
829
|
previousSource: seedCurrent ? 'current-seed' : 'last-submitted',
|
|
717
830
|
reactive: reasons.length > 0,
|
|
718
831
|
reactiveReasons: reasons,
|
|
719
|
-
previousTransform:
|
|
832
|
+
previousTransform:
|
|
833
|
+
seedCurrent || previousWorld === undefined
|
|
834
|
+
? previous.transform
|
|
835
|
+
: { ...previous.transform, world: previousWorld },
|
|
720
836
|
previousInstances: previous.instances,
|
|
721
837
|
previousSkin: previous.skin,
|
|
722
838
|
previousMorphWeights: previous.morph?.weights,
|
|
@@ -734,15 +850,14 @@ export class RenderScene {
|
|
|
734
850
|
* bounds for its independent per-instance visibility pass. Unknown or
|
|
735
851
|
* empty instance facts deliberately return undefined (conservative no-cull).
|
|
736
852
|
*/
|
|
737
|
-
|
|
738
|
-
let candidate = this.
|
|
739
|
-
if (candidate !== undefined
|
|
740
|
-
|
|
741
|
-
}
|
|
853
|
+
cullingWorldBoundsAt(slot: RenderSceneSlot): RenderSceneBounds | undefined {
|
|
854
|
+
let candidate = this.worldBoundsBySlot[slot.slot];
|
|
855
|
+
if (candidate !== undefined) return candidate ?? undefined;
|
|
856
|
+
const snapshot = slot.snapshot;
|
|
742
857
|
if (snapshot.instances !== undefined) {
|
|
743
858
|
const instanceCount = snapshot.instances.instanceCount;
|
|
744
859
|
if (instanceCount === 0 || snapshot.localAabb === undefined) {
|
|
745
|
-
this.
|
|
860
|
+
this.worldBoundsBySlot[slot.slot] = null;
|
|
746
861
|
return undefined;
|
|
747
862
|
}
|
|
748
863
|
const derived = this.instanceBoundsCache.get({
|
|
@@ -757,28 +872,75 @@ export class RenderScene {
|
|
|
757
872
|
});
|
|
758
873
|
candidate = boundsFromArray(derived ?? []);
|
|
759
874
|
} else {
|
|
760
|
-
candidate = worldBounds(snapshot);
|
|
875
|
+
candidate = worldBounds(snapshot, candidate ?? undefined);
|
|
761
876
|
}
|
|
762
|
-
this.
|
|
877
|
+
this.worldBoundsBySlot[slot.slot] = candidate ?? null;
|
|
763
878
|
return candidate;
|
|
764
879
|
}
|
|
765
|
-
}
|
|
766
880
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
881
|
+
cullingWorldBounds(snapshot: RenderableSnapshot): RenderSceneBounds | undefined {
|
|
882
|
+
const slot = this.lookup(snapshot.worldId, snapshot.entityKey);
|
|
883
|
+
return slot === undefined ? worldBounds(snapshot) : this.cullingWorldBoundsAt(slot);
|
|
884
|
+
}
|
|
770
885
|
}
|
|
771
886
|
|
|
772
887
|
export interface PersistentRenderSceneOptions {
|
|
773
888
|
readonly getDevice?: (() => RhiDevice) | undefined;
|
|
774
889
|
readonly onGpuError?: ((error: RhiError) => void) | undefined;
|
|
775
890
|
readonly onSharedRefMutation?: ((worldId: number, handle: number) => void) | undefined;
|
|
891
|
+
readonly configuredQueryBudget?: number | undefined;
|
|
776
892
|
}
|
|
777
893
|
|
|
778
894
|
export interface PersistentGpuDrivenState {
|
|
779
895
|
readonly scene: GpuScene;
|
|
780
896
|
readonly plan: SubmissionPlan;
|
|
781
897
|
readonly slots: readonly RenderSceneSlot[];
|
|
898
|
+
/** Stable renderer-local World keys aligned with RenderSceneSlot.worldId. */
|
|
899
|
+
readonly worldKeys?: readonly number[];
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
export interface CpuDirectViewInput {
|
|
903
|
+
readonly view: ViewKey;
|
|
904
|
+
readonly primitives: readonly PrimitiveKey[];
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
export interface CpuDirectCandidate {
|
|
908
|
+
readonly view: ViewKey;
|
|
909
|
+
readonly primitive: PrimitiveKey;
|
|
910
|
+
readonly candidate: VisibilityCandidate;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* The one renderer-owned submission projection for a display view.
|
|
915
|
+
*
|
|
916
|
+
* Occlusion completion updates the facet asynchronously, so extraction must
|
|
917
|
+
* retain every query candidate while record/prepare consume this projection.
|
|
918
|
+
* Keeping the dispatch reindex here prevents CPU direct and GPU indirect
|
|
919
|
+
* lanes from rebuilding a second visibility map with different identity
|
|
920
|
+
* semantics.
|
|
921
|
+
*/
|
|
922
|
+
export interface PersistentVisibilityProjection {
|
|
923
|
+
readonly renderables: readonly RenderableSnapshot[];
|
|
924
|
+
readonly dispatch: readonly DispatchEntry[];
|
|
925
|
+
readonly activeEntityKeys: ReadonlySet<number>;
|
|
926
|
+
/** Monotonic owner revision for exactly this active-key projection. */
|
|
927
|
+
readonly activeEntityRevision: number;
|
|
928
|
+
readonly suppressed: number;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/** Project one selected candidate per view and primitive from the facet owner. */
|
|
932
|
+
export function projectCpuDirectCandidates(
|
|
933
|
+
facets: VisibilityFacetStore,
|
|
934
|
+
views: readonly CpuDirectViewInput[],
|
|
935
|
+
): readonly CpuDirectCandidate[] {
|
|
936
|
+
const candidates: CpuDirectCandidate[] = [];
|
|
937
|
+
for (const { view, primitives } of views) {
|
|
938
|
+
for (const primitive of primitives) {
|
|
939
|
+
const candidate = facets.getCandidate(view, primitive);
|
|
940
|
+
if (candidate !== undefined) candidates.push({ view, primitive, candidate });
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
return candidates;
|
|
782
944
|
}
|
|
783
945
|
|
|
784
946
|
type PersistentGpuSceneInspection = PersistentRenderSceneInspection['gpu'];
|
|
@@ -791,16 +953,29 @@ interface PersistentCompositionEntry {
|
|
|
791
953
|
readonly projection: RenderScene;
|
|
792
954
|
readonly topology: BatchTopology;
|
|
793
955
|
readonly transmissionDemand: PersistentTransmissionDemandProjection;
|
|
794
|
-
readonly
|
|
956
|
+
readonly structure: Pick<
|
|
957
|
+
ExtractedFrame,
|
|
958
|
+
| 'dispatch'
|
|
959
|
+
| 'visibilityStats'
|
|
960
|
+
| 'visibilitySnapshots'
|
|
961
|
+
| 'featureVisibilitySnapshots'
|
|
962
|
+
| 'hiddenEntityReports'
|
|
963
|
+
>;
|
|
964
|
+
/** Stable renderer-local World keys; unlike RenderableSnapshot.worldId they survive reorder. */
|
|
965
|
+
readonly worldKeys: readonly number[];
|
|
966
|
+
readonly readVersions: RenderReadVersion[];
|
|
795
967
|
readonly leaseIdentities: readonly string[];
|
|
968
|
+
readonly transformQueries: readonly GlobalTransformChangeQuery[];
|
|
796
969
|
catalogEpoch: number;
|
|
797
970
|
readonly rigidOnly: boolean;
|
|
798
|
-
|
|
799
|
-
|
|
971
|
+
readonly probeProjection: ProbeBlendSceneProjection;
|
|
972
|
+
temporalDemanded: boolean;
|
|
973
|
+
gpuDrivenSceneRequired: boolean;
|
|
800
974
|
}
|
|
801
975
|
|
|
802
976
|
const RENDER_RELEVANT_COMPONENT_IDS = new Set([
|
|
803
977
|
componentId(Transform),
|
|
978
|
+
componentId(GlobalTransform),
|
|
804
979
|
componentId(ChildOf),
|
|
805
980
|
componentId(MeshFilter),
|
|
806
981
|
componentId(MeshRenderer),
|
|
@@ -811,7 +986,9 @@ const RENDER_RELEVANT_COMPONENT_IDS = new Set([
|
|
|
811
986
|
componentId(Layer),
|
|
812
987
|
componentId(Visibility),
|
|
813
988
|
componentId(Camera),
|
|
989
|
+
componentId(MotionBlur),
|
|
814
990
|
componentId(DirectionalLight),
|
|
991
|
+
componentId(LightProbe),
|
|
815
992
|
componentId(PointLight),
|
|
816
993
|
componentId(PointLightShadow),
|
|
817
994
|
componentId(SpotLight),
|
|
@@ -822,12 +999,20 @@ const RENDER_RELEVANT_COMPONENT_IDS = new Set([
|
|
|
822
999
|
componentId(VolumetricFog),
|
|
823
1000
|
]);
|
|
824
1001
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
1002
|
+
function createGlobalTransformChangeQuery(world: World): GlobalTransformChangeQuery {
|
|
1003
|
+
const result = world.query({ read: [GlobalTransform], changed: [GlobalTransform] });
|
|
1004
|
+
if (!result.ok) throw result.error;
|
|
1005
|
+
const query = result.value as GlobalTransformChangeQuery;
|
|
1006
|
+
for (const _span of query.spans().unwrap()) {
|
|
1007
|
+
// The full rebuild consumed these values; establish the observation baseline.
|
|
1008
|
+
}
|
|
1009
|
+
return query;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
function frameDemandsTemporal(frame: ExtractedFrame): boolean {
|
|
1013
|
+
const camera = frame.cameras[0];
|
|
1014
|
+
return camera?.antialias === 'taa' || (camera?.motionBlur?.shutterAngle ?? 0) > 0;
|
|
1015
|
+
}
|
|
831
1016
|
|
|
832
1017
|
function cameraFrusta(cameras: readonly CameraSnapshot[]): readonly Float32Array[] {
|
|
833
1018
|
const planes: Float32Array[] = [];
|
|
@@ -864,15 +1049,23 @@ function cameraFrusta(cameras: readonly CameraSnapshot[]): readonly Float32Array
|
|
|
864
1049
|
return planes;
|
|
865
1050
|
}
|
|
866
1051
|
|
|
1052
|
+
let cullProjectedIndexScratch = new Int32Array(0);
|
|
1053
|
+
|
|
867
1054
|
function cullPersistentFrame(
|
|
868
1055
|
frame: ExtractedFrame,
|
|
869
1056
|
candidates: readonly RenderableSnapshot[],
|
|
870
|
-
boundsOf: (snapshot: RenderableSnapshot) => RenderSceneBounds | undefined =
|
|
1057
|
+
boundsOf: (snapshot: RenderableSnapshot, index: number) => RenderSceneBounds | undefined = (
|
|
1058
|
+
snapshot,
|
|
1059
|
+
) => worldBounds(snapshot),
|
|
871
1060
|
): ExtractedFrame {
|
|
872
1061
|
const planes = cameraFrusta(frame.cameras);
|
|
873
1062
|
const visible: RenderableSnapshot[] = [];
|
|
874
|
-
|
|
875
|
-
|
|
1063
|
+
if (cullProjectedIndexScratch.length < candidates.length) {
|
|
1064
|
+
cullProjectedIndexScratch = new Int32Array(candidates.length);
|
|
1065
|
+
}
|
|
1066
|
+
const projectedIndex = cullProjectedIndexScratch;
|
|
1067
|
+
projectedIndex.fill(-1, 0, candidates.length);
|
|
1068
|
+
const worldAabb = box3.create();
|
|
876
1069
|
let total = 0;
|
|
877
1070
|
let culled = 0;
|
|
878
1071
|
|
|
@@ -880,10 +1073,9 @@ function cullPersistentFrame(
|
|
|
880
1073
|
const candidate = candidates[index];
|
|
881
1074
|
if (candidate === undefined) continue;
|
|
882
1075
|
let isVisible = true;
|
|
883
|
-
const candidateBounds = boundsOf(candidate);
|
|
1076
|
+
const candidateBounds = boundsOf(candidate, index);
|
|
884
1077
|
if (candidateBounds !== undefined) {
|
|
885
1078
|
total += 1;
|
|
886
|
-
const worldAabb = box3.create();
|
|
887
1079
|
worldAabb[0] = candidateBounds.min[0] ?? 0;
|
|
888
1080
|
worldAabb[1] = candidateBounds.min[1] ?? 0;
|
|
889
1081
|
worldAabb[2] = candidateBounds.min[2] ?? 0;
|
|
@@ -906,7 +1098,12 @@ function cullPersistentFrame(
|
|
|
906
1098
|
continue;
|
|
907
1099
|
}
|
|
908
1100
|
projectedIndex[index] = visible.length;
|
|
909
|
-
|
|
1101
|
+
// The culling candidate is the persistent projection snapshot used for
|
|
1102
|
+
// bounds, while `frame.renderables[index]` may carry frame-owned
|
|
1103
|
+
// attachments (for example the probe blend record). Preserve that
|
|
1104
|
+
// attached renderable in the output; pushing the bare candidate here
|
|
1105
|
+
// silently drops the object-level Probe ABI before record selection.
|
|
1106
|
+
visible.push(frame.renderables[index] ?? candidate);
|
|
910
1107
|
}
|
|
911
1108
|
|
|
912
1109
|
const dispatch: DispatchEntry[] = [];
|
|
@@ -924,8 +1121,33 @@ function cullPersistentFrame(
|
|
|
924
1121
|
};
|
|
925
1122
|
}
|
|
926
1123
|
|
|
1124
|
+
function attachProbeRecords(
|
|
1125
|
+
frame: ExtractedFrame,
|
|
1126
|
+
candidates: RenderableSnapshot[],
|
|
1127
|
+
slots: readonly RenderSceneSlot[],
|
|
1128
|
+
projection: ProbeBlendSceneProjection,
|
|
1129
|
+
): ExtractedFrame {
|
|
1130
|
+
if (!projection.hasRecords()) {
|
|
1131
|
+
return frame.renderables === candidates ? frame : { ...frame, renderables: candidates };
|
|
1132
|
+
}
|
|
1133
|
+
const objectKeys = new Map<string, number>();
|
|
1134
|
+
for (const slot of slots)
|
|
1135
|
+
objectKeys.set(identityKey(slot.worldId, slot.snapshot.entityKey), slot.slot);
|
|
1136
|
+
const renderables = candidates.map((candidate) => {
|
|
1137
|
+
const slot = objectKeys.get(identityKey(candidate.worldId, candidate.entityKey));
|
|
1138
|
+
const record = slot === undefined ? undefined : projection.getRecord(slot);
|
|
1139
|
+
return record === undefined ? candidate : { ...candidate, probeBlendRecord: record };
|
|
1140
|
+
});
|
|
1141
|
+
return { ...frame, renderables };
|
|
1142
|
+
}
|
|
1143
|
+
|
|
927
1144
|
/** Persistent scene projection used by the renderer's multi-World composition path. */
|
|
928
1145
|
export class PersistentRenderScene {
|
|
1146
|
+
private readonly visibilityBudget: VisibilityBudget;
|
|
1147
|
+
private readonly visibilityFacets: VisibilityFacetStore;
|
|
1148
|
+
/** World identity is the author/runtime identity; array position is only a frame routing detail. */
|
|
1149
|
+
private readonly visibilityWorldKeys = new Map<string, number>();
|
|
1150
|
+
private nextVisibilityWorldKey = 0;
|
|
929
1151
|
private readonly materialSnapshotCaches: MaterialSnapshotCachesByWorld = new WeakMap();
|
|
930
1152
|
private fullRebuilds = 0;
|
|
931
1153
|
private worldEntitiesScanned = 0;
|
|
@@ -942,28 +1164,300 @@ export class PersistentRenderScene {
|
|
|
942
1164
|
private temporalCapture:
|
|
943
1165
|
| { readonly projection: RenderScene; readonly capture: RenderSceneSubmissionCapture }
|
|
944
1166
|
| undefined;
|
|
1167
|
+
/** Monotonic token for camera/dynamic frame facts consumed by visibility caches. */
|
|
1168
|
+
private frameEpoch = 0;
|
|
1169
|
+
private visibilityFacetFrame:
|
|
1170
|
+
| {
|
|
1171
|
+
readonly frameEpoch: number;
|
|
1172
|
+
readonly viewId: string;
|
|
1173
|
+
readonly renderables: readonly RenderableSnapshot[];
|
|
1174
|
+
}
|
|
1175
|
+
| undefined;
|
|
1176
|
+
private visibilityProjectionCache:
|
|
1177
|
+
| {
|
|
1178
|
+
readonly frameEpoch: number;
|
|
1179
|
+
readonly drawRevision: number;
|
|
1180
|
+
readonly viewId: string;
|
|
1181
|
+
readonly renderables: readonly RenderableSnapshot[];
|
|
1182
|
+
readonly dispatch: readonly DispatchEntry[];
|
|
1183
|
+
readonly projection: PersistentVisibilityProjection;
|
|
1184
|
+
}
|
|
1185
|
+
| undefined;
|
|
1186
|
+
private visibilityProjectionRevision = 0;
|
|
1187
|
+
private lastActiveEntityKeys: ReadonlySet<number> | undefined;
|
|
1188
|
+
/** Stable renderer-world/entity lookup; rebuilt only with composition topology. */
|
|
1189
|
+
private readonly stableSlotByEntity = new Map<number, RenderSceneSlot>();
|
|
945
1190
|
private pointsLinesInspections: readonly PointsLinesInspection[] = [];
|
|
946
1191
|
private readonly reflectionProbes = new ReflectionProbeProjection();
|
|
947
1192
|
|
|
948
|
-
constructor(private readonly options: PersistentRenderSceneOptions = {}) {
|
|
1193
|
+
constructor(private readonly options: PersistentRenderSceneOptions = {}) {
|
|
1194
|
+
this.visibilityBudget = createVisibilityBudget(options.configuredQueryBudget);
|
|
1195
|
+
this.visibilityFacets = new VisibilityFacetStore(this.visibilityBudget);
|
|
1196
|
+
}
|
|
949
1197
|
|
|
950
1198
|
/** RenderScene owns the per-World material fact cache used during extraction. */
|
|
951
1199
|
materialSnapshotCacheStore(): MaterialSnapshotCachesByWorld {
|
|
952
1200
|
return this.materialSnapshotCaches;
|
|
953
1201
|
}
|
|
954
1202
|
|
|
1203
|
+
/** The sole renderer-owned ViewKey x PrimitiveKey visibility state. */
|
|
1204
|
+
visibilityFacetStore(): VisibilityFacetStore {
|
|
1205
|
+
return this.visibilityFacets;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
visibilityBudgetValue(): VisibilityBudget {
|
|
1209
|
+
return this.visibilityBudget;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
private visibilityWorldKey(world: World | undefined, fallback: number): number {
|
|
1213
|
+
if (world === undefined) return fallback;
|
|
1214
|
+
const existing = this.visibilityWorldKeys.get(world.identity);
|
|
1215
|
+
if (existing !== undefined) return existing;
|
|
1216
|
+
const key = this.nextVisibilityWorldKey;
|
|
1217
|
+
this.nextVisibilityWorldKey += 1;
|
|
1218
|
+
this.visibilityWorldKeys.set(world.identity, key);
|
|
1219
|
+
return key;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
private worldKeyAt(worlds: readonly World[], worldId: number): number {
|
|
1223
|
+
return this.visibilityWorldKey(worlds[worldId], worldId);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
/** Stable World routing keys for the current worlds[] frame projection. */
|
|
1227
|
+
visibilityWorldKeysFor(worlds: readonly World[]): readonly number[] {
|
|
1228
|
+
return worlds.map((world, worldId) => this.visibilityWorldKey(world, worldId));
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
/** Current retained slots, available to CPU query transport as well as GPU. */
|
|
1232
|
+
compositionSlots(): readonly RenderSceneSlot[] {
|
|
1233
|
+
return this.composition?.projection.slotsSnapshot() ?? [];
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
/** Stable renderer-world/entity lookup shared by query and final projection paths. */
|
|
1237
|
+
compositionSlotByStableEntity(): ReadonlyMap<number, RenderSceneSlot> {
|
|
1238
|
+
return this.stableSlotByEntity;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
private stableEntityKey(
|
|
1242
|
+
worlds: readonly World[],
|
|
1243
|
+
renderable: Pick<RenderableSnapshot, 'worldId' | 'entityKey'>,
|
|
1244
|
+
): number {
|
|
1245
|
+
return worldEntityKey(this.worldKeyAt(worlds, renderable.worldId), renderable.entityKey);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
/** Feed the facet from extracted renderables using persistent slot identity. */
|
|
1249
|
+
updateVisibilityFacet(
|
|
1250
|
+
worlds: readonly World[],
|
|
1251
|
+
camera: CameraSnapshot | undefined,
|
|
1252
|
+
renderables: readonly RenderableSnapshot[],
|
|
1253
|
+
): void {
|
|
1254
|
+
const world = worlds[camera?.worldId ?? 0];
|
|
1255
|
+
if (world === undefined || camera === undefined) return;
|
|
1256
|
+
const view = viewKey({
|
|
1257
|
+
attachmentId: world.identity,
|
|
1258
|
+
cameraEntity: camera.entityKey ?? 0,
|
|
1259
|
+
viewRole: 'main',
|
|
1260
|
+
viewGeneration: camera.historyVersion ?? 0,
|
|
1261
|
+
});
|
|
1262
|
+
const viewId = viewKeyId(view);
|
|
1263
|
+
const previous = this.visibilityFacetFrame;
|
|
1264
|
+
if (
|
|
1265
|
+
previous?.frameEpoch === this.frameEpoch &&
|
|
1266
|
+
previous.viewId === viewId &&
|
|
1267
|
+
previous.renderables === renderables
|
|
1268
|
+
) {
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
this.visibilityFacets.activateView(view);
|
|
1272
|
+
const activePrimitiveIds = new Set<string>();
|
|
1273
|
+
for (const renderable of renderables) {
|
|
1274
|
+
const bounds = renderable.localAabb;
|
|
1275
|
+
if (
|
|
1276
|
+
renderable.lods === undefined ||
|
|
1277
|
+
renderable.lods.length === 0 ||
|
|
1278
|
+
bounds === undefined ||
|
|
1279
|
+
bounds.length < 6 ||
|
|
1280
|
+
[...bounds].some((value) => !Number.isFinite(value))
|
|
1281
|
+
) {
|
|
1282
|
+
continue;
|
|
1283
|
+
}
|
|
1284
|
+
const slot = this.stableSlotByEntity.get(this.stableEntityKey(worlds, renderable));
|
|
1285
|
+
if (slot === undefined) continue;
|
|
1286
|
+
const primitive = primitiveKey({
|
|
1287
|
+
attachmentId: world.identity,
|
|
1288
|
+
worldGeneration: this.worldKeyAt(worlds, renderable.worldId),
|
|
1289
|
+
primitiveSlot: slot.slot,
|
|
1290
|
+
slotGeneration: slot.generation,
|
|
1291
|
+
});
|
|
1292
|
+
activePrimitiveIds.add(primitiveKeyId(primitive));
|
|
1293
|
+
this.visibilityFacets.setCandidate(view, primitive, { level: 0, confidence: 1 });
|
|
1294
|
+
}
|
|
1295
|
+
this.visibilityFacets.pruneCandidates(view, activePrimitiveIds);
|
|
1296
|
+
this.visibilityFacetFrame = { frameEpoch: this.frameEpoch, viewId, renderables };
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
/**
|
|
1300
|
+
* Project the current facet into the final primary-raster submission.
|
|
1301
|
+
*
|
|
1302
|
+
* Only renderables carrying authored lower-detail levels are queryable. A
|
|
1303
|
+
* plain mesh therefore remains on the ordinary visibility path, while an
|
|
1304
|
+
* LOD candidate with two accepted zero-sample results is suppressed until
|
|
1305
|
+
* the confidence scheduler requests its bounded re-test. Unknown identity,
|
|
1306
|
+
* invalid bounds, and non-queryable confidence all remain conservatively
|
|
1307
|
+
* visible through `decideVisibility`.
|
|
1308
|
+
*/
|
|
1309
|
+
projectVisibility(
|
|
1310
|
+
worlds: readonly World[],
|
|
1311
|
+
camera: CameraSnapshot | undefined,
|
|
1312
|
+
renderables: readonly RenderableSnapshot[],
|
|
1313
|
+
dispatch: readonly DispatchEntry[],
|
|
1314
|
+
): PersistentVisibilityProjection {
|
|
1315
|
+
if (camera === undefined) {
|
|
1316
|
+
const activeEntityKeys = new Set(
|
|
1317
|
+
renderables.map((renderable) => this.stableEntityKey(worlds, renderable)),
|
|
1318
|
+
);
|
|
1319
|
+
return {
|
|
1320
|
+
renderables,
|
|
1321
|
+
dispatch,
|
|
1322
|
+
activeEntityKeys,
|
|
1323
|
+
activeEntityRevision: this.activeEntityRevision(activeEntityKeys),
|
|
1324
|
+
suppressed: 0,
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
const cameraWorld = worlds[camera.worldId ?? 0];
|
|
1328
|
+
if (cameraWorld === undefined) {
|
|
1329
|
+
const activeEntityKeys = new Set(
|
|
1330
|
+
renderables.map((renderable) => this.stableEntityKey(worlds, renderable)),
|
|
1331
|
+
);
|
|
1332
|
+
return {
|
|
1333
|
+
renderables,
|
|
1334
|
+
dispatch,
|
|
1335
|
+
activeEntityKeys,
|
|
1336
|
+
activeEntityRevision: this.activeEntityRevision(activeEntityKeys),
|
|
1337
|
+
suppressed: 0,
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
const view = viewKey({
|
|
1341
|
+
attachmentId: cameraWorld.identity,
|
|
1342
|
+
cameraEntity: camera.entityKey ?? 0,
|
|
1343
|
+
viewRole: 'main',
|
|
1344
|
+
viewGeneration: camera.historyVersion ?? 0,
|
|
1345
|
+
});
|
|
1346
|
+
const viewId = viewKeyId(view);
|
|
1347
|
+
const cached = this.visibilityProjectionCache;
|
|
1348
|
+
if (
|
|
1349
|
+
cached?.frameEpoch === this.frameEpoch &&
|
|
1350
|
+
cached.drawRevision === this.visibilityFacets.drawRevisionValue &&
|
|
1351
|
+
cached.viewId === viewId &&
|
|
1352
|
+
cached.renderables === renderables &&
|
|
1353
|
+
cached.dispatch === dispatch
|
|
1354
|
+
) {
|
|
1355
|
+
return cached.projection;
|
|
1356
|
+
}
|
|
1357
|
+
const projectedIndex = new Int32Array(renderables.length);
|
|
1358
|
+
projectedIndex.fill(-1);
|
|
1359
|
+
const projected: RenderableSnapshot[] = [];
|
|
1360
|
+
const activeEntityKeys = new Set<number>();
|
|
1361
|
+
let suppressed = 0;
|
|
1362
|
+
for (let index = 0; index < renderables.length; index += 1) {
|
|
1363
|
+
const renderable = renderables[index];
|
|
1364
|
+
if (renderable === undefined) continue;
|
|
1365
|
+
const slot = this.stableSlotByEntity.get(this.stableEntityKey(worlds, renderable));
|
|
1366
|
+
let draw = true;
|
|
1367
|
+
if (slot !== undefined && (renderable.lods?.length ?? 0) > 0) {
|
|
1368
|
+
const primitive = primitiveKey({
|
|
1369
|
+
attachmentId: cameraWorld.identity,
|
|
1370
|
+
worldGeneration: this.worldKeyAt(worlds, renderable.worldId),
|
|
1371
|
+
primitiveSlot: slot.slot,
|
|
1372
|
+
slotGeneration: slot.generation,
|
|
1373
|
+
});
|
|
1374
|
+
const bounds = renderable.localAabb;
|
|
1375
|
+
const validBounds =
|
|
1376
|
+
bounds !== undefined &&
|
|
1377
|
+
bounds.length >= 6 &&
|
|
1378
|
+
[...bounds].every((value) => Number.isFinite(value));
|
|
1379
|
+
draw = decideVisibility({
|
|
1380
|
+
authorVisible: true,
|
|
1381
|
+
validBounds,
|
|
1382
|
+
frustumVisible: true,
|
|
1383
|
+
lodReady: true,
|
|
1384
|
+
occlusion: this.visibilityFacets.getConfidence(view, primitive),
|
|
1385
|
+
lane: 'gpu',
|
|
1386
|
+
}).draw;
|
|
1387
|
+
}
|
|
1388
|
+
if (!draw) {
|
|
1389
|
+
suppressed += 1;
|
|
1390
|
+
continue;
|
|
1391
|
+
}
|
|
1392
|
+
projectedIndex[index] = projected.length;
|
|
1393
|
+
projected.push(renderable);
|
|
1394
|
+
activeEntityKeys.add(this.stableEntityKey(worlds, renderable));
|
|
1395
|
+
}
|
|
1396
|
+
const projectedDispatch: DispatchEntry[] = [];
|
|
1397
|
+
for (const entry of dispatch) {
|
|
1398
|
+
const renderableIndex = projectedIndex[entry.renderableIndex];
|
|
1399
|
+
if (renderableIndex === undefined || renderableIndex < 0) continue;
|
|
1400
|
+
projectedDispatch.push({ ...entry, renderableIndex });
|
|
1401
|
+
}
|
|
1402
|
+
const projection = {
|
|
1403
|
+
renderables: Object.freeze(projected),
|
|
1404
|
+
dispatch: Object.freeze(projectedDispatch),
|
|
1405
|
+
activeEntityKeys,
|
|
1406
|
+
activeEntityRevision: this.activeEntityRevision(activeEntityKeys),
|
|
1407
|
+
suppressed,
|
|
1408
|
+
};
|
|
1409
|
+
this.visibilityProjectionCache = {
|
|
1410
|
+
frameEpoch: this.frameEpoch,
|
|
1411
|
+
drawRevision: this.visibilityFacets.drawRevisionValue,
|
|
1412
|
+
viewId,
|
|
1413
|
+
renderables,
|
|
1414
|
+
dispatch,
|
|
1415
|
+
projection,
|
|
1416
|
+
};
|
|
1417
|
+
return projection;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* Advance the projection revision only when the active stable-entity set
|
|
1422
|
+
* changes. Camera fingerprints and scene-plan identity cover view/topology
|
|
1423
|
+
* changes; keeping this token set-stable preserves the GPU filtered-plan
|
|
1424
|
+
* cache on ordinary no-change frames.
|
|
1425
|
+
*/
|
|
1426
|
+
private activeEntityRevision(activeEntityKeys: ReadonlySet<number>): number {
|
|
1427
|
+
const previous = this.lastActiveEntityKeys;
|
|
1428
|
+
if (
|
|
1429
|
+
previous === undefined ||
|
|
1430
|
+
previous.size !== activeEntityKeys.size ||
|
|
1431
|
+
[...previous].some((key) => !activeEntityKeys.has(key))
|
|
1432
|
+
) {
|
|
1433
|
+
this.lastActiveEntityKeys = new Set(activeEntityKeys);
|
|
1434
|
+
this.visibilityProjectionRevision += 1;
|
|
1435
|
+
}
|
|
1436
|
+
return this.visibilityProjectionRevision;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
private invalidateActiveEntityRevision(): void {
|
|
1440
|
+
this.lastActiveEntityKeys = undefined;
|
|
1441
|
+
this.visibilityProjectionRevision += 1;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
955
1444
|
/** Project probe facts and per-primitive selection into the persistent scene owner. */
|
|
956
1445
|
projectReflectionProbes(probes: readonly ReflectionProbeFact[]): void {
|
|
957
1446
|
const projection = this.composition?.projection;
|
|
958
1447
|
if (projection === undefined) return;
|
|
1448
|
+
if (probes.length === 0) {
|
|
1449
|
+
this.reflectionProbes.update([], []);
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
959
1452
|
const primitives = projection
|
|
960
1453
|
.slotsSnapshot()
|
|
961
1454
|
.map((slot) => {
|
|
962
|
-
const resolved =
|
|
1455
|
+
const resolved = projection.cullingWorldBoundsAt(slot);
|
|
963
1456
|
if (resolved === undefined || resolved === null) return undefined;
|
|
964
1457
|
return {
|
|
965
1458
|
worldId: slot.worldId,
|
|
966
1459
|
entityKey: slot.entityKey,
|
|
1460
|
+
renderableKey: `${slot.worldId}:${slot.entityKey}`,
|
|
967
1461
|
center: [
|
|
968
1462
|
(resolved.min[0] + resolved.max[0]) / 2,
|
|
969
1463
|
(resolved.min[1] + resolved.max[1]) / 2,
|
|
@@ -987,11 +1481,32 @@ export class PersistentRenderScene {
|
|
|
987
1481
|
worlds: readonly World[],
|
|
988
1482
|
owner: { readonly cameraOwner: number; readonly resourceOwner: number },
|
|
989
1483
|
catalogEpoch: number,
|
|
990
|
-
buildCandidateFrame: () => ExtractedFrame,
|
|
1484
|
+
buildCandidateFrame: (renderables: 'full' | 'none') => ExtractedFrame,
|
|
991
1485
|
leases?: readonly RenderReadLease[],
|
|
992
1486
|
): ExtractedFrame {
|
|
993
1487
|
this.worldEntitiesScanned = 0;
|
|
994
1488
|
const entry = this.composition;
|
|
1489
|
+
const sameWorldSet =
|
|
1490
|
+
entry !== undefined &&
|
|
1491
|
+
entry.worlds.length === worlds.length &&
|
|
1492
|
+
entry.worlds.every((world) => worlds.includes(world));
|
|
1493
|
+
const worldOrderChanged =
|
|
1494
|
+
entry !== undefined &&
|
|
1495
|
+
sameWorldSet &&
|
|
1496
|
+
entry.worlds.some((world, index) => world !== worlds[index]);
|
|
1497
|
+
const leaseSetPreserved =
|
|
1498
|
+
entry !== undefined &&
|
|
1499
|
+
leases !== undefined &&
|
|
1500
|
+
leases.length === worlds.length &&
|
|
1501
|
+
entry.leaseIdentities.length === leases.length &&
|
|
1502
|
+
entry.leaseIdentities.every((identity) =>
|
|
1503
|
+
leases.some((lease) => lease.worldIdentity === identity),
|
|
1504
|
+
);
|
|
1505
|
+
const preserveVisibilityOnReorder =
|
|
1506
|
+
entry !== undefined &&
|
|
1507
|
+
worldOrderChanged &&
|
|
1508
|
+
entry.catalogEpoch === catalogEpoch &&
|
|
1509
|
+
leaseSetPreserved;
|
|
995
1510
|
if (
|
|
996
1511
|
entry === undefined ||
|
|
997
1512
|
entry.catalogEpoch !== catalogEpoch ||
|
|
@@ -1002,13 +1517,19 @@ export class PersistentRenderScene {
|
|
|
1002
1517
|
leases === undefined ||
|
|
1003
1518
|
leases.length !== worlds.length
|
|
1004
1519
|
) {
|
|
1005
|
-
return this.rebuildComposition(
|
|
1520
|
+
return this.rebuildComposition(
|
|
1521
|
+
worlds,
|
|
1522
|
+
owner,
|
|
1523
|
+
catalogEpoch,
|
|
1524
|
+
buildCandidateFrame,
|
|
1525
|
+
leases,
|
|
1526
|
+
preserveVisibilityOnReorder,
|
|
1527
|
+
);
|
|
1006
1528
|
}
|
|
1007
1529
|
if (!entry.rigidOnly) {
|
|
1008
1530
|
return this.rebuildComposition(worlds, owner, catalogEpoch, buildCandidateFrame, leases);
|
|
1009
1531
|
}
|
|
1010
1532
|
|
|
1011
|
-
const operations: RenderSceneOperation[] = [];
|
|
1012
1533
|
let requiresRebuild = false;
|
|
1013
1534
|
for (let worldId = 0; worldId < worlds.length; worldId += 1) {
|
|
1014
1535
|
const world = worlds[worldId];
|
|
@@ -1018,14 +1539,19 @@ export class PersistentRenderScene {
|
|
|
1018
1539
|
requiresRebuild = true;
|
|
1019
1540
|
break;
|
|
1020
1541
|
}
|
|
1542
|
+
const version = entry.readVersions[worldId];
|
|
1543
|
+
if (version === undefined) {
|
|
1544
|
+
requiresRebuild = true;
|
|
1545
|
+
break;
|
|
1546
|
+
}
|
|
1021
1547
|
let read: ReturnType<RenderReadLease['readChanges']>;
|
|
1022
1548
|
try {
|
|
1023
|
-
read = lease.readChanges(
|
|
1549
|
+
read = lease.readChanges(version);
|
|
1024
1550
|
} catch {
|
|
1025
1551
|
requiresRebuild = true;
|
|
1026
1552
|
break;
|
|
1027
1553
|
}
|
|
1028
|
-
if (read.status
|
|
1554
|
+
if (read.status !== 'ok') {
|
|
1029
1555
|
requiresRebuild = true;
|
|
1030
1556
|
break;
|
|
1031
1557
|
}
|
|
@@ -1038,119 +1564,86 @@ export class PersistentRenderScene {
|
|
|
1038
1564
|
requiresRebuild = true;
|
|
1039
1565
|
break;
|
|
1040
1566
|
}
|
|
1041
|
-
const
|
|
1042
|
-
|
|
1043
|
-
if (
|
|
1044
|
-
record.kind === 'derived-component-changed' &&
|
|
1045
|
-
record.componentId === componentId(Transform)
|
|
1046
|
-
) {
|
|
1047
|
-
derivedTransforms.add(record.entity as EntityHandle);
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
for (const record of changes.records) {
|
|
1051
|
-
if (
|
|
1052
|
-
record.kind !== 'entity-removed' &&
|
|
1053
|
-
record.componentId !== undefined &&
|
|
1054
|
-
!RENDER_RELEVANT_COMPONENT_IDS.has(record.componentId)
|
|
1055
|
-
) {
|
|
1056
|
-
continue;
|
|
1057
|
-
}
|
|
1058
|
-
const authoredTransform =
|
|
1059
|
-
record.kind === 'component-changed' && record.componentId === componentId(Transform);
|
|
1060
|
-
const derivedTransform =
|
|
1061
|
-
record.kind === 'derived-component-changed' &&
|
|
1062
|
-
record.componentId === componentId(Transform);
|
|
1063
|
-
if (derivedTransform) continue;
|
|
1064
|
-
if (authoredTransform && derivedTransforms.has(record.entity as EntityHandle)) continue;
|
|
1567
|
+
for (const changedComponentId of changes.changedComponentIds) {
|
|
1568
|
+
if (!RENDER_RELEVANT_COMPONENT_IDS.has(changedComponentId)) continue;
|
|
1065
1569
|
if (
|
|
1066
|
-
|
|
1067
|
-
|
|
1570
|
+
changedComponentId !== componentId(Transform) &&
|
|
1571
|
+
changedComponentId !== componentId(GlobalTransform)
|
|
1068
1572
|
) {
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
requiresRebuild = true;
|
|
1072
|
-
break;
|
|
1073
|
-
}
|
|
1074
|
-
operations.push({
|
|
1075
|
-
kind: 'update-transform',
|
|
1076
|
-
worldId,
|
|
1077
|
-
entityKey: record.entity,
|
|
1078
|
-
world: new Float32Array(worldView),
|
|
1079
|
-
});
|
|
1080
|
-
continue;
|
|
1573
|
+
requiresRebuild = true;
|
|
1574
|
+
break;
|
|
1081
1575
|
}
|
|
1082
|
-
requiresRebuild = true;
|
|
1083
|
-
break;
|
|
1084
1576
|
}
|
|
1085
|
-
if (
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
}
|
|
1101
|
-
// Camera snapshots are rebuilt from the current candidate frame and
|
|
1102
|
-
// drive culling there; moving a camera must not invalidate the
|
|
1103
|
-
// retained scene projection or recreate its GPU tables.
|
|
1104
|
-
if (
|
|
1105
|
-
!world.hasComponent(entity, Camera) &&
|
|
1106
|
-
NON_RENDERABLE_TRANSFORM_CONSUMERS.some((component) =>
|
|
1107
|
-
world.hasComponent(entity, component),
|
|
1108
|
-
)
|
|
1109
|
-
) {
|
|
1110
|
-
requiresRebuild = true;
|
|
1111
|
-
break;
|
|
1112
|
-
}
|
|
1577
|
+
if (requiresRebuild) break;
|
|
1578
|
+
entry.readVersions[worldId] = read.version;
|
|
1579
|
+
}
|
|
1580
|
+
if (requiresRebuild) {
|
|
1581
|
+
return this.rebuildComposition(worlds, owner, catalogEpoch, buildCandidateFrame, leases);
|
|
1582
|
+
}
|
|
1583
|
+
const transformSpans: WorldTransformSpan[] = [];
|
|
1584
|
+
for (let worldId = 0; worldId < entry.transformQueries.length; worldId += 1) {
|
|
1585
|
+
const query = entry.transformQueries[worldId];
|
|
1586
|
+
if (query === undefined) continue;
|
|
1587
|
+
for (const span of query.spans().unwrap()) {
|
|
1588
|
+
const changedWorlds = span.get(GlobalTransform).world;
|
|
1589
|
+
if (!(changedWorlds instanceof Float32Array)) {
|
|
1590
|
+
requiresRebuild = true;
|
|
1591
|
+
break;
|
|
1113
1592
|
}
|
|
1593
|
+
transformSpans.push({ worldId, entities: span.entities, worlds: changedWorlds });
|
|
1114
1594
|
}
|
|
1115
1595
|
if (requiresRebuild) break;
|
|
1116
|
-
entry.readCursors[worldId] = read.cursor;
|
|
1117
1596
|
}
|
|
1118
1597
|
if (requiresRebuild) {
|
|
1119
1598
|
return this.rebuildComposition(worlds, owner, catalogEpoch, buildCandidateFrame, leases);
|
|
1120
1599
|
}
|
|
1121
|
-
if (
|
|
1600
|
+
if (transformSpans.length === 0) {
|
|
1122
1601
|
this.noChangeFrames += 1;
|
|
1123
1602
|
this.syncGpuOwner(entry.token, entry.projection);
|
|
1124
1603
|
return this.deriveFramePlan(entry, buildCandidateFrame);
|
|
1125
1604
|
}
|
|
1126
|
-
const delta = entry.projection.
|
|
1605
|
+
const delta = entry.projection.applyTransformSpans(transformSpans);
|
|
1127
1606
|
entry.topology.apply(delta);
|
|
1128
1607
|
this.syncGpuOwner(entry.token, entry.projection, delta);
|
|
1129
1608
|
this.deltaFrames += 1;
|
|
1130
|
-
this.transformUpdates +=
|
|
1609
|
+
this.transformUpdates += delta.updated;
|
|
1131
1610
|
return this.deriveFramePlan(entry, buildCandidateFrame);
|
|
1132
1611
|
}
|
|
1133
1612
|
|
|
1134
1613
|
invalidate(): void {
|
|
1135
1614
|
this.composition = undefined;
|
|
1136
1615
|
this.temporalCapture = undefined;
|
|
1616
|
+
this.visibilityFacets.clear();
|
|
1617
|
+
this.invalidateActiveEntityRevision();
|
|
1618
|
+
this.visibilityFacetFrame = undefined;
|
|
1619
|
+
this.visibilityProjectionCache = undefined;
|
|
1620
|
+
this.stableSlotByEntity.clear();
|
|
1137
1621
|
this.lastResyncReason = 'explicit-invalidate';
|
|
1138
1622
|
}
|
|
1139
1623
|
|
|
1140
|
-
|
|
1624
|
+
setCompositionGpuSceneDemand(sceneRowsRequired: boolean): void {
|
|
1141
1625
|
const entry = this.composition;
|
|
1142
1626
|
if (entry === undefined) return;
|
|
1143
|
-
entry.
|
|
1144
|
-
entry.gpuDrivenOwnsAll = ownsAll;
|
|
1627
|
+
entry.gpuDrivenSceneRequired = sceneRowsRequired;
|
|
1145
1628
|
}
|
|
1146
1629
|
|
|
1147
1630
|
detach(world: World): void {
|
|
1631
|
+
this.visibilityFacets.detachAttachment(world.identity);
|
|
1148
1632
|
this.materialSnapshotCaches.delete(world);
|
|
1149
1633
|
const detachedComposition = this.composition?.worlds.includes(world)
|
|
1150
1634
|
? this.composition
|
|
1151
1635
|
: undefined;
|
|
1152
1636
|
if (detachedComposition !== undefined) this.composition = undefined;
|
|
1153
1637
|
if (detachedComposition !== undefined) this.temporalCapture = undefined;
|
|
1638
|
+
if (detachedComposition !== undefined) {
|
|
1639
|
+
this.visibilityFacets.clear();
|
|
1640
|
+
this.invalidateActiveEntityRevision();
|
|
1641
|
+
}
|
|
1642
|
+
if (detachedComposition !== undefined) {
|
|
1643
|
+
this.visibilityFacetFrame = undefined;
|
|
1644
|
+
this.visibilityProjectionCache = undefined;
|
|
1645
|
+
this.stableSlotByEntity.clear();
|
|
1646
|
+
}
|
|
1154
1647
|
if (this.gpuOwner !== world && this.gpuOwner !== detachedComposition?.token) return;
|
|
1155
1648
|
this.gpuScene?.dispose();
|
|
1156
1649
|
this.gpuScene = undefined;
|
|
@@ -1161,7 +1654,7 @@ export class PersistentRenderScene {
|
|
|
1161
1654
|
|
|
1162
1655
|
/** Drop only device-owned tables; the CPU projection remains the recovery authority. */
|
|
1163
1656
|
resetGpuForRecover(): void {
|
|
1164
|
-
if (this.composition !== undefined) this.composition.
|
|
1657
|
+
if (this.composition !== undefined) this.composition.gpuDrivenSceneRequired = true;
|
|
1165
1658
|
this.gpuScene = undefined;
|
|
1166
1659
|
this.gpuOwner = undefined;
|
|
1167
1660
|
this.gpuDevice = undefined;
|
|
@@ -1184,6 +1677,11 @@ export class PersistentRenderScene {
|
|
|
1184
1677
|
this.gpuStatus = 'inactive';
|
|
1185
1678
|
this.composition = undefined;
|
|
1186
1679
|
this.temporalCapture = undefined;
|
|
1680
|
+
this.visibilityFacets.clear();
|
|
1681
|
+
this.invalidateActiveEntityRevision();
|
|
1682
|
+
this.visibilityFacetFrame = undefined;
|
|
1683
|
+
this.visibilityProjectionCache = undefined;
|
|
1684
|
+
this.stableSlotByEntity.clear();
|
|
1187
1685
|
this.pointsLinesInspections = [];
|
|
1188
1686
|
}
|
|
1189
1687
|
|
|
@@ -1198,6 +1696,7 @@ export class PersistentRenderScene {
|
|
|
1198
1696
|
transformUpdates: this.transformUpdates,
|
|
1199
1697
|
lastResyncReason: this.lastResyncReason,
|
|
1200
1698
|
projectionRecords: entry?.projection.inspect().records.length ?? 0,
|
|
1699
|
+
...(entry === undefined ? {} : { probeBlend: entry.probeProjection.inspect() }),
|
|
1201
1700
|
topology:
|
|
1202
1701
|
entry?.topology.inspect() ??
|
|
1203
1702
|
({
|
|
@@ -1222,12 +1721,15 @@ export class PersistentRenderScene {
|
|
|
1222
1721
|
scene: this.gpuScene,
|
|
1223
1722
|
plan: entry.topology.plan(),
|
|
1224
1723
|
slots: entry.projection.slotsSnapshot(),
|
|
1724
|
+
worldKeys: entry.worldKeys,
|
|
1225
1725
|
};
|
|
1226
1726
|
}
|
|
1227
1727
|
|
|
1228
1728
|
/** Publish renderer-owned previous transforms after queue submission. */
|
|
1229
1729
|
commitTemporalFrame(): Result<void, RhiError> {
|
|
1230
|
-
|
|
1730
|
+
// Previous transforms are consumed only by TAA and motion blur. Ordinary
|
|
1731
|
+
// frames must not copy and upload the complete GPU transform table.
|
|
1732
|
+
const gpuCommit = this.gpuScene?.commitTemporalFrame(this.temporalCapture !== undefined);
|
|
1231
1733
|
if (gpuCommit !== undefined && !gpuCommit.ok) return gpuCommit;
|
|
1232
1734
|
const temporalCapture = this.temporalCapture;
|
|
1233
1735
|
if (temporalCapture !== undefined) {
|
|
@@ -1279,19 +1781,76 @@ export class PersistentRenderScene {
|
|
|
1279
1781
|
worlds: readonly World[],
|
|
1280
1782
|
owner: { readonly cameraOwner: number; readonly resourceOwner: number },
|
|
1281
1783
|
catalogEpoch: number,
|
|
1282
|
-
buildCandidateFrame: () => ExtractedFrame,
|
|
1784
|
+
buildCandidateFrame: (renderables: 'full' | 'none') => ExtractedFrame,
|
|
1283
1785
|
leases?: readonly RenderReadLease[],
|
|
1786
|
+
preserveVisibilityOnReorder = false,
|
|
1284
1787
|
): ExtractedFrame {
|
|
1285
|
-
const
|
|
1788
|
+
const previousEntry = this.composition;
|
|
1789
|
+
const candidateFrame = buildCandidateFrame('full');
|
|
1286
1790
|
this.temporalCapture = undefined;
|
|
1287
1791
|
this.worldEntitiesScanned = candidateFrame.renderables.length;
|
|
1792
|
+
const worldKeys = this.visibilityWorldKeysFor(worlds);
|
|
1793
|
+
if (!preserveVisibilityOnReorder) {
|
|
1794
|
+
this.visibilityFacets.clear();
|
|
1795
|
+
this.invalidateActiveEntityRevision();
|
|
1796
|
+
} else if (previousEntry !== undefined) {
|
|
1797
|
+
const previousKeys = new Set(
|
|
1798
|
+
previousEntry.projection
|
|
1799
|
+
.slotsSnapshot()
|
|
1800
|
+
.map((slot) =>
|
|
1801
|
+
worldEntityKey(previousEntry.worldKeys[slot.worldId] ?? slot.worldId, slot.entityKey),
|
|
1802
|
+
),
|
|
1803
|
+
);
|
|
1804
|
+
const currentKeys = new Set(
|
|
1805
|
+
candidateFrame.renderables.map((renderable) =>
|
|
1806
|
+
worldEntityKey(worldKeys[renderable.worldId] ?? renderable.worldId, renderable.entityKey),
|
|
1807
|
+
),
|
|
1808
|
+
);
|
|
1809
|
+
if (
|
|
1810
|
+
previousKeys.size !== currentKeys.size ||
|
|
1811
|
+
[...previousKeys].some((key) => !currentKeys.has(key))
|
|
1812
|
+
) {
|
|
1813
|
+
this.visibilityFacets.clear();
|
|
1814
|
+
this.invalidateActiveEntityRevision();
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
// Slot allocation is renderer-owned identity. Sort by stable World identity
|
|
1818
|
+
// before allocating so a worlds[] reorder does not silently change a
|
|
1819
|
+
// primitive's slot/generation. Dispatch is remapped below to the sorted
|
|
1820
|
+
// renderable projection and remains frame-order independent.
|
|
1821
|
+
const sorted = candidateFrame.renderables
|
|
1822
|
+
.map((snapshot, sourceIndex) => ({ snapshot, sourceIndex }))
|
|
1823
|
+
.sort((left, right) => {
|
|
1824
|
+
const leftWorld = worldKeys[left.snapshot.worldId] ?? left.snapshot.worldId;
|
|
1825
|
+
const rightWorld = worldKeys[right.snapshot.worldId] ?? right.snapshot.worldId;
|
|
1826
|
+
return leftWorld - rightWorld || left.snapshot.entityKey - right.snapshot.entityKey;
|
|
1827
|
+
});
|
|
1828
|
+
const sortedRenderables = sorted.map(({ snapshot }) => snapshot);
|
|
1829
|
+
const renderableIndex = new Int32Array(candidateFrame.renderables.length);
|
|
1830
|
+
for (let index = 0; index < sorted.length; index += 1) {
|
|
1831
|
+
const sourceIndex = sorted[index]?.sourceIndex;
|
|
1832
|
+
if (sourceIndex !== undefined) renderableIndex[sourceIndex] = index;
|
|
1833
|
+
}
|
|
1834
|
+
const sortedDispatch = candidateFrame.dispatch.flatMap((entry) => {
|
|
1835
|
+
const index = renderableIndex[entry.renderableIndex];
|
|
1836
|
+
return index === undefined ? [] : [{ ...entry, renderableIndex: index }];
|
|
1837
|
+
});
|
|
1838
|
+
const stableCandidateFrame = {
|
|
1839
|
+
...candidateFrame,
|
|
1840
|
+
renderables: sortedRenderables,
|
|
1841
|
+
dispatch: sortedDispatch,
|
|
1842
|
+
};
|
|
1288
1843
|
const projection = new RenderScene();
|
|
1289
|
-
projection.reset(
|
|
1844
|
+
projection.reset(sortedRenderables);
|
|
1845
|
+
this.stableSlotByEntity.clear();
|
|
1846
|
+
for (const slot of projection.slotsSnapshot()) {
|
|
1847
|
+
const stableWorld = worldKeys[slot.worldId] ?? slot.worldId;
|
|
1848
|
+
this.stableSlotByEntity.set(worldEntityKey(stableWorld, slot.entityKey), slot);
|
|
1849
|
+
}
|
|
1290
1850
|
const transmissionDemand = new PersistentTransmissionDemandProjection();
|
|
1291
1851
|
transmissionDemand.apply(transmissionDemandOperations(candidateFrame.renderables));
|
|
1292
1852
|
const topology = new BatchTopology();
|
|
1293
1853
|
topology.rebuild(projection.slotsSnapshot());
|
|
1294
|
-
this.projectReflectionProbes(candidateFrame.reflectionProbes ?? []);
|
|
1295
1854
|
const token = {};
|
|
1296
1855
|
const entry: PersistentCompositionEntry = {
|
|
1297
1856
|
token,
|
|
@@ -1301,31 +1860,69 @@ export class PersistentRenderScene {
|
|
|
1301
1860
|
projection,
|
|
1302
1861
|
topology,
|
|
1303
1862
|
transmissionDemand,
|
|
1304
|
-
|
|
1863
|
+
structure: {
|
|
1864
|
+
dispatch: stableCandidateFrame.dispatch,
|
|
1865
|
+
visibilityStats: stableCandidateFrame.visibilityStats,
|
|
1866
|
+
visibilitySnapshots: stableCandidateFrame.visibilitySnapshots,
|
|
1867
|
+
featureVisibilitySnapshots: stableCandidateFrame.featureVisibilitySnapshots,
|
|
1868
|
+
hiddenEntityReports: stableCandidateFrame.hiddenEntityReports,
|
|
1869
|
+
},
|
|
1870
|
+
worldKeys,
|
|
1871
|
+
readVersions:
|
|
1305
1872
|
leases === undefined || leases.length !== worlds.length
|
|
1306
1873
|
? []
|
|
1307
|
-
: leases.map((lease) => lease.
|
|
1874
|
+
: leases.map((lease) => lease.captureVersion()),
|
|
1308
1875
|
leaseIdentities:
|
|
1309
1876
|
leases === undefined || leases.length !== worlds.length
|
|
1310
1877
|
? []
|
|
1311
1878
|
: leases.map((lease) => lease.worldIdentity),
|
|
1879
|
+
transformQueries:
|
|
1880
|
+
leases === undefined || leases.length !== worlds.length
|
|
1881
|
+
? []
|
|
1882
|
+
: worlds.map(createGlobalTransformChangeQuery),
|
|
1312
1883
|
catalogEpoch,
|
|
1313
1884
|
rigidOnly: candidateFrame.renderables.every((renderable) => renderable.skin === undefined),
|
|
1314
|
-
|
|
1315
|
-
|
|
1885
|
+
probeProjection: new ProbeBlendSceneProjection(),
|
|
1886
|
+
temporalDemanded: frameDemandsTemporal(candidateFrame),
|
|
1887
|
+
gpuDrivenSceneRequired: true,
|
|
1316
1888
|
};
|
|
1317
1889
|
this.composition = entry;
|
|
1890
|
+
projection.setTemporalTracking(entry.temporalDemanded);
|
|
1891
|
+
// The projection must be installed before deriving probe primitives. On
|
|
1892
|
+
// the first composition rebuild `projectReflectionProbes` otherwise sees
|
|
1893
|
+
// no composition and silently drops every per-renderable selection,
|
|
1894
|
+
// leaving the producer on its neutral/no-demand path until an unrelated
|
|
1895
|
+
// later rebuild.
|
|
1896
|
+
this.projectReflectionProbes(candidateFrame.reflectionProbes ?? []);
|
|
1897
|
+
this.projectProbes(entry, candidateFrame);
|
|
1318
1898
|
this.syncGpuOwner(token, projection, undefined, true);
|
|
1319
1899
|
this.fullRebuilds += 1;
|
|
1320
1900
|
this.lastResyncReason = 'attach';
|
|
1321
|
-
const
|
|
1322
|
-
|
|
1323
|
-
.map((snapshot) => projection.withTemporal(snapshot))
|
|
1324
|
-
|
|
1325
|
-
|
|
1901
|
+
const materialized = projection.materialize();
|
|
1902
|
+
const candidates = entry.temporalDemanded
|
|
1903
|
+
? materialized.map((snapshot) => projection.withTemporal(snapshot))
|
|
1904
|
+
: materialized;
|
|
1905
|
+
const frameWithProbes = attachProbeRecords(
|
|
1906
|
+
{ ...stableCandidateFrame, renderables: candidates },
|
|
1326
1907
|
candidates,
|
|
1327
|
-
|
|
1908
|
+
projection.slotsSnapshot(),
|
|
1909
|
+
entry.probeProjection,
|
|
1910
|
+
);
|
|
1911
|
+
const slots = projection.slotsSnapshot();
|
|
1912
|
+
const result = cullPersistentFrame(
|
|
1913
|
+
frameWithProbes,
|
|
1914
|
+
frameWithProbes.renderables,
|
|
1915
|
+
(snapshot, index) => {
|
|
1916
|
+
const slot = slots[index];
|
|
1917
|
+
return slot === undefined
|
|
1918
|
+
? projection.cullingWorldBounds(snapshot)
|
|
1919
|
+
: projection.cullingWorldBoundsAt(slot);
|
|
1920
|
+
},
|
|
1328
1921
|
);
|
|
1922
|
+
this.frameEpoch += 1;
|
|
1923
|
+
this.visibilityFacetFrame = undefined;
|
|
1924
|
+
this.visibilityProjectionCache = undefined;
|
|
1925
|
+
return result;
|
|
1329
1926
|
}
|
|
1330
1927
|
|
|
1331
1928
|
/** Internal topology fact; the renderer does not expose demand as public API. */
|
|
@@ -1340,22 +1937,89 @@ export class PersistentRenderScene {
|
|
|
1340
1937
|
|
|
1341
1938
|
/**
|
|
1342
1939
|
* Derive an ephemeral frame plan from persistent scene facts. The scene
|
|
1343
|
-
* retains identity/topology/
|
|
1940
|
+
* retains identity/topology/read versions; no prior frame object is an authority.
|
|
1344
1941
|
*/
|
|
1345
1942
|
private deriveFramePlan(
|
|
1346
1943
|
entry: PersistentCompositionEntry,
|
|
1347
|
-
buildCandidateFrame: () => ExtractedFrame,
|
|
1944
|
+
buildCandidateFrame: (renderables: 'full' | 'none') => ExtractedFrame,
|
|
1348
1945
|
): ExtractedFrame {
|
|
1349
|
-
const
|
|
1946
|
+
const resourceFrame = buildCandidateFrame('none');
|
|
1947
|
+
const candidateFrame: ExtractedFrame = { ...resourceFrame, ...entry.structure };
|
|
1948
|
+
entry.temporalDemanded = frameDemandsTemporal(candidateFrame);
|
|
1949
|
+
entry.projection.setTemporalTracking(entry.temporalDemanded);
|
|
1950
|
+
this.projectProbes(entry, candidateFrame);
|
|
1350
1951
|
this.projectReflectionProbes(candidateFrame.reflectionProbes ?? []);
|
|
1351
|
-
const
|
|
1352
|
-
|
|
1353
|
-
.map((snapshot) => entry.projection.withTemporal(snapshot))
|
|
1354
|
-
|
|
1355
|
-
|
|
1952
|
+
const materialized = entry.projection.materialize();
|
|
1953
|
+
const candidates = entry.temporalDemanded
|
|
1954
|
+
? materialized.map((snapshot) => entry.projection.withTemporal(snapshot))
|
|
1955
|
+
: materialized;
|
|
1956
|
+
const frameWithProbes = attachProbeRecords(
|
|
1957
|
+
{ ...candidateFrame, renderables: candidates },
|
|
1356
1958
|
candidates,
|
|
1357
|
-
|
|
1959
|
+
entry.projection.slotsSnapshot(),
|
|
1960
|
+
entry.probeProjection,
|
|
1961
|
+
);
|
|
1962
|
+
const slots = entry.projection.slotsSnapshot();
|
|
1963
|
+
const result = cullPersistentFrame(
|
|
1964
|
+
frameWithProbes,
|
|
1965
|
+
frameWithProbes.renderables,
|
|
1966
|
+
(snapshot, index) => {
|
|
1967
|
+
const slot = slots[index];
|
|
1968
|
+
return slot === undefined
|
|
1969
|
+
? entry.projection.cullingWorldBounds(snapshot)
|
|
1970
|
+
: entry.projection.cullingWorldBoundsAt(slot);
|
|
1971
|
+
},
|
|
1358
1972
|
);
|
|
1973
|
+
this.frameEpoch += 1;
|
|
1974
|
+
this.visibilityFacetFrame = undefined;
|
|
1975
|
+
this.visibilityProjectionCache = undefined;
|
|
1976
|
+
return result;
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
private projectProbes(entry: PersistentCompositionEntry, frame: ExtractedFrame): void {
|
|
1980
|
+
const sky =
|
|
1981
|
+
frame.skylight === undefined
|
|
1982
|
+
? {
|
|
1983
|
+
available: false,
|
|
1984
|
+
irradiance: [0, 0, 0] as const,
|
|
1985
|
+
fallbackReason: 'no-skylight',
|
|
1986
|
+
}
|
|
1987
|
+
: {
|
|
1988
|
+
available: true,
|
|
1989
|
+
identity: `skylight:${frame.skylight.entityHandle}`,
|
|
1990
|
+
sourceKey:
|
|
1991
|
+
frame.skylight.equirectHandle > 0
|
|
1992
|
+
? `equirect:${frame.skylight.equirectHandle}`
|
|
1993
|
+
: `skylight:${frame.skylight.entityHandle}`,
|
|
1994
|
+
irradiance: [
|
|
1995
|
+
frame.skylight.color[0] * frame.skylight.intensity,
|
|
1996
|
+
frame.skylight.color[1] * frame.skylight.intensity,
|
|
1997
|
+
frame.skylight.color[2] * frame.skylight.intensity,
|
|
1998
|
+
] as [number, number, number],
|
|
1999
|
+
};
|
|
2000
|
+
const probes = frame.lightProbes ?? [];
|
|
2001
|
+
// Sky is evaluated by the global environment path. With no local light
|
|
2002
|
+
// probes there can be no per-object ProbeBlendRecord, regardless of how
|
|
2003
|
+
// many dynamic objects moved this frame. Preserve the projection call so
|
|
2004
|
+
// a transition from probes to no probes still clears retained records.
|
|
2005
|
+
if (probes.length === 0) {
|
|
2006
|
+
entry.probeProjection.apply({ objects: [], probes, sky });
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
const objects: ProbeSceneObjectInput[] = entry.projection.slotsSnapshot().map((slot) => ({
|
|
2010
|
+
objectKey: slot.slot,
|
|
2011
|
+
generation: slot.generation,
|
|
2012
|
+
position: [
|
|
2013
|
+
slot.snapshot.transform.world[12] ?? 0,
|
|
2014
|
+
slot.snapshot.transform.world[13] ?? 0,
|
|
2015
|
+
slot.snapshot.transform.world[14] ?? 0,
|
|
2016
|
+
],
|
|
2017
|
+
}));
|
|
2018
|
+
entry.probeProjection.apply({
|
|
2019
|
+
objects,
|
|
2020
|
+
probes,
|
|
2021
|
+
sky,
|
|
2022
|
+
});
|
|
1359
2023
|
}
|
|
1360
2024
|
|
|
1361
2025
|
private syncGpuOwner(
|
|
@@ -1364,6 +2028,7 @@ export class PersistentRenderScene {
|
|
|
1364
2028
|
delta?: RenderSceneApplyResult,
|
|
1365
2029
|
full = false,
|
|
1366
2030
|
): void {
|
|
2031
|
+
if (this.composition?.token === owner && !this.composition.gpuDrivenSceneRequired) return;
|
|
1367
2032
|
const acquired = this.acquireGpuScene(owner, projection);
|
|
1368
2033
|
if (acquired === undefined || acquired.rebuilt) return;
|
|
1369
2034
|
const result = full
|