@forgeax/engine-render 0.1.19 → 0.1.21
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 +215 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/assembly/device-feature-admission.d.ts +18 -0
- package/dist/assembly/device-feature-admission.d.ts.map +1 -0
- package/dist/assembly/directional-shadow-inspection.d.ts +38 -0
- package/dist/assembly/directional-shadow-inspection.d.ts.map +1 -0
- package/dist/assembly/factory.d.ts +34 -20
- package/dist/assembly/factory.d.ts.map +1 -1
- package/dist/assembly/host-contract.d.ts +9 -0
- package/dist/assembly/host-contract.d.ts.map +1 -1
- package/dist/assembly/render-target-host.d.ts +46 -0
- package/dist/assembly/render-target-host.d.ts.map +1 -0
- package/dist/assets/render-asset-port.d.ts.map +1 -1
- package/dist/authoring.mjs +3 -0
- package/dist/authoring.mjs.map +1 -1
- package/dist/capture/cube-views.d.ts +21 -0
- package/dist/capture/cube-views.d.ts.map +1 -0
- package/dist/capture/scheduler.d.ts +54 -0
- package/dist/capture/scheduler.d.ts.map +1 -0
- package/dist/{chunk-RJRUS55L.mjs → chunk-5YADUCA7.mjs} +1896 -1624
- package/dist/chunk-5YADUCA7.mjs.map +1 -0
- package/dist/{chunk-LOSO4CTV.mjs → chunk-6OPXDQ2E.mjs} +423 -46
- package/dist/chunk-6OPXDQ2E.mjs.map +1 -0
- package/dist/{chunk-IPXCABEP.mjs → chunk-BXBQQKFQ.mjs} +3 -9
- package/dist/chunk-BXBQQKFQ.mjs.map +1 -0
- package/dist/chunk-FS2KNUQH.mjs +1294 -0
- package/dist/chunk-FS2KNUQH.mjs.map +1 -0
- package/dist/components/camera.d.ts +1 -0
- package/dist/components/camera.d.ts.map +1 -1
- package/dist/components/cube-camera.d.ts +20 -0
- package/dist/components/cube-camera.d.ts.map +1 -0
- package/dist/components/directional-light.d.ts +11 -2
- package/dist/components/directional-light.d.ts.map +1 -1
- package/dist/components/directional-shadow-filter.d.ts +25 -0
- package/dist/components/directional-shadow-filter.d.ts.map +1 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/light-helpers.d.ts.map +1 -1
- package/dist/components/reflection-probe.d.ts +17 -0
- package/dist/components/reflection-probe.d.ts.map +1 -0
- package/dist/components/spot-light.d.ts +12 -0
- package/dist/components/spot-light.d.ts.map +1 -1
- package/dist/construct-renderer.mjs +8552 -1612
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/device/gpu-residency.d.ts +19 -2
- package/dist/device/gpu-residency.d.ts.map +1 -1
- package/dist/environment/frame.d.ts.map +1 -1
- package/dist/environment/lifecycle.d.ts +5 -0
- package/dist/environment/lifecycle.d.ts.map +1 -1
- package/dist/errors/render.d.ts +209 -93
- package/dist/errors/render.d.ts.map +1 -1
- package/dist/extract/directional-shadow-projection.d.ts +23 -0
- package/dist/extract/directional-shadow-projection.d.ts.map +1 -0
- package/dist/extract/environment.d.ts +14 -0
- package/dist/extract/environment.d.ts.map +1 -1
- package/dist/extract/visibility.d.ts.map +1 -1
- package/dist/features/fullscreen.d.ts.map +1 -1
- package/dist/features/targets.d.ts.map +1 -1
- package/dist/features/types.d.ts +10 -0
- package/dist/features/types.d.ts.map +1 -1
- package/dist/fullscreen-post-process-pass.d.ts +13 -2
- package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
- package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
- package/dist/gpu-driven/production-raster.d.ts +0 -1
- package/dist/gpu-driven/production-raster.d.ts.map +1 -1
- package/dist/gpu-scene.d.ts.map +1 -1
- package/dist/gpu-usage.d.ts +2 -0
- package/dist/gpu-usage.d.ts.map +1 -1
- package/dist/hdrp-buffers.d.ts +19 -17
- package/dist/hdrp-buffers.d.ts.map +1 -1
- package/dist/ibl/IblPipelineCache.d.ts +4 -0
- package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
- package/dist/ibl/kernel-cache.d.ts +36 -0
- package/dist/ibl/kernel-cache.d.ts.map +1 -0
- package/dist/index.d.ts +35 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +4 -4
- package/dist/index.mjs.map +1 -1
- package/dist/inspection-types.d.ts +51 -0
- package/dist/inspection-types.d.ts.map +1 -1
- package/dist/instances-derived-bounds.d.ts +50 -0
- package/dist/instances-derived-bounds.d.ts.map +1 -0
- package/dist/internal.mjs +1 -1
- package/dist/light-buffer-layout.d.ts +30 -13
- package/dist/light-buffer-layout.d.ts.map +1 -1
- package/dist/mesh-material-bindings.d.ts +54 -0
- package/dist/mesh-material-bindings.d.ts.map +1 -1
- package/dist/pbr-pipeline.d.ts +20 -7
- package/dist/pbr-pipeline.d.ts.map +1 -1
- package/dist/pipeline/standard-forward-lane.d.ts +2 -1
- package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
- package/dist/pipeline/standard-lighting/errors.d.ts +4 -0
- package/dist/pipeline/standard-lighting/errors.d.ts.map +1 -0
- package/dist/pipeline/standard-lighting/graph.d.ts +24 -0
- package/dist/pipeline/standard-lighting/graph.d.ts.map +1 -0
- package/dist/pipeline/standard-lighting/inspection.d.ts +38 -0
- package/dist/pipeline/standard-lighting/inspection.d.ts.map +1 -0
- package/dist/pipeline/standard-lighting/layout.d.ts +28 -0
- package/dist/pipeline/standard-lighting/layout.d.ts.map +1 -0
- package/dist/pipeline/standard-lighting/light-frame.d.ts +33 -0
- package/dist/pipeline/standard-lighting/light-frame.d.ts.map +1 -0
- package/dist/pipeline/standard-lighting/membership.d.ts +14 -0
- package/dist/pipeline/standard-lighting/membership.d.ts.map +1 -0
- package/dist/pipeline/standard-lighting/prepare.d.ts +16 -0
- package/dist/pipeline/standard-lighting/prepare.d.ts.map +1 -0
- package/dist/pipeline/standard-lighting/topology.d.ts +45 -0
- package/dist/pipeline/standard-lighting/topology.d.ts.map +1 -0
- package/dist/pipeline/standard-lighting/transport.d.ts +36 -0
- package/dist/pipeline/standard-lighting/transport.d.ts.map +1 -0
- package/dist/pipeline/standard-pipeline.d.ts +4 -13
- package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
- package/dist/pipeline/standard-post.d.ts.map +1 -1
- package/dist/pipeline/standard-profile.d.ts +12 -20
- package/dist/pipeline/standard-profile.d.ts.map +1 -1
- package/dist/pipeline-spec.d.ts +40 -7
- package/dist/pipeline-spec.d.ts.map +1 -1
- package/dist/point-shadow-inspection.d.ts +21 -0
- package/dist/point-shadow-inspection.d.ts.map +1 -0
- package/dist/record/frame-lighting.d.ts +29 -32
- package/dist/record/frame-lighting.d.ts.map +1 -1
- package/dist/record/frame-snapshot.d.ts +65 -15
- package/dist/record/frame-snapshot.d.ts.map +1 -1
- package/dist/record/frame-targets.d.ts +5 -0
- package/dist/record/frame-targets.d.ts.map +1 -1
- package/dist/record/frame.d.ts +12 -5
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/gpu-pass-timing/contract.d.ts +96 -0
- package/dist/record/gpu-pass-timing/contract.d.ts.map +1 -0
- package/dist/record/gpu-pass-timing/errors.d.ts +18 -0
- package/dist/record/gpu-pass-timing/errors.d.ts.map +1 -0
- package/dist/record/gpu-pass-timing/index.d.ts +8 -0
- package/dist/record/gpu-pass-timing/index.d.ts.map +1 -0
- package/dist/record/gpu-pass-timing/parser.d.ts +11 -0
- package/dist/record/gpu-pass-timing/parser.d.ts.map +1 -0
- package/dist/record/gpu-pass-timing/session.d.ts +43 -0
- package/dist/record/gpu-pass-timing/session.d.ts.map +1 -0
- package/dist/record/gpu-timing.d.ts +37 -0
- package/dist/record/gpu-timing.d.ts.map +1 -0
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/main-pass-material.d.ts +23 -2
- 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 +2 -0
- package/dist/record/main-pass.d.ts.map +1 -1
- package/dist/record/render-context.d.ts +48 -3
- package/dist/record/render-context.d.ts.map +1 -1
- package/dist/record/shadow-pass.d.ts.map +1 -1
- package/dist/record/skybox-post-pass.d.ts.map +1 -1
- package/dist/record/typed-frame-graph.d.ts +81 -5
- package/dist/record/typed-frame-graph.d.ts.map +1 -1
- package/dist/record/view-ubo.d.ts +7 -4
- package/dist/record/view-ubo.d.ts.map +1 -1
- package/dist/reflection/filter.d.ts +22 -0
- package/dist/reflection/filter.d.ts.map +1 -0
- package/dist/reflection/gpu-table.d.ts +25 -0
- package/dist/reflection/gpu-table.d.ts.map +1 -0
- package/dist/reflection/projection.d.ts +91 -0
- package/dist/reflection/projection.d.ts.map +1 -0
- package/dist/render-contract.d.ts +114 -7
- package/dist/render-contract.d.ts.map +1 -1
- package/dist/render-data.d.ts +10 -6
- package/dist/render-data.d.ts.map +1 -1
- package/dist/render-graph-primitives.d.ts +1 -0
- package/dist/render-graph-primitives.d.ts.map +1 -1
- package/dist/render-pipeline.d.ts +78 -2
- package/dist/render-pipeline.d.ts.map +1 -1
- package/dist/render-system-extract.d.ts +113 -20
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts +24 -3
- package/dist/render-system.d.ts.map +1 -1
- package/dist/renderer-inspect.d.ts +29 -0
- package/dist/renderer-inspect.d.ts.map +1 -0
- package/dist/renderer.d.ts +43 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/scene/render-scene.d.ts +14 -0
- package/dist/scene/render-scene.d.ts.map +1 -1
- package/dist/shadow-atlas.d.ts +3 -3
- package/dist/shadow-atlas.d.ts.map +1 -1
- package/dist/ssao-buffers.d.ts +3 -0
- package/dist/ssao-buffers.d.ts.map +1 -1
- package/dist/targets/contracts.d.ts +66 -0
- package/dist/targets/contracts.d.ts.map +1 -0
- package/dist/targets/graph-projection.d.ts +29 -0
- package/dist/targets/graph-projection.d.ts.map +1 -0
- package/dist/targets/material-source.d.ts +34 -0
- package/dist/targets/material-source.d.ts.map +1 -0
- package/dist/targets/owner.d.ts +32 -0
- package/dist/targets/owner.d.ts.map +1 -0
- package/dist/targets/physical.d.ts +18 -0
- package/dist/targets/physical.d.ts.map +1 -0
- package/dist/targets/readback.d.ts +39 -0
- package/dist/targets/readback.d.ts.map +1 -0
- package/dist/temporal/frame.d.ts +44 -0
- package/dist/temporal/frame.d.ts.map +1 -0
- package/dist/temporal/index.mjs +3 -3
- package/dist/typed-render-graph-primitives.d.ts +6 -1
- package/dist/typed-render-graph-primitives.d.ts.map +1 -1
- package/dist/typed-shadow-atlas.d.ts +10 -0
- package/dist/typed-shadow-atlas.d.ts.map +1 -0
- package/dist/typed-shadow-passes.d.ts +1 -1
- package/dist/typed-shadow-passes.d.ts.map +1 -1
- package/dist/volume/capability.d.ts +57 -0
- package/dist/volume/capability.d.ts.map +1 -0
- package/dist/volume/component.d.ts +61 -0
- package/dist/volume/component.d.ts.map +1 -0
- package/dist/volume/extract.d.ts +11 -0
- package/dist/volume/extract.d.ts.map +1 -0
- package/dist/volume/inspection.d.ts +38 -0
- package/dist/volume/inspection.d.ts.map +1 -0
- package/dist/volume/passes.d.ts +38 -0
- package/dist/volume/passes.d.ts.map +1 -0
- package/dist/volume/recovery.d.ts +38 -0
- package/dist/volume/recovery.d.ts.map +1 -0
- package/dist/volume/resources.d.ts +57 -0
- package/dist/volume/resources.d.ts.map +1 -0
- package/dist/volume/temporal.d.ts +35 -0
- package/dist/volume/temporal.d.ts.map +1 -0
- package/package.json +20 -20
- package/src/__tests__/bloom-exact-zero.unit.test.ts +48 -19
- package/src/__tests__/bloom-lifecycle.integration.test.ts +41 -13
- package/src/__tests__/camera-target-consumption.unit.test.ts +53 -0
- package/src/__tests__/cluster-binner.unit.test.ts +35 -0
- package/src/__tests__/cluster-grid-default-surface.unit.test.ts +7 -2
- package/src/__tests__/cube-camera-scheduler.unit.test.ts +70 -0
- package/src/__tests__/cube-camera-single-submit.perf.test.ts +75 -0
- package/src/__tests__/cube-camera-view-matrices.unit.test.ts +41 -0
- package/src/__tests__/cube-camera-vocabulary.unit.test.ts +47 -0
- package/src/__tests__/cube-camera.integration.test.ts +42 -0
- package/src/__tests__/device-feature-admission.unit.test.ts +73 -0
- package/src/__tests__/direct-light-snapshot.unit.test.ts +3 -3
- package/src/__tests__/docs-gate-green.unit.test.ts +38 -0
- package/src/__tests__/factory-contract.integration.test.ts +255 -29
- package/src/__tests__/fog-component.unit.test.ts +67 -0
- package/src/__tests__/fog.integration.test.ts +86 -0
- package/src/__tests__/fullscreen-feature-plan.unit.test.ts +37 -0
- package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +10 -9
- package/src/__tests__/gpu-driven-indirect-raster.dawn.test.ts +17 -1
- package/src/__tests__/gpu-driven-production.integration.test.ts +16 -17
- package/src/__tests__/gpu-driven-view-graph.integration.test.ts +12 -4
- package/src/__tests__/gpu-pass-timing-browser-runner.ts +281 -0
- package/src/__tests__/gpu-pass-timing-coexistence.integration.test.ts +80 -0
- package/src/__tests__/gpu-pass-timing-contract.unit.test.ts +243 -0
- package/src/__tests__/gpu-pass-timing-exact-zero.unit.test.ts +131 -0
- package/src/__tests__/gpu-pass-timing-lifecycle.integration.test.ts +230 -0
- package/src/__tests__/gpu-pass-timing-observation.unit.test.ts +233 -0
- package/src/__tests__/gpu-pass-timing-session.unit.test.ts +438 -0
- package/src/__tests__/gpu-pass-timing.browser.test.ts +26 -0
- package/src/__tests__/gpu-pass-timing.rhi-null.unit.test.ts +125 -0
- package/src/__tests__/hdrp-group2-visibility.unit.test.ts +24 -0
- package/src/__tests__/instances-culling.integration.test.ts +94 -0
- package/src/__tests__/instances-derived-bounds.unit.test.ts +118 -0
- package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +14 -14
- package/src/__tests__/material-observation.integration.test.ts +101 -0
- package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +1 -1
- package/src/__tests__/pbr-view-layout.unit.test.ts +16 -0
- package/src/__tests__/point-shadow-inspection.unit.test.ts +60 -0
- package/src/__tests__/public-surface.test-d.ts +10 -0
- package/src/__tests__/reflection-probe-authoring.unit.test.ts +71 -0
- package/src/__tests__/reflection-probe-filter.unit.test.ts +54 -0
- package/src/__tests__/reflection-probe-ibl-topology.unit.test.ts +45 -0
- package/src/__tests__/reflection-probe-pass-encode.integration.test.ts +101 -0
- package/src/__tests__/reflection-probe-performance.perf.test.ts +60 -0
- package/src/__tests__/reflection-probe-recovery.integration.test.ts +55 -0
- package/src/__tests__/reflection-probe-selection.unit.test.ts +46 -0
- package/src/__tests__/render-error-code-owner.test-d.ts +23 -3
- package/src/__tests__/render-feature-docs.unit.test.ts +1 -1
- package/src/__tests__/render-target-admission.unit.test.ts +80 -0
- package/src/__tests__/render-target-graph-projection.unit.test.ts +102 -0
- package/src/__tests__/render-target-inspection.unit.test.ts +52 -0
- package/src/__tests__/render-target-material-source.test-d.ts +30 -0
- package/src/__tests__/render-target-material-source.unit.test.ts +97 -0
- package/src/__tests__/render-target-owner-placement.unit.test.ts +203 -0
- package/src/__tests__/render-target-owner.unit.test.ts +122 -0
- package/src/__tests__/render-target-probe-soak.perf.test.ts +94 -0
- package/src/__tests__/render-target-public-schema.json +107 -0
- package/src/__tests__/render-target-public-schema.unit.test.ts +91 -0
- package/src/__tests__/render-target-public-surface.test-d.ts +83 -0
- package/src/__tests__/render-target-readback.integration.test.ts +90 -0
- package/src/__tests__/render-target-recovery.integration.test.ts +85 -0
- package/src/__tests__/renderer-factory-material-contract.unit.test.ts +76 -0
- package/src/__tests__/renderer-frame-transaction.integration.test.ts +1 -1
- package/src/__tests__/renderer-lifecycle.integration.test.ts +14 -0
- package/src/__tests__/shader-stage-surface.unit.test.ts +19 -4
- package/src/__tests__/skin-motion-regression.unit.test.ts +132 -0
- package/src/__tests__/skinned-shadow-caster.test.ts +32 -2
- package/src/__tests__/standard-cluster-capability.integration.test.ts +63 -0
- package/src/__tests__/standard-cluster-transport.unit.test.ts +121 -0
- package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +232 -0
- package/src/__tests__/standard-clustered-pipeline.unit.test.ts +8 -5
- package/src/__tests__/standard-lighting-inspection.integration.test.ts +138 -0
- package/src/__tests__/standard-lighting-ssot.unit.test.ts +18 -0
- package/src/__tests__/standard-lighting-write.unit.test.ts +377 -0
- package/src/__tests__/standard-lighting.integration.test.ts +67 -0
- package/src/__tests__/standard-pipeline.dawn.test.ts +206 -5
- package/src/__tests__/standard-pipeline.integration.test.ts +181 -150
- package/src/__tests__/standard-probe-binding.unit.test.ts +25 -0
- package/src/__tests__/standard-profile-oracle.unit.test.ts +23 -36
- package/src/__tests__/typed-pipeline-topology.unit.test.ts +395 -14
- package/src/__tests__/urp-pipeline-targets.unit.test.ts +2 -2
- package/src/__tests__/visibility-resolver.unit.test.ts +15 -0
- package/src/__tests__/volume-optics-oracle.unit.test.ts +85 -0
- package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +655 -0
- package/src/__tests__/volumetric-fog-component.unit.test.ts +71 -0
- package/src/__tests__/volumetric-fog-composite-stage-readback.dawn.test.ts +376 -0
- package/src/__tests__/volumetric-fog-errors.unit.test.ts +51 -0
- package/src/__tests__/volumetric-fog-inspection.unit.test.ts +77 -0
- package/src/__tests__/volumetric-fog-light-capability.unit.test.ts +19 -0
- package/src/__tests__/volumetric-fog-light-consumers.integration.test.ts +18 -0
- package/src/__tests__/volumetric-fog-light-errors.unit.test.ts +17 -0
- package/src/__tests__/volumetric-fog-light-topology.unit.test.ts +21 -0
- package/src/__tests__/volumetric-fog-light-transaction.integration.test.ts +53 -0
- package/src/__tests__/volumetric-fog-m9-watcher.integration.test.ts +27 -0
- package/src/__tests__/volumetric-fog-point-light-owner.integration.test.ts +44 -0
- package/src/__tests__/volumetric-fog-point-spot-accumulation.integration.test.ts +49 -0
- package/src/__tests__/volumetric-fog-point-spot-contract.unit.test.ts +28 -0
- package/src/__tests__/volumetric-fog-point-spot-lkg.integration.test.ts +65 -0
- package/src/__tests__/volumetric-fog-point-spot-selection.integration.test.ts +49 -0
- package/src/__tests__/volumetric-fog-point-spot-temporal.unit.test.ts +55 -0
- package/src/__tests__/volumetric-fog-projector-contract.unit.test.ts +22 -0
- package/src/__tests__/volumetric-fog-projector-inspection.unit.test.ts +12 -0
- package/src/__tests__/volumetric-fog-projector-surface-volume.integration.test.ts +11 -0
- package/src/__tests__/volumetric-fog-projector-transaction.integration.test.ts +26 -0
- package/src/__tests__/volumetric-fog-recovery.integration.test.ts +47 -0
- package/src/__tests__/volumetric-fog-selected-light-resolution.integration.test.ts +63 -0
- package/src/__tests__/volumetric-fog-spot-closure.integration.test.ts +26 -0
- package/src/__tests__/volumetric-fog-spot-contract.test-d.ts +21 -0
- package/src/__tests__/volumetric-fog-spot-contract.unit.test.ts +27 -0
- package/src/__tests__/volumetric-fog-spot-frame-optics.integration.test.ts +14 -0
- package/src/__tests__/volumetric-fog-spot-optics.integration.test.ts +29 -0
- package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +550 -0
- package/src/__tests__/volumetric-fog-temporal-selected-light.unit.test.ts +33 -0
- package/src/__tests__/volumetric-fog-temporal.unit.test.ts +45 -0
- package/src/__tests__/volumetric-fog-topology.unit.test.ts +237 -0
- package/src/__tests__/volumetric-fog-transparent.integration.test.ts +27 -0
- package/src/assembly/device-feature-admission.ts +73 -0
- package/src/assembly/directional-shadow-inspection.ts +122 -0
- package/src/assembly/factory.ts +1093 -246
- package/src/assembly/host-contract.ts +25 -0
- package/src/assembly/render-target-host.ts +847 -0
- package/src/assets/asset-decoders.ts +1 -1
- package/src/assets/render-asset-port.ts +7 -1
- package/src/capture/cube-views.ts +69 -0
- package/src/capture/scheduler.ts +265 -0
- package/src/components/__tests__/schema-contract.unit.test.ts +12 -0
- package/src/components/__tests__/schema-parity.unit.test.ts +9 -0
- package/src/components/camera.ts +4 -0
- package/src/components/cube-camera.ts +52 -0
- package/src/components/directional-light.ts +22 -2
- package/src/components/directional-shadow-filter.ts +57 -0
- package/src/components/index.ts +2 -0
- package/src/components/light-helpers.ts +85 -7
- package/src/components/reflection-probe.ts +49 -0
- package/src/components/spot-light.ts +22 -3
- package/src/device/gpu-residency.ts +168 -58
- package/src/environment/frame.ts +6 -24
- package/src/environment/lifecycle.ts +12 -0
- package/src/errors/render.ts +405 -143
- package/src/extract/directional-shadow-projection.ts +61 -0
- package/src/extract/environment.ts +57 -0
- package/src/extract/visibility.ts +25 -1
- package/src/features/fullscreen.ts +3 -0
- package/src/features/targets.ts +4 -1
- package/src/features/types.ts +10 -0
- package/src/fullscreen-post-process-pass.ts +43 -2
- package/src/gpu-driven/batch-topology.ts +1 -0
- package/src/gpu-driven/production-raster.ts +1 -3
- package/src/gpu-scene.ts +8 -1
- package/src/gpu-usage.ts +2 -0
- package/src/hdrp-buffers.ts +165 -59
- package/src/ibl/IblPipelineCache.ts +11 -0
- package/src/ibl/kernel-cache.ts +88 -0
- package/src/index.ts +186 -1
- package/src/inspection-types.ts +56 -0
- package/src/instances-derived-bounds.ts +171 -0
- package/src/light-buffer-layout.ts +74 -26
- package/src/mesh-material-bindings.ts +132 -0
- package/src/pbr-pipeline.ts +139 -27
- package/src/pipeline/standard-forward-lane.ts +65 -15
- package/src/pipeline/standard-lighting/errors.ts +6 -0
- package/src/pipeline/standard-lighting/graph.ts +157 -0
- package/src/pipeline/standard-lighting/inspection.ts +88 -0
- package/src/pipeline/standard-lighting/layout.ts +31 -0
- package/src/pipeline/standard-lighting/light-frame.ts +34 -0
- package/src/pipeline/standard-lighting/membership.ts +43 -0
- package/src/pipeline/standard-lighting/prepare.ts +37 -0
- package/src/pipeline/standard-lighting/topology.ts +120 -0
- package/src/pipeline/standard-lighting/transport.ts +77 -0
- package/src/pipeline/standard-pipeline.ts +141 -192
- package/src/pipeline/standard-post.ts +26 -7
- package/src/pipeline/standard-profile.ts +23 -30
- package/src/pipeline-spec.ts +84 -21
- package/src/point-shadow-inspection.ts +55 -0
- package/src/record/__tests__/temporal-payload.unit.test.ts +42 -1
- package/src/record/__tests__/transparent-group2-selection.unit.test.ts +73 -0
- package/src/record/__tests__/view-ubo-layout.unit.test.ts +40 -0
- package/src/record/frame-lighting.ts +470 -516
- package/src/record/frame-snapshot.ts +76 -28
- package/src/record/frame-targets.ts +23 -2
- package/src/record/frame.ts +1169 -79
- package/src/record/gpu-pass-timing/contract.ts +180 -0
- package/src/record/gpu-pass-timing/errors.ts +34 -0
- package/src/record/gpu-pass-timing/index.ts +37 -0
- package/src/record/gpu-pass-timing/parser.ts +97 -0
- package/src/record/gpu-pass-timing/session.ts +938 -0
- package/src/record/gpu-timing.ts +233 -0
- package/src/record/main-pass-geometry.ts +120 -80
- package/src/record/main-pass-material.ts +101 -11
- package/src/record/main-pass-sprite-draws.ts +54 -25
- package/src/record/main-pass.ts +71 -15
- package/src/record/render-context.ts +77 -0
- package/src/record/shadow-pass.ts +104 -66
- package/src/record/skybox-post-pass.ts +20 -4
- package/src/record/typed-frame-graph.ts +1016 -32
- package/src/record/view-ubo.ts +63 -28
- package/src/reflection/filter.ts +81 -0
- package/src/reflection/gpu-table.ts +44 -0
- package/src/reflection/projection.ts +281 -0
- package/src/render-contract.ts +147 -4
- package/src/render-data.ts +50 -22
- package/src/render-graph-primitives.ts +9 -4
- package/src/render-pipeline.ts +161 -2
- package/src/render-system-extract.ts +886 -103
- package/src/render-system.ts +1174 -66
- package/src/renderer-inspect.ts +66 -0
- package/src/renderer.ts +222 -0
- package/src/scene/render-scene.ts +117 -9
- package/src/shadow-atlas.ts +5 -5
- package/src/ssao-buffers.ts +9 -2
- package/src/targets/contracts.ts +182 -0
- package/src/targets/graph-projection.ts +169 -0
- package/src/targets/material-source.ts +143 -0
- package/src/targets/owner.ts +214 -0
- package/src/targets/physical.ts +18 -0
- package/src/targets/readback.ts +153 -0
- package/src/temporal/frame.ts +128 -0
- package/src/transmission/__tests__/standard-transmission.dawn.test.ts +61 -23
- package/src/typed-render-graph-primitives.ts +34 -2
- package/src/typed-shadow-atlas.ts +17 -0
- package/src/typed-shadow-passes.ts +47 -40
- package/src/volume/capability.ts +173 -0
- package/src/volume/component.ts +110 -0
- package/src/volume/extract.ts +21 -0
- package/src/volume/inspection.ts +78 -0
- package/src/volume/passes.ts +686 -0
- package/src/volume/recovery.ts +86 -0
- package/src/volume/resources.ts +236 -0
- package/src/volume/temporal.ts +89 -0
- package/dist/chunk-IPXCABEP.mjs.map +0 -1
- package/dist/chunk-LOSO4CTV.mjs.map +0 -1
- package/dist/chunk-RJRUS55L.mjs.map +0 -1
- package/dist/chunk-YTQ5C7YQ.mjs +0 -584
- package/dist/chunk-YTQ5C7YQ.mjs.map +0 -1
package/src/assembly/factory.ts
CHANGED
|
@@ -50,7 +50,6 @@ import type {
|
|
|
50
50
|
PipelineLayout,
|
|
51
51
|
RenderPipeline,
|
|
52
52
|
Result,
|
|
53
|
-
RhiAdapter,
|
|
54
53
|
RhiCanvasContext,
|
|
55
54
|
RhiDevice,
|
|
56
55
|
Sampler,
|
|
@@ -59,6 +58,7 @@ import type {
|
|
|
59
58
|
import { err, ok, RhiError, validateDrawArgs } from '@forgeax/engine-rhi';
|
|
60
59
|
import { registerPropagateTransforms } from '@forgeax/engine-scene';
|
|
61
60
|
import {
|
|
61
|
+
DEFAULT_STANDARD_PBR_PARAM_SCHEMA,
|
|
62
62
|
findVariantByKey,
|
|
63
63
|
type MaterialShaderEntry,
|
|
64
64
|
type MaterialShaderManifestEntry,
|
|
@@ -91,12 +91,16 @@ import {
|
|
|
91
91
|
RendererOperationError,
|
|
92
92
|
RenderFeatureCapabilityMissingError,
|
|
93
93
|
RenderFeatureStageFailedError,
|
|
94
|
+
StandardProfileInvalidError,
|
|
94
95
|
} from '../errors/render';
|
|
95
96
|
import { createRenderFeatureHost, type RenderFeatureHost } from '../features/host';
|
|
96
97
|
import type { RenderFeatureMaterialShaderBindingContract } from '../features/plan';
|
|
97
98
|
import { RENDER_FEATURE_VERTEX_LAYOUTS } from '../features/prepared-graphics';
|
|
98
99
|
import type { RenderFeature, RenderFeatureShaderModuleMode } from '../features/types';
|
|
99
|
-
import
|
|
100
|
+
import {
|
|
101
|
+
type PostProcessShaderEntry,
|
|
102
|
+
postProcessShaderModuleLabel,
|
|
103
|
+
} from '../fullscreen-post-process-pass';
|
|
100
104
|
import { glyphTextLayoutSystem } from '../glyph-text-layout-system';
|
|
101
105
|
import { GpuBuffer } from '../gpu-resource';
|
|
102
106
|
import { GPU_SHADER_STAGE_FRAGMENT, GPU_SHADER_STAGE_VERTEX } from '../gpu-stage';
|
|
@@ -134,9 +138,12 @@ import {
|
|
|
134
138
|
SPOT_LIGHT_STD430_BYTES,
|
|
135
139
|
} from '../light-buffer-layout';
|
|
136
140
|
import {
|
|
141
|
+
buildPbrMaterialUserRegionEntries,
|
|
137
142
|
buildPbrPipelineLayouts,
|
|
138
143
|
buildPbrSkinLayouts,
|
|
139
|
-
|
|
144
|
+
createHdrpSkinBindGroupLayoutDescriptor,
|
|
145
|
+
type PipelineGroup2Contract,
|
|
146
|
+
resolvePipelineGroup2Contract,
|
|
140
147
|
SKIN_MATERIAL_SHADER_ID,
|
|
141
148
|
} from '../pbr-pipeline';
|
|
142
149
|
import { STANDARD_CLUSTER_MEMBERSHIP_WGSL, standardPipeline } from '../pipeline/standard-pipeline';
|
|
@@ -163,6 +170,15 @@ import {
|
|
|
163
170
|
} from '../pipeline-spec';
|
|
164
171
|
import { POINTS_LINES_MATERIAL_SHADER_ID } from '../points-lines/record';
|
|
165
172
|
import type { FrameObservationOptions } from '../record/frame';
|
|
173
|
+
import type { GpuPassTimingReason } from '../record/gpu-pass-timing/errors.js';
|
|
174
|
+
import {
|
|
175
|
+
createGpuPassTimingSession,
|
|
176
|
+
DEFAULT_GPU_PASS_TIMING_OPTIONS,
|
|
177
|
+
type GpuPassTimingCapture,
|
|
178
|
+
type GpuPassTimingObservation,
|
|
179
|
+
type GpuPassTimingSession,
|
|
180
|
+
} from '../record/gpu-pass-timing/index.js';
|
|
181
|
+
import { GpuTimingCapture, type VolumeTimingObservation } from '../record/gpu-timing';
|
|
166
182
|
import type { BloomPersistentBundle } from '../record/render-context';
|
|
167
183
|
import { SHADOW_CASTER_BUFFER_SIZE } from '../record/shadow-pass';
|
|
168
184
|
import { POINTS_LINES_VIEW_BUFFER_SIZE, VIEW_UNIFORM_BUFFER_SIZE } from '../record/view-ubo';
|
|
@@ -185,9 +201,12 @@ import type {
|
|
|
185
201
|
RenderProfile,
|
|
186
202
|
RenderResult,
|
|
187
203
|
RenderWorldLease,
|
|
204
|
+
VolumetricFogShaderSources,
|
|
188
205
|
} from '../render-contract';
|
|
189
|
-
import { STANDARD_OUTPUT_TRANSFORM_FEATURE_ID } from '../render-contract';
|
|
206
|
+
import { FXAA_POST_PROCESS_ID, STANDARD_OUTPUT_TRANSFORM_FEATURE_ID } from '../render-contract';
|
|
207
|
+
|
|
190
208
|
import {
|
|
209
|
+
attachGpuPassTimingSession,
|
|
191
210
|
configureSurface,
|
|
192
211
|
createRenderSystem,
|
|
193
212
|
MATERIAL_PER_ENTITY_STRIDE,
|
|
@@ -195,6 +214,7 @@ import {
|
|
|
195
214
|
type RenderSystem,
|
|
196
215
|
selectSwapChainFormat,
|
|
197
216
|
} from '../render-system';
|
|
217
|
+
import { createGpuPassTimingObservationStore, observeGpuPassTimingDisabled } from '../renderer.js';
|
|
198
218
|
import { postSpawnResolveJoints } from '../scene-instances/post-spawn-resolve-joints';
|
|
199
219
|
import {
|
|
200
220
|
createSkinPaletteAllocator,
|
|
@@ -202,7 +222,12 @@ import {
|
|
|
202
222
|
} from '../systems/skin-palette-allocator';
|
|
203
223
|
import { tilemapChunkExtractSystem } from '../tilemap-chunk-extract-system';
|
|
204
224
|
import { EngineEnvironmentError, type RhiBackendPack } from './backend-contract';
|
|
225
|
+
import {
|
|
226
|
+
deviceOptionsForAdapter,
|
|
227
|
+
STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES,
|
|
228
|
+
} from './device-feature-admission';
|
|
205
229
|
import type { RendererAssemblyImplementation, RendererHostImplementation } from './host-contract';
|
|
230
|
+
import { createRenderTargetHost } from './render-target-host';
|
|
206
231
|
import { projectRendererOutputInspection } from './renderer-inspection';
|
|
207
232
|
import { isRendererStateOperational } from './renderer-lifecycle';
|
|
208
233
|
|
|
@@ -222,6 +247,19 @@ const GPU_ENVIRONMENT_ERROR_CODES = new Set([
|
|
|
222
247
|
'oom',
|
|
223
248
|
]);
|
|
224
249
|
|
|
250
|
+
/**
|
|
251
|
+
* The boot artifact is only a registry/prewarm seed. It is deliberately not
|
|
252
|
+
* an active lighting decision: every frame derives its Standard variant from
|
|
253
|
+
* `RenderPipelineFrame.standardLighting.kind` at record time. Keeping this
|
|
254
|
+
* boot-only value named as such prevents the eager module from becoming a
|
|
255
|
+
* second topology authority.
|
|
256
|
+
*/
|
|
257
|
+
const STANDARD_BOOT_TOPOLOGY_KIND: 'no-local-lights' | 'clustered' = 'no-local-lights';
|
|
258
|
+
|
|
259
|
+
function standardBootClusterAxis(): boolean {
|
|
260
|
+
return STANDARD_BOOT_TOPOLOGY_KIND === 'clustered';
|
|
261
|
+
}
|
|
262
|
+
|
|
225
263
|
function classifyEnvErrorReason(
|
|
226
264
|
base: string,
|
|
227
265
|
primary: { code?: unknown; name?: unknown } | undefined,
|
|
@@ -446,18 +484,14 @@ export interface BundlerOptions {
|
|
|
446
484
|
/**
|
|
447
485
|
* Per-variant PipelineLayout selector (M4.5 / D-10 option A).
|
|
448
486
|
*
|
|
449
|
-
* The HDRP
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
* 1-slot mesh-array BGL chain (`pbrPipelineLayout`).
|
|
487
|
+
* The resolved HDRP group(2) contract needs the 7-slot BGL chain
|
|
488
|
+
* (`hdrpPbrPipelineLayout`); ordinary material contracts keep the 1-slot
|
|
489
|
+
* mesh-array BGL chain (`pbrPipelineLayout`). Variant keys select source
|
|
490
|
+
* artifacts only and never override this contract.
|
|
454
491
|
*
|
|
455
|
-
* Returns `null`
|
|
456
|
-
*
|
|
457
|
-
*
|
|
458
|
-
* resolved upstream by `findVariantByKey` is the URP variant in that
|
|
459
|
-
* case (manifest entry with all-true definesKey === '' was registered
|
|
460
|
-
* with isHdrpActive=false at boot when storage-buffer caps are absent).
|
|
492
|
+
* Returns `null` when the selected layout is unavailable. An HDRP variant
|
|
493
|
+
* never falls back to the URP layout: its clustered WGSL and group(2) ABI
|
|
494
|
+
* must be admitted together or the draw fails closed.
|
|
461
495
|
*
|
|
462
496
|
* Pure function — exported for unit-test access (M4.5 / w35).
|
|
463
497
|
*/
|
|
@@ -465,8 +499,9 @@ export interface BundlerOptions {
|
|
|
465
499
|
* Closed enum of pipeline-layout kinds the selector can dispatch to.
|
|
466
500
|
*
|
|
467
501
|
* `pbr` — standard PBR (1-entry mesh-array BGL).
|
|
468
|
-
* `pbr-skin` — bug-20260611-skin-pipeline-layout:
|
|
469
|
-
* (binding 0 meshes + binding 1 palette
|
|
502
|
+
* `pbr-skin` — bug-20260611-skin-pipeline-layout: 3-entry mesh-array BGL
|
|
503
|
+
* (binding 0 meshes + binding 1 current palette + binding 2 previous
|
|
504
|
+
* palette) for the `forgeax::pbr-skin`
|
|
470
505
|
* material shader.
|
|
471
506
|
* `hdrp-pbr` — feat-20260609 HDRP cluster-forward variant (7-slot group(2)
|
|
472
507
|
* BGL substituted at slot 2).
|
|
@@ -486,7 +521,15 @@ export interface BundlerOptions {
|
|
|
486
521
|
* material-shader layouts add a `LayoutKind` member + a PipelineState slot
|
|
487
522
|
* + a switch arm here in one cut (charter P3 extensibility).
|
|
488
523
|
*/
|
|
489
|
-
export type LayoutKind = 'pbr' | 'pbr-skin' | 'hdrp-pbr' | 'sprite-urp' | 'unlit-urp';
|
|
524
|
+
export type LayoutKind = 'pbr' | 'pbr-skin' | 'hdrp-pbr' | 'hdrp-skin' | 'sprite-urp' | 'unlit-urp';
|
|
525
|
+
|
|
526
|
+
/** The closed group(2) contract resolved from the selected WGSL artifact. */
|
|
527
|
+
export type { PipelineGroup2Contract } from '../pbr-pipeline';
|
|
528
|
+
export { resolvePipelineGroup2Contract } from '../pbr-pipeline';
|
|
529
|
+
|
|
530
|
+
function directionalPcssCapable(backendKind: RhiDevice['caps']['backendKind']): boolean {
|
|
531
|
+
return backendKind === 'webgpu' || backendKind === 'wgpu-native';
|
|
532
|
+
}
|
|
490
533
|
|
|
491
534
|
/**
|
|
492
535
|
* Return every device-capability HDRP PBR variant that needs a lazy module
|
|
@@ -499,17 +542,44 @@ export function selectHdrpPbrPrewarmVariants(
|
|
|
499
542
|
manifestEntry: MaterialShaderManifestEntry | undefined,
|
|
500
543
|
storageBufferCapable: boolean,
|
|
501
544
|
transmissionCapable: boolean,
|
|
545
|
+
directionalPcssAvailable = true,
|
|
546
|
+
projectorAvailable: boolean = true,
|
|
502
547
|
): readonly MaterialShaderManifestEntry['variants'][number][] {
|
|
548
|
+
// HDRP's clustered group(2) ABI is storage-buffer backed. A downlevel
|
|
549
|
+
// WebGL2 device must stay on the URP variant; prewarming an HDRP source
|
|
550
|
+
// here would advertise an inactive route and can exceed its fragment
|
|
551
|
+
// uniform-buffer budget before the first frame.
|
|
552
|
+
if (!storageBufferCapable) return [];
|
|
503
553
|
return (
|
|
504
554
|
manifestEntry?.variants.filter(
|
|
505
555
|
(variant) =>
|
|
506
556
|
variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
|
|
507
557
|
variant.defines.CLUSTER_FORWARD_AVAILABLE === true &&
|
|
558
|
+
(!('DIRECTIONAL_PCSS_AVAILABLE' in variant.defines) ||
|
|
559
|
+
variant.defines.DIRECTIONAL_PCSS_AVAILABLE === directionalPcssAvailable) &&
|
|
560
|
+
(!('PROJECTOR_AVAILABLE' in variant.defines) ||
|
|
561
|
+
variant.defines.PROJECTOR_AVAILABLE === projectorAvailable) &&
|
|
508
562
|
(transmissionCapable || variant.defines.TRANSMISSION_AVAILABLE !== true),
|
|
509
563
|
) ?? []
|
|
510
564
|
);
|
|
511
565
|
}
|
|
512
566
|
|
|
567
|
+
/** Return every active-capability pbr-skin variant used by URP and HDRP draws. */
|
|
568
|
+
export function selectSkinPrewarmVariants(
|
|
569
|
+
manifestEntry: MaterialShaderManifestEntry | undefined,
|
|
570
|
+
storageBufferCapable: boolean,
|
|
571
|
+
): readonly MaterialShaderManifestEntry['variants'][number][] {
|
|
572
|
+
return (
|
|
573
|
+
manifestEntry?.variants.filter(
|
|
574
|
+
(variant) =>
|
|
575
|
+
variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
|
|
576
|
+
// The clustered skin ABI is only active on the storage-backed tier;
|
|
577
|
+
// storage=false must never prewarm or expose it as a usable variant.
|
|
578
|
+
(storageBufferCapable || variant.defines.CLUSTER_FORWARD_AVAILABLE !== true),
|
|
579
|
+
) ?? []
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
|
|
513
583
|
/**
|
|
514
584
|
* Select both Standard transmission states from the manifest for exact-label
|
|
515
585
|
* prewarming. The requested keys come from declared variants; no key is
|
|
@@ -518,6 +588,8 @@ export function selectHdrpPbrPrewarmVariants(
|
|
|
518
588
|
export function selectStandardPbrTransmissionPrewarmVariants(
|
|
519
589
|
manifestEntry: MaterialShaderManifestEntry | undefined,
|
|
520
590
|
storageBufferCapable: boolean,
|
|
591
|
+
directionalPcssAvailable = true,
|
|
592
|
+
projectorAvailable: boolean = true,
|
|
521
593
|
): readonly MaterialShaderManifestEntry['variants'][number][] {
|
|
522
594
|
if (manifestEntry === undefined) {
|
|
523
595
|
throw new Error('Standard material shader manifest row is missing');
|
|
@@ -527,8 +599,12 @@ export function selectStandardPbrTransmissionPrewarmVariants(
|
|
|
527
599
|
const declared = manifestEntry.variants.find(
|
|
528
600
|
(variant) =>
|
|
529
601
|
variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
|
|
530
|
-
variant.defines.CLUSTER_FORWARD_AVAILABLE ===
|
|
602
|
+
variant.defines.CLUSTER_FORWARD_AVAILABLE === standardBootClusterAxis() &&
|
|
531
603
|
variant.defines.VERTEX_COLOR_AVAILABLE === false &&
|
|
604
|
+
(!('DIRECTIONAL_PCSS_AVAILABLE' in variant.defines) ||
|
|
605
|
+
variant.defines.DIRECTIONAL_PCSS_AVAILABLE === directionalPcssAvailable) &&
|
|
606
|
+
(!('PROJECTOR_AVAILABLE' in variant.defines) ||
|
|
607
|
+
variant.defines.PROJECTOR_AVAILABLE === projectorAvailable) &&
|
|
532
608
|
variant.defines.TRANSMISSION_AVAILABLE === transmissionAvailable,
|
|
533
609
|
);
|
|
534
610
|
if (declared === undefined) {
|
|
@@ -556,20 +632,28 @@ export function selectNoColorPbrVariant(
|
|
|
556
632
|
manifestEntry: MaterialShaderManifestEntry | undefined,
|
|
557
633
|
storageBufferCapable: boolean,
|
|
558
634
|
variantSet: string | undefined,
|
|
635
|
+
directionalPcssAvailable = true,
|
|
636
|
+
projectorAvailable: boolean = true,
|
|
559
637
|
): MaterialShaderManifestEntry['variants'][number] | undefined {
|
|
560
|
-
const hdrpRequested =
|
|
561
|
-
variantSet === '' ||
|
|
562
|
-
// biome-ignore lint/complexity/useOptionalChain: G-14 requires an explicit undefined guard so no variant request becomes false.
|
|
563
|
-
(variantSet !== undefined && variantSet.includes('CLUSTER_FORWARD_AVAILABLE=true'));
|
|
638
|
+
const hdrpRequested = variantSet === '' || requestsClusterVariant(variantSet);
|
|
564
639
|
return manifestEntry?.variants.find(
|
|
565
640
|
(variant) =>
|
|
566
641
|
variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
|
|
567
642
|
variant.defines.VERTEX_COLOR_AVAILABLE === false &&
|
|
643
|
+
(!('DIRECTIONAL_PCSS_AVAILABLE' in variant.defines) ||
|
|
644
|
+
variant.defines.DIRECTIONAL_PCSS_AVAILABLE === directionalPcssAvailable) &&
|
|
645
|
+
(!('PROJECTOR_AVAILABLE' in variant.defines) ||
|
|
646
|
+
variant.defines.PROJECTOR_AVAILABLE === projectorAvailable) &&
|
|
568
647
|
(!('CLUSTER_FORWARD_AVAILABLE' in variant.defines) ||
|
|
569
648
|
variant.defines.CLUSTER_FORWARD_AVAILABLE === hdrpRequested),
|
|
570
649
|
);
|
|
571
650
|
}
|
|
572
651
|
|
|
652
|
+
function requestsClusterVariant(variantSet: string | undefined): boolean {
|
|
653
|
+
if (variantSet === undefined) return false;
|
|
654
|
+
return variantSet.includes('CLUSTER_FORWARD_AVAILABLE=true');
|
|
655
|
+
}
|
|
656
|
+
|
|
573
657
|
export type MaterialShaderBindingContract = RenderFeatureMaterialShaderBindingContract;
|
|
574
658
|
export type MaterialShaderVertexInputContract = 'none' | 'render-material';
|
|
575
659
|
|
|
@@ -623,6 +707,46 @@ export function resolveMaterialShaderVertexInputContract(
|
|
|
623
707
|
: 'none';
|
|
624
708
|
}
|
|
625
709
|
|
|
710
|
+
/**
|
|
711
|
+
* Decide whether a custom material's user-region can use the shared PBR
|
|
712
|
+
* bind-group layout. Resource names are not part of a WebGPU layout contract:
|
|
713
|
+
* the only safe reuse test is that every derived binding in the custom schema
|
|
714
|
+
* is an identical prefix of the canonical user-region descriptor, including
|
|
715
|
+
* binding kind and its full shape (sample type, view dimension, sampler mode,
|
|
716
|
+
* dynamic-offset flags, and visibility). A texture3d/array or comparison /
|
|
717
|
+
* storage resource must therefore take the per-shader layout path even when
|
|
718
|
+
* its field count is small.
|
|
719
|
+
*/
|
|
720
|
+
export function isSharedMaterialUserRegionCompatible(
|
|
721
|
+
paramSchema: readonly ParamSchemaEntry[],
|
|
722
|
+
): boolean {
|
|
723
|
+
const shared = buildPbrMaterialUserRegionEntries(DEFAULT_STANDARD_PBR_PARAM_SCHEMA);
|
|
724
|
+
const candidate = buildPbrMaterialUserRegionEntries(paramSchema);
|
|
725
|
+
if (candidate.length > shared.length) return false;
|
|
726
|
+
return candidate.every((entry, index) => {
|
|
727
|
+
const canonical = shared[index];
|
|
728
|
+
if (canonical === undefined) return false;
|
|
729
|
+
return (
|
|
730
|
+
JSON.stringify({
|
|
731
|
+
binding: entry.binding,
|
|
732
|
+
visibility: entry.visibility,
|
|
733
|
+
buffer: entry.buffer ?? null,
|
|
734
|
+
sampler: entry.sampler ?? null,
|
|
735
|
+
texture: entry.texture ?? null,
|
|
736
|
+
storageTexture: entry.storageTexture ?? null,
|
|
737
|
+
}) ===
|
|
738
|
+
JSON.stringify({
|
|
739
|
+
binding: canonical.binding,
|
|
740
|
+
visibility: canonical.visibility,
|
|
741
|
+
buffer: canonical.buffer ?? null,
|
|
742
|
+
sampler: canonical.sampler ?? null,
|
|
743
|
+
texture: canonical.texture ?? null,
|
|
744
|
+
storageTexture: canonical.storageTexture ?? null,
|
|
745
|
+
})
|
|
746
|
+
);
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
626
750
|
export function allowsUnlitPreparedFallback(
|
|
627
751
|
depthFormatOverride: GPUTextureFormat | null | undefined,
|
|
628
752
|
vertexLayout: string | undefined,
|
|
@@ -656,8 +780,9 @@ export function selectPipelineLayoutForVariant(
|
|
|
656
780
|
readonly pbrPipelineLayout: PipelineLayout | null;
|
|
657
781
|
readonly hdrpPbrPipelineLayout: PipelineLayout | null;
|
|
658
782
|
readonly pbrSkinPipelineLayout: PipelineLayout | null;
|
|
783
|
+
readonly hdrpSkinPipelineLayout?: PipelineLayout | null;
|
|
659
784
|
} | null,
|
|
660
|
-
|
|
785
|
+
_variantSet: string | undefined,
|
|
661
786
|
layoutKind?: LayoutKind,
|
|
662
787
|
): PipelineLayout | null {
|
|
663
788
|
if (state === null) return null;
|
|
@@ -670,6 +795,9 @@ export function selectPipelineLayoutForVariant(
|
|
|
670
795
|
if (layoutKind === 'pbr-skin') {
|
|
671
796
|
return state.pbrSkinPipelineLayout;
|
|
672
797
|
}
|
|
798
|
+
if (layoutKind === 'hdrp-skin') {
|
|
799
|
+
return state.hdrpSkinPipelineLayout ?? null;
|
|
800
|
+
}
|
|
673
801
|
// bug-20260708 M2 (c): sprite / sprite-lit shaders reuse the URP 1-slot
|
|
674
802
|
// mesh-array pipeline layout but their canonical all-true variant key
|
|
675
803
|
// `''` (SPRITE_PASS_PER_INSTANCE_REGION_VARIANT_SET) must NOT trigger
|
|
@@ -685,33 +813,19 @@ export function selectPipelineLayoutForVariant(
|
|
|
685
813
|
if (layoutKind === 'unlit-urp') {
|
|
686
814
|
return state.pbrPipelineLayout;
|
|
687
815
|
}
|
|
688
|
-
//
|
|
689
|
-
//
|
|
690
|
-
// the
|
|
691
|
-
//
|
|
816
|
+
// The resolved group(2) contract is authoritative for custom material
|
|
817
|
+
// shaders. A mesh-only composed WGSL uses the one-entry mesh BGL even when
|
|
818
|
+
// the renderer is otherwise running its HDRP lighting variant; allowing the
|
|
819
|
+
// variant key to re-select HDRP here recreates the mesh-vs-cluster mismatch.
|
|
692
820
|
if (layoutKind === 'pbr') {
|
|
693
821
|
return state.pbrPipelineLayout;
|
|
694
822
|
}
|
|
695
|
-
//
|
|
696
|
-
//
|
|
697
|
-
//
|
|
698
|
-
//
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
// URP variants have CLUSTER_FORWARD_AVAILABLE=false explicitly, or omit
|
|
702
|
-
// the axis altogether, or pass undefined (no variant routing).
|
|
703
|
-
// G-14 (D-11): empty-string is the canonical all-true HDRP key, so we
|
|
704
|
-
// explicitly distinguish '' vs undefined rather than using an optional-chain
|
|
705
|
-
// (charter P3: explicit failure / no implicit empty-vs-missing collapse).
|
|
706
|
-
const isHdrpVariant =
|
|
707
|
-
layoutKind === 'hdrp-pbr' ||
|
|
708
|
-
variantSet === '' ||
|
|
709
|
-
// biome-ignore lint/complexity/useOptionalChain: G-14 grep gate forbids the optional-chain falsy collapse on variantSet.
|
|
710
|
-
(variantSet !== undefined && variantSet.includes('CLUSTER_FORWARD_AVAILABLE=true'));
|
|
711
|
-
if (isHdrpVariant && state.hdrpPbrPipelineLayout !== null) {
|
|
712
|
-
return state.hdrpPbrPipelineLayout;
|
|
713
|
-
}
|
|
714
|
-
return state.pbrPipelineLayout;
|
|
823
|
+
// Group(2) contract/layout kind is the sole layout authority. Variant keys
|
|
824
|
+
// identify WGSL artifacts, but must not independently select a bind-group
|
|
825
|
+
// layout; this prevents an empty/expanded key from bypassing the resolved
|
|
826
|
+
// storage contract.
|
|
827
|
+
const isHdrpVariant = layoutKind === 'hdrp-pbr';
|
|
828
|
+
return isHdrpVariant ? state.hdrpPbrPipelineLayout : state.pbrPipelineLayout;
|
|
715
829
|
}
|
|
716
830
|
|
|
717
831
|
/**
|
|
@@ -727,10 +841,14 @@ export function resolveMaterialShaderVariantSet(
|
|
|
727
841
|
}>,
|
|
728
842
|
backendKind: RhiDevice['caps']['backendKind'],
|
|
729
843
|
storageBuffer: boolean,
|
|
844
|
+
projectorAvailable: boolean = true,
|
|
730
845
|
): string | undefined {
|
|
731
846
|
const declaredAxes = new Set(variants.flatMap((variant) => Object.keys(variant.defines)));
|
|
732
847
|
const declaresCapabilityVariants =
|
|
733
|
-
declaredAxes.has('WEBGL2_COMPAT') ||
|
|
848
|
+
declaredAxes.has('WEBGL2_COMPAT') ||
|
|
849
|
+
declaredAxes.has('STORAGE_BUFFER_AVAILABLE') ||
|
|
850
|
+
declaredAxes.has('DIRECTIONAL_PCSS_AVAILABLE') ||
|
|
851
|
+
declaredAxes.has('PROJECTOR_AVAILABLE');
|
|
734
852
|
if (!declaresCapabilityVariants) return requestedVariantSet;
|
|
735
853
|
|
|
736
854
|
const requested: Record<string, boolean> = {};
|
|
@@ -748,20 +866,24 @@ export function resolveMaterialShaderVariantSet(
|
|
|
748
866
|
: axis === 'SKINNING_DISABLED'
|
|
749
867
|
? (requested[axis] ?? true)
|
|
750
868
|
: axis === 'CLUSTER_FORWARD_AVAILABLE'
|
|
751
|
-
? backendKind !== 'wgpu-webgl2' && (requested[axis] ?? false)
|
|
869
|
+
? storageBuffer && backendKind !== 'wgpu-webgl2' && (requested[axis] ?? false)
|
|
752
870
|
: axis === 'PER_INSTANCE_REGION'
|
|
753
871
|
? requestedVariantSet === '' || (requested[axis] ?? false)
|
|
754
872
|
: axis === 'VERTEX_COLOR_AVAILABLE'
|
|
755
873
|
? requestedVariantSet === '' || (requested[axis] ?? false)
|
|
756
874
|
: axis === 'TRANSMISSION_AVAILABLE'
|
|
757
875
|
? (requested[axis] ?? false)
|
|
758
|
-
:
|
|
876
|
+
: axis === 'DIRECTIONAL_PCSS_AVAILABLE'
|
|
877
|
+
? directionalPcssCapable(backendKind)
|
|
878
|
+
: axis === 'PROJECTOR_AVAILABLE'
|
|
879
|
+
? projectorAvailable
|
|
880
|
+
: (requested[axis] ?? true);
|
|
759
881
|
}
|
|
760
882
|
const sorted = Object.entries(filtered).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
761
883
|
return sorted.every(([, value]) => value)
|
|
762
|
-
? requestedVariantSet ===
|
|
763
|
-
?
|
|
764
|
-
:
|
|
884
|
+
? requestedVariantSet === undefined
|
|
885
|
+
? undefined
|
|
886
|
+
: ''
|
|
765
887
|
: sorted.map(([name, value]) => `${name}=${value}`).join('+');
|
|
766
888
|
}
|
|
767
889
|
|
|
@@ -825,6 +947,28 @@ export async function createRenderer(
|
|
|
825
947
|
});
|
|
826
948
|
}
|
|
827
949
|
|
|
950
|
+
function markRendererBootstrapStage(
|
|
951
|
+
name: string,
|
|
952
|
+
detail?: Readonly<Record<string, unknown>>,
|
|
953
|
+
): void {
|
|
954
|
+
const host = globalThis as {
|
|
955
|
+
__forgeaxRendererBootstrap?: {
|
|
956
|
+
readonly name: string;
|
|
957
|
+
readonly startedAt: number;
|
|
958
|
+
readonly previousElapsedMs: number;
|
|
959
|
+
readonly detail?: Readonly<Record<string, unknown>>;
|
|
960
|
+
};
|
|
961
|
+
};
|
|
962
|
+
const previous = host.__forgeaxRendererBootstrap;
|
|
963
|
+
const now = Date.now();
|
|
964
|
+
host.__forgeaxRendererBootstrap = {
|
|
965
|
+
name,
|
|
966
|
+
startedAt: now,
|
|
967
|
+
previousElapsedMs: previous === undefined ? 0 : now - previous.startedAt,
|
|
968
|
+
...(detail === undefined ? {} : { detail }),
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
|
|
828
972
|
// ─── WebGPU branch ──────────────────────────────────────────────────────────
|
|
829
973
|
|
|
830
974
|
/**
|
|
@@ -935,44 +1079,6 @@ type WebGPUOutcome =
|
|
|
935
1079
|
| { kind: 'rhi-err'; error: RhiError }
|
|
936
1080
|
| { kind: 'throw'; error: Error };
|
|
937
1081
|
|
|
938
|
-
// Optional compression is exposed on a device only when it is requested from
|
|
939
|
-
// the adapter. Keep this capability projection shared by initial construction
|
|
940
|
-
// and device-loss recovery; otherwise recovery silently drops BC/ETC2/ASTC
|
|
941
|
-
// and already-catalogued compressed textures fail on the first new frame.
|
|
942
|
-
const COMPRESSION_FEATURES: GPUFeatureName[] = [
|
|
943
|
-
'texture-compression-bc',
|
|
944
|
-
'texture-compression-etc2',
|
|
945
|
-
'texture-compression-astc',
|
|
946
|
-
];
|
|
947
|
-
|
|
948
|
-
// The Standard transmission fragment ABI needs 17 sampled textures across its
|
|
949
|
-
// material, IBL, clustered-light, and SSAO bindings. Ordinary Standard draws
|
|
950
|
-
// use the non-transmission variant and remain valid on the WebGPU minimum of
|
|
951
|
-
// 16; only request the elevated limit when the adapter can provide it.
|
|
952
|
-
const STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES = 17;
|
|
953
|
-
|
|
954
|
-
function deviceOptionsForAdapter(adapter: Pick<RhiAdapter, 'features' | 'limits'>):
|
|
955
|
-
| {
|
|
956
|
-
requiredFeatures?: GPUFeatureName[];
|
|
957
|
-
requiredLimits?: GPUDeviceDescriptor['requiredLimits'];
|
|
958
|
-
}
|
|
959
|
-
| undefined {
|
|
960
|
-
const requiredFeatures = COMPRESSION_FEATURES.filter((feature) => adapter.features.has(feature));
|
|
961
|
-
const supportsTransmission =
|
|
962
|
-
(adapter.limits.maxSampledTexturesPerShaderStage ?? 0) >=
|
|
963
|
-
STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES;
|
|
964
|
-
return {
|
|
965
|
-
...(requiredFeatures.length > 0 ? { requiredFeatures } : {}),
|
|
966
|
-
...(supportsTransmission
|
|
967
|
-
? {
|
|
968
|
-
requiredLimits: {
|
|
969
|
-
maxSampledTexturesPerShaderStage: STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES,
|
|
970
|
-
},
|
|
971
|
-
}
|
|
972
|
-
: {}),
|
|
973
|
-
};
|
|
974
|
-
}
|
|
975
|
-
|
|
976
1082
|
/**
|
|
977
1083
|
* Goes through the strict two-step path exposed by the injected backend pack:
|
|
978
1084
|
* `rhi.requestAdapter()` -> `adapter.requestDevice()`
|
|
@@ -1013,19 +1119,32 @@ async function tryCreateWebGPURenderer(
|
|
|
1013
1119
|
// backend singleton (rhi-webgpu / rhi-wgpu / explicit escape hatch).
|
|
1014
1120
|
// w19: thread canvas as compatibleSurface so the wgpu GL backend can
|
|
1015
1121
|
// enumerate adapters against it (escape hatch, capability-gated).
|
|
1122
|
+
markRendererBootstrapStage('adapter-request-start');
|
|
1016
1123
|
const adapterResult = await pack.rhi.requestAdapter(undefined, canvas);
|
|
1124
|
+
markRendererBootstrapStage('adapter-request-end', {
|
|
1125
|
+
ok: adapterResult.ok,
|
|
1126
|
+
...(adapterResult.ok ? {} : { code: adapterResult.error.code }),
|
|
1127
|
+
});
|
|
1017
1128
|
if (!adapterResult.ok) {
|
|
1018
1129
|
return { kind: 'rhi-err', error: adapterResult.error };
|
|
1019
1130
|
}
|
|
1020
1131
|
const adapter = adapterResult.value;
|
|
1021
1132
|
// M4 w28: filter compression features from adapter.features (AC-07). The
|
|
1022
1133
|
// same helper is used by recover() below so both paths share one profile.
|
|
1023
|
-
const deviceOpts = deviceOptionsForAdapter(adapter);
|
|
1134
|
+
const deviceOpts = deviceOptionsForAdapter(adapter, options);
|
|
1135
|
+
markRendererBootstrapStage('device-request-start');
|
|
1024
1136
|
const result = await adapter.requestDevice(deviceOpts);
|
|
1137
|
+
markRendererBootstrapStage('device-request-end', {
|
|
1138
|
+
ok: result.ok,
|
|
1139
|
+
...(result.ok ? {} : { code: result.error.code }),
|
|
1140
|
+
});
|
|
1025
1141
|
if (!result.ok) {
|
|
1026
1142
|
return { kind: 'rhi-err', error: result.error };
|
|
1027
1143
|
}
|
|
1028
1144
|
const device: RhiDevice = result.value;
|
|
1145
|
+
markRendererBootstrapStage('device-acquired', {
|
|
1146
|
+
backendKind: device.caps.backendKind,
|
|
1147
|
+
});
|
|
1029
1148
|
|
|
1030
1149
|
// Acquire the canvas context through the backend pack (M3 / w16).
|
|
1031
1150
|
// rhi-webgpu: internally calls canvas.getContext('webgpu') + branded wrap.
|
|
@@ -1150,16 +1269,30 @@ interface WebGPURendererInternals {
|
|
|
1150
1269
|
* has wired the controller; identity stable across grow events.
|
|
1151
1270
|
*/
|
|
1152
1271
|
meshSsboState?: MeshSsboState;
|
|
1272
|
+
/** Device-bound, manifest-owned volumetric utility shader sources. */
|
|
1273
|
+
volumetricFogShaders?: VolumetricFogShaderSources | undefined;
|
|
1153
1274
|
/** Host-owned producer features; created before device-bound assembly starts. */
|
|
1154
1275
|
featureHost?: RenderFeatureHost;
|
|
1155
1276
|
/** App-owned overlay capability read by the typed graph at frame time. */
|
|
1156
1277
|
debugOverlay?: RenderDebugOverlay;
|
|
1278
|
+
gpuPassTimingSubmittedWork?: Promise<void> | undefined;
|
|
1157
1279
|
}
|
|
1158
1280
|
|
|
1159
1281
|
async function makeWebGPURenderer(
|
|
1160
1282
|
internals: WebGPURendererInternals,
|
|
1161
1283
|
): Promise<RendererAssemblyImplementation> {
|
|
1162
1284
|
let disposed = false;
|
|
1285
|
+
let renderSystemForTargetPromotion: RenderSystem | undefined;
|
|
1286
|
+
// The host keeps the zero-option createRenderTargetHost() seam compatible
|
|
1287
|
+
// for package-local lifecycle tests while this assembly supplies the live
|
|
1288
|
+
// DeviceScope generation callback.
|
|
1289
|
+
const renderTargetHost = createRenderTargetHost({
|
|
1290
|
+
rendererId: Symbol('renderer'),
|
|
1291
|
+
getGeneration: () => activeDeviceScope.generation,
|
|
1292
|
+
getDevice: () => internals.device,
|
|
1293
|
+
canPromoteTarget: (target) =>
|
|
1294
|
+
renderSystemForTargetPromotion?.isCubeCapturePending(target) !== true,
|
|
1295
|
+
});
|
|
1163
1296
|
const featureHostResult = createRenderFeatureHost(internals.options?.features ?? []);
|
|
1164
1297
|
if (!featureHostResult.ok) throw featureHostResult.error;
|
|
1165
1298
|
internals.featureHost = featureHostResult.value;
|
|
@@ -1174,6 +1307,28 @@ async function makeWebGPURenderer(
|
|
|
1174
1307
|
featureHostResult.value.features.flatMap((feature) => feature.requiredMaterialShaders ?? []),
|
|
1175
1308
|
);
|
|
1176
1309
|
let requiredMaterialShaders = Object.freeze([...requiredMaterialShaderSet]);
|
|
1310
|
+
const collectRequiredFullscreenPostProcesses = (
|
|
1311
|
+
features: readonly RenderFeature<unknown>[],
|
|
1312
|
+
): readonly { readonly identity: string; readonly source: string }[] => {
|
|
1313
|
+
const entries = new Map<string, { readonly identity: string; readonly source: string }>();
|
|
1314
|
+
for (const feature of features) {
|
|
1315
|
+
for (const entry of feature.requiredFullscreenPostProcesses ?? []) {
|
|
1316
|
+
const existing = entries.get(entry.identity);
|
|
1317
|
+
if (existing !== undefined && existing.source !== entry.source) {
|
|
1318
|
+
throw new RhiError({
|
|
1319
|
+
code: 'internal-error',
|
|
1320
|
+
expected: `fullscreen identity '${entry.identity}' has one source across render features`,
|
|
1321
|
+
hint: 'rename the conflicting fullscreen identity or make its WGSL source identical',
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
entries.set(entry.identity, entry);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
return Object.freeze([...entries.values()]);
|
|
1328
|
+
};
|
|
1329
|
+
let requiredFullscreenPostProcesses = collectRequiredFullscreenPostProcesses(
|
|
1330
|
+
featureHostResult.value.features,
|
|
1331
|
+
);
|
|
1177
1332
|
// Lazy ShaderCatalog instance (plan-strategy section S-10 / D-R10 / OQ-5
|
|
1178
1333
|
// close): constructed on first access; subsequent accesses return the
|
|
1179
1334
|
// same instance.
|
|
@@ -1216,6 +1371,28 @@ async function makeWebGPURenderer(
|
|
|
1216
1371
|
}
|
|
1217
1372
|
return sharedImmediateShaderModuleAdapter;
|
|
1218
1373
|
};
|
|
1374
|
+
const prewarmFullscreenFeatureModules = async (
|
|
1375
|
+
feature: RenderFeature<unknown>,
|
|
1376
|
+
): Promise<Result<void, RhiError>> => {
|
|
1377
|
+
for (const postProcess of feature.requiredFullscreenPostProcesses ?? []) {
|
|
1378
|
+
const label = postProcessShaderModuleLabel(postProcess.identity, postProcess.source);
|
|
1379
|
+
const shaderResult = internals.pack.createShaderModule
|
|
1380
|
+
? await internals.pack.createShaderModule(internals.device, {
|
|
1381
|
+
code: postProcess.source,
|
|
1382
|
+
label,
|
|
1383
|
+
})
|
|
1384
|
+
: await invokeDeviceCreateShaderModule(internals.device, {
|
|
1385
|
+
code: postProcess.source,
|
|
1386
|
+
label,
|
|
1387
|
+
});
|
|
1388
|
+
if (!shaderResult.ok) {
|
|
1389
|
+
internals.errorRegistry.fire(shaderResult.error);
|
|
1390
|
+
return err(shaderResult.error);
|
|
1391
|
+
}
|
|
1392
|
+
getShaderModuleAdapter().seedModule(label, shaderResult.value);
|
|
1393
|
+
}
|
|
1394
|
+
return ok(undefined);
|
|
1395
|
+
};
|
|
1219
1396
|
const getShader = (): ShaderCatalog => {
|
|
1220
1397
|
if (shaderInstance === null) {
|
|
1221
1398
|
// feat-20260608-create-app-param-surface-trim / M2 / D-2 q5-A:
|
|
@@ -1280,6 +1457,56 @@ async function makeWebGPURenderer(
|
|
|
1280
1457
|
// lives in a standalone store; `assets` keeps the CPU POD registry only.
|
|
1281
1458
|
const gpuStore = new GpuResidencyCache();
|
|
1282
1459
|
let activeDeviceScope = DeviceScope.create(0, 'renderer');
|
|
1460
|
+
const gpuPassTimingOptions = internals.options?.gpuPassTiming;
|
|
1461
|
+
const timingRetentionFrames =
|
|
1462
|
+
gpuPassTimingOptions?.retentionFrames !== undefined &&
|
|
1463
|
+
Number.isInteger(gpuPassTimingOptions.retentionFrames) &&
|
|
1464
|
+
gpuPassTimingOptions.retentionFrames >= 1 &&
|
|
1465
|
+
gpuPassTimingOptions.retentionFrames <= 8
|
|
1466
|
+
? gpuPassTimingOptions.retentionFrames
|
|
1467
|
+
: DEFAULT_GPU_PASS_TIMING_OPTIONS.retentionFrames;
|
|
1468
|
+
let gpuPassTimingSession: GpuPassTimingSession | undefined;
|
|
1469
|
+
let gpuPassTimingUnavailable: GpuPassTimingObservation | undefined;
|
|
1470
|
+
const timingObservationStore =
|
|
1471
|
+
gpuPassTimingOptions === undefined
|
|
1472
|
+
? undefined
|
|
1473
|
+
: createGpuPassTimingObservationStore({
|
|
1474
|
+
retentionFrames: timingRetentionFrames,
|
|
1475
|
+
currentDeviceGeneration: () => activeDeviceScope.generation,
|
|
1476
|
+
});
|
|
1477
|
+
const retireGpuPassTimingSession = (): void => {
|
|
1478
|
+
const retired = gpuPassTimingSession;
|
|
1479
|
+
gpuPassTimingSession = undefined;
|
|
1480
|
+
retired?.dispose();
|
|
1481
|
+
};
|
|
1482
|
+
const failedGpuPassTimingObservation = (error: GpuPassTimingReason): GpuPassTimingObservation => {
|
|
1483
|
+
const latestKnownGood = timingObservationStore?.inspect().latestKnownGood;
|
|
1484
|
+
return {
|
|
1485
|
+
status: 'failed',
|
|
1486
|
+
error,
|
|
1487
|
+
...(latestKnownGood === undefined ? {} : { latestKnownGood }),
|
|
1488
|
+
};
|
|
1489
|
+
};
|
|
1490
|
+
const establishGpuPassTimingSession = (): void => {
|
|
1491
|
+
retireGpuPassTimingSession();
|
|
1492
|
+
gpuPassTimingUnavailable = undefined;
|
|
1493
|
+
const timingOptions = gpuPassTimingOptions;
|
|
1494
|
+
if (timingOptions === undefined) return;
|
|
1495
|
+
const created = createGpuPassTimingSession(internals.device, timingOptions);
|
|
1496
|
+
if (created.ok) {
|
|
1497
|
+
gpuPassTimingSession = created.value;
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
gpuPassTimingUnavailable = {
|
|
1501
|
+
status: 'unavailable',
|
|
1502
|
+
reason: created.error,
|
|
1503
|
+
capability: {
|
|
1504
|
+
timestampQuery: internals.device.caps.timestampQuery,
|
|
1505
|
+
timestampPeriodNanoseconds: internals.device.caps.timestampPeriodNanoseconds,
|
|
1506
|
+
},
|
|
1507
|
+
};
|
|
1508
|
+
};
|
|
1509
|
+
establishGpuPassTimingSession();
|
|
1283
1510
|
// feat-20260623-world-space-video-asset M4 / w16 (D-3): transient per-frame
|
|
1284
1511
|
// video texture store, fully independent of gpuStore (AC-08). Configured with
|
|
1285
1512
|
// the device alongside gpuStore below; threaded into the record stage via the
|
|
@@ -1413,7 +1640,10 @@ async function makeWebGPURenderer(
|
|
|
1413
1640
|
// (the seeding closure captures this Map by reference; ordering is safe
|
|
1414
1641
|
// because the prewarm step runs strictly before the first frame's
|
|
1415
1642
|
// `getMaterialShaderPipeline` lookup that would consume it).
|
|
1416
|
-
const materialShaderPipelineCache = new Map<
|
|
1643
|
+
const materialShaderPipelineCache = new Map<
|
|
1644
|
+
string,
|
|
1645
|
+
{ readonly pipeline: RenderPipeline; readonly group2Contract: PipelineGroup2Contract }
|
|
1646
|
+
>();
|
|
1417
1647
|
const materialShaderManifestEntryCache = new Map<string, MaterialShaderManifestEntry>();
|
|
1418
1648
|
// Variant resolution only depends on the requested axes, device capability
|
|
1419
1649
|
// axes, and the manifest entry that owns the declarations. Cache it by the
|
|
@@ -1628,6 +1858,18 @@ async function makeWebGPURenderer(
|
|
|
1628
1858
|
}
|
|
1629
1859
|
if (pipelineState === null) return null;
|
|
1630
1860
|
const paramSchema = lookup.value.paramSchema;
|
|
1861
|
+
// Reuse the shared BGL only when the complete derived resource shape is a
|
|
1862
|
+
// canonical prefix. Field names alone are not a layout contract: a
|
|
1863
|
+
// texture3d/array, comparison sampler, or storage buffer must never be
|
|
1864
|
+
// paired with the PBR texture2d/filtering descriptor by accident.
|
|
1865
|
+
if (isSharedMaterialUserRegionCompatible(paramSchema)) {
|
|
1866
|
+
perShaderMaterialLayoutCache.set(materialShaderId, {
|
|
1867
|
+
source: lookup.value.source,
|
|
1868
|
+
paramSchema,
|
|
1869
|
+
layout: null,
|
|
1870
|
+
});
|
|
1871
|
+
return null;
|
|
1872
|
+
}
|
|
1631
1873
|
const spec: PipelineSpec = {
|
|
1632
1874
|
shader: { id: materialShaderId, passKind: 'forward', variantSet: undefined },
|
|
1633
1875
|
attachments: { colorFormats: [], depthFormat: undefined, sampleCount: 1 },
|
|
@@ -1674,6 +1916,8 @@ async function makeWebGPURenderer(
|
|
|
1674
1916
|
[],
|
|
1675
1917
|
internals.device.caps.backendKind,
|
|
1676
1918
|
internals.device.caps.storageBuffer,
|
|
1919
|
+
(internals.device.limits.maxSampledTexturesPerShaderStage ?? 0) >=
|
|
1920
|
+
STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES,
|
|
1677
1921
|
);
|
|
1678
1922
|
}
|
|
1679
1923
|
const variants = manifestEntry.variants;
|
|
@@ -1682,13 +1926,17 @@ async function makeWebGPURenderer(
|
|
|
1682
1926
|
byRequest = new Map();
|
|
1683
1927
|
materialShaderVariantResolutionCache.set(variants, byRequest);
|
|
1684
1928
|
}
|
|
1685
|
-
const
|
|
1929
|
+
const projectorAvailable =
|
|
1930
|
+
(internals.device.limits.maxSampledTexturesPerShaderStage ?? 0) >=
|
|
1931
|
+
STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES;
|
|
1932
|
+
const cacheKey = `${requestedVariantSet ?? '\u0000'}|${internals.device.caps.backendKind}|${internals.device.caps.storageBuffer ? '1' : '0'}|${projectorAvailable ? '1' : '0'}`;
|
|
1686
1933
|
if (byRequest.has(cacheKey)) return byRequest.get(cacheKey);
|
|
1687
1934
|
const resolved = resolveMaterialShaderVariantSet(
|
|
1688
1935
|
requestedVariantSet,
|
|
1689
1936
|
variants,
|
|
1690
1937
|
internals.device.caps.backendKind,
|
|
1691
1938
|
internals.device.caps.storageBuffer,
|
|
1939
|
+
projectorAvailable,
|
|
1692
1940
|
);
|
|
1693
1941
|
byRequest.set(cacheKey, resolved);
|
|
1694
1942
|
return resolved;
|
|
@@ -1714,6 +1962,7 @@ async function makeWebGPURenderer(
|
|
|
1714
1962
|
// `internals.device` / `internals.pack` are read live, so a rebuild after a
|
|
1715
1963
|
// device swap compiles against the new device.
|
|
1716
1964
|
let tonemapRegistered = false;
|
|
1965
|
+
let fxaaRegistered = false;
|
|
1717
1966
|
let temporalPostProcessesRegistered = false;
|
|
1718
1967
|
const buildPipeline = (scope: DeviceScope = activeDeviceScope): Promise<PipelineState> =>
|
|
1719
1968
|
buildReadyWebGPU(
|
|
@@ -1724,6 +1973,7 @@ async function makeWebGPURenderer(
|
|
|
1724
1973
|
internals.pack.createShaderModule,
|
|
1725
1974
|
internals.errorRegistry,
|
|
1726
1975
|
requiredMaterialShaders,
|
|
1976
|
+
requiredFullscreenPostProcesses,
|
|
1727
1977
|
// feat-20260608-mesh-ssbo-dynamic-grow-l1-lift-1024-entity-cap M2 /
|
|
1728
1978
|
// T-M2-05 + M3 / T-M3-04: callback set by buildReadyWebGPU once
|
|
1729
1979
|
// meshSsboController is wired. Both the grow hook AND the read-only
|
|
@@ -1743,9 +1993,12 @@ async function makeWebGPURenderer(
|
|
|
1743
1993
|
// SPEC_CONST prewarm. Closure captures the Map declared just above so
|
|
1744
1994
|
// the URP-variant prewarmed PSOs become visible to the URP record path
|
|
1745
1995
|
// immediately (no 1-frame async-compile skip-draw window).
|
|
1746
|
-
(key, pso) => {
|
|
1996
|
+
(key, pso, group2Contract) => {
|
|
1747
1997
|
if (!materialShaderPipelineCache.has(key)) {
|
|
1748
|
-
materialShaderPipelineCache.set(key,
|
|
1998
|
+
materialShaderPipelineCache.set(key, {
|
|
1999
|
+
pipeline: pso,
|
|
2000
|
+
group2Contract,
|
|
2001
|
+
});
|
|
1749
2002
|
}
|
|
1750
2003
|
},
|
|
1751
2004
|
// feat-20260621 M-A3 (D-5): register the built-in tonemap on the unified
|
|
@@ -1764,6 +2017,19 @@ async function makeWebGPURenderer(
|
|
|
1764
2017
|
});
|
|
1765
2018
|
tonemapRegistered = true;
|
|
1766
2019
|
},
|
|
2020
|
+
// FXAA is recorded by the special graph primitive, but its final-output
|
|
2021
|
+
// dither switch still uses the renderer-owned post-process params UBO.
|
|
2022
|
+
// Register the declaration through the same lifecycle so recovery
|
|
2023
|
+
// recreates the buffer on the replacement device.
|
|
2024
|
+
(source: string) => {
|
|
2025
|
+
if (fxaaRegistered) return;
|
|
2026
|
+
renderSystem.registerBuiltinPostProcess(FXAA_POST_PROCESS_ID, {
|
|
2027
|
+
source,
|
|
2028
|
+
params: { byteSize: 16, defaultValue: new Uint8Array(16) },
|
|
2029
|
+
reads: ['ldrColor'],
|
|
2030
|
+
});
|
|
2031
|
+
fxaaRegistered = true;
|
|
2032
|
+
},
|
|
1767
2033
|
(entries: { readonly motionBlur?: string; readonly taaResolve?: string }) => {
|
|
1768
2034
|
if (temporalPostProcessesRegistered) return;
|
|
1769
2035
|
if (entries.motionBlur !== undefined) {
|
|
@@ -1782,15 +2048,23 @@ async function makeWebGPURenderer(
|
|
|
1782
2048
|
}
|
|
1783
2049
|
temporalPostProcessesRegistered = true;
|
|
1784
2050
|
},
|
|
2051
|
+
(sources: VolumetricFogShaderSources) => {
|
|
2052
|
+
internals.volumetricFogShaders = sources;
|
|
2053
|
+
},
|
|
1785
2054
|
);
|
|
2055
|
+
markRendererBootstrapStage('renderer-initialization-start');
|
|
1786
2056
|
const ready: Promise<Result<void, RhiError>> = buildPipeline().then(
|
|
1787
2057
|
(state): Result<void, RhiError> => {
|
|
1788
2058
|
pipelineState = state;
|
|
1789
2059
|
readySettled = true;
|
|
2060
|
+
markRendererBootstrapStage('renderer-initialized');
|
|
1790
2061
|
return ok(undefined);
|
|
1791
2062
|
},
|
|
1792
2063
|
(e: unknown): Result<void, RhiError> => {
|
|
1793
2064
|
readySettled = true;
|
|
2065
|
+
markRendererBootstrapStage('renderer-initialization-failed', {
|
|
2066
|
+
message: e instanceof Error ? e.message : String(e),
|
|
2067
|
+
});
|
|
1794
2068
|
if (e instanceof RhiError) return err(e);
|
|
1795
2069
|
const message = e instanceof Error ? e.message : String(e);
|
|
1796
2070
|
return err(
|
|
@@ -1827,6 +2101,9 @@ async function makeWebGPURenderer(
|
|
|
1827
2101
|
pbrManifestEntry,
|
|
1828
2102
|
internals.device.caps.storageBuffer,
|
|
1829
2103
|
variantSet,
|
|
2104
|
+
directionalPcssCapable(internals.device.caps.backendKind),
|
|
2105
|
+
(internals.device.limits.maxSampledTexturesPerShaderStage ?? 0) >=
|
|
2106
|
+
STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES,
|
|
1830
2107
|
);
|
|
1831
2108
|
if (noColorVariant !== undefined && noColorVariant.composedWgsl !== standardEntry.wgsl) {
|
|
1832
2109
|
return { ...standardEntry, wgsl: noColorVariant.composedWgsl };
|
|
@@ -1842,10 +2119,10 @@ async function makeWebGPURenderer(
|
|
|
1842
2119
|
// paths (D-3). Null when manifest is empty (Camera-only path).
|
|
1843
2120
|
//
|
|
1844
2121
|
// feat-20260609-hdrp-cluster-fragment-ggx M4.5 / w37 (D-10 option A):
|
|
1845
|
-
// `variantSet`
|
|
1846
|
-
//
|
|
1847
|
-
//
|
|
1848
|
-
//
|
|
2122
|
+
// `variantSet` identifies the shader artifact, while the resolved group(2)
|
|
2123
|
+
// contract selects the matching Standard layout. A missing cluster layout
|
|
2124
|
+
// is an explicit unavailable result; it must not silently bind the mesh-only
|
|
2125
|
+
// layout to a clustered shader.
|
|
1849
2126
|
const buildPipelineContext = (
|
|
1850
2127
|
variantSet?: string,
|
|
1851
2128
|
materialShaderId?: string,
|
|
@@ -1856,40 +2133,31 @@ async function makeWebGPURenderer(
|
|
|
1856
2133
|
// consume. A caller passing a real `MeshAsset.attributes` produces the
|
|
1857
2134
|
// exact 6-attribute / 72-byte layout the skin shader expects; passing
|
|
1858
2135
|
// undefined falls into the synthetic 6-key sentinel below (the skin
|
|
1859
|
-
// path is fully determined by
|
|
1860
|
-
// signal `deriveVertexBufferLayout` reads
|
|
2136
|
+
// path is fully determined by the resolved group-2 contract, so
|
|
2137
|
+
// key-presence is the only signal `deriveVertexBufferLayout` reads).
|
|
1861
2138
|
meshAttributes?: VertexAttributeMap,
|
|
1862
2139
|
vertexLayout?: string,
|
|
1863
2140
|
vertexLayoutProjection?: VertexLayoutProjection,
|
|
1864
2141
|
shaderModuleMode?: RenderFeatureShaderModuleMode,
|
|
2142
|
+
group2Contract: PipelineGroup2Contract = 'mesh',
|
|
1865
2143
|
layoutKindOverride?: LayoutKind,
|
|
1866
2144
|
) => {
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
const layoutKind: LayoutKind | undefined =
|
|
1877
|
-
layoutKindOverride ??
|
|
1878
|
-
(materialShaderId === SKIN_MATERIAL_SHADER_ID ||
|
|
1879
|
-
isSkinnedShadowCasterVariant(materialShaderId, variantSet)
|
|
1880
|
-
? 'pbr-skin'
|
|
1881
|
-
: materialShaderId === 'forgeax::sprite' || materialShaderId === 'forgeax::sprite-lit'
|
|
1882
|
-
? 'sprite-urp'
|
|
1883
|
-
: materialShaderId === 'forgeax::default-unlit'
|
|
1884
|
-
? 'unlit-urp'
|
|
1885
|
-
: undefined);
|
|
2145
|
+
const contractLayoutKind: LayoutKind =
|
|
2146
|
+
group2Contract === 'cluster'
|
|
2147
|
+
? 'hdrp-pbr'
|
|
2148
|
+
: group2Contract === 'skin-cluster'
|
|
2149
|
+
? 'hdrp-skin'
|
|
2150
|
+
: group2Contract === 'skin'
|
|
2151
|
+
? 'pbr-skin'
|
|
2152
|
+
: 'pbr';
|
|
2153
|
+
const resolvedLayoutKind = layoutKindOverride ?? contractLayoutKind;
|
|
1886
2154
|
const usesSharedBootMaterialLayout =
|
|
1887
2155
|
materialShaderId !== undefined && isEngineOwnedMaterialShader(materialShaderId);
|
|
1888
2156
|
// Built-in PBR/unlit/skin layouts are assembled at boot. Re-deriving a
|
|
1889
2157
|
// per-shader layout from the expanded PBR schema would bypass the HDRP
|
|
1890
2158
|
// group(2) selector and bind the URP mesh BGL to the cluster pipeline.
|
|
1891
2159
|
const perShaderLayout =
|
|
1892
|
-
!usesSharedBootMaterialLayout &&
|
|
2160
|
+
!usesSharedBootMaterialLayout && materialShaderId !== undefined
|
|
1893
2161
|
? getOrBuildPerShaderMaterialLayout(materialShaderId)
|
|
1894
2162
|
: null;
|
|
1895
2163
|
// feat-20260621-learn-render-5-5-parallax M2 / w6 (D-1): an authored
|
|
@@ -1941,7 +2209,7 @@ async function makeWebGPURenderer(
|
|
|
1941
2209
|
? preparedMaterialLayout
|
|
1942
2210
|
: perShaderLayout !== null
|
|
1943
2211
|
? perShaderLayout.pipelineLayout
|
|
1944
|
-
: selectPipelineLayoutForVariant(pipelineState, variantSet,
|
|
2212
|
+
: selectPipelineLayoutForVariant(pipelineState, variantSet, resolvedLayoutKind);
|
|
1945
2213
|
if (pipelineLayout === null) return null;
|
|
1946
2214
|
// feat-20260611-fox-skinning-vertex-attribute-chain M4 / w16 (D-4):
|
|
1947
2215
|
// pbr-skin -> deriveVertexBufferLayout (single SSOT for skin/non-skin
|
|
@@ -1992,7 +2260,7 @@ async function makeWebGPURenderer(
|
|
|
1992
2260
|
: undefined,
|
|
1993
2261
|
),
|
|
1994
2262
|
)
|
|
1995
|
-
:
|
|
2263
|
+
: materialShaderId === SKIN_MATERIAL_SHADER_ID
|
|
1996
2264
|
? toGpuVertexBufferLayouts(
|
|
1997
2265
|
deriveVertexBufferLayout(
|
|
1998
2266
|
meshAttributes ??
|
|
@@ -2038,7 +2306,7 @@ async function makeWebGPURenderer(
|
|
|
2038
2306
|
: getShaderModuleAdapter(),
|
|
2039
2307
|
pipelineLayout,
|
|
2040
2308
|
vertexBuffers,
|
|
2041
|
-
...(
|
|
2309
|
+
...(resolvedLayoutKind === 'pbr-skin' ? { layoutKind: 'pbr-skin' as const } : {}),
|
|
2042
2310
|
};
|
|
2043
2311
|
};
|
|
2044
2312
|
/**
|
|
@@ -2062,7 +2330,7 @@ async function makeWebGPURenderer(
|
|
|
2062
2330
|
passKind: PassKind = 'forward',
|
|
2063
2331
|
// bug-20260611-skin-pipeline-layout: thread materialShaderId so
|
|
2064
2332
|
// buildPipelineContext can derive `LayoutKind === 'pbr-skin'` and pick
|
|
2065
|
-
// the
|
|
2333
|
+
// the 3-entry mesh-array BGL chain.
|
|
2066
2334
|
materialShaderId?: string,
|
|
2067
2335
|
// feat-20260611-fox-skinning-vertex-attribute-chain M4 / w16 (D-4):
|
|
2068
2336
|
// pass the per-mesh `VertexAttributeMap` so the pbr-skin path's
|
|
@@ -2098,7 +2366,11 @@ async function makeWebGPURenderer(
|
|
|
2098
2366
|
vertexLayoutProjection?: VertexLayoutProjection,
|
|
2099
2367
|
shaderModuleMode?: RenderFeatureShaderModuleMode,
|
|
2100
2368
|
layoutKindOverride?: LayoutKind,
|
|
2101
|
-
):
|
|
2369
|
+
): {
|
|
2370
|
+
readonly pipeline: RenderPipeline;
|
|
2371
|
+
readonly group2Contract: PipelineGroup2Contract;
|
|
2372
|
+
} | null => {
|
|
2373
|
+
const group2Contract = resolvePipelineGroup2Contract(entry.source);
|
|
2102
2374
|
const ctx = buildPipelineContext(
|
|
2103
2375
|
variantSet,
|
|
2104
2376
|
materialShaderId,
|
|
@@ -2106,6 +2378,7 @@ async function makeWebGPURenderer(
|
|
|
2106
2378
|
vertexLayout,
|
|
2107
2379
|
vertexLayoutProjection,
|
|
2108
2380
|
shaderModuleMode,
|
|
2381
|
+
group2Contract,
|
|
2109
2382
|
layoutKindOverride,
|
|
2110
2383
|
);
|
|
2111
2384
|
if (ctx === null) return null;
|
|
@@ -2165,10 +2438,14 @@ async function makeWebGPURenderer(
|
|
|
2165
2438
|
}
|
|
2166
2439
|
return null;
|
|
2167
2440
|
}
|
|
2168
|
-
|
|
2169
|
-
|
|
2441
|
+
const pipelineEntry = {
|
|
2442
|
+
pipeline: built.value,
|
|
2443
|
+
group2Contract,
|
|
2444
|
+
} as const;
|
|
2445
|
+
materialShaderPipelineCache.set(cacheKey, pipelineEntry);
|
|
2446
|
+
return pipelineEntry;
|
|
2170
2447
|
};
|
|
2171
|
-
const
|
|
2448
|
+
const resolveMaterialShaderPipelineEntry = (
|
|
2172
2449
|
materialShaderId: string,
|
|
2173
2450
|
isHdr: boolean,
|
|
2174
2451
|
renderState?: MaterialRenderState,
|
|
@@ -2215,7 +2492,10 @@ async function makeWebGPURenderer(
|
|
|
2215
2492
|
vertexLayoutProjection?: VertexLayoutProjection,
|
|
2216
2493
|
shaderModuleMode: RenderFeatureShaderModuleMode = 'validated',
|
|
2217
2494
|
layoutKindOverride?: LayoutKind,
|
|
2218
|
-
):
|
|
2495
|
+
): {
|
|
2496
|
+
readonly pipeline: RenderPipeline;
|
|
2497
|
+
readonly group2Contract: PipelineGroup2Contract;
|
|
2498
|
+
} | null => {
|
|
2219
2499
|
// WebGL2 is a downlevel backend even when the record stage asks for the
|
|
2220
2500
|
// native-capability variant string. Resolve the variant axes at this
|
|
2221
2501
|
// shared PSO seam so the shader source, BGL layout, and cache key agree on
|
|
@@ -2335,6 +2615,11 @@ async function makeWebGPURenderer(
|
|
|
2335
2615
|
if (pipelineState === null) return null;
|
|
2336
2616
|
const lookup = getShader().findMaterialArtifact(materialShaderId);
|
|
2337
2617
|
if (!lookup.ok) {
|
|
2618
|
+
// An explicit variant request is an exact artifact contract. There is
|
|
2619
|
+
// no safe boot/PBR substitute: a substitute can pair the requested
|
|
2620
|
+
// source identity with a different group(2) ABI and only fail at GPU
|
|
2621
|
+
// validation. Let the caller retry after the manifest/artifact is fixed.
|
|
2622
|
+
if (effectiveVariantSet !== undefined) return null;
|
|
2338
2623
|
if (shouldDeferMissingPreparedMaterialShader(vertexLayout)) {
|
|
2339
2624
|
// Prepared VFX draws bind only their feature-owned groups. The
|
|
2340
2625
|
// built-in PBR fallback declares mesh/instance groups 2/3 and would
|
|
@@ -2455,24 +2740,26 @@ async function makeWebGPURenderer(
|
|
|
2455
2740
|
// Use `!== undefined` so the empty-string case enters the lookup.
|
|
2456
2741
|
if (effectiveVariantSet !== undefined) {
|
|
2457
2742
|
const registry = getShader();
|
|
2743
|
+
let matchedVariant = false;
|
|
2458
2744
|
for (const msEntry of registry.materialShaderManifestEntries()) {
|
|
2459
2745
|
if (msEntry.identifier === materialShaderId) {
|
|
2460
2746
|
const variant = findVariantByKey(msEntry, effectiveVariantSet);
|
|
2461
|
-
if (variant)
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
}
|
|
2747
|
+
if (variant === undefined) return null;
|
|
2748
|
+
matchedVariant = true;
|
|
2749
|
+
// M3 / w12-w13: variant substitution carries the same source +
|
|
2750
|
+
// paramSchema as the boot-registered entry; the binding layout
|
|
2751
|
+
// is no longer carried on MaterialShaderEntry (deleted in
|
|
2752
|
+
// w13) — buildPbrPipelineLayouts is the BGL SSOT and reads
|
|
2753
|
+
// derive(paramSchema).bglEntries on demand.
|
|
2754
|
+
shaderEntry = {
|
|
2755
|
+
source: variant.composedWgsl,
|
|
2756
|
+
paramSchema: lookup.value.paramSchema,
|
|
2757
|
+
paramSchemaProjection: lookup.value.paramSchemaProjection,
|
|
2758
|
+
};
|
|
2473
2759
|
break;
|
|
2474
2760
|
}
|
|
2475
2761
|
}
|
|
2762
|
+
if (!matchedVariant) return null;
|
|
2476
2763
|
}
|
|
2477
2764
|
// feat-20260609 M4 / R3-fixup: append `-${passKind}` to the PSO label so
|
|
2478
2765
|
// GPU debug captures (and the shadow-caster branch in the builder)
|
|
@@ -2513,7 +2800,7 @@ async function makeWebGPURenderer(
|
|
|
2513
2800
|
passKind,
|
|
2514
2801
|
// bug-20260611-skin-pipeline-layout: thread the registered materialShaderId
|
|
2515
2802
|
// so buildPipelineContext can resolve LayoutKind='pbr-skin' for the skin
|
|
2516
|
-
// shader (
|
|
2803
|
+
// shader (3-entry mesh-array BGL).
|
|
2517
2804
|
materialShaderId,
|
|
2518
2805
|
// feat-20260611-fox-skinning-vertex-attribute-chain M4 / w16 (D-4):
|
|
2519
2806
|
// forward meshAttributes so the pbr-skin path's vertex buffer layout
|
|
@@ -2530,6 +2817,12 @@ async function makeWebGPURenderer(
|
|
|
2530
2817
|
layoutKindOverride,
|
|
2531
2818
|
);
|
|
2532
2819
|
};
|
|
2820
|
+
const getMaterialShaderPipelineEntry = (
|
|
2821
|
+
...args: Parameters<typeof resolveMaterialShaderPipelineEntry>
|
|
2822
|
+
) => resolveMaterialShaderPipelineEntry(...args);
|
|
2823
|
+
const getMaterialShaderPipeline = (
|
|
2824
|
+
...args: Parameters<typeof resolveMaterialShaderPipelineEntry>
|
|
2825
|
+
): RenderPipeline | null => getMaterialShaderPipelineEntry(...args)?.pipeline ?? null;
|
|
2533
2826
|
const getParamSchema = (materialShaderId: string) => {
|
|
2534
2827
|
const lookup = getShader().findMaterialArtifact(materialShaderId);
|
|
2535
2828
|
return lookup.ok ? lookup.value.paramSchema : undefined;
|
|
@@ -2650,6 +2943,10 @@ async function makeWebGPURenderer(
|
|
|
2650
2943
|
shaderModuleFactory: {
|
|
2651
2944
|
createShaderModule: (descriptor) => getShaderModuleAdapter().createShaderModule(descriptor),
|
|
2652
2945
|
},
|
|
2946
|
+
createShaderModule: (device, descriptor) =>
|
|
2947
|
+
internals.pack.createShaderModule === undefined
|
|
2948
|
+
? Promise.resolve(invokeDeviceCreateShaderModule(device, descriptor))
|
|
2949
|
+
: internals.pack.createShaderModule(device, descriptor),
|
|
2653
2950
|
immediateShaderModuleFactory: {
|
|
2654
2951
|
createShaderModule: (descriptor) =>
|
|
2655
2952
|
getImmediateShaderModuleAdapter().createShaderModule(descriptor),
|
|
@@ -2686,6 +2983,7 @@ async function makeWebGPURenderer(
|
|
|
2686
2983
|
errorRegistry: internals.errorRegistry,
|
|
2687
2984
|
healthRegistry: internals.healthRegistry,
|
|
2688
2985
|
getMaterialShaderPipeline,
|
|
2986
|
+
getMaterialShaderPipelineEntry,
|
|
2689
2987
|
getMaterialShaderBindingContract: getCachedMaterialShaderBindingContract,
|
|
2690
2988
|
getParamSchema,
|
|
2691
2989
|
getMaterialBindGroupLayout,
|
|
@@ -2705,7 +3003,93 @@ async function makeWebGPURenderer(
|
|
|
2705
3003
|
get meshSsboState() {
|
|
2706
3004
|
return internals.meshSsboState;
|
|
2707
3005
|
},
|
|
3006
|
+
get gpuPassTimingSession() {
|
|
3007
|
+
return (
|
|
3008
|
+
internals as WebGPURendererInternals & {
|
|
3009
|
+
gpuPassTimingSession?: GpuPassTimingSession | undefined;
|
|
3010
|
+
}
|
|
3011
|
+
).gpuPassTimingSession;
|
|
3012
|
+
},
|
|
3013
|
+
set gpuPassTimingSession(session: GpuPassTimingSession | undefined) {
|
|
3014
|
+
(
|
|
3015
|
+
internals as WebGPURendererInternals & {
|
|
3016
|
+
gpuPassTimingSession?: GpuPassTimingSession | undefined;
|
|
3017
|
+
}
|
|
3018
|
+
).gpuPassTimingSession = session;
|
|
3019
|
+
},
|
|
3020
|
+
get gpuPassTimingCapture() {
|
|
3021
|
+
return (
|
|
3022
|
+
internals as WebGPURendererInternals & {
|
|
3023
|
+
gpuPassTimingCapture?: GpuPassTimingCapture | undefined;
|
|
3024
|
+
}
|
|
3025
|
+
).gpuPassTimingCapture;
|
|
3026
|
+
},
|
|
3027
|
+
set gpuPassTimingCapture(capture: GpuPassTimingCapture | undefined) {
|
|
3028
|
+
(
|
|
3029
|
+
internals as WebGPURendererInternals & {
|
|
3030
|
+
gpuPassTimingCapture?: GpuPassTimingCapture | undefined;
|
|
3031
|
+
}
|
|
3032
|
+
).gpuPassTimingCapture = capture;
|
|
3033
|
+
},
|
|
3034
|
+
get gpuPassTimingSubmittedWork() {
|
|
3035
|
+
return (
|
|
3036
|
+
internals as WebGPURendererInternals & {
|
|
3037
|
+
gpuPassTimingSubmittedWork?: Promise<void> | undefined;
|
|
3038
|
+
}
|
|
3039
|
+
).gpuPassTimingSubmittedWork;
|
|
3040
|
+
},
|
|
3041
|
+
set gpuPassTimingSubmittedWork(completion: Promise<void> | undefined) {
|
|
3042
|
+
(
|
|
3043
|
+
internals as WebGPURendererInternals & {
|
|
3044
|
+
gpuPassTimingSubmittedWork?: Promise<void> | undefined;
|
|
3045
|
+
}
|
|
3046
|
+
).gpuPassTimingSubmittedWork = completion;
|
|
3047
|
+
},
|
|
3048
|
+
get gpuPassTimingFrameIdentity() {
|
|
3049
|
+
return (
|
|
3050
|
+
internals as WebGPURendererInternals & {
|
|
3051
|
+
gpuPassTimingFrameIdentity?:
|
|
3052
|
+
| import('../record/gpu-pass-timing/session.js').GpuPassTimingFrameIdentity
|
|
3053
|
+
| undefined;
|
|
3054
|
+
}
|
|
3055
|
+
).gpuPassTimingFrameIdentity;
|
|
3056
|
+
},
|
|
3057
|
+
set gpuPassTimingFrameIdentity(identity:
|
|
3058
|
+
| import('../record/gpu-pass-timing/session.js').GpuPassTimingFrameIdentity
|
|
3059
|
+
| undefined) {
|
|
3060
|
+
(
|
|
3061
|
+
internals as WebGPURendererInternals & {
|
|
3062
|
+
gpuPassTimingFrameIdentity?:
|
|
3063
|
+
| import('../record/gpu-pass-timing/session.js').GpuPassTimingFrameIdentity
|
|
3064
|
+
| undefined;
|
|
3065
|
+
}
|
|
3066
|
+
).gpuPassTimingFrameIdentity = identity;
|
|
3067
|
+
},
|
|
3068
|
+
get gpuPassTimingBeginReason() {
|
|
3069
|
+
return (
|
|
3070
|
+
internals as WebGPURendererInternals & {
|
|
3071
|
+
gpuPassTimingBeginReason?: GpuPassTimingReason | undefined;
|
|
3072
|
+
}
|
|
3073
|
+
).gpuPassTimingBeginReason;
|
|
3074
|
+
},
|
|
3075
|
+
set gpuPassTimingBeginReason(reason: GpuPassTimingReason | undefined) {
|
|
3076
|
+
(
|
|
3077
|
+
internals as WebGPURendererInternals & {
|
|
3078
|
+
gpuPassTimingBeginReason?: GpuPassTimingReason | undefined;
|
|
3079
|
+
}
|
|
3080
|
+
).gpuPassTimingBeginReason = reason;
|
|
3081
|
+
},
|
|
3082
|
+
getRenderTargetPhysical: (target) => renderTargetHost.getPhysicalTarget(target),
|
|
3083
|
+
resolveRenderTargetTextureSource: (source) =>
|
|
3084
|
+
renderTargetHost.resolveRenderTargetTextureSource(source),
|
|
3085
|
+
encodeRenderTargetReadbacks: (encoder, faces) =>
|
|
3086
|
+
renderTargetHost.encodePendingReadbacks(encoder, faces),
|
|
3087
|
+
get volumetricFogShaders() {
|
|
3088
|
+
return internals.volumetricFogShaders;
|
|
3089
|
+
},
|
|
2708
3090
|
});
|
|
3091
|
+
attachGpuPassTimingSession(internals, gpuPassTimingSession);
|
|
3092
|
+
renderSystemForTargetPromotion = renderSystem;
|
|
2709
3093
|
if (
|
|
2710
3094
|
internals.device.caps.backendKind === 'null' &&
|
|
2711
3095
|
Array.from(getShader().entries()).length === 0
|
|
@@ -2730,7 +3114,12 @@ async function makeWebGPURenderer(
|
|
|
2730
3114
|
const transformReleases = new Map<World, () => void>();
|
|
2731
3115
|
const attachmentOwner = {};
|
|
2732
3116
|
let frameId = 0;
|
|
2733
|
-
|
|
3117
|
+
// Receipts remain valid for observation until their device generation is
|
|
3118
|
+
// retired. Keeping a renderer-owned registry avoids treating a concurrent
|
|
3119
|
+
// RAF submission as invalidation of an otherwise current receipt.
|
|
3120
|
+
const currentReceipts = new Set<FrameReceipt>();
|
|
3121
|
+
const issuedReceipts = new WeakSet<FrameReceipt>();
|
|
3122
|
+
const receiptTimings = new WeakMap<FrameReceipt, Promise<VolumeTimingObservation>>();
|
|
2734
3123
|
let surfaceReleased = false;
|
|
2735
3124
|
|
|
2736
3125
|
const renderer: RendererAssemblyImplementation = {
|
|
@@ -2744,13 +3133,21 @@ async function makeWebGPURenderer(
|
|
|
2744
3133
|
leasesByWorld.set(world, lease);
|
|
2745
3134
|
return ok(lease);
|
|
2746
3135
|
},
|
|
3136
|
+
createRenderTarget: (descriptor) => renderTargetHost.createRenderTarget(descriptor),
|
|
3137
|
+
resizeRenderTarget: (target, descriptor) =>
|
|
3138
|
+
renderTargetHost.resizeRenderTarget(target, descriptor),
|
|
3139
|
+
createRenderTargetTextureSource: (target, options) =>
|
|
3140
|
+
renderTargetHost.createRenderTargetTextureSource(target, options),
|
|
3141
|
+
requestTargetReadback: (target, request) =>
|
|
3142
|
+
renderTargetHost.requestTargetReadback(target, request),
|
|
3143
|
+
destroyRenderTarget: (target) => renderTargetHost.destroyRenderTarget(target),
|
|
2747
3144
|
setProfile(profile: RenderProfile): RenderResult<void, RenderError> {
|
|
2748
3145
|
const invalid = validateRenderProfile(profile);
|
|
2749
3146
|
if (invalid !== undefined) {
|
|
2750
3147
|
return err(
|
|
2751
3148
|
new RendererOperationError('frame-input-invalid', {
|
|
2752
3149
|
operation: 'set-profile',
|
|
2753
|
-
cause: new
|
|
3150
|
+
cause: new StandardProfileInvalidError(invalid),
|
|
2754
3151
|
}),
|
|
2755
3152
|
);
|
|
2756
3153
|
}
|
|
@@ -2794,6 +3191,8 @@ async function makeWebGPURenderer(
|
|
|
2794
3191
|
? {}
|
|
2795
3192
|
: { presentationProof: internals.context.presentationProof }),
|
|
2796
3193
|
});
|
|
3194
|
+
const volumetricFog = renderSystem.volumetricFog;
|
|
3195
|
+
const directionalShadow = renderSystem.directionalShadow;
|
|
2797
3196
|
return Object.freeze({
|
|
2798
3197
|
state,
|
|
2799
3198
|
surface: surfaceReleased ? 'released' : 'available',
|
|
@@ -2807,12 +3206,15 @@ async function makeWebGPURenderer(
|
|
|
2807
3206
|
(internals.featureHost?.features ?? []).map((feature) => feature.identity),
|
|
2808
3207
|
),
|
|
2809
3208
|
featureDiagnostics: internals.featureHost?.diagnostics() ?? Object.freeze([]),
|
|
3209
|
+
volumetricFog,
|
|
2810
3210
|
frustumStats: Object.freeze({ ...renderSystem.frustumStats }),
|
|
2811
3211
|
visibilityStats: Object.freeze({ ...renderSystem.visibilityStats }),
|
|
2812
3212
|
renderScene: renderSystem.renderScene,
|
|
3213
|
+
reflectionProbes: renderSystem.reflectionProbes,
|
|
2813
3214
|
environment: renderSystem.environment,
|
|
2814
3215
|
temporal: renderSystem.temporal,
|
|
2815
3216
|
bloom: renderSystem.bloom,
|
|
3217
|
+
directionalShadow,
|
|
2816
3218
|
...(renderSystem.temporalTargetInspection === undefined
|
|
2817
3219
|
? {}
|
|
2818
3220
|
: { temporalTarget: Object.freeze(renderSystem.temporalTargetInspection) }),
|
|
@@ -2822,6 +3224,12 @@ async function makeWebGPURenderer(
|
|
|
2822
3224
|
...(renderSystem.transmission === undefined
|
|
2823
3225
|
? {}
|
|
2824
3226
|
: { transmission: renderSystem.transmission }),
|
|
3227
|
+
...(renderSystem.standardLightingInspection === undefined
|
|
3228
|
+
? {}
|
|
3229
|
+
: { standardLighting: Object.freeze({ ...renderSystem.standardLightingInspection }) }),
|
|
3230
|
+
...(renderSystem.pointShadowInspection === undefined
|
|
3231
|
+
? {}
|
|
3232
|
+
: { pointShadow: Object.freeze({ ...renderSystem.pointShadowInspection }) }),
|
|
2825
3233
|
meshMaterialBindings: Object.freeze(
|
|
2826
3234
|
renderSystem.meshMaterialBindings.map((observation) =>
|
|
2827
3235
|
Object.freeze({
|
|
@@ -2970,6 +3378,9 @@ async function makeWebGPURenderer(
|
|
|
2970
3378
|
get bindGroupCounts() {
|
|
2971
3379
|
return renderSystem.bindGroupCounts;
|
|
2972
3380
|
},
|
|
3381
|
+
configureStandard(config) {
|
|
3382
|
+
renderSystem.configureStandard(config);
|
|
3383
|
+
},
|
|
2973
3384
|
renderFeatureDiagnostics() {
|
|
2974
3385
|
return internals.featureHost?.diagnostics() ?? [];
|
|
2975
3386
|
},
|
|
@@ -2995,6 +3406,35 @@ async function makeWebGPURenderer(
|
|
|
2995
3406
|
new RenderFeatureCapabilityMissingError(feature.identity, host.size, missingCapability),
|
|
2996
3407
|
);
|
|
2997
3408
|
}
|
|
3409
|
+
let nextRequiredFullscreenPostProcesses: readonly {
|
|
3410
|
+
readonly identity: string;
|
|
3411
|
+
readonly source: string;
|
|
3412
|
+
}[];
|
|
3413
|
+
try {
|
|
3414
|
+
nextRequiredFullscreenPostProcesses = collectRequiredFullscreenPostProcesses([
|
|
3415
|
+
...host.features,
|
|
3416
|
+
feature,
|
|
3417
|
+
]);
|
|
3418
|
+
} catch (cause) {
|
|
3419
|
+
const conflict =
|
|
3420
|
+
cause instanceof RhiError
|
|
3421
|
+
? cause
|
|
3422
|
+
: new RhiError({
|
|
3423
|
+
code: 'internal-error',
|
|
3424
|
+
expected: 'fullscreen feature declarations are internally consistent',
|
|
3425
|
+
hint: 'repair the conflicting fullscreen identity/source declarations',
|
|
3426
|
+
});
|
|
3427
|
+
internals.errorRegistry.fire(conflict);
|
|
3428
|
+
return err(
|
|
3429
|
+
new RenderFeatureStageFailedError(feature.identity, -1, 'prepare', 'registration'),
|
|
3430
|
+
);
|
|
3431
|
+
}
|
|
3432
|
+
const fullscreenPrewarm = await prewarmFullscreenFeatureModules(feature);
|
|
3433
|
+
if (!fullscreenPrewarm.ok) {
|
|
3434
|
+
return err(
|
|
3435
|
+
new RenderFeatureStageFailedError(feature.identity, -1, 'prepare', 'registration'),
|
|
3436
|
+
);
|
|
3437
|
+
}
|
|
2998
3438
|
for (const materialShaderId of feature.requiredMaterialShaders ?? []) {
|
|
2999
3439
|
const lookup = getShader().findMaterialArtifact(materialShaderId);
|
|
3000
3440
|
if (!lookup.ok) {
|
|
@@ -3033,6 +3473,7 @@ async function makeWebGPURenderer(
|
|
|
3033
3473
|
requiredMaterialShaderSet.add(materialShaderId);
|
|
3034
3474
|
}
|
|
3035
3475
|
requiredMaterialShaders = Object.freeze([...requiredMaterialShaderSet]);
|
|
3476
|
+
requiredFullscreenPostProcesses = nextRequiredFullscreenPostProcesses;
|
|
3036
3477
|
}
|
|
3037
3478
|
return installed;
|
|
3038
3479
|
},
|
|
@@ -3061,6 +3502,7 @@ async function makeWebGPURenderer(
|
|
|
3061
3502
|
}
|
|
3062
3503
|
}
|
|
3063
3504
|
requiredMaterialShaders = Object.freeze([...requiredMaterialShaderSet]);
|
|
3505
|
+
requiredFullscreenPostProcesses = collectRequiredFullscreenPostProcesses(host.features);
|
|
3064
3506
|
}
|
|
3065
3507
|
return removed;
|
|
3066
3508
|
},
|
|
@@ -3234,8 +3676,52 @@ async function makeWebGPURenderer(
|
|
|
3234
3676
|
// through onError separately; the facade Result is the synchronous
|
|
3235
3677
|
// summary AI users can ignore or branch on).
|
|
3236
3678
|
try {
|
|
3237
|
-
|
|
3679
|
+
renderTargetHost.beginFrame();
|
|
3680
|
+
const timingHost = internals as WebGPURendererInternals & {
|
|
3681
|
+
gpuPassTimingFrameIdentity?:
|
|
3682
|
+
| {
|
|
3683
|
+
readonly frameId: number;
|
|
3684
|
+
readonly deviceGeneration: number;
|
|
3685
|
+
readonly graphGeneration: number;
|
|
3686
|
+
}
|
|
3687
|
+
| undefined;
|
|
3688
|
+
gpuPassTimingCapture?:
|
|
3689
|
+
| import('../record/gpu-pass-timing/session.js').GpuPassTimingCapture
|
|
3690
|
+
| undefined;
|
|
3691
|
+
gpuPassTimingBeginReason?: GpuPassTimingReason | undefined;
|
|
3692
|
+
};
|
|
3693
|
+
const timingRequested =
|
|
3694
|
+
isFrameRequest &&
|
|
3695
|
+
(frameRequest?.profileFrame !== undefined ||
|
|
3696
|
+
internals.options?.captureGpuTimings === true);
|
|
3697
|
+
let volumeTimingCapture: GpuTimingCapture | undefined;
|
|
3698
|
+
const volumeTimingUnavailableReason = timingRequested
|
|
3699
|
+
? 'timestamp capture is unavailable on this device'
|
|
3700
|
+
: 'timestamp capture was not requested for this frame';
|
|
3701
|
+
// The pass-timing session is authoritative when the feature is
|
|
3702
|
+
// enabled. Keep the volume-specific capture for its existing
|
|
3703
|
+
// benchmark path, but do not allocate two timestamp readbacks for a
|
|
3704
|
+
// single receipt when both options are present.
|
|
3705
|
+
if (timingRequested && gpuPassTimingOptions === undefined) {
|
|
3706
|
+
const createdTiming = GpuTimingCapture.create(internals.device);
|
|
3707
|
+
if (createdTiming.ok) volumeTimingCapture = createdTiming.value;
|
|
3708
|
+
}
|
|
3709
|
+
if (isFrameRequest) {
|
|
3710
|
+
timingHost.gpuPassTimingFrameIdentity = {
|
|
3711
|
+
frameId: frameId + 1,
|
|
3712
|
+
deviceGeneration: activeDeviceScope.generation,
|
|
3713
|
+
graphGeneration: 0,
|
|
3714
|
+
};
|
|
3715
|
+
}
|
|
3716
|
+
const submitted = renderSystem.draw(
|
|
3717
|
+
worlds,
|
|
3718
|
+
drawOptions,
|
|
3719
|
+
renderReadLeases,
|
|
3720
|
+
volumeTimingCapture,
|
|
3721
|
+
);
|
|
3722
|
+
timingHost.gpuPassTimingFrameIdentity = undefined;
|
|
3238
3723
|
if (!submitted) {
|
|
3724
|
+
volumeTimingCapture?.discard();
|
|
3239
3725
|
return err(
|
|
3240
3726
|
new RendererContractFailureError(
|
|
3241
3727
|
'draw',
|
|
@@ -3243,29 +3729,78 @@ async function makeWebGPURenderer(
|
|
|
3243
3729
|
),
|
|
3244
3730
|
);
|
|
3245
3731
|
}
|
|
3732
|
+
const timingCompletion = timingHost.gpuPassTimingSubmittedWork;
|
|
3733
|
+
timingHost.gpuPassTimingSubmittedWork = undefined;
|
|
3246
3734
|
internals.pack.instrumentation?.onFrameBoundary?.();
|
|
3735
|
+
const receiptFrameId = ++frameId;
|
|
3736
|
+
const receiptGeneration = activeDeviceScope.generation;
|
|
3737
|
+
const completed: FrameReceipt['completed'] = (
|
|
3738
|
+
timingCompletion ?? internals.device.queue.onSubmittedWorkDone()
|
|
3739
|
+
)
|
|
3740
|
+
.then(() => ok(undefined))
|
|
3741
|
+
.catch((cause: unknown) =>
|
|
3742
|
+
err(
|
|
3743
|
+
new RendererOperationError('device-operation-failed', {
|
|
3744
|
+
operation: 'complete-frame',
|
|
3745
|
+
frameId: receiptFrameId,
|
|
3746
|
+
deviceGeneration: receiptGeneration,
|
|
3747
|
+
cause: structuredRendererCause(cause, 'complete-frame'),
|
|
3748
|
+
}),
|
|
3749
|
+
),
|
|
3750
|
+
);
|
|
3751
|
+
renderTargetHost.onFrameSubmitted(completed);
|
|
3247
3752
|
if (isFrameRequest) {
|
|
3248
|
-
const receiptFrameId = ++frameId;
|
|
3249
|
-
const receiptGeneration = activeDeviceScope.generation;
|
|
3250
|
-
const completed: FrameReceipt['completed'] = internals.device.queue
|
|
3251
|
-
.onSubmittedWorkDone()
|
|
3252
|
-
.then(() => ok(undefined))
|
|
3253
|
-
.catch((cause: unknown) =>
|
|
3254
|
-
err(
|
|
3255
|
-
new RendererOperationError('device-operation-failed', {
|
|
3256
|
-
operation: 'complete-frame',
|
|
3257
|
-
frameId: receiptFrameId,
|
|
3258
|
-
deviceGeneration: receiptGeneration,
|
|
3259
|
-
cause: structuredRendererCause(cause, 'complete-frame'),
|
|
3260
|
-
}),
|
|
3261
|
-
),
|
|
3262
|
-
);
|
|
3263
3753
|
const receipt = Object.freeze({
|
|
3264
3754
|
frameId: receiptFrameId,
|
|
3265
3755
|
deviceGeneration: receiptGeneration,
|
|
3266
3756
|
completed,
|
|
3267
3757
|
});
|
|
3268
|
-
|
|
3758
|
+
const passTimingCapture = timingHost.gpuPassTimingCapture;
|
|
3759
|
+
const timingBeginReason = timingHost.gpuPassTimingBeginReason;
|
|
3760
|
+
const timingSource =
|
|
3761
|
+
passTimingCapture === undefined
|
|
3762
|
+
? gpuPassTimingUnavailable === undefined && timingBeginReason === undefined
|
|
3763
|
+
? undefined
|
|
3764
|
+
: async (): Promise<GpuPassTimingObservation> =>
|
|
3765
|
+
gpuPassTimingUnavailable ??
|
|
3766
|
+
failedGpuPassTimingObservation(timingBeginReason as GpuPassTimingReason)
|
|
3767
|
+
: async (): Promise<GpuPassTimingObservation> => {
|
|
3768
|
+
const observed = await passTimingCapture.observe();
|
|
3769
|
+
if (!observed.ok) {
|
|
3770
|
+
return failedGpuPassTimingObservation(observed.error);
|
|
3771
|
+
}
|
|
3772
|
+
const frame = observed.value;
|
|
3773
|
+
const unmeasured = frame.passes.find((pass) => pass.status === 'unmeasured');
|
|
3774
|
+
if (unmeasured !== undefined) {
|
|
3775
|
+
return { status: 'partial', frame, reason: unmeasured.reason };
|
|
3776
|
+
}
|
|
3777
|
+
if (frame.droppedPassCount > 0) {
|
|
3778
|
+
return {
|
|
3779
|
+
status: 'partial',
|
|
3780
|
+
frame,
|
|
3781
|
+
reason: {
|
|
3782
|
+
code: 'query-budget-exceeded',
|
|
3783
|
+
expected: 'all executed passes fit within the bounded query budget',
|
|
3784
|
+
hint: 'increase maxPassesPerFrame or simplify the pass graph',
|
|
3785
|
+
detail: { droppedPassCount: frame.droppedPassCount },
|
|
3786
|
+
},
|
|
3787
|
+
};
|
|
3788
|
+
}
|
|
3789
|
+
return { status: 'complete', frame };
|
|
3790
|
+
};
|
|
3791
|
+
timingObservationStore?.register(receipt, timingSource);
|
|
3792
|
+
currentReceipts.add(receipt);
|
|
3793
|
+
if (timingRequested && gpuPassTimingOptions === undefined) {
|
|
3794
|
+
receiptTimings.set(
|
|
3795
|
+
receipt,
|
|
3796
|
+
volumeTimingCapture?.observation() ??
|
|
3797
|
+
Promise.resolve({
|
|
3798
|
+
status: 'unavailable',
|
|
3799
|
+
reason: volumeTimingUnavailableReason,
|
|
3800
|
+
}),
|
|
3801
|
+
);
|
|
3802
|
+
}
|
|
3803
|
+
issuedReceipts.add(receipt);
|
|
3269
3804
|
return ok(receipt);
|
|
3270
3805
|
}
|
|
3271
3806
|
return ok(undefined);
|
|
@@ -3289,11 +3824,18 @@ async function makeWebGPURenderer(
|
|
|
3289
3824
|
receipt: FrameReceipt,
|
|
3290
3825
|
request: FrameObservationRequest,
|
|
3291
3826
|
): Promise<RenderResult<FrameReceiptObservation, RenderError>> {
|
|
3827
|
+
// The bounded pass contract owns `timings`; the volume timing producer
|
|
3828
|
+
// owns the separate `volumeTimings` field. Keeping the fields distinct
|
|
3829
|
+
// prevents consumers from having to discriminate an untagged union.
|
|
3830
|
+
if (gpuPassTimingOptions !== undefined) {
|
|
3831
|
+
return timingObservationStore === undefined
|
|
3832
|
+
? observeGpuPassTimingDisabled(receipt, request, () => activeDeviceScope.generation)
|
|
3833
|
+
: timingObservationStore.observe(receipt, request);
|
|
3834
|
+
}
|
|
3292
3835
|
if (
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
latestReceipt.deviceGeneration !== receipt.deviceGeneration
|
|
3836
|
+
!currentReceipts.has(receipt) ||
|
|
3837
|
+
!issuedReceipts.has(receipt) ||
|
|
3838
|
+
receipt.deviceGeneration !== activeDeviceScope.generation
|
|
3297
3839
|
) {
|
|
3298
3840
|
const currentGeneration = activeDeviceScope.generation;
|
|
3299
3841
|
return err(
|
|
@@ -3316,11 +3858,27 @@ async function makeWebGPURenderer(
|
|
|
3316
3858
|
}),
|
|
3317
3859
|
);
|
|
3318
3860
|
}
|
|
3861
|
+
const targetReadbacks =
|
|
3862
|
+
request.targetReadbacks === undefined || request.targetReadbacks.length === 0
|
|
3863
|
+
? undefined
|
|
3864
|
+
: await renderTargetHost.observeTargetReadbacks(receipt, request.targetReadbacks);
|
|
3865
|
+
if (targetReadbacks !== undefined && !targetReadbacks.ok) return targetReadbacks;
|
|
3866
|
+
const volumeTimings = request.include.includes('timings')
|
|
3867
|
+
? await (receiptTimings.get(receipt) ??
|
|
3868
|
+
Promise.resolve<VolumeTimingObservation>({
|
|
3869
|
+
status: 'unavailable',
|
|
3870
|
+
reason: 'timestamp capture was not requested for this frame',
|
|
3871
|
+
}))
|
|
3872
|
+
: undefined;
|
|
3319
3873
|
return ok(
|
|
3320
3874
|
Object.freeze({
|
|
3321
3875
|
frameId: receipt.frameId,
|
|
3322
3876
|
deviceGeneration: receipt.deviceGeneration,
|
|
3323
3877
|
include: Object.freeze([...request.include]),
|
|
3878
|
+
...(targetReadbacks === undefined
|
|
3879
|
+
? {}
|
|
3880
|
+
: { targetReadbacks: Object.freeze(targetReadbacks.value) }),
|
|
3881
|
+
...(volumeTimings === undefined ? {} : { volumeTimings }),
|
|
3324
3882
|
}),
|
|
3325
3883
|
);
|
|
3326
3884
|
},
|
|
@@ -3395,6 +3953,8 @@ async function makeWebGPURenderer(
|
|
|
3395
3953
|
dispose(): RenderResult<void, RenderError> {
|
|
3396
3954
|
if (disposed) return ok(undefined);
|
|
3397
3955
|
disposed = true;
|
|
3956
|
+
currentReceipts.clear();
|
|
3957
|
+
renderTargetHost.dispose();
|
|
3398
3958
|
const cleanupFailures: RendererOperationCause[] = [];
|
|
3399
3959
|
for (const world of attachedWorlds) {
|
|
3400
3960
|
try {
|
|
@@ -3575,6 +4135,8 @@ async function makeWebGPURenderer(
|
|
|
3575
4135
|
// a layout from the replacement device.
|
|
3576
4136
|
emptyPostProcessBgl = null;
|
|
3577
4137
|
internals.context.unconfigure();
|
|
4138
|
+
retireGpuPassTimingSession();
|
|
4139
|
+
attachGpuPassTimingSession(internals, undefined);
|
|
3578
4140
|
|
|
3579
4141
|
// Step (b) / B-2 / B-AC-02: shed device-bound RenderSystem state minted by
|
|
3580
4142
|
// the lost device — the render-graph pendingDestroy queue (its PooledTextures
|
|
@@ -3595,7 +4157,7 @@ async function makeWebGPURenderer(
|
|
|
3595
4157
|
let device: RhiDevice;
|
|
3596
4158
|
try {
|
|
3597
4159
|
const deviceResult = await adapterResult.value.requestDevice(
|
|
3598
|
-
deviceOptionsForAdapter(adapterResult.value),
|
|
4160
|
+
deviceOptionsForAdapter(adapterResult.value, internals.options),
|
|
3599
4161
|
);
|
|
3600
4162
|
if (!deviceResult.ok) {
|
|
3601
4163
|
return err(new RecoverError('recover-device-unavailable'));
|
|
@@ -3721,8 +4283,12 @@ async function makeWebGPURenderer(
|
|
|
3721
4283
|
const previousDeviceScope = activeDeviceScope;
|
|
3722
4284
|
activeDeviceScope = recoveryScope;
|
|
3723
4285
|
pipelineState = candidatePipelineState;
|
|
4286
|
+
currentReceipts.clear();
|
|
3724
4287
|
gpuStore.bindDeviceScope(activeDeviceScope);
|
|
4288
|
+
renderTargetHost.recover();
|
|
3725
4289
|
previousDeviceScope.retire();
|
|
4290
|
+
establishGpuPassTimingSession();
|
|
4291
|
+
attachGpuPassTimingSession(internals, gpuPassTimingSession);
|
|
3726
4292
|
|
|
3727
4293
|
// Re-evaluate feature capability gates against the replacement device.
|
|
3728
4294
|
// Feature recovery is isolated from renderer recovery: a feature that is
|
|
@@ -3808,7 +4374,7 @@ export function exposeRenderer(renderer: RendererHostImplementation): Renderer {
|
|
|
3808
4374
|
});
|
|
3809
4375
|
|
|
3810
4376
|
return {
|
|
3811
|
-
attach: (world) => {
|
|
4377
|
+
attach: (world: World) => {
|
|
3812
4378
|
const result = renderer.attach(world);
|
|
3813
4379
|
return result.ok
|
|
3814
4380
|
? result
|
|
@@ -3819,6 +4385,12 @@ export function exposeRenderer(renderer: RendererHostImplementation): Renderer {
|
|
|
3819
4385
|
}),
|
|
3820
4386
|
);
|
|
3821
4387
|
},
|
|
4388
|
+
createRenderTarget: (descriptor) => renderer.createRenderTarget(descriptor),
|
|
4389
|
+
resizeRenderTarget: (target, descriptor) => renderer.resizeRenderTarget(target, descriptor),
|
|
4390
|
+
createRenderTargetTextureSource: (target, options) =>
|
|
4391
|
+
renderer.createRenderTargetTextureSource(target, options),
|
|
4392
|
+
requestTargetReadback: (target, request) => renderer.requestTargetReadback(target, request),
|
|
4393
|
+
destroyRenderTarget: (target) => renderer.destroyRenderTarget(target),
|
|
3822
4394
|
setProfile: (profile) => {
|
|
3823
4395
|
if (!isRendererStateOperational(currentState)) {
|
|
3824
4396
|
return err(
|
|
@@ -3831,7 +4403,7 @@ export function exposeRenderer(renderer: RendererHostImplementation): Renderer {
|
|
|
3831
4403
|
return renderer.setProfile(profile);
|
|
3832
4404
|
},
|
|
3833
4405
|
state: () => currentState,
|
|
3834
|
-
draw: (request) => {
|
|
4406
|
+
draw: (request: RenderFrameInput) => {
|
|
3835
4407
|
const result = renderer.drawFrame(request);
|
|
3836
4408
|
if (!result.ok) return err(drawError(result.error));
|
|
3837
4409
|
emit(
|
|
@@ -3844,8 +4416,9 @@ export function exposeRenderer(renderer: RendererHostImplementation): Renderer {
|
|
|
3844
4416
|
return ok(result.value);
|
|
3845
4417
|
},
|
|
3846
4418
|
inspect: () => renderer.inspect(),
|
|
3847
|
-
observe: (receipt, request) =>
|
|
3848
|
-
|
|
4419
|
+
observe: (receipt: FrameReceipt, request: FrameObservationRequest) =>
|
|
4420
|
+
renderer.observe(receipt, request),
|
|
4421
|
+
subscribe: (listener: RendererEventListener) => {
|
|
3849
4422
|
if (disposed) return () => undefined;
|
|
3850
4423
|
listeners.add(listener);
|
|
3851
4424
|
return () => listeners.delete(listener);
|
|
@@ -3959,12 +4532,11 @@ function structuredRendererCause(cause: unknown, operation: string): RendererOpe
|
|
|
3959
4532
|
const RENDER_PROFILE_KEYS = new Set([
|
|
3960
4533
|
'pipelineId',
|
|
3961
4534
|
'lightCount',
|
|
3962
|
-
'
|
|
4535
|
+
'renderPath',
|
|
3963
4536
|
'shadows',
|
|
3964
4537
|
'pbr',
|
|
3965
4538
|
'ibl',
|
|
3966
4539
|
'ssao',
|
|
3967
|
-
'fallback',
|
|
3968
4540
|
'postStages',
|
|
3969
4541
|
]);
|
|
3970
4542
|
|
|
@@ -3978,15 +4550,12 @@ function validateRenderProfile(profile: RenderProfile): string | undefined {
|
|
|
3978
4550
|
if (!STANDARD_LIGHT_COUNTS.some((count) => count === profile.lightCount)) {
|
|
3979
4551
|
return 'RenderProfile.lightCount must be 1, 32, or 256';
|
|
3980
4552
|
}
|
|
3981
|
-
if (profile.
|
|
3982
|
-
return "RenderProfile.
|
|
4553
|
+
if (profile.renderPath !== 'forward' && profile.renderPath !== 'deferred') {
|
|
4554
|
+
return "RenderProfile.renderPath must be 'forward' or 'deferred'";
|
|
3983
4555
|
}
|
|
3984
4556
|
if (profile.shadows !== 'off' && profile.shadows !== 'hard' && profile.shadows !== 'filtered') {
|
|
3985
4557
|
return "RenderProfile.shadows must be 'off', 'hard', or 'filtered'";
|
|
3986
4558
|
}
|
|
3987
|
-
if (profile.fallback !== 'native' && profile.fallback !== 'cpu-webgl2') {
|
|
3988
|
-
return "RenderProfile.fallback must be 'native' or 'cpu-webgl2'";
|
|
3989
|
-
}
|
|
3990
4559
|
for (const field of ['pbr', 'ibl', 'ssao'] as const) {
|
|
3991
4560
|
if (typeof profile[field] !== 'boolean') return `RenderProfile.${field} must be boolean`;
|
|
3992
4561
|
}
|
|
@@ -4262,23 +4831,20 @@ function makeRhiNotAvailableError(): RhiError {
|
|
|
4262
4831
|
// inverseViewProj mat4x4<f32> (64 B) appended at tail (offset 176, align 16,
|
|
4263
4832
|
// total 176 + 64 = 240 B). Field added to common.wgsl View struct; host
|
|
4264
4833
|
// writes the tail via queue.writeBuffer in render-system-record.
|
|
4265
|
-
// feat-20260613-csm-cascaded-shadow-maps M4 / w16: extended to
|
|
4834
|
+
// feat-20260613-csm-cascaded-shadow-maps M4 / w16: extended to 960 B.
|
|
4266
4835
|
// lightSpaceMatrix replaced by lightViewProj_A at offset 112; inverseViewProj
|
|
4267
4836
|
// stays at offset 176; lightViewProj_B..D + splitPlanes[4] + cascadeCount +
|
|
4268
|
-
// cascadeBlend + tail padding appended after inverseViewProj.
|
|
4269
|
-
// =
|
|
4270
|
-
//
|
|
4271
|
-
//
|
|
4272
|
-
// append at the formerly-free tail pad (bytes 504/508/512, floats 126/127/128).
|
|
4273
|
-
// VIEW_UBO_BYTES stays 592 (the host tail pad shrinks 88 B -> 64 B); the WGSL
|
|
4274
|
-
// View struct in common.wgsl carries the matching 3 f32 (SSOT comment synced).
|
|
4837
|
+
// cascadeBlend + tail padding appended after inverseViewProj. The directional
|
|
4838
|
+
// payload is 240 f32 = 960 B, placed in a 1024 B dynamic-uniform slot (AC-08).
|
|
4839
|
+
// M1 reserves [126]/[127] for depthBias/normalBias and [128..131] for the
|
|
4840
|
+
// directional filter carrier; the Spot matrix tail begins at float 132.
|
|
4275
4841
|
// feat-20260625-spot-light-shadow-mapping w25 (scope-amend webkit-fallback):
|
|
4276
|
-
// grew
|
|
4842
|
+
// grew from the directional 960 B payload to the 1024 B slot. The per-spot fragment-read perspective lightViewProj matrices
|
|
4277
4843
|
// (`array<mat4x4<f32>, 4>` = 256 B) fold from a standalone @group(0) binding 9
|
|
4278
4844
|
// uniform buffer into the View UBO tail (bytes 528..784, 16 B-aligned after
|
|
4279
|
-
//
|
|
4845
|
+
// the directional filter carrier at bytes 512..528). This removes the 12th fragment-stage uniform
|
|
4280
4846
|
// buffer that overflowed GLES 3.0's max_uniform_buffers_per_shader_stage = 11 on
|
|
4281
|
-
// the WebGL2 fallback path. render-system-record writes the full
|
|
4847
|
+
// the WebGL2 fallback path. render-system-record writes the full 240 f32 payload;
|
|
4282
4848
|
// common.wgsl `View.spotLightViewProj` is the matching SSOT.
|
|
4283
4849
|
const VIEW_UBO_BYTES = VIEW_UNIFORM_BUFFER_SIZE;
|
|
4284
4850
|
|
|
@@ -4767,7 +5333,14 @@ async function prepareMaterialShaders(
|
|
|
4767
5333
|
if (!capCheck.ok) throw capCheck.error;
|
|
4768
5334
|
storageBufferCapable = !webgl2Downlevel && capCheck.value;
|
|
4769
5335
|
}
|
|
4770
|
-
|
|
5336
|
+
// The shared PBR view/material ABI consumes the WebGPU minimum of 16
|
|
5337
|
+
// fragment sampled textures. The authored SpotLight projector is the
|
|
5338
|
+
// optional 17th texture; select its shader/BGL pair from the same limit so
|
|
5339
|
+
// low-limit adapters keep analytic lighting instead of failing layout
|
|
5340
|
+
// creation at boot.
|
|
5341
|
+
const projectorAvailable =
|
|
5342
|
+
(rhiDevice.limits.maxSampledTexturesPerShaderStage ?? 0) >=
|
|
5343
|
+
STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES;
|
|
4771
5344
|
// ── Step 1: manifest load ─────────────────────────────────────────────────
|
|
4772
5345
|
const registry = getShader();
|
|
4773
5346
|
const loaded = await registry.loadManifest();
|
|
@@ -4776,12 +5349,11 @@ async function prepareMaterialShaders(
|
|
|
4776
5349
|
}
|
|
4777
5350
|
|
|
4778
5351
|
// ── Step 1b: material shader variant resolution + registration ───────────
|
|
4779
|
-
//
|
|
4780
|
-
//
|
|
4781
|
-
//
|
|
4782
|
-
//
|
|
4783
|
-
|
|
4784
|
-
const isHdrpActive = false; // M4: wire to actual renderSystem.frameState.isHdrpActive
|
|
5352
|
+
// Install the non-clustered artifact as the eager registry seed. This value
|
|
5353
|
+
// only makes the pre-frame registry usable; record-time Standard topology is
|
|
5354
|
+
// derived from the frame's prepared `standardLighting.kind` and never reads
|
|
5355
|
+
// this boot seed as an active lighting decision.
|
|
5356
|
+
const bootClusterAxis = standardBootClusterAxis();
|
|
4785
5357
|
for (const msEntry of registry.materialShaderManifestEntries()) {
|
|
4786
5358
|
// bug-20260610: skip synthetic non-material engine entries piggy-backing
|
|
4787
5359
|
// on the materialShaders channel for variant surfacing (shadow_caster).
|
|
@@ -4792,6 +5364,11 @@ async function prepareMaterialShaders(
|
|
|
4792
5364
|
const variantDefines: Record<string, boolean> = {
|
|
4793
5365
|
STORAGE_BUFFER_AVAILABLE: storageBufferCapable,
|
|
4794
5366
|
};
|
|
5367
|
+
if (msEntry.variants.some((v) => 'DIRECTIONAL_PCSS_AVAILABLE' in v.defines)) {
|
|
5368
|
+
variantDefines.DIRECTIONAL_PCSS_AVAILABLE = directionalPcssCapable(
|
|
5369
|
+
rhiDevice.caps.backendKind,
|
|
5370
|
+
);
|
|
5371
|
+
}
|
|
4795
5372
|
// User material manifests may declare WEBGL2_COMPAT alongside the shared
|
|
4796
5373
|
// mesh-storage axis. The fallback backend must select that authored
|
|
4797
5374
|
// branch at boot; otherwise it asks for the impossible bare
|
|
@@ -4803,11 +5380,17 @@ async function prepareMaterialShaders(
|
|
|
4803
5380
|
// the axis (has a variant with this key), preserving backward compat for
|
|
4804
5381
|
// entries without the pragma.
|
|
4805
5382
|
if (msEntry.variants.some((v) => 'CLUSTER_FORWARD_AVAILABLE' in v.defines)) {
|
|
4806
|
-
variantDefines.CLUSTER_FORWARD_AVAILABLE =
|
|
5383
|
+
variantDefines.CLUSTER_FORWARD_AVAILABLE = bootClusterAxis;
|
|
4807
5384
|
}
|
|
4808
5385
|
if (msEntry.variants.some((v) => 'VERTEX_COLOR_AVAILABLE' in v.defines)) {
|
|
4809
5386
|
variantDefines.VERTEX_COLOR_AVAILABLE = false;
|
|
4810
5387
|
}
|
|
5388
|
+
if (
|
|
5389
|
+
msEntry.identifier === 'forgeax::default-standard-pbr' &&
|
|
5390
|
+
msEntry.variants.some((v) => 'PROJECTOR_AVAILABLE' in v.defines)
|
|
5391
|
+
) {
|
|
5392
|
+
variantDefines.PROJECTOR_AVAILABLE = projectorAvailable;
|
|
5393
|
+
}
|
|
4811
5394
|
if (
|
|
4812
5395
|
msEntry.identifier === 'forgeax::default-standard-pbr' &&
|
|
4813
5396
|
msEntry.variants.some((v) => 'TRANSMISSION_AVAILABLE' in v.defines)
|
|
@@ -4942,6 +5525,11 @@ async function buildReadyWebGPU(
|
|
|
4942
5525
|
* frame while the shared async shader adapter is still warming up.
|
|
4943
5526
|
*/
|
|
4944
5527
|
requiredMaterialShaders: readonly string[],
|
|
5528
|
+
/** Fullscreen feature modules compiled before the first synchronous frame. */
|
|
5529
|
+
requiredFullscreenPostProcesses: readonly {
|
|
5530
|
+
readonly identity: string;
|
|
5531
|
+
readonly source: string;
|
|
5532
|
+
}[],
|
|
4945
5533
|
// feat-20260608-mesh-ssbo-dynamic-grow-l1-lift-1024-entity-cap M2 /
|
|
4946
5534
|
// T-M2-05 + M3 / T-M3-04: surface for setting `internals.growMeshSsbo`
|
|
4947
5535
|
// + `internals.meshSsboState` so the record stage's
|
|
@@ -4970,7 +5558,11 @@ async function buildReadyWebGPU(
|
|
|
4970
5558
|
// `getMaterialShaderPipeline` lookup hits a live PSO instead of falling
|
|
4971
5559
|
// into a 1-frame async-compile skip-draw window. Idempotent: caller
|
|
4972
5560
|
// guards against re-seeding when a key already exists.
|
|
4973
|
-
seedMaterialShaderPipelineCache: (
|
|
5561
|
+
seedMaterialShaderPipelineCache: (
|
|
5562
|
+
key: string,
|
|
5563
|
+
pso: RenderPipeline,
|
|
5564
|
+
group2Contract: PipelineGroup2Contract,
|
|
5565
|
+
) => void,
|
|
4974
5566
|
// feat-20260621 M-A3 (D-5): register the engine built-in tonemap onto the
|
|
4975
5567
|
// unified post-process channel once the tonemap manifest entry's composed
|
|
4976
5568
|
// WGSL is resolved. Invoked with the tonemap WGSL source string; the outer
|
|
@@ -4980,10 +5572,14 @@ async function buildReadyWebGPU(
|
|
|
4980
5572
|
// tonemap source resolves inside this async function (after the manifest-load
|
|
4981
5573
|
// await), by which point the synchronous `renderSystem` const is defined.
|
|
4982
5574
|
registerBuiltinTonemap: (source: string) => void,
|
|
5575
|
+
/** Register the special FXAA pass' final-output policy params UBO. */
|
|
5576
|
+
registerBuiltinFxaa: (source: string) => void,
|
|
4983
5577
|
registerBuiltinTemporalPostProcesses: (entries: {
|
|
4984
5578
|
readonly motionBlur?: string;
|
|
4985
5579
|
readonly taaResolve?: string;
|
|
4986
5580
|
}) => void,
|
|
5581
|
+
/** Install manifest-owned volume utility sources only after all three modules compile. */
|
|
5582
|
+
setVolumetricFogShaderSources: (sources: VolumetricFogShaderSources) => void,
|
|
4987
5583
|
): Promise<PipelineState> {
|
|
4988
5584
|
// bug-20260610: WebGL2 fallback path — read storage-buffer capability up
|
|
4989
5585
|
// front so both Step 2 (shader-module compile, where we patch engine
|
|
@@ -5001,7 +5597,9 @@ async function buildReadyWebGPU(
|
|
|
5001
5597
|
const capCheck = assertStorageBufferCap(capValue);
|
|
5002
5598
|
storageBufferCapable = capCheck.ok ? !webgl2Downlevel && capCheck.value : !webgl2Downlevel;
|
|
5003
5599
|
}
|
|
5004
|
-
|
|
5600
|
+
const projectorAvailable =
|
|
5601
|
+
(rhiDevice.limits.maxSampledTexturesPerShaderStage ?? 0) >=
|
|
5602
|
+
STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES;
|
|
5005
5603
|
// bug-20260612: choose swap-chain storage / view formats by backend.
|
|
5006
5604
|
// Channel 2 (native WebGPU) follows navigator.gpu.getPreferredCanvasFormat();
|
|
5007
5605
|
// Channel 3 (wgpu-wasm GLES, storageBufferCapable=false) hard-codes
|
|
@@ -5072,6 +5670,10 @@ async function buildReadyWebGPU(
|
|
|
5072
5670
|
let unlitModule: ShaderModule | null = null;
|
|
5073
5671
|
let spriteModule: ShaderModule | null = null;
|
|
5074
5672
|
let spriteLitModule: ShaderModule | null = null;
|
|
5673
|
+
let pbrGroup2Contract: PipelineGroup2Contract | undefined;
|
|
5674
|
+
let unlitGroup2Contract: PipelineGroup2Contract | undefined;
|
|
5675
|
+
let spriteGroup2Contract: PipelineGroup2Contract | undefined;
|
|
5676
|
+
let spriteLitGroup2Contract: PipelineGroup2Contract | undefined;
|
|
5075
5677
|
let fxaaModule: ShaderModule | null = null;
|
|
5076
5678
|
let skyboxModule: ShaderModule | null = null;
|
|
5077
5679
|
let bloomBrightModule: ShaderModule | null = null;
|
|
@@ -5290,20 +5892,25 @@ async function buildReadyWebGPU(
|
|
|
5290
5892
|
// CLUSTER_FORWARD_AVAILABLE in PR #344) only emits compound keys like
|
|
5291
5893
|
// `CLUSTER_FORWARD_AVAILABLE=false+STORAGE_BUFFER_AVAILABLE=false`, so
|
|
5292
5894
|
// findVariantByKey('STORAGE_BUFFER_AVAILABLE=false') silently misses.
|
|
5293
|
-
// The
|
|
5294
|
-
//
|
|
5295
|
-
//
|
|
5296
|
-
// otherwise each STORAGE_BUFFER_AVAILABLE=false variant.
|
|
5895
|
+
// The eager registry seed is the non-clustered boot artifact (the active
|
|
5896
|
+
// frame topology is not known here). Pick by defines fields and prefer
|
|
5897
|
+
// the boot cluster axis when present; otherwise use the storage axis.
|
|
5297
5898
|
const idToVariantWgsl = new Map<string, string>();
|
|
5298
5899
|
for (const ms of registry.materialShaderManifestEntries()) {
|
|
5299
5900
|
const exact = ms.variants.find(
|
|
5300
|
-
(
|
|
5301
|
-
|
|
5302
|
-
(!('CLUSTER_FORWARD_AVAILABLE' in
|
|
5303
|
-
|
|
5304
|
-
(!('VERTEX_COLOR_AVAILABLE' in
|
|
5305
|
-
|
|
5306
|
-
(!('
|
|
5901
|
+
(variant) =>
|
|
5902
|
+
variant.defines.STORAGE_BUFFER_AVAILABLE === false &&
|
|
5903
|
+
(!('CLUSTER_FORWARD_AVAILABLE' in variant.defines) ||
|
|
5904
|
+
variant.defines.CLUSTER_FORWARD_AVAILABLE === standardBootClusterAxis()) &&
|
|
5905
|
+
(!('VERTEX_COLOR_AVAILABLE' in variant.defines) ||
|
|
5906
|
+
variant.defines.VERTEX_COLOR_AVAILABLE === false) &&
|
|
5907
|
+
(!('DIRECTIONAL_PCSS_AVAILABLE' in variant.defines) ||
|
|
5908
|
+
variant.defines.DIRECTIONAL_PCSS_AVAILABLE ===
|
|
5909
|
+
directionalPcssCapable(rhiDevice.caps.backendKind)) &&
|
|
5910
|
+
(!('PROJECTOR_AVAILABLE' in variant.defines) ||
|
|
5911
|
+
variant.defines.PROJECTOR_AVAILABLE === projectorAvailable) &&
|
|
5912
|
+
(!('SKINNING_DISABLED' in variant.defines) ||
|
|
5913
|
+
variant.defines.SKINNING_DISABLED === true),
|
|
5307
5914
|
);
|
|
5308
5915
|
const v = exact === undefined ? undefined : findVariantByKey(ms, exact.definesKey);
|
|
5309
5916
|
if (v !== undefined) {
|
|
@@ -5332,8 +5939,8 @@ async function buildReadyWebGPU(
|
|
|
5332
5939
|
}
|
|
5333
5940
|
}
|
|
5334
5941
|
// The flat `registry.entries()` map carries the canonical composed source,
|
|
5335
|
-
// while
|
|
5336
|
-
// matching
|
|
5942
|
+
// while this eager module uses the boot layout contract. Select the
|
|
5943
|
+
// matching boot variant for every eager engine module, including native
|
|
5337
5944
|
// WebGPU. Without this step the canonical PBR/Unlit source declares the
|
|
5338
5945
|
// optional COLOR_0 vertex input while the boot SPEC_CONST layouts for
|
|
5339
5946
|
// ordinary meshes do not provide location 13.
|
|
@@ -5343,11 +5950,16 @@ async function buildReadyWebGPU(
|
|
|
5343
5950
|
(variant) =>
|
|
5344
5951
|
variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
|
|
5345
5952
|
(!('CLUSTER_FORWARD_AVAILABLE' in variant.defines) ||
|
|
5346
|
-
variant.defines.CLUSTER_FORWARD_AVAILABLE ===
|
|
5953
|
+
variant.defines.CLUSTER_FORWARD_AVAILABLE === standardBootClusterAxis()) &&
|
|
5347
5954
|
(!('VERTEX_COLOR_AVAILABLE' in variant.defines) ||
|
|
5348
5955
|
variant.defines.VERTEX_COLOR_AVAILABLE === false) &&
|
|
5956
|
+
(!('PROJECTOR_AVAILABLE' in variant.defines) ||
|
|
5957
|
+
variant.defines.PROJECTOR_AVAILABLE === projectorAvailable) &&
|
|
5349
5958
|
(!('SKINNING_DISABLED' in variant.defines) ||
|
|
5350
5959
|
variant.defines.SKINNING_DISABLED === true) &&
|
|
5960
|
+
(!('DIRECTIONAL_PCSS_AVAILABLE' in variant.defines) ||
|
|
5961
|
+
variant.defines.DIRECTIONAL_PCSS_AVAILABLE ===
|
|
5962
|
+
directionalPcssCapable(rhiDevice.caps.backendKind)) &&
|
|
5351
5963
|
(!('TRANSMISSION_AVAILABLE' in variant.defines) ||
|
|
5352
5964
|
variant.defines.TRANSMISSION_AVAILABLE === false),
|
|
5353
5965
|
);
|
|
@@ -5480,6 +6092,101 @@ async function buildReadyWebGPU(
|
|
|
5480
6092
|
seedShaderModule(label, shaderResult.value);
|
|
5481
6093
|
}
|
|
5482
6094
|
|
|
6095
|
+
// A boot-installed fullscreen feature has the same synchronous record
|
|
6096
|
+
// contract as the built-in output transform: its module must be seeded
|
|
6097
|
+
// before the caller can drive a tight frame loop. Otherwise the typed
|
|
6098
|
+
// graph's first post-effect admission is correctly held back, but a
|
|
6099
|
+
// synchronous consumer would only see the base image until it yields.
|
|
6100
|
+
// Late-installed features use the matching registration path below.
|
|
6101
|
+
for (const postProcess of requiredFullscreenPostProcesses) {
|
|
6102
|
+
const label = postProcessShaderModuleLabel(postProcess.identity, postProcess.source);
|
|
6103
|
+
const shaderResult = await runShimStep(
|
|
6104
|
+
() =>
|
|
6105
|
+
asyncCreateShaderModule
|
|
6106
|
+
? asyncCreateShaderModule(rhiDevice, { code: postProcess.source, label })
|
|
6107
|
+
: invokeDeviceCreateShaderModule(rhiDevice, { code: postProcess.source, label }),
|
|
6108
|
+
'shader-compile-failed',
|
|
6109
|
+
`declared fullscreen render feature '${postProcess.identity}' compiled`,
|
|
6110
|
+
`inspect the composed WGSL for '${postProcess.identity}' and check device.features`,
|
|
6111
|
+
);
|
|
6112
|
+
if (!shaderResult.ok) throw shaderResult.error;
|
|
6113
|
+
seedShaderModule(label, shaderResult.value);
|
|
6114
|
+
}
|
|
6115
|
+
|
|
6116
|
+
// The volume graph is authored from ECS state and therefore cannot be
|
|
6117
|
+
// compiled while the static material manifest is assembled. Its pass
|
|
6118
|
+
// encoder nevertheless uses the same shared lazy shader adapter, so the
|
|
6119
|
+
// four utility modules must be prewarmed before the first synchronous
|
|
6120
|
+
// draw. The sources come from the shader manifest (the package-owned WGSL
|
|
6121
|
+
// files are the sole source of truth); compile all four first and only
|
|
6122
|
+
// then publish the labels and source bundle. A partial bundle would let a
|
|
6123
|
+
// later frame mix generations after a device recovery.
|
|
6124
|
+
if (rhiDevice.caps.compute && rhiDevice.caps.storageTexture) {
|
|
6125
|
+
const volumeSources = [
|
|
6126
|
+
['inject', 'volume_inject', (source: string) => source.includes('fn volume_inject(')],
|
|
6127
|
+
['temporal', 'volume_temporal', (source: string) => source.includes('fn volume_temporal(')],
|
|
6128
|
+
[
|
|
6129
|
+
'integrate',
|
|
6130
|
+
'volume_integrate',
|
|
6131
|
+
(source: string) => source.includes('fn volume_integrate('),
|
|
6132
|
+
],
|
|
6133
|
+
['composite', 'volume_composite', (source: string) => source.includes('fn volume_fs(')],
|
|
6134
|
+
] as const;
|
|
6135
|
+
const resolved = volumeSources.map(([stage, label, matches]) => {
|
|
6136
|
+
const entry = manifestEntries.find((candidate) => matches(candidate.wgsl));
|
|
6137
|
+
return { stage, label, entry };
|
|
6138
|
+
});
|
|
6139
|
+
// Volumetric fog is an optional graph feature. Minimal hosts/tests may
|
|
6140
|
+
// intentionally ship only material/post-process entries; keep those
|
|
6141
|
+
// renderers bootable while still rejecting a partially declared volume
|
|
6142
|
+
// bundle (which would otherwise fail later as a lazy pending pipeline).
|
|
6143
|
+
const present = resolved.filter((candidate) => candidate.entry !== undefined).length;
|
|
6144
|
+
if (present === 0) {
|
|
6145
|
+
// No volume manifest entries: leave the source/module bundle unset and
|
|
6146
|
+
// let the topology capability gate keep volume work disabled.
|
|
6147
|
+
} else {
|
|
6148
|
+
const missing = resolved.find((candidate) => candidate.entry === undefined);
|
|
6149
|
+
if (missing !== undefined) {
|
|
6150
|
+
throw new RhiError({
|
|
6151
|
+
code: 'shader-compile-failed',
|
|
6152
|
+
expected: 'shader manifest contains all four volumetric fog utility entries',
|
|
6153
|
+
hint: `add the package-owned WGSL entry for '${missing.label}' to the engine shader manifest`,
|
|
6154
|
+
});
|
|
6155
|
+
}
|
|
6156
|
+
const compiled = [] as Array<{
|
|
6157
|
+
readonly stage: (typeof volumeSources)[number][0];
|
|
6158
|
+
readonly label: string;
|
|
6159
|
+
readonly source: string;
|
|
6160
|
+
readonly module: ShaderModule;
|
|
6161
|
+
}>;
|
|
6162
|
+
for (const { stage, label, entry } of resolved) {
|
|
6163
|
+
// `missing` above proves the narrowing for this loop; keep the guard
|
|
6164
|
+
// explicit so a future refactor cannot publish an undefined source.
|
|
6165
|
+
if (entry === undefined) throw new Error(`missing volumetric shader ${label}`);
|
|
6166
|
+
const shaderResult = await runShimStep(
|
|
6167
|
+
() =>
|
|
6168
|
+
asyncCreateShaderModule
|
|
6169
|
+
? asyncCreateShaderModule(rhiDevice, { code: entry.wgsl, label })
|
|
6170
|
+
: invokeDeviceCreateShaderModule(rhiDevice, { code: entry.wgsl, label }),
|
|
6171
|
+
'shader-compile-failed',
|
|
6172
|
+
`volumetric fog shader module '${label}' compiled`,
|
|
6173
|
+
`inspect manifest WGSL for '${label}' and check device.features`,
|
|
6174
|
+
);
|
|
6175
|
+
if (!shaderResult.ok) throw shaderResult.error;
|
|
6176
|
+
compiled.push({ stage, label, source: entry.wgsl, module: shaderResult.value });
|
|
6177
|
+
}
|
|
6178
|
+
for (const { label, module } of compiled) seedShaderModule(label, module);
|
|
6179
|
+
setVolumetricFogShaderSources({
|
|
6180
|
+
inject: compiled.find((candidate) => candidate.stage === 'inject')?.source as string,
|
|
6181
|
+
temporal: compiled.find((candidate) => candidate.stage === 'temporal')?.source as string,
|
|
6182
|
+
integrate: compiled.find((candidate) => candidate.stage === 'integrate')
|
|
6183
|
+
?.source as string,
|
|
6184
|
+
composite: compiled.find((candidate) => candidate.stage === 'composite')
|
|
6185
|
+
?.source as string,
|
|
6186
|
+
});
|
|
6187
|
+
}
|
|
6188
|
+
}
|
|
6189
|
+
|
|
5483
6190
|
const pbrShaderResult = await runShimStep(
|
|
5484
6191
|
() =>
|
|
5485
6192
|
asyncCreateShaderModule
|
|
@@ -5491,6 +6198,7 @@ async function buildReadyWebGPU(
|
|
|
5491
6198
|
);
|
|
5492
6199
|
if (!pbrShaderResult.ok) throw pbrShaderResult.error;
|
|
5493
6200
|
pbrModule = pbrShaderResult.value;
|
|
6201
|
+
pbrGroup2Contract = resolvePipelineGroup2Contract(pbrEntry.wgsl);
|
|
5494
6202
|
// feat-20260629-multi-uv-set-support: a real extra-UV mesh creates a
|
|
5495
6203
|
// layout-specific material PSO after the boot-time standard-layout PSO
|
|
5496
6204
|
// has been pre-warmed. Seed the shared shader-module adapter under the
|
|
@@ -5511,7 +6219,12 @@ async function buildReadyWebGPU(
|
|
|
5511
6219
|
(rhiDevice.limits.maxSampledTexturesPerShaderStage ?? 0) >=
|
|
5512
6220
|
STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES;
|
|
5513
6221
|
const transmissionVariants = transmissionCapable
|
|
5514
|
-
? selectStandardPbrTransmissionPrewarmVariants(
|
|
6222
|
+
? selectStandardPbrTransmissionPrewarmVariants(
|
|
6223
|
+
pbrManifestEntry,
|
|
6224
|
+
storageBufferCapable,
|
|
6225
|
+
directionalPcssCapable(rhiDevice.caps.backendKind),
|
|
6226
|
+
projectorAvailable,
|
|
6227
|
+
)
|
|
5515
6228
|
: [];
|
|
5516
6229
|
|
|
5517
6230
|
// HDRP uses the canonical all-true variant key (`''`) and the lazy
|
|
@@ -5549,6 +6262,8 @@ async function buildReadyWebGPU(
|
|
|
5549
6262
|
pbrManifestEntry,
|
|
5550
6263
|
storageBufferCapable,
|
|
5551
6264
|
transmissionCapable,
|
|
6265
|
+
directionalPcssCapable(rhiDevice.caps.backendKind),
|
|
6266
|
+
projectorAvailable,
|
|
5552
6267
|
)) {
|
|
5553
6268
|
const moduleLabel = `module-forgeax::default-standard-pbr#${hdrpVariant.definesKey}`;
|
|
5554
6269
|
let hdrpModule = prewarmedHdrpModules.get(hdrpVariant.composedWgsl);
|
|
@@ -5575,6 +6290,43 @@ async function buildReadyWebGPU(
|
|
|
5575
6290
|
seedShaderModule(moduleLabel, hdrpModule);
|
|
5576
6291
|
}
|
|
5577
6292
|
|
|
6293
|
+
// Skin geometry uses the same lazy material-pipeline adapter as standard
|
|
6294
|
+
// PBR, but its dedicated module was previously never warmed. A record
|
|
6295
|
+
// frame therefore received a pending module result, skipped the skin PSO,
|
|
6296
|
+
// and silently emitted no draw. Compile and seed every active-capability
|
|
6297
|
+
// skin variant so both URP and HDRP resolve their exact PSO consumer on
|
|
6298
|
+
// frame one (including the geometry-owned COLOR_0=false variant).
|
|
6299
|
+
const pbrSkinManifestEntry = [...registry.materialShaderManifestEntries()].find(
|
|
6300
|
+
(entry) => entry.identifier === SKIN_MATERIAL_SHADER_ID,
|
|
6301
|
+
);
|
|
6302
|
+
const activeSkinVariants = selectSkinPrewarmVariants(
|
|
6303
|
+
pbrSkinManifestEntry,
|
|
6304
|
+
storageBufferCapable,
|
|
6305
|
+
);
|
|
6306
|
+
for (const skinVariant of activeSkinVariants) {
|
|
6307
|
+
const moduleLabel = `module-${SKIN_MATERIAL_SHADER_ID}#${skinVariant.definesKey}`;
|
|
6308
|
+
const skinModuleResult = await runShimStep(
|
|
6309
|
+
() =>
|
|
6310
|
+
asyncCreateShaderModule
|
|
6311
|
+
? asyncCreateShaderModule(rhiDevice, {
|
|
6312
|
+
code: skinVariant.composedWgsl,
|
|
6313
|
+
label: moduleLabel,
|
|
6314
|
+
})
|
|
6315
|
+
: invokeDeviceCreateShaderModule(rhiDevice, {
|
|
6316
|
+
code: skinVariant.composedWgsl,
|
|
6317
|
+
label: moduleLabel,
|
|
6318
|
+
}),
|
|
6319
|
+
'shader-compile-failed',
|
|
6320
|
+
`pbr-skin variant ${skinVariant.definesKey || '<default>'} compiled`,
|
|
6321
|
+
'inspect the selected pbr-skin storage/cluster/color variant and device.features',
|
|
6322
|
+
);
|
|
6323
|
+
if (!skinModuleResult.ok) throw skinModuleResult.error;
|
|
6324
|
+
seedShaderModule(moduleLabel, skinModuleResult.value);
|
|
6325
|
+
if (skinVariant.definesKey === '') {
|
|
6326
|
+
seedShaderModule(`module-${SKIN_MATERIAL_SHADER_ID}`, skinModuleResult.value);
|
|
6327
|
+
}
|
|
6328
|
+
}
|
|
6329
|
+
|
|
5578
6330
|
// M2-07-c: Materials.standard with an explicit renderState reaches the
|
|
5579
6331
|
// lazy material pipeline path, whose module label includes the requested
|
|
5580
6332
|
// variant set. Reuse the boot-compiled PBR module only for manifest
|
|
@@ -5599,6 +6351,7 @@ async function buildReadyWebGPU(
|
|
|
5599
6351
|
);
|
|
5600
6352
|
if (!unlitShaderResult.ok) throw unlitShaderResult.error;
|
|
5601
6353
|
unlitModule = unlitShaderResult.value;
|
|
6354
|
+
unlitGroup2Contract = resolvePipelineGroup2Contract(unlitEntry.wgsl);
|
|
5602
6355
|
// Seed the shared lazy adapter with the eagerly compiled module so
|
|
5603
6356
|
// prepared color-only PSOs build synchronously on their first frame.
|
|
5604
6357
|
seedShaderModule('unlit', unlitModule);
|
|
@@ -5745,7 +6498,8 @@ async function buildReadyWebGPU(
|
|
|
5745
6498
|
// frames (e.g. the hello-tonemap dawn smoke) would stall on a black frame
|
|
5746
6499
|
// forever. Mirror the shadow_caster prewarm: await-compile the tonemap module
|
|
5747
6500
|
// here and `seedShaderModule` it under the exact label
|
|
5748
|
-
// `buildPostProcessPipeline` requests
|
|
6501
|
+
// `buildPostProcessPipeline` requests the content-addressed module label,
|
|
6502
|
+
// so the
|
|
5749
6503
|
// first `getPostProcessPipeline(STANDARD_OUTPUT_TRANSFORM_FEATURE_ID, …)` hits the module cache
|
|
5750
6504
|
// synchronously and builds the pipeline on frame 1.
|
|
5751
6505
|
const tonemapPrewarm = await runShimStep(
|
|
@@ -5753,11 +6507,17 @@ async function buildReadyWebGPU(
|
|
|
5753
6507
|
asyncCreateShaderModule
|
|
5754
6508
|
? asyncCreateShaderModule(rhiDevice, {
|
|
5755
6509
|
code: tonemapEntry.wgsl,
|
|
5756
|
-
label:
|
|
6510
|
+
label: postProcessShaderModuleLabel(
|
|
6511
|
+
STANDARD_OUTPUT_TRANSFORM_FEATURE_ID,
|
|
6512
|
+
tonemapEntry.wgsl,
|
|
6513
|
+
),
|
|
5757
6514
|
})
|
|
5758
6515
|
: invokeDeviceCreateShaderModule(rhiDevice, {
|
|
5759
6516
|
code: tonemapEntry.wgsl,
|
|
5760
|
-
label:
|
|
6517
|
+
label: postProcessShaderModuleLabel(
|
|
6518
|
+
STANDARD_OUTPUT_TRANSFORM_FEATURE_ID,
|
|
6519
|
+
tonemapEntry.wgsl,
|
|
6520
|
+
),
|
|
5761
6521
|
}),
|
|
5762
6522
|
'shader-compile-failed',
|
|
5763
6523
|
'tonemap shader module compiled (unified post-process prewarm)',
|
|
@@ -5765,7 +6525,7 @@ async function buildReadyWebGPU(
|
|
|
5765
6525
|
);
|
|
5766
6526
|
if (!tonemapPrewarm.ok) throw tonemapPrewarm.error;
|
|
5767
6527
|
seedShaderModule(
|
|
5768
|
-
|
|
6528
|
+
postProcessShaderModuleLabel(STANDARD_OUTPUT_TRANSFORM_FEATURE_ID, tonemapEntry.wgsl),
|
|
5769
6529
|
tonemapPrewarm.value,
|
|
5770
6530
|
);
|
|
5771
6531
|
registerBuiltinTonemap(tonemapEntry.wgsl);
|
|
@@ -5782,18 +6542,18 @@ async function buildReadyWebGPU(
|
|
|
5782
6542
|
asyncCreateShaderModule
|
|
5783
6543
|
? asyncCreateShaderModule(rhiDevice, {
|
|
5784
6544
|
code: entry.wgsl,
|
|
5785
|
-
label:
|
|
6545
|
+
label: postProcessShaderModuleLabel(id, entry.wgsl),
|
|
5786
6546
|
})
|
|
5787
6547
|
: invokeDeviceCreateShaderModule(rhiDevice, {
|
|
5788
6548
|
code: entry.wgsl,
|
|
5789
|
-
label:
|
|
6549
|
+
label: postProcessShaderModuleLabel(id, entry.wgsl),
|
|
5790
6550
|
}),
|
|
5791
6551
|
'shader-compile-failed',
|
|
5792
6552
|
`${id} shader module compiled (unified post-process prewarm)`,
|
|
5793
6553
|
`inspect manifest ${id} entry composed wgsl; check device.features`,
|
|
5794
6554
|
);
|
|
5795
6555
|
if (!prewarm.ok) throw prewarm.error;
|
|
5796
|
-
seedShaderModule(
|
|
6556
|
+
seedShaderModule(postProcessShaderModuleLabel(id, entry.wgsl), prewarm.value);
|
|
5797
6557
|
};
|
|
5798
6558
|
await prewarmTemporal(motionBlurEntry, 'forgeax.motion-blur');
|
|
5799
6559
|
await prewarmTemporal(taaResolveEntry, 'forgeax.taa-resolve');
|
|
@@ -5822,6 +6582,7 @@ async function buildReadyWebGPU(
|
|
|
5822
6582
|
);
|
|
5823
6583
|
if (!spriteShaderResult.ok) throw spriteShaderResult.error;
|
|
5824
6584
|
spriteModule = spriteShaderResult.value;
|
|
6585
|
+
spriteGroup2Contract = resolvePipelineGroup2Contract(spriteEntry.wgsl);
|
|
5825
6586
|
// feat-20260625-refactor-sprite-as-transparent-mesh CI-fix: seed the
|
|
5826
6587
|
// lazy adapter cache under the same label (`module-forgeax::sprite`)
|
|
5827
6588
|
// that `getMaterialShaderPipeline -> buildAndCachePipeline ->
|
|
@@ -5892,7 +6653,44 @@ async function buildReadyWebGPU(
|
|
|
5892
6653
|
);
|
|
5893
6654
|
if (!spriteLitShaderResult.ok) throw spriteLitShaderResult.error;
|
|
5894
6655
|
spriteLitModule = spriteLitShaderResult.value;
|
|
6656
|
+
spriteLitGroup2Contract = resolvePipelineGroup2Contract(spriteLitEntryConst.wgsl);
|
|
5895
6657
|
seedShaderModule('module-forgeax::sprite-lit', spriteLitShaderResult.value);
|
|
6658
|
+
// Sprite-lit participates in the same Standard capability split as
|
|
6659
|
+
// PBR. Record requests the explicit URP/HDRP variant per frame, so seed
|
|
6660
|
+
// every device-matched artifact during readiness; a tight dawn/browser
|
|
6661
|
+
// frame loop cannot yield to the async module adapter between draws.
|
|
6662
|
+
const spriteLitManifestEntry = Array.from(registry.materialShaderManifestEntries()).find(
|
|
6663
|
+
(entry) => entry.identifier === 'forgeax::sprite-lit',
|
|
6664
|
+
);
|
|
6665
|
+
const prewarmedSpriteLitModules = new Map<string, ShaderModule>([
|
|
6666
|
+
[spriteLitEntryConst.wgsl, spriteLitShaderResult.value],
|
|
6667
|
+
]);
|
|
6668
|
+
for (const variant of spriteLitManifestEntry?.variants ?? []) {
|
|
6669
|
+
if (variant.defines.STORAGE_BUFFER_AVAILABLE !== storageBufferCapable) continue;
|
|
6670
|
+
const moduleLabel = `module-forgeax::sprite-lit#${variant.definesKey}`;
|
|
6671
|
+
let variantModule = prewarmedSpriteLitModules.get(variant.composedWgsl);
|
|
6672
|
+
if (variantModule === undefined) {
|
|
6673
|
+
const variantResult = await runShimStep(
|
|
6674
|
+
() =>
|
|
6675
|
+
asyncCreateShaderModule
|
|
6676
|
+
? asyncCreateShaderModule(rhiDevice, {
|
|
6677
|
+
code: variant.composedWgsl,
|
|
6678
|
+
label: moduleLabel,
|
|
6679
|
+
})
|
|
6680
|
+
: invokeDeviceCreateShaderModule(rhiDevice, {
|
|
6681
|
+
code: variant.composedWgsl,
|
|
6682
|
+
label: moduleLabel,
|
|
6683
|
+
}),
|
|
6684
|
+
'shader-compile-failed',
|
|
6685
|
+
`sprite-lit variant ${variant.definesKey || '<default>'} compiled`,
|
|
6686
|
+
'inspect the selected sprite-lit Standard capability variant and device.features',
|
|
6687
|
+
);
|
|
6688
|
+
if (!variantResult.ok) throw variantResult.error;
|
|
6689
|
+
variantModule = variantResult.value;
|
|
6690
|
+
prewarmedSpriteLitModules.set(variant.composedWgsl, variantModule);
|
|
6691
|
+
}
|
|
6692
|
+
seedShaderModule(moduleLabel, variantModule);
|
|
6693
|
+
}
|
|
5896
6694
|
}
|
|
5897
6695
|
|
|
5898
6696
|
// feat-20260528-fxaa-post-processing: fxaa shader module is optional
|
|
@@ -5915,6 +6713,7 @@ async function buildReadyWebGPU(
|
|
|
5915
6713
|
);
|
|
5916
6714
|
if (!fxaaShaderResult.ok) throw fxaaShaderResult.error;
|
|
5917
6715
|
fxaaModule = fxaaShaderResult.value;
|
|
6716
|
+
registerBuiltinFxaa(fxaaEntry.wgsl);
|
|
5918
6717
|
}
|
|
5919
6718
|
|
|
5920
6719
|
// feat-20260531-skybox-env-background M3 / w15: skybox shader module.
|
|
@@ -6042,7 +6841,13 @@ async function buildReadyWebGPU(
|
|
|
6042
6841
|
// buildReadyWebGPU (above) so Step 2 (variant patch) and this layout
|
|
6043
6842
|
// build read the same value.
|
|
6044
6843
|
const pbrLayouts = runShimSyncStep(
|
|
6045
|
-
() =>
|
|
6844
|
+
() =>
|
|
6845
|
+
ok(
|
|
6846
|
+
buildPbrPipelineLayouts(rhiDevice, {
|
|
6847
|
+
storageBuffer: storageBufferCapable,
|
|
6848
|
+
projectorAvailable,
|
|
6849
|
+
}),
|
|
6850
|
+
),
|
|
6046
6851
|
'webgpu-runtime-error',
|
|
6047
6852
|
'buildPbrPipelineLayouts succeeded',
|
|
6048
6853
|
'check device.limits.maxBindingsPerBindGroup (need >=14) and maxBindGroupsPerPipelineLayout',
|
|
@@ -6061,15 +6866,14 @@ async function buildReadyWebGPU(
|
|
|
6061
6866
|
// BGL at group(2) so an HDRP-variant PSO validates against the 7-slot
|
|
6062
6867
|
// group(2) bindGroup that the record stage sets via hdrpClusterBindGroup.
|
|
6063
6868
|
// Stays null when createBindGroupLayout / createPipelineLayout fails;
|
|
6064
|
-
// selectPipelineLayoutForVariant
|
|
6065
|
-
//
|
|
6066
|
-
// valid PSO instead of hard-disabling the entire HDRP-variant build path.
|
|
6869
|
+
// selectPipelineLayoutForVariant propagates that null so the clustered
|
|
6870
|
+
// material remains unavailable instead of binding an incompatible layout.
|
|
6067
6871
|
let hdrpPbrPipelineLayoutHandle: PipelineLayout | null = null;
|
|
6068
6872
|
// The cluster layout is storage-buffer backed. On the WebGL2/fallback
|
|
6069
6873
|
// capability tier its uniform-buffer projection exceeds the backend's
|
|
6070
6874
|
// fragment-stage limit, and HDRP is not a valid route there anyway.
|
|
6071
6875
|
if (storageBufferCapable) {
|
|
6072
|
-
const hdrpUnifiedBglDesc = createHdrpBindGroupLayoutDescriptor(
|
|
6876
|
+
const hdrpUnifiedBglDesc = createHdrpBindGroupLayoutDescriptor();
|
|
6073
6877
|
const hdrpUnifiedBglRes = rhiDevice.createBindGroupLayout(hdrpUnifiedBglDesc);
|
|
6074
6878
|
if (hdrpUnifiedBglRes.ok) {
|
|
6075
6879
|
const hdrpPlRes = rhiDevice.createPipelineLayout({
|
|
@@ -6140,23 +6944,26 @@ async function buildReadyWebGPU(
|
|
|
6140
6944
|
// bug-20260611-skin-pipeline-layout-mesh-array-bgl-2bindings: boot-time
|
|
6141
6945
|
// build of the skin-variant PipelineLayout. Mirrors the HDRP block above
|
|
6142
6946
|
// (D-2 / D-3 in plan-strategy): reuse view / material / instances BGLs
|
|
6143
|
-
// from `pbrLayouts` and only create a
|
|
6144
|
-
// meshes + binding 1 palette) so the
|
|
6947
|
+
// from `pbrLayouts` and only create a 3-entry mesh-array BGL (binding 0
|
|
6948
|
+
// meshes + binding 1 current palette + binding 2 previous palette) so the
|
|
6949
|
+
// `forgeax::pbr-skin` shader's
|
|
6145
6950
|
// `@group(2) @binding(1) palette` declaration validates. Stays null when
|
|
6146
6951
|
// createBindGroupLayout / createPipelineLayout fails;
|
|
6147
6952
|
// selectPipelineLayoutForVariant returns null in that case (charter P3
|
|
6148
6953
|
// explicit failure -- no silent fallback to URP layout, mirroring memory
|
|
6149
6954
|
// anchor `hdrp-active-must-not-fallback-to-urp-pipeline`).
|
|
6150
6955
|
let pbrSkinPipelineLayoutHandle: PipelineLayout | null = null;
|
|
6151
|
-
// feat-20260611 R2 / M8 / w28: capture the
|
|
6956
|
+
// feat-20260611 R2 / M8 / w28: capture the 3-binding skin mesh-array BGL
|
|
6152
6957
|
// produced by `buildPbrSkinLayouts` so the record stage can build a BG
|
|
6153
|
-
// matching `pbr-skin-pl` (pipeline-layout BGL[2] is this
|
|
6958
|
+
// matching `pbr-skin-pl` (pipeline-layout BGL[2] is this 3-entry skin
|
|
6154
6959
|
// BGL, NOT the 1-entry `pbr-mesh-array-bgl`). The BGL handle stays null
|
|
6155
6960
|
// when the skin pipeline layout itself failed to build, keeping the skin
|
|
6156
6961
|
// path explicitly disabled (charter P3 — record-stage falls back to URP
|
|
6157
6962
|
// path which is correct for non-skin entries; skin entries hit the
|
|
6158
6963
|
// explicit-failure branch).
|
|
6159
6964
|
let pbrSkinMeshBindGroupLayoutHandle: BindGroupLayout | null = null;
|
|
6965
|
+
let hdrpSkinPipelineLayoutHandle: PipelineLayout | null = null;
|
|
6966
|
+
let hdrpSkinMeshBindGroupLayoutHandle: BindGroupLayout | null = null;
|
|
6160
6967
|
{
|
|
6161
6968
|
const skinLayoutsResult = runShimSyncStep(
|
|
6162
6969
|
() =>
|
|
@@ -6172,6 +6979,29 @@ async function buildReadyWebGPU(
|
|
|
6172
6979
|
pbrSkinMeshBindGroupLayoutHandle = skinLayoutsResult.value.meshArrayBgl;
|
|
6173
6980
|
}
|
|
6174
6981
|
}
|
|
6982
|
+
// Keep HDRP skin construction on the same storage-capability gate as the
|
|
6983
|
+
// ordinary Standard cluster layout above. There is no uniform-backed
|
|
6984
|
+
// clustered skin route when storage buffers are unavailable.
|
|
6985
|
+
if (storageBufferCapable) {
|
|
6986
|
+
const skinBglResult = rhiDevice.createBindGroupLayout(
|
|
6987
|
+
createHdrpSkinBindGroupLayoutDescriptor(),
|
|
6988
|
+
);
|
|
6989
|
+
if (skinBglResult.ok) {
|
|
6990
|
+
const skinPlResult = rhiDevice.createPipelineLayout({
|
|
6991
|
+
label: 'hdrp-skin-pl',
|
|
6992
|
+
bindGroupLayouts: [
|
|
6993
|
+
viewBglResult.value,
|
|
6994
|
+
materialBglResult.value,
|
|
6995
|
+
skinBglResult.value,
|
|
6996
|
+
instancesBglResult.value,
|
|
6997
|
+
],
|
|
6998
|
+
});
|
|
6999
|
+
if (skinPlResult.ok) {
|
|
7000
|
+
hdrpSkinPipelineLayoutHandle = skinPlResult.value;
|
|
7001
|
+
hdrpSkinMeshBindGroupLayoutHandle = skinBglResult.value;
|
|
7002
|
+
}
|
|
7003
|
+
}
|
|
7004
|
+
}
|
|
6175
7005
|
|
|
6176
7006
|
// feat-20260612-skin-palette-per-frame-upload M1 / m1-2: skin palette
|
|
6177
7007
|
// allocator. Replaces the prior 16320 B identity-seeded UBO stub (PR #353,
|
|
@@ -7048,17 +7878,20 @@ async function buildReadyWebGPU(
|
|
|
7048
7878
|
fragmentEntryPoint?: string;
|
|
7049
7879
|
layout?: unknown;
|
|
7050
7880
|
label?: string;
|
|
7881
|
+
group2Contract?: PipelineGroup2Contract;
|
|
7051
7882
|
}
|
|
7052
7883
|
>();
|
|
7053
7884
|
if (unlitModule !== null)
|
|
7054
7885
|
shaderModuleMap.set('forgeax::default-unlit', {
|
|
7055
7886
|
vertex: unlitModule,
|
|
7056
7887
|
fragment: unlitModule,
|
|
7888
|
+
...(unlitGroup2Contract === undefined ? {} : { group2Contract: unlitGroup2Contract }),
|
|
7057
7889
|
});
|
|
7058
7890
|
if (pbrModule !== null)
|
|
7059
7891
|
shaderModuleMap.set('forgeax::default-standard-pbr', {
|
|
7060
7892
|
vertex: pbrModule,
|
|
7061
7893
|
fragment: pbrModule,
|
|
7894
|
+
...(pbrGroup2Contract === undefined ? {} : { group2Contract: pbrGroup2Contract }),
|
|
7062
7895
|
});
|
|
7063
7896
|
if (spriteModule !== null)
|
|
7064
7897
|
// feat-20260625-refactor-sprite-as-transparent-mesh M3 / w14 (D-7):
|
|
@@ -7070,6 +7903,7 @@ async function buildReadyWebGPU(
|
|
|
7070
7903
|
shaderModuleMap.set('forgeax::sprite', {
|
|
7071
7904
|
vertex: spriteModule,
|
|
7072
7905
|
fragment: spriteModule,
|
|
7906
|
+
...(spriteGroup2Contract === undefined ? {} : { group2Contract: spriteGroup2Contract }),
|
|
7073
7907
|
});
|
|
7074
7908
|
// feat-20260624-sprite-lit-shading-model-pure-2d-lighting M1' / t7:
|
|
7075
7909
|
// sprite-lit shader registers under canonical `forgeax::sprite-lit` —
|
|
@@ -7078,6 +7912,7 @@ async function buildReadyWebGPU(
|
|
|
7078
7912
|
shaderModuleMap.set('forgeax::sprite-lit', {
|
|
7079
7913
|
vertex: spriteLitModule,
|
|
7080
7914
|
fragment: spriteLitModule,
|
|
7915
|
+
...(spriteLitGroup2Contract === undefined ? {} : { group2Contract: spriteLitGroup2Contract }),
|
|
7081
7916
|
});
|
|
7082
7917
|
// M2-T4: fullscreen-post shader modules (tonemap + skybox) are registered
|
|
7083
7918
|
// in their respective blocks after pipeline layout creation — layout is
|
|
@@ -7179,7 +8014,6 @@ async function buildReadyWebGPU(
|
|
|
7179
8014
|
...runtimeSpecConstTable,
|
|
7180
8015
|
...runtimeLinearLdrMaterialSpecTable,
|
|
7181
8016
|
];
|
|
7182
|
-
|
|
7183
8017
|
// Boot-time pre-warm: build SPEC_CONST entries whose shader modules are
|
|
7184
8018
|
// compiled. Entries referencing a missing module are silently skipped
|
|
7185
8019
|
// (the empty-manifest path — D-3). Each build failure for an available
|
|
@@ -7212,7 +8046,7 @@ async function buildReadyWebGPU(
|
|
|
7212
8046
|
// `makeWebGPURenderer` scope) from the prewarmed `pipelineCache` for
|
|
7213
8047
|
// SPEC_CONST entries whose `variantSet !== undefined`. The URP record
|
|
7214
8048
|
// path queries `getMaterialShaderPipeline(...)` keyed off
|
|
7215
|
-
// `cacheKeyOf(spec)` with
|
|
8049
|
+
// `cacheKeyOf(spec)` with the non-clustered boot variant; both caches
|
|
7216
8050
|
// generate keys via the same `cacheKeyOf` so the lookup hits the
|
|
7217
8051
|
// boot-time prewarmed PSO instead of triggering a 1-frame async-compile
|
|
7218
8052
|
// skip-draw. Seeding only variantSet-bearing entries keeps the
|
|
@@ -7222,11 +8056,12 @@ async function buildReadyWebGPU(
|
|
|
7222
8056
|
// explicit instead of collapsing into one map (charter P3).
|
|
7223
8057
|
for (const spec of runtimeSpecConstTable) {
|
|
7224
8058
|
if (spec.shader.variantSet === undefined) continue;
|
|
7225
|
-
|
|
8059
|
+
const modules = shaderModuleMap.get(spec.shader.id);
|
|
8060
|
+
if (modules === undefined) continue;
|
|
7226
8061
|
const key = cacheKeyOf(spec);
|
|
7227
8062
|
const built = pipelineCache.get(key);
|
|
7228
|
-
if (built !== undefined) {
|
|
7229
|
-
seedMaterialShaderPipelineCache(key, built as RenderPipeline);
|
|
8063
|
+
if (built !== undefined && modules.group2Contract !== undefined) {
|
|
8064
|
+
seedMaterialShaderPipelineCache(key, built as RenderPipeline, modules.group2Contract);
|
|
7230
8065
|
}
|
|
7231
8066
|
}
|
|
7232
8067
|
|
|
@@ -7234,11 +8069,12 @@ async function buildReadyWebGPU(
|
|
|
7234
8069
|
// swap-chain view target. Seed every material entry, including no-variant
|
|
7235
8070
|
// unlit, because tonemap=none reaches this cache directly on frame one.
|
|
7236
8071
|
for (const spec of runtimeLinearLdrMaterialSpecTable) {
|
|
7237
|
-
|
|
8072
|
+
const modules = shaderModuleMap.get(spec.shader.id);
|
|
8073
|
+
if (modules === undefined) continue;
|
|
7238
8074
|
const key = cacheKeyOf(spec);
|
|
7239
8075
|
const built = pipelineCache.get(key);
|
|
7240
|
-
if (built !== undefined) {
|
|
7241
|
-
seedMaterialShaderPipelineCache(key, built as RenderPipeline);
|
|
8076
|
+
if (built !== undefined && modules.group2Contract !== undefined) {
|
|
8077
|
+
seedMaterialShaderPipelineCache(key, built as RenderPipeline, modules.group2Contract);
|
|
7242
8078
|
}
|
|
7243
8079
|
}
|
|
7244
8080
|
}
|
|
@@ -7446,15 +8282,15 @@ async function buildReadyWebGPU(
|
|
|
7446
8282
|
|
|
7447
8283
|
// feat-20260528-fxaa-post-processing M2 / w10: FXAA pipeline prebuilt.
|
|
7448
8284
|
// When the manifest contains the fxaa entry (rgb2luma marker, D-5),
|
|
7449
|
-
// construct the
|
|
8285
|
+
// construct the 3-entry BGL (texture + sampler + output-policy UBO),
|
|
7450
8286
|
// pipeline layout, fullscreen render pipeline (vertex = fullscreen
|
|
7451
8287
|
// triangle from fxaa.wgsl, fragment = FXAA 3.11 algorithm), and
|
|
7452
8288
|
// linear clamp-to-edge sampler. Mirrors the tonemap pipeline
|
|
7453
8289
|
// construction pattern directly above.
|
|
7454
8290
|
if (fxaaModule !== null) {
|
|
7455
|
-
// FXAA BindGroupLayout:
|
|
7456
|
-
//
|
|
7457
|
-
//
|
|
8291
|
+
// FXAA BindGroupLayout: 3 entries (texture + sampler + output-policy UBO).
|
|
8292
|
+
// The fxaa.wgsl fragment stage declares @binding(0) texture_2d<f32>,
|
|
8293
|
+
// @binding(1) sampler, and @binding(2) FxaaParams.
|
|
7458
8294
|
const fxaaBglResult = runShimSyncStep(
|
|
7459
8295
|
() =>
|
|
7460
8296
|
rhiDevice.createBindGroupLayout({
|
|
@@ -7470,6 +8306,11 @@ async function buildReadyWebGPU(
|
|
|
7470
8306
|
visibility: GPU_SHADER_STAGE_FRAGMENT,
|
|
7471
8307
|
sampler: { type: 'filtering' },
|
|
7472
8308
|
},
|
|
8309
|
+
{
|
|
8310
|
+
binding: 2,
|
|
8311
|
+
visibility: GPU_SHADER_STAGE_FRAGMENT,
|
|
8312
|
+
buffer: { type: 'uniform' },
|
|
8313
|
+
},
|
|
7473
8314
|
],
|
|
7474
8315
|
}),
|
|
7475
8316
|
'webgpu-runtime-error',
|
|
@@ -8505,6 +9346,7 @@ async function buildReadyWebGPU(
|
|
|
8505
9346
|
}
|
|
8506
9347
|
|
|
8507
9348
|
const pipelineState: PipelineState = {
|
|
9349
|
+
device: rhiDevice,
|
|
8508
9350
|
// feat-20260518-pbr-direct-lighting-mvp M5 / w22.10 (AC-06 dual->triple
|
|
8509
9351
|
// pipeline + D-2 + D-10): three distinct GPU render-pipeline handles
|
|
8510
9352
|
// backed by 2 distinct shader modules (unlit + pbr) x 2 vertex stride
|
|
@@ -8559,7 +9401,7 @@ async function buildReadyWebGPU(
|
|
|
8559
9401
|
hdrpClusterMembershipPipeline,
|
|
8560
9402
|
hdrpClusterMembershipBindGroupLayout,
|
|
8561
9403
|
// bug-20260611-skin-pipeline-layout: skin-variant PipelineLayout (4-BGL
|
|
8562
|
-
// chain with
|
|
9404
|
+
// chain with 3-entry mesh-array BGL substituted for the standard 1-entry
|
|
8563
9405
|
// mesh-array BGL). Built at boot above (see pbrSkinPipelineLayoutHandle);
|
|
8564
9406
|
// gated on the same unlit/pbr module presence so Camera-only and empty-
|
|
8565
9407
|
// manifest paths see null. Selected per call by
|
|
@@ -8571,13 +9413,17 @@ async function buildReadyWebGPU(
|
|
|
8571
9413
|
// Mirror the gating used by `pbrSkinPipelineLayout` -- both fields
|
|
8572
9414
|
// come from the same `buildPbrSkinLayouts` call so they live or die
|
|
8573
9415
|
// together. Surfaced on PipelineState so render-system-record can
|
|
8574
|
-
// build a
|
|
9416
|
+
// build a 3-binding BG matching `pbr-skin-pl` (was: 1-binding
|
|
8575
9417
|
// `pbr-mesh-bg` + `pbr-mesh-array-bgl`, which the device rejected
|
|
8576
9418
|
// every frame with `Bind group layout pbr-skin-mesh-array-bgl ... does
|
|
8577
9419
|
// not match layout pbr-mesh-array-bgl` + Invalid CommandBuffer +
|
|
8578
9420
|
// queue-submit-failed).
|
|
8579
9421
|
pbrSkinMeshBindGroupLayout:
|
|
8580
9422
|
unlitModule !== null && pbrModule !== null ? pbrSkinMeshBindGroupLayoutHandle : null,
|
|
9423
|
+
hdrpSkinPipelineLayout:
|
|
9424
|
+
unlitModule !== null && pbrModule !== null ? hdrpSkinPipelineLayoutHandle : null,
|
|
9425
|
+
hdrpSkinMeshBindGroupLayout:
|
|
9426
|
+
unlitModule !== null && pbrModule !== null ? hdrpSkinMeshBindGroupLayoutHandle : null,
|
|
8581
9427
|
// feat-20260612-skin-palette-per-frame-upload M1 / m1-2: animator-ready
|
|
8582
9428
|
// skin-palette allocator (replaces the prior identity-buffer stub).
|
|
8583
9429
|
// Same gating as `pbrSkinPipelineLayout` -- `null` when the skin
|
|
@@ -8589,6 +9435,7 @@ async function buildReadyWebGPU(
|
|
|
8589
9435
|
colorAttachmentFormat: swapChainFormats.view,
|
|
8590
9436
|
surfaceProfile: surfaceViewFormats ? 'dual-view' : 'raw-only',
|
|
8591
9437
|
viewBindGroupLayout: viewBglResult.value,
|
|
9438
|
+
projectorAvailable,
|
|
8592
9439
|
materialBindGroupLayout: materialBglResult.value,
|
|
8593
9440
|
meshBindGroupLayout: meshArrayBglResult.value,
|
|
8594
9441
|
viewUniformBuffer: viewUboResult.value,
|