@forgeax/engine-render 0.1.21 → 0.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +281 -38
- package/dist/assembly/backend-contract.d.ts +13 -0
- package/dist/assembly/backend-contract.d.ts.map +1 -1
- package/dist/assembly/bundler-contract.d.ts +9 -0
- package/dist/assembly/bundler-contract.d.ts.map +1 -0
- package/dist/assembly/device-feature-admission.d.ts +8 -0
- package/dist/assembly/device-feature-admission.d.ts.map +1 -1
- package/dist/assembly/extended-lighting-fallback.d.ts +13 -0
- package/dist/assembly/extended-lighting-fallback.d.ts.map +1 -0
- package/dist/assembly/factory.d.ts +7 -235
- package/dist/assembly/factory.d.ts.map +1 -1
- package/dist/assembly/host-contract.d.ts +10 -0
- package/dist/assembly/host-contract.d.ts.map +1 -1
- package/dist/assembly/material/assembly.d.ts +3 -2
- package/dist/assembly/material/assembly.d.ts.map +1 -1
- package/dist/assembly/material/standard-layer-projection.d.ts +10 -0
- package/dist/assembly/material/standard-layer-projection.d.ts.map +1 -0
- package/dist/assembly/material-shader-policy.d.ts +168 -0
- package/dist/assembly/material-shader-policy.d.ts.map +1 -0
- package/dist/assembly/material-shader-prewarm.d.ts +21 -0
- package/dist/assembly/material-shader-prewarm.d.ts.map +1 -0
- package/dist/assembly/mesh-ssbo-grow.d.ts +88 -0
- package/dist/assembly/mesh-ssbo-grow.d.ts.map +1 -0
- package/dist/assembly/recovery/device-loss-fanout.d.ts +14 -0
- package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -0
- package/dist/assembly/recovery/generation.d.ts +46 -0
- package/dist/assembly/recovery/generation.d.ts.map +1 -0
- package/dist/assembly/recovery/recovery-attempt.d.ts +63 -0
- package/dist/assembly/recovery/recovery-attempt.d.ts.map +1 -0
- package/dist/assembly/recovery/renderer-recover.d.ts +115 -0
- package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -0
- package/dist/assembly/renderer-facade.d.ts +10 -0
- package/dist/assembly/renderer-facade.d.ts.map +1 -0
- package/dist/assembly/renderer-frame-transaction.d.ts +17 -0
- package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
- package/dist/assembly/renderer-helpers.d.ts +19 -0
- package/dist/assembly/renderer-helpers.d.ts.map +1 -0
- package/dist/assembly/renderer-lifecycle.d.ts +34 -0
- package/dist/assembly/renderer-lifecycle.d.ts.map +1 -1
- package/dist/assembly/shader-prewarm-policy.d.ts +15 -0
- package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -0
- package/dist/assembly/webgpu-ready-contract.d.ts +9 -0
- package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -0
- package/dist/assembly/webgpu-ready.d.ts +55 -0
- package/dist/assembly/webgpu-ready.d.ts.map +1 -0
- package/dist/assembly/webgpu-renderer.d.ts +152 -0
- package/dist/assembly/webgpu-renderer.d.ts.map +1 -0
- package/dist/authoring.mjs +1 -1
- package/dist/{chunk-PCHBPYML.mjs → chunk-GE3SDD72.mjs} +37 -5
- package/dist/chunk-GE3SDD72.mjs.map +1 -0
- package/dist/chunk-KZFTK6DO.mjs +1191 -0
- package/dist/chunk-KZFTK6DO.mjs.map +1 -0
- package/dist/{chunk-FS2KNUQH.mjs → chunk-Q752SHI5.mjs} +332 -568
- package/dist/chunk-Q752SHI5.mjs.map +1 -0
- package/dist/{chunk-6OPXDQ2E.mjs → chunk-RR5L4SRS.mjs} +33 -294
- package/dist/chunk-RR5L4SRS.mjs.map +1 -0
- package/dist/chunk-TFH5I7JJ.mjs +296 -0
- package/dist/chunk-TFH5I7JJ.mjs.map +1 -0
- package/dist/chunk-UQYS7FRM.mjs +1003 -0
- package/dist/chunk-UQYS7FRM.mjs.map +1 -0
- package/dist/chunk-YKB5DQV7.mjs +345 -0
- package/dist/chunk-YKB5DQV7.mjs.map +1 -0
- package/dist/{chunk-5YADUCA7.mjs → chunk-YRUYGA4A.mjs} +1189 -1217
- package/dist/chunk-YRUYGA4A.mjs.map +1 -0
- package/dist/chunk-Z3YYH2MB.mjs +5082 -0
- package/dist/chunk-Z3YYH2MB.mjs.map +1 -0
- package/dist/cluster-binner.d.ts +5 -0
- package/dist/cluster-binner.d.ts.map +1 -1
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/light-helpers.d.ts +4 -0
- package/dist/components/light-helpers.d.ts.map +1 -1
- package/dist/components/light-probe.d.ts +9 -0
- package/dist/components/light-probe.d.ts.map +1 -0
- package/dist/components/rect-area-light.d.ts +12 -0
- package/dist/components/rect-area-light.d.ts.map +1 -0
- package/dist/components/scene-instance.d.ts +4 -0
- package/dist/components/scene-instance.d.ts.map +1 -1
- package/dist/components/spot-light.d.ts +3 -0
- package/dist/components/spot-light.d.ts.map +1 -1
- package/dist/construct-renderer.mjs +27276 -23232
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/device/device-scope.d.ts +2 -0
- package/dist/device/device-scope.d.ts.map +1 -1
- package/dist/device/gpu-residency.d.ts +32 -5
- package/dist/device/gpu-residency.d.ts.map +1 -1
- package/dist/device/lifecycle-transaction.d.ts +2 -0
- package/dist/device/lifecycle-transaction.d.ts.map +1 -0
- package/dist/environment/frame.d.ts +5 -0
- package/dist/environment/frame.d.ts.map +1 -1
- package/dist/environment/inspection.d.ts +7 -0
- package/dist/environment/inspection.d.ts.map +1 -1
- package/dist/environment/lifecycle.d.ts +20 -2
- package/dist/environment/lifecycle.d.ts.map +1 -1
- package/dist/errors/gpu-driven.d.ts +26 -0
- package/dist/errors/gpu-driven.d.ts.map +1 -0
- package/dist/errors/index.d.ts +1 -0
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/recover.d.ts +67 -3
- package/dist/errors/recover.d.ts.map +1 -1
- package/dist/errors/render.d.ts +39 -1
- package/dist/errors/render.d.ts.map +1 -1
- package/dist/extract/contracts.d.ts +899 -0
- package/dist/extract/contracts.d.ts.map +1 -0
- package/dist/extract/gpu-driven.d.ts +50 -0
- package/dist/extract/gpu-driven.d.ts.map +1 -0
- package/dist/extract/visibility.d.ts.map +1 -1
- package/dist/features/host.d.ts +2 -0
- package/dist/features/host.d.ts.map +1 -1
- package/dist/features/prepared-gpu-work.d.ts +3 -0
- package/dist/features/prepared-gpu-work.d.ts.map +1 -1
- package/dist/features/prepared-graphics-store.d.ts +2 -0
- package/dist/features/prepared-graphics-store.d.ts.map +1 -1
- package/dist/gpu-driven/batch-topology.d.ts +34 -0
- package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
- package/dist/gpu-driven/prepared-draw.d.ts +63 -0
- package/dist/gpu-driven/prepared-draw.d.ts.map +1 -0
- package/dist/gpu-driven/production-raster.d.ts +126 -2
- package/dist/gpu-driven/production-raster.d.ts.map +1 -1
- package/dist/gpu-driven/view-gpu.d.ts +64 -1
- package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
- package/dist/gpu-scene-schema.d.ts +14 -0
- package/dist/gpu-scene-schema.d.ts.map +1 -1
- package/dist/gpu-scene.d.ts +9 -1
- package/dist/gpu-scene.d.ts.map +1 -1
- package/dist/gpu-usage.d.ts +0 -1
- package/dist/gpu-usage.d.ts.map +1 -1
- package/dist/hdrp-buffers.d.ts +3 -3
- package/dist/hdrp-buffers.d.ts.map +1 -1
- package/dist/ibl/IblPipelineCache.d.ts +7 -6
- package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
- package/dist/ibl/skylight-bind-group.d.ts +7 -1
- package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
- package/dist/index.d.ts +13 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +90 -5
- package/dist/index.mjs.map +1 -1
- package/dist/inspection-types.d.ts +163 -1
- package/dist/inspection-types.d.ts.map +1 -1
- package/dist/internal.d.ts +9 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.mjs +23 -2
- package/dist/internal.mjs.map +1 -1
- package/dist/light-buffer-layout.d.ts +60 -148
- package/dist/light-buffer-layout.d.ts.map +1 -1
- package/dist/materials.d.ts +34 -10
- package/dist/materials.d.ts.map +1 -1
- package/dist/mesh-material-bindings.d.ts +56 -0
- package/dist/mesh-material-bindings.d.ts.map +1 -1
- package/dist/occlusion-proxy.d.ts +4 -0
- package/dist/occlusion-proxy.d.ts.map +1 -0
- package/dist/pbr-pipeline.d.ts +58 -21
- package/dist/pbr-pipeline.d.ts.map +1 -1
- package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
- package/dist/pipeline/standard-lighting/graph.d.ts.map +1 -1
- package/dist/pipeline/standard-lighting/light-frame.d.ts +3 -3
- package/dist/pipeline/standard-lighting/light-frame.d.ts.map +1 -1
- package/dist/pipeline/standard-lighting/topology.d.ts +1 -1
- package/dist/pipeline/standard-lighting/topology.d.ts.map +1 -1
- package/dist/pipeline/standard-lighting/transport.d.ts.map +1 -1
- package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
- package/dist/pipeline-builder.d.ts +2 -0
- package/dist/pipeline-builder.d.ts.map +1 -1
- package/dist/pipeline-spec.d.ts +1 -1
- package/dist/pipeline-spec.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/points-lines/prepare.d.ts +3 -0
- package/dist/points-lines/prepare.d.ts.map +1 -1
- package/dist/points-lines/standard-owner.d.ts +33 -0
- package/dist/points-lines/standard-owner.d.ts.map +1 -0
- package/dist/prepare/extended-lighting/ltc-resources.d.ts +31 -0
- package/dist/prepare/extended-lighting/ltc-resources.d.ts.map +1 -0
- package/dist/prepare/extended-lighting/resources.d.ts +72 -0
- package/dist/prepare/extended-lighting/resources.d.ts.map +1 -0
- package/dist/prepare/extended-lighting/spot-modifiers.d.ts +42 -0
- package/dist/prepare/extended-lighting/spot-modifiers.d.ts.map +1 -0
- package/dist/prepare/extended-lighting/state.d.ts +24 -0
- package/dist/prepare/extended-lighting/state.d.ts.map +1 -0
- package/dist/record/frame-lighting.d.ts +8 -6
- package/dist/record/frame-lighting.d.ts.map +1 -1
- package/dist/record/frame-snapshot.d.ts +55 -10
- package/dist/record/frame-snapshot.d.ts.map +1 -1
- package/dist/record/frame-targets.d.ts.map +1 -1
- package/dist/record/frame.d.ts +21 -5
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/gpu-timing.d.ts +19 -7
- package/dist/record/gpu-timing.d.ts.map +1 -1
- package/dist/record/helpers.d.ts +0 -20
- package/dist/record/helpers.d.ts.map +1 -1
- package/dist/record/main-pass-geometry.d.ts +20 -2
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/main-pass-material.d.ts +10 -18
- package/dist/record/main-pass-material.d.ts.map +1 -1
- package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
- package/dist/record/main-pass.d.ts.map +1 -1
- package/dist/record/mesh-ssbo.d.ts +11 -3
- package/dist/record/mesh-ssbo.d.ts.map +1 -1
- package/dist/record/recovery-pipeline.d.ts +30 -0
- package/dist/record/recovery-pipeline.d.ts.map +1 -0
- package/dist/record/render-context.d.ts +82 -9
- package/dist/record/render-context.d.ts.map +1 -1
- package/dist/record/shadow-pass.d.ts.map +1 -1
- package/dist/record/typed-frame-graph.d.ts +49 -2
- package/dist/record/typed-frame-graph.d.ts.map +1 -1
- package/dist/record/view-ubo.d.ts +1 -2
- package/dist/record/view-ubo.d.ts.map +1 -1
- package/dist/recovery/render-system-candidate.d.ts +65 -0
- package/dist/recovery/render-system-candidate.d.ts.map +1 -0
- package/dist/reflection/inspection.d.ts +10 -0
- package/dist/reflection/inspection.d.ts.map +1 -0
- package/dist/reflection/projection.d.ts +49 -0
- package/dist/reflection/projection.d.ts.map +1 -1
- package/dist/reflection/record-owner.d.ts +60 -0
- package/dist/reflection/record-owner.d.ts.map +1 -0
- package/dist/render-contract.d.ts +78 -15
- package/dist/render-contract.d.ts.map +1 -1
- package/dist/render-pipeline.d.ts +7 -0
- package/dist/render-pipeline.d.ts.map +1 -1
- package/dist/render-system-extract-tail.d.ts +12 -0
- package/dist/render-system-extract-tail.d.ts.map +1 -0
- package/dist/render-system-extract.d.ts +252 -30
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts +140 -5
- package/dist/render-system.d.ts.map +1 -1
- package/dist/scene/probe-blend-record.d.ts +22 -0
- package/dist/scene/probe-blend-record.d.ts.map +1 -0
- package/dist/scene/probe-blend.d.ts +142 -0
- package/dist/scene/probe-blend.d.ts.map +1 -0
- package/dist/scene/render-scene-types.d.ts +0 -6
- package/dist/scene/render-scene-types.d.ts.map +1 -1
- package/dist/scene/render-scene.d.ts +122 -9
- package/dist/scene/render-scene.d.ts.map +1 -1
- package/dist/scene/visibility/bounds.d.ts +20 -0
- package/dist/scene/visibility/bounds.d.ts.map +1 -0
- package/dist/scene/visibility/budget.d.ts +16 -0
- package/dist/scene/visibility/budget.d.ts.map +1 -0
- package/dist/scene/visibility/facet.d.ts +73 -0
- package/dist/scene/visibility/facet.d.ts.map +1 -0
- package/dist/scene/visibility/gpu-frame-timing.d.ts +41 -0
- package/dist/scene/visibility/gpu-frame-timing.d.ts.map +1 -0
- package/dist/scene/visibility/gpu-lod.d.ts +46 -0
- package/dist/scene/visibility/gpu-lod.d.ts.map +1 -0
- package/dist/scene/visibility/inspection.d.ts +132 -0
- package/dist/scene/visibility/inspection.d.ts.map +1 -0
- package/dist/scene/visibility/lod-selector.d.ts +25 -0
- package/dist/scene/visibility/lod-selector.d.ts.map +1 -0
- package/dist/scene/visibility/occlusion-confidence.d.ts +81 -0
- package/dist/scene/visibility/occlusion-confidence.d.ts.map +1 -0
- package/dist/scene/visibility/occlusion-pass.d.ts +57 -0
- package/dist/scene/visibility/occlusion-pass.d.ts.map +1 -0
- package/dist/scene/visibility/occlusion-query-pool.d.ts +57 -0
- package/dist/scene/visibility/occlusion-query-pool.d.ts.map +1 -0
- package/dist/scene/visibility/occlusion-runtime.d.ts +96 -0
- package/dist/scene/visibility/occlusion-runtime.d.ts.map +1 -0
- package/dist/scene/visibility/types.d.ts +34 -0
- package/dist/scene/visibility/types.d.ts.map +1 -0
- package/dist/ssr/admission.d.ts +122 -0
- package/dist/ssr/admission.d.ts.map +1 -0
- package/dist/ssr/composition.d.ts +32 -0
- package/dist/ssr/composition.d.ts.map +1 -0
- package/dist/ssr/errors.d.ts +39 -0
- package/dist/ssr/errors.d.ts.map +1 -0
- package/dist/ssr/identity.d.ts +13 -0
- package/dist/ssr/identity.d.ts.map +1 -0
- package/dist/systems/skin-palette-allocator.d.ts +1 -1
- package/dist/temporal/frame-coordinator.d.ts +13 -0
- package/dist/temporal/frame-coordinator.d.ts.map +1 -1
- package/dist/temporal/frame.d.ts +2 -0
- package/dist/temporal/frame.d.ts.map +1 -1
- package/dist/temporal/index.mjs +6 -5
- package/dist/temporal/taa-history-store.d.ts +15 -1
- package/dist/temporal/taa-history-store.d.ts.map +1 -1
- package/dist/temporal/transaction.d.ts +5 -0
- package/dist/temporal/transaction.d.ts.map +1 -1
- package/dist/temporal/view.d.ts +7 -0
- package/dist/temporal/view.d.ts.map +1 -1
- package/dist/tilemap-chunk-extract-system.d.ts +1 -1
- package/dist/typed-render-graph-primitives.d.ts +4 -0
- package/dist/typed-render-graph-primitives.d.ts.map +1 -1
- package/dist/volume/passes.d.ts +8 -4
- package/dist/volume/passes.d.ts.map +1 -1
- package/package.json +20 -20
- package/src/__tests__/cluster-grid-default-surface.unit.test.ts +5 -0
- package/src/__tests__/detached-inspection.integration.test.ts +6 -0
- package/src/__tests__/device-feature-admission.unit.test.ts +19 -1
- package/src/__tests__/device-loss-fanout.unit.test.ts +107 -0
- package/src/__tests__/device-scope-lifecycle.unit.test.ts +21 -0
- package/src/__tests__/direct-light-snapshot.unit.test.ts +59 -3
- package/src/__tests__/environment-generation.integration.test.ts +39 -0
- package/src/__tests__/evidence-identity.ts +13 -0
- package/src/__tests__/evidence-identity.unit.test.ts +24 -0
- package/src/__tests__/expose-renderer-errors.unit.test.ts +34 -0
- package/src/__tests__/extended-lighting-recovery.browser.test.ts +14 -0
- package/src/__tests__/extended-lighting-recovery.dawn.test.ts +14 -0
- package/src/__tests__/extended-lighting-recovery.integration.test.ts +81 -0
- package/src/__tests__/extended-lighting-resources.unit.test.ts +51 -0
- package/src/__tests__/extended-lighting-zero-feature.integration.test.ts +52 -0
- package/src/__tests__/factory-contract.integration.test.ts +154 -6
- package/src/__tests__/fallback-row-stride-surface.unit.test.ts +4 -1
- package/src/__tests__/frame-observation.contract.test.ts +15 -0
- package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +146 -0
- package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +1 -0
- package/src/__tests__/gpu-driven-indirect-raster.dawn.test.ts +1 -0
- package/src/__tests__/gpu-driven-production.integration.test.ts +572 -9
- package/src/__tests__/gpu-driven-scaling.unit.test.ts +66 -2
- package/src/__tests__/gpu-driven-view-gpu-evidence.ts +26 -1
- package/src/__tests__/gpu-driven-view-graph.integration.test.ts +89 -2
- package/src/__tests__/gpu-driven-view.browser.test.ts +3 -3
- package/src/__tests__/gpu-driven-view.dawn.test.ts +3 -3
- package/src/__tests__/gpu-driven-view.unit.test.ts +50 -0
- package/src/__tests__/gpu-lod-evidence.ts +188 -0
- package/src/__tests__/gpu-lod-fallback.integration.test.ts +10 -0
- package/src/__tests__/gpu-lod-indirect.integration.test.ts +40 -0
- package/src/__tests__/gpu-lod-parity.dawn.test.ts +29 -0
- package/src/__tests__/gpu-lod-readiness.integration.test.ts +52 -0
- package/src/__tests__/gpu-lod-schema.unit.test.ts +66 -0
- package/src/__tests__/gpu-lod-selector.unit.test.ts +28 -0
- package/src/__tests__/gpu-lod-silhouette.browser.test.ts +13 -0
- package/src/__tests__/gpu-resource-store-stride.unit.test.ts +66 -1
- package/src/__tests__/gpu-scene.dawn.test.ts +8 -1
- package/src/__tests__/gpu-scene.unit.test.ts +49 -3
- package/src/__tests__/ibl/face-uniforms.unit.test.ts +2 -0
- package/src/__tests__/ibl-residency-publication.unit.test.ts +113 -0
- package/src/__tests__/light-buffer-layout.unit.test.ts +113 -3
- package/src/__tests__/light-inspection.unit.test.ts +55 -0
- package/src/__tests__/light-resource-error.unit.test.ts +43 -0
- package/src/__tests__/lighting-scene-roundtrip.integration.test.ts +109 -0
- package/src/__tests__/lod-bounds.unit.test.ts +42 -0
- package/src/__tests__/lod-cpu-lane.integration.test.ts +55 -0
- package/src/__tests__/lod-cpu-no-change.perf.test.ts +19 -0
- package/src/__tests__/lod-gpu-frame-timing.unit.test.ts +49 -0
- package/src/__tests__/lod-multi-view.integration.test.ts +63 -0
- package/src/__tests__/lod-occlusion-device-loss.integration.test.ts +27 -0
- package/src/__tests__/lod-occlusion-final-decision.integration.test.ts +47 -0
- package/src/__tests__/lod-occlusion-inspection.integration.test.ts +217 -0
- package/src/__tests__/lod-occlusion-inspection.schema.test-d.ts +14 -0
- package/src/__tests__/lod-occlusion-inspection.unit.test.ts +72 -0
- package/src/__tests__/lod-occlusion-visual-evidence.ts +91 -0
- package/src/__tests__/lod-occlusion.browser.test.ts +15 -0
- package/src/__tests__/lod-occlusion.dawn.test.ts +20 -0
- package/src/__tests__/lod-selector.profile.test.ts +35 -0
- package/src/__tests__/lod-selector.unit.test.ts +68 -0
- package/src/__tests__/lod-visibility-identity.unit.test.ts +198 -0
- package/src/__tests__/lod-visibility-lifecycle.integration.test.ts +132 -0
- package/src/__tests__/ltc-resources.unit.test.ts +32 -0
- package/src/__tests__/material-contract-inventory.unit.test.ts +41 -9
- package/src/__tests__/material-cooked-projection.unit.test.ts +19 -1
- package/src/__tests__/material-static-dynamic-route.unit.test.ts +29 -0
- package/src/__tests__/material-stride-surface.unit.test.ts +7 -3
- package/src/__tests__/materials-standard-contract.unit.test.ts +104 -0
- package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +1 -1
- package/src/__tests__/occlusion-confidence.unit.test.ts +168 -0
- package/src/__tests__/occlusion-invalidation.integration.test.ts +37 -0
- package/src/__tests__/occlusion-pass-routing.unit.test.ts +16 -0
- package/src/__tests__/occlusion-pass.dawn.test.ts +20 -0
- package/src/__tests__/occlusion-pass.integration.test.ts +43 -0
- package/src/__tests__/occlusion-pass.rhi-null.test.ts +22 -0
- package/src/__tests__/occlusion-query-pool.rhi-null.test.ts +21 -0
- package/src/__tests__/occlusion-query-pool.unit.test.ts +95 -0
- package/src/__tests__/occlusion-query-ticket.integration.test.ts +47 -0
- package/src/__tests__/occlusion-query.dawn.test.ts +16 -0
- package/src/__tests__/occlusion-query.evidence.ts +178 -0
- package/src/__tests__/occlusion-query.rhi-null.test.ts +31 -0
- package/src/__tests__/occlusion-runtime.unit.test.ts +120 -0
- package/src/__tests__/physical-clearcoat.integration.test.ts +89 -0
- package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +196 -0
- package/src/__tests__/prepared-graphics-pipeline-warmup.unit.test.ts +1 -1
- package/src/__tests__/probe-bindings.unit.test.ts +200 -0
- package/src/__tests__/probe-blend-numeric.unit.test.ts +67 -0
- package/src/__tests__/probe-blend-oracle.unit.test.ts +136 -0
- package/src/__tests__/probe-capacity.integration.test.ts +97 -0
- package/src/__tests__/probe-recovery.integration.test.ts +104 -0
- package/src/__tests__/probe-scene-dirty.integration.test.ts +211 -0
- package/src/__tests__/profiler-phase-catalog.test.ts +4 -1
- package/src/__tests__/profiler-phase-token.test.ts +1 -0
- package/src/__tests__/recover-error-policy-owner.unit.test.ts +6 -3
- package/src/__tests__/recovery-candidate-prepare.contract.test.ts +140 -0
- package/src/__tests__/rect-area-boundary.unit.test.ts +24 -0
- package/src/__tests__/reflection-fallback-graph.unit.test.ts +67 -0
- package/src/__tests__/reflection-fallback-inspection.unit.test.ts +77 -0
- package/src/__tests__/reflection-fallback-lifecycle.integration.test.ts +21 -0
- package/src/__tests__/reflection-fallback-projection.unit.test.ts +90 -0
- package/src/__tests__/reflection-fallback-source.unit.test.ts +32 -0
- package/src/__tests__/reflection-probe-owner-placement.unit.test.ts +39 -0
- package/src/__tests__/reflection-probe-record-owner.characterization.unit.test.ts +120 -0
- package/src/__tests__/render-error-code-owner.test-d.ts +1 -0
- package/src/__tests__/render-feature-docs.unit.test.ts +12 -7
- package/src/__tests__/render-graph-fallback.integration.test.ts +24 -0
- package/src/__tests__/render-perf/extended-lighting.bench.test.ts +66 -0
- package/src/__tests__/render-scene-projection.unit.test.ts +447 -18
- package/src/__tests__/renderer-factory-material-contract.unit.test.ts +160 -17
- package/src/__tests__/renderer-frame-generation-fence.integration.test.ts +47 -0
- package/src/__tests__/renderer-generation-assembly.unit.test.ts +77 -0
- package/src/__tests__/renderer-generation-failure.unit.test.ts +108 -0
- package/src/__tests__/renderer-producer-root-matrix.integration.test.ts +33 -0
- package/src/__tests__/renderer-recovery-capability-matrix.unit.test.ts +59 -0
- package/src/__tests__/renderer-recovery-contract.unit.test.ts +73 -0
- package/src/__tests__/renderer-recovery-lifecycle.unit.test.ts +216 -0
- package/src/__tests__/renderer-target-history-recovery.integration.test.ts +80 -0
- package/src/__tests__/shader-manifest-fixture.ts +13 -16
- package/src/__tests__/shader-stage-surface.unit.test.ts +1 -1
- package/src/__tests__/skin-motion-regression.unit.test.ts +22 -0
- package/src/__tests__/skinned-shadow-caster.test.ts +2 -2
- package/src/__tests__/spot-modifier-projection.unit.test.ts +70 -0
- package/src/__tests__/ssr-admission-matrix.integration.test.ts +206 -0
- package/src/__tests__/ssr-composition.unit.test.ts +46 -0
- package/src/__tests__/ssr-fallback-zero-work.bench.test.ts +94 -0
- package/src/__tests__/ssr-recovery-errors.unit.test.ts +58 -0
- package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +67 -0
- package/src/__tests__/standard-clustered-pipeline.unit.test.ts +7 -0
- package/src/__tests__/standard-layer-projection.unit.test.ts +23 -0
- package/src/__tests__/standard-lighting-inspection.integration.test.ts +1 -1
- package/src/__tests__/standard-lighting-write.unit.test.ts +66 -2
- package/src/__tests__/temporal-fallback-generation.integration.test.ts +16 -0
- package/src/__tests__/temporal-view.unit.test.ts +3 -3
- package/src/__tests__/transmission-prewarm.unit.test.ts +111 -32
- package/src/__tests__/visibility-budget.integration.test.ts +140 -0
- package/src/__tests__/visibility-budget.unit.test.ts +70 -0
- package/src/__tests__/visibility-extract.integration.test.ts +11 -6
- package/src/__tests__/volumetric-fog-point-spot-accumulation.integration.test.ts +6 -3
- package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +31 -28
- package/src/__tests__/volumetric-fog-topology.unit.test.ts +3 -1
- package/src/assembly/backend-contract.ts +13 -0
- package/src/assembly/bundler-contract.ts +9 -0
- package/src/assembly/device-feature-admission.ts +26 -9
- package/src/assembly/extended-lighting-fallback.ts +216 -0
- package/src/assembly/factory.ts +81 -9634
- package/src/assembly/host-contract.ts +10 -0
- package/src/assembly/material/assembly.ts +18 -2
- package/src/assembly/material/standard-layer-projection.ts +16 -0
- package/src/assembly/material-shader-policy.ts +854 -0
- package/src/assembly/material-shader-prewarm.ts +88 -0
- package/src/assembly/mesh-ssbo-grow.ts +270 -0
- package/src/assembly/recovery/device-loss-fanout.ts +89 -0
- package/src/assembly/recovery/generation.ts +141 -0
- package/src/assembly/recovery/recovery-attempt.ts +800 -0
- package/src/assembly/recovery/renderer-recover.ts +906 -0
- package/src/assembly/renderer-facade.ts +342 -0
- package/src/assembly/renderer-frame-transaction.ts +45 -0
- package/src/assembly/renderer-helpers.ts +99 -0
- package/src/assembly/renderer-lifecycle.ts +159 -0
- package/src/assembly/shader-prewarm-policy.ts +157 -0
- package/src/assembly/webgpu-ready-contract.ts +14 -0
- package/src/assembly/webgpu-ready.ts +4079 -0
- package/src/assembly/webgpu-renderer.ts +4095 -0
- package/src/cluster-binner.ts +31 -0
- package/src/components/__tests__/lighting-authoring.unit.test.ts +39 -0
- package/src/components/__tests__/rect-area-light.unit.test.ts +33 -0
- package/src/components/camera.ts +1 -1
- package/src/components/index.ts +3 -0
- package/src/components/light-helpers.ts +52 -1
- package/src/components/light-probe.ts +13 -0
- package/src/components/rect-area-light.ts +15 -0
- package/src/components/scene-instance.ts +6 -0
- package/src/components/spot-light.ts +3 -0
- package/src/debug-draw-glue.ts +1 -1
- package/src/device/__tests__/extended-lighting-recovery.unit.test.ts +24 -0
- package/src/device/device-scope.ts +35 -6
- package/src/device/gpu-residency.ts +334 -35
- package/src/device/lifecycle-transaction.ts +12 -0
- package/src/environment/frame.ts +6 -0
- package/src/environment/inspection.ts +10 -0
- package/src/environment/lifecycle.ts +122 -9
- package/src/errors/gpu-driven.ts +92 -0
- package/src/errors/index.ts +1 -0
- package/src/errors/recover.ts +108 -5
- package/src/errors/render.ts +72 -1
- package/src/extract/camera.ts +3 -3
- package/src/extract/contracts.ts +974 -0
- package/src/extract/gpu-driven.ts +108 -0
- package/src/extract/visibility.ts +0 -23
- package/src/features/host.ts +16 -0
- package/src/features/prepared-gpu-work.ts +20 -2
- package/src/features/prepared-graphics-store.ts +17 -0
- package/src/gpu-driven/batch-topology.ts +206 -28
- package/src/gpu-driven/prepared-draw.ts +208 -0
- package/src/gpu-driven/production-raster.ts +1046 -39
- package/src/gpu-driven/view-gpu.ts +411 -29
- package/src/gpu-scene-schema.ts +13 -0
- package/src/gpu-scene.ts +137 -67
- package/src/gpu-usage.ts +0 -1
- package/src/hdrp-buffers.ts +6 -6
- package/src/ibl/IblPipelineCache.ts +134 -50
- package/src/ibl/skylight-bind-group.ts +26 -0
- package/src/index.ts +83 -1
- package/src/inspection-types.ts +220 -1
- package/src/internal.ts +38 -0
- package/src/light-buffer-layout.ts +157 -324
- package/src/materials.ts +278 -47
- package/src/mesh-material-bindings.ts +59 -0
- package/src/occlusion-proxy.ts +64 -0
- package/src/pbr-pipeline.ts +290 -60
- package/src/pipeline/standard-forward-lane.ts +47 -0
- package/src/pipeline/standard-lighting/graph.ts +2 -1
- package/src/pipeline/standard-lighting/light-frame.ts +3 -2
- package/src/pipeline/standard-lighting/topology.ts +1 -1
- package/src/pipeline/standard-lighting/transport.ts +2 -1
- package/src/pipeline/standard-pipeline.ts +2 -0
- package/src/pipeline-builder.ts +5 -2
- package/src/pipeline-spec.ts +12 -1
- package/src/plugin.ts +4 -0
- package/src/points-lines/__tests__/cache-recovery.unit.test.ts +22 -0
- package/src/points-lines/prepare.ts +9 -0
- package/src/points-lines/standard-owner.ts +403 -0
- package/src/prepare/__tests__/extended-lighting-capability.unit.test.ts +56 -0
- package/src/prepare/extended-lighting/ltc-resources.ts +136 -0
- package/src/prepare/extended-lighting/resources.ts +389 -0
- package/src/prepare/extended-lighting/spot-modifiers.ts +194 -0
- package/src/prepare/extended-lighting/state.ts +92 -0
- package/src/record/__tests__/frame-targets-recovery.unit.test.ts +60 -0
- package/src/record/__tests__/gpu-timing.unit.test.ts +177 -0
- package/src/record/__tests__/main-pass-geometry.unit.test.ts +26 -1
- package/src/record/__tests__/mesh-ssbo.unit.test.ts +38 -0
- package/src/record/__tests__/recovery-pipeline.unit.test.ts +124 -0
- package/src/record/__tests__/sprite-material-abi.unit.test.ts +28 -13
- package/src/record/__tests__/standard-pbr-ubo-layout.unit.test.ts +47 -4
- package/src/record/__tests__/temporal-payload.unit.test.ts +29 -28
- package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +21 -2
- package/src/record/frame-lighting.ts +252 -53
- package/src/record/frame-snapshot.ts +72 -12
- package/src/record/frame-targets.ts +11 -1
- package/src/record/frame.ts +416 -95
- package/src/record/gpu-timing.ts +108 -44
- package/src/record/helpers.ts +1 -59
- package/src/record/main-pass-geometry.ts +434 -44
- package/src/record/main-pass-material.ts +324 -210
- package/src/record/main-pass-sprite-draws.ts +58 -92
- package/src/record/main-pass.ts +94 -6
- package/src/record/mesh-ssbo.ts +79 -46
- package/src/record/recovery-pipeline.ts +695 -0
- package/src/record/render-context.ts +99 -7
- package/src/record/shadow-pass.ts +66 -40
- package/src/record/typed-frame-graph.ts +270 -84
- package/src/record/view-ubo.ts +17 -27
- package/src/recovery/render-system-candidate.ts +839 -0
- package/src/reflection/inspection.ts +67 -0
- package/src/reflection/projection.ts +196 -1
- package/src/reflection/record-owner.ts +1283 -0
- package/src/render-contract.ts +82 -12
- package/src/render-pipeline.ts +5 -0
- package/src/render-system-extract-tail.ts +2983 -0
- package/src/render-system-extract.ts +290 -2784
- package/src/render-system.ts +1597 -1192
- package/src/scene/__tests__/changed-block-summary.candidate.test.ts +36 -0
- package/src/scene/probe-blend-record.ts +54 -0
- package/src/scene/probe-blend.ts +813 -0
- package/src/scene/render-scene-types.ts +0 -7
- package/src/scene/render-scene.ts +972 -184
- package/src/scene/visibility/bounds.ts +57 -0
- package/src/scene/visibility/budget.ts +40 -0
- package/src/scene/visibility/facet.ts +400 -0
- package/src/scene/visibility/gpu-frame-timing.ts +101 -0
- package/src/scene/visibility/gpu-lod.ts +119 -0
- package/src/scene/visibility/inspection.ts +313 -0
- package/src/scene/visibility/lod-selector.ts +84 -0
- package/src/scene/visibility/occlusion-confidence.ts +478 -0
- package/src/scene/visibility/occlusion-pass.ts +165 -0
- package/src/scene/visibility/occlusion-query-pool.ts +178 -0
- package/src/scene/visibility/occlusion-runtime.ts +672 -0
- package/src/scene/visibility/types.ts +50 -0
- package/src/ssr/admission.ts +451 -0
- package/src/ssr/composition.ts +73 -0
- package/src/ssr/errors.ts +76 -0
- package/src/ssr/identity.ts +12 -0
- package/src/systems/skin-palette-allocator.ts +1 -1
- package/src/temporal/frame-coordinator.ts +30 -1
- package/src/temporal/frame.ts +14 -1
- package/src/temporal/taa-history-store.ts +46 -2
- package/src/temporal/temporal-view.ts +1 -1
- package/src/temporal/transaction.ts +9 -0
- package/src/temporal/view.ts +11 -0
- package/src/tilemap-chunk-extract-system.ts +5 -5
- package/src/transmission/__tests__/standard-transmission.dawn.test.ts +19 -12
- package/src/typed-render-graph-primitives.ts +48 -0
- package/src/volume/passes.ts +46 -49
- package/dist/.tsbuildinfo +0 -1
- package/dist/chunk-5YADUCA7.mjs.map +0 -1
- package/dist/chunk-6OPXDQ2E.mjs.map +0 -1
- package/dist/chunk-BXBQQKFQ.mjs +0 -67
- package/dist/chunk-BXBQQKFQ.mjs.map +0 -1
- package/dist/chunk-FS2KNUQH.mjs.map +0 -1
- package/dist/chunk-PCHBPYML.mjs.map +0 -1
- package/dist/chunk-VFPLTKCU.mjs +0 -192
- package/dist/chunk-VFPLTKCU.mjs.map +0 -1
|
@@ -0,0 +1,4095 @@
|
|
|
1
|
+
// @forgeax/engine-render — concrete Renderer lifecycle and frame interpreter.
|
|
2
|
+
//
|
|
3
|
+
// The Runtime host selects one typed backend pack before this assembly runs.
|
|
4
|
+
//
|
|
5
|
+
// **D-P4 three-channel error propagation** (RK-04 + plan-strategy §7.3):
|
|
6
|
+
// (a) Construction-time → `createRenderer(...)` rejects with
|
|
7
|
+
// `EngineEnvironmentError` whose `detail.webgpuError` is the Channel 2
|
|
8
|
+
// `RhiError` and `detail.wgpuError` is the Channel 3 failure (when both
|
|
9
|
+
// channels fail; AI users `try { await createRenderer(...) } catch (e) {
|
|
10
|
+
// switch (e.detail.webgpuError?.code) { ... } }`).
|
|
11
|
+
// (b) Run-time pipeline build → `renderer.initialization` rejects with the
|
|
12
|
+
// structured `RhiError` (`shader-compile-failed` / `limit-exceeded` /
|
|
13
|
+
// `webgpu-runtime-error` / ...); AI users `await renderer.initialization`.
|
|
14
|
+
// (c) error fan-out → `renderer.subscribe(listener)` captures the same
|
|
15
|
+
// errors as a fallback observability channel (charter proposition 4
|
|
16
|
+
// structured + proposition 9 graceful degradation).
|
|
17
|
+
//
|
|
18
|
+
// AC-15 source-level: this file uses `globalThis.navigator` rather than
|
|
19
|
+
// `navigator` directly; it never touches `window` or `document`.
|
|
20
|
+
|
|
21
|
+
import type { MipmapShaderModuleFactory } from '@forgeax/engine-assets-runtime';
|
|
22
|
+
import {
|
|
23
|
+
AssetRegistry,
|
|
24
|
+
adaptDynamicTextureDevice,
|
|
25
|
+
DynamicTextureStore,
|
|
26
|
+
} from '@forgeax/engine-assets-runtime';
|
|
27
|
+
import { audioLoader } from '@forgeax/engine-audio-webaudio';
|
|
28
|
+
import { Update, type World } from '@forgeax/engine-ecs';
|
|
29
|
+
import { createRenderReadLease, type RenderReadLease } from '@forgeax/engine-ecs/projection';
|
|
30
|
+
import {
|
|
31
|
+
deriveVertexBufferLayout,
|
|
32
|
+
deriveVertexBufferLayoutFromProjection,
|
|
33
|
+
type GpuVertexBufferLayoutEntry,
|
|
34
|
+
type VertexLayoutProjection,
|
|
35
|
+
} from '@forgeax/engine-geometry';
|
|
36
|
+
import type {
|
|
37
|
+
BindGroupLayout,
|
|
38
|
+
PipelineLayout,
|
|
39
|
+
RenderPipeline,
|
|
40
|
+
Result,
|
|
41
|
+
RhiCanvasContext,
|
|
42
|
+
RhiDevice,
|
|
43
|
+
} from '@forgeax/engine-rhi';
|
|
44
|
+
import { err, ok, RhiError, validateDrawArgs } from '@forgeax/engine-rhi';
|
|
45
|
+
import { registerPropagateTransforms } from '@forgeax/engine-scene';
|
|
46
|
+
import {
|
|
47
|
+
findVariantByKey,
|
|
48
|
+
type MaterialShaderEntry,
|
|
49
|
+
type MaterialShaderManifestEntry,
|
|
50
|
+
ShaderCatalog,
|
|
51
|
+
standardPhysicalTextureFields,
|
|
52
|
+
} from '@forgeax/engine-shader';
|
|
53
|
+
import type {
|
|
54
|
+
ImportTransport,
|
|
55
|
+
MaterialRenderState,
|
|
56
|
+
ParamSchemaEntry,
|
|
57
|
+
PassKind,
|
|
58
|
+
PrimitiveTopology,
|
|
59
|
+
VertexAttributeMap,
|
|
60
|
+
} from '@forgeax/engine-types';
|
|
61
|
+
|
|
62
|
+
export type { MaterialShaderManifestEntry } from '@forgeax/engine-shader';
|
|
63
|
+
|
|
64
|
+
import { GpuResidencyCache } from '../device/gpu-residency';
|
|
65
|
+
import type { DeviceResourceKind } from '../device/resource-types';
|
|
66
|
+
import { createEngineMetrics } from '../engine-metrics';
|
|
67
|
+
import { createRecoveryFailedError, RecoverError, type RecoverFailure } from '../errors/recover';
|
|
68
|
+
import {
|
|
69
|
+
FrameReceiptStaleError,
|
|
70
|
+
type RenderError,
|
|
71
|
+
RendererContractFailureError,
|
|
72
|
+
type RendererOperationCause,
|
|
73
|
+
RendererOperationError,
|
|
74
|
+
RenderFeatureCapabilityMissingError,
|
|
75
|
+
RenderFeatureStageFailedError,
|
|
76
|
+
} from '../errors/render';
|
|
77
|
+
import { createRenderFeatureHost, type RenderFeatureHost } from '../features/host';
|
|
78
|
+
import { RENDER_FEATURE_VERTEX_LAYOUTS } from '../features/prepared-graphics';
|
|
79
|
+
import type { RenderFeature, RenderFeatureShaderModuleMode } from '../features/types';
|
|
80
|
+
import {
|
|
81
|
+
type PostProcessShaderEntry,
|
|
82
|
+
postProcessShaderModuleLabel,
|
|
83
|
+
} from '../fullscreen-post-process-pass';
|
|
84
|
+
import { glyphTextLayoutSystem } from '../glyph-text-layout-system';
|
|
85
|
+
import { GPU_SHADER_STAGE_FRAGMENT, GPU_SHADER_STAGE_VERTEX } from '../gpu-stage';
|
|
86
|
+
import type { BloomInspection } from '../inspection-types';
|
|
87
|
+
import {
|
|
88
|
+
DeviceScope,
|
|
89
|
+
HealthListenerRegistry,
|
|
90
|
+
LostListenerRegistry,
|
|
91
|
+
RhiErrorListenerRegistry,
|
|
92
|
+
} from '../lifecycle';
|
|
93
|
+
import {
|
|
94
|
+
isCanonicalStandardPbrMaterialShader,
|
|
95
|
+
isStandardPbrMaterialShader,
|
|
96
|
+
type PipelineGroup2Contract,
|
|
97
|
+
resolvePipelineGroup2Contract,
|
|
98
|
+
SKIN_MATERIAL_SHADER_ID,
|
|
99
|
+
} from '../pbr-pipeline';
|
|
100
|
+
import { standardPipeline } from '../pipeline/standard-pipeline';
|
|
101
|
+
import { DEFAULT_STANDARD_PROFILE } from '../pipeline/standard-profile';
|
|
102
|
+
import { buildPipelineForMaterialShader } from '../pipeline-builder';
|
|
103
|
+
import {
|
|
104
|
+
buildBindGroupLayoutDescriptor,
|
|
105
|
+
cacheKeyOf,
|
|
106
|
+
colorFormatsForPassKind,
|
|
107
|
+
type PipelineSpec,
|
|
108
|
+
passKindPolicyTable,
|
|
109
|
+
} from '../pipeline-spec';
|
|
110
|
+
import type { ExtendedLightingResourceCandidate } from '../prepare/extended-lighting/resources';
|
|
111
|
+
import {
|
|
112
|
+
COOKIE_MATRIX_BYTES,
|
|
113
|
+
COOKIE_SLICE_SIZE,
|
|
114
|
+
deriveExtendedLightingCapability,
|
|
115
|
+
EXTENDED_LIGHTING_TOPOLOGY,
|
|
116
|
+
IES_SLICE_HEIGHT,
|
|
117
|
+
IES_SLICE_WIDTH,
|
|
118
|
+
} from '../prepare/extended-lighting/resources';
|
|
119
|
+
import {
|
|
120
|
+
createExtendedLightingState,
|
|
121
|
+
projectExtendedLightingInspection,
|
|
122
|
+
promoteExtendedLightingCandidate,
|
|
123
|
+
} from '../prepare/extended-lighting/state';
|
|
124
|
+
import type { FrameObservationOptions } from '../record/frame';
|
|
125
|
+
import type { GpuPassTimingReason } from '../record/gpu-pass-timing/errors.js';
|
|
126
|
+
import {
|
|
127
|
+
createGpuPassTimingSession,
|
|
128
|
+
DEFAULT_GPU_PASS_TIMING_OPTIONS,
|
|
129
|
+
type GpuPassTimingCapture,
|
|
130
|
+
type GpuPassTimingObservation,
|
|
131
|
+
type GpuPassTimingSession,
|
|
132
|
+
} from '../record/gpu-pass-timing/index.js';
|
|
133
|
+
import { GpuTimingCapture, type VolumeTimingObservation } from '../record/gpu-timing';
|
|
134
|
+
import type { RecoveryColdWorkGuard } from '../record/render-context';
|
|
135
|
+
import type {
|
|
136
|
+
DrawOwnerOptions,
|
|
137
|
+
FrameObservationRequest,
|
|
138
|
+
FrameReceipt,
|
|
139
|
+
FrameReceiptObservation,
|
|
140
|
+
HealthSnapshot,
|
|
141
|
+
RenderDebugOverlay,
|
|
142
|
+
RendererErrorListener,
|
|
143
|
+
RendererLostListener,
|
|
144
|
+
RendererOptions,
|
|
145
|
+
RenderFrameInput,
|
|
146
|
+
RenderInspection,
|
|
147
|
+
RenderProfile,
|
|
148
|
+
RenderResult,
|
|
149
|
+
RenderWorldLease,
|
|
150
|
+
VolumetricFogShaderSources,
|
|
151
|
+
} from '../render-contract';
|
|
152
|
+
import { FXAA_POST_PROCESS_ID, STANDARD_OUTPUT_TRANSFORM_FEATURE_ID } from '../render-contract';
|
|
153
|
+
import {
|
|
154
|
+
attachGpuPassTimingSession,
|
|
155
|
+
createRenderSystem,
|
|
156
|
+
type PipelineState,
|
|
157
|
+
type RecoveryGraphCandidate,
|
|
158
|
+
type RecoveryPostProcessResources,
|
|
159
|
+
type RecoveryRootBundle,
|
|
160
|
+
type RenderSystem,
|
|
161
|
+
} from '../render-system';
|
|
162
|
+
import { createGpuPassTimingObservationStore, observeGpuPassTimingDisabled } from '../renderer.js';
|
|
163
|
+
import { postSpawnResolveJoints } from '../scene-instances/post-spawn-resolve-joints';
|
|
164
|
+
import type { RenderTarget } from '../targets/contracts';
|
|
165
|
+
import { tilemapChunkExtractSystem } from '../tilemap-chunk-extract-system';
|
|
166
|
+
import type { RhiBackendPack } from './backend-contract';
|
|
167
|
+
import type { BundlerOptions } from './bundler-contract';
|
|
168
|
+
import { deviceOptionsForAdapter } from './device-feature-admission';
|
|
169
|
+
import type { RendererAssemblyImplementation } from './host-contract';
|
|
170
|
+
import {
|
|
171
|
+
allowsUnlitPreparedFallback,
|
|
172
|
+
invokeDeviceCreateShaderModule,
|
|
173
|
+
isSharedMaterialUserRegionCompatible,
|
|
174
|
+
type LayoutKind,
|
|
175
|
+
type MaterialShaderBindingContract,
|
|
176
|
+
makeShaderDeviceAdapter,
|
|
177
|
+
normalizeMaterialShaderVariantSet,
|
|
178
|
+
prepareMaterialShaders,
|
|
179
|
+
resolveMaterialShaderBindingContract,
|
|
180
|
+
resolveMaterialShaderVariantSet,
|
|
181
|
+
resolveMaterialShaderVertexInputContract,
|
|
182
|
+
type ShaderDeviceAdapterInternal,
|
|
183
|
+
selectNoColorPbrVariant,
|
|
184
|
+
selectPipelineLayoutForVariant,
|
|
185
|
+
shouldDeferMissingPreparedMaterialShader,
|
|
186
|
+
} from './material-shader-policy';
|
|
187
|
+
import type { MeshSsboGrowResult, MeshSsboState } from './mesh-ssbo-grow';
|
|
188
|
+
import { attachDeviceLostFanout } from './recovery/device-loss-fanout';
|
|
189
|
+
import {
|
|
190
|
+
type GenerationAggregate,
|
|
191
|
+
type GenerationPublication,
|
|
192
|
+
publishGeneration,
|
|
193
|
+
} from './recovery/generation';
|
|
194
|
+
import { createRendererRecovery, type RendererRecovery } from './recovery/renderer-recover';
|
|
195
|
+
import { createRenderTargetHost } from './render-target-host';
|
|
196
|
+
import {
|
|
197
|
+
freezeRenderProfile,
|
|
198
|
+
structuredRendererCause,
|
|
199
|
+
validateRenderProfile,
|
|
200
|
+
} from './renderer-facade';
|
|
201
|
+
import {
|
|
202
|
+
type ContinuationTerminator,
|
|
203
|
+
createContinuationTerminator,
|
|
204
|
+
} from './renderer-frame-transaction';
|
|
205
|
+
import { ensureContextConfigured, wrapDisposeError } from './renderer-helpers';
|
|
206
|
+
import { projectRendererOutputInspection } from './renderer-inspection';
|
|
207
|
+
import {
|
|
208
|
+
createRecoveryContinuation,
|
|
209
|
+
createRecoveryDeadline,
|
|
210
|
+
createSingleFlight,
|
|
211
|
+
type RecoveryGuidance,
|
|
212
|
+
type RecoveryPhase,
|
|
213
|
+
} from './renderer-lifecycle';
|
|
214
|
+
import { STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES } from './shader-prewarm-policy';
|
|
215
|
+
import { buildReadyWebGPU } from './webgpu-ready';
|
|
216
|
+
import { DEPTH_TEXTURE_FORMAT, HDR_COLOR_ATTACHMENT_FORMAT } from './webgpu-ready-contract';
|
|
217
|
+
|
|
218
|
+
export type { BundlerOptions } from './bundler-contract';
|
|
219
|
+
export { assembleMaterialProjection } from './material/assembly';
|
|
220
|
+
export {
|
|
221
|
+
projectMaterialPipeline,
|
|
222
|
+
routeMaterialPipeline,
|
|
223
|
+
updateMaterialRuntimeValues,
|
|
224
|
+
} from './material/pipeline-projection';
|
|
225
|
+
export type {
|
|
226
|
+
LayoutKind,
|
|
227
|
+
MaterialShaderBindingContract,
|
|
228
|
+
MaterialShaderVertexInputContract,
|
|
229
|
+
} from './material-shader-policy';
|
|
230
|
+
export {
|
|
231
|
+
allowsUnlitPreparedFallback,
|
|
232
|
+
isSharedMaterialUserRegionCompatible,
|
|
233
|
+
normalizeMaterialShaderVariantSet,
|
|
234
|
+
resolveMaterialShaderBindingContract,
|
|
235
|
+
resolveMaterialShaderVariantSet,
|
|
236
|
+
resolveMaterialShaderVertexInputContract,
|
|
237
|
+
selectNoColorPbrVariant,
|
|
238
|
+
selectPipelineLayoutForVariant,
|
|
239
|
+
shouldDeferMissingPreparedMaterialShader,
|
|
240
|
+
} from './material-shader-policy';
|
|
241
|
+
export type {
|
|
242
|
+
MeshSsboBufferWrapper,
|
|
243
|
+
MeshSsboGrowController,
|
|
244
|
+
MeshSsboGrowControllerInit,
|
|
245
|
+
MeshSsboGrowDevice,
|
|
246
|
+
MeshSsboGrowErrorRegistry,
|
|
247
|
+
MeshSsboGrowResult,
|
|
248
|
+
MeshSsboState,
|
|
249
|
+
} from './mesh-ssbo-grow';
|
|
250
|
+
export {
|
|
251
|
+
createMeshSsboGrowController,
|
|
252
|
+
deriveStorageBufferCeiling,
|
|
253
|
+
INITIAL_MESH_SSBO_SLOT_COUNT,
|
|
254
|
+
requireMeshSsboBuffer,
|
|
255
|
+
} from './mesh-ssbo-grow';
|
|
256
|
+
export { exposeRenderer } from './renderer-facade';
|
|
257
|
+
export {
|
|
258
|
+
selectHdrpPbrPrewarmVariants,
|
|
259
|
+
selectProbePrewarmVariants,
|
|
260
|
+
selectSkinPrewarmVariants,
|
|
261
|
+
selectStandardPbrTransmissionPrewarmVariants,
|
|
262
|
+
} from './shader-prewarm-policy';
|
|
263
|
+
|
|
264
|
+
function adaptMipmapShaderModuleFactory(
|
|
265
|
+
factory: RhiBackendPack['createShaderModule'],
|
|
266
|
+
): MipmapShaderModuleFactory | undefined {
|
|
267
|
+
if (factory === undefined) return undefined;
|
|
268
|
+
return (device, descriptor) => factory(device as RhiDevice, descriptor);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Re-export registerAdvanceAnimationPlayer so consumers can wire it.
|
|
272
|
+
// Re-export registerPropagateTransforms so consumers can wire the
|
|
273
|
+
// Transform.world mat4 derivation (audio listener sync + picking read the
|
|
274
|
+
// derived Transform.world from scripts).
|
|
275
|
+
|
|
276
|
+
// feat-20260611-fox-skinning-vertex-attribute-chain M4 / w16 (D-4): a single
|
|
277
|
+
// shared empty ArrayBuffer used by `buildPipelineContext` to synthesize the
|
|
278
|
+
// 6-key VertexAttributeMap when no caller-supplied attributes are available
|
|
279
|
+
// for the pbr-skin path. `deriveVertexBufferLayout` is the SSOT for the layout
|
|
280
|
+
// (vertex-attribute-layout.ts) and reads only key presence -- value identity
|
|
281
|
+
// never matters -- so a zero-byte ArrayBuffer per key produces the exact
|
|
282
|
+
// 6-attribute / 72-byte stride layout that the @forgeax::pbr-skin shader's
|
|
283
|
+
// @location(0..5) declarations expect.
|
|
284
|
+
const PBR_SKIN_SENTINEL_ATTR_BUFFER = new ArrayBuffer(0);
|
|
285
|
+
|
|
286
|
+
/** Default 4-attribute vertex layout used as fallback when meshAttributes is undefined. */
|
|
287
|
+
const DEFAULT_VERTEX_ATTRS: VertexAttributeMap = {
|
|
288
|
+
position: new Float32Array(0),
|
|
289
|
+
normal: new Float32Array(0),
|
|
290
|
+
uv: new Float32Array(0),
|
|
291
|
+
tangent: new Float32Array(0),
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const PREPARED_INSTANCE_VERTEX_ATTRS: VertexAttributeMap = {
|
|
295
|
+
position: new Float32Array(0),
|
|
296
|
+
normal: new Float32Array(0),
|
|
297
|
+
uv: new Float32Array(0),
|
|
298
|
+
tangent: new Float32Array(0),
|
|
299
|
+
uv1: new Float32Array(0),
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const PREPARED_MATERIAL_INSTANCE_VERTEX_ATTRS: VertexAttributeMap = {
|
|
303
|
+
position: new Float32Array(0),
|
|
304
|
+
normal: new Float32Array(0),
|
|
305
|
+
uv: new Float32Array(0),
|
|
306
|
+
tangent: new Float32Array(0),
|
|
307
|
+
skinIndex: new Uint16Array(0),
|
|
308
|
+
skinWeight: new Float32Array(0),
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const POSITION_SIZE_COLOR_INSTANCE_VERTEX_BUFFERS = [
|
|
312
|
+
{
|
|
313
|
+
arrayStride: 9 * 4,
|
|
314
|
+
stepMode: 'instance' as const,
|
|
315
|
+
attributes: [
|
|
316
|
+
{ shaderLocation: 0, offset: 0, format: 'float32x3' as const },
|
|
317
|
+
{ shaderLocation: 1, offset: 3 * 4, format: 'float32x2' as const },
|
|
318
|
+
{ shaderLocation: 2, offset: 5 * 4, format: 'float32x4' as const },
|
|
319
|
+
],
|
|
320
|
+
},
|
|
321
|
+
] satisfies readonly GPUVertexBufferLayout[];
|
|
322
|
+
|
|
323
|
+
const BILLBOARD_MATERIAL_INSTANCE_VERTEX_BUFFERS = [
|
|
324
|
+
{
|
|
325
|
+
arrayStride: 31 * 4,
|
|
326
|
+
stepMode: 'instance' as const,
|
|
327
|
+
attributes: [
|
|
328
|
+
{ shaderLocation: 0, offset: 0, format: 'float32x3' as const },
|
|
329
|
+
{ shaderLocation: 1, offset: 3 * 4, format: 'float32x2' as const },
|
|
330
|
+
{ shaderLocation: 2, offset: 5 * 4, format: 'float32x2' as const },
|
|
331
|
+
{ shaderLocation: 3, offset: 7 * 4, format: 'float32x4' as const },
|
|
332
|
+
{ shaderLocation: 4, offset: 11 * 4, format: 'float32x4' as const },
|
|
333
|
+
{ shaderLocation: 5, offset: 15 * 4, format: 'float32x4' as const },
|
|
334
|
+
{ shaderLocation: 6, offset: 19 * 4, format: 'float32x4' as const },
|
|
335
|
+
{ shaderLocation: 7, offset: 23 * 4, format: 'float32x4' as const },
|
|
336
|
+
{ shaderLocation: 8, offset: 27 * 4, format: 'float32x4' as const },
|
|
337
|
+
],
|
|
338
|
+
},
|
|
339
|
+
] satisfies readonly GPUVertexBufferLayout[];
|
|
340
|
+
|
|
341
|
+
const TOPOLOGY_SEGMENT_INSTANCE_VERTEX_BUFFERS = [
|
|
342
|
+
{
|
|
343
|
+
arrayStride: 12 * 4,
|
|
344
|
+
stepMode: 'instance' as const,
|
|
345
|
+
attributes: [
|
|
346
|
+
{ shaderLocation: 0, offset: 0, format: 'float32x3' as const },
|
|
347
|
+
{ shaderLocation: 1, offset: 3 * 4, format: 'float32x3' as const },
|
|
348
|
+
{ shaderLocation: 2, offset: 6 * 4, format: 'float32x4' as const },
|
|
349
|
+
{ shaderLocation: 3, offset: 10 * 4, format: 'float32x2' as const },
|
|
350
|
+
],
|
|
351
|
+
},
|
|
352
|
+
] satisfies readonly GPUVertexBufferLayout[];
|
|
353
|
+
|
|
354
|
+
const MESH_GEOMETRY_MATERIAL_INSTANCE_VERTEX_BUFFERS = [
|
|
355
|
+
{
|
|
356
|
+
arrayStride: 12 * 4,
|
|
357
|
+
stepMode: 'vertex' as const,
|
|
358
|
+
attributes: [
|
|
359
|
+
{ shaderLocation: 0, offset: 0, format: 'float32x3' as const },
|
|
360
|
+
{ shaderLocation: 1, offset: 3 * 4, format: 'float32x3' as const },
|
|
361
|
+
{ shaderLocation: 2, offset: 6 * 4, format: 'float32x2' as const },
|
|
362
|
+
{ shaderLocation: 3, offset: 8 * 4, format: 'float32x4' as const },
|
|
363
|
+
],
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
arrayStride: 28 * 4,
|
|
367
|
+
stepMode: 'instance' as const,
|
|
368
|
+
attributes: [
|
|
369
|
+
{ shaderLocation: 4, offset: 0, format: 'float32x3' as const },
|
|
370
|
+
{ shaderLocation: 5, offset: 3 * 4, format: 'float32x3' as const },
|
|
371
|
+
{ shaderLocation: 6, offset: 6 * 4, format: 'float32x3' as const },
|
|
372
|
+
{ shaderLocation: 7, offset: 9 * 4, format: 'float32x3' as const },
|
|
373
|
+
{ shaderLocation: 8, offset: 12 * 4, format: 'float32x4' as const },
|
|
374
|
+
{ shaderLocation: 9, offset: 16 * 4, format: 'float32x4' as const },
|
|
375
|
+
{ shaderLocation: 10, offset: 20 * 4, format: 'float32x4' as const },
|
|
376
|
+
{ shaderLocation: 11, offset: 24 * 4, format: 'float32x4' as const },
|
|
377
|
+
],
|
|
378
|
+
},
|
|
379
|
+
] satisfies readonly GPUVertexBufferLayout[];
|
|
380
|
+
|
|
381
|
+
function toGpuVertexFormat(
|
|
382
|
+
format: GpuVertexBufferLayoutEntry['attributes'][number]['format'],
|
|
383
|
+
): GPUVertexFormat {
|
|
384
|
+
switch (format) {
|
|
385
|
+
case 'float32x2':
|
|
386
|
+
case 'float32x3':
|
|
387
|
+
case 'float32x4':
|
|
388
|
+
case 'uint16x4':
|
|
389
|
+
return format;
|
|
390
|
+
default:
|
|
391
|
+
throw new Error(`Unsupported geometry vertex format: ${format}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function toGpuVertexBufferLayouts(
|
|
396
|
+
entries: readonly GpuVertexBufferLayoutEntry[],
|
|
397
|
+
): readonly GPUVertexBufferLayout[] {
|
|
398
|
+
return entries.map((entry) => ({
|
|
399
|
+
arrayStride: entry.arrayStride,
|
|
400
|
+
...(entry.stepMode === undefined ? {} : { stepMode: entry.stepMode }),
|
|
401
|
+
attributes: entry.attributes.map((attribute) => ({
|
|
402
|
+
shaderLocation: attribute.shaderLocation,
|
|
403
|
+
offset: attribute.offset,
|
|
404
|
+
format: toGpuVertexFormat(attribute.format),
|
|
405
|
+
})),
|
|
406
|
+
}));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Bundler-layer injection accepted by `createRenderer` (and proxied by
|
|
411
|
+
* `createApp`) as the optional third argument.
|
|
412
|
+
*
|
|
413
|
+
* feat-20260608-create-app-param-surface-trim / M2 / D-3: aggregates the two
|
|
414
|
+
* host-injected build-tool channels:
|
|
415
|
+
*
|
|
416
|
+
* - shaderManifestUrl: the URL the host's vite-plugin-shader emit step
|
|
417
|
+
* wrote `manifest.json` to. When this field is omitted (or `bundler`
|
|
418
|
+
* itself is omitted), createRenderer falls back to
|
|
419
|
+
* '/shaders/manifest.json' (D-2 q5-A) so the LO 1.1 zero-config takeoff
|
|
420
|
+
* path keeps working without explicit injection. Tests can inject via a
|
|
421
|
+
* `data:application/json,...` URL to bypass fetch.
|
|
422
|
+
*
|
|
423
|
+
* - importTransport: dev-only ImportTransport forwarded verbatim to the
|
|
424
|
+
* AssetRegistry third ctor slot so DDC-miss assets can lazy-import.
|
|
425
|
+
* Absent => shipped form (a DDC miss fails fast with `asset-not-imported`).
|
|
426
|
+
*
|
|
427
|
+
* The interface is structurally shared with `@forgeax/engine-app` while this
|
|
428
|
+
* package keeps the renderer independent from the host package.
|
|
429
|
+
*/
|
|
430
|
+
// ─── Backend pack — M3 auto-select internal shape ───────────────────────────
|
|
431
|
+
|
|
432
|
+
// ─── WebGPU branch ──────────────────────────────────────────────────────────
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Wire the spec `device.lost` Promise into the lost / error / health channels
|
|
436
|
+
* (research §F-4 / R2). A genuine device loss fans out through `errorRegistry`
|
|
437
|
+
* so the public `renderer.subscribe` error event triggers, and
|
|
438
|
+
* through `healthRegistry` so `health().reason` flips to `'device-lost'`
|
|
439
|
+
* (feat-20260622-s5 M1/M2). Explicit renderer teardown is observable through
|
|
440
|
+
* `onLost`, but is not a runtime error and must not manufacture an RhiError.
|
|
441
|
+
*
|
|
442
|
+
* Extracted to a single helper (SSOT) so the createRenderer assembly path AND
|
|
443
|
+
* the recover() rebuild path attach byte-identical fan-out to whichever device
|
|
444
|
+
* is current — recover() mints a fresh device whose own `lost` Promise must be
|
|
445
|
+
* re-wired to the SAME registries the host already subscribed to.
|
|
446
|
+
*/
|
|
447
|
+
// Device loss wiring lives in the recovery assembly seam so boot and recovery
|
|
448
|
+
// cannot drift into separate lifecycle event paths.
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* tryCreateWebGPURenderer returns three kinds of outcome (fix-f1 / w16):
|
|
452
|
+
* - `ok` → Renderer created successfully
|
|
453
|
+
* - `rhi-err` → RHI Result.err path (preserves the original RhiError
|
|
454
|
+
* for AI consumers to read; now includes context acquisition
|
|
455
|
+
* failures via pack.rhi.acquireCanvasContext)
|
|
456
|
+
* - `throw` → exception path (caught and wrapped by the caller as
|
|
457
|
+
* an Error)
|
|
458
|
+
*/
|
|
459
|
+
export type WebGPUOutcome =
|
|
460
|
+
| { kind: 'ok'; renderer: RendererAssemblyImplementation }
|
|
461
|
+
| { kind: 'rhi-err'; error: RhiError }
|
|
462
|
+
| { kind: 'throw'; error: Error };
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Goes through the strict two-step path exposed by the injected backend pack:
|
|
466
|
+
* `rhi.requestAdapter()` -> `adapter.requestDevice()`
|
|
467
|
+
* (M6 fix-up [w51] retires the legacy single-step factory per plan-strategy
|
|
468
|
+
* §6 M3 break-point #2 + AGENTS.md break-point list 2026-05-10 #2). Returns
|
|
469
|
+
* `Result<RhiAdapter, RhiError>` then `Result<RhiDevice, RhiError>`; each
|
|
470
|
+
* failure / failure to acquire the canvas WebGPU context returns a
|
|
471
|
+
* structured outcome so the caller can decide on fallback and error-
|
|
472
|
+
* preservation strategy (plan-strategy §7.3).
|
|
473
|
+
*
|
|
474
|
+
* fix-f1: on either Result.err path the original `RhiError` is preserved
|
|
475
|
+
* via the `rhi-err` outcome — when fallback also fails, AI consumers read
|
|
476
|
+
* it via `EngineEnvironmentError.detail.webgpuError.code`.
|
|
477
|
+
*
|
|
478
|
+
* w19 / M4: context acquisition goes through `pack.rhi.acquireCanvasContext(canvas)`
|
|
479
|
+
* — a polymorphic entry owned by the selected backend adapter.
|
|
480
|
+
* The backend pack is selected BEFORE this call (in `loadBackendPack` /
|
|
481
|
+
* the Channel 2→3 fallback in `createRenderer`), so the canvas context
|
|
482
|
+
* is never type-locked by a wrong backend's acquisition attempt.
|
|
483
|
+
*/
|
|
484
|
+
let nextDerivedSystemId = 0;
|
|
485
|
+
|
|
486
|
+
export async function tryCreateWebGPURenderer(
|
|
487
|
+
canvas: HTMLCanvasElement | OffscreenCanvas,
|
|
488
|
+
options: RendererOptions | undefined,
|
|
489
|
+
pack: RhiBackendPack,
|
|
490
|
+
// feat-20260608 M2 / D-3: BundlerOptions threaded verbatim. Carries
|
|
491
|
+
// shaderManifestUrl (consumed in getShader fallback ~30 lines below) and
|
|
492
|
+
// importTransport (forwarded to the AssetRegistry third ctor slot;
|
|
493
|
+
// undefined keeps the shipped form, AC-08).
|
|
494
|
+
bundler: BundlerOptions | undefined,
|
|
495
|
+
): Promise<WebGPUOutcome> {
|
|
496
|
+
const importTransport = bundler?.importTransport;
|
|
497
|
+
// M6 fix-up [w51]: spec-aligned two-step path. Step 1 - requestAdapter()
|
|
498
|
+
// surfaces adapter.features / adapter.limits (capability pre-screen,
|
|
499
|
+
// charter proposition 4 forward-reachable). Step 2 - adapter.requestDevice()
|
|
500
|
+
// requests the actual device. M3 D-P4: `pack.rhi` is the auto-selected
|
|
501
|
+
// backend singleton (rhi-webgpu / rhi-wgpu / explicit escape hatch).
|
|
502
|
+
// w19: thread canvas as compatibleSurface so the wgpu GL backend can
|
|
503
|
+
// enumerate adapters against it (escape hatch, capability-gated).
|
|
504
|
+
const adapterResult = await pack.rhi.requestAdapter(undefined, canvas);
|
|
505
|
+
if (!adapterResult.ok) {
|
|
506
|
+
return { kind: 'rhi-err', error: adapterResult.error };
|
|
507
|
+
}
|
|
508
|
+
const adapter = adapterResult.value;
|
|
509
|
+
// M4 w28: filter compression features from adapter.features (AC-07). The
|
|
510
|
+
// same helper is used by recover() below so both paths share one profile.
|
|
511
|
+
const deviceOpts = deviceOptionsForAdapter(adapter, options);
|
|
512
|
+
const result = await adapter.requestDevice(deviceOpts);
|
|
513
|
+
if (!result.ok) {
|
|
514
|
+
return { kind: 'rhi-err', error: result.error };
|
|
515
|
+
}
|
|
516
|
+
const device: RhiDevice = result.value;
|
|
517
|
+
|
|
518
|
+
// Acquire the canvas context through the backend pack (M3 / w16).
|
|
519
|
+
// rhi-webgpu: internally calls canvas.getContext('webgpu') + branded wrap.
|
|
520
|
+
// rhi-wgpu: internally calls wasm createSurface(canvas) + configure.
|
|
521
|
+
// When context acquisition fails (null from canvas.getContext or wasm
|
|
522
|
+
// createSurface throws), the pack returns Result.err(RhiError).
|
|
523
|
+
const ctxResult = pack.rhi.acquireCanvasContext(canvas);
|
|
524
|
+
if (!ctxResult.ok) {
|
|
525
|
+
return { kind: 'rhi-err', error: ctxResult.error };
|
|
526
|
+
}
|
|
527
|
+
const context: RhiCanvasContext = ctxResult.value;
|
|
528
|
+
|
|
529
|
+
const lostRegistry = new LostListenerRegistry();
|
|
530
|
+
const errorRegistry = new RhiErrorListenerRegistry();
|
|
531
|
+
const healthRegistry = new HealthListenerRegistry();
|
|
532
|
+
const lossObserver: {
|
|
533
|
+
current?: (detail: string) => void;
|
|
534
|
+
} = {};
|
|
535
|
+
const generationState: {
|
|
536
|
+
current: number;
|
|
537
|
+
onStaleLoss?: () => void;
|
|
538
|
+
} = { current: 0 };
|
|
539
|
+
// device.lost dual-track (research §F-4 / R2 countermeasure): the spec
|
|
540
|
+
// Promise is passed through to the lost-fan-out registry (extracted to
|
|
541
|
+
// attachDeviceLostFanout so the recover() rebuild path re-attaches the
|
|
542
|
+
// SAME wiring to the freshly-minted device — SSOT, one fan-out shape).
|
|
543
|
+
attachDeviceLostFanout(device, pack, {
|
|
544
|
+
lostRegistry,
|
|
545
|
+
errorRegistry,
|
|
546
|
+
healthRegistry,
|
|
547
|
+
generation: 0,
|
|
548
|
+
currentGeneration: () => generationState.current,
|
|
549
|
+
onStaleLoss: () => generationState.onStaleLoss?.(),
|
|
550
|
+
onDeviceLost: (detail) => lossObserver.current?.(detail),
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// D-VD2 Round 2 wire-up part 2: register the spec `onuncapturederror`
|
|
554
|
+
// listener on the raw GPUDevice so GPUUncapturedErrorEvent (validation /
|
|
555
|
+
// oom / internal) is translated to the 17-member RhiErrorCode union +
|
|
556
|
+
// dispatched through the public Renderer event subscription. When the pack does not expose a
|
|
557
|
+
// raw-device escape hatch (rhi-wgpu wasm path, custom escape hatch), the
|
|
558
|
+
// listener registration is skipped — the device.lost dual-channel above
|
|
559
|
+
// still fires through `RhiDevice.lost` Promise on every path (graceful
|
|
560
|
+
// degradation per charter proposition 9; per-path coverage matrix in
|
|
561
|
+
// packages/engine/README.md `## Error model`).
|
|
562
|
+
if (pack._internal_getRawDevice && pack.translateErrorEventToRhiError) {
|
|
563
|
+
const backendDevice = pack._internal_getRawDevice(device);
|
|
564
|
+
if (backendDevice && typeof backendDevice === 'object') {
|
|
565
|
+
const target = backendDevice as {
|
|
566
|
+
onuncapturederror?: ((event: unknown) => void) | null;
|
|
567
|
+
};
|
|
568
|
+
// Spec form: `device.onuncapturederror = (event) => ...`. The listener
|
|
569
|
+
// sets a property handler (spec normative GPUDevice extends EventTarget
|
|
570
|
+
// and exposes `onuncapturederror` as a settable callback property);
|
|
571
|
+
// engine writes through the property so each pre-existing handler that
|
|
572
|
+
// an AI user assigned would be replaced (last-write-wins; spec lifetime
|
|
573
|
+
// is device-scoped so engine ownership is OK — AI users observe via
|
|
574
|
+
// the Renderer event subscription, not the raw property).
|
|
575
|
+
target.onuncapturederror = (event: unknown): void => {
|
|
576
|
+
const translated = pack.translateErrorEventToRhiError?.(event);
|
|
577
|
+
if (translated && !translated.ok) {
|
|
578
|
+
errorRegistry.fire(translated.error);
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return {
|
|
584
|
+
kind: 'ok',
|
|
585
|
+
renderer: await makeWebGPURenderer({
|
|
586
|
+
canvas,
|
|
587
|
+
device,
|
|
588
|
+
context,
|
|
589
|
+
options,
|
|
590
|
+
bundler,
|
|
591
|
+
lostRegistry,
|
|
592
|
+
errorRegistry,
|
|
593
|
+
healthRegistry,
|
|
594
|
+
pack,
|
|
595
|
+
importTransport,
|
|
596
|
+
lossObserver,
|
|
597
|
+
generationState,
|
|
598
|
+
}),
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
export interface WebGPURendererInternals {
|
|
603
|
+
canvas: HTMLCanvasElement | OffscreenCanvas;
|
|
604
|
+
device: RhiDevice;
|
|
605
|
+
// M6 / w41 (feat-20260510-rhi-resource-creation): forgeax RhiCanvasContext
|
|
606
|
+
// brand; RenderSystem + ensureContextConfigured both go through the RHI
|
|
607
|
+
// surface (charter proposition 5 consistent abstraction red line).
|
|
608
|
+
context: RhiCanvasContext;
|
|
609
|
+
options: RendererOptions | undefined;
|
|
610
|
+
/**
|
|
611
|
+
* feat-20260608-create-app-param-surface-trim / M2 / D-3: BundlerOptions
|
|
612
|
+
* forwarded verbatim from createRenderer's third arg. Carries
|
|
613
|
+
* shaderManifestUrl (host-injected vite-plugin-shader emit URL; absent =>
|
|
614
|
+
* createRenderer falls back to '/shaders/manifest.json') and the
|
|
615
|
+
* importTransport that previously rode a dedicated internal slot.
|
|
616
|
+
*/
|
|
617
|
+
bundler: BundlerOptions | undefined;
|
|
618
|
+
lostRegistry: LostListenerRegistry;
|
|
619
|
+
errorRegistry: RhiErrorListenerRegistry;
|
|
620
|
+
healthRegistry: HealthListenerRegistry;
|
|
621
|
+
/** M3 D-P4 auto-select pack — carries the dynamic-imported rhi-webgpu / rhi-wgpu singleton + optional async shader factory. */
|
|
622
|
+
pack: RhiBackendPack;
|
|
623
|
+
/**
|
|
624
|
+
* feat-20260604-hdr-equirect-cube-importer-loader M4 / w16 (D-3): the
|
|
625
|
+
* dev-only ImportTransport, forwarded verbatim from createRenderer to the
|
|
626
|
+
* AssetRegistry third ctor slot. `undefined` keeps the shipped form (DDC
|
|
627
|
+
* miss fails fast with `asset-not-imported`, AC-08).
|
|
628
|
+
*/
|
|
629
|
+
importTransport?: ImportTransport | undefined;
|
|
630
|
+
/**
|
|
631
|
+
* feat-20260608-mesh-ssbo-dynamic-grow-l1-lift-1024-entity-cap M2 / T-M2-05:
|
|
632
|
+
* mesh-SSBO grow hook surfaced by `makeWebGPURenderer` after the controller
|
|
633
|
+
* is constructed. Undefined before `makeWebGPURenderer` runs; populated by
|
|
634
|
+
* the time the renderer is returned to the caller. M3's record stage reads
|
|
635
|
+
* this field via the runtime path that bridges into render-system-record.ts
|
|
636
|
+
* (the bridging plumbing is in M3's targetFiles).
|
|
637
|
+
*
|
|
638
|
+
* Returns `{ ok: true }` when the grow completed (or short-circuited
|
|
639
|
+
* idempotently); `{ ok: false, code }` with a fired structured error
|
|
640
|
+
* (`MeshSsboCeilingReachedError` or `MeshSsboCapacityExceededError`) when
|
|
641
|
+
* the request cannot be satisfied. Never throws (D-5).
|
|
642
|
+
*/
|
|
643
|
+
growMeshSsbo?: (neededSlots: number) => MeshSsboGrowResult;
|
|
644
|
+
/**
|
|
645
|
+
* feat-20260608-mesh-ssbo-dynamic-grow-l1-lift-1024-entity-cap M3 / T-M3-04:
|
|
646
|
+
* read-only handle to the grow controller's state (slotCount + wrapper
|
|
647
|
+
* refs), surfaced alongside `growMeshSsbo` so `ensureMeshSsboCapacity`
|
|
648
|
+
* (record-stage wiring) can short-circuit when slotCount already covers the
|
|
649
|
+
* frame's renderable count and so the dev-mode info log can report the
|
|
650
|
+
* before/after slot count transition. Undefined until buildReadyWebGPU
|
|
651
|
+
* has wired the controller; identity stable across grow events.
|
|
652
|
+
*/
|
|
653
|
+
meshSsboState?: MeshSsboState;
|
|
654
|
+
/** Host-owned producer features; created before device-bound assembly starts. */
|
|
655
|
+
featureHost?: RenderFeatureHost;
|
|
656
|
+
/** App-owned overlay capability read by the typed graph at frame time. */
|
|
657
|
+
debugOverlay?: RenderDebugOverlay;
|
|
658
|
+
/** Composed renderer utility sources installed atomically at build-ready. */
|
|
659
|
+
volumetricFogShaders?: VolumetricFogShaderSources | undefined;
|
|
660
|
+
gpuPassTimingSubmittedWork?: Promise<void> | undefined;
|
|
661
|
+
lossObserver: { current?: (detail: string) => void };
|
|
662
|
+
generationState: {
|
|
663
|
+
current: number;
|
|
664
|
+
onStaleLoss?: () => void;
|
|
665
|
+
};
|
|
666
|
+
recoveryColdWorkGuard?: RecoveryColdWorkGuard | undefined;
|
|
667
|
+
getMaterialShaderUvSetCount?: (materialShaderId: string) => number | undefined;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
export interface RecoveryFailureLocation {
|
|
671
|
+
readonly phase: RecoveryPhase;
|
|
672
|
+
readonly retryable: boolean;
|
|
673
|
+
readonly guidance: RecoveryGuidance;
|
|
674
|
+
readonly owner: string;
|
|
675
|
+
readonly resourceKind: DeviceResourceKind;
|
|
676
|
+
readonly cause: unknown;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function recoveryCauseCode(cause: unknown): string | undefined {
|
|
680
|
+
if (typeof cause !== 'object' || cause === null) return undefined;
|
|
681
|
+
const directCode = (cause as { readonly code?: unknown }).code;
|
|
682
|
+
if (typeof directCode === 'string') return directCode;
|
|
683
|
+
const detail = (cause as { readonly detail?: unknown }).detail;
|
|
684
|
+
if (typeof detail !== 'object' || detail === null) return undefined;
|
|
685
|
+
const detailCode = (detail as { readonly code?: unknown }).code;
|
|
686
|
+
if (typeof detailCode === 'string') return detailCode;
|
|
687
|
+
return recoveryCauseCode((detail as { readonly cause?: unknown }).cause);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function createRecoveryFailureLocation(
|
|
691
|
+
phase: RecoveryPhase,
|
|
692
|
+
cause: unknown,
|
|
693
|
+
overrides: Partial<Omit<RecoveryFailureLocation, 'phase' | 'cause'>> = {},
|
|
694
|
+
): RecoveryFailureLocation {
|
|
695
|
+
const code = recoveryCauseCode(cause) ?? '';
|
|
696
|
+
const resourceKind: DeviceResourceKind = code.includes('shader')
|
|
697
|
+
? 'shader'
|
|
698
|
+
: code.includes('buffer')
|
|
699
|
+
? 'buffer'
|
|
700
|
+
: code.includes('texture')
|
|
701
|
+
? 'texture'
|
|
702
|
+
: code.includes('binding')
|
|
703
|
+
? 'binding'
|
|
704
|
+
: phase === 'rehydrate'
|
|
705
|
+
? 'shader'
|
|
706
|
+
: phase === 'acquire-adapter' || phase === 'acquire-device'
|
|
707
|
+
? 'surface'
|
|
708
|
+
: phase === 'publish' || phase === 'cleanup'
|
|
709
|
+
? 'surface'
|
|
710
|
+
: 'pipeline';
|
|
711
|
+
const owner =
|
|
712
|
+
code.includes('shader') || phase === 'rehydrate'
|
|
713
|
+
? 'shader'
|
|
714
|
+
: phase === 'acquire-adapter' || phase === 'acquire-device'
|
|
715
|
+
? 'backend'
|
|
716
|
+
: phase === 'compile-graph'
|
|
717
|
+
? 'pipeline'
|
|
718
|
+
: 'renderer';
|
|
719
|
+
return {
|
|
720
|
+
phase,
|
|
721
|
+
retryable: false,
|
|
722
|
+
guidance: 'repair-owner',
|
|
723
|
+
owner,
|
|
724
|
+
resourceKind,
|
|
725
|
+
cause,
|
|
726
|
+
...overrides,
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
async function makeWebGPURenderer(
|
|
731
|
+
internals: WebGPURendererInternals,
|
|
732
|
+
): Promise<RendererAssemblyImplementation> {
|
|
733
|
+
let disposed = false;
|
|
734
|
+
const frameContinuations = new Set<ContinuationTerminator>();
|
|
735
|
+
type RecoveryInspection = RenderInspection['recovery'];
|
|
736
|
+
let recoveryAttempt = 0;
|
|
737
|
+
let recoveryStaleLossEvents = 0;
|
|
738
|
+
let recoveryInspection: RecoveryInspection = Object.freeze({
|
|
739
|
+
phase: null,
|
|
740
|
+
fromGeneration: 0,
|
|
741
|
+
candidateGeneration: 0,
|
|
742
|
+
attempt: 0,
|
|
743
|
+
elapsedMs: 0,
|
|
744
|
+
lastOutcome: 'none',
|
|
745
|
+
rehydratedRoots: 0,
|
|
746
|
+
staleLossEvents: 0,
|
|
747
|
+
});
|
|
748
|
+
const updateRecoveryInspection = (
|
|
749
|
+
patch: Partial<RecoveryInspection>,
|
|
750
|
+
clearFailureLocation = false,
|
|
751
|
+
): void => {
|
|
752
|
+
const next = { ...recoveryInspection, ...patch, staleLossEvents: recoveryStaleLossEvents };
|
|
753
|
+
if (clearFailureLocation) {
|
|
754
|
+
delete next.failedOwner;
|
|
755
|
+
delete next.failedResourceKind;
|
|
756
|
+
}
|
|
757
|
+
recoveryInspection = Object.freeze(next);
|
|
758
|
+
};
|
|
759
|
+
internals.generationState.onStaleLoss = () => {
|
|
760
|
+
recoveryStaleLossEvents = Math.min(Number.MAX_SAFE_INTEGER, recoveryStaleLossEvents + 1);
|
|
761
|
+
updateRecoveryInspection({});
|
|
762
|
+
};
|
|
763
|
+
internals.lossObserver.current = (detail) => {
|
|
764
|
+
const code = detail.includes('destroyed') ? 'disposed' : 'device-lost';
|
|
765
|
+
for (const continuation of frameContinuations) continuation.terminate({ code });
|
|
766
|
+
};
|
|
767
|
+
let renderTargetDevice = internals.device;
|
|
768
|
+
let renderTargetGeneration = 0;
|
|
769
|
+
let renderSystemForTargetPromotion: RenderSystem | undefined;
|
|
770
|
+
const featureHostResult = createRenderFeatureHost(internals.options?.features ?? []);
|
|
771
|
+
if (!featureHostResult.ok) throw featureHostResult.error;
|
|
772
|
+
internals.featureHost = featureHostResult.value;
|
|
773
|
+
let rendererRecovery: RendererRecovery | undefined;
|
|
774
|
+
const recoveryFlight = createSingleFlight<Result<void, RecoverFailure>>(async () => {
|
|
775
|
+
if (disposed) return err(new RecoverError('recover-not-needed'));
|
|
776
|
+
if (internals.healthRegistry.getLastSnapshot().reason !== 'device-lost') {
|
|
777
|
+
return err(new RecoverError('recover-not-needed'));
|
|
778
|
+
}
|
|
779
|
+
const attempt = ++recoveryAttempt;
|
|
780
|
+
const deadline = createRecoveryDeadline(Date.now());
|
|
781
|
+
const fromGeneration = activeDeviceScope.generation;
|
|
782
|
+
const candidateGeneration = fromGeneration + 1;
|
|
783
|
+
let recoveryFailurePhase: RecoveryPhase = 'quiesce';
|
|
784
|
+
updateRecoveryInspection(
|
|
785
|
+
{
|
|
786
|
+
phase: 'quiesce',
|
|
787
|
+
fromGeneration,
|
|
788
|
+
candidateGeneration,
|
|
789
|
+
attempt,
|
|
790
|
+
elapsedMs: 0,
|
|
791
|
+
lastOutcome: 'none',
|
|
792
|
+
rehydratedRoots: 0,
|
|
793
|
+
},
|
|
794
|
+
true,
|
|
795
|
+
);
|
|
796
|
+
const setRecoveryPhase = (phase: RecoveryPhase): void => {
|
|
797
|
+
if (phase !== 'cleanup') recoveryFailurePhase = phase;
|
|
798
|
+
updateRecoveryInspection({ phase, elapsedMs: deadline.elapsed(Date.now()) });
|
|
799
|
+
};
|
|
800
|
+
let candidateCleanup = (): void => undefined;
|
|
801
|
+
const continuation = createRecoveryContinuation(deadline, () => candidateCleanup());
|
|
802
|
+
const deadlineExpired = (phase: RecoveryPhase): boolean => {
|
|
803
|
+
if (continuation.isValid(phase, Date.now())) return false;
|
|
804
|
+
continuation.abandon(Date.now());
|
|
805
|
+
continuation.cleanupOnce();
|
|
806
|
+
return true;
|
|
807
|
+
};
|
|
808
|
+
let recoveryFailureLocation: RecoveryFailureLocation | undefined;
|
|
809
|
+
let result: Result<void, RecoverFailure>;
|
|
810
|
+
try {
|
|
811
|
+
const recovery = rendererRecovery;
|
|
812
|
+
if (recovery === undefined) {
|
|
813
|
+
return err(new RecoverError('recover-device-unavailable'));
|
|
814
|
+
}
|
|
815
|
+
result = await recovery.recoverOnce(
|
|
816
|
+
deadline,
|
|
817
|
+
deadlineExpired,
|
|
818
|
+
(cleanup) => {
|
|
819
|
+
candidateCleanup = cleanup;
|
|
820
|
+
},
|
|
821
|
+
continuation,
|
|
822
|
+
setRecoveryPhase,
|
|
823
|
+
(count) => updateRecoveryInspection({ rehydratedRoots: count }),
|
|
824
|
+
(failure) => {
|
|
825
|
+
recoveryFailureLocation = failure;
|
|
826
|
+
},
|
|
827
|
+
);
|
|
828
|
+
} catch (cause) {
|
|
829
|
+
recoveryFailureLocation ??= createRecoveryFailureLocation('compile-graph', cause);
|
|
830
|
+
candidateCleanup();
|
|
831
|
+
result = err(new RecoverError('recover-device-unavailable'));
|
|
832
|
+
}
|
|
833
|
+
if (result.ok) {
|
|
834
|
+
updateRecoveryInspection(
|
|
835
|
+
{
|
|
836
|
+
phase: null,
|
|
837
|
+
fromGeneration,
|
|
838
|
+
candidateGeneration: activeDeviceScope.generation,
|
|
839
|
+
elapsedMs: deadline.elapsed(Date.now()),
|
|
840
|
+
lastOutcome: 'succeeded',
|
|
841
|
+
},
|
|
842
|
+
true,
|
|
843
|
+
);
|
|
844
|
+
return result;
|
|
845
|
+
}
|
|
846
|
+
if (result.error.code === 'recover-not-needed' && !disposed) {
|
|
847
|
+
updateRecoveryInspection(
|
|
848
|
+
{ phase: null, elapsedMs: deadline.elapsed(Date.now()), lastOutcome: 'none' },
|
|
849
|
+
true,
|
|
850
|
+
);
|
|
851
|
+
return result;
|
|
852
|
+
}
|
|
853
|
+
const outcome = disposed ? 'disposed' : 'failed';
|
|
854
|
+
const failureLocation = outcome === 'disposed' ? undefined : recoveryFailureLocation;
|
|
855
|
+
const failurePhase =
|
|
856
|
+
outcome === 'disposed' ? 'cleanup' : (failureLocation?.phase ?? recoveryFailurePhase);
|
|
857
|
+
const isAcquisitionPhase = (phase: RecoveryPhase): boolean =>
|
|
858
|
+
phase === 'acquire-adapter' || phase === 'acquire-device';
|
|
859
|
+
const retryable =
|
|
860
|
+
outcome === 'failed' && (failureLocation?.retryable ?? isAcquisitionPhase(failurePhase));
|
|
861
|
+
const failure = createRecoveryFailedError({
|
|
862
|
+
phase: failurePhase,
|
|
863
|
+
oldGeneration: fromGeneration,
|
|
864
|
+
candidateGeneration,
|
|
865
|
+
attempt,
|
|
866
|
+
elapsedMs: deadline.elapsed(Date.now()),
|
|
867
|
+
retryable,
|
|
868
|
+
guidance:
|
|
869
|
+
outcome === 'disposed'
|
|
870
|
+
? 'rebuild-renderer'
|
|
871
|
+
: (failureLocation?.guidance ?? (retryable ? 'retry' : 'repair-owner')),
|
|
872
|
+
owner: outcome === 'disposed' ? 'renderer' : (failureLocation?.owner ?? 'renderer'),
|
|
873
|
+
resourceKind:
|
|
874
|
+
outcome === 'disposed' ? 'surface' : (failureLocation?.resourceKind ?? 'pipeline'),
|
|
875
|
+
lastOutcome: outcome,
|
|
876
|
+
rehydratedRoots: recoveryInspection.rehydratedRoots,
|
|
877
|
+
staleLossEvents: recoveryStaleLossEvents,
|
|
878
|
+
cause: outcome === 'disposed' ? result.error : (failureLocation?.cause ?? result.error),
|
|
879
|
+
cleanupFailures: [],
|
|
880
|
+
receipt: activeDeviceScope._receipt(),
|
|
881
|
+
});
|
|
882
|
+
updateRecoveryInspection(
|
|
883
|
+
{
|
|
884
|
+
phase: null,
|
|
885
|
+
elapsedMs: failure.detail.elapsedMs,
|
|
886
|
+
lastOutcome: outcome,
|
|
887
|
+
rehydratedRoots: 0,
|
|
888
|
+
},
|
|
889
|
+
false,
|
|
890
|
+
);
|
|
891
|
+
return err(failure);
|
|
892
|
+
});
|
|
893
|
+
// Keep producer-declared shader identities live across the renderer
|
|
894
|
+
// lifetime. Features can be installed after boot (the public late-install
|
|
895
|
+
// seam used by asset-driven hosts), so a recovery rebuild must include their
|
|
896
|
+
// modules in the same prewarm set as boot. A boot-only snapshot would let the
|
|
897
|
+
// first recovered frame observe a cold async shader adapter and emit a
|
|
898
|
+
// spurious prepared-pipeline failure before the next-frame retry succeeds.
|
|
899
|
+
const requiredMaterialShaderSet = new Set(
|
|
900
|
+
featureHostResult.value.features.flatMap((feature) => feature.requiredMaterialShaders ?? []),
|
|
901
|
+
);
|
|
902
|
+
let requiredMaterialShaders = Object.freeze([...requiredMaterialShaderSet]);
|
|
903
|
+
const collectRequiredFullscreenPostProcesses = (
|
|
904
|
+
features: readonly RenderFeature<unknown>[],
|
|
905
|
+
): readonly { readonly identity: string; readonly source: string }[] => {
|
|
906
|
+
const entries = new Map<string, { readonly identity: string; readonly source: string }>();
|
|
907
|
+
for (const feature of features) {
|
|
908
|
+
for (const entry of feature.requiredFullscreenPostProcesses ?? []) {
|
|
909
|
+
const existing = entries.get(entry.identity);
|
|
910
|
+
if (existing !== undefined && existing.source !== entry.source) {
|
|
911
|
+
throw new RhiError({
|
|
912
|
+
code: 'internal-error',
|
|
913
|
+
expected: `fullscreen identity '${entry.identity}' has one source across render features`,
|
|
914
|
+
hint: 'rename the conflicting fullscreen identity or make its WGSL source identical',
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
entries.set(entry.identity, entry);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return Object.freeze([...entries.values()]);
|
|
921
|
+
};
|
|
922
|
+
let requiredFullscreenPostProcesses = collectRequiredFullscreenPostProcesses(
|
|
923
|
+
featureHostResult.value.features,
|
|
924
|
+
);
|
|
925
|
+
// Lazy ShaderCatalog instance (plan-strategy section S-10 / D-R10 / OQ-5
|
|
926
|
+
// close): constructed on first access; subsequent accesses return the
|
|
927
|
+
// same instance. Recovery owns a second, candidate-only state object while
|
|
928
|
+
// the active state continues serving frames. The two states are swapped
|
|
929
|
+
// only by the synchronous generation publication boundary below; no
|
|
930
|
+
// recovery await can expose a partially rebuilt adapter or catalog.
|
|
931
|
+
type RendererShaderState = {
|
|
932
|
+
readonly device: RhiDevice;
|
|
933
|
+
shaderInstance: ShaderCatalog | null;
|
|
934
|
+
sharedShaderModuleAdapter: ShaderDeviceAdapterInternal | null;
|
|
935
|
+
sharedImmediateShaderModuleAdapter: ShaderDeviceAdapterInternal | null;
|
|
936
|
+
};
|
|
937
|
+
let activeShaderState: RendererShaderState = {
|
|
938
|
+
device: internals.device,
|
|
939
|
+
shaderInstance: null,
|
|
940
|
+
sharedShaderModuleAdapter: null,
|
|
941
|
+
sharedImmediateShaderModuleAdapter: null,
|
|
942
|
+
};
|
|
943
|
+
let candidateShaderState: RendererShaderState | undefined;
|
|
944
|
+
const getShaderModuleAdapter = (): ShaderDeviceAdapterInternal => {
|
|
945
|
+
const state = candidateShaderState ?? activeShaderState;
|
|
946
|
+
if (state.sharedShaderModuleAdapter === null) {
|
|
947
|
+
state.sharedShaderModuleAdapter = makeShaderDeviceAdapter(
|
|
948
|
+
state.device,
|
|
949
|
+
internals.errorRegistry,
|
|
950
|
+
internals.pack.createShaderModule,
|
|
951
|
+
internals.pack.createShaderModuleImmediate,
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
return state.sharedShaderModuleAdapter;
|
|
955
|
+
};
|
|
956
|
+
const getImmediateShaderModuleAdapter = (): ShaderDeviceAdapterInternal => {
|
|
957
|
+
const state = candidateShaderState ?? activeShaderState;
|
|
958
|
+
if (state.sharedImmediateShaderModuleAdapter === null) {
|
|
959
|
+
state.sharedImmediateShaderModuleAdapter = makeShaderDeviceAdapter(
|
|
960
|
+
state.device,
|
|
961
|
+
internals.errorRegistry,
|
|
962
|
+
internals.pack.createShaderModule,
|
|
963
|
+
internals.pack.createShaderModuleImmediate,
|
|
964
|
+
'immediate',
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
return state.sharedImmediateShaderModuleAdapter;
|
|
968
|
+
};
|
|
969
|
+
const prewarmFullscreenFeatureModules = async (
|
|
970
|
+
feature: RenderFeature<unknown>,
|
|
971
|
+
): Promise<Result<void, RhiError>> => {
|
|
972
|
+
for (const postProcess of feature.requiredFullscreenPostProcesses ?? []) {
|
|
973
|
+
const label = postProcessShaderModuleLabel(postProcess.identity, postProcess.source);
|
|
974
|
+
const shaderResult = internals.pack.createShaderModule
|
|
975
|
+
? await internals.pack.createShaderModule(internals.device, {
|
|
976
|
+
code: postProcess.source,
|
|
977
|
+
label,
|
|
978
|
+
})
|
|
979
|
+
: await invokeDeviceCreateShaderModule(internals.device, {
|
|
980
|
+
code: postProcess.source,
|
|
981
|
+
label,
|
|
982
|
+
});
|
|
983
|
+
if (!shaderResult.ok) {
|
|
984
|
+
internals.errorRegistry.fire(shaderResult.error);
|
|
985
|
+
return err(shaderResult.error);
|
|
986
|
+
}
|
|
987
|
+
getShaderModuleAdapter().seedModule(label, shaderResult.value);
|
|
988
|
+
}
|
|
989
|
+
return ok(undefined);
|
|
990
|
+
};
|
|
991
|
+
const getShader = (): ShaderCatalog => {
|
|
992
|
+
const state = candidateShaderState ?? activeShaderState;
|
|
993
|
+
if (state.shaderInstance === null) {
|
|
994
|
+
// feat-20260608-create-app-param-surface-trim / M2 / D-2 q5-A:
|
|
995
|
+
// shaderManifestUrl moved to BundlerOptions (third arg). The fallback
|
|
996
|
+
// literal '/shaders/manifest.json' stays here so the LO 1.1
|
|
997
|
+
// hello-window zero-config takeoff path keeps working without an
|
|
998
|
+
// bundler injection. The 'shaderManifestUrl' in (...) check preserves
|
|
999
|
+
// the zero-entry opt-in: explicitly passing
|
|
1000
|
+
// `bundler: { shaderManifestUrl: undefined }` retains the
|
|
1001
|
+
// old "no manifest fetched" mode used by tests / camera-only worlds.
|
|
1002
|
+
const bundler = internals.bundler;
|
|
1003
|
+
const explicitUrl = bundler?.shaderManifestUrl;
|
|
1004
|
+
state.shaderInstance = new ShaderCatalog({
|
|
1005
|
+
device: getShaderModuleAdapter(),
|
|
1006
|
+
manifestUrl:
|
|
1007
|
+
bundler !== undefined && 'shaderManifestUrl' in bundler
|
|
1008
|
+
? explicitUrl
|
|
1009
|
+
: '/shaders/manifest.json',
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
return state.shaderInstance;
|
|
1013
|
+
};
|
|
1014
|
+
// D-S9: AssetRegistry instance shared with RenderSystem. RenderSystem
|
|
1015
|
+
// looks up MeshFilter.assetHandle here per frame; buildReadyWebGPU step 3
|
|
1016
|
+
// uploads the registry's builtin mesh geometry to GPU buffers so the
|
|
1017
|
+
// RenderSystem can route MeshFilter.assetHandle to a real (vbo, ibo) pair.
|
|
1018
|
+
//
|
|
1019
|
+
// feat-20260527 M1 / w1: ShaderCatalog is now eager-constructed and
|
|
1020
|
+
// constructor-injected into AssetRegistry (plan-strategy D-1). The
|
|
1021
|
+
// ShaderCatalogDevice adapter (shared between the catalog and per-MaterialShader
|
|
1022
|
+
// pipeline cache) is already available at this point.
|
|
1023
|
+
const shaderCatalog = getShader();
|
|
1024
|
+
|
|
1025
|
+
// feat-20260528-material-shader-registration-unification M3 / w14:
|
|
1026
|
+
// placeholder hardcoded installMaterialArtifact calls deleted.
|
|
1027
|
+
//
|
|
1028
|
+
// bug-20260601-hello-tonemap-material-register M1 (plan-strategy D-1):
|
|
1029
|
+
// All material-shader entries are now registered from the manifest
|
|
1030
|
+
// BEFORE the renderer is returned, so `register<MaterialAsset>` referencing
|
|
1031
|
+
// an engine shader succeeds without waiting for `renderer.initialization`.
|
|
1032
|
+
|
|
1033
|
+
// feat-20260623-asset-payload-generic-open-registry M3 / w10: host apps
|
|
1034
|
+
// that need custom loaders register them on `assets.loaders.register(...)`.
|
|
1035
|
+
// feat-20260604-hdr-equirect-cube-importer-loader M4 / w16 (D-3 / AC-05):
|
|
1036
|
+
// the host-injected ImportTransport (or undefined for the shipped form) is
|
|
1037
|
+
// threaded into the AssetRegistry ctor -- the construction-time-
|
|
1038
|
+
// only single injection point (no setter, no illegal intermediate state).
|
|
1039
|
+
// feat-20260705-runtime-tier2-decomposition M1 / w10 (D-1 / D-2): the sole
|
|
1040
|
+
// production assembly point injects (a) the post-spawn hook
|
|
1041
|
+
// `postSpawnResolveJoints` (auto-wire Skin.joints; D-1) and the audio
|
|
1042
|
+
// catalog-entry loader. The latter stays at this assembly boundary so
|
|
1043
|
+
// assets-runtime remains independent of the concrete Web Audio backend.
|
|
1044
|
+
// videoLoader is wired internally from graphics-extras; the full registry has
|
|
1045
|
+
// 11 engine kinds. This is the only `new AssetRegistry(...)` call in the repo.
|
|
1046
|
+
const assets = new AssetRegistry(
|
|
1047
|
+
shaderCatalog,
|
|
1048
|
+
internals.importTransport,
|
|
1049
|
+
[audioLoader],
|
|
1050
|
+
postSpawnResolveJoints,
|
|
1051
|
+
);
|
|
1052
|
+
// feat-20260601-device/gpu-residency-extraction M1: the GPU residency layer
|
|
1053
|
+
// lives in a standalone store; `assets` keeps the CPU POD registry only.
|
|
1054
|
+
let gpuStore = new GpuResidencyCache();
|
|
1055
|
+
let activeDeviceScope = DeviceScope.create(0, 'renderer');
|
|
1056
|
+
// feat-20260623-world-space-video-asset M4 / w16 (D-3): transient per-frame
|
|
1057
|
+
// video texture store, fully independent of gpuStore (AC-08). Configured with
|
|
1058
|
+
// the device alongside gpuStore below; threaded into the record stage via the
|
|
1059
|
+
// RenderSystemRuntime so a `videoTextureFields` material field uploads its
|
|
1060
|
+
// frame here instead of entering the static ensureResident cache.
|
|
1061
|
+
let dynamicTextureStore = new DynamicTextureStore();
|
|
1062
|
+
const renderTargetHost = createRenderTargetHost({
|
|
1063
|
+
rendererId: Symbol('renderer'),
|
|
1064
|
+
getGeneration: () => renderTargetGeneration,
|
|
1065
|
+
getDevice: () => renderTargetDevice,
|
|
1066
|
+
canPromoteTarget: (target: RenderTarget) =>
|
|
1067
|
+
renderSystemForTargetPromotion?.isCubeCapturePending(target) !== true,
|
|
1068
|
+
});
|
|
1069
|
+
// feat-20260527-sprite-nineslice M4 / w16 (D-5): detached host-owned
|
|
1070
|
+
// EngineMetrics counter, threaded to the record
|
|
1071
|
+
// stage via `RenderSystemRuntime.metrics` so soft-warns
|
|
1072
|
+
// (`nineslice.scale-too-small`, `nineslice.tile-needs-repeat-sampler`) bump
|
|
1073
|
+
// counters through the owning service (charter P3 machine-readable signals
|
|
1074
|
+
// over a per-frame console.warn flood). Each renderer assembly owns its own
|
|
1075
|
+
// counter Map (D-5 candidate 1 isolation); Renderer does not expose it.
|
|
1076
|
+
const metrics = createEngineMetrics();
|
|
1077
|
+
let extendedLightingState = createExtendedLightingState(activeDeviceScope.generation);
|
|
1078
|
+
const gpuPassTimingOptions = internals.options?.gpuPassTiming;
|
|
1079
|
+
const timingRetentionFrames =
|
|
1080
|
+
gpuPassTimingOptions?.retentionFrames !== undefined &&
|
|
1081
|
+
Number.isInteger(gpuPassTimingOptions.retentionFrames) &&
|
|
1082
|
+
gpuPassTimingOptions.retentionFrames >= 1 &&
|
|
1083
|
+
gpuPassTimingOptions.retentionFrames <= 8
|
|
1084
|
+
? gpuPassTimingOptions.retentionFrames
|
|
1085
|
+
: DEFAULT_GPU_PASS_TIMING_OPTIONS.retentionFrames;
|
|
1086
|
+
let gpuPassTimingSession: GpuPassTimingSession | undefined;
|
|
1087
|
+
let gpuPassTimingUnavailable: GpuPassTimingObservation | undefined;
|
|
1088
|
+
const timingObservationStore =
|
|
1089
|
+
gpuPassTimingOptions === undefined
|
|
1090
|
+
? undefined
|
|
1091
|
+
: createGpuPassTimingObservationStore({
|
|
1092
|
+
retentionFrames: timingRetentionFrames,
|
|
1093
|
+
currentDeviceGeneration: () => activeDeviceScope.generation,
|
|
1094
|
+
});
|
|
1095
|
+
const retireGpuPassTimingSession = (): void => {
|
|
1096
|
+
const retired = gpuPassTimingSession;
|
|
1097
|
+
gpuPassTimingSession = undefined;
|
|
1098
|
+
retired?.dispose();
|
|
1099
|
+
};
|
|
1100
|
+
const failedGpuPassTimingObservation = (error: GpuPassTimingReason): GpuPassTimingObservation => {
|
|
1101
|
+
const latestKnownGood = timingObservationStore?.inspect().latestKnownGood;
|
|
1102
|
+
return {
|
|
1103
|
+
status: 'failed',
|
|
1104
|
+
error,
|
|
1105
|
+
...(latestKnownGood === undefined ? {} : { latestKnownGood }),
|
|
1106
|
+
};
|
|
1107
|
+
};
|
|
1108
|
+
const establishGpuPassTimingSession = (): void => {
|
|
1109
|
+
retireGpuPassTimingSession();
|
|
1110
|
+
gpuPassTimingUnavailable = undefined;
|
|
1111
|
+
if (gpuPassTimingOptions === undefined) return;
|
|
1112
|
+
const created = createGpuPassTimingSession(internals.device, gpuPassTimingOptions);
|
|
1113
|
+
if (created.ok) {
|
|
1114
|
+
gpuPassTimingSession = created.value;
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
gpuPassTimingUnavailable = {
|
|
1118
|
+
status: 'unavailable',
|
|
1119
|
+
reason: created.error,
|
|
1120
|
+
capability: {
|
|
1121
|
+
timestampQuery: internals.device.caps.timestampQuery,
|
|
1122
|
+
timestampPeriodNanoseconds: internals.device.caps.timestampPeriodNanoseconds,
|
|
1123
|
+
},
|
|
1124
|
+
};
|
|
1125
|
+
};
|
|
1126
|
+
establishGpuPassTimingSession();
|
|
1127
|
+
// feat-20260629 M4: per-material-shader UV set count from naga vertex
|
|
1128
|
+
// @location reflection. Populated during prepareMaterialShaders from
|
|
1129
|
+
// MaterialShaderManifestEntry.uvSetCount. Read by getMaterialShaderPipeline
|
|
1130
|
+
// to auto-fill shaderUvSetCount for clamp-to-last alias.
|
|
1131
|
+
let materialShaderUvSetCounts = new Map<string, number>();
|
|
1132
|
+
// feat-20260527-sprite-nineslice M4 / w18 prep (D-9): hand the same
|
|
1133
|
+
// EngineMetrics instance to AssetRegistry so register-time soft-warns
|
|
1134
|
+
// (sliceMode=1 + sampler.addressMode !== 'repeat') bump
|
|
1135
|
+
// 'nineslice.tile-needs-repeat-sampler' on the SAME counter the runtime
|
|
1136
|
+
// reads.
|
|
1137
|
+
assets.setMetrics(metrics);
|
|
1138
|
+
// feat-20260707 M5 / w33 (D-11 + D-8): project the RhiCaps three-way
|
|
1139
|
+
// compression triple into the codec-facing `TranscodeCaps` and wire it into
|
|
1140
|
+
// the registry so the texture / equirect Basis arms can pick a transcode
|
|
1141
|
+
// target. One-line projection; the loader stays a pure consumer of the
|
|
1142
|
+
// declared ctx input (Pipeline Isolation).
|
|
1143
|
+
assets.setTranscodeCaps({
|
|
1144
|
+
bc: internals.device.caps.textureCompressionBc,
|
|
1145
|
+
etc2: internals.device.caps.textureCompressionEtc2,
|
|
1146
|
+
astc: internals.device.caps.textureCompressionAstc,
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
// M1 (bug-20260601-hello-tonemap-material-register D-1/D-2): prepare
|
|
1150
|
+
// engine-shipped material shaders (cap gate + manifest load + registration)
|
|
1151
|
+
// so they are available in ShaderCatalog before `register<MaterialAsset>`.
|
|
1152
|
+
// Failures throw structured RhiError / ShaderError through `createRenderer`.
|
|
1153
|
+
await prepareMaterialShaders(internals.device, getShader, assets, materialShaderUvSetCounts);
|
|
1154
|
+
// MaterialAsset per-slot texCoord: the built-in standard PBR + skin shaders
|
|
1155
|
+
// unconditionally declare all eight supported UV sets so they can honor
|
|
1156
|
+
// per-slot coordinate selection. Their vertex layout must therefore always
|
|
1157
|
+
// carry the declared UV slots or CreateRenderPipeline rejects the module.
|
|
1158
|
+
// in VertexState"). `materialShaderUvSetCounts` is the SSOT both PSO paths read
|
|
1159
|
+
// (buildPipelineContext + getMaterialShaderPipeline) to drive the clamp-to-last
|
|
1160
|
+
// alias; naga reflection can be stale for engine-shipped modules, so the
|
|
1161
|
+
// count is asserted explicitly. Missing mesh sets are clamp-to-last aliases.
|
|
1162
|
+
materialShaderUvSetCounts.set('forgeax::default-standard-pbr', 8);
|
|
1163
|
+
materialShaderUvSetCounts.set(SKIN_MATERIAL_SHADER_ID, 8);
|
|
1164
|
+
// M5 wiring (feat-20260517-vite-plugin-image-build-time-cook w14b): hand
|
|
1165
|
+
// the RhiDevice to the AssetRegistry so `loadByGuid<TextureAsset>` ->
|
|
1166
|
+
// `loadTextureFromEntry` -> `uploadTexture` actually runs the GPU
|
|
1167
|
+
// upload step plan-strategy section 3.2 sequence specifies. Without
|
|
1168
|
+
// this call uploadTexture step 3 short-circuits with `Result.ok(undefined)`
|
|
1169
|
+
// and the render-system materialBindGroup falls through to the 1x1
|
|
1170
|
+
// white fallback view (charter P3 violation: silent loss of texture).
|
|
1171
|
+
// bug-20260518 D-1: configureGpuDevice gains a 2nd parameter --
|
|
1172
|
+
// `pack.createShaderModule`, the top-level async shader-module factory
|
|
1173
|
+
// (rhi-webgpu / rhi-wgpu both expose it). The runtime mipmap utility uses
|
|
1174
|
+
// this factory inside `generateMipmaps` instead of the (now-deleted)
|
|
1175
|
+
// synchronous `device.createShaderModule` member. The parameter is
|
|
1176
|
+
// structurally optional at this call-site so the explicit-rhi escape
|
|
1177
|
+
// hatch (D-R5) without a top-level factory still routes through the
|
|
1178
|
+
// existing `invokeDeviceCreateShaderModule` fallback in
|
|
1179
|
+
// buildReadyWebGPU; AssetRegistry.uploadTexture's mipmap branch surfaces
|
|
1180
|
+
// the missing-factory case as a structured `rhi-not-available` error
|
|
1181
|
+
// (charter P3 explicit failure) rather than throwing here.
|
|
1182
|
+
//
|
|
1183
|
+
const packShaderFactory = adaptMipmapShaderModuleFactory(internals.pack.createShaderModule);
|
|
1184
|
+
// The RhiDevice surface satisfies `MipmapBlitDevice` (createTexture +
|
|
1185
|
+
// createTextureView + createCommandEncoder + createBindGroup +
|
|
1186
|
+
// queue.submit + queue.writeTexture all live on RhiDevice).
|
|
1187
|
+
// feat-20260601-device/gpu-residency-extraction M1 (D-3 / D-8): device +
|
|
1188
|
+
// shader-module factory + cube-POD register relay are wired onto the store
|
|
1189
|
+
// together. feat-20260614 M8 (D-15 / D-17): `registerCube` is the wire-layer
|
|
1190
|
+
// closure `(world, pod) => world.allocSharedRef('EquirectAsset', pod)` --
|
|
1191
|
+
// the runtime-minted cube POD lands in the draw-time world's user-tier
|
|
1192
|
+
// SharedRefStore (the AssetRegistry owns no handles). feat-20260630 M2 / w11:
|
|
1193
|
+
// the retired cube-texture asset kind is gone; the relay mints an EquirectAsset
|
|
1194
|
+
// shared ref as the cubemap GPU residency's identity token.
|
|
1195
|
+
gpuStore.configureGpuDevice(
|
|
1196
|
+
internals.device,
|
|
1197
|
+
packShaderFactory,
|
|
1198
|
+
(world, pod) => {
|
|
1199
|
+
const handle = world.allocSharedRef('EquirectAsset', pod);
|
|
1200
|
+
return ok(handle);
|
|
1201
|
+
},
|
|
1202
|
+
internals.device.caps,
|
|
1203
|
+
);
|
|
1204
|
+
gpuStore.configureIblDevice(
|
|
1205
|
+
internals.device,
|
|
1206
|
+
internals.pack.createShaderModule === undefined
|
|
1207
|
+
? undefined
|
|
1208
|
+
: (
|
|
1209
|
+
(createShaderModule) => (device, descriptor) =>
|
|
1210
|
+
createShaderModule(device, descriptor)
|
|
1211
|
+
)(internals.pack.createShaderModule),
|
|
1212
|
+
);
|
|
1213
|
+
gpuStore.bindDeviceScope(activeDeviceScope);
|
|
1214
|
+
// feat-20260623-world-space-video-asset M4 / w16 (D-3): wire the same device
|
|
1215
|
+
// into the transient video texture store (createTexture / createTextureView /
|
|
1216
|
+
// destroyTexture / queue.copyExternalImageToTexture all live on RhiDevice).
|
|
1217
|
+
dynamicTextureStore.configureGpuDevice(adaptDynamicTextureDevice(internals.device));
|
|
1218
|
+
// D-S3: Renderer.initialization three-step strict-serial Promise. Kicked off
|
|
1219
|
+
// synchronously here so `await renderer.initialization` is the AI-user-facing
|
|
1220
|
+
// barrier; failure is structured and goes through Promise reject (no
|
|
1221
|
+
// throw, no silent skip — charter proposition 4 explicit failure).
|
|
1222
|
+
let pipelineState: PipelineState | null = null;
|
|
1223
|
+
type RendererGeneration = GenerationAggregate<
|
|
1224
|
+
RhiDevice,
|
|
1225
|
+
RhiCanvasContext,
|
|
1226
|
+
PipelineState,
|
|
1227
|
+
RendererGenerationBindings,
|
|
1228
|
+
undefined
|
|
1229
|
+
>;
|
|
1230
|
+
const generationPublication: GenerationPublication<RendererGeneration | undefined> = {
|
|
1231
|
+
current: undefined,
|
|
1232
|
+
};
|
|
1233
|
+
const publishRendererGeneration = (candidate: RendererGeneration): void => {
|
|
1234
|
+
publishGeneration(generationPublication, candidate, (value) => value.scope.isAlive());
|
|
1235
|
+
activeDeviceScope = candidate.scope;
|
|
1236
|
+
internals.generationState.current = activeDeviceScope.generation;
|
|
1237
|
+
pipelineState = candidate.pipeline;
|
|
1238
|
+
const bindings = candidate.producerBindings;
|
|
1239
|
+
gpuStore = bindings.gpuStore;
|
|
1240
|
+
dynamicTextureStore = bindings.dynamicTextureStore;
|
|
1241
|
+
activeShaderState = bindings.shaderState;
|
|
1242
|
+
activePipelineCacheState = bindings.pipelineCacheState;
|
|
1243
|
+
materialShaderUvSetCounts = bindings.materialShaderUvSetCounts;
|
|
1244
|
+
emptyPostProcessBgl = bindings.emptyPostProcessBgl;
|
|
1245
|
+
if (bindings.growMeshSsbo === undefined) delete internals.growMeshSsbo;
|
|
1246
|
+
else internals.growMeshSsbo = bindings.growMeshSsbo;
|
|
1247
|
+
if (bindings.meshSsboState === undefined) delete internals.meshSsboState;
|
|
1248
|
+
else internals.meshSsboState = bindings.meshSsboState;
|
|
1249
|
+
gpuStore.bindDeviceScope(activeDeviceScope);
|
|
1250
|
+
};
|
|
1251
|
+
let readySettled = false;
|
|
1252
|
+
// w24 — Renderer.initialization returns Promise<Result<void, RhiError>>: resolve
|
|
1253
|
+
// ok(undefined) on success / resolve err(RhiError) on failure. The legacy
|
|
1254
|
+
// reject path is converted by a `.then() / .catch()` wrap so the inner
|
|
1255
|
+
// factory (`buildReadyWebGPU`) keeps its reject-on-error shape (research
|
|
1256
|
+
// F-3 internal contract preserved) while the public surface settles
|
|
1257
|
+
// strictly resolve-only (charter proposition 4 explicit failure - AI
|
|
1258
|
+
// users branch on `.ok` instead of try/catch).
|
|
1259
|
+
// feat-20260523-shader-template-instance-split M9-T03 (D-PipelineBuilder):
|
|
1260
|
+
// per-MaterialShader pipeline cache (LDR + HDR variants keyed by
|
|
1261
|
+
// `${materialShaderId}:${ldr|hdr}`). Map is owned by the renderer (lifetime
|
|
1262
|
+
// = Renderer) so cache hits persist across draw calls but reset between
|
|
1263
|
+
// renderers. Map.get / .set are sync; the underlying shader-module compile
|
|
1264
|
+
// is async-cached inside `makeShaderDeviceAdapter` (1-frame warmup -- first
|
|
1265
|
+
// miss returns Result.err('rhi-not-available'); after the async build
|
|
1266
|
+
// resolves the adapter caches the module, the next miss-then-build returns
|
|
1267
|
+
// Ok and lands in this Map).
|
|
1268
|
+
//
|
|
1269
|
+
// M6 fix-up: declared BEFORE `buildReadyWebGPU` so the boot-time SPEC_CONST
|
|
1270
|
+
// prewarm step inside that promise can seed URP-variant entries directly
|
|
1271
|
+
// (the seeding closure captures this Map by reference; ordering is safe
|
|
1272
|
+
// because the prewarm step runs strictly before the first frame's
|
|
1273
|
+
// `getMaterialShaderPipeline` lookup that would consume it).
|
|
1274
|
+
const materialShaderPipelineCache = new Map<string, RenderPipeline>();
|
|
1275
|
+
const materialShaderPipelineGroup2Contracts = new WeakMap<object, PipelineGroup2Contract>();
|
|
1276
|
+
const materialShaderManifestEntryCache = new Map<string, MaterialShaderManifestEntry>();
|
|
1277
|
+
// Variant resolution only depends on the requested axes, device capability
|
|
1278
|
+
// axes, and the manifest entry that owns the declarations. Cache it by the
|
|
1279
|
+
// manifest's variants-array identity so hot replacement naturally gets a
|
|
1280
|
+
// fresh result without adding a global strong reference to shader metadata.
|
|
1281
|
+
const materialShaderVariantResolutionCache = new WeakMap<
|
|
1282
|
+
object,
|
|
1283
|
+
Map<string, string | undefined>
|
|
1284
|
+
>();
|
|
1285
|
+
const group0MaterialLayout: {
|
|
1286
|
+
materialBgl: BindGroupLayout;
|
|
1287
|
+
pipelineLayout: PipelineLayout;
|
|
1288
|
+
} | null = null;
|
|
1289
|
+
const viewOnlyMaterialPipelineLayout: PipelineLayout | null = null;
|
|
1290
|
+
const viewAndSceneDepthMaterialPipelineLayout: PipelineLayout | null = null;
|
|
1291
|
+
const group0ResourceLayouts = new Map<
|
|
1292
|
+
string,
|
|
1293
|
+
{ materialBgl: BindGroupLayout; pipelineLayout: PipelineLayout }
|
|
1294
|
+
>();
|
|
1295
|
+
const preparedMaterialPipelineLayoutCache = new Map<string, PipelineLayout>();
|
|
1296
|
+
// feat-20260621-learn-render-5-5-parallax M2 / w6 (D-1): per-shader material
|
|
1297
|
+
// BGL + pipeline layout cache. Every authored material shader owns the
|
|
1298
|
+
// user-region binding order emitted by its paramSchema, plus the shared
|
|
1299
|
+
// engine-injection suffix. A custom shader with four-or-fewer textures can
|
|
1300
|
+
// still differ from the standard PBR order (for example, baseColorTexture
|
|
1301
|
+
// followed directly by normalTexture), so it cannot safely reuse the
|
|
1302
|
+
// standard material BGL. Built lazily on first request and cached by
|
|
1303
|
+
// shaderId; built-in shader IDs continue to use the boot-time shared layout.
|
|
1304
|
+
type PerShaderMaterialLayout = {
|
|
1305
|
+
materialBgl: BindGroupLayout;
|
|
1306
|
+
pipelineLayout: PipelineLayout;
|
|
1307
|
+
};
|
|
1308
|
+
type PerShaderMaterialLayoutCacheEntry = {
|
|
1309
|
+
readonly source: string;
|
|
1310
|
+
readonly paramSchema: readonly ParamSchemaEntry[];
|
|
1311
|
+
readonly layoutKind: LayoutKind;
|
|
1312
|
+
readonly layout: PerShaderMaterialLayout | null;
|
|
1313
|
+
};
|
|
1314
|
+
const perShaderMaterialLayoutCache = new Map<string, PerShaderMaterialLayoutCacheEntry>();
|
|
1315
|
+
const isEngineOwnedMaterialShader = (materialShaderId: string): boolean =>
|
|
1316
|
+
materialShaderId === 'forgeax::default-unlit' ||
|
|
1317
|
+
materialShaderId === 'forgeax::default-shadow-caster' ||
|
|
1318
|
+
materialShaderId === 'forgeax::sprite' ||
|
|
1319
|
+
materialShaderId === 'forgeax::sprite-lit' ||
|
|
1320
|
+
materialShaderId === 'forgeax::default-sprite' ||
|
|
1321
|
+
materialShaderId === 'forgeax::msdf-text' ||
|
|
1322
|
+
materialShaderId === 'forgeax::default-standard-pbr' ||
|
|
1323
|
+
materialShaderId === 'forgeax::pbr-skin' ||
|
|
1324
|
+
materialShaderId === 'forgeax::default-standard-pbr-skin';
|
|
1325
|
+
// Material binding contracts are derived from WGSL source, but the lookup
|
|
1326
|
+
// is also used by the per-submesh bind-group path. Cache the derived value
|
|
1327
|
+
// per renderer so a frame does not re-run the comment stripping and regex
|
|
1328
|
+
// scan for every visible submesh. Keep the source alongside the result so
|
|
1329
|
+
// shader hot-replacement invalidates the entry naturally.
|
|
1330
|
+
const materialShaderBindingContractCache = new Map<
|
|
1331
|
+
string,
|
|
1332
|
+
{ source: string; contract: MaterialShaderBindingContract }
|
|
1333
|
+
>();
|
|
1334
|
+
type RendererPipelineCacheState = {
|
|
1335
|
+
materialShaderPipelineCache: typeof materialShaderPipelineCache;
|
|
1336
|
+
materialShaderManifestEntryCache: typeof materialShaderManifestEntryCache;
|
|
1337
|
+
materialShaderVariantResolutionCache: typeof materialShaderVariantResolutionCache;
|
|
1338
|
+
group0MaterialLayout: {
|
|
1339
|
+
materialBgl: BindGroupLayout;
|
|
1340
|
+
pipelineLayout: PipelineLayout;
|
|
1341
|
+
} | null;
|
|
1342
|
+
viewOnlyMaterialPipelineLayout: PipelineLayout | null;
|
|
1343
|
+
viewAndSceneDepthMaterialPipelineLayout: PipelineLayout | null;
|
|
1344
|
+
group0ResourceLayouts: typeof group0ResourceLayouts;
|
|
1345
|
+
preparedMaterialPipelineLayoutCache: typeof preparedMaterialPipelineLayoutCache;
|
|
1346
|
+
perShaderMaterialLayoutCache: typeof perShaderMaterialLayoutCache;
|
|
1347
|
+
materialShaderBindingContractCache: typeof materialShaderBindingContractCache;
|
|
1348
|
+
};
|
|
1349
|
+
const createRendererPipelineCacheState = (): RendererPipelineCacheState => ({
|
|
1350
|
+
materialShaderPipelineCache: new Map(),
|
|
1351
|
+
materialShaderManifestEntryCache: new Map(),
|
|
1352
|
+
materialShaderVariantResolutionCache: new WeakMap(),
|
|
1353
|
+
group0MaterialLayout: null,
|
|
1354
|
+
viewOnlyMaterialPipelineLayout: null,
|
|
1355
|
+
viewAndSceneDepthMaterialPipelineLayout: null,
|
|
1356
|
+
group0ResourceLayouts: new Map(),
|
|
1357
|
+
preparedMaterialPipelineLayoutCache: new Map(),
|
|
1358
|
+
perShaderMaterialLayoutCache: new Map(),
|
|
1359
|
+
materialShaderBindingContractCache: new Map(),
|
|
1360
|
+
});
|
|
1361
|
+
let activePipelineCacheState: RendererPipelineCacheState = {
|
|
1362
|
+
materialShaderPipelineCache,
|
|
1363
|
+
materialShaderManifestEntryCache,
|
|
1364
|
+
materialShaderVariantResolutionCache,
|
|
1365
|
+
group0MaterialLayout,
|
|
1366
|
+
viewOnlyMaterialPipelineLayout,
|
|
1367
|
+
viewAndSceneDepthMaterialPipelineLayout,
|
|
1368
|
+
group0ResourceLayouts,
|
|
1369
|
+
preparedMaterialPipelineLayoutCache,
|
|
1370
|
+
perShaderMaterialLayoutCache,
|
|
1371
|
+
materialShaderBindingContractCache,
|
|
1372
|
+
};
|
|
1373
|
+
let candidatePipelineCacheState: RendererPipelineCacheState | undefined;
|
|
1374
|
+
let candidateMaterialShaderUvSetCounts: Map<string, number> | undefined;
|
|
1375
|
+
let emptyPostProcessBgl: BindGroupLayout | null = null;
|
|
1376
|
+
let candidateEmptyPostProcessBgl: BindGroupLayout | null | undefined;
|
|
1377
|
+
type RendererCandidateState = {
|
|
1378
|
+
readonly device: RhiDevice;
|
|
1379
|
+
readonly context: RhiCanvasContext;
|
|
1380
|
+
readonly scope: DeviceScope;
|
|
1381
|
+
readonly gpuStore: GpuResidencyCache;
|
|
1382
|
+
readonly dynamicTextureStore: DynamicTextureStore;
|
|
1383
|
+
readonly shaderState: RendererShaderState;
|
|
1384
|
+
readonly pipelineCacheState: RendererPipelineCacheState;
|
|
1385
|
+
readonly materialShaderUvSetCounts: Map<string, number>;
|
|
1386
|
+
pipelineState: PipelineState | null;
|
|
1387
|
+
emptyPostProcessBgl: BindGroupLayout | null;
|
|
1388
|
+
growMeshSsbo: ((neededSlots: number) => MeshSsboGrowResult) | undefined;
|
|
1389
|
+
meshSsboState: MeshSsboState | undefined;
|
|
1390
|
+
};
|
|
1391
|
+
type RendererGenerationBindings = {
|
|
1392
|
+
readonly gpuStore: GpuResidencyCache;
|
|
1393
|
+
readonly dynamicTextureStore: DynamicTextureStore;
|
|
1394
|
+
readonly shaderState: RendererShaderState;
|
|
1395
|
+
readonly pipelineCacheState: RendererPipelineCacheState;
|
|
1396
|
+
readonly materialShaderUvSetCounts: Map<string, number>;
|
|
1397
|
+
readonly emptyPostProcessBgl: BindGroupLayout | null;
|
|
1398
|
+
readonly growMeshSsbo: ((neededSlots: number) => MeshSsboGrowResult) | undefined;
|
|
1399
|
+
readonly meshSsboState: MeshSsboState | undefined;
|
|
1400
|
+
};
|
|
1401
|
+
let candidateBuildState: RendererCandidateState | undefined;
|
|
1402
|
+
const currentPipelineCacheState = (): RendererPipelineCacheState =>
|
|
1403
|
+
candidatePipelineCacheState ?? activePipelineCacheState;
|
|
1404
|
+
const currentPipelineState = (): PipelineState | null =>
|
|
1405
|
+
candidateBuildState === undefined ? pipelineState : candidateBuildState.pipelineState;
|
|
1406
|
+
const currentBuildDevice = (): RhiDevice => candidateBuildState?.device ?? internals.device;
|
|
1407
|
+
const currentMaterialShaderUvSetCounts = (): Map<string, number> =>
|
|
1408
|
+
candidateMaterialShaderUvSetCounts ?? materialShaderUvSetCounts;
|
|
1409
|
+
const currentEmptyPostProcessBgl = (): BindGroupLayout | null =>
|
|
1410
|
+
candidateEmptyPostProcessBgl === undefined ? emptyPostProcessBgl : candidateEmptyPostProcessBgl;
|
|
1411
|
+
const getCachedMaterialShaderBindingContract = (
|
|
1412
|
+
materialShaderId: string,
|
|
1413
|
+
): MaterialShaderBindingContract => {
|
|
1414
|
+
const cacheState = currentPipelineCacheState();
|
|
1415
|
+
const lookup = getShader().findMaterialArtifact(materialShaderId);
|
|
1416
|
+
if (!lookup.ok) return 'render-material';
|
|
1417
|
+
const cached = cacheState.materialShaderBindingContractCache.get(materialShaderId);
|
|
1418
|
+
if (cached?.source === lookup.value.source) return cached.contract;
|
|
1419
|
+
const contract = resolveMaterialShaderBindingContract(lookup.value.source);
|
|
1420
|
+
cacheState.materialShaderBindingContractCache.set(materialShaderId, {
|
|
1421
|
+
source: lookup.value.source,
|
|
1422
|
+
contract,
|
|
1423
|
+
});
|
|
1424
|
+
return contract;
|
|
1425
|
+
};
|
|
1426
|
+
const getOrBuildGroup0MaterialLayout = (): {
|
|
1427
|
+
materialBgl: BindGroupLayout;
|
|
1428
|
+
pipelineLayout: PipelineLayout;
|
|
1429
|
+
} | null => {
|
|
1430
|
+
const cacheState = currentPipelineCacheState();
|
|
1431
|
+
if (cacheState.group0MaterialLayout !== null) return cacheState.group0MaterialLayout;
|
|
1432
|
+
if (currentPipelineState() === null) return null;
|
|
1433
|
+
const bglRes = currentBuildDevice().createBindGroupLayout({ entries: [] });
|
|
1434
|
+
if (!bglRes.ok) {
|
|
1435
|
+
internals.errorRegistry.fire(bglRes.error);
|
|
1436
|
+
return null;
|
|
1437
|
+
}
|
|
1438
|
+
const plRes = currentBuildDevice().createPipelineLayout({
|
|
1439
|
+
label: 'material-group-0-pipeline-layout',
|
|
1440
|
+
bindGroupLayouts: [bglRes.value],
|
|
1441
|
+
});
|
|
1442
|
+
if (!plRes.ok) {
|
|
1443
|
+
internals.errorRegistry.fire(plRes.error);
|
|
1444
|
+
return null;
|
|
1445
|
+
}
|
|
1446
|
+
cacheState.group0MaterialLayout = { materialBgl: bglRes.value, pipelineLayout: plRes.value };
|
|
1447
|
+
return cacheState.group0MaterialLayout;
|
|
1448
|
+
};
|
|
1449
|
+
const getOrBuildViewOnlyMaterialPipelineLayout = (): PipelineLayout | null => {
|
|
1450
|
+
const cacheState = currentPipelineCacheState();
|
|
1451
|
+
if (cacheState.viewOnlyMaterialPipelineLayout !== null) {
|
|
1452
|
+
return cacheState.viewOnlyMaterialPipelineLayout;
|
|
1453
|
+
}
|
|
1454
|
+
const currentState = currentPipelineState();
|
|
1455
|
+
if (currentState === null) return null;
|
|
1456
|
+
const plRes = currentBuildDevice().createPipelineLayout({
|
|
1457
|
+
label: 'material-view-only-pipeline-layout',
|
|
1458
|
+
bindGroupLayouts: [currentState.viewBindGroupLayout],
|
|
1459
|
+
});
|
|
1460
|
+
if (!plRes.ok) {
|
|
1461
|
+
internals.errorRegistry.fire(plRes.error);
|
|
1462
|
+
return null;
|
|
1463
|
+
}
|
|
1464
|
+
cacheState.viewOnlyMaterialPipelineLayout = plRes.value;
|
|
1465
|
+
return cacheState.viewOnlyMaterialPipelineLayout;
|
|
1466
|
+
};
|
|
1467
|
+
const getOrBuildViewAndSceneDepthMaterialPipelineLayout = (): PipelineLayout | null => {
|
|
1468
|
+
const cacheState = currentPipelineCacheState();
|
|
1469
|
+
if (cacheState.viewAndSceneDepthMaterialPipelineLayout !== null) {
|
|
1470
|
+
return cacheState.viewAndSceneDepthMaterialPipelineLayout;
|
|
1471
|
+
}
|
|
1472
|
+
const bglRes = currentBuildDevice().createBindGroupLayout({
|
|
1473
|
+
label: 'material-view-scene-depth-bgl',
|
|
1474
|
+
entries: [
|
|
1475
|
+
{
|
|
1476
|
+
binding: 0,
|
|
1477
|
+
visibility: GPU_SHADER_STAGE_VERTEX | GPU_SHADER_STAGE_FRAGMENT,
|
|
1478
|
+
buffer: { type: 'uniform' },
|
|
1479
|
+
},
|
|
1480
|
+
{
|
|
1481
|
+
binding: 1,
|
|
1482
|
+
visibility: GPU_SHADER_STAGE_FRAGMENT,
|
|
1483
|
+
texture: { sampleType: 'depth', viewDimension: '2d', multisampled: false },
|
|
1484
|
+
},
|
|
1485
|
+
],
|
|
1486
|
+
});
|
|
1487
|
+
if (!bglRes.ok) {
|
|
1488
|
+
internals.errorRegistry.fire(bglRes.error);
|
|
1489
|
+
return null;
|
|
1490
|
+
}
|
|
1491
|
+
const plRes = currentBuildDevice().createPipelineLayout({
|
|
1492
|
+
label: 'material-view-scene-depth-pipeline-layout',
|
|
1493
|
+
bindGroupLayouts: [bglRes.value],
|
|
1494
|
+
});
|
|
1495
|
+
if (!plRes.ok) {
|
|
1496
|
+
internals.errorRegistry.fire(plRes.error);
|
|
1497
|
+
return null;
|
|
1498
|
+
}
|
|
1499
|
+
cacheState.viewAndSceneDepthMaterialPipelineLayout = plRes.value;
|
|
1500
|
+
return cacheState.viewAndSceneDepthMaterialPipelineLayout;
|
|
1501
|
+
};
|
|
1502
|
+
const getOrBuildGroup0ResourceLayout = (
|
|
1503
|
+
materialShaderId: string,
|
|
1504
|
+
): { materialBgl: BindGroupLayout; pipelineLayout: PipelineLayout } | null => {
|
|
1505
|
+
const cacheState = currentPipelineCacheState();
|
|
1506
|
+
const cached = cacheState.group0ResourceLayouts.get(materialShaderId);
|
|
1507
|
+
if (cached !== undefined) return cached;
|
|
1508
|
+
const lookup = getShader().findMaterialArtifact(materialShaderId);
|
|
1509
|
+
if (
|
|
1510
|
+
!lookup.ok ||
|
|
1511
|
+
!/@group\s*\(\s*0\s*\)\s*@binding\s*\(\s*0\s*\)[^;]*texture_depth_2d/u.test(
|
|
1512
|
+
lookup.value.source,
|
|
1513
|
+
)
|
|
1514
|
+
) {
|
|
1515
|
+
return null;
|
|
1516
|
+
}
|
|
1517
|
+
const bgl = currentBuildDevice().createBindGroupLayout({
|
|
1518
|
+
label: `material-group-0-resource-${materialShaderId}`,
|
|
1519
|
+
entries: [
|
|
1520
|
+
{
|
|
1521
|
+
binding: 0,
|
|
1522
|
+
visibility: 2,
|
|
1523
|
+
texture: { sampleType: 'depth', viewDimension: '2d', multisampled: false },
|
|
1524
|
+
},
|
|
1525
|
+
],
|
|
1526
|
+
});
|
|
1527
|
+
if (!bgl.ok) return null;
|
|
1528
|
+
const pipelineLayout = currentBuildDevice().createPipelineLayout({
|
|
1529
|
+
label: `material-group-0-resource-pl-${materialShaderId}`,
|
|
1530
|
+
bindGroupLayouts: [bgl.value],
|
|
1531
|
+
});
|
|
1532
|
+
if (!pipelineLayout.ok) return null;
|
|
1533
|
+
const built = { materialBgl: bgl.value, pipelineLayout: pipelineLayout.value };
|
|
1534
|
+
cacheState.group0ResourceLayouts.set(materialShaderId, built);
|
|
1535
|
+
return built;
|
|
1536
|
+
};
|
|
1537
|
+
const getOrBuildPreparedMaterialPipelineLayout = (
|
|
1538
|
+
materialShaderId: string,
|
|
1539
|
+
): PipelineLayout | null => {
|
|
1540
|
+
const cacheState = currentPipelineCacheState();
|
|
1541
|
+
const cached = cacheState.preparedMaterialPipelineLayoutCache.get(materialShaderId);
|
|
1542
|
+
if (cached !== undefined) return cached;
|
|
1543
|
+
const currentState = currentPipelineState();
|
|
1544
|
+
if (currentState === null) return null;
|
|
1545
|
+
const materialBgl =
|
|
1546
|
+
(isEngineOwnedMaterialShader(materialShaderId)
|
|
1547
|
+
? undefined
|
|
1548
|
+
: getOrBuildPerShaderMaterialLayout(materialShaderId)?.materialBgl) ??
|
|
1549
|
+
currentState.materialBindGroupLayout;
|
|
1550
|
+
const result = currentBuildDevice().createPipelineLayout({
|
|
1551
|
+
label: `prepared-material-pl-${materialShaderId}`,
|
|
1552
|
+
bindGroupLayouts: [currentState.viewBindGroupLayout, materialBgl],
|
|
1553
|
+
});
|
|
1554
|
+
if (!result.ok) {
|
|
1555
|
+
internals.errorRegistry.fire(result.error);
|
|
1556
|
+
return null;
|
|
1557
|
+
}
|
|
1558
|
+
cacheState.preparedMaterialPipelineLayoutCache.set(materialShaderId, result.value);
|
|
1559
|
+
return result.value;
|
|
1560
|
+
};
|
|
1561
|
+
const getOrBuildPerShaderMaterialLayout = (
|
|
1562
|
+
materialShaderId: string,
|
|
1563
|
+
explicitParamSchema?: readonly ParamSchemaEntry[],
|
|
1564
|
+
layoutKind: LayoutKind = 'pbr',
|
|
1565
|
+
): PerShaderMaterialLayout | null => {
|
|
1566
|
+
const cacheState = currentPipelineCacheState();
|
|
1567
|
+
const currentState = currentPipelineState();
|
|
1568
|
+
if (currentState === null) return null;
|
|
1569
|
+
if (isEngineOwnedMaterialShader(materialShaderId)) return null;
|
|
1570
|
+
const lookup = getShader().findMaterialArtifact(materialShaderId);
|
|
1571
|
+
const source = lookup.ok ? lookup.value.source : '';
|
|
1572
|
+
const paramSchema = lookup.ok ? lookup.value.paramSchema : explicitParamSchema;
|
|
1573
|
+
if (paramSchema === undefined) return null;
|
|
1574
|
+
const cached = cacheState.perShaderMaterialLayoutCache.get(materialShaderId);
|
|
1575
|
+
if (
|
|
1576
|
+
cached?.source === source &&
|
|
1577
|
+
cached.paramSchema === paramSchema &&
|
|
1578
|
+
cached.layoutKind === layoutKind
|
|
1579
|
+
) {
|
|
1580
|
+
return cached.layout;
|
|
1581
|
+
}
|
|
1582
|
+
// A custom schema may reuse the shared PBR layout only when every derived
|
|
1583
|
+
// binding is an identical canonical prefix. Otherwise build its own layout
|
|
1584
|
+
// so texture dimensions and sampler/storage shapes cannot drift.
|
|
1585
|
+
const requiresStandardMapLayout =
|
|
1586
|
+
isCanonicalStandardPbrMaterialShader(materialShaderId) ||
|
|
1587
|
+
isStandardPbrMaterialShader(materialShaderId) ||
|
|
1588
|
+
standardPhysicalTextureFields(paramSchema).length > 0;
|
|
1589
|
+
const isStandardMapLayout = requiresStandardMapLayout;
|
|
1590
|
+
if (!isStandardMapLayout && isSharedMaterialUserRegionCompatible(paramSchema)) {
|
|
1591
|
+
cacheState.perShaderMaterialLayoutCache.set(materialShaderId, {
|
|
1592
|
+
source,
|
|
1593
|
+
paramSchema,
|
|
1594
|
+
layoutKind,
|
|
1595
|
+
layout: null,
|
|
1596
|
+
});
|
|
1597
|
+
return null;
|
|
1598
|
+
}
|
|
1599
|
+
const spec: PipelineSpec = {
|
|
1600
|
+
shader: { id: materialShaderId, passKind: 'forward', variantSet: undefined },
|
|
1601
|
+
attachments: { colorFormats: [], depthFormat: undefined, sampleCount: 1 },
|
|
1602
|
+
geometry: { topology: 'triangle-list', vertexLayout: {} },
|
|
1603
|
+
renderState: undefined,
|
|
1604
|
+
};
|
|
1605
|
+
const desc = buildBindGroupLayoutDescriptor(spec, {
|
|
1606
|
+
kind: 'pbr-material-merged',
|
|
1607
|
+
materialParamSchema: paramSchema,
|
|
1608
|
+
});
|
|
1609
|
+
const bglRes = currentBuildDevice().createBindGroupLayout(desc);
|
|
1610
|
+
if (!bglRes.ok) {
|
|
1611
|
+
internals.errorRegistry.fire(bglRes.error);
|
|
1612
|
+
return null;
|
|
1613
|
+
}
|
|
1614
|
+
const meshLayout =
|
|
1615
|
+
layoutKind === 'pbr-skin'
|
|
1616
|
+
? currentState.pbrSkinMeshBindGroupLayout
|
|
1617
|
+
: layoutKind === 'hdrp-skin'
|
|
1618
|
+
? currentState.hdrpSkinMeshBindGroupLayout
|
|
1619
|
+
: currentState.meshBindGroupLayout;
|
|
1620
|
+
if (meshLayout === null) return null;
|
|
1621
|
+
const plRes = currentBuildDevice().createPipelineLayout({
|
|
1622
|
+
label: `pbr-pl-${materialShaderId}`,
|
|
1623
|
+
bindGroupLayouts: [
|
|
1624
|
+
currentState.viewBindGroupLayout,
|
|
1625
|
+
bglRes.value,
|
|
1626
|
+
meshLayout,
|
|
1627
|
+
currentState.instancesBindGroupLayout,
|
|
1628
|
+
],
|
|
1629
|
+
});
|
|
1630
|
+
if (!plRes.ok) {
|
|
1631
|
+
internals.errorRegistry.fire(plRes.error);
|
|
1632
|
+
return null;
|
|
1633
|
+
}
|
|
1634
|
+
const built = { materialBgl: bglRes.value, pipelineLayout: plRes.value };
|
|
1635
|
+
cacheState.perShaderMaterialLayoutCache.set(materialShaderId, {
|
|
1636
|
+
source,
|
|
1637
|
+
paramSchema,
|
|
1638
|
+
layoutKind,
|
|
1639
|
+
layout: built,
|
|
1640
|
+
});
|
|
1641
|
+
return built;
|
|
1642
|
+
};
|
|
1643
|
+
const resolveCachedMaterialShaderVariantSet = (
|
|
1644
|
+
requestedVariantSet: string | undefined,
|
|
1645
|
+
manifestEntry: import('@forgeax/engine-shader').MaterialShaderManifestEntry | undefined,
|
|
1646
|
+
): string | undefined => {
|
|
1647
|
+
// Keep lazy PSO resolution aligned with buildReadyWebGPU's sampled-texture gate.
|
|
1648
|
+
const device = currentBuildDevice();
|
|
1649
|
+
const sampledTextureLimit = device.limits.maxSampledTexturesPerShaderStage;
|
|
1650
|
+
if (manifestEntry === undefined) {
|
|
1651
|
+
return resolveMaterialShaderVariantSet(
|
|
1652
|
+
requestedVariantSet,
|
|
1653
|
+
[],
|
|
1654
|
+
device.caps.backendKind,
|
|
1655
|
+
device.caps.storageBuffer,
|
|
1656
|
+
sampledTextureLimit,
|
|
1657
|
+
);
|
|
1658
|
+
}
|
|
1659
|
+
const variants = manifestEntry.variants;
|
|
1660
|
+
let byRequest = materialShaderVariantResolutionCache.get(variants);
|
|
1661
|
+
if (byRequest === undefined) {
|
|
1662
|
+
byRequest = new Map();
|
|
1663
|
+
materialShaderVariantResolutionCache.set(variants, byRequest);
|
|
1664
|
+
}
|
|
1665
|
+
const cacheKey = `${requestedVariantSet ?? '\u0000'}|${device.caps.backendKind}|${device.caps.storageBuffer ? '1' : '0'}|${sampledTextureLimit ?? '\u0000'}`;
|
|
1666
|
+
if (byRequest.has(cacheKey)) return byRequest.get(cacheKey);
|
|
1667
|
+
const resolved = resolveMaterialShaderVariantSet(
|
|
1668
|
+
requestedVariantSet,
|
|
1669
|
+
variants,
|
|
1670
|
+
device.caps.backendKind,
|
|
1671
|
+
device.caps.storageBuffer,
|
|
1672
|
+
sampledTextureLimit,
|
|
1673
|
+
);
|
|
1674
|
+
byRequest.set(cacheKey, resolved);
|
|
1675
|
+
return resolved;
|
|
1676
|
+
};
|
|
1677
|
+
const findMaterialShaderManifestEntry = (
|
|
1678
|
+
materialShaderId: string,
|
|
1679
|
+
): MaterialShaderManifestEntry | undefined => {
|
|
1680
|
+
const cacheState = currentPipelineCacheState();
|
|
1681
|
+
const cached = cacheState.materialShaderManifestEntryCache.get(materialShaderId);
|
|
1682
|
+
if (cached !== undefined) return cached;
|
|
1683
|
+
for (const candidate of getShader().materialShaderManifestEntries()) {
|
|
1684
|
+
if (candidate.identifier === materialShaderId) {
|
|
1685
|
+
cacheState.materialShaderManifestEntryCache.set(materialShaderId, candidate);
|
|
1686
|
+
return candidate;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
return undefined;
|
|
1690
|
+
};
|
|
1691
|
+
// feat-20260622-s5 M3 / w17: the pipeline build is factored into a closure so
|
|
1692
|
+
// the recover() rebuild can re-run the SAME three-step assembly against the
|
|
1693
|
+
// freshly-acquired device (SSOT — one build path, one set of seed callbacks).
|
|
1694
|
+
// All captured references (getShader / gpuStore / getShaderModuleAdapter /
|
|
1695
|
+
// materialShaderPipelineCache / renderSystem) are stable across recover; only
|
|
1696
|
+
// `internals.device` / `internals.pack` are read live, so a rebuild after a
|
|
1697
|
+
// device swap compiles against the new device.
|
|
1698
|
+
const promotePipelineExtendedLighting = (state: PipelineState, scope: DeviceScope): void => {
|
|
1699
|
+
if (
|
|
1700
|
+
state.extendedLightingAvailable !== true ||
|
|
1701
|
+
state.iesProfileTexture === undefined ||
|
|
1702
|
+
state.cookieTexture === undefined ||
|
|
1703
|
+
state.cookieMatrixBuffer === undefined ||
|
|
1704
|
+
state.iesProfileTextureView === undefined ||
|
|
1705
|
+
state.cookieTextureView === undefined ||
|
|
1706
|
+
state.ltcLambertTextureView === undefined ||
|
|
1707
|
+
state.ltcGgxTextureView === undefined
|
|
1708
|
+
) {
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
const candidate: ExtendedLightingResourceCandidate = {
|
|
1712
|
+
topology: EXTENDED_LIGHTING_TOPOLOGY,
|
|
1713
|
+
generation: scope.generation,
|
|
1714
|
+
scope,
|
|
1715
|
+
iesSliceCount: 0,
|
|
1716
|
+
cookieSliceCount: 0,
|
|
1717
|
+
cookieMatrices: 0,
|
|
1718
|
+
sampler: state.defaultSampler,
|
|
1719
|
+
iesTexture: state.iesProfileTexture,
|
|
1720
|
+
cookieTexture: state.cookieTexture,
|
|
1721
|
+
cookieMatrixBuffer: state.cookieMatrixBuffer,
|
|
1722
|
+
descriptorBytes:
|
|
1723
|
+
IES_SLICE_WIDTH * IES_SLICE_HEIGHT * 2 * 32 +
|
|
1724
|
+
COOKIE_SLICE_SIZE * COOKIE_SLICE_SIZE * 4 * 32 +
|
|
1725
|
+
COOKIE_MATRIX_BYTES,
|
|
1726
|
+
uploadCount: 5,
|
|
1727
|
+
};
|
|
1728
|
+
extendedLightingState = promoteExtendedLightingCandidate(extendedLightingState, candidate);
|
|
1729
|
+
};
|
|
1730
|
+
let tonemapRegistered = false;
|
|
1731
|
+
let fxaaRegistered = false;
|
|
1732
|
+
let temporalPostProcessesRegistered = false;
|
|
1733
|
+
const buildPipeline = (
|
|
1734
|
+
scope: DeviceScope = activeDeviceScope,
|
|
1735
|
+
device: RhiDevice = currentBuildDevice(),
|
|
1736
|
+
residencyStore: GpuResidencyCache = gpuStore,
|
|
1737
|
+
): Promise<PipelineState> =>
|
|
1738
|
+
buildReadyWebGPU(
|
|
1739
|
+
device,
|
|
1740
|
+
scope,
|
|
1741
|
+
getShader,
|
|
1742
|
+
residencyStore,
|
|
1743
|
+
internals.pack.createShaderModule,
|
|
1744
|
+
internals.errorRegistry,
|
|
1745
|
+
requiredMaterialShaders,
|
|
1746
|
+
requiredFullscreenPostProcesses,
|
|
1747
|
+
// feat-20260608-mesh-ssbo-dynamic-grow-l1-lift-1024-entity-cap M2 /
|
|
1748
|
+
// T-M2-05 + M3 / T-M3-04: callback set by buildReadyWebGPU once
|
|
1749
|
+
// meshSsboController is wired. Both the grow hook AND the read-only
|
|
1750
|
+
// state ref land on `internals` so the record stage (M3
|
|
1751
|
+
// ensureMeshSsboCapacity) can read slotCount + call grow through
|
|
1752
|
+
// RenderSystemInternals.
|
|
1753
|
+
(hook, state) => {
|
|
1754
|
+
if (candidateBuildState !== undefined) {
|
|
1755
|
+
candidateBuildState.growMeshSsbo = hook;
|
|
1756
|
+
candidateBuildState.meshSsboState = state;
|
|
1757
|
+
} else {
|
|
1758
|
+
internals.growMeshSsbo = hook;
|
|
1759
|
+
internals.meshSsboState = state;
|
|
1760
|
+
}
|
|
1761
|
+
},
|
|
1762
|
+
// feat-20260609 R3-fixup: seed the lazy adapter cache with the
|
|
1763
|
+
// eagerly-compiled shadow_caster module.
|
|
1764
|
+
(label, module) => {
|
|
1765
|
+
getShaderModuleAdapter().seedModule(label, module);
|
|
1766
|
+
},
|
|
1767
|
+
// M6 fix-up: seed the active or candidate material pipeline cache from
|
|
1768
|
+
// the boot-time SPEC_CONST prewarm. Recovery builds must never put a
|
|
1769
|
+
// candidate-device PSO into the retired generation's cache.
|
|
1770
|
+
(key, pso, group2Contract) => {
|
|
1771
|
+
const pipelineCache = currentPipelineCacheState().materialShaderPipelineCache;
|
|
1772
|
+
if (!pipelineCache.has(key)) {
|
|
1773
|
+
pipelineCache.set(key, pso);
|
|
1774
|
+
}
|
|
1775
|
+
materialShaderPipelineGroup2Contracts.set(pso as object, group2Contract);
|
|
1776
|
+
},
|
|
1777
|
+
// feat-20260621 M-A3 (D-5): register the built-in tonemap on the unified
|
|
1778
|
+
// post-process channel once buildReadyWebGPU resolves the tonemap manifest
|
|
1779
|
+
// entry's composed WGSL. Fires after the manifest-load await, by which point
|
|
1780
|
+
// `renderSystem` (declared synchronously below) is defined. The 16 B params
|
|
1781
|
+
// schema mirrors the prior dedicated UBO: [exposure(f32), whitePoint(f32),
|
|
1782
|
+
// mode(u32), pad(f32)]; the extract stage bridges Camera.exposure/whitePoint/
|
|
1783
|
+
// tonemap into this channel each frame (render-system-extract.ts w13).
|
|
1784
|
+
(source: string) => {
|
|
1785
|
+
if (candidateBuildState !== undefined || tonemapRegistered) return;
|
|
1786
|
+
renderSystem.registerBuiltinPostProcess(STANDARD_OUTPUT_TRANSFORM_FEATURE_ID, {
|
|
1787
|
+
source,
|
|
1788
|
+
params: { byteSize: 16, defaultValue: new Uint8Array(16) },
|
|
1789
|
+
reads: ['hdrColor'],
|
|
1790
|
+
});
|
|
1791
|
+
tonemapRegistered = true;
|
|
1792
|
+
},
|
|
1793
|
+
(source: string) => {
|
|
1794
|
+
if (fxaaRegistered) return;
|
|
1795
|
+
renderSystem.registerBuiltinPostProcess(FXAA_POST_PROCESS_ID, {
|
|
1796
|
+
source,
|
|
1797
|
+
params: { byteSize: 16, defaultValue: new Uint8Array(16) },
|
|
1798
|
+
reads: ['ldrColor'],
|
|
1799
|
+
});
|
|
1800
|
+
fxaaRegistered = true;
|
|
1801
|
+
},
|
|
1802
|
+
(entries: { readonly motionBlur?: string; readonly taaResolve?: string }) => {
|
|
1803
|
+
if (candidateBuildState !== undefined || temporalPostProcessesRegistered) return;
|
|
1804
|
+
if (entries.motionBlur !== undefined) {
|
|
1805
|
+
renderSystem.registerBuiltinPostProcess('forgeax.motion-blur', {
|
|
1806
|
+
source: entries.motionBlur,
|
|
1807
|
+
params: { byteSize: 16, defaultValue: new Uint8Array(16) },
|
|
1808
|
+
reads: ['scene-color', 'scene-temporal'],
|
|
1809
|
+
});
|
|
1810
|
+
}
|
|
1811
|
+
if (entries.taaResolve !== undefined) {
|
|
1812
|
+
renderSystem.registerBuiltinPostProcess('forgeax.taa-resolve', {
|
|
1813
|
+
source: entries.taaResolve,
|
|
1814
|
+
params: { byteSize: 16, defaultValue: new Uint8Array(16) },
|
|
1815
|
+
reads: ['scene-color', 'scene-temporal', 'taa-history-color', 'taa-history-temporal'],
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
temporalPostProcessesRegistered = true;
|
|
1819
|
+
},
|
|
1820
|
+
(sources: VolumetricFogShaderSources) => {
|
|
1821
|
+
internals.volumetricFogShaders = sources;
|
|
1822
|
+
},
|
|
1823
|
+
);
|
|
1824
|
+
const ready: Promise<Result<void, RhiError>> = buildPipeline().then(
|
|
1825
|
+
(state): Result<void, RhiError> => {
|
|
1826
|
+
pipelineState = state;
|
|
1827
|
+
promotePipelineExtendedLighting(state, activeDeviceScope);
|
|
1828
|
+
readySettled = true;
|
|
1829
|
+
return ok(undefined);
|
|
1830
|
+
},
|
|
1831
|
+
(e: unknown): Result<void, RhiError> => {
|
|
1832
|
+
readySettled = true;
|
|
1833
|
+
if (e instanceof RhiError) return err(e);
|
|
1834
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
1835
|
+
return err(
|
|
1836
|
+
new RhiError({
|
|
1837
|
+
code: 'webgpu-runtime-error',
|
|
1838
|
+
expected: 'Renderer.initialization three-step strict-serial succeeds',
|
|
1839
|
+
hint: `pipeline build raised: ${message}`,
|
|
1840
|
+
}),
|
|
1841
|
+
);
|
|
1842
|
+
},
|
|
1843
|
+
);
|
|
1844
|
+
/**
|
|
1845
|
+
* bug-20260527-renderstate-pipeline-dispatch-gap D-3:
|
|
1846
|
+
* finds the engine-shipped PBR manifest entry from the shader registry
|
|
1847
|
+
* by content marker (f_schlick BRDF helper call, same marker
|
|
1848
|
+
* buildReadyWebGPU uses). Returns undefined when the manifest is
|
|
1849
|
+
* empty (Camera-only path) or the pbr entry is not found.
|
|
1850
|
+
*/
|
|
1851
|
+
const findStandardPbrEntry = (
|
|
1852
|
+
variantSet?: string,
|
|
1853
|
+
): import('@forgeax/engine-types').ManifestEntry | undefined => {
|
|
1854
|
+
const device = currentBuildDevice();
|
|
1855
|
+
let standardEntry: import('@forgeax/engine-types').ManifestEntry | undefined;
|
|
1856
|
+
for (const entry of getShader().entries()) {
|
|
1857
|
+
if (entry.wgsl.includes('f_schlick')) {
|
|
1858
|
+
standardEntry = entry;
|
|
1859
|
+
break;
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
if (standardEntry === undefined) return undefined;
|
|
1863
|
+
const pbrManifestEntry = [...getShader().materialShaderManifestEntries()].find(
|
|
1864
|
+
(entry) => entry.identifier === 'forgeax::default-standard-pbr',
|
|
1865
|
+
);
|
|
1866
|
+
const noColorVariant = selectNoColorPbrVariant(
|
|
1867
|
+
pbrManifestEntry,
|
|
1868
|
+
device.caps.storageBuffer,
|
|
1869
|
+
variantSet,
|
|
1870
|
+
deriveExtendedLightingCapability(device).admitted,
|
|
1871
|
+
device.caps.backendKind === 'webgpu' || device.caps.backendKind === 'wgpu-native',
|
|
1872
|
+
(device.limits.maxSampledTexturesPerShaderStage ?? 0) >=
|
|
1873
|
+
STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES,
|
|
1874
|
+
);
|
|
1875
|
+
if (noColorVariant !== undefined && noColorVariant.composedWgsl !== standardEntry.wgsl) {
|
|
1876
|
+
return { ...standardEntry, wgsl: noColorVariant.composedWgsl };
|
|
1877
|
+
}
|
|
1878
|
+
const runtimePbr = getShader().findMaterialArtifact('forgeax::default-standard-pbr');
|
|
1879
|
+
if (runtimePbr.ok && runtimePbr.value.source !== standardEntry.wgsl) {
|
|
1880
|
+
return { ...standardEntry, wgsl: runtimePbr.value.source };
|
|
1881
|
+
}
|
|
1882
|
+
return standardEntry;
|
|
1883
|
+
};
|
|
1884
|
+
// Shared builder context for the per-MaterialShader pipeline cache.
|
|
1885
|
+
// Built once; reused for both registered shader IDs and fallback
|
|
1886
|
+
// paths (D-3). Null when manifest is empty (Camera-only path).
|
|
1887
|
+
//
|
|
1888
|
+
// feat-20260609-hdrp-cluster-fragment-ggx M4.5 / w37 (D-10 option A):
|
|
1889
|
+
// `variantSet` parameter selects between URP `pbrPipelineLayout` (1-slot
|
|
1890
|
+
// group(2) BGL) and HDRP `hdrpPbrPipelineLayout` (7-slot group(2) BGL)
|
|
1891
|
+
// via `selectPipelineLayoutForVariant`. When undefined (legacy callers
|
|
1892
|
+
// / fallback path) the URP layout is used preserving prior behaviour.
|
|
1893
|
+
const buildPipelineContext = (
|
|
1894
|
+
variantSet?: string,
|
|
1895
|
+
materialShaderId?: string,
|
|
1896
|
+
group2Contract: PipelineGroup2Contract = 'mesh',
|
|
1897
|
+
// feat-20260611-fox-skinning-vertex-attribute-chain M4 / w16 (D-4):
|
|
1898
|
+
// when layoutKind === 'pbr-skin' the SSOT for the vertex buffer layout
|
|
1899
|
+
// is `deriveVertexBufferLayout` (vertex-attribute-layout.ts) — the same
|
|
1900
|
+
// function that the WGSL @location(N) declarations / naga reflect tests
|
|
1901
|
+
// consume. A caller passing a real `MeshAsset.attributes` produces the
|
|
1902
|
+
// exact 6-attribute / 72-byte layout the skin shader expects; passing
|
|
1903
|
+
// undefined falls into the synthetic 6-key sentinel below (the skin
|
|
1904
|
+
// path is fully determined by layoutKind, so key-presence is the only
|
|
1905
|
+
// signal `deriveVertexBufferLayout` reads — values never matter).
|
|
1906
|
+
meshAttributes?: VertexAttributeMap,
|
|
1907
|
+
vertexLayout?: string,
|
|
1908
|
+
vertexLayoutProjection?: VertexLayoutProjection,
|
|
1909
|
+
shaderModuleMode?: RenderFeatureShaderModuleMode,
|
|
1910
|
+
layoutKindOverride?: LayoutKind,
|
|
1911
|
+
) => {
|
|
1912
|
+
const contractLayoutKind: LayoutKind =
|
|
1913
|
+
group2Contract === 'cluster'
|
|
1914
|
+
? 'hdrp-pbr'
|
|
1915
|
+
: group2Contract === 'skin-cluster'
|
|
1916
|
+
? 'hdrp-skin'
|
|
1917
|
+
: group2Contract === 'skin'
|
|
1918
|
+
? 'pbr-skin'
|
|
1919
|
+
: 'pbr';
|
|
1920
|
+
const layoutKind = layoutKindOverride ?? contractLayoutKind;
|
|
1921
|
+
const usesSharedBootMaterialLayout =
|
|
1922
|
+
materialShaderId !== undefined && isEngineOwnedMaterialShader(materialShaderId);
|
|
1923
|
+
// Built-in PBR/unlit/skin layouts are assembled at boot. Re-deriving a
|
|
1924
|
+
// per-shader layout from the expanded PBR schema would bypass the HDRP
|
|
1925
|
+
// group(2) selector and bind the URP mesh BGL to the cluster pipeline.
|
|
1926
|
+
const perShaderLayout =
|
|
1927
|
+
!usesSharedBootMaterialLayout && materialShaderId !== undefined
|
|
1928
|
+
? getOrBuildPerShaderMaterialLayout(materialShaderId, undefined, layoutKind)
|
|
1929
|
+
: null;
|
|
1930
|
+
// feat-20260621-learn-render-5-5-parallax M2 / w6 (D-1): an authored
|
|
1931
|
+
// shader owns a per-shader pipeline layout derived from its paramSchema,
|
|
1932
|
+
// because its compact texture order may differ from standard PBR even
|
|
1933
|
+
// when it declares only a few textures. Skin / HDRP keep their dedicated
|
|
1934
|
+
// layouts; built-in material IDs remain on the shared boot-time layout.
|
|
1935
|
+
const bindingContract =
|
|
1936
|
+
materialShaderId === undefined
|
|
1937
|
+
? 'render-material'
|
|
1938
|
+
: getCachedMaterialShaderBindingContract(materialShaderId);
|
|
1939
|
+
const group0Layout = bindingContract === 'group-0' ? getOrBuildGroup0MaterialLayout() : null;
|
|
1940
|
+
const group0ResourceLayout =
|
|
1941
|
+
bindingContract === 'group-0-resource' && materialShaderId !== undefined
|
|
1942
|
+
? getOrBuildGroup0ResourceLayout(materialShaderId)
|
|
1943
|
+
: null;
|
|
1944
|
+
const viewOnlyLayout =
|
|
1945
|
+
bindingContract === 'view-only' ? getOrBuildViewOnlyMaterialPipelineLayout() : null;
|
|
1946
|
+
const viewAndSceneDepthLayout =
|
|
1947
|
+
bindingContract === 'view-and-scene-depth'
|
|
1948
|
+
? getOrBuildViewAndSceneDepthMaterialPipelineLayout()
|
|
1949
|
+
: null;
|
|
1950
|
+
const preparedMaterialLayout =
|
|
1951
|
+
bindingContract === 'render-material' &&
|
|
1952
|
+
materialShaderId !== undefined &&
|
|
1953
|
+
(vertexLayout === RENDER_FEATURE_VERTEX_LAYOUTS.billboardMaterialInstance ||
|
|
1954
|
+
vertexLayout === RENDER_FEATURE_VERTEX_LAYOUTS.meshGeometryMaterialInstance)
|
|
1955
|
+
? getOrBuildPreparedMaterialPipelineLayout(materialShaderId)
|
|
1956
|
+
: null;
|
|
1957
|
+
const vertexInputContract =
|
|
1958
|
+
materialShaderId === undefined
|
|
1959
|
+
? 'render-material'
|
|
1960
|
+
: (() => {
|
|
1961
|
+
const lookup = getShader().findMaterialArtifact(materialShaderId);
|
|
1962
|
+
return lookup.ok
|
|
1963
|
+
? resolveMaterialShaderVertexInputContract(lookup.value.source)
|
|
1964
|
+
: 'render-material';
|
|
1965
|
+
})();
|
|
1966
|
+
const currentState = currentPipelineState();
|
|
1967
|
+
const pipelineLayout =
|
|
1968
|
+
group0Layout !== null
|
|
1969
|
+
? group0Layout.pipelineLayout
|
|
1970
|
+
: group0ResourceLayout !== null
|
|
1971
|
+
? group0ResourceLayout.pipelineLayout
|
|
1972
|
+
: viewOnlyLayout !== null
|
|
1973
|
+
? viewOnlyLayout
|
|
1974
|
+
: viewAndSceneDepthLayout !== null
|
|
1975
|
+
? viewAndSceneDepthLayout
|
|
1976
|
+
: preparedMaterialLayout !== null
|
|
1977
|
+
? preparedMaterialLayout
|
|
1978
|
+
: perShaderLayout !== null
|
|
1979
|
+
? perShaderLayout.pipelineLayout
|
|
1980
|
+
: selectPipelineLayoutForVariant(currentState, variantSet, layoutKind);
|
|
1981
|
+
if (pipelineLayout === null) return null;
|
|
1982
|
+
const resolvedUvSetCount =
|
|
1983
|
+
materialShaderId !== undefined
|
|
1984
|
+
? currentMaterialShaderUvSetCounts().get(materialShaderId)
|
|
1985
|
+
: undefined;
|
|
1986
|
+
const vertexBuffers: readonly GPUVertexBufferLayout[] =
|
|
1987
|
+
vertexInputContract === 'none'
|
|
1988
|
+
? []
|
|
1989
|
+
: vertexLayout === RENDER_FEATURE_VERTEX_LAYOUTS.positionSizeColorInstance
|
|
1990
|
+
? POSITION_SIZE_COLOR_INSTANCE_VERTEX_BUFFERS
|
|
1991
|
+
: vertexLayout === RENDER_FEATURE_VERTEX_LAYOUTS.billboardMaterialInstance
|
|
1992
|
+
? BILLBOARD_MATERIAL_INSTANCE_VERTEX_BUFFERS
|
|
1993
|
+
: vertexLayout === RENDER_FEATURE_VERTEX_LAYOUTS.topologySegmentInstance
|
|
1994
|
+
? TOPOLOGY_SEGMENT_INSTANCE_VERTEX_BUFFERS
|
|
1995
|
+
: vertexLayout === RENDER_FEATURE_VERTEX_LAYOUTS.meshGeometryMaterialInstance
|
|
1996
|
+
? MESH_GEOMETRY_MATERIAL_INSTANCE_VERTEX_BUFFERS
|
|
1997
|
+
: vertexLayoutProjection !== undefined
|
|
1998
|
+
? toGpuVertexBufferLayouts(
|
|
1999
|
+
deriveVertexBufferLayoutFromProjection(
|
|
2000
|
+
vertexLayoutProjection,
|
|
2001
|
+
resolvedUvSetCount !== undefined
|
|
2002
|
+
? { shaderUvSetCount: resolvedUvSetCount }
|
|
2003
|
+
: undefined,
|
|
2004
|
+
),
|
|
2005
|
+
)
|
|
2006
|
+
: layoutKind === 'pbr-skin'
|
|
2007
|
+
? toGpuVertexBufferLayouts(
|
|
2008
|
+
deriveVertexBufferLayout(
|
|
2009
|
+
meshAttributes ??
|
|
2010
|
+
({
|
|
2011
|
+
position: PBR_SKIN_SENTINEL_ATTR_BUFFER,
|
|
2012
|
+
normal: PBR_SKIN_SENTINEL_ATTR_BUFFER,
|
|
2013
|
+
uv: PBR_SKIN_SENTINEL_ATTR_BUFFER,
|
|
2014
|
+
tangent: PBR_SKIN_SENTINEL_ATTR_BUFFER,
|
|
2015
|
+
skinIndex: PBR_SKIN_SENTINEL_ATTR_BUFFER,
|
|
2016
|
+
skinWeight: PBR_SKIN_SENTINEL_ATTR_BUFFER,
|
|
2017
|
+
} satisfies VertexAttributeMap),
|
|
2018
|
+
resolvedUvSetCount !== undefined
|
|
2019
|
+
? { shaderUvSetCount: resolvedUvSetCount }
|
|
2020
|
+
: undefined,
|
|
2021
|
+
),
|
|
2022
|
+
)
|
|
2023
|
+
: meshAttributes !== undefined ||
|
|
2024
|
+
(resolvedUvSetCount !== undefined && resolvedUvSetCount > 1)
|
|
2025
|
+
? toGpuVertexBufferLayouts(
|
|
2026
|
+
deriveVertexBufferLayout(
|
|
2027
|
+
meshAttributes ?? DEFAULT_VERTEX_ATTRS,
|
|
2028
|
+
resolvedUvSetCount !== undefined && resolvedUvSetCount > 1
|
|
2029
|
+
? { shaderUvSetCount: resolvedUvSetCount }
|
|
2030
|
+
: undefined,
|
|
2031
|
+
),
|
|
2032
|
+
)
|
|
2033
|
+
: ([
|
|
2034
|
+
{
|
|
2035
|
+
arrayStride: 12 * 4,
|
|
2036
|
+
attributes: [
|
|
2037
|
+
{ shaderLocation: 0, offset: 0, format: 'float32x3' as const },
|
|
2038
|
+
{ shaderLocation: 1, offset: 3 * 4, format: 'float32x3' as const },
|
|
2039
|
+
{ shaderLocation: 2, offset: 6 * 4, format: 'float32x2' as const },
|
|
2040
|
+
{ shaderLocation: 3, offset: 8 * 4, format: 'float32x4' as const },
|
|
2041
|
+
],
|
|
2042
|
+
},
|
|
2043
|
+
] satisfies readonly GPUVertexBufferLayout[]);
|
|
2044
|
+
return {
|
|
2045
|
+
device: currentBuildDevice(),
|
|
2046
|
+
shaderModuleFactory:
|
|
2047
|
+
shaderModuleMode === 'immediate'
|
|
2048
|
+
? getImmediateShaderModuleAdapter()
|
|
2049
|
+
: getShaderModuleAdapter(),
|
|
2050
|
+
pipelineLayout,
|
|
2051
|
+
vertexBuffers,
|
|
2052
|
+
...(layoutKind === 'pbr-skin' ? { layoutKind } : {}),
|
|
2053
|
+
};
|
|
2054
|
+
};
|
|
2055
|
+
/**
|
|
2056
|
+
* Builds a pipeline for the given entry, caches it keyed by cacheKey,
|
|
2057
|
+
* and returns it. Returns null on build failure (firing errorRegistry
|
|
2058
|
+
* for non-transient errors).
|
|
2059
|
+
*/
|
|
2060
|
+
const buildAndCachePipeline = (
|
|
2061
|
+
cacheKey: string,
|
|
2062
|
+
entry: MaterialShaderEntry,
|
|
2063
|
+
label: string,
|
|
2064
|
+
moduleLabel: string,
|
|
2065
|
+
isHdr: boolean,
|
|
2066
|
+
renderState: MaterialRenderState | undefined,
|
|
2067
|
+
topology: PrimitiveTopology | undefined,
|
|
2068
|
+
stripIndexFormat: 'uint16' | 'uint32' | undefined,
|
|
2069
|
+
// feat-20260609 M4.5 / w37 (D-10): thread variantSet to the layout selector
|
|
2070
|
+
// so HDRP-variant PSOs build with `hdrpPbrPipelineLayout` (7-slot group(2))
|
|
2071
|
+
// and URP-variant PSOs build with `pbrPipelineLayout` (1-slot group(2)).
|
|
2072
|
+
variantSet?: string,
|
|
2073
|
+
passKind: PassKind = 'forward',
|
|
2074
|
+
// bug-20260611-skin-pipeline-layout: thread materialShaderId so
|
|
2075
|
+
// buildPipelineContext can derive `LayoutKind === 'pbr-skin'` and pick
|
|
2076
|
+
// the 2-entry mesh-array BGL chain.
|
|
2077
|
+
materialShaderId?: string,
|
|
2078
|
+
// feat-20260611-fox-skinning-vertex-attribute-chain M4 / w16 (D-4):
|
|
2079
|
+
// pass the per-mesh `VertexAttributeMap` so the pbr-skin path's
|
|
2080
|
+
// vertex buffer layout flows from the SSOT `deriveVertexBufferLayout`
|
|
2081
|
+
// (vertex-attribute-layout.ts) instead of a parallel hardcoded copy.
|
|
2082
|
+
// Undefined falls into the synthetic 6-key sentinel inside
|
|
2083
|
+
// `buildPipelineContext` (key-presence-only); non-skin layoutKinds
|
|
2084
|
+
// ignore this parameter entirely.
|
|
2085
|
+
meshAttributes?: VertexAttributeMap,
|
|
2086
|
+
// bug-20260615 M2 / m2-1: sampleCount drives the multisample descriptor
|
|
2087
|
+
// field in buildPipelineForMaterialShader — it is a CAMERA fact (per-frame
|
|
2088
|
+
// antialias setting), not a material renderState value. Default 1 preserves
|
|
2089
|
+
// byte-identity of every existing pre-M2 cache slot + descriptor.
|
|
2090
|
+
sampleCount: number = 1,
|
|
2091
|
+
// feat-20260625-refactor-sprite-as-transparent-mesh R2 fix-up: LDR-color
|
|
2092
|
+
// override for sub-passes that write to a non-default attachment view.
|
|
2093
|
+
// Pre-feat the dedicated `forgeax::default-sprite` SPEC_CONST entries
|
|
2094
|
+
// (SPRITE_ATTACHMENTS_LDR_S1/S4) targeted the swap-chain STORAGE format
|
|
2095
|
+
// (non-sRGB) directly. Post-w14 sprite falls into the generic lazy build
|
|
2096
|
+
// path which defaults to `pipelineState.colorAttachmentFormat` (the sRGB
|
|
2097
|
+
// VIEW format used by the geometry pass) — incompatible with the sprite
|
|
2098
|
+
// sub-pass's non-sRGB attachment view (bgra8unorm / rgba8unorm), firing
|
|
2099
|
+
// a per-frame "Attachment state ... not compatible" validation error.
|
|
2100
|
+
// The sprite sub-pass call site passes `pipelineState.format` (storage,
|
|
2101
|
+
// non-sRGB) here so the resulting PSO matches the encoder's attachment
|
|
2102
|
+
// state. Undefined preserves the default (sRGB view) path used by every
|
|
2103
|
+
// pre-fix-up caller. Ignored when `isHdr=true` or `passKind='shadow-caster'`.
|
|
2104
|
+
colorFormatOverride?: GPUTextureFormat,
|
|
2105
|
+
// Prepared graphics passes use `null` to request an explicit color-only
|
|
2106
|
+
// pipeline; omitted preserves the material forward depth default.
|
|
2107
|
+
depthFormatOverride?: GPUTextureFormat | null,
|
|
2108
|
+
vertexLayout?: string,
|
|
2109
|
+
vertexLayoutProjection?: VertexLayoutProjection,
|
|
2110
|
+
shaderModuleMode?: RenderFeatureShaderModuleMode,
|
|
2111
|
+
layoutKindOverride?: LayoutKind,
|
|
2112
|
+
additionalColorFormats?: readonly GPUTextureFormat[],
|
|
2113
|
+
): RenderPipeline | null => {
|
|
2114
|
+
const group2Contract = resolvePipelineGroup2Contract(entry.source);
|
|
2115
|
+
const ctx = buildPipelineContext(
|
|
2116
|
+
variantSet,
|
|
2117
|
+
materialShaderId,
|
|
2118
|
+
group2Contract,
|
|
2119
|
+
meshAttributes,
|
|
2120
|
+
vertexLayout,
|
|
2121
|
+
vertexLayoutProjection,
|
|
2122
|
+
shaderModuleMode,
|
|
2123
|
+
layoutKindOverride,
|
|
2124
|
+
);
|
|
2125
|
+
if (ctx === null) return null;
|
|
2126
|
+
// bug-20260612: route the LDR color format through pipelineState so the
|
|
2127
|
+
// backend-aware swap-chain format chosen at buildReadyWebGPU flows here.
|
|
2128
|
+
// Callers gate on pipelineState !== null before reaching this lambda
|
|
2129
|
+
// (search "buildAndCachePipeline" call sites — every one is preceded by
|
|
2130
|
+
// the explicit null check).
|
|
2131
|
+
const currentState = currentPipelineState();
|
|
2132
|
+
if (currentState === null) return null;
|
|
2133
|
+
const ldrColorFormat = colorFormatOverride ?? currentState.colorAttachmentFormat;
|
|
2134
|
+
const fragmentEntry =
|
|
2135
|
+
isHdr &&
|
|
2136
|
+
(materialShaderId === 'forgeax::sprite' || materialShaderId === 'forgeax::sprite-lit')
|
|
2137
|
+
? 'fs_main_hdr'
|
|
2138
|
+
: undefined;
|
|
2139
|
+
const built = buildPipelineForMaterialShader(
|
|
2140
|
+
cacheKey,
|
|
2141
|
+
// The catalog entry is projected into the material-shader entry shape.
|
|
2142
|
+
entry,
|
|
2143
|
+
{
|
|
2144
|
+
...ctx,
|
|
2145
|
+
colorFormat: isHdr ? HDR_COLOR_ATTACHMENT_FORMAT : ldrColorFormat,
|
|
2146
|
+
colorFormats:
|
|
2147
|
+
additionalColorFormats === undefined
|
|
2148
|
+
? undefined
|
|
2149
|
+
: [isHdr ? HDR_COLOR_ATTACHMENT_FORMAT : ldrColorFormat, ...additionalColorFormats],
|
|
2150
|
+
depthFormat:
|
|
2151
|
+
depthFormatOverride === null ? undefined : (depthFormatOverride ?? DEPTH_TEXTURE_FORMAT),
|
|
2152
|
+
label,
|
|
2153
|
+
// feat-20260604 w16-b: the shader-MODULE cache identity. Stable across
|
|
2154
|
+
// topology / renderState / isHdr / indexFormat (all baked into the PSO,
|
|
2155
|
+
// not the module), so every pipeline variant of the same shader source
|
|
2156
|
+
// reuses one compiled module instead of forcing a fresh async compile
|
|
2157
|
+
// per variant. See PipelineBuilderContext.moduleLabel.
|
|
2158
|
+
moduleLabel,
|
|
2159
|
+
},
|
|
2160
|
+
renderState,
|
|
2161
|
+
// w8/w15: pack topology (+ stripIndexFormat) into the builder's geometry
|
|
2162
|
+
// param. Strip topologies bake stripIndexFormat into the immutable PSO
|
|
2163
|
+
// (WebGPU spec: only valid for line-strip / triangle-strip). The record
|
|
2164
|
+
// stage (w9 + w15) threads each mesh's topology + indexFormat here; when
|
|
2165
|
+
// the caller omits stripIndexFormat we fall back to 'uint32' (the engine
|
|
2166
|
+
// procedural index width: createBoxGeometry etc. emit Uint32 indices).
|
|
2167
|
+
topology !== undefined
|
|
2168
|
+
? {
|
|
2169
|
+
topology,
|
|
2170
|
+
...(topology === 'line-strip' || topology === 'triangle-strip'
|
|
2171
|
+
? { stripIndexFormat: stripIndexFormat ?? ('uint32' as const) }
|
|
2172
|
+
: {}),
|
|
2173
|
+
}
|
|
2174
|
+
: undefined,
|
|
2175
|
+
undefined, // vertexEntry — default vs_main
|
|
2176
|
+
fragmentEntry,
|
|
2177
|
+
undefined, // defines — none
|
|
2178
|
+
passKind,
|
|
2179
|
+
sampleCount,
|
|
2180
|
+
);
|
|
2181
|
+
if (!built.ok) {
|
|
2182
|
+
if (built.error.code !== 'rhi-not-available') {
|
|
2183
|
+
internals.errorRegistry.fire(built.error);
|
|
2184
|
+
}
|
|
2185
|
+
return null;
|
|
2186
|
+
}
|
|
2187
|
+
currentPipelineCacheState().materialShaderPipelineCache.set(cacheKey, built.value);
|
|
2188
|
+
materialShaderPipelineGroup2Contracts.set(
|
|
2189
|
+
built.value as object,
|
|
2190
|
+
resolvePipelineGroup2Contract(entry.source),
|
|
2191
|
+
);
|
|
2192
|
+
return built.value;
|
|
2193
|
+
};
|
|
2194
|
+
const getMaterialShaderPipeline = (
|
|
2195
|
+
materialShaderId: string,
|
|
2196
|
+
isHdr: boolean,
|
|
2197
|
+
renderState?: MaterialRenderState,
|
|
2198
|
+
topology?: PrimitiveTopology,
|
|
2199
|
+
indexFormat?: 'uint16' | 'uint32',
|
|
2200
|
+
variantSet?: string,
|
|
2201
|
+
passKind: PassKind = 'forward',
|
|
2202
|
+
// feat-20260611-fox-skinning-vertex-attribute-chain M4 / w16 (D-4):
|
|
2203
|
+
// forwarded to `buildAndCachePipeline -> buildPipelineContext` so the
|
|
2204
|
+
// pbr-skin layout chain reads from the `deriveVertexBufferLayout` SSOT.
|
|
2205
|
+
// Optional: existing callers (URP / HDRP / shadow-depth) pass undefined
|
|
2206
|
+
// and the hardcoded 4-attribute layout is preserved (AC-04 zero-regression).
|
|
2207
|
+
meshAttributes?: VertexAttributeMap,
|
|
2208
|
+
// bug-20260615 M2 / m2-1: sampleCount is threaded through to the cache key,
|
|
2209
|
+
// buildAndCachePipeline, and ultimately buildPipelineForMaterialShader which
|
|
2210
|
+
// sets the multisample descriptor field. Default 1 preserves byte-identity
|
|
2211
|
+
// of every pre-M2 caller.
|
|
2212
|
+
sampleCount: number = 1,
|
|
2213
|
+
// feat-20260625-refactor-sprite-as-transparent-mesh R2 fix-up: LDR-color
|
|
2214
|
+
// override for sub-passes that write to a non-default attachment view.
|
|
2215
|
+
// The LDR sprite split sub-pass writes through the storage (non-sRGB)
|
|
2216
|
+
// view of the swap-chain texture; the encoder's beginRenderPass
|
|
2217
|
+
// colorFormats uses `pipelineState.format` (storage), so the PSO must
|
|
2218
|
+
// build with the same non-sRGB format or WebGPU rejects SetPipeline
|
|
2219
|
+
// with "Attachment state ... not compatible". Pre-w14 the dedicated
|
|
2220
|
+
// `forgeax::default-sprite` SPEC_CONST entries (deleted) baked this
|
|
2221
|
+
// mapping into SPRITE_ATTACHMENTS_LDR_S1/S4; the generic lazy build
|
|
2222
|
+
// path that replaced them defaults to `colorAttachmentFormat` (the
|
|
2223
|
+
// sRGB view used by the geometry pass), so transparent-split callers
|
|
2224
|
+
// must override. Threaded into both the cache key (via the spec's
|
|
2225
|
+
// `attachments.colorFormats`) and the actual PSO descriptor. Ignored
|
|
2226
|
+
// for `isHdr=true` (HDR sub-pass uses rgba16float) and
|
|
2227
|
+
// `passKind='shadow-caster'` (depth-only, no color attachment).
|
|
2228
|
+
colorFormatOverride?: GPUTextureFormat,
|
|
2229
|
+
// feat-20260629-multi-uv-set-support m3-w5: shader-declared UV set count,
|
|
2230
|
+
// forwarded to PipelineSpec.geometry.shaderUvSetCount for clamp-to-last
|
|
2231
|
+
// alias. Undefined = fallback to mesh-provided count (no clamping).
|
|
2232
|
+
// m4-w3: auto-filled from naga reflection when caller passes undefined.
|
|
2233
|
+
shaderUvSetCount?: number,
|
|
2234
|
+
// Prepared graphics passes use `null` to request an explicit color-only
|
|
2235
|
+
// pipeline; omitted preserves the material forward depth default.
|
|
2236
|
+
depthFormatOverride?: GPUTextureFormat | null,
|
|
2237
|
+
vertexLayout?: string,
|
|
2238
|
+
vertexLayoutProjection?: VertexLayoutProjection,
|
|
2239
|
+
shaderModuleMode: RenderFeatureShaderModuleMode = 'validated',
|
|
2240
|
+
layoutKindOverride?: LayoutKind,
|
|
2241
|
+
additionalColorFormats?: readonly GPUTextureFormat[],
|
|
2242
|
+
): RenderPipeline | null => {
|
|
2243
|
+
// WebGL2 is a downlevel backend even when the record stage asks for the
|
|
2244
|
+
// native-capability variant string. Resolve the variant axes at this
|
|
2245
|
+
// shared PSO seam so the shader source, BGL layout, and cache key agree on
|
|
2246
|
+
// the actual device capabilities. Without this, WebGL2 can build a
|
|
2247
|
+
// storage-buffer PSO around a uniform-buffer shader and only reject it at
|
|
2248
|
+
// queue submit as an invalid RenderPipeline.
|
|
2249
|
+
const currentState = currentPipelineState();
|
|
2250
|
+
const ldrColorFormat: GPUTextureFormat =
|
|
2251
|
+
colorFormatOverride ??
|
|
2252
|
+
(currentState !== null ? currentState.colorAttachmentFormat : 'bgra8unorm-srgb');
|
|
2253
|
+
// feat-20260629 M4: auto-fill shaderUvSetCount from naga reflection
|
|
2254
|
+
// (stored in materialShaderUvSetCounts during prepareMaterialShaders).
|
|
2255
|
+
const resolvedUvSetCount =
|
|
2256
|
+
shaderUvSetCount ?? currentMaterialShaderUvSetCounts().get(materialShaderId);
|
|
2257
|
+
const manifestEntry = findMaterialShaderManifestEntry(materialShaderId);
|
|
2258
|
+
// Sprite's omitted request is the boot-selected default artifact. The
|
|
2259
|
+
// explicit empty key remains the PER_INSTANCE_REGION=true request used by
|
|
2260
|
+
// SpriteInstances, so only that path is capability-rewritten here.
|
|
2261
|
+
const resolvedVariantSet =
|
|
2262
|
+
variantSet === undefined &&
|
|
2263
|
+
(materialShaderId === 'forgeax::sprite' || materialShaderId === 'forgeax::sprite-lit')
|
|
2264
|
+
? undefined
|
|
2265
|
+
: resolveCachedMaterialShaderVariantSet(variantSet, manifestEntry);
|
|
2266
|
+
// The record stage also composes engine capability axes for user shaders.
|
|
2267
|
+
// Keep only manifest-declared axes for non-engine material IDs: a plain
|
|
2268
|
+
// custom shader must not collapse that request to '' because '' is the
|
|
2269
|
+
// canonical HDRP variant key and would pair an HDRP PSO with the URP mesh
|
|
2270
|
+
// bind group. Builtin forgeax:: shaders keep the compatibility request
|
|
2271
|
+
// when a reduced test/legacy manifest omits variant metadata.
|
|
2272
|
+
// A single-source manifest entry must stay on its canonical module key;
|
|
2273
|
+
// otherwise a synthesized capability axis races the prewarmed module.
|
|
2274
|
+
let effectiveVariantSet = normalizeMaterialShaderVariantSet(resolvedVariantSet, manifestEntry);
|
|
2275
|
+
if (
|
|
2276
|
+
vertexLayoutProjection === undefined &&
|
|
2277
|
+
manifestEntry?.variants.some((variant) => 'VERTEX_COLOR_AVAILABLE' in variant.defines)
|
|
2278
|
+
) {
|
|
2279
|
+
if (effectiveVariantSet === '') {
|
|
2280
|
+
const noColorVariant = manifestEntry.variants.find(
|
|
2281
|
+
(variant) =>
|
|
2282
|
+
variant.defines.VERTEX_COLOR_AVAILABLE === false &&
|
|
2283
|
+
Object.entries(variant.defines).every(
|
|
2284
|
+
([axis, value]) => axis === 'VERTEX_COLOR_AVAILABLE' || value === true,
|
|
2285
|
+
),
|
|
2286
|
+
);
|
|
2287
|
+
effectiveVariantSet = noColorVariant?.definesKey ?? 'VERTEX_COLOR_AVAILABLE=false';
|
|
2288
|
+
} else {
|
|
2289
|
+
effectiveVariantSet = effectiveVariantSet?.replace(
|
|
2290
|
+
'VERTEX_COLOR_AVAILABLE=true',
|
|
2291
|
+
'VERTEX_COLOR_AVAILABLE=false',
|
|
2292
|
+
);
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
if (
|
|
2296
|
+
manifestEntry !== undefined &&
|
|
2297
|
+
!materialShaderId.startsWith('forgeax::') &&
|
|
2298
|
+
effectiveVariantSet !== undefined &&
|
|
2299
|
+
effectiveVariantSet !== ''
|
|
2300
|
+
) {
|
|
2301
|
+
const declaredAxes = new Set<string>();
|
|
2302
|
+
for (const variant of manifestEntry.variants) {
|
|
2303
|
+
for (const axis of Object.keys(variant.defines)) declaredAxes.add(axis);
|
|
2304
|
+
}
|
|
2305
|
+
const filteredVariantParts = effectiveVariantSet
|
|
2306
|
+
.split('+')
|
|
2307
|
+
.filter((part) => declaredAxes.has(part.slice(0, part.indexOf('='))))
|
|
2308
|
+
.sort();
|
|
2309
|
+
effectiveVariantSet =
|
|
2310
|
+
filteredVariantParts.length === 0 ? undefined : filteredVariantParts.join('+');
|
|
2311
|
+
}
|
|
2312
|
+
// feat-20260615-pipeline-spec-ssot M2-T2: cache key derived from PipelineSpec
|
|
2313
|
+
// 4-axis SSOT via cacheKeyOf(spec). The spec carries all 4 axes (shader /
|
|
2314
|
+
// attachments / geometry / renderState), replacing the legacy 8-segment string
|
|
2315
|
+
// construction. vertexLayout is now included in the cache key (was previously
|
|
2316
|
+
// threaded but not hashed — research F1 R-VertexLayout-Cache). sampleCount is
|
|
2317
|
+
// cast to 1 | 4 for the closed spec axis.
|
|
2318
|
+
// M2-fixup follow-up: LDR color format SSOT is `pipelineState.colorAttachmentFormat`
|
|
2319
|
+
// (set in buildReadyWebGPU from selectSwapChainFormat). Hardcoding 'bgra8unorm-srgb'
|
|
2320
|
+
// here let the cache key drift from the prewarm key on backends where
|
|
2321
|
+
// getPreferredCanvasFormat returns rgba8unorm (dawn-node, lavapipe, wgpu-wasm GLES),
|
|
2322
|
+
// forcing a redundant second PSO build on first-frame URP record path.
|
|
2323
|
+
const colorFormat: GPUTextureFormat = isHdr ? 'rgba16float' : ldrColorFormat;
|
|
2324
|
+
const isDepthOnlyPass = passKindPolicyTable[passKind]?.shape === 'depth-only';
|
|
2325
|
+
const spec: PipelineSpec = {
|
|
2326
|
+
shader: { id: materialShaderId, passKind, variantSet: effectiveVariantSet },
|
|
2327
|
+
attachments: {
|
|
2328
|
+
colorFormats:
|
|
2329
|
+
additionalColorFormats === undefined
|
|
2330
|
+
? colorFormatsForPassKind(passKind, colorFormat)
|
|
2331
|
+
: [colorFormat, ...additionalColorFormats],
|
|
2332
|
+
depthFormat: isDepthOnlyPass
|
|
2333
|
+
? 'depth32float'
|
|
2334
|
+
: depthFormatOverride === null
|
|
2335
|
+
? undefined
|
|
2336
|
+
: (depthFormatOverride ?? 'depth24plus-stencil8'),
|
|
2337
|
+
sampleCount: (sampleCount === 4 ? 4 : 1) as 1 | 4,
|
|
2338
|
+
},
|
|
2339
|
+
geometry: {
|
|
2340
|
+
topology: topology ?? 'triangle-list',
|
|
2341
|
+
stripIndexFormat: indexFormat,
|
|
2342
|
+
vertexLayout:
|
|
2343
|
+
meshAttributes ??
|
|
2344
|
+
(vertexLayout === RENDER_FEATURE_VERTEX_LAYOUTS.positionSizeColorInstance
|
|
2345
|
+
? PREPARED_INSTANCE_VERTEX_ATTRS
|
|
2346
|
+
: vertexLayout === RENDER_FEATURE_VERTEX_LAYOUTS.billboardMaterialInstance ||
|
|
2347
|
+
vertexLayout === RENDER_FEATURE_VERTEX_LAYOUTS.topologySegmentInstance ||
|
|
2348
|
+
vertexLayout === RENDER_FEATURE_VERTEX_LAYOUTS.meshGeometryMaterialInstance
|
|
2349
|
+
? PREPARED_MATERIAL_INSTANCE_VERTEX_ATTRS
|
|
2350
|
+
: DEFAULT_VERTEX_ATTRS),
|
|
2351
|
+
// Thread the shader-declared count whenever it is greater than one.
|
|
2352
|
+
// Built-in PBR reserves all eight supported UV inputs; single-UV meshes
|
|
2353
|
+
// remain byte-stable because missing sets are clamp-to-last aliases.
|
|
2354
|
+
...(resolvedUvSetCount !== undefined && resolvedUvSetCount > 1
|
|
2355
|
+
? { shaderUvSetCount: resolvedUvSetCount }
|
|
2356
|
+
: {}),
|
|
2357
|
+
...(vertexLayoutProjection === undefined ? {} : { vertexLayoutProjection }),
|
|
2358
|
+
},
|
|
2359
|
+
renderState,
|
|
2360
|
+
};
|
|
2361
|
+
const cacheKey = cacheKeyOf(spec);
|
|
2362
|
+
const cached = currentPipelineCacheState().materialShaderPipelineCache.get(cacheKey);
|
|
2363
|
+
if (cached !== undefined) return cached;
|
|
2364
|
+
if (pipelineState === null) return null;
|
|
2365
|
+
const lookup = getShader().findMaterialArtifact(materialShaderId);
|
|
2366
|
+
if (!lookup.ok) {
|
|
2367
|
+
if (shouldDeferMissingPreparedMaterialShader(vertexLayout)) {
|
|
2368
|
+
// Prepared VFX draws bind only their feature-owned groups. The
|
|
2369
|
+
// built-in PBR fallback declares mesh/instance groups 2/3 and would
|
|
2370
|
+
// consequently emit an invalid pipeline before the authored shader
|
|
2371
|
+
// becomes available. Let the feature retry on the next frame.
|
|
2372
|
+
return null;
|
|
2373
|
+
}
|
|
2374
|
+
// bug-20260527-renderstate-pipeline-dispatch-gap D-3:
|
|
2375
|
+
// fallback path parity -- when renderState is defined and the
|
|
2376
|
+
// shader id is not registered, build a renderState-variant of
|
|
2377
|
+
// the standard pipeline from the engine-shipped PBR entry.
|
|
2378
|
+
if (
|
|
2379
|
+
renderState === undefined &&
|
|
2380
|
+
!allowsUnlitPreparedFallback(depthFormatOverride, vertexLayout, materialShaderId)
|
|
2381
|
+
) {
|
|
2382
|
+
// A prepared feature declares the vertex layout it owns. Falling back
|
|
2383
|
+
// to default-unlit here would pair that data with the unlit shader's
|
|
2384
|
+
// position/normal/uv/tangent inputs and let WebGPU reject the
|
|
2385
|
+
// pipeline at validation time. Keep the missing shader retryable and
|
|
2386
|
+
// preserve the declared input contract instead.
|
|
2387
|
+
return null;
|
|
2388
|
+
}
|
|
2389
|
+
if (renderState === undefined) {
|
|
2390
|
+
const unlitLookup = getShader().findMaterialArtifact('forgeax::default-unlit');
|
|
2391
|
+
if (!unlitLookup.ok) return null;
|
|
2392
|
+
return buildAndCachePipeline(
|
|
2393
|
+
cacheKey,
|
|
2394
|
+
unlitLookup.value,
|
|
2395
|
+
`pbr-pipeline-prepared-${materialShaderId}`,
|
|
2396
|
+
// Reuse the eagerly compiled engine unlit module. The adapter cache
|
|
2397
|
+
// is seeded under this label during renderer readiness; a new label
|
|
2398
|
+
// would keep color-only prepared passes in perpetual warm-up.
|
|
2399
|
+
'unlit',
|
|
2400
|
+
isHdr,
|
|
2401
|
+
undefined,
|
|
2402
|
+
topology,
|
|
2403
|
+
indexFormat,
|
|
2404
|
+
effectiveVariantSet,
|
|
2405
|
+
passKind,
|
|
2406
|
+
'forgeax::default-unlit',
|
|
2407
|
+
meshAttributes,
|
|
2408
|
+
sampleCount,
|
|
2409
|
+
colorFormatOverride,
|
|
2410
|
+
depthFormatOverride,
|
|
2411
|
+
vertexLayout,
|
|
2412
|
+
vertexLayoutProjection,
|
|
2413
|
+
shaderModuleMode,
|
|
2414
|
+
layoutKindOverride,
|
|
2415
|
+
additionalColorFormats,
|
|
2416
|
+
);
|
|
2417
|
+
}
|
|
2418
|
+
const pbrEntry = findStandardPbrEntry(effectiveVariantSet);
|
|
2419
|
+
if (pbrEntry === undefined) return null;
|
|
2420
|
+
return buildAndCachePipeline(
|
|
2421
|
+
cacheKey,
|
|
2422
|
+
{ source: pbrEntry.wgsl, paramSchema: [] },
|
|
2423
|
+
`pbr-pipeline-fallback-${materialShaderId}${isHdr ? '-hdr' : ''}`,
|
|
2424
|
+
// w16-b: module identity is the fallback PBR source, stable across
|
|
2425
|
+
// topology / renderState / HDR (all baked into the PSO) so every
|
|
2426
|
+
// variant reuses one compiled module.
|
|
2427
|
+
'module-fallback-pbr',
|
|
2428
|
+
isHdr,
|
|
2429
|
+
renderState,
|
|
2430
|
+
topology,
|
|
2431
|
+
indexFormat,
|
|
2432
|
+
// M4.5 / w37 (D-10): the fallback path also threads variantSet so the
|
|
2433
|
+
// layout selector picks HDRP layout when an HDRP caller falls into
|
|
2434
|
+
// this branch (registered shader id missing).
|
|
2435
|
+
effectiveVariantSet,
|
|
2436
|
+
// feat-20260609 / T-002: passKind threaded through the fallback path
|
|
2437
|
+
// for parity with the main path; default 'forward' keeps every prior
|
|
2438
|
+
// fallback caller byte-identical.
|
|
2439
|
+
passKind,
|
|
2440
|
+
// bug-20260611-skin-pipeline-layout: passing materialShaderId here is
|
|
2441
|
+
// intentional even on the fallback (registered-id-missing) branch --
|
|
2442
|
+
// a missing skin shader registration should not silently pick the
|
|
2443
|
+
// wrong BGL chain. With LayoutKind='pbr-skin' the selector returns
|
|
2444
|
+
// null when pbrSkinPipelineLayout is null (charter P3 explicit fail).
|
|
2445
|
+
//
|
|
2446
|
+
// MaterialAsset per-slot texCoord: this branch always compiles the
|
|
2447
|
+
// built-in PBR module (`pbrEntry.wgsl` / 'module-fallback-pbr'), whose
|
|
2448
|
+
// vertex stage declares all eight UV inputs. The vertex-buffer layout
|
|
2449
|
+
// therefore has to be the PBR layout with all declared slots present,
|
|
2450
|
+
// not the caller shader's -- e.g. a transparent sprite / sprite-lit
|
|
2451
|
+
// material lands here with meshAttributes carrying only uv0, so the
|
|
2452
|
+
// sprite id would resolve a 48-byte 4-attribute layout and the PBR
|
|
2453
|
+
// module would reject a missing UV slot. Pass the built-in PBR id for
|
|
2454
|
+
// layout resolution so buildPipelineContext derives the layout that
|
|
2455
|
+
// matches the compiled module. Skin keeps its
|
|
2456
|
+
// own id so the pbr-skin fail-fast (null layout) is preserved.
|
|
2457
|
+
materialShaderId === SKIN_MATERIAL_SHADER_ID
|
|
2458
|
+
? materialShaderId
|
|
2459
|
+
: 'forgeax::default-standard-pbr',
|
|
2460
|
+
// feat-20260611-fox-skinning-vertex-attribute-chain M4 / w16 (D-4):
|
|
2461
|
+
// forward meshAttributes through the fallback path so the pbr-skin
|
|
2462
|
+
// layout chain reads from the deriveVertexBufferLayout SSOT here too.
|
|
2463
|
+
meshAttributes,
|
|
2464
|
+
sampleCount,
|
|
2465
|
+
colorFormatOverride,
|
|
2466
|
+
depthFormatOverride,
|
|
2467
|
+
vertexLayout,
|
|
2468
|
+
vertexLayoutProjection,
|
|
2469
|
+
shaderModuleMode,
|
|
2470
|
+
layoutKindOverride,
|
|
2471
|
+
additionalColorFormats,
|
|
2472
|
+
);
|
|
2473
|
+
}
|
|
2474
|
+
// feat-20260609 M4 / w31: resolve variant WGSL from manifest when
|
|
2475
|
+
// variantSet is non-empty. The boot-time registered shader (from
|
|
2476
|
+
// `installMaterialArtifact` at line ~2473) carries the default (all-true)
|
|
2477
|
+
// variant's WGSL. For URP callers that want a different variant
|
|
2478
|
+
// (e.g. STORAGE_BUFFER_AVAILABLE=true without CLUSTER_FORWARD_AVAILABLE),
|
|
2479
|
+
// we look up the manifest entry, find the matching variant, and
|
|
2480
|
+
// substitute its composedWgsl into the PSO build path. The pipeline
|
|
2481
|
+
// layout itself is built by buildPbrPipelineLayouts (M3 / w12).
|
|
2482
|
+
// When variantSet is empty/undefined, the boot-time registered entry
|
|
2483
|
+
// (which is the all-true default) is used verbatim — backward compat.
|
|
2484
|
+
let shaderEntry = lookup.value;
|
|
2485
|
+
// M4.5 / w38 (D-11): `variantSet === ''` is canonical all-true (HDRP
|
|
2486
|
+
// path) and MUST hit the manifest variant lookup -- treat it as a
|
|
2487
|
+
// first-class variant request, not a falsy "no variant" signal.
|
|
2488
|
+
// Use `!== undefined` so the empty-string case enters the lookup.
|
|
2489
|
+
if (effectiveVariantSet !== undefined) {
|
|
2490
|
+
const registry = getShader();
|
|
2491
|
+
for (const msEntry of registry.materialShaderManifestEntries()) {
|
|
2492
|
+
if (msEntry.identifier === materialShaderId) {
|
|
2493
|
+
const variant = findVariantByKey(msEntry, effectiveVariantSet);
|
|
2494
|
+
if (variant) {
|
|
2495
|
+
// M3 / w12-w13: variant substitution carries the same source +
|
|
2496
|
+
// paramSchema as the boot-registered entry; the binding layout
|
|
2497
|
+
// is no longer carried on MaterialShaderEntry (deleted in
|
|
2498
|
+
// w13) — buildPbrPipelineLayouts is the BGL SSOT and reads
|
|
2499
|
+
// derive(paramSchema).bglEntries on demand.
|
|
2500
|
+
shaderEntry = {
|
|
2501
|
+
source: variant.composedWgsl,
|
|
2502
|
+
paramSchema: lookup.value.paramSchema,
|
|
2503
|
+
paramSchemaProjection: lookup.value.paramSchemaProjection,
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
break;
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
// feat-20260609 M4 / R3-fixup: append `-${passKind}` to the PSO label so
|
|
2511
|
+
// GPU debug captures (and the shadow-caster branch in the builder)
|
|
2512
|
+
// make the cache variant visible. The fallback path above keeps the
|
|
2513
|
+
// pre-existing fallback-* label shape (no shadow-caster fallback
|
|
2514
|
+
// exists today; the only shadow caller registers shadowCaster directly).
|
|
2515
|
+
const passKindLabelSegment = passKind === 'forward' ? '' : `-${passKind}`;
|
|
2516
|
+
return buildAndCachePipeline(
|
|
2517
|
+
cacheKey,
|
|
2518
|
+
shaderEntry,
|
|
2519
|
+
`pbr-pipeline-${materialShaderId}${isHdr ? '-hdr' : ''}${passKindLabelSegment}`,
|
|
2520
|
+
// feat-20260609 M4 / w31: when variantSet is non-empty, the module identity
|
|
2521
|
+
// includes the variant key so URP (STORAGE_BUFFER_AVAILABLE=true) and HDRP
|
|
2522
|
+
// (CLUSTER_FORWARD_AVAILABLE=true+STORAGE_BUFFER_AVAILABLE=true) variants
|
|
2523
|
+
// compile as separate shader modules (they have different WGSL sources).
|
|
2524
|
+
// When variantSet is empty/undefined, the module identity stays pre-M4
|
|
2525
|
+
// backward-compatible for all PSOs of the default variant.
|
|
2526
|
+
// M4.5 / w38 (D-11): same `!== undefined` discipline as the cache
|
|
2527
|
+
// key -- `''` (canonical all-true) gets its own module-label slot,
|
|
2528
|
+
// distinct from the no-variant path. Trailing `#` for the empty
|
|
2529
|
+
// case is intentional (parallel to the cache key's `:variant:`
|
|
2530
|
+
// empty-tail segment); module identity stays a function of the
|
|
2531
|
+
// exact variantSet string.
|
|
2532
|
+
effectiveVariantSet !== undefined
|
|
2533
|
+
? `module-${materialShaderId}#${effectiveVariantSet}`
|
|
2534
|
+
: `module-${materialShaderId}`,
|
|
2535
|
+
isHdr,
|
|
2536
|
+
renderState,
|
|
2537
|
+
topology,
|
|
2538
|
+
indexFormat,
|
|
2539
|
+
// M4.5 / w37 (D-10): main path threads variantSet so HDRP-variant PSO
|
|
2540
|
+
// builds against `hdrpPbrPipelineLayout` (7-slot group(2) BGL) and URP
|
|
2541
|
+
// builds against `pbrPipelineLayout` (1-slot group(2) BGL).
|
|
2542
|
+
effectiveVariantSet,
|
|
2543
|
+
// feat-20260609 / T-002: passKind selects createRenderPipeline
|
|
2544
|
+
// attachment shape (forward color+DS vs shadow-caster depth32float
|
|
2545
|
+
// no-color). Orthogonal to variantSet (which selects the BGL chain).
|
|
2546
|
+
passKind,
|
|
2547
|
+
// bug-20260611-skin-pipeline-layout: thread the registered materialShaderId
|
|
2548
|
+
// so buildPipelineContext can resolve LayoutKind='pbr-skin' for the skin
|
|
2549
|
+
// shader (2-entry mesh-array BGL).
|
|
2550
|
+
materialShaderId,
|
|
2551
|
+
// feat-20260611-fox-skinning-vertex-attribute-chain M4 / w16 (D-4):
|
|
2552
|
+
// forward meshAttributes so the pbr-skin path's vertex buffer layout
|
|
2553
|
+
// is derived via the SSOT (deriveVertexBufferLayout). For URP/HDRP
|
|
2554
|
+
// callers (and for a caller passing undefined) the synthetic 6-key
|
|
2555
|
+
// sentinel inside buildPipelineContext keeps the layout deterministic.
|
|
2556
|
+
meshAttributes,
|
|
2557
|
+
sampleCount,
|
|
2558
|
+
colorFormatOverride,
|
|
2559
|
+
depthFormatOverride,
|
|
2560
|
+
vertexLayout,
|
|
2561
|
+
vertexLayoutProjection,
|
|
2562
|
+
shaderModuleMode,
|
|
2563
|
+
layoutKindOverride,
|
|
2564
|
+
additionalColorFormats,
|
|
2565
|
+
);
|
|
2566
|
+
};
|
|
2567
|
+
const getMaterialShaderPipelineEntry = (
|
|
2568
|
+
...args: Parameters<typeof getMaterialShaderPipeline>
|
|
2569
|
+
) => {
|
|
2570
|
+
const pipeline = getMaterialShaderPipeline(...args);
|
|
2571
|
+
if (pipeline === null) return null;
|
|
2572
|
+
return {
|
|
2573
|
+
pipeline,
|
|
2574
|
+
group2Contract:
|
|
2575
|
+
materialShaderPipelineGroup2Contracts.get(pipeline as object) ?? ('mesh' as const),
|
|
2576
|
+
};
|
|
2577
|
+
};
|
|
2578
|
+
const getParamSchema = (materialShaderId: string) => {
|
|
2579
|
+
const lookup = getShader().findMaterialArtifact(materialShaderId);
|
|
2580
|
+
return lookup.ok ? lookup.value.paramSchema : undefined;
|
|
2581
|
+
};
|
|
2582
|
+
// feat-20260621-learn-render-5-5-parallax M2 / w6 (D-1): expose the per-shader
|
|
2583
|
+
// material BGL so the record stage creates the material bind group against
|
|
2584
|
+
// the matching paramSchema-derived layout. Returns undefined only for
|
|
2585
|
+
// group-0 / view-only contracts or an unavailable shader layout; built-in
|
|
2586
|
+
// material IDs continue to resolve through the shared layout path.
|
|
2587
|
+
const getMaterialBindGroupLayout = (
|
|
2588
|
+
materialShaderId: string,
|
|
2589
|
+
materialParamSchema?: readonly ParamSchemaEntry[],
|
|
2590
|
+
): BindGroupLayout | undefined => {
|
|
2591
|
+
if (isEngineOwnedMaterialShader(materialShaderId)) return undefined;
|
|
2592
|
+
const contract = getCachedMaterialShaderBindingContract(materialShaderId);
|
|
2593
|
+
if (contract === 'group-0') {
|
|
2594
|
+
return getOrBuildGroup0MaterialLayout()?.materialBgl;
|
|
2595
|
+
}
|
|
2596
|
+
if (contract === 'group-0-resource') {
|
|
2597
|
+
return getOrBuildGroup0ResourceLayout(materialShaderId)?.materialBgl;
|
|
2598
|
+
}
|
|
2599
|
+
if (contract === 'view-and-scene-depth') return undefined;
|
|
2600
|
+
return (
|
|
2601
|
+
getOrBuildPerShaderMaterialLayout(materialShaderId, materialParamSchema)?.materialBgl ??
|
|
2602
|
+
undefined
|
|
2603
|
+
);
|
|
2604
|
+
};
|
|
2605
|
+
// feat-20260609 M4 / T-10-a: post-process pipeline factory backing
|
|
2606
|
+
// RenderSystemRuntime.getPostProcessPipeline. Solves M1 CONCERN-1: previously
|
|
2607
|
+
// the dispatcher in render-graph-primitives.ts passed `pipeline=null` to
|
|
2608
|
+
// built.createHandle because per-frame execute closures cannot await
|
|
2609
|
+
// device.createShaderModule (async). This factory uses the same shared
|
|
2610
|
+
// makeShaderDeviceAdapter the material-shader pipeline cache uses (sync
|
|
2611
|
+
// wrapper + 1-frame warmup); first-call returns null while the async compile
|
|
2612
|
+
// is in flight; second frame onward returns the built pipeline.
|
|
2613
|
+
//
|
|
2614
|
+
// The pipeline layout is fixed:
|
|
2615
|
+
// group(0) = empty BGL (reserved per render-graph-primitives.ts convention
|
|
2616
|
+
// for view bind groups; populated by future post-process passes
|
|
2617
|
+
// that need view UBOs)
|
|
2618
|
+
// group(1) = the input-texture BGL the dispatcher already composed via
|
|
2619
|
+
// buildFullscreenPostProcessPass (texture + sampler)
|
|
2620
|
+
// Vertex stage: vs_main (no vertex buffers); fragment stage: fs_main targeting
|
|
2621
|
+
// `colorFormat`. Topology: triangle-list with cullMode='none' (3-vertex
|
|
2622
|
+
// fullscreen draw via the canonical fullscreen_triangle pattern).
|
|
2623
|
+
const buildPostProcessPipeline = (
|
|
2624
|
+
entry: PostProcessShaderEntry,
|
|
2625
|
+
bgl: BindGroupLayout,
|
|
2626
|
+
colorFormat: GPUTextureFormat,
|
|
2627
|
+
label: string,
|
|
2628
|
+
): RenderPipeline | null => {
|
|
2629
|
+
const moduleFactory = getShaderModuleAdapter();
|
|
2630
|
+
const moduleResult = moduleFactory.createShaderModule({
|
|
2631
|
+
code: entry.source,
|
|
2632
|
+
label: `${label}-module`,
|
|
2633
|
+
});
|
|
2634
|
+
if (!moduleResult.ok) {
|
|
2635
|
+
// 'rhi-not-available' = async compile in flight: caller falls back one frame.
|
|
2636
|
+
// Other codes are real failures: surface through the error registry so AI
|
|
2637
|
+
// users see a structured RhiError instead of a silent black screen (charter P3).
|
|
2638
|
+
if (moduleResult.error.code !== 'rhi-not-available') {
|
|
2639
|
+
internals.errorRegistry.fire(moduleResult.error);
|
|
2640
|
+
}
|
|
2641
|
+
return null;
|
|
2642
|
+
}
|
|
2643
|
+
if (currentEmptyPostProcessBgl() === null) {
|
|
2644
|
+
const bglRes = currentBuildDevice().createBindGroupLayout({ entries: [] });
|
|
2645
|
+
if (!bglRes.ok) {
|
|
2646
|
+
internals.errorRegistry.fire(bglRes.error);
|
|
2647
|
+
return null;
|
|
2648
|
+
}
|
|
2649
|
+
if (candidateBuildState !== undefined) {
|
|
2650
|
+
candidateBuildState.emptyPostProcessBgl = bglRes.value;
|
|
2651
|
+
candidateEmptyPostProcessBgl = bglRes.value;
|
|
2652
|
+
} else {
|
|
2653
|
+
emptyPostProcessBgl = bglRes.value;
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
const layoutRes = currentBuildDevice().createPipelineLayout({
|
|
2657
|
+
label: `${label}-layout`,
|
|
2658
|
+
bindGroupLayouts: [currentEmptyPostProcessBgl() as BindGroupLayout, bgl],
|
|
2659
|
+
});
|
|
2660
|
+
if (!layoutRes.ok) {
|
|
2661
|
+
internals.errorRegistry.fire(layoutRes.error);
|
|
2662
|
+
return null;
|
|
2663
|
+
}
|
|
2664
|
+
const pipelineRes = currentBuildDevice().createRenderPipeline({
|
|
2665
|
+
label,
|
|
2666
|
+
layout: layoutRes.value,
|
|
2667
|
+
vertex: {
|
|
2668
|
+
module: moduleResult.value,
|
|
2669
|
+
entryPoint: 'vs_main',
|
|
2670
|
+
buffers: [],
|
|
2671
|
+
},
|
|
2672
|
+
fragment: {
|
|
2673
|
+
module: moduleResult.value,
|
|
2674
|
+
entryPoint: 'fs_main',
|
|
2675
|
+
targets:
|
|
2676
|
+
entry.source.includes('@location(1) temporal') &&
|
|
2677
|
+
entry.source.includes('TaaResolveOutput')
|
|
2678
|
+
? [{ format: colorFormat }, { format: colorFormat }]
|
|
2679
|
+
: [{ format: colorFormat }],
|
|
2680
|
+
},
|
|
2681
|
+
primitive: { topology: 'triangle-list', cullMode: 'none', frontFace: 'ccw' },
|
|
2682
|
+
depthStencil: undefined,
|
|
2683
|
+
multisample: undefined,
|
|
2684
|
+
});
|
|
2685
|
+
if (!pipelineRes.ok) {
|
|
2686
|
+
internals.errorRegistry.fire(pipelineRes.error);
|
|
2687
|
+
return null;
|
|
2688
|
+
}
|
|
2689
|
+
return pipelineRes.value;
|
|
2690
|
+
};
|
|
2691
|
+
let activeProfile = freezeRenderProfile(
|
|
2692
|
+
internals.options?.standardProfile ?? DEFAULT_STANDARD_PROFILE,
|
|
2693
|
+
);
|
|
2694
|
+
const renderSystem: RenderSystem = createRenderSystem({
|
|
2695
|
+
canvas: internals.canvas,
|
|
2696
|
+
get build() {
|
|
2697
|
+
return internals.bundler?.build;
|
|
2698
|
+
},
|
|
2699
|
+
ssrIdentity: internals.options?.ssrIdentity,
|
|
2700
|
+
get standardProfile() {
|
|
2701
|
+
return activeProfile;
|
|
2702
|
+
},
|
|
2703
|
+
standardPipeline,
|
|
2704
|
+
get featureHost() {
|
|
2705
|
+
return internals.featureHost;
|
|
2706
|
+
},
|
|
2707
|
+
setFeatureHost: (host) => {
|
|
2708
|
+
internals.featureHost = host;
|
|
2709
|
+
},
|
|
2710
|
+
// Keep the feature-facing factory stable while resolving the adapter at
|
|
2711
|
+
// call time. Renderer recovery swaps the device and invalidates the
|
|
2712
|
+
// per-device adapter; a boot-time snapshot would feed recovered VFX
|
|
2713
|
+
// programs shader modules owned by the lost device.
|
|
2714
|
+
shaderModuleFactory: {
|
|
2715
|
+
createShaderModule: (descriptor) => getShaderModuleAdapter().createShaderModule(descriptor),
|
|
2716
|
+
},
|
|
2717
|
+
immediateShaderModuleFactory: {
|
|
2718
|
+
createShaderModule: (descriptor) =>
|
|
2719
|
+
getImmediateShaderModuleAdapter().createShaderModule(descriptor),
|
|
2720
|
+
},
|
|
2721
|
+
profiler: internals.options?.profiler,
|
|
2722
|
+
// feat-20260622-s5 M3 / w17: device + context read live off `internals` via
|
|
2723
|
+
// getters so the recover() rebuild (which swaps internals.device /
|
|
2724
|
+
// internals.context for a freshly-acquired pair) is observed by the record
|
|
2725
|
+
// stage without reconstructing the RenderSystem (RenderSystemRuntime.device
|
|
2726
|
+
// / RenderSystemInternals.context are read at frame time, not cached).
|
|
2727
|
+
get device() {
|
|
2728
|
+
return internals.device;
|
|
2729
|
+
},
|
|
2730
|
+
get beforeSubmit() {
|
|
2731
|
+
return internals.pack.instrumentation?.beforeSubmit;
|
|
2732
|
+
},
|
|
2733
|
+
createShaderModule: (device, descriptor) =>
|
|
2734
|
+
internals.pack.createShaderModule === undefined
|
|
2735
|
+
? Promise.resolve(invokeDeviceCreateShaderModule(device, descriptor))
|
|
2736
|
+
: internals.pack.createShaderModule(device, descriptor),
|
|
2737
|
+
get deviceScope() {
|
|
2738
|
+
return activeDeviceScope;
|
|
2739
|
+
},
|
|
2740
|
+
...(internals.pack.instrumentation?.resolveSurfaceDevice === undefined
|
|
2741
|
+
? {}
|
|
2742
|
+
: { resolveSurfaceDevice: internals.pack.instrumentation.resolveSurfaceDevice }),
|
|
2743
|
+
invalidateShaderModule: (label: string) => {
|
|
2744
|
+
activeShaderState.sharedShaderModuleAdapter?.invalidateModule(label);
|
|
2745
|
+
},
|
|
2746
|
+
get context() {
|
|
2747
|
+
return internals.context;
|
|
2748
|
+
},
|
|
2749
|
+
get debugOverlay() {
|
|
2750
|
+
return internals.debugOverlay;
|
|
2751
|
+
},
|
|
2752
|
+
// feat-20260608-create-app-param-surface-trim / M1 / AC-02: clearColor
|
|
2753
|
+
// is no longer threaded through createRenderSystem; the record stage
|
|
2754
|
+
// reads `camera.clearColor` straight from the Camera SoA column
|
|
2755
|
+
// (array<f32,4>, feat-20260709 M3).
|
|
2756
|
+
getPipelineState: () => pipelineState,
|
|
2757
|
+
assets,
|
|
2758
|
+
get gpuStore() {
|
|
2759
|
+
return gpuStore;
|
|
2760
|
+
},
|
|
2761
|
+
get dynamicTextureStore() {
|
|
2762
|
+
return dynamicTextureStore;
|
|
2763
|
+
},
|
|
2764
|
+
errorRegistry: internals.errorRegistry,
|
|
2765
|
+
healthRegistry: internals.healthRegistry,
|
|
2766
|
+
getMaterialShaderPipeline,
|
|
2767
|
+
getMaterialShaderPipelineEntry,
|
|
2768
|
+
getMaterialShaderBindingContract: getCachedMaterialShaderBindingContract,
|
|
2769
|
+
getParamSchema,
|
|
2770
|
+
getMaterialBindGroupLayout,
|
|
2771
|
+
metrics,
|
|
2772
|
+
// feat-20260609 M4 / T-10-a: post-process pipeline factory (CONCERN-1 fix).
|
|
2773
|
+
// createRenderSystem wraps this in a per-RenderSystem cache + the public
|
|
2774
|
+
// getPostProcessPipeline lookup the dispatcher reads at frame time.
|
|
2775
|
+
buildPostProcessPipeline,
|
|
2776
|
+
// feat-20260608-mesh-ssbo-dynamic-grow-l1-lift-1024-entity-cap M3 / T-M3-04:
|
|
2777
|
+
// forward the grow hook + state via getter closures — buildReadyWebGPU
|
|
2778
|
+
// sets `internals.growMeshSsbo` / `internals.meshSsboState` after this
|
|
2779
|
+
// factory call, so the record stage reads them through the closures
|
|
2780
|
+
// (read at frame time, when ready has already settled).
|
|
2781
|
+
get growMeshSsbo() {
|
|
2782
|
+
return internals.growMeshSsbo;
|
|
2783
|
+
},
|
|
2784
|
+
get meshSsboState() {
|
|
2785
|
+
return internals.meshSsboState;
|
|
2786
|
+
},
|
|
2787
|
+
get gpuPassTimingSession() {
|
|
2788
|
+
return (
|
|
2789
|
+
internals as WebGPURendererInternals & {
|
|
2790
|
+
gpuPassTimingSession?: GpuPassTimingSession | undefined;
|
|
2791
|
+
}
|
|
2792
|
+
).gpuPassTimingSession;
|
|
2793
|
+
},
|
|
2794
|
+
set gpuPassTimingSession(session: GpuPassTimingSession | undefined) {
|
|
2795
|
+
(
|
|
2796
|
+
internals as WebGPURendererInternals & {
|
|
2797
|
+
gpuPassTimingSession?: GpuPassTimingSession | undefined;
|
|
2798
|
+
}
|
|
2799
|
+
).gpuPassTimingSession = session;
|
|
2800
|
+
},
|
|
2801
|
+
get gpuPassTimingCapture() {
|
|
2802
|
+
return (
|
|
2803
|
+
internals as WebGPURendererInternals & {
|
|
2804
|
+
gpuPassTimingCapture?: GpuPassTimingCapture | undefined;
|
|
2805
|
+
}
|
|
2806
|
+
).gpuPassTimingCapture;
|
|
2807
|
+
},
|
|
2808
|
+
set gpuPassTimingCapture(capture: GpuPassTimingCapture | undefined) {
|
|
2809
|
+
(
|
|
2810
|
+
internals as WebGPURendererInternals & {
|
|
2811
|
+
gpuPassTimingCapture?: GpuPassTimingCapture | undefined;
|
|
2812
|
+
}
|
|
2813
|
+
).gpuPassTimingCapture = capture;
|
|
2814
|
+
},
|
|
2815
|
+
get gpuPassTimingSubmittedWork() {
|
|
2816
|
+
return internals.gpuPassTimingSubmittedWork;
|
|
2817
|
+
},
|
|
2818
|
+
set gpuPassTimingSubmittedWork(completion: Promise<void> | undefined) {
|
|
2819
|
+
internals.gpuPassTimingSubmittedWork = completion;
|
|
2820
|
+
},
|
|
2821
|
+
get gpuPassTimingFrameIdentity() {
|
|
2822
|
+
return (
|
|
2823
|
+
internals as WebGPURendererInternals & {
|
|
2824
|
+
gpuPassTimingFrameIdentity?:
|
|
2825
|
+
| import('../record/gpu-pass-timing/session.js').GpuPassTimingFrameIdentity
|
|
2826
|
+
| undefined;
|
|
2827
|
+
}
|
|
2828
|
+
).gpuPassTimingFrameIdentity;
|
|
2829
|
+
},
|
|
2830
|
+
set gpuPassTimingFrameIdentity(identity:
|
|
2831
|
+
| import('../record/gpu-pass-timing/session.js').GpuPassTimingFrameIdentity
|
|
2832
|
+
| undefined,) {
|
|
2833
|
+
(
|
|
2834
|
+
internals as WebGPURendererInternals & {
|
|
2835
|
+
gpuPassTimingFrameIdentity?:
|
|
2836
|
+
| import('../record/gpu-pass-timing/session.js').GpuPassTimingFrameIdentity
|
|
2837
|
+
| undefined;
|
|
2838
|
+
}
|
|
2839
|
+
).gpuPassTimingFrameIdentity = identity;
|
|
2840
|
+
},
|
|
2841
|
+
get gpuPassTimingBeginReason() {
|
|
2842
|
+
return (
|
|
2843
|
+
internals as WebGPURendererInternals & {
|
|
2844
|
+
gpuPassTimingBeginReason?: GpuPassTimingReason | undefined;
|
|
2845
|
+
}
|
|
2846
|
+
).gpuPassTimingBeginReason;
|
|
2847
|
+
},
|
|
2848
|
+
set gpuPassTimingBeginReason(reason: GpuPassTimingReason | undefined) {
|
|
2849
|
+
(
|
|
2850
|
+
internals as WebGPURendererInternals & {
|
|
2851
|
+
gpuPassTimingBeginReason?: GpuPassTimingReason | undefined;
|
|
2852
|
+
}
|
|
2853
|
+
).gpuPassTimingBeginReason = reason;
|
|
2854
|
+
},
|
|
2855
|
+
getRenderTargetPhysical: (target) => renderTargetHost.getPhysicalTarget(target),
|
|
2856
|
+
resolveRenderTargetTextureSource: (source) =>
|
|
2857
|
+
renderTargetHost.resolveRenderTargetTextureSource(source),
|
|
2858
|
+
encodeRenderTargetReadbacks: (encoder, faces) =>
|
|
2859
|
+
renderTargetHost.encodePendingReadbacks(encoder, faces),
|
|
2860
|
+
get volumetricFogShaders() {
|
|
2861
|
+
return internals.volumetricFogShaders;
|
|
2862
|
+
},
|
|
2863
|
+
});
|
|
2864
|
+
attachGpuPassTimingSession(internals, gpuPassTimingSession);
|
|
2865
|
+
renderSystemForTargetPromotion = renderSystem;
|
|
2866
|
+
if (
|
|
2867
|
+
internals.device.caps.backendKind === 'null' &&
|
|
2868
|
+
Array.from(getShader().entries()).length === 0
|
|
2869
|
+
) {
|
|
2870
|
+
// RhiNull validates graph topology only; keep the built-in identity
|
|
2871
|
+
// registered so structural frames do not fail before graph inspection.
|
|
2872
|
+
renderSystem.registerBuiltinPostProcess(STANDARD_OUTPUT_TRANSFORM_FEATURE_ID, {
|
|
2873
|
+
source: '',
|
|
2874
|
+
params: { byteSize: 16, defaultValue: new Uint8Array(16) },
|
|
2875
|
+
reads: ['hdrColor'],
|
|
2876
|
+
});
|
|
2877
|
+
}
|
|
2878
|
+
// The Standard pipeline is the sole built-in graph owner. Feature plans are
|
|
2879
|
+
// projected into that graph directly; no second post-process registry is
|
|
2880
|
+
// installed for fullscreen producers.
|
|
2881
|
+
renderSystem.configureStandard(undefined);
|
|
2882
|
+
renderSystem.restorePostProcessResources();
|
|
2883
|
+
const attachedWorlds = new Set<World>();
|
|
2884
|
+
const attachedLeases = new Map<RenderReadLease, World>();
|
|
2885
|
+
const leasesByWorld = new Map<World, RenderReadLease>();
|
|
2886
|
+
const derivedSystemNames = new Map<World, string>();
|
|
2887
|
+
const transformReleases = new Map<World, () => void>();
|
|
2888
|
+
const attachmentOwner = {};
|
|
2889
|
+
let frameId = 0;
|
|
2890
|
+
// Receipts remain observable until their device generation is retired. A
|
|
2891
|
+
// registry avoids invalidating a still-valid frame merely because another
|
|
2892
|
+
// submission completed in the meantime.
|
|
2893
|
+
const currentReceipts = new Set<FrameReceipt>();
|
|
2894
|
+
const issuedReceipts = new WeakSet<FrameReceipt>();
|
|
2895
|
+
const receiptTimings = new WeakMap<FrameReceipt, Promise<VolumeTimingObservation>>();
|
|
2896
|
+
let latestReceipt: FrameReceipt | undefined;
|
|
2897
|
+
let surfaceReleased = false;
|
|
2898
|
+
|
|
2899
|
+
const renderer: RendererAssemblyImplementation = {
|
|
2900
|
+
attach(world: World): RenderResult<RenderWorldLease, RenderError> {
|
|
2901
|
+
const attached = this.attachScene(world);
|
|
2902
|
+
if (!attached.ok) {
|
|
2903
|
+
return err(new RendererContractFailureError('attach', attached.error.hint));
|
|
2904
|
+
}
|
|
2905
|
+
const lease = createRenderReadLease(world, attachmentOwner);
|
|
2906
|
+
attachedLeases.set(lease, world);
|
|
2907
|
+
leasesByWorld.set(world, lease);
|
|
2908
|
+
return ok(lease);
|
|
2909
|
+
},
|
|
2910
|
+
createRenderTarget: (descriptor) => renderTargetHost.createRenderTarget(descriptor),
|
|
2911
|
+
resizeRenderTarget: (target, descriptor) =>
|
|
2912
|
+
renderTargetHost.resizeRenderTarget(target, descriptor),
|
|
2913
|
+
createRenderTargetTextureSource: (target, options) =>
|
|
2914
|
+
renderTargetHost.createRenderTargetTextureSource(target, options),
|
|
2915
|
+
requestTargetReadback: (target, request) =>
|
|
2916
|
+
renderTargetHost.requestTargetReadback(target, request),
|
|
2917
|
+
destroyRenderTarget: (target) => renderTargetHost.destroyRenderTarget(target),
|
|
2918
|
+
setProfile(profile: RenderProfile): RenderResult<void, RenderError> {
|
|
2919
|
+
const invalid = validateRenderProfile(profile);
|
|
2920
|
+
if (invalid !== undefined) {
|
|
2921
|
+
return err(
|
|
2922
|
+
new RendererOperationError('frame-input-invalid', {
|
|
2923
|
+
operation: 'set-profile',
|
|
2924
|
+
cause: new RendererContractFailureError('draw', invalid),
|
|
2925
|
+
}),
|
|
2926
|
+
);
|
|
2927
|
+
}
|
|
2928
|
+
const previous = activeProfile;
|
|
2929
|
+
activeProfile = freezeRenderProfile(profile);
|
|
2930
|
+
try {
|
|
2931
|
+
renderSystem.configureStandard(undefined);
|
|
2932
|
+
return ok(undefined);
|
|
2933
|
+
} catch (cause) {
|
|
2934
|
+
activeProfile = previous;
|
|
2935
|
+
return err(
|
|
2936
|
+
new RendererOperationError('graph-build-failed', {
|
|
2937
|
+
operation: 'set-profile',
|
|
2938
|
+
cause: structuredRendererCause(cause, 'set-profile'),
|
|
2939
|
+
}),
|
|
2940
|
+
);
|
|
2941
|
+
}
|
|
2942
|
+
},
|
|
2943
|
+
inspectLodOcclusion() {
|
|
2944
|
+
return {
|
|
2945
|
+
lodOcclusion: renderSystem.lodOcclusionInspection,
|
|
2946
|
+
gpuDriven: renderSystem.gpuDrivenInspection,
|
|
2947
|
+
};
|
|
2948
|
+
},
|
|
2949
|
+
inspect(): RenderInspection {
|
|
2950
|
+
const health = internals.healthRegistry.getLastSnapshot();
|
|
2951
|
+
const deviceLost = health.reason === 'device-lost';
|
|
2952
|
+
const state = disposed
|
|
2953
|
+
? 'disposed'
|
|
2954
|
+
: deviceLost
|
|
2955
|
+
? 'device-lost'
|
|
2956
|
+
: health.reason === 'internal-fault' || !readySettled
|
|
2957
|
+
? 'faulted'
|
|
2958
|
+
: 'alive';
|
|
2959
|
+
const graph = deviceLost ? undefined : renderSystem.perFrameGraphInfo;
|
|
2960
|
+
const standardLighting = deviceLost ? undefined : renderSystem.standardLightingInspection;
|
|
2961
|
+
const bloom: BloomInspection = deviceLost
|
|
2962
|
+
? {
|
|
2963
|
+
graphStatus: 'empty',
|
|
2964
|
+
enabled: false,
|
|
2965
|
+
targetCount: 0,
|
|
2966
|
+
targetBytes: 0,
|
|
2967
|
+
resourceCount: 0,
|
|
2968
|
+
passCount: 0,
|
|
2969
|
+
encodeCount: 0,
|
|
2970
|
+
bindGroupCount: 0,
|
|
2971
|
+
uploadCount: 0,
|
|
2972
|
+
residentChildBytes: 0,
|
|
2973
|
+
generation: 0,
|
|
2974
|
+
state: 'off',
|
|
2975
|
+
}
|
|
2976
|
+
: renderSystem.bloom;
|
|
2977
|
+
const surfaceStorage = pipelineState?.format ?? 'rgba8unorm';
|
|
2978
|
+
const surfaceDisplay = pipelineState?.colorAttachmentFormat ?? 'rgba8unorm';
|
|
2979
|
+
const observationId = `${STANDARD_OUTPUT_TRANSFORM_FEATURE_ID}:frame-${frameId}`;
|
|
2980
|
+
const outputInspection = projectRendererOutputInspection({
|
|
2981
|
+
graph,
|
|
2982
|
+
surfaceAvailable: !surfaceReleased && pipelineState !== null,
|
|
2983
|
+
surfaceStorage,
|
|
2984
|
+
surfaceDisplay,
|
|
2985
|
+
...(pipelineState?.surfaceProfile === undefined
|
|
2986
|
+
? {}
|
|
2987
|
+
: { surfaceProfile: pipelineState.surfaceProfile }),
|
|
2988
|
+
rgba16floatRenderable: internals.device.caps.rgba16floatRenderable,
|
|
2989
|
+
...(internals.context?.presentationProof === undefined
|
|
2990
|
+
? {}
|
|
2991
|
+
: { presentationProof: internals.context.presentationProof }),
|
|
2992
|
+
});
|
|
2993
|
+
const volumetricFog = renderSystem.volumetricFog;
|
|
2994
|
+
const directionalShadow = renderSystem.directionalShadow;
|
|
2995
|
+
const recoveryEvidence = renderSystem.recoveryEvidence;
|
|
2996
|
+
return Object.freeze({
|
|
2997
|
+
state,
|
|
2998
|
+
recovery: recoveryInspection,
|
|
2999
|
+
surface: surfaceReleased ? 'released' : 'available',
|
|
3000
|
+
profile: activeProfile,
|
|
3001
|
+
capabilities: Object.freeze({ ...internals.device.caps }),
|
|
3002
|
+
frame: Object.freeze({
|
|
3003
|
+
frameId,
|
|
3004
|
+
deviceGeneration: activeDeviceScope.generation,
|
|
3005
|
+
}),
|
|
3006
|
+
features: Object.freeze(
|
|
3007
|
+
(internals.featureHost?.features ?? []).map((feature) => feature.identity),
|
|
3008
|
+
),
|
|
3009
|
+
featureDiagnostics: internals.featureHost?.diagnostics() ?? Object.freeze([]),
|
|
3010
|
+
frustumStats: Object.freeze({ ...renderSystem.frustumStats }),
|
|
3011
|
+
visibilityStats: Object.freeze({ ...renderSystem.visibilityStats }),
|
|
3012
|
+
renderScene: renderSystem.renderScene,
|
|
3013
|
+
reflectionProbes: renderSystem.reflectionProbes,
|
|
3014
|
+
ssrDependencies: renderSystem.ssrDependencies,
|
|
3015
|
+
volumetricFog,
|
|
3016
|
+
environment: renderSystem.environment,
|
|
3017
|
+
temporal: renderSystem.temporal,
|
|
3018
|
+
bloom,
|
|
3019
|
+
extendedLighting: projectExtendedLightingInspection(extendedLightingState),
|
|
3020
|
+
directionalShadow,
|
|
3021
|
+
...(renderSystem.temporalTargetInspection === undefined
|
|
3022
|
+
? {}
|
|
3023
|
+
: { temporalTarget: Object.freeze(renderSystem.temporalTargetInspection) }),
|
|
3024
|
+
...(renderSystem.motionBlurInspection === undefined
|
|
3025
|
+
? {}
|
|
3026
|
+
: { motionBlur: Object.freeze(renderSystem.motionBlurInspection) }),
|
|
3027
|
+
...(renderSystem.lodOcclusionInspection === undefined
|
|
3028
|
+
? {}
|
|
3029
|
+
: { lodOcclusion: Object.freeze(renderSystem.lodOcclusionInspection) }),
|
|
3030
|
+
...(renderSystem.transmission === undefined
|
|
3031
|
+
? {}
|
|
3032
|
+
: { transmission: renderSystem.transmission }),
|
|
3033
|
+
...(standardLighting === undefined
|
|
3034
|
+
? {}
|
|
3035
|
+
: { standardLighting: Object.freeze({ ...standardLighting }) }),
|
|
3036
|
+
...(renderSystem.pointShadowInspection === undefined
|
|
3037
|
+
? {}
|
|
3038
|
+
: { pointShadow: Object.freeze({ ...renderSystem.pointShadowInspection }) }),
|
|
3039
|
+
meshMaterialBindings: Object.freeze(
|
|
3040
|
+
renderSystem.meshMaterialBindings.map((observation) =>
|
|
3041
|
+
Object.freeze({
|
|
3042
|
+
...observation,
|
|
3043
|
+
bindings: Object.freeze(
|
|
3044
|
+
observation.bindings.map((binding) => Object.freeze({ ...binding })),
|
|
3045
|
+
),
|
|
3046
|
+
diagnostics: Object.freeze(
|
|
3047
|
+
observation.diagnostics.map((diagnostic) => Object.freeze({ ...diagnostic })),
|
|
3048
|
+
),
|
|
3049
|
+
}),
|
|
3050
|
+
),
|
|
3051
|
+
),
|
|
3052
|
+
perFramePassNames: Object.freeze(graph?.passes.map((pass) => pass.name) ?? []),
|
|
3053
|
+
bindGroupCounts: Object.freeze({
|
|
3054
|
+
createBindGroup: renderSystem.bindGroupCounts.createBindGroup,
|
|
3055
|
+
keys: Object.freeze([...renderSystem.bindGroupCounts.keys]),
|
|
3056
|
+
}),
|
|
3057
|
+
recoveryEvidence: Object.freeze({
|
|
3058
|
+
...recoveryEvidence,
|
|
3059
|
+
graph: deviceLost
|
|
3060
|
+
? { ready: false, generation: 0, passCount: 0, resourceCount: 0 }
|
|
3061
|
+
: recoveryEvidence.graph,
|
|
3062
|
+
producerRoots: Object.freeze([...recoveryEvidence.producerRoots]),
|
|
3063
|
+
receipts: Object.freeze({
|
|
3064
|
+
count: frameId,
|
|
3065
|
+
lastGeneration: latestReceipt?.deviceGeneration,
|
|
3066
|
+
}),
|
|
3067
|
+
}),
|
|
3068
|
+
...outputInspection,
|
|
3069
|
+
observation: Object.freeze({
|
|
3070
|
+
observationId,
|
|
3071
|
+
frameId,
|
|
3072
|
+
...(renderSystem.lastSuccessfulCameraAntialias === undefined
|
|
3073
|
+
? {}
|
|
3074
|
+
: { antialias: renderSystem.lastSuccessfulCameraAntialias }),
|
|
3075
|
+
...(outputInspection.surfaceProfile === undefined
|
|
3076
|
+
? {}
|
|
3077
|
+
: { surfaceProfile: outputInspection.surfaceProfile }),
|
|
3078
|
+
rgba16floatRenderable: internals.device.caps.rgba16floatRenderable,
|
|
3079
|
+
passNames: Object.freeze([...outputInspection.graphPassNames]),
|
|
3080
|
+
...(outputInspection.standardOutputColor === undefined
|
|
3081
|
+
? {}
|
|
3082
|
+
: { standardOutputColor: Object.freeze({ ...outputInspection.standardOutputColor }) }),
|
|
3083
|
+
}),
|
|
3084
|
+
});
|
|
3085
|
+
},
|
|
3086
|
+
get device(): RhiDevice {
|
|
3087
|
+
return internals.device;
|
|
3088
|
+
},
|
|
3089
|
+
// Keep engine-owned shader consumers on the exact backend pack selected
|
|
3090
|
+
// for this renderer. Importing a backend adapter again from a
|
|
3091
|
+
// feature glue module can produce a second bundled module instance whose
|
|
3092
|
+
// RAW_DEVICE_MAP does not contain this opaque RhiDevice handle.
|
|
3093
|
+
_internal_createShaderModule:
|
|
3094
|
+
internals.pack.createShaderModule ??
|
|
3095
|
+
((device, desc) => invokeDeviceCreateShaderModule(device, desc)),
|
|
3096
|
+
_internal_setRenderOverlay(overlay) {
|
|
3097
|
+
if (overlay === undefined) {
|
|
3098
|
+
delete internals.debugOverlay;
|
|
3099
|
+
} else {
|
|
3100
|
+
internals.debugOverlay = overlay;
|
|
3101
|
+
}
|
|
3102
|
+
},
|
|
3103
|
+
assetRegistry: assets,
|
|
3104
|
+
initialization: ready,
|
|
3105
|
+
attachScene(world: World): Result<void, RhiError> {
|
|
3106
|
+
if (disposed) {
|
|
3107
|
+
return err(
|
|
3108
|
+
new RhiError({
|
|
3109
|
+
code: 'rhi-not-available',
|
|
3110
|
+
expected: 'renderer not disposed before attaching a World',
|
|
3111
|
+
hint: 'rebuild the renderer before attaching another World',
|
|
3112
|
+
}),
|
|
3113
|
+
);
|
|
3114
|
+
}
|
|
3115
|
+
if (attachedWorlds.has(world)) return ok(undefined);
|
|
3116
|
+
try {
|
|
3117
|
+
const releaseTransforms = registerPropagateTransforms(world);
|
|
3118
|
+
const derivedSystemName = `renderDerived:${++nextDerivedSystemId}`;
|
|
3119
|
+
const derived = world.addSystem(Update, {
|
|
3120
|
+
name: derivedSystemName,
|
|
3121
|
+
queries: [],
|
|
3122
|
+
fn: (world) => {
|
|
3123
|
+
tilemapChunkExtractSystem(world, (guid) => assets.lookup(guid));
|
|
3124
|
+
const glyphResult = glyphTextLayoutSystem(world, gpuStore);
|
|
3125
|
+
if (!glyphResult.ok) throw glyphResult.error;
|
|
3126
|
+
},
|
|
3127
|
+
});
|
|
3128
|
+
if (!derived.ok) {
|
|
3129
|
+
releaseTransforms();
|
|
3130
|
+
throw new Error(`failed to register ${derivedSystemName}: ${derived.error.code}`);
|
|
3131
|
+
}
|
|
3132
|
+
transformReleases.set(world, releaseTransforms);
|
|
3133
|
+
derivedSystemNames.set(world, derivedSystemName);
|
|
3134
|
+
attachedWorlds.add(world);
|
|
3135
|
+
return ok(undefined);
|
|
3136
|
+
} catch (cause) {
|
|
3137
|
+
const error = new RhiError({
|
|
3138
|
+
code: 'webgpu-runtime-error',
|
|
3139
|
+
expected: 'renderer.attach(world) installs derived-state systems once',
|
|
3140
|
+
hint: 'inspect the World schedule registration or derived-state system failure',
|
|
3141
|
+
detail: {
|
|
3142
|
+
error: {
|
|
3143
|
+
code: 'unknown',
|
|
3144
|
+
message: cause instanceof Error ? cause.message : String(cause),
|
|
3145
|
+
...(cause instanceof Error ? { name: cause.name } : {}),
|
|
3146
|
+
},
|
|
3147
|
+
},
|
|
3148
|
+
});
|
|
3149
|
+
internals.errorRegistry.fire(error);
|
|
3150
|
+
return err(error);
|
|
3151
|
+
}
|
|
3152
|
+
},
|
|
3153
|
+
detachScene(world: World): void {
|
|
3154
|
+
if (!attachedWorlds.delete(world)) return;
|
|
3155
|
+
for (const [lease, leaseWorld] of attachedLeases) {
|
|
3156
|
+
if (leaseWorld === world) {
|
|
3157
|
+
lease.dispose();
|
|
3158
|
+
attachedLeases.delete(lease);
|
|
3159
|
+
leasesByWorld.delete(world);
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
renderSystem.detachScene(world);
|
|
3163
|
+
const derivedSystemName = derivedSystemNames.get(world);
|
|
3164
|
+
if (derivedSystemName !== undefined) {
|
|
3165
|
+
world.removeSystem(Update, derivedSystemName);
|
|
3166
|
+
derivedSystemNames.delete(world);
|
|
3167
|
+
}
|
|
3168
|
+
transformReleases.get(world)?.();
|
|
3169
|
+
transformReleases.delete(world);
|
|
3170
|
+
},
|
|
3171
|
+
observeCurrentFrame(options: FrameObservationOptions) {
|
|
3172
|
+
return renderSystem.observeCurrentFrame(options);
|
|
3173
|
+
},
|
|
3174
|
+
getCurrentGraphTarget(name: string) {
|
|
3175
|
+
return renderSystem.getCurrentGraphTarget(name);
|
|
3176
|
+
},
|
|
3177
|
+
requestGraphTargetCapture(request) {
|
|
3178
|
+
renderSystem.requestGraphTargetCapture(request);
|
|
3179
|
+
},
|
|
3180
|
+
get frustumStats() {
|
|
3181
|
+
return renderSystem.frustumStats;
|
|
3182
|
+
},
|
|
3183
|
+
get visibilityStats() {
|
|
3184
|
+
return renderSystem.visibilityStats;
|
|
3185
|
+
},
|
|
3186
|
+
get renderScene() {
|
|
3187
|
+
return renderSystem.renderScene;
|
|
3188
|
+
},
|
|
3189
|
+
get meshMaterialBindings() {
|
|
3190
|
+
return renderSystem.meshMaterialBindings;
|
|
3191
|
+
},
|
|
3192
|
+
get perFramePassNames() {
|
|
3193
|
+
return renderSystem.perFramePassNames;
|
|
3194
|
+
},
|
|
3195
|
+
get bindGroupCounts() {
|
|
3196
|
+
return renderSystem.bindGroupCounts;
|
|
3197
|
+
},
|
|
3198
|
+
configureStandard(config) {
|
|
3199
|
+
renderSystem.configureStandard(config);
|
|
3200
|
+
},
|
|
3201
|
+
renderFeatureDiagnostics() {
|
|
3202
|
+
return internals.featureHost?.diagnostics() ?? [];
|
|
3203
|
+
},
|
|
3204
|
+
async installRenderFeature(
|
|
3205
|
+
feature: RenderFeature<unknown>,
|
|
3206
|
+
): Promise<RenderResult<void, RenderError>> {
|
|
3207
|
+
if (disposed) {
|
|
3208
|
+
return err(
|
|
3209
|
+
new RenderFeatureStageFailedError(feature.identity, -1, 'extract', 'registration'),
|
|
3210
|
+
);
|
|
3211
|
+
}
|
|
3212
|
+
const host = internals.featureHost;
|
|
3213
|
+
if (host === undefined) {
|
|
3214
|
+
return err(
|
|
3215
|
+
new RenderFeatureStageFailedError(feature.identity, -1, 'extract', 'registration'),
|
|
3216
|
+
);
|
|
3217
|
+
}
|
|
3218
|
+
const missingCapability = feature.requiredCapabilities?.find(
|
|
3219
|
+
(capability) => internals.device.caps[capability] !== true,
|
|
3220
|
+
);
|
|
3221
|
+
if (missingCapability !== undefined) {
|
|
3222
|
+
return err(
|
|
3223
|
+
new RenderFeatureCapabilityMissingError(feature.identity, host.size, missingCapability),
|
|
3224
|
+
);
|
|
3225
|
+
}
|
|
3226
|
+
let nextRequiredFullscreenPostProcesses: readonly {
|
|
3227
|
+
readonly identity: string;
|
|
3228
|
+
readonly source: string;
|
|
3229
|
+
}[];
|
|
3230
|
+
try {
|
|
3231
|
+
nextRequiredFullscreenPostProcesses = collectRequiredFullscreenPostProcesses([
|
|
3232
|
+
...host.features,
|
|
3233
|
+
feature,
|
|
3234
|
+
]);
|
|
3235
|
+
} catch (cause) {
|
|
3236
|
+
const conflict =
|
|
3237
|
+
cause instanceof RhiError
|
|
3238
|
+
? cause
|
|
3239
|
+
: new RhiError({
|
|
3240
|
+
code: 'internal-error',
|
|
3241
|
+
expected: 'fullscreen feature declarations are internally consistent',
|
|
3242
|
+
hint: 'repair the conflicting fullscreen identity/source declarations',
|
|
3243
|
+
});
|
|
3244
|
+
internals.errorRegistry.fire(conflict);
|
|
3245
|
+
return err(
|
|
3246
|
+
new RenderFeatureStageFailedError(feature.identity, -1, 'prepare', 'registration'),
|
|
3247
|
+
);
|
|
3248
|
+
}
|
|
3249
|
+
const fullscreenPrewarm = await prewarmFullscreenFeatureModules(feature);
|
|
3250
|
+
if (!fullscreenPrewarm.ok) {
|
|
3251
|
+
return err(
|
|
3252
|
+
new RenderFeatureStageFailedError(feature.identity, -1, 'prepare', 'registration'),
|
|
3253
|
+
);
|
|
3254
|
+
}
|
|
3255
|
+
for (const materialShaderId of feature.requiredMaterialShaders ?? []) {
|
|
3256
|
+
const lookup = getShader().findMaterialArtifact(materialShaderId);
|
|
3257
|
+
if (!lookup.ok) {
|
|
3258
|
+
const error = new RhiError({
|
|
3259
|
+
code: 'shader-compile-failed',
|
|
3260
|
+
expected: `declared render feature material shader '${materialShaderId}' is present in the loaded manifest`,
|
|
3261
|
+
hint: `add material shader '${materialShaderId}' to the shader manifest or remove it from the feature declaration`,
|
|
3262
|
+
});
|
|
3263
|
+
internals.errorRegistry.fire(error);
|
|
3264
|
+
return err(
|
|
3265
|
+
new RenderFeatureStageFailedError(feature.identity, -1, 'prepare', 'registration'),
|
|
3266
|
+
);
|
|
3267
|
+
}
|
|
3268
|
+
const label = `module-${materialShaderId}`;
|
|
3269
|
+
const moduleResult = internals.pack.createShaderModule
|
|
3270
|
+
? await internals.pack.createShaderModule(internals.device, {
|
|
3271
|
+
code: lookup.value.source,
|
|
3272
|
+
label,
|
|
3273
|
+
})
|
|
3274
|
+
: await invokeDeviceCreateShaderModule(internals.device, {
|
|
3275
|
+
code: lookup.value.source,
|
|
3276
|
+
label,
|
|
3277
|
+
});
|
|
3278
|
+
if (!moduleResult.ok) {
|
|
3279
|
+
internals.errorRegistry.fire(moduleResult.error);
|
|
3280
|
+
return err(
|
|
3281
|
+
new RenderFeatureStageFailedError(feature.identity, -1, 'prepare', 'registration'),
|
|
3282
|
+
);
|
|
3283
|
+
}
|
|
3284
|
+
getShaderModuleAdapter().seedModule(label, moduleResult.value);
|
|
3285
|
+
}
|
|
3286
|
+
const installed = host.install(feature);
|
|
3287
|
+
if (!installed.ok) return installed;
|
|
3288
|
+
if (installed.ok) {
|
|
3289
|
+
for (const materialShaderId of feature.requiredMaterialShaders ?? []) {
|
|
3290
|
+
requiredMaterialShaderSet.add(materialShaderId);
|
|
3291
|
+
}
|
|
3292
|
+
requiredMaterialShaders = Object.freeze([...requiredMaterialShaderSet]);
|
|
3293
|
+
requiredFullscreenPostProcesses = nextRequiredFullscreenPostProcesses;
|
|
3294
|
+
}
|
|
3295
|
+
return installed;
|
|
3296
|
+
},
|
|
3297
|
+
async uninstallRenderFeature(
|
|
3298
|
+
feature: RenderFeature<unknown>,
|
|
3299
|
+
): Promise<RenderResult<void, RenderError>> {
|
|
3300
|
+
const host = internals.featureHost;
|
|
3301
|
+
if (disposed || host === undefined) {
|
|
3302
|
+
return err(
|
|
3303
|
+
new RenderFeatureStageFailedError(feature.identity, -1, 'dispose', 'registration'),
|
|
3304
|
+
);
|
|
3305
|
+
}
|
|
3306
|
+
try {
|
|
3307
|
+
await internals.device.queue.onSubmittedWorkDone();
|
|
3308
|
+
} catch {
|
|
3309
|
+
return err(
|
|
3310
|
+
new RenderFeatureStageFailedError(feature.identity, -1, 'dispose', 'registration'),
|
|
3311
|
+
);
|
|
3312
|
+
}
|
|
3313
|
+
const removed = host.uninstall(feature);
|
|
3314
|
+
if (removed.ok) {
|
|
3315
|
+
requiredMaterialShaderSet.clear();
|
|
3316
|
+
for (const remaining of host.features) {
|
|
3317
|
+
for (const materialShaderId of remaining.requiredMaterialShaders ?? []) {
|
|
3318
|
+
requiredMaterialShaderSet.add(materialShaderId);
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
requiredMaterialShaders = Object.freeze([...requiredMaterialShaderSet]);
|
|
3322
|
+
requiredFullscreenPostProcesses = collectRequiredFullscreenPostProcesses(host.features);
|
|
3323
|
+
}
|
|
3324
|
+
return removed;
|
|
3325
|
+
},
|
|
3326
|
+
drawFrame(request: RenderFrameInput): RenderResult<FrameReceipt, RhiError | RenderError> {
|
|
3327
|
+
const result = this.draw(request);
|
|
3328
|
+
if (!result.ok) return result;
|
|
3329
|
+
return result.value === undefined
|
|
3330
|
+
? err(
|
|
3331
|
+
new RendererContractFailureError(
|
|
3332
|
+
'draw',
|
|
3333
|
+
'the lease-bound draw path must return a FrameReceipt after submit',
|
|
3334
|
+
),
|
|
3335
|
+
)
|
|
3336
|
+
: ok(result.value);
|
|
3337
|
+
},
|
|
3338
|
+
draw(
|
|
3339
|
+
worldsOrRequest: readonly World[] | RenderFrameInput,
|
|
3340
|
+
options?: DrawOwnerOptions,
|
|
3341
|
+
): Result<void | FrameReceipt, RhiError | RenderError> {
|
|
3342
|
+
const isFrameRequest = !Array.isArray(worldsOrRequest);
|
|
3343
|
+
const frameRequest = isFrameRequest ? (worldsOrRequest as RenderFrameInput) : undefined;
|
|
3344
|
+
const worlds: readonly World[] =
|
|
3345
|
+
frameRequest !== undefined
|
|
3346
|
+
? frameRequest.leases
|
|
3347
|
+
.map((lease) => attachedLeases.get(lease))
|
|
3348
|
+
.filter((world): world is World => world !== undefined)
|
|
3349
|
+
: (worldsOrRequest as readonly World[]);
|
|
3350
|
+
const readLeases: readonly RenderReadLease[] | undefined =
|
|
3351
|
+
frameRequest !== undefined
|
|
3352
|
+
? frameRequest.leases
|
|
3353
|
+
: worlds.map((world) => leasesByWorld.get(world)).every((lease) => lease !== undefined)
|
|
3354
|
+
? worlds.map((world) => leasesByWorld.get(world) as RenderReadLease)
|
|
3355
|
+
: undefined;
|
|
3356
|
+
if (frameRequest !== undefined && worlds.length !== frameRequest.leases.length) {
|
|
3357
|
+
return err(
|
|
3358
|
+
new RendererOperationError('world-lease-invalid', {
|
|
3359
|
+
operation: 'draw',
|
|
3360
|
+
cause: new RendererContractFailureError(
|
|
3361
|
+
'draw',
|
|
3362
|
+
'every RenderFrameInput lease must be attached to this Renderer',
|
|
3363
|
+
),
|
|
3364
|
+
}),
|
|
3365
|
+
);
|
|
3366
|
+
}
|
|
3367
|
+
const cameraOwner =
|
|
3368
|
+
frameRequest === undefined ? -1 : frameRequest.leases.indexOf(frameRequest.camera.lease);
|
|
3369
|
+
const resourceOwner =
|
|
3370
|
+
frameRequest === undefined
|
|
3371
|
+
? -1
|
|
3372
|
+
: frameRequest.leases.indexOf(frameRequest.environment.lease);
|
|
3373
|
+
if (frameRequest !== undefined && (cameraOwner < 0 || resourceOwner < 0)) {
|
|
3374
|
+
return err(
|
|
3375
|
+
new RendererOperationError('frame-input-invalid', {
|
|
3376
|
+
operation: 'draw',
|
|
3377
|
+
cause: new RendererContractFailureError(
|
|
3378
|
+
'draw',
|
|
3379
|
+
cameraOwner < 0
|
|
3380
|
+
? 'camera lease must be present in RenderFrameInput.leases'
|
|
3381
|
+
: 'environment lease must be present in RenderFrameInput.leases',
|
|
3382
|
+
),
|
|
3383
|
+
}),
|
|
3384
|
+
);
|
|
3385
|
+
}
|
|
3386
|
+
const drawOptions: DrawOwnerOptions =
|
|
3387
|
+
frameRequest !== undefined
|
|
3388
|
+
? {
|
|
3389
|
+
cameraOwner,
|
|
3390
|
+
resourceOwner,
|
|
3391
|
+
...(frameRequest.profileFrame === undefined
|
|
3392
|
+
? {}
|
|
3393
|
+
: { profileFrame: frameRequest.profileFrame }),
|
|
3394
|
+
}
|
|
3395
|
+
: (options ?? { cameraOwner: 0, resourceOwner: 0 });
|
|
3396
|
+
// feat-20260612-rhi-destroy-renderer-dispose-gpu-lifecycle / M5 / w21
|
|
3397
|
+
// (plan-strategy D-1, D-8): post-dispose the renderer is dead. AI
|
|
3398
|
+
// users observing `result.ok === false && err.code === 'rhi-not-
|
|
3399
|
+
// available'` know to rebuild the renderer (mirrors the "ready not
|
|
3400
|
+
// settled" + "pipelineState null" fail-fast paths below; reuses the
|
|
3401
|
+
// existing closed-union member, no new ErrorCode introduced).
|
|
3402
|
+
if (disposed) {
|
|
3403
|
+
const e = new RhiError({
|
|
3404
|
+
code: 'rhi-not-available',
|
|
3405
|
+
expected:
|
|
3406
|
+
'renderer not disposed before calling renderer.draw(worlds, { cameraOwner, resourceOwner })',
|
|
3407
|
+
hint: 'renderer.dispose() flipped the lifecycle latch; rebuild via createRenderer / Engine.create',
|
|
3408
|
+
});
|
|
3409
|
+
internals.errorRegistry.fire(e);
|
|
3410
|
+
return err(e);
|
|
3411
|
+
}
|
|
3412
|
+
if (surfaceReleased) {
|
|
3413
|
+
return err(
|
|
3414
|
+
new RhiError({
|
|
3415
|
+
code: 'rhi-not-available',
|
|
3416
|
+
expected: 'renderer surface restored before drawing',
|
|
3417
|
+
hint: 'call renderer.restoreSurface() after the temporary surface owner stops',
|
|
3418
|
+
}),
|
|
3419
|
+
);
|
|
3420
|
+
}
|
|
3421
|
+
// M2 / w9 (A-IN-5): device-lost guard — draw() silently returns err
|
|
3422
|
+
// without firing onError each frame. The device-lost channel fires once
|
|
3423
|
+
// through the dual-channel fan-out (:750-797); draw() does not repeat it
|
|
3424
|
+
// (canvas holds previous frame). Host observes health().reason ===
|
|
3425
|
+
// 'device-lost' and calls recover() when ready.
|
|
3426
|
+
if (internals.healthRegistry.getLastSnapshot().reason === 'device-lost') {
|
|
3427
|
+
return err(
|
|
3428
|
+
new RhiError({
|
|
3429
|
+
code: 'rhi-not-available',
|
|
3430
|
+
expected: 'GPUDevice is lost; recover() to rebuild and resume rendering',
|
|
3431
|
+
hint: 'call renderer.recover() after a host-chosen delay; camera holds previous frame',
|
|
3432
|
+
}),
|
|
3433
|
+
);
|
|
3434
|
+
}
|
|
3435
|
+
// D-S4: ready not settled => fire onError + skip frame. Uses
|
|
3436
|
+
// 'rhi-not-available' (closed union placeholder semantics; charter
|
|
3437
|
+
// proposition 4 explicit failure - AI users observe through onError
|
|
3438
|
+
// and decide whether to retry).
|
|
3439
|
+
if (!readySettled) {
|
|
3440
|
+
const e = new RhiError({
|
|
3441
|
+
code: 'rhi-not-available',
|
|
3442
|
+
expected:
|
|
3443
|
+
'await renderer.initialization before calling renderer.draw(worlds, { cameraOwner, resourceOwner })',
|
|
3444
|
+
hint: 'await renderer.initialization resolves once the manifest / pipeline / asset upload chain completes',
|
|
3445
|
+
});
|
|
3446
|
+
internals.errorRegistry.fire(e);
|
|
3447
|
+
return err(e);
|
|
3448
|
+
}
|
|
3449
|
+
// pipeline build rejected: ready Promise has already surfaced the
|
|
3450
|
+
// structured error to AI users through `await renderer.initialization`. Skip
|
|
3451
|
+
// to keep draw(world) idempotent; a transient retry next frame is
|
|
3452
|
+
// the responsibility of the AI user (charter proposition 9).
|
|
3453
|
+
if (pipelineState === null) {
|
|
3454
|
+
const e = new RhiError({
|
|
3455
|
+
code: 'rhi-not-available',
|
|
3456
|
+
expected: 'pipelineState built during Renderer.initialization',
|
|
3457
|
+
hint: 'await renderer.initialization resolved successfully; rebuild renderer or fix the upstream RhiError',
|
|
3458
|
+
});
|
|
3459
|
+
internals.errorRegistry.fire(e);
|
|
3460
|
+
return err(e);
|
|
3461
|
+
}
|
|
3462
|
+
// feat-20260708-composited-multi-world-rendering M3 / D-5: draw-args
|
|
3463
|
+
// entry validation runs before each extract or context configuration.
|
|
3464
|
+
// Empty worlds / owner out of range returns a structured Result.err
|
|
3465
|
+
// (never silent, charter P3) without touching GPU state. The two codes
|
|
3466
|
+
// are non-exclusive: an empty array short-circuits to empty-worlds. The
|
|
3467
|
+
// check is defensive against JS callers passing a non-array despite the
|
|
3468
|
+
// compile-time World[] type (red-window migration safety).
|
|
3469
|
+
const worldCount = Array.isArray(worlds) ? worlds.length : 0;
|
|
3470
|
+
if (worldCount === 0 && internals.device.caps.backendKind === 'null') {
|
|
3471
|
+
return ok(undefined);
|
|
3472
|
+
}
|
|
3473
|
+
// Validate both owner indices (cameraOwner before resourceOwner, first
|
|
3474
|
+
// offender wins). The empty object only protects the JS boundary; it is
|
|
3475
|
+
// rejected by the same validation and never becomes another draw shape.
|
|
3476
|
+
const drawOwners = drawOptions;
|
|
3477
|
+
const argsCheck = validateDrawArgs(worldCount, drawOwners);
|
|
3478
|
+
if (!argsCheck.ok) {
|
|
3479
|
+
internals.errorRegistry.fire(argsCheck.error);
|
|
3480
|
+
return argsCheck;
|
|
3481
|
+
}
|
|
3482
|
+
// Configure context lazily on first draw (D-S1 single-point
|
|
3483
|
+
// exemption): GPUCanvasContext.configure({device}) needs a raw
|
|
3484
|
+
// The canvas context is configured through the RHI device scope.
|
|
3485
|
+
const contextConfigured = ensureContextConfigured(
|
|
3486
|
+
internals,
|
|
3487
|
+
pipelineState,
|
|
3488
|
+
internals.errorRegistry,
|
|
3489
|
+
);
|
|
3490
|
+
if (!contextConfigured.ok) return contextConfigured;
|
|
3491
|
+
renderTargetHost.beginFrame();
|
|
3492
|
+
// w24 — facade-level try/catch produces Result.err on unexpected throw
|
|
3493
|
+
// (D-P6 dual-channel preserved: per-stage RhiError continues to fan out
|
|
3494
|
+
// through onError separately; the facade Result is the synchronous
|
|
3495
|
+
// summary AI users can ignore or branch on).
|
|
3496
|
+
try {
|
|
3497
|
+
const timingHost = internals as WebGPURendererInternals & {
|
|
3498
|
+
gpuPassTimingFrameIdentity?:
|
|
3499
|
+
| {
|
|
3500
|
+
readonly frameId: number;
|
|
3501
|
+
readonly deviceGeneration: number;
|
|
3502
|
+
readonly graphGeneration: number;
|
|
3503
|
+
}
|
|
3504
|
+
| undefined;
|
|
3505
|
+
gpuPassTimingCapture?: GpuPassTimingCapture | undefined;
|
|
3506
|
+
gpuPassTimingSubmittedWork?: Promise<void> | undefined;
|
|
3507
|
+
gpuPassTimingBeginReason?: GpuPassTimingReason | undefined;
|
|
3508
|
+
};
|
|
3509
|
+
const timingRequested =
|
|
3510
|
+
isFrameRequest &&
|
|
3511
|
+
(frameRequest?.profileFrame !== undefined ||
|
|
3512
|
+
internals.options?.captureGpuTimings === true);
|
|
3513
|
+
let volumeTimingCapture: GpuTimingCapture | undefined;
|
|
3514
|
+
const volumeTimingUnavailableReason = timingRequested
|
|
3515
|
+
? 'timestamp capture is unavailable on this device'
|
|
3516
|
+
: 'timestamp capture was not requested for this frame';
|
|
3517
|
+
if (timingRequested && gpuPassTimingOptions === undefined) {
|
|
3518
|
+
const createdTiming = GpuTimingCapture.create(internals.device);
|
|
3519
|
+
if (createdTiming.ok) volumeTimingCapture = createdTiming.value;
|
|
3520
|
+
}
|
|
3521
|
+
if (isFrameRequest) {
|
|
3522
|
+
timingHost.gpuPassTimingFrameIdentity = {
|
|
3523
|
+
frameId: frameId + 1,
|
|
3524
|
+
deviceGeneration: activeDeviceScope.generation,
|
|
3525
|
+
graphGeneration: 0,
|
|
3526
|
+
};
|
|
3527
|
+
}
|
|
3528
|
+
const submitted = renderSystem.draw(worlds, drawOptions, readLeases, volumeTimingCapture);
|
|
3529
|
+
timingHost.gpuPassTimingFrameIdentity = undefined;
|
|
3530
|
+
if (!submitted) {
|
|
3531
|
+
volumeTimingCapture?.discard();
|
|
3532
|
+
return err(
|
|
3533
|
+
new RendererContractFailureError(
|
|
3534
|
+
'draw',
|
|
3535
|
+
'the Standard render owner did not submit a command buffer; inspect Renderer error events and recover the owning frame path',
|
|
3536
|
+
),
|
|
3537
|
+
);
|
|
3538
|
+
}
|
|
3539
|
+
const timingCompletion = timingHost.gpuPassTimingSubmittedWork;
|
|
3540
|
+
timingHost.gpuPassTimingSubmittedWork = undefined;
|
|
3541
|
+
const reflectionFallbackCompletion = renderSystem.reflectionFallbackCompletion;
|
|
3542
|
+
internals.pack.instrumentation?.onFrameBoundary?.();
|
|
3543
|
+
if (!isFrameRequest) {
|
|
3544
|
+
renderTargetHost.onFrameSubmitted();
|
|
3545
|
+
return ok(undefined);
|
|
3546
|
+
}
|
|
3547
|
+
const receiptFrameId = ++frameId;
|
|
3548
|
+
const receiptGeneration = activeDeviceScope.generation;
|
|
3549
|
+
const queueCompletion = timingCompletion ?? internals.device.queue.onSubmittedWorkDone();
|
|
3550
|
+
const completion: FrameReceipt['completed'] = Promise.all([
|
|
3551
|
+
queueCompletion,
|
|
3552
|
+
reflectionFallbackCompletion ?? Promise.resolve(),
|
|
3553
|
+
])
|
|
3554
|
+
.then(() => ok(undefined))
|
|
3555
|
+
.catch((cause: unknown) =>
|
|
3556
|
+
err(
|
|
3557
|
+
new RendererOperationError('device-operation-failed', {
|
|
3558
|
+
operation: 'complete-frame',
|
|
3559
|
+
frameId: receiptFrameId,
|
|
3560
|
+
deviceGeneration: receiptGeneration,
|
|
3561
|
+
cause: structuredRendererCause(cause, 'complete-frame'),
|
|
3562
|
+
}),
|
|
3563
|
+
),
|
|
3564
|
+
);
|
|
3565
|
+
const continuation = createContinuationTerminator();
|
|
3566
|
+
frameContinuations.add(continuation);
|
|
3567
|
+
const guardedCompletion = completion.then(
|
|
3568
|
+
() =>
|
|
3569
|
+
continuation.guard('queue-completion')
|
|
3570
|
+
? ok(undefined)
|
|
3571
|
+
: err(
|
|
3572
|
+
new RendererOperationError('device-operation-failed', {
|
|
3573
|
+
operation: 'complete-frame',
|
|
3574
|
+
frameId: receiptFrameId,
|
|
3575
|
+
deviceGeneration: receiptGeneration,
|
|
3576
|
+
cause: structuredRendererCause(
|
|
3577
|
+
{
|
|
3578
|
+
code: 'stale-generation',
|
|
3579
|
+
expected: 'frame completion belongs to its device generation',
|
|
3580
|
+
hint: 'discard the stale receipt',
|
|
3581
|
+
},
|
|
3582
|
+
'complete-frame',
|
|
3583
|
+
),
|
|
3584
|
+
}),
|
|
3585
|
+
),
|
|
3586
|
+
(cause: unknown) =>
|
|
3587
|
+
err(
|
|
3588
|
+
new RendererOperationError('device-operation-failed', {
|
|
3589
|
+
operation: 'complete-frame',
|
|
3590
|
+
frameId: receiptFrameId,
|
|
3591
|
+
deviceGeneration: receiptGeneration,
|
|
3592
|
+
cause: structuredRendererCause(cause, 'complete-frame'),
|
|
3593
|
+
}),
|
|
3594
|
+
),
|
|
3595
|
+
);
|
|
3596
|
+
const terminated = continuation.promise().then((reason) =>
|
|
3597
|
+
err(
|
|
3598
|
+
new RendererOperationError('device-operation-failed', {
|
|
3599
|
+
operation: 'complete-frame',
|
|
3600
|
+
frameId: receiptFrameId,
|
|
3601
|
+
deviceGeneration: receiptGeneration,
|
|
3602
|
+
cause: structuredRendererCause(
|
|
3603
|
+
{
|
|
3604
|
+
code: reason.code,
|
|
3605
|
+
expected: 'a frame continuation completes before device loss or disposal',
|
|
3606
|
+
hint: 'discard the stale receipt and inspect the current renderer generation',
|
|
3607
|
+
detail: reason,
|
|
3608
|
+
},
|
|
3609
|
+
'complete-frame',
|
|
3610
|
+
),
|
|
3611
|
+
}),
|
|
3612
|
+
),
|
|
3613
|
+
);
|
|
3614
|
+
const completed: FrameReceipt['completed'] = Promise.race([
|
|
3615
|
+
guardedCompletion,
|
|
3616
|
+
terminated,
|
|
3617
|
+
]).finally(() => {
|
|
3618
|
+
frameContinuations.delete(continuation);
|
|
3619
|
+
});
|
|
3620
|
+
const receipt = Object.freeze({
|
|
3621
|
+
frameId: receiptFrameId,
|
|
3622
|
+
deviceGeneration: receiptGeneration,
|
|
3623
|
+
completed,
|
|
3624
|
+
});
|
|
3625
|
+
renderTargetHost.onFrameSubmitted(completed);
|
|
3626
|
+
latestReceipt = receipt;
|
|
3627
|
+
const passTimingCapture = timingHost.gpuPassTimingCapture;
|
|
3628
|
+
const timingBeginReason = timingHost.gpuPassTimingBeginReason;
|
|
3629
|
+
const timingSource =
|
|
3630
|
+
passTimingCapture === undefined
|
|
3631
|
+
? gpuPassTimingUnavailable === undefined && timingBeginReason === undefined
|
|
3632
|
+
? undefined
|
|
3633
|
+
: async (): Promise<GpuPassTimingObservation> =>
|
|
3634
|
+
gpuPassTimingUnavailable ??
|
|
3635
|
+
failedGpuPassTimingObservation(timingBeginReason as GpuPassTimingReason)
|
|
3636
|
+
: async (): Promise<GpuPassTimingObservation> => {
|
|
3637
|
+
const observed = await passTimingCapture.observe();
|
|
3638
|
+
if (!observed.ok) return failedGpuPassTimingObservation(observed.error);
|
|
3639
|
+
const frame = observed.value;
|
|
3640
|
+
const unmeasured = frame.passes.find((pass) => pass.status === 'unmeasured');
|
|
3641
|
+
if (unmeasured !== undefined) {
|
|
3642
|
+
return { status: 'partial', frame, reason: unmeasured.reason };
|
|
3643
|
+
}
|
|
3644
|
+
if (frame.droppedPassCount > 0) {
|
|
3645
|
+
return {
|
|
3646
|
+
status: 'partial',
|
|
3647
|
+
frame,
|
|
3648
|
+
reason: {
|
|
3649
|
+
code: 'query-budget-exceeded',
|
|
3650
|
+
expected: 'all executed passes fit within the bounded query budget',
|
|
3651
|
+
hint: 'increase maxPassesPerFrame or simplify the pass graph',
|
|
3652
|
+
detail: { droppedPassCount: frame.droppedPassCount },
|
|
3653
|
+
},
|
|
3654
|
+
};
|
|
3655
|
+
}
|
|
3656
|
+
return { status: 'complete', frame };
|
|
3657
|
+
};
|
|
3658
|
+
timingObservationStore?.register(receipt, timingSource);
|
|
3659
|
+
currentReceipts.add(receipt);
|
|
3660
|
+
if (timingRequested && gpuPassTimingOptions === undefined) {
|
|
3661
|
+
receiptTimings.set(
|
|
3662
|
+
receipt,
|
|
3663
|
+
volumeTimingCapture?.observation() ??
|
|
3664
|
+
Promise.resolve({
|
|
3665
|
+
status: 'unavailable',
|
|
3666
|
+
reason: volumeTimingUnavailableReason,
|
|
3667
|
+
}),
|
|
3668
|
+
);
|
|
3669
|
+
}
|
|
3670
|
+
issuedReceipts.add(receipt);
|
|
3671
|
+
return ok(receipt);
|
|
3672
|
+
} catch (cause) {
|
|
3673
|
+
const error =
|
|
3674
|
+
cause instanceof Error
|
|
3675
|
+
? { code: 'unknown' as const, message: cause.message, name: cause.name }
|
|
3676
|
+
: { code: 'unknown' as const, message: String(cause) };
|
|
3677
|
+
const e = new RhiError({
|
|
3678
|
+
code: 'webgpu-runtime-error',
|
|
3679
|
+
expected:
|
|
3680
|
+
'renderSystem.draw(worlds, { cameraOwner, resourceOwner }) completes without throwing',
|
|
3681
|
+
hint: `RenderSystem internal error: ${error.message}`,
|
|
3682
|
+
detail: { error },
|
|
3683
|
+
});
|
|
3684
|
+
internals.errorRegistry.fire(e);
|
|
3685
|
+
return err(e);
|
|
3686
|
+
}
|
|
3687
|
+
},
|
|
3688
|
+
async observe(
|
|
3689
|
+
receipt: FrameReceipt,
|
|
3690
|
+
request: FrameObservationRequest,
|
|
3691
|
+
): Promise<RenderResult<FrameReceiptObservation, RenderError>> {
|
|
3692
|
+
if (gpuPassTimingOptions !== undefined) {
|
|
3693
|
+
return timingObservationStore === undefined
|
|
3694
|
+
? observeGpuPassTimingDisabled(receipt, request, () => activeDeviceScope.generation)
|
|
3695
|
+
: timingObservationStore.observe(receipt, request);
|
|
3696
|
+
}
|
|
3697
|
+
if (
|
|
3698
|
+
!currentReceipts.has(receipt) ||
|
|
3699
|
+
!issuedReceipts.has(receipt) ||
|
|
3700
|
+
receipt.deviceGeneration !== activeDeviceScope.generation
|
|
3701
|
+
) {
|
|
3702
|
+
const currentGeneration = activeDeviceScope.generation;
|
|
3703
|
+
return err(
|
|
3704
|
+
new FrameReceiptStaleError({
|
|
3705
|
+
frameId: receipt.frameId,
|
|
3706
|
+
receiptGeneration: receipt.deviceGeneration,
|
|
3707
|
+
currentGeneration,
|
|
3708
|
+
}),
|
|
3709
|
+
);
|
|
3710
|
+
}
|
|
3711
|
+
const completed = await receipt.completed;
|
|
3712
|
+
if (!completed.ok) return completed;
|
|
3713
|
+
const currentGeneration = activeDeviceScope.generation;
|
|
3714
|
+
if (receipt.deviceGeneration !== currentGeneration) {
|
|
3715
|
+
return err(
|
|
3716
|
+
new FrameReceiptStaleError({
|
|
3717
|
+
frameId: receipt.frameId,
|
|
3718
|
+
receiptGeneration: receipt.deviceGeneration,
|
|
3719
|
+
currentGeneration,
|
|
3720
|
+
}),
|
|
3721
|
+
);
|
|
3722
|
+
}
|
|
3723
|
+
await renderSystem.observeLodOcclusion();
|
|
3724
|
+
const targetReadbacks =
|
|
3725
|
+
request.targetReadbacks === undefined || request.targetReadbacks.length === 0
|
|
3726
|
+
? undefined
|
|
3727
|
+
: await renderTargetHost.observeTargetReadbacks(receipt, request.targetReadbacks);
|
|
3728
|
+
if (targetReadbacks !== undefined && !targetReadbacks.ok) return targetReadbacks;
|
|
3729
|
+
const volumeTimings = request.include.includes('timings')
|
|
3730
|
+
? await (receiptTimings.get(receipt) ??
|
|
3731
|
+
Promise.resolve<VolumeTimingObservation>({
|
|
3732
|
+
status: 'unavailable',
|
|
3733
|
+
reason: 'timestamp capture was not requested for this frame',
|
|
3734
|
+
}))
|
|
3735
|
+
: undefined;
|
|
3736
|
+
return ok(
|
|
3737
|
+
Object.freeze({
|
|
3738
|
+
frameId: receipt.frameId,
|
|
3739
|
+
deviceGeneration: receipt.deviceGeneration,
|
|
3740
|
+
include: Object.freeze([...request.include]),
|
|
3741
|
+
...(targetReadbacks === undefined
|
|
3742
|
+
? {}
|
|
3743
|
+
: { targetReadbacks: Object.freeze(targetReadbacks.value) }),
|
|
3744
|
+
...(volumeTimings === undefined ? {} : { volumeTimings }),
|
|
3745
|
+
}),
|
|
3746
|
+
);
|
|
3747
|
+
},
|
|
3748
|
+
releaseSurface(): Result<void, RhiError> {
|
|
3749
|
+
if (surfaceReleased) return ok(undefined);
|
|
3750
|
+
if (disposed) {
|
|
3751
|
+
return err(
|
|
3752
|
+
new RhiError({
|
|
3753
|
+
code: 'rhi-not-available',
|
|
3754
|
+
expected: 'live renderer before releasing its surface',
|
|
3755
|
+
hint: 'create a new Renderer; disposed renderers are terminal',
|
|
3756
|
+
}),
|
|
3757
|
+
);
|
|
3758
|
+
}
|
|
3759
|
+
try {
|
|
3760
|
+
internals.context.unconfigure();
|
|
3761
|
+
if (pipelineState !== null) pipelineState.perPassResources.configured = false;
|
|
3762
|
+
surfaceReleased = true;
|
|
3763
|
+
return ok(undefined);
|
|
3764
|
+
} catch (cause) {
|
|
3765
|
+
return err(wrapDisposeError(cause, 'context.unconfigure'));
|
|
3766
|
+
}
|
|
3767
|
+
},
|
|
3768
|
+
restoreSurface(): Result<void, RhiError> {
|
|
3769
|
+
if (!surfaceReleased) return ok(undefined);
|
|
3770
|
+
if (disposed) {
|
|
3771
|
+
return err(
|
|
3772
|
+
new RhiError({
|
|
3773
|
+
code: 'rhi-not-available',
|
|
3774
|
+
expected: 'live renderer before restoring its surface',
|
|
3775
|
+
hint: 'create a new Renderer; disposed renderers are terminal',
|
|
3776
|
+
}),
|
|
3777
|
+
);
|
|
3778
|
+
}
|
|
3779
|
+
surfaceReleased = false;
|
|
3780
|
+
return ok(undefined);
|
|
3781
|
+
},
|
|
3782
|
+
/**
|
|
3783
|
+
* Release every GPU resource the renderer owns + detach the listener
|
|
3784
|
+
* registries; flip the `disposed` latch so subsequent `draw(world)`
|
|
3785
|
+
* calls fail-fast with `'rhi-not-available'`.
|
|
3786
|
+
*
|
|
3787
|
+
* feat-20260612-rhi-destroy-renderer-dispose-gpu-lifecycle / M5 / w21
|
|
3788
|
+
* 6-step cascade (plan-strategy D-2 ordering):
|
|
3789
|
+
* 1. `context.unconfigure()` -- release the current surface image
|
|
3790
|
+
* 2. `gpuStore.destroyAll()` -- texture / cubemap / mesh maps
|
|
3791
|
+
* 3. `renderSystem.disposeFrameState()` -- graph.drain() + instanceBuffers
|
|
3792
|
+
* 4. `featureHost.dispose()` -- feature resources + lifecycle
|
|
3793
|
+
* 5. DeviceScope retirement -- generation-keyed IBL state becomes stale
|
|
3794
|
+
* 6. `lostRegistry.clear() / errorRegistry.clear()`
|
|
3795
|
+
*
|
|
3796
|
+
* Each step runs inside its own try/catch (D-3 method A): a sub-step
|
|
3797
|
+
* failure DOES NOT halt the cascade; the structured RhiError (or wrapped
|
|
3798
|
+
* runtime exception) fans out through `errorRegistry.fire` so AI users
|
|
3799
|
+
* observing Renderer error events see every dispose-time fault. The
|
|
3800
|
+
* `disposed` latch flips up-front so a re-entrant dispose (or a draw
|
|
3801
|
+
* that races with the cascade) short-circuits.
|
|
3802
|
+
*
|
|
3803
|
+
* NOT calling `device.destroy()` on the raw GPUDevice (w25 lesson from
|
|
3804
|
+
* feat-20260517-vite-plugin-image-build-time-cook CI-fix v4): w23
|
|
3805
|
+
* attempted to evict stale devices from the chromium WebGPU adapter
|
|
3806
|
+
* pool via `_internal_getRawDevice`, but the explicit destroy
|
|
3807
|
+
* accelerated pool poisoning -- after the first `dispose()` the
|
|
3808
|
+
* chromium adapter pool started recycling the just-destroyed device
|
|
3809
|
+
* into the next `requestDevice` so the NEXT test's `Engine.create`
|
|
3810
|
+
* resolved with `ready.error.code: 'device-lost'`. Cross-test isolation
|
|
3811
|
+
* moved to the vitest infra layer (`browser.isolate: true`) instead of
|
|
3812
|
+
* test code self-managing device lifecycle.
|
|
3813
|
+
*
|
|
3814
|
+
* Stays in sync with the `Renderer.dispose` row of the README "API index".
|
|
3815
|
+
*/
|
|
3816
|
+
dispose(): RenderResult<void, RenderError> {
|
|
3817
|
+
if (disposed) return ok(undefined);
|
|
3818
|
+
disposed = true;
|
|
3819
|
+
for (const continuation of frameContinuations) {
|
|
3820
|
+
continuation.terminate({ code: 'disposed' });
|
|
3821
|
+
}
|
|
3822
|
+
currentReceipts.clear();
|
|
3823
|
+
const cleanupFailures: RendererOperationCause[] = [];
|
|
3824
|
+
for (const world of attachedWorlds) {
|
|
3825
|
+
try {
|
|
3826
|
+
const derivedSystemName = derivedSystemNames.get(world);
|
|
3827
|
+
if (derivedSystemName !== undefined) {
|
|
3828
|
+
world.removeSystem(Update, derivedSystemName);
|
|
3829
|
+
derivedSystemNames.delete(world);
|
|
3830
|
+
}
|
|
3831
|
+
transformReleases.get(world)?.();
|
|
3832
|
+
transformReleases.delete(world);
|
|
3833
|
+
} catch (cause) {
|
|
3834
|
+
const error = wrapDisposeError(cause, 'world.removeSystem(renderDerived)');
|
|
3835
|
+
cleanupFailures.push(error);
|
|
3836
|
+
internals.errorRegistry.fire(error);
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
attachedWorlds.clear();
|
|
3840
|
+
for (const lease of attachedLeases.keys()) {
|
|
3841
|
+
try {
|
|
3842
|
+
lease.dispose();
|
|
3843
|
+
} catch (cause) {
|
|
3844
|
+
const error = wrapDisposeError(cause, 'read-lease.dispose');
|
|
3845
|
+
cleanupFailures.push(error);
|
|
3846
|
+
internals.errorRegistry.fire(error);
|
|
3847
|
+
}
|
|
3848
|
+
}
|
|
3849
|
+
attachedLeases.clear();
|
|
3850
|
+
leasesByWorld.clear();
|
|
3851
|
+
// Release the current swap-chain image before destroying a resource
|
|
3852
|
+
// wrappers that may share its underlying device allocation. The wgpu
|
|
3853
|
+
// WebGL2 surface owns an explicit SurfaceTexture; leaving this until
|
|
3854
|
+
// after the resource sweep lets wasm drop it against a dead Surface.
|
|
3855
|
+
if (!surfaceReleased) {
|
|
3856
|
+
try {
|
|
3857
|
+
internals.context.unconfigure();
|
|
3858
|
+
} catch (cause) {
|
|
3859
|
+
const error = wrapDisposeError(cause, 'context.unconfigure');
|
|
3860
|
+
cleanupFailures.push(error);
|
|
3861
|
+
internals.errorRegistry.fire(error);
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
try {
|
|
3865
|
+
renderTargetHost.dispose();
|
|
3866
|
+
} catch (cause) {
|
|
3867
|
+
const error = wrapDisposeError(cause, 'renderTargetHost.dispose');
|
|
3868
|
+
cleanupFailures.push(error);
|
|
3869
|
+
internals.errorRegistry.fire(error);
|
|
3870
|
+
}
|
|
3871
|
+
// Step 2: release every Buffer / Texture handle owned by the runtime
|
|
3872
|
+
// GPU residency layer (feat-20260601-device/gpu-residency-extraction).
|
|
3873
|
+
try {
|
|
3874
|
+
gpuStore.destroyAll();
|
|
3875
|
+
} catch (cause) {
|
|
3876
|
+
const error = wrapDisposeError(cause, 'gpuStore.destroyAll');
|
|
3877
|
+
cleanupFailures.push(error);
|
|
3878
|
+
internals.errorRegistry.fire(error);
|
|
3879
|
+
}
|
|
3880
|
+
// Step 3: drain the per-frame render-graph pool + the per-entity
|
|
3881
|
+
// instanceBuffers GPU storage cache. Both walks live on the
|
|
3882
|
+
// RenderSystem closure (frameState is closure-private).
|
|
3883
|
+
try {
|
|
3884
|
+
renderSystem.disposeFrameState();
|
|
3885
|
+
} catch (cause) {
|
|
3886
|
+
const error = wrapDisposeError(cause, 'renderSystem.disposeFrameState');
|
|
3887
|
+
cleanupFailures.push(error);
|
|
3888
|
+
internals.errorRegistry.fire(error);
|
|
3889
|
+
}
|
|
3890
|
+
try {
|
|
3891
|
+
renderSystem.releaseProfilerCatalog();
|
|
3892
|
+
} catch (cause) {
|
|
3893
|
+
const error = wrapDisposeError(cause, 'renderSystem.releaseProfilerCatalog');
|
|
3894
|
+
cleanupFailures.push(error);
|
|
3895
|
+
internals.errorRegistry.fire(error);
|
|
3896
|
+
}
|
|
3897
|
+
// Step 4: release feature-owned resources and invoke feature disposal
|
|
3898
|
+
// hooks after render-graph state has been drained. The host is already
|
|
3899
|
+
// idempotent, and its structured cleanup detail is preserved by the
|
|
3900
|
+
// error registry when a feature cleanup fails.
|
|
3901
|
+
try {
|
|
3902
|
+
const featureDispose = internals.featureHost?.dispose();
|
|
3903
|
+
if (featureDispose !== undefined && !featureDispose.ok) {
|
|
3904
|
+
cleanupFailures.push(featureDispose.error);
|
|
3905
|
+
internals.errorRegistry.fire(featureDispose.error);
|
|
3906
|
+
}
|
|
3907
|
+
} catch (cause) {
|
|
3908
|
+
const error = wrapDisposeError(cause, 'featureHost.dispose');
|
|
3909
|
+
cleanupFailures.push(error);
|
|
3910
|
+
internals.errorRegistry.fire(error);
|
|
3911
|
+
}
|
|
3912
|
+
// Retire assembly-owned device resources only after the surface and
|
|
3913
|
+
// renderer-owned GPU pools have been released. The wgpu WebGL2 surface
|
|
3914
|
+
// still needs a live device while `unconfigure()` presents its pending
|
|
3915
|
+
// image; disposing the scope first lets wasm finalizers race that
|
|
3916
|
+
// presentation and can surface a parking_lot panic in WebKit.
|
|
3917
|
+
try {
|
|
3918
|
+
activeDeviceScope.dispose();
|
|
3919
|
+
} catch (cause) {
|
|
3920
|
+
const error = wrapDisposeError(cause, 'activeDeviceScope.dispose');
|
|
3921
|
+
cleanupFailures.push(error);
|
|
3922
|
+
internals.errorRegistry.fire(error);
|
|
3923
|
+
}
|
|
3924
|
+
// Step 5: the IBL cache is keyed by the retired DeviceScope generation.
|
|
3925
|
+
// It is no longer reachable through the active renderer scope, while
|
|
3926
|
+
// the device teardown owns the child GPU handles.
|
|
3927
|
+
// Step 6: detach the listener registries so a post-dispose error
|
|
3928
|
+
// event (race with the spec layer) does not fan out to user-supplied
|
|
3929
|
+
// listeners (charter P3 explicit failure: post-dispose the renderer
|
|
3930
|
+
// is dead, no observable side-effects). Performed last so steps 1-5
|
|
3931
|
+
// can still surface failures through `errorRegistry.fire`.
|
|
3932
|
+
try {
|
|
3933
|
+
internals.lostRegistry.clear();
|
|
3934
|
+
internals.errorRegistry.clear();
|
|
3935
|
+
} catch (cause) {
|
|
3936
|
+
cleanupFailures.push(structuredRendererCause(cause, 'listenerRegistry.clear'));
|
|
3937
|
+
}
|
|
3938
|
+
return cleanupFailures.length === 0
|
|
3939
|
+
? ok(undefined)
|
|
3940
|
+
: err(
|
|
3941
|
+
new RendererOperationError('cleanup-failed', {
|
|
3942
|
+
operation: 'dispose',
|
|
3943
|
+
causes: Object.freeze(cleanupFailures),
|
|
3944
|
+
}),
|
|
3945
|
+
);
|
|
3946
|
+
},
|
|
3947
|
+
onError(listener: RendererErrorListener): () => void {
|
|
3948
|
+
return internals.errorRegistry.add(listener);
|
|
3949
|
+
},
|
|
3950
|
+
subscribeHostEvents(listener) {
|
|
3951
|
+
const offError = internals.errorRegistry.add((error) => {
|
|
3952
|
+
listener(Object.freeze({ kind: 'error', error }));
|
|
3953
|
+
});
|
|
3954
|
+
const offHealth = internals.healthRegistry.add((health) => {
|
|
3955
|
+
listener(Object.freeze({ kind: 'health', health }));
|
|
3956
|
+
});
|
|
3957
|
+
let subscribed = true;
|
|
3958
|
+
return () => {
|
|
3959
|
+
if (!subscribed) return;
|
|
3960
|
+
subscribed = false;
|
|
3961
|
+
offError();
|
|
3962
|
+
offHealth();
|
|
3963
|
+
};
|
|
3964
|
+
},
|
|
3965
|
+
onLost(listener: RendererLostListener): () => void {
|
|
3966
|
+
return internals.lostRegistry.add(listener);
|
|
3967
|
+
},
|
|
3968
|
+
health(): HealthSnapshot {
|
|
3969
|
+
return internals.healthRegistry.getLastSnapshot();
|
|
3970
|
+
},
|
|
3971
|
+
recover(): Promise<Result<void, RecoverFailure>> {
|
|
3972
|
+
return recoveryFlight.run();
|
|
3973
|
+
},
|
|
3974
|
+
};
|
|
3975
|
+
/**
|
|
3976
|
+
* Publish a fully prepared generation and retire the previous owner set.
|
|
3977
|
+
*
|
|
3978
|
+
* This is deliberately synchronous: all candidate state is complete before
|
|
3979
|
+
* entering the function, and no callback or await is allowed between the
|
|
3980
|
+
* active-reference swap and the old-generation retirement. The candidate
|
|
3981
|
+
* carries its own shader adapters, pipeline caches, stores, hooks, and
|
|
3982
|
+
* post-process handles; a failed build never reaches this boundary.
|
|
3983
|
+
*/
|
|
3984
|
+
const publishAndRetireRendererGeneration = (
|
|
3985
|
+
candidate: RendererGeneration,
|
|
3986
|
+
candidatePostProcessResources: RecoveryPostProcessResources,
|
|
3987
|
+
recoveryRootBundle: RecoveryRootBundle,
|
|
3988
|
+
recoveryGraphCandidate: RecoveryGraphCandidate | undefined,
|
|
3989
|
+
recoveryColdWorkGuard: RecoveryColdWorkGuard,
|
|
3990
|
+
): void => {
|
|
3991
|
+
if (!candidate.scope.isAlive()) {
|
|
3992
|
+
throw new Error('Renderer generation candidate is not publishable.');
|
|
3993
|
+
}
|
|
3994
|
+
const previous = generationPublication.current;
|
|
3995
|
+
const previousBindings = previous?.producerBindings;
|
|
3996
|
+
const previousGpuStore = previousBindings?.gpuStore ?? gpuStore;
|
|
3997
|
+
const previousDynamicTextureStore =
|
|
3998
|
+
previousBindings?.dynamicTextureStore ?? dynamicTextureStore;
|
|
3999
|
+
const previousScope = previous?.scope ?? activeDeviceScope;
|
|
4000
|
+
// This is the first line of the synchronous publication boundary. The
|
|
4001
|
+
// candidate was already compiled and its setup work completed; now shed
|
|
4002
|
+
// lost-device RenderSystem owners before installing the candidate state.
|
|
4003
|
+
renderSystem.resetForRecover(previous?.pipeline, candidate.device);
|
|
4004
|
+
// Candidate readiness is the first point where the target owner may shed
|
|
4005
|
+
// old physical handles. Keep its device getter pinned to the old device,
|
|
4006
|
+
// but advance its generation so active targets enter their rebuild state;
|
|
4007
|
+
// this call is inside the same synchronous publication boundary.
|
|
4008
|
+
renderTargetGeneration = candidate.scope.generation;
|
|
4009
|
+
renderTargetHost.recover();
|
|
4010
|
+
// GPUCanvasContext is canvas-owned rather than generation-owned: the
|
|
4011
|
+
// candidate and previous RHI wrappers address the same underlying
|
|
4012
|
+
// context. Detach the previous configuration first, then configure the
|
|
4013
|
+
// candidate again after that detach. Otherwise the old wrapper's
|
|
4014
|
+
// unconfigure() can silently revoke the candidate's configuration while
|
|
4015
|
+
// candidate.perPassResources.configured still says true, producing a
|
|
4016
|
+
// submitted-but-black recovery frame.
|
|
4017
|
+
const previousContext = previous?.context ?? internals.context;
|
|
4018
|
+
candidate.pipeline.perPassResources.configured = false;
|
|
4019
|
+
previousContext.unconfigure();
|
|
4020
|
+
const configured = ensureContextConfigured(
|
|
4021
|
+
internals,
|
|
4022
|
+
candidate.pipeline,
|
|
4023
|
+
internals.errorRegistry,
|
|
4024
|
+
candidate.context,
|
|
4025
|
+
candidate.device,
|
|
4026
|
+
);
|
|
4027
|
+
if (!configured.ok) throw configured.error;
|
|
4028
|
+
internals.device = candidate.device;
|
|
4029
|
+
internals.context = candidate.context;
|
|
4030
|
+
publishRendererGeneration(candidate);
|
|
4031
|
+
renderTargetDevice = candidate.device;
|
|
4032
|
+
// Arm the first-frame guard before any publication code can resolve a
|
|
4033
|
+
// fallback pipeline or residency entry. Candidate preparation populated
|
|
4034
|
+
// these caches already; a miss here is a hard recovery failure, not an
|
|
4035
|
+
// opportunity to hide cold work inside the first visible frame.
|
|
4036
|
+
candidate.producerBindings.gpuStore.setRecoveryColdWorkGuard(recoveryColdWorkGuard);
|
|
4037
|
+
internals.recoveryColdWorkGuard = recoveryColdWorkGuard;
|
|
4038
|
+
if (recoveryGraphCandidate !== undefined) recoveryColdWorkGuard.arm();
|
|
4039
|
+
renderSystem.publishRecoveryPostProcessResources(candidatePostProcessResources);
|
|
4040
|
+
if (recoveryGraphCandidate !== undefined) {
|
|
4041
|
+
renderSystem.publishRecoveryGraphCandidate(recoveryGraphCandidate);
|
|
4042
|
+
}
|
|
4043
|
+
candidate.pipeline.perPassResources.commitBloomResources?.();
|
|
4044
|
+
recoveryRootBundle.publish();
|
|
4045
|
+
recoveryGraphCandidate?.markPublished();
|
|
4046
|
+
// RenderSystem owns frame/feature GPU state; the generation bindings own
|
|
4047
|
+
// residency stores; DeviceScope owns the remaining lifecycle roots. Their
|
|
4048
|
+
// retirement is intentionally centralized and ordered after publication.
|
|
4049
|
+
previousGpuStore.destroyAll();
|
|
4050
|
+
previousDynamicTextureStore.destroyAll();
|
|
4051
|
+
previousScope.retire();
|
|
4052
|
+
};
|
|
4053
|
+
rendererRecovery = createRendererRecovery({
|
|
4054
|
+
isDisposed: () => disposed,
|
|
4055
|
+
internals,
|
|
4056
|
+
getActiveDeviceScope: () => activeDeviceScope,
|
|
4057
|
+
getActiveShaderState: () => activeShaderState,
|
|
4058
|
+
getMaterialShaderUvSetCounts: () => materialShaderUvSetCounts,
|
|
4059
|
+
createRendererPipelineCacheState,
|
|
4060
|
+
getCandidateBuildState: () => candidateBuildState,
|
|
4061
|
+
setCandidateBuildState(state) {
|
|
4062
|
+
candidateBuildState = state;
|
|
4063
|
+
},
|
|
4064
|
+
getCandidateShaderState: () => candidateShaderState,
|
|
4065
|
+
setCandidateShaderState(state) {
|
|
4066
|
+
candidateShaderState = state;
|
|
4067
|
+
},
|
|
4068
|
+
getCandidatePipelineCacheState: () => candidatePipelineCacheState,
|
|
4069
|
+
setCandidatePipelineCacheState(state) {
|
|
4070
|
+
candidatePipelineCacheState = state;
|
|
4071
|
+
},
|
|
4072
|
+
getCandidateMaterialShaderUvSetCounts: () => candidateMaterialShaderUvSetCounts,
|
|
4073
|
+
setCandidateMaterialShaderUvSetCounts(state) {
|
|
4074
|
+
candidateMaterialShaderUvSetCounts = state;
|
|
4075
|
+
},
|
|
4076
|
+
setCandidateEmptyPostProcessBgl(layout) {
|
|
4077
|
+
candidateEmptyPostProcessBgl = layout;
|
|
4078
|
+
},
|
|
4079
|
+
getShader,
|
|
4080
|
+
assets,
|
|
4081
|
+
adaptMipmapShaderModuleFactory,
|
|
4082
|
+
buildPipeline,
|
|
4083
|
+
getMaterialShaderPipeline,
|
|
4084
|
+
getMaterialShaderPipelineEntry,
|
|
4085
|
+
getCachedMaterialShaderBindingContract,
|
|
4086
|
+
getParamSchema,
|
|
4087
|
+
getMaterialBindGroupLayout,
|
|
4088
|
+
metrics,
|
|
4089
|
+
buildPostProcessPipeline,
|
|
4090
|
+
renderSystem,
|
|
4091
|
+
createRecoveryFailureLocation,
|
|
4092
|
+
publishAndRetireRendererGeneration,
|
|
4093
|
+
});
|
|
4094
|
+
return renderer;
|
|
4095
|
+
}
|