@forgeax/engine-render 0.1.20 → 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 +21 -18
- 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-4XINDLAY.mjs → chunk-5YADUCA7.mjs} +1552 -1361
- package/dist/chunk-5YADUCA7.mjs.map +1 -0
- package/dist/{chunk-TBQJOK2L.mjs → chunk-6OPXDQ2E.mjs} +364 -51
- package/dist/chunk-6OPXDQ2E.mjs.map +1 -0
- package/dist/{chunk-M2MP4ZOU.mjs → chunk-BXBQQKFQ.mjs} +3 -9
- package/dist/chunk-BXBQQKFQ.mjs.map +1 -0
- package/dist/{chunk-CDTWTHZR.mjs → chunk-FS2KNUQH.mjs} +548 -6
- 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/construct-renderer.mjs +5826 -1003
- package/dist/construct-renderer.mjs.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 +141 -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/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 +28 -3
- 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 +10 -1
- 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 +14 -4
- 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 +5 -26
- 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 +27 -32
- package/dist/record/frame-lighting.d.ts.map +1 -1
- package/dist/record/frame-snapshot.d.ts +28 -14
- 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 +10 -4
- 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 +8 -3
- package/dist/record/gpu-timing.d.ts.map +1 -1
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/main-pass-material.d.ts +22 -1
- package/dist/record/main-pass-material.d.ts.map +1 -1
- package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
- package/dist/record/main-pass.d.ts +2 -0
- package/dist/record/main-pass.d.ts.map +1 -1
- package/dist/record/render-context.d.ts +43 -2
- 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 +72 -4
- 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 +49 -7
- package/dist/render-contract.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 +42 -2
- package/dist/render-pipeline.d.ts.map +1 -1
- package/dist/render-system-extract.d.ts +58 -17
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts +17 -2
- package/dist/render-system.d.ts.map +1 -1
- 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/index.mjs +3 -3
- package/dist/typed-render-graph-primitives.d.ts +1 -1
- package/dist/typed-render-graph-primitives.d.ts.map +1 -1
- package/dist/typed-shadow-passes.d.ts +1 -1
- package/dist/typed-shadow-passes.d.ts.map +1 -1
- package/dist/volume/passes.d.ts +2 -2
- package/dist/volume/passes.d.ts.map +1 -1
- 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__/material-observation.integration.test.ts +101 -0
- 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 +18 -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 +48 -0
- 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 +11 -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 -38
- 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/assembly/device-feature-admission.ts +73 -0
- package/src/assembly/directional-shadow-inspection.ts +122 -0
- package/src/assembly/factory.ts +877 -286
- 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 +81 -7
- package/src/components/reflection-probe.ts +49 -0
- package/src/environment/frame.ts +6 -24
- package/src/environment/lifecycle.ts +12 -0
- package/src/errors/render.ts +286 -142
- 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/hdrp-buffers.ts +165 -59
- package/src/ibl/IblPipelineCache.ts +11 -0
- package/src/ibl/kernel-cache.ts +88 -0
- package/src/index.ts +148 -0
- package/src/inspection-types.ts +56 -0
- package/src/instances-derived-bounds.ts +171 -0
- package/src/light-buffer-layout.ts +20 -5
- package/src/mesh-material-bindings.ts +132 -0
- package/src/pbr-pipeline.ts +106 -17
- package/src/pipeline/standard-forward-lane.ts +46 -16
- 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 +122 -193
- package/src/pipeline/standard-post.ts +26 -7
- package/src/pipeline/standard-profile.ts +17 -47
- 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 +367 -427
- package/src/record/frame-snapshot.ts +35 -27
- package/src/record/frame-targets.ts +17 -0
- package/src/record/frame.ts +261 -74
- 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 +21 -12
- package/src/record/main-pass-geometry.ts +120 -80
- package/src/record/main-pass-material.ts +95 -9
- package/src/record/main-pass-sprite-draws.ts +54 -25
- package/src/record/main-pass.ts +71 -15
- package/src/record/render-context.ts +72 -0
- package/src/record/shadow-pass.ts +17 -28
- package/src/record/skybox-post-pass.ts +20 -4
- package/src/record/typed-frame-graph.ts +893 -44
- 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 +76 -5
- package/src/render-graph-primitives.ts +9 -4
- package/src/render-pipeline.ts +117 -2
- package/src/render-system-extract.ts +557 -100
- package/src/render-system.ts +1095 -66
- package/src/renderer.ts +222 -0
- package/src/scene/render-scene.ts +115 -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/transmission/__tests__/standard-transmission.dawn.test.ts +59 -20
- package/src/typed-render-graph-primitives.ts +5 -3
- package/src/typed-shadow-passes.ts +46 -40
- package/src/volume/passes.ts +15 -9
- package/dist/chunk-4XINDLAY.mjs.map +0 -1
- package/dist/chunk-CDTWTHZR.mjs.map +0 -1
- package/dist/chunk-M2MP4ZOU.mjs.map +0 -1
- package/dist/chunk-TBQJOK2L.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,
|
|
@@ -92,12 +91,16 @@ import {
|
|
|
92
91
|
RendererOperationError,
|
|
93
92
|
RenderFeatureCapabilityMissingError,
|
|
94
93
|
RenderFeatureStageFailedError,
|
|
94
|
+
StandardProfileInvalidError,
|
|
95
95
|
} from '../errors/render';
|
|
96
96
|
import { createRenderFeatureHost, type RenderFeatureHost } from '../features/host';
|
|
97
97
|
import type { RenderFeatureMaterialShaderBindingContract } from '../features/plan';
|
|
98
98
|
import { RENDER_FEATURE_VERTEX_LAYOUTS } from '../features/prepared-graphics';
|
|
99
99
|
import type { RenderFeature, RenderFeatureShaderModuleMode } from '../features/types';
|
|
100
|
-
import
|
|
100
|
+
import {
|
|
101
|
+
type PostProcessShaderEntry,
|
|
102
|
+
postProcessShaderModuleLabel,
|
|
103
|
+
} from '../fullscreen-post-process-pass';
|
|
101
104
|
import { glyphTextLayoutSystem } from '../glyph-text-layout-system';
|
|
102
105
|
import { GpuBuffer } from '../gpu-resource';
|
|
103
106
|
import { GPU_SHADER_STAGE_FRAGMENT, GPU_SHADER_STAGE_VERTEX } from '../gpu-stage';
|
|
@@ -138,7 +141,9 @@ import {
|
|
|
138
141
|
buildPbrMaterialUserRegionEntries,
|
|
139
142
|
buildPbrPipelineLayouts,
|
|
140
143
|
buildPbrSkinLayouts,
|
|
141
|
-
|
|
144
|
+
createHdrpSkinBindGroupLayoutDescriptor,
|
|
145
|
+
type PipelineGroup2Contract,
|
|
146
|
+
resolvePipelineGroup2Contract,
|
|
142
147
|
SKIN_MATERIAL_SHADER_ID,
|
|
143
148
|
} from '../pbr-pipeline';
|
|
144
149
|
import { STANDARD_CLUSTER_MEMBERSHIP_WGSL, standardPipeline } from '../pipeline/standard-pipeline';
|
|
@@ -165,6 +170,14 @@ import {
|
|
|
165
170
|
} from '../pipeline-spec';
|
|
166
171
|
import { POINTS_LINES_MATERIAL_SHADER_ID } from '../points-lines/record';
|
|
167
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';
|
|
168
181
|
import { GpuTimingCapture, type VolumeTimingObservation } from '../record/gpu-timing';
|
|
169
182
|
import type { BloomPersistentBundle } from '../record/render-context';
|
|
170
183
|
import { SHADOW_CASTER_BUFFER_SIZE } from '../record/shadow-pass';
|
|
@@ -190,8 +203,10 @@ import type {
|
|
|
190
203
|
RenderWorldLease,
|
|
191
204
|
VolumetricFogShaderSources,
|
|
192
205
|
} from '../render-contract';
|
|
193
|
-
import { STANDARD_OUTPUT_TRANSFORM_FEATURE_ID } from '../render-contract';
|
|
206
|
+
import { FXAA_POST_PROCESS_ID, STANDARD_OUTPUT_TRANSFORM_FEATURE_ID } from '../render-contract';
|
|
207
|
+
|
|
194
208
|
import {
|
|
209
|
+
attachGpuPassTimingSession,
|
|
195
210
|
configureSurface,
|
|
196
211
|
createRenderSystem,
|
|
197
212
|
MATERIAL_PER_ENTITY_STRIDE,
|
|
@@ -199,6 +214,7 @@ import {
|
|
|
199
214
|
type RenderSystem,
|
|
200
215
|
selectSwapChainFormat,
|
|
201
216
|
} from '../render-system';
|
|
217
|
+
import { createGpuPassTimingObservationStore, observeGpuPassTimingDisabled } from '../renderer.js';
|
|
202
218
|
import { postSpawnResolveJoints } from '../scene-instances/post-spawn-resolve-joints';
|
|
203
219
|
import {
|
|
204
220
|
createSkinPaletteAllocator,
|
|
@@ -206,7 +222,12 @@ import {
|
|
|
206
222
|
} from '../systems/skin-palette-allocator';
|
|
207
223
|
import { tilemapChunkExtractSystem } from '../tilemap-chunk-extract-system';
|
|
208
224
|
import { EngineEnvironmentError, type RhiBackendPack } from './backend-contract';
|
|
225
|
+
import {
|
|
226
|
+
deviceOptionsForAdapter,
|
|
227
|
+
STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES,
|
|
228
|
+
} from './device-feature-admission';
|
|
209
229
|
import type { RendererAssemblyImplementation, RendererHostImplementation } from './host-contract';
|
|
230
|
+
import { createRenderTargetHost } from './render-target-host';
|
|
210
231
|
import { projectRendererOutputInspection } from './renderer-inspection';
|
|
211
232
|
import { isRendererStateOperational } from './renderer-lifecycle';
|
|
212
233
|
|
|
@@ -226,6 +247,19 @@ const GPU_ENVIRONMENT_ERROR_CODES = new Set([
|
|
|
226
247
|
'oom',
|
|
227
248
|
]);
|
|
228
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
|
+
|
|
229
263
|
function classifyEnvErrorReason(
|
|
230
264
|
base: string,
|
|
231
265
|
primary: { code?: unknown; name?: unknown } | undefined,
|
|
@@ -450,18 +484,14 @@ export interface BundlerOptions {
|
|
|
450
484
|
/**
|
|
451
485
|
* Per-variant PipelineLayout selector (M4.5 / D-10 option A).
|
|
452
486
|
*
|
|
453
|
-
* The HDRP
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
457
|
-
* 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.
|
|
458
491
|
*
|
|
459
|
-
* Returns `null`
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
* resolved upstream by `findVariantByKey` is the URP variant in that
|
|
463
|
-
* case (manifest entry with all-true definesKey === '' was registered
|
|
464
|
-
* 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.
|
|
465
495
|
*
|
|
466
496
|
* Pure function — exported for unit-test access (M4.5 / w35).
|
|
467
497
|
*/
|
|
@@ -469,8 +499,9 @@ export interface BundlerOptions {
|
|
|
469
499
|
* Closed enum of pipeline-layout kinds the selector can dispatch to.
|
|
470
500
|
*
|
|
471
501
|
* `pbr` — standard PBR (1-entry mesh-array BGL).
|
|
472
|
-
* `pbr-skin` — bug-20260611-skin-pipeline-layout:
|
|
473
|
-
* (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`
|
|
474
505
|
* material shader.
|
|
475
506
|
* `hdrp-pbr` — feat-20260609 HDRP cluster-forward variant (7-slot group(2)
|
|
476
507
|
* BGL substituted at slot 2).
|
|
@@ -490,7 +521,15 @@ export interface BundlerOptions {
|
|
|
490
521
|
* material-shader layouts add a `LayoutKind` member + a PipelineState slot
|
|
491
522
|
* + a switch arm here in one cut (charter P3 extensibility).
|
|
492
523
|
*/
|
|
493
|
-
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
|
+
}
|
|
494
533
|
|
|
495
534
|
/**
|
|
496
535
|
* Return every device-capability HDRP PBR variant that needs a lazy module
|
|
@@ -503,13 +542,21 @@ export function selectHdrpPbrPrewarmVariants(
|
|
|
503
542
|
manifestEntry: MaterialShaderManifestEntry | undefined,
|
|
504
543
|
storageBufferCapable: boolean,
|
|
505
544
|
transmissionCapable: boolean,
|
|
545
|
+
directionalPcssAvailable = true,
|
|
506
546
|
projectorAvailable: boolean = true,
|
|
507
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 [];
|
|
508
553
|
return (
|
|
509
554
|
manifestEntry?.variants.filter(
|
|
510
555
|
(variant) =>
|
|
511
556
|
variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
|
|
512
557
|
variant.defines.CLUSTER_FORWARD_AVAILABLE === true &&
|
|
558
|
+
(!('DIRECTIONAL_PCSS_AVAILABLE' in variant.defines) ||
|
|
559
|
+
variant.defines.DIRECTIONAL_PCSS_AVAILABLE === directionalPcssAvailable) &&
|
|
513
560
|
(!('PROJECTOR_AVAILABLE' in variant.defines) ||
|
|
514
561
|
variant.defines.PROJECTOR_AVAILABLE === projectorAvailable) &&
|
|
515
562
|
(transmissionCapable || variant.defines.TRANSMISSION_AVAILABLE !== true),
|
|
@@ -517,6 +564,22 @@ export function selectHdrpPbrPrewarmVariants(
|
|
|
517
564
|
);
|
|
518
565
|
}
|
|
519
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
|
+
|
|
520
583
|
/**
|
|
521
584
|
* Select both Standard transmission states from the manifest for exact-label
|
|
522
585
|
* prewarming. The requested keys come from declared variants; no key is
|
|
@@ -525,6 +588,7 @@ export function selectHdrpPbrPrewarmVariants(
|
|
|
525
588
|
export function selectStandardPbrTransmissionPrewarmVariants(
|
|
526
589
|
manifestEntry: MaterialShaderManifestEntry | undefined,
|
|
527
590
|
storageBufferCapable: boolean,
|
|
591
|
+
directionalPcssAvailable = true,
|
|
528
592
|
projectorAvailable: boolean = true,
|
|
529
593
|
): readonly MaterialShaderManifestEntry['variants'][number][] {
|
|
530
594
|
if (manifestEntry === undefined) {
|
|
@@ -535,8 +599,10 @@ export function selectStandardPbrTransmissionPrewarmVariants(
|
|
|
535
599
|
const declared = manifestEntry.variants.find(
|
|
536
600
|
(variant) =>
|
|
537
601
|
variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
|
|
538
|
-
variant.defines.CLUSTER_FORWARD_AVAILABLE ===
|
|
602
|
+
variant.defines.CLUSTER_FORWARD_AVAILABLE === standardBootClusterAxis() &&
|
|
539
603
|
variant.defines.VERTEX_COLOR_AVAILABLE === false &&
|
|
604
|
+
(!('DIRECTIONAL_PCSS_AVAILABLE' in variant.defines) ||
|
|
605
|
+
variant.defines.DIRECTIONAL_PCSS_AVAILABLE === directionalPcssAvailable) &&
|
|
540
606
|
(!('PROJECTOR_AVAILABLE' in variant.defines) ||
|
|
541
607
|
variant.defines.PROJECTOR_AVAILABLE === projectorAvailable) &&
|
|
542
608
|
variant.defines.TRANSMISSION_AVAILABLE === transmissionAvailable,
|
|
@@ -566,16 +632,16 @@ export function selectNoColorPbrVariant(
|
|
|
566
632
|
manifestEntry: MaterialShaderManifestEntry | undefined,
|
|
567
633
|
storageBufferCapable: boolean,
|
|
568
634
|
variantSet: string | undefined,
|
|
635
|
+
directionalPcssAvailable = true,
|
|
569
636
|
projectorAvailable: boolean = true,
|
|
570
637
|
): MaterialShaderManifestEntry['variants'][number] | undefined {
|
|
571
|
-
const hdrpRequested =
|
|
572
|
-
variantSet === '' ||
|
|
573
|
-
// biome-ignore lint/complexity/useOptionalChain: G-14 requires an explicit undefined guard so no variant request becomes false.
|
|
574
|
-
(variantSet !== undefined && variantSet.includes('CLUSTER_FORWARD_AVAILABLE=true'));
|
|
638
|
+
const hdrpRequested = variantSet === '' || requestsClusterVariant(variantSet);
|
|
575
639
|
return manifestEntry?.variants.find(
|
|
576
640
|
(variant) =>
|
|
577
641
|
variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
|
|
578
642
|
variant.defines.VERTEX_COLOR_AVAILABLE === false &&
|
|
643
|
+
(!('DIRECTIONAL_PCSS_AVAILABLE' in variant.defines) ||
|
|
644
|
+
variant.defines.DIRECTIONAL_PCSS_AVAILABLE === directionalPcssAvailable) &&
|
|
579
645
|
(!('PROJECTOR_AVAILABLE' in variant.defines) ||
|
|
580
646
|
variant.defines.PROJECTOR_AVAILABLE === projectorAvailable) &&
|
|
581
647
|
(!('CLUSTER_FORWARD_AVAILABLE' in variant.defines) ||
|
|
@@ -583,6 +649,11 @@ export function selectNoColorPbrVariant(
|
|
|
583
649
|
);
|
|
584
650
|
}
|
|
585
651
|
|
|
652
|
+
function requestsClusterVariant(variantSet: string | undefined): boolean {
|
|
653
|
+
if (variantSet === undefined) return false;
|
|
654
|
+
return variantSet.includes('CLUSTER_FORWARD_AVAILABLE=true');
|
|
655
|
+
}
|
|
656
|
+
|
|
586
657
|
export type MaterialShaderBindingContract = RenderFeatureMaterialShaderBindingContract;
|
|
587
658
|
export type MaterialShaderVertexInputContract = 'none' | 'render-material';
|
|
588
659
|
|
|
@@ -709,8 +780,9 @@ export function selectPipelineLayoutForVariant(
|
|
|
709
780
|
readonly pbrPipelineLayout: PipelineLayout | null;
|
|
710
781
|
readonly hdrpPbrPipelineLayout: PipelineLayout | null;
|
|
711
782
|
readonly pbrSkinPipelineLayout: PipelineLayout | null;
|
|
783
|
+
readonly hdrpSkinPipelineLayout?: PipelineLayout | null;
|
|
712
784
|
} | null,
|
|
713
|
-
|
|
785
|
+
_variantSet: string | undefined,
|
|
714
786
|
layoutKind?: LayoutKind,
|
|
715
787
|
): PipelineLayout | null {
|
|
716
788
|
if (state === null) return null;
|
|
@@ -723,6 +795,9 @@ export function selectPipelineLayoutForVariant(
|
|
|
723
795
|
if (layoutKind === 'pbr-skin') {
|
|
724
796
|
return state.pbrSkinPipelineLayout;
|
|
725
797
|
}
|
|
798
|
+
if (layoutKind === 'hdrp-skin') {
|
|
799
|
+
return state.hdrpSkinPipelineLayout ?? null;
|
|
800
|
+
}
|
|
726
801
|
// bug-20260708 M2 (c): sprite / sprite-lit shaders reuse the URP 1-slot
|
|
727
802
|
// mesh-array pipeline layout but their canonical all-true variant key
|
|
728
803
|
// `''` (SPRITE_PASS_PER_INSTANCE_REGION_VARIANT_SET) must NOT trigger
|
|
@@ -738,33 +813,19 @@ export function selectPipelineLayoutForVariant(
|
|
|
738
813
|
if (layoutKind === 'unlit-urp') {
|
|
739
814
|
return state.pbrPipelineLayout;
|
|
740
815
|
}
|
|
741
|
-
//
|
|
742
|
-
//
|
|
743
|
-
// the
|
|
744
|
-
//
|
|
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.
|
|
745
820
|
if (layoutKind === 'pbr') {
|
|
746
821
|
return state.pbrPipelineLayout;
|
|
747
822
|
}
|
|
748
|
-
//
|
|
749
|
-
//
|
|
750
|
-
//
|
|
751
|
-
//
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
// URP variants have CLUSTER_FORWARD_AVAILABLE=false explicitly, or omit
|
|
755
|
-
// the axis altogether, or pass undefined (no variant routing).
|
|
756
|
-
// G-14 (D-11): empty-string is the canonical all-true HDRP key, so we
|
|
757
|
-
// explicitly distinguish '' vs undefined rather than using an optional-chain
|
|
758
|
-
// (charter P3: explicit failure / no implicit empty-vs-missing collapse).
|
|
759
|
-
const isHdrpVariant =
|
|
760
|
-
layoutKind === 'hdrp-pbr' ||
|
|
761
|
-
variantSet === '' ||
|
|
762
|
-
// biome-ignore lint/complexity/useOptionalChain: G-14 grep gate forbids the optional-chain falsy collapse on variantSet.
|
|
763
|
-
(variantSet !== undefined && variantSet.includes('CLUSTER_FORWARD_AVAILABLE=true'));
|
|
764
|
-
if (isHdrpVariant && state.hdrpPbrPipelineLayout !== null) {
|
|
765
|
-
return state.hdrpPbrPipelineLayout;
|
|
766
|
-
}
|
|
767
|
-
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;
|
|
768
829
|
}
|
|
769
830
|
|
|
770
831
|
/**
|
|
@@ -786,6 +847,7 @@ export function resolveMaterialShaderVariantSet(
|
|
|
786
847
|
const declaresCapabilityVariants =
|
|
787
848
|
declaredAxes.has('WEBGL2_COMPAT') ||
|
|
788
849
|
declaredAxes.has('STORAGE_BUFFER_AVAILABLE') ||
|
|
850
|
+
declaredAxes.has('DIRECTIONAL_PCSS_AVAILABLE') ||
|
|
789
851
|
declaredAxes.has('PROJECTOR_AVAILABLE');
|
|
790
852
|
if (!declaresCapabilityVariants) return requestedVariantSet;
|
|
791
853
|
|
|
@@ -804,22 +866,24 @@ export function resolveMaterialShaderVariantSet(
|
|
|
804
866
|
: axis === 'SKINNING_DISABLED'
|
|
805
867
|
? (requested[axis] ?? true)
|
|
806
868
|
: axis === 'CLUSTER_FORWARD_AVAILABLE'
|
|
807
|
-
? backendKind !== 'wgpu-webgl2' && (requested[axis] ?? false)
|
|
869
|
+
? storageBuffer && backendKind !== 'wgpu-webgl2' && (requested[axis] ?? false)
|
|
808
870
|
: axis === 'PER_INSTANCE_REGION'
|
|
809
871
|
? requestedVariantSet === '' || (requested[axis] ?? false)
|
|
810
872
|
: axis === 'VERTEX_COLOR_AVAILABLE'
|
|
811
873
|
? requestedVariantSet === '' || (requested[axis] ?? false)
|
|
812
874
|
: axis === 'TRANSMISSION_AVAILABLE'
|
|
813
875
|
? (requested[axis] ?? false)
|
|
814
|
-
: axis === '
|
|
815
|
-
?
|
|
816
|
-
:
|
|
876
|
+
: axis === 'DIRECTIONAL_PCSS_AVAILABLE'
|
|
877
|
+
? directionalPcssCapable(backendKind)
|
|
878
|
+
: axis === 'PROJECTOR_AVAILABLE'
|
|
879
|
+
? projectorAvailable
|
|
880
|
+
: (requested[axis] ?? true);
|
|
817
881
|
}
|
|
818
882
|
const sorted = Object.entries(filtered).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
819
883
|
return sorted.every(([, value]) => value)
|
|
820
|
-
? requestedVariantSet ===
|
|
821
|
-
?
|
|
822
|
-
:
|
|
884
|
+
? requestedVariantSet === undefined
|
|
885
|
+
? undefined
|
|
886
|
+
: ''
|
|
823
887
|
: sorted.map(([name, value]) => `${name}=${value}`).join('+');
|
|
824
888
|
}
|
|
825
889
|
|
|
@@ -883,6 +947,28 @@ export async function createRenderer(
|
|
|
883
947
|
});
|
|
884
948
|
}
|
|
885
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
|
+
|
|
886
972
|
// ─── WebGPU branch ──────────────────────────────────────────────────────────
|
|
887
973
|
|
|
888
974
|
/**
|
|
@@ -993,50 +1079,6 @@ type WebGPUOutcome =
|
|
|
993
1079
|
| { kind: 'rhi-err'; error: RhiError }
|
|
994
1080
|
| { kind: 'throw'; error: Error };
|
|
995
1081
|
|
|
996
|
-
// Optional compression is exposed on a device only when it is requested from
|
|
997
|
-
// the adapter. Keep this capability projection shared by initial construction
|
|
998
|
-
// and device-loss recovery; otherwise recovery silently drops BC/ETC2/ASTC
|
|
999
|
-
// and already-catalogued compressed textures fail on the first new frame.
|
|
1000
|
-
const COMPRESSION_FEATURES: GPUFeatureName[] = [
|
|
1001
|
-
'texture-compression-bc',
|
|
1002
|
-
'texture-compression-etc2',
|
|
1003
|
-
'texture-compression-astc',
|
|
1004
|
-
];
|
|
1005
|
-
|
|
1006
|
-
// The Standard transmission fragment ABI needs 17 sampled textures across its
|
|
1007
|
-
// material, IBL, clustered-light, and SSAO bindings. Ordinary Standard draws
|
|
1008
|
-
// use the non-transmission variant and remain valid on the WebGPU minimum of
|
|
1009
|
-
// 16; only request the elevated limit when the adapter can provide it.
|
|
1010
|
-
const STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES = 17;
|
|
1011
|
-
|
|
1012
|
-
function deviceOptionsForAdapter(
|
|
1013
|
-
adapter: Pick<RhiAdapter, 'features' | 'limits'>,
|
|
1014
|
-
options?: Pick<RendererOptions, 'captureGpuTimings'>,
|
|
1015
|
-
):
|
|
1016
|
-
| {
|
|
1017
|
-
requiredFeatures?: GPUFeatureName[];
|
|
1018
|
-
requiredLimits?: GPUDeviceDescriptor['requiredLimits'];
|
|
1019
|
-
}
|
|
1020
|
-
| undefined {
|
|
1021
|
-
const requiredFeatures = COMPRESSION_FEATURES.filter((feature) => adapter.features.has(feature));
|
|
1022
|
-
const supportsTransmission =
|
|
1023
|
-
(adapter.limits.maxSampledTexturesPerShaderStage ?? 0) >=
|
|
1024
|
-
STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES;
|
|
1025
|
-
if (options?.captureGpuTimings === true && adapter.features.has('timestamp-query')) {
|
|
1026
|
-
requiredFeatures.push('timestamp-query');
|
|
1027
|
-
}
|
|
1028
|
-
return {
|
|
1029
|
-
...(requiredFeatures.length > 0 ? { requiredFeatures } : {}),
|
|
1030
|
-
...(supportsTransmission
|
|
1031
|
-
? {
|
|
1032
|
-
requiredLimits: {
|
|
1033
|
-
maxSampledTexturesPerShaderStage: STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES,
|
|
1034
|
-
},
|
|
1035
|
-
}
|
|
1036
|
-
: {}),
|
|
1037
|
-
};
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
1082
|
/**
|
|
1041
1083
|
* Goes through the strict two-step path exposed by the injected backend pack:
|
|
1042
1084
|
* `rhi.requestAdapter()` -> `adapter.requestDevice()`
|
|
@@ -1077,7 +1119,12 @@ async function tryCreateWebGPURenderer(
|
|
|
1077
1119
|
// backend singleton (rhi-webgpu / rhi-wgpu / explicit escape hatch).
|
|
1078
1120
|
// w19: thread canvas as compatibleSurface so the wgpu GL backend can
|
|
1079
1121
|
// enumerate adapters against it (escape hatch, capability-gated).
|
|
1122
|
+
markRendererBootstrapStage('adapter-request-start');
|
|
1080
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
|
+
});
|
|
1081
1128
|
if (!adapterResult.ok) {
|
|
1082
1129
|
return { kind: 'rhi-err', error: adapterResult.error };
|
|
1083
1130
|
}
|
|
@@ -1085,11 +1132,19 @@ async function tryCreateWebGPURenderer(
|
|
|
1085
1132
|
// M4 w28: filter compression features from adapter.features (AC-07). The
|
|
1086
1133
|
// same helper is used by recover() below so both paths share one profile.
|
|
1087
1134
|
const deviceOpts = deviceOptionsForAdapter(adapter, options);
|
|
1135
|
+
markRendererBootstrapStage('device-request-start');
|
|
1088
1136
|
const result = await adapter.requestDevice(deviceOpts);
|
|
1137
|
+
markRendererBootstrapStage('device-request-end', {
|
|
1138
|
+
ok: result.ok,
|
|
1139
|
+
...(result.ok ? {} : { code: result.error.code }),
|
|
1140
|
+
});
|
|
1089
1141
|
if (!result.ok) {
|
|
1090
1142
|
return { kind: 'rhi-err', error: result.error };
|
|
1091
1143
|
}
|
|
1092
1144
|
const device: RhiDevice = result.value;
|
|
1145
|
+
markRendererBootstrapStage('device-acquired', {
|
|
1146
|
+
backendKind: device.caps.backendKind,
|
|
1147
|
+
});
|
|
1093
1148
|
|
|
1094
1149
|
// Acquire the canvas context through the backend pack (M3 / w16).
|
|
1095
1150
|
// rhi-webgpu: internally calls canvas.getContext('webgpu') + branded wrap.
|
|
@@ -1220,12 +1275,24 @@ interface WebGPURendererInternals {
|
|
|
1220
1275
|
featureHost?: RenderFeatureHost;
|
|
1221
1276
|
/** App-owned overlay capability read by the typed graph at frame time. */
|
|
1222
1277
|
debugOverlay?: RenderDebugOverlay;
|
|
1278
|
+
gpuPassTimingSubmittedWork?: Promise<void> | undefined;
|
|
1223
1279
|
}
|
|
1224
1280
|
|
|
1225
1281
|
async function makeWebGPURenderer(
|
|
1226
1282
|
internals: WebGPURendererInternals,
|
|
1227
1283
|
): Promise<RendererAssemblyImplementation> {
|
|
1228
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
|
+
});
|
|
1229
1296
|
const featureHostResult = createRenderFeatureHost(internals.options?.features ?? []);
|
|
1230
1297
|
if (!featureHostResult.ok) throw featureHostResult.error;
|
|
1231
1298
|
internals.featureHost = featureHostResult.value;
|
|
@@ -1240,6 +1307,28 @@ async function makeWebGPURenderer(
|
|
|
1240
1307
|
featureHostResult.value.features.flatMap((feature) => feature.requiredMaterialShaders ?? []),
|
|
1241
1308
|
);
|
|
1242
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
|
+
);
|
|
1243
1332
|
// Lazy ShaderCatalog instance (plan-strategy section S-10 / D-R10 / OQ-5
|
|
1244
1333
|
// close): constructed on first access; subsequent accesses return the
|
|
1245
1334
|
// same instance.
|
|
@@ -1282,6 +1371,28 @@ async function makeWebGPURenderer(
|
|
|
1282
1371
|
}
|
|
1283
1372
|
return sharedImmediateShaderModuleAdapter;
|
|
1284
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
|
+
};
|
|
1285
1396
|
const getShader = (): ShaderCatalog => {
|
|
1286
1397
|
if (shaderInstance === null) {
|
|
1287
1398
|
// feat-20260608-create-app-param-surface-trim / M2 / D-2 q5-A:
|
|
@@ -1346,6 +1457,56 @@ async function makeWebGPURenderer(
|
|
|
1346
1457
|
// lives in a standalone store; `assets` keeps the CPU POD registry only.
|
|
1347
1458
|
const gpuStore = new GpuResidencyCache();
|
|
1348
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();
|
|
1349
1510
|
// feat-20260623-world-space-video-asset M4 / w16 (D-3): transient per-frame
|
|
1350
1511
|
// video texture store, fully independent of gpuStore (AC-08). Configured with
|
|
1351
1512
|
// the device alongside gpuStore below; threaded into the record stage via the
|
|
@@ -1479,7 +1640,10 @@ async function makeWebGPURenderer(
|
|
|
1479
1640
|
// (the seeding closure captures this Map by reference; ordering is safe
|
|
1480
1641
|
// because the prewarm step runs strictly before the first frame's
|
|
1481
1642
|
// `getMaterialShaderPipeline` lookup that would consume it).
|
|
1482
|
-
const materialShaderPipelineCache = new Map<
|
|
1643
|
+
const materialShaderPipelineCache = new Map<
|
|
1644
|
+
string,
|
|
1645
|
+
{ readonly pipeline: RenderPipeline; readonly group2Contract: PipelineGroup2Contract }
|
|
1646
|
+
>();
|
|
1483
1647
|
const materialShaderManifestEntryCache = new Map<string, MaterialShaderManifestEntry>();
|
|
1484
1648
|
// Variant resolution only depends on the requested axes, device capability
|
|
1485
1649
|
// axes, and the manifest entry that owns the declarations. Cache it by the
|
|
@@ -1798,6 +1962,7 @@ async function makeWebGPURenderer(
|
|
|
1798
1962
|
// `internals.device` / `internals.pack` are read live, so a rebuild after a
|
|
1799
1963
|
// device swap compiles against the new device.
|
|
1800
1964
|
let tonemapRegistered = false;
|
|
1965
|
+
let fxaaRegistered = false;
|
|
1801
1966
|
let temporalPostProcessesRegistered = false;
|
|
1802
1967
|
const buildPipeline = (scope: DeviceScope = activeDeviceScope): Promise<PipelineState> =>
|
|
1803
1968
|
buildReadyWebGPU(
|
|
@@ -1808,6 +1973,7 @@ async function makeWebGPURenderer(
|
|
|
1808
1973
|
internals.pack.createShaderModule,
|
|
1809
1974
|
internals.errorRegistry,
|
|
1810
1975
|
requiredMaterialShaders,
|
|
1976
|
+
requiredFullscreenPostProcesses,
|
|
1811
1977
|
// feat-20260608-mesh-ssbo-dynamic-grow-l1-lift-1024-entity-cap M2 /
|
|
1812
1978
|
// T-M2-05 + M3 / T-M3-04: callback set by buildReadyWebGPU once
|
|
1813
1979
|
// meshSsboController is wired. Both the grow hook AND the read-only
|
|
@@ -1827,9 +1993,12 @@ async function makeWebGPURenderer(
|
|
|
1827
1993
|
// SPEC_CONST prewarm. Closure captures the Map declared just above so
|
|
1828
1994
|
// the URP-variant prewarmed PSOs become visible to the URP record path
|
|
1829
1995
|
// immediately (no 1-frame async-compile skip-draw window).
|
|
1830
|
-
(key, pso) => {
|
|
1996
|
+
(key, pso, group2Contract) => {
|
|
1831
1997
|
if (!materialShaderPipelineCache.has(key)) {
|
|
1832
|
-
materialShaderPipelineCache.set(key,
|
|
1998
|
+
materialShaderPipelineCache.set(key, {
|
|
1999
|
+
pipeline: pso,
|
|
2000
|
+
group2Contract,
|
|
2001
|
+
});
|
|
1833
2002
|
}
|
|
1834
2003
|
},
|
|
1835
2004
|
// feat-20260621 M-A3 (D-5): register the built-in tonemap on the unified
|
|
@@ -1848,6 +2017,19 @@ async function makeWebGPURenderer(
|
|
|
1848
2017
|
});
|
|
1849
2018
|
tonemapRegistered = true;
|
|
1850
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
|
+
},
|
|
1851
2033
|
(entries: { readonly motionBlur?: string; readonly taaResolve?: string }) => {
|
|
1852
2034
|
if (temporalPostProcessesRegistered) return;
|
|
1853
2035
|
if (entries.motionBlur !== undefined) {
|
|
@@ -1870,14 +2052,19 @@ async function makeWebGPURenderer(
|
|
|
1870
2052
|
internals.volumetricFogShaders = sources;
|
|
1871
2053
|
},
|
|
1872
2054
|
);
|
|
2055
|
+
markRendererBootstrapStage('renderer-initialization-start');
|
|
1873
2056
|
const ready: Promise<Result<void, RhiError>> = buildPipeline().then(
|
|
1874
2057
|
(state): Result<void, RhiError> => {
|
|
1875
2058
|
pipelineState = state;
|
|
1876
2059
|
readySettled = true;
|
|
2060
|
+
markRendererBootstrapStage('renderer-initialized');
|
|
1877
2061
|
return ok(undefined);
|
|
1878
2062
|
},
|
|
1879
2063
|
(e: unknown): Result<void, RhiError> => {
|
|
1880
2064
|
readySettled = true;
|
|
2065
|
+
markRendererBootstrapStage('renderer-initialization-failed', {
|
|
2066
|
+
message: e instanceof Error ? e.message : String(e),
|
|
2067
|
+
});
|
|
1881
2068
|
if (e instanceof RhiError) return err(e);
|
|
1882
2069
|
const message = e instanceof Error ? e.message : String(e);
|
|
1883
2070
|
return err(
|
|
@@ -1914,6 +2101,7 @@ async function makeWebGPURenderer(
|
|
|
1914
2101
|
pbrManifestEntry,
|
|
1915
2102
|
internals.device.caps.storageBuffer,
|
|
1916
2103
|
variantSet,
|
|
2104
|
+
directionalPcssCapable(internals.device.caps.backendKind),
|
|
1917
2105
|
(internals.device.limits.maxSampledTexturesPerShaderStage ?? 0) >=
|
|
1918
2106
|
STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES,
|
|
1919
2107
|
);
|
|
@@ -1931,10 +2119,10 @@ async function makeWebGPURenderer(
|
|
|
1931
2119
|
// paths (D-3). Null when manifest is empty (Camera-only path).
|
|
1932
2120
|
//
|
|
1933
2121
|
// feat-20260609-hdrp-cluster-fragment-ggx M4.5 / w37 (D-10 option A):
|
|
1934
|
-
// `variantSet`
|
|
1935
|
-
//
|
|
1936
|
-
//
|
|
1937
|
-
//
|
|
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.
|
|
1938
2126
|
const buildPipelineContext = (
|
|
1939
2127
|
variantSet?: string,
|
|
1940
2128
|
materialShaderId?: string,
|
|
@@ -1945,40 +2133,31 @@ async function makeWebGPURenderer(
|
|
|
1945
2133
|
// consume. A caller passing a real `MeshAsset.attributes` produces the
|
|
1946
2134
|
// exact 6-attribute / 72-byte layout the skin shader expects; passing
|
|
1947
2135
|
// undefined falls into the synthetic 6-key sentinel below (the skin
|
|
1948
|
-
// path is fully determined by
|
|
1949
|
-
// signal `deriveVertexBufferLayout` reads
|
|
2136
|
+
// path is fully determined by the resolved group-2 contract, so
|
|
2137
|
+
// key-presence is the only signal `deriveVertexBufferLayout` reads).
|
|
1950
2138
|
meshAttributes?: VertexAttributeMap,
|
|
1951
2139
|
vertexLayout?: string,
|
|
1952
2140
|
vertexLayoutProjection?: VertexLayoutProjection,
|
|
1953
2141
|
shaderModuleMode?: RenderFeatureShaderModuleMode,
|
|
2142
|
+
group2Contract: PipelineGroup2Contract = 'mesh',
|
|
1954
2143
|
layoutKindOverride?: LayoutKind,
|
|
1955
2144
|
) => {
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
const layoutKind: LayoutKind | undefined =
|
|
1966
|
-
layoutKindOverride ??
|
|
1967
|
-
(materialShaderId === SKIN_MATERIAL_SHADER_ID ||
|
|
1968
|
-
isSkinnedShadowCasterVariant(materialShaderId, variantSet)
|
|
1969
|
-
? 'pbr-skin'
|
|
1970
|
-
: materialShaderId === 'forgeax::sprite' || materialShaderId === 'forgeax::sprite-lit'
|
|
1971
|
-
? 'sprite-urp'
|
|
1972
|
-
: materialShaderId === 'forgeax::default-unlit'
|
|
1973
|
-
? 'unlit-urp'
|
|
1974
|
-
: 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;
|
|
1975
2154
|
const usesSharedBootMaterialLayout =
|
|
1976
2155
|
materialShaderId !== undefined && isEngineOwnedMaterialShader(materialShaderId);
|
|
1977
2156
|
// Built-in PBR/unlit/skin layouts are assembled at boot. Re-deriving a
|
|
1978
2157
|
// per-shader layout from the expanded PBR schema would bypass the HDRP
|
|
1979
2158
|
// group(2) selector and bind the URP mesh BGL to the cluster pipeline.
|
|
1980
2159
|
const perShaderLayout =
|
|
1981
|
-
!usesSharedBootMaterialLayout &&
|
|
2160
|
+
!usesSharedBootMaterialLayout && materialShaderId !== undefined
|
|
1982
2161
|
? getOrBuildPerShaderMaterialLayout(materialShaderId)
|
|
1983
2162
|
: null;
|
|
1984
2163
|
// feat-20260621-learn-render-5-5-parallax M2 / w6 (D-1): an authored
|
|
@@ -2030,7 +2209,7 @@ async function makeWebGPURenderer(
|
|
|
2030
2209
|
? preparedMaterialLayout
|
|
2031
2210
|
: perShaderLayout !== null
|
|
2032
2211
|
? perShaderLayout.pipelineLayout
|
|
2033
|
-
: selectPipelineLayoutForVariant(pipelineState, variantSet,
|
|
2212
|
+
: selectPipelineLayoutForVariant(pipelineState, variantSet, resolvedLayoutKind);
|
|
2034
2213
|
if (pipelineLayout === null) return null;
|
|
2035
2214
|
// feat-20260611-fox-skinning-vertex-attribute-chain M4 / w16 (D-4):
|
|
2036
2215
|
// pbr-skin -> deriveVertexBufferLayout (single SSOT for skin/non-skin
|
|
@@ -2081,7 +2260,7 @@ async function makeWebGPURenderer(
|
|
|
2081
2260
|
: undefined,
|
|
2082
2261
|
),
|
|
2083
2262
|
)
|
|
2084
|
-
:
|
|
2263
|
+
: materialShaderId === SKIN_MATERIAL_SHADER_ID
|
|
2085
2264
|
? toGpuVertexBufferLayouts(
|
|
2086
2265
|
deriveVertexBufferLayout(
|
|
2087
2266
|
meshAttributes ??
|
|
@@ -2127,7 +2306,7 @@ async function makeWebGPURenderer(
|
|
|
2127
2306
|
: getShaderModuleAdapter(),
|
|
2128
2307
|
pipelineLayout,
|
|
2129
2308
|
vertexBuffers,
|
|
2130
|
-
...(
|
|
2309
|
+
...(resolvedLayoutKind === 'pbr-skin' ? { layoutKind: 'pbr-skin' as const } : {}),
|
|
2131
2310
|
};
|
|
2132
2311
|
};
|
|
2133
2312
|
/**
|
|
@@ -2151,7 +2330,7 @@ async function makeWebGPURenderer(
|
|
|
2151
2330
|
passKind: PassKind = 'forward',
|
|
2152
2331
|
// bug-20260611-skin-pipeline-layout: thread materialShaderId so
|
|
2153
2332
|
// buildPipelineContext can derive `LayoutKind === 'pbr-skin'` and pick
|
|
2154
|
-
// the
|
|
2333
|
+
// the 3-entry mesh-array BGL chain.
|
|
2155
2334
|
materialShaderId?: string,
|
|
2156
2335
|
// feat-20260611-fox-skinning-vertex-attribute-chain M4 / w16 (D-4):
|
|
2157
2336
|
// pass the per-mesh `VertexAttributeMap` so the pbr-skin path's
|
|
@@ -2187,7 +2366,11 @@ async function makeWebGPURenderer(
|
|
|
2187
2366
|
vertexLayoutProjection?: VertexLayoutProjection,
|
|
2188
2367
|
shaderModuleMode?: RenderFeatureShaderModuleMode,
|
|
2189
2368
|
layoutKindOverride?: LayoutKind,
|
|
2190
|
-
):
|
|
2369
|
+
): {
|
|
2370
|
+
readonly pipeline: RenderPipeline;
|
|
2371
|
+
readonly group2Contract: PipelineGroup2Contract;
|
|
2372
|
+
} | null => {
|
|
2373
|
+
const group2Contract = resolvePipelineGroup2Contract(entry.source);
|
|
2191
2374
|
const ctx = buildPipelineContext(
|
|
2192
2375
|
variantSet,
|
|
2193
2376
|
materialShaderId,
|
|
@@ -2195,6 +2378,7 @@ async function makeWebGPURenderer(
|
|
|
2195
2378
|
vertexLayout,
|
|
2196
2379
|
vertexLayoutProjection,
|
|
2197
2380
|
shaderModuleMode,
|
|
2381
|
+
group2Contract,
|
|
2198
2382
|
layoutKindOverride,
|
|
2199
2383
|
);
|
|
2200
2384
|
if (ctx === null) return null;
|
|
@@ -2254,10 +2438,14 @@ async function makeWebGPURenderer(
|
|
|
2254
2438
|
}
|
|
2255
2439
|
return null;
|
|
2256
2440
|
}
|
|
2257
|
-
|
|
2258
|
-
|
|
2441
|
+
const pipelineEntry = {
|
|
2442
|
+
pipeline: built.value,
|
|
2443
|
+
group2Contract,
|
|
2444
|
+
} as const;
|
|
2445
|
+
materialShaderPipelineCache.set(cacheKey, pipelineEntry);
|
|
2446
|
+
return pipelineEntry;
|
|
2259
2447
|
};
|
|
2260
|
-
const
|
|
2448
|
+
const resolveMaterialShaderPipelineEntry = (
|
|
2261
2449
|
materialShaderId: string,
|
|
2262
2450
|
isHdr: boolean,
|
|
2263
2451
|
renderState?: MaterialRenderState,
|
|
@@ -2304,7 +2492,10 @@ async function makeWebGPURenderer(
|
|
|
2304
2492
|
vertexLayoutProjection?: VertexLayoutProjection,
|
|
2305
2493
|
shaderModuleMode: RenderFeatureShaderModuleMode = 'validated',
|
|
2306
2494
|
layoutKindOverride?: LayoutKind,
|
|
2307
|
-
):
|
|
2495
|
+
): {
|
|
2496
|
+
readonly pipeline: RenderPipeline;
|
|
2497
|
+
readonly group2Contract: PipelineGroup2Contract;
|
|
2498
|
+
} | null => {
|
|
2308
2499
|
// WebGL2 is a downlevel backend even when the record stage asks for the
|
|
2309
2500
|
// native-capability variant string. Resolve the variant axes at this
|
|
2310
2501
|
// shared PSO seam so the shader source, BGL layout, and cache key agree on
|
|
@@ -2424,6 +2615,11 @@ async function makeWebGPURenderer(
|
|
|
2424
2615
|
if (pipelineState === null) return null;
|
|
2425
2616
|
const lookup = getShader().findMaterialArtifact(materialShaderId);
|
|
2426
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;
|
|
2427
2623
|
if (shouldDeferMissingPreparedMaterialShader(vertexLayout)) {
|
|
2428
2624
|
// Prepared VFX draws bind only their feature-owned groups. The
|
|
2429
2625
|
// built-in PBR fallback declares mesh/instance groups 2/3 and would
|
|
@@ -2544,24 +2740,26 @@ async function makeWebGPURenderer(
|
|
|
2544
2740
|
// Use `!== undefined` so the empty-string case enters the lookup.
|
|
2545
2741
|
if (effectiveVariantSet !== undefined) {
|
|
2546
2742
|
const registry = getShader();
|
|
2743
|
+
let matchedVariant = false;
|
|
2547
2744
|
for (const msEntry of registry.materialShaderManifestEntries()) {
|
|
2548
2745
|
if (msEntry.identifier === materialShaderId) {
|
|
2549
2746
|
const variant = findVariantByKey(msEntry, effectiveVariantSet);
|
|
2550
|
-
if (variant)
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
}
|
|
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
|
+
};
|
|
2562
2759
|
break;
|
|
2563
2760
|
}
|
|
2564
2761
|
}
|
|
2762
|
+
if (!matchedVariant) return null;
|
|
2565
2763
|
}
|
|
2566
2764
|
// feat-20260609 M4 / R3-fixup: append `-${passKind}` to the PSO label so
|
|
2567
2765
|
// GPU debug captures (and the shadow-caster branch in the builder)
|
|
@@ -2602,7 +2800,7 @@ async function makeWebGPURenderer(
|
|
|
2602
2800
|
passKind,
|
|
2603
2801
|
// bug-20260611-skin-pipeline-layout: thread the registered materialShaderId
|
|
2604
2802
|
// so buildPipelineContext can resolve LayoutKind='pbr-skin' for the skin
|
|
2605
|
-
// shader (
|
|
2803
|
+
// shader (3-entry mesh-array BGL).
|
|
2606
2804
|
materialShaderId,
|
|
2607
2805
|
// feat-20260611-fox-skinning-vertex-attribute-chain M4 / w16 (D-4):
|
|
2608
2806
|
// forward meshAttributes so the pbr-skin path's vertex buffer layout
|
|
@@ -2619,6 +2817,12 @@ async function makeWebGPURenderer(
|
|
|
2619
2817
|
layoutKindOverride,
|
|
2620
2818
|
);
|
|
2621
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;
|
|
2622
2826
|
const getParamSchema = (materialShaderId: string) => {
|
|
2623
2827
|
const lookup = getShader().findMaterialArtifact(materialShaderId);
|
|
2624
2828
|
return lookup.ok ? lookup.value.paramSchema : undefined;
|
|
@@ -2739,6 +2943,10 @@ async function makeWebGPURenderer(
|
|
|
2739
2943
|
shaderModuleFactory: {
|
|
2740
2944
|
createShaderModule: (descriptor) => getShaderModuleAdapter().createShaderModule(descriptor),
|
|
2741
2945
|
},
|
|
2946
|
+
createShaderModule: (device, descriptor) =>
|
|
2947
|
+
internals.pack.createShaderModule === undefined
|
|
2948
|
+
? Promise.resolve(invokeDeviceCreateShaderModule(device, descriptor))
|
|
2949
|
+
: internals.pack.createShaderModule(device, descriptor),
|
|
2742
2950
|
immediateShaderModuleFactory: {
|
|
2743
2951
|
createShaderModule: (descriptor) =>
|
|
2744
2952
|
getImmediateShaderModuleAdapter().createShaderModule(descriptor),
|
|
@@ -2775,6 +2983,7 @@ async function makeWebGPURenderer(
|
|
|
2775
2983
|
errorRegistry: internals.errorRegistry,
|
|
2776
2984
|
healthRegistry: internals.healthRegistry,
|
|
2777
2985
|
getMaterialShaderPipeline,
|
|
2986
|
+
getMaterialShaderPipelineEntry,
|
|
2778
2987
|
getMaterialShaderBindingContract: getCachedMaterialShaderBindingContract,
|
|
2779
2988
|
getParamSchema,
|
|
2780
2989
|
getMaterialBindGroupLayout,
|
|
@@ -2794,10 +3003,93 @@ async function makeWebGPURenderer(
|
|
|
2794
3003
|
get meshSsboState() {
|
|
2795
3004
|
return internals.meshSsboState;
|
|
2796
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),
|
|
2797
3087
|
get volumetricFogShaders() {
|
|
2798
3088
|
return internals.volumetricFogShaders;
|
|
2799
3089
|
},
|
|
2800
3090
|
});
|
|
3091
|
+
attachGpuPassTimingSession(internals, gpuPassTimingSession);
|
|
3092
|
+
renderSystemForTargetPromotion = renderSystem;
|
|
2801
3093
|
if (
|
|
2802
3094
|
internals.device.caps.backendKind === 'null' &&
|
|
2803
3095
|
Array.from(getShader().entries()).length === 0
|
|
@@ -2822,6 +3114,10 @@ async function makeWebGPURenderer(
|
|
|
2822
3114
|
const transformReleases = new Map<World, () => void>();
|
|
2823
3115
|
const attachmentOwner = {};
|
|
2824
3116
|
let frameId = 0;
|
|
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>();
|
|
2825
3121
|
const issuedReceipts = new WeakSet<FrameReceipt>();
|
|
2826
3122
|
const receiptTimings = new WeakMap<FrameReceipt, Promise<VolumeTimingObservation>>();
|
|
2827
3123
|
let surfaceReleased = false;
|
|
@@ -2837,13 +3133,21 @@ async function makeWebGPURenderer(
|
|
|
2837
3133
|
leasesByWorld.set(world, lease);
|
|
2838
3134
|
return ok(lease);
|
|
2839
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),
|
|
2840
3144
|
setProfile(profile: RenderProfile): RenderResult<void, RenderError> {
|
|
2841
3145
|
const invalid = validateRenderProfile(profile);
|
|
2842
3146
|
if (invalid !== undefined) {
|
|
2843
3147
|
return err(
|
|
2844
3148
|
new RendererOperationError('frame-input-invalid', {
|
|
2845
3149
|
operation: 'set-profile',
|
|
2846
|
-
cause: new
|
|
3150
|
+
cause: new StandardProfileInvalidError(invalid),
|
|
2847
3151
|
}),
|
|
2848
3152
|
);
|
|
2849
3153
|
}
|
|
@@ -2888,6 +3192,7 @@ async function makeWebGPURenderer(
|
|
|
2888
3192
|
: { presentationProof: internals.context.presentationProof }),
|
|
2889
3193
|
});
|
|
2890
3194
|
const volumetricFog = renderSystem.volumetricFog;
|
|
3195
|
+
const directionalShadow = renderSystem.directionalShadow;
|
|
2891
3196
|
return Object.freeze({
|
|
2892
3197
|
state,
|
|
2893
3198
|
surface: surfaceReleased ? 'released' : 'available',
|
|
@@ -2905,9 +3210,11 @@ async function makeWebGPURenderer(
|
|
|
2905
3210
|
frustumStats: Object.freeze({ ...renderSystem.frustumStats }),
|
|
2906
3211
|
visibilityStats: Object.freeze({ ...renderSystem.visibilityStats }),
|
|
2907
3212
|
renderScene: renderSystem.renderScene,
|
|
3213
|
+
reflectionProbes: renderSystem.reflectionProbes,
|
|
2908
3214
|
environment: renderSystem.environment,
|
|
2909
3215
|
temporal: renderSystem.temporal,
|
|
2910
3216
|
bloom: renderSystem.bloom,
|
|
3217
|
+
directionalShadow,
|
|
2911
3218
|
...(renderSystem.temporalTargetInspection === undefined
|
|
2912
3219
|
? {}
|
|
2913
3220
|
: { temporalTarget: Object.freeze(renderSystem.temporalTargetInspection) }),
|
|
@@ -2917,6 +3224,12 @@ async function makeWebGPURenderer(
|
|
|
2917
3224
|
...(renderSystem.transmission === undefined
|
|
2918
3225
|
? {}
|
|
2919
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 }) }),
|
|
2920
3233
|
meshMaterialBindings: Object.freeze(
|
|
2921
3234
|
renderSystem.meshMaterialBindings.map((observation) =>
|
|
2922
3235
|
Object.freeze({
|
|
@@ -3065,6 +3378,9 @@ async function makeWebGPURenderer(
|
|
|
3065
3378
|
get bindGroupCounts() {
|
|
3066
3379
|
return renderSystem.bindGroupCounts;
|
|
3067
3380
|
},
|
|
3381
|
+
configureStandard(config) {
|
|
3382
|
+
renderSystem.configureStandard(config);
|
|
3383
|
+
},
|
|
3068
3384
|
renderFeatureDiagnostics() {
|
|
3069
3385
|
return internals.featureHost?.diagnostics() ?? [];
|
|
3070
3386
|
},
|
|
@@ -3090,6 +3406,35 @@ async function makeWebGPURenderer(
|
|
|
3090
3406
|
new RenderFeatureCapabilityMissingError(feature.identity, host.size, missingCapability),
|
|
3091
3407
|
);
|
|
3092
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
|
+
}
|
|
3093
3438
|
for (const materialShaderId of feature.requiredMaterialShaders ?? []) {
|
|
3094
3439
|
const lookup = getShader().findMaterialArtifact(materialShaderId);
|
|
3095
3440
|
if (!lookup.ok) {
|
|
@@ -3128,6 +3473,7 @@ async function makeWebGPURenderer(
|
|
|
3128
3473
|
requiredMaterialShaderSet.add(materialShaderId);
|
|
3129
3474
|
}
|
|
3130
3475
|
requiredMaterialShaders = Object.freeze([...requiredMaterialShaderSet]);
|
|
3476
|
+
requiredFullscreenPostProcesses = nextRequiredFullscreenPostProcesses;
|
|
3131
3477
|
}
|
|
3132
3478
|
return installed;
|
|
3133
3479
|
},
|
|
@@ -3156,6 +3502,7 @@ async function makeWebGPURenderer(
|
|
|
3156
3502
|
}
|
|
3157
3503
|
}
|
|
3158
3504
|
requiredMaterialShaders = Object.freeze([...requiredMaterialShaderSet]);
|
|
3505
|
+
requiredFullscreenPostProcesses = collectRequiredFullscreenPostProcesses(host.features);
|
|
3159
3506
|
}
|
|
3160
3507
|
return removed;
|
|
3161
3508
|
},
|
|
@@ -3329,23 +3676,52 @@ async function makeWebGPURenderer(
|
|
|
3329
3676
|
// through onError separately; the facade Result is the synchronous
|
|
3330
3677
|
// summary AI users can ignore or branch on).
|
|
3331
3678
|
try {
|
|
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
|
+
};
|
|
3332
3693
|
const timingRequested =
|
|
3333
3694
|
isFrameRequest &&
|
|
3334
3695
|
(frameRequest?.profileFrame !== undefined ||
|
|
3335
3696
|
internals.options?.captureGpuTimings === true);
|
|
3336
|
-
let
|
|
3337
|
-
|
|
3697
|
+
let volumeTimingCapture: GpuTimingCapture | undefined;
|
|
3698
|
+
const volumeTimingUnavailableReason = timingRequested
|
|
3338
3699
|
? 'timestamp capture is unavailable on this device'
|
|
3339
3700
|
: 'timestamp capture was not requested for this frame';
|
|
3340
|
-
|
|
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) {
|
|
3341
3706
|
const createdTiming = GpuTimingCapture.create(internals.device);
|
|
3342
|
-
if (createdTiming.ok)
|
|
3343
|
-
|
|
3344
|
-
|
|
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
|
+
};
|
|
3345
3715
|
}
|
|
3346
|
-
const submitted = renderSystem.draw(
|
|
3716
|
+
const submitted = renderSystem.draw(
|
|
3717
|
+
worlds,
|
|
3718
|
+
drawOptions,
|
|
3719
|
+
renderReadLeases,
|
|
3720
|
+
volumeTimingCapture,
|
|
3721
|
+
);
|
|
3722
|
+
timingHost.gpuPassTimingFrameIdentity = undefined;
|
|
3347
3723
|
if (!submitted) {
|
|
3348
|
-
|
|
3724
|
+
volumeTimingCapture?.discard();
|
|
3349
3725
|
return err(
|
|
3350
3726
|
new RendererContractFailureError(
|
|
3351
3727
|
'draw',
|
|
@@ -3353,36 +3729,77 @@ async function makeWebGPURenderer(
|
|
|
3353
3729
|
),
|
|
3354
3730
|
);
|
|
3355
3731
|
}
|
|
3732
|
+
const timingCompletion = timingHost.gpuPassTimingSubmittedWork;
|
|
3733
|
+
timingHost.gpuPassTimingSubmittedWork = undefined;
|
|
3356
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);
|
|
3357
3752
|
if (isFrameRequest) {
|
|
3358
|
-
const receiptFrameId = ++frameId;
|
|
3359
|
-
const receiptGeneration = activeDeviceScope.generation;
|
|
3360
|
-
const completed: FrameReceipt['completed'] = internals.device.queue
|
|
3361
|
-
.onSubmittedWorkDone()
|
|
3362
|
-
.then(() => ok(undefined))
|
|
3363
|
-
.catch((cause: unknown) =>
|
|
3364
|
-
err(
|
|
3365
|
-
new RendererOperationError('device-operation-failed', {
|
|
3366
|
-
operation: 'complete-frame',
|
|
3367
|
-
frameId: receiptFrameId,
|
|
3368
|
-
deviceGeneration: receiptGeneration,
|
|
3369
|
-
cause: structuredRendererCause(cause, 'complete-frame'),
|
|
3370
|
-
}),
|
|
3371
|
-
),
|
|
3372
|
-
);
|
|
3373
3753
|
const receipt = Object.freeze({
|
|
3374
3754
|
frameId: receiptFrameId,
|
|
3375
3755
|
deviceGeneration: receiptGeneration,
|
|
3376
3756
|
completed,
|
|
3377
3757
|
});
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
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
|
+
}
|
|
3386
3803
|
issuedReceipts.add(receipt);
|
|
3387
3804
|
return ok(receipt);
|
|
3388
3805
|
}
|
|
@@ -3407,7 +3824,16 @@ async function makeWebGPURenderer(
|
|
|
3407
3824
|
receipt: FrameReceipt,
|
|
3408
3825
|
request: FrameObservationRequest,
|
|
3409
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
|
+
}
|
|
3410
3835
|
if (
|
|
3836
|
+
!currentReceipts.has(receipt) ||
|
|
3411
3837
|
!issuedReceipts.has(receipt) ||
|
|
3412
3838
|
receipt.deviceGeneration !== activeDeviceScope.generation
|
|
3413
3839
|
) {
|
|
@@ -3432,11 +3858,16 @@ async function makeWebGPURenderer(
|
|
|
3432
3858
|
}),
|
|
3433
3859
|
);
|
|
3434
3860
|
}
|
|
3435
|
-
const
|
|
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')
|
|
3436
3867
|
? await (receiptTimings.get(receipt) ??
|
|
3437
|
-
Promise.resolve({
|
|
3868
|
+
Promise.resolve<VolumeTimingObservation>({
|
|
3438
3869
|
status: 'unavailable',
|
|
3439
|
-
reason: '
|
|
3870
|
+
reason: 'timestamp capture was not requested for this frame',
|
|
3440
3871
|
}))
|
|
3441
3872
|
: undefined;
|
|
3442
3873
|
return ok(
|
|
@@ -3444,7 +3875,10 @@ async function makeWebGPURenderer(
|
|
|
3444
3875
|
frameId: receipt.frameId,
|
|
3445
3876
|
deviceGeneration: receipt.deviceGeneration,
|
|
3446
3877
|
include: Object.freeze([...request.include]),
|
|
3447
|
-
...(
|
|
3878
|
+
...(targetReadbacks === undefined
|
|
3879
|
+
? {}
|
|
3880
|
+
: { targetReadbacks: Object.freeze(targetReadbacks.value) }),
|
|
3881
|
+
...(volumeTimings === undefined ? {} : { volumeTimings }),
|
|
3448
3882
|
}),
|
|
3449
3883
|
);
|
|
3450
3884
|
},
|
|
@@ -3519,6 +3953,8 @@ async function makeWebGPURenderer(
|
|
|
3519
3953
|
dispose(): RenderResult<void, RenderError> {
|
|
3520
3954
|
if (disposed) return ok(undefined);
|
|
3521
3955
|
disposed = true;
|
|
3956
|
+
currentReceipts.clear();
|
|
3957
|
+
renderTargetHost.dispose();
|
|
3522
3958
|
const cleanupFailures: RendererOperationCause[] = [];
|
|
3523
3959
|
for (const world of attachedWorlds) {
|
|
3524
3960
|
try {
|
|
@@ -3699,6 +4135,8 @@ async function makeWebGPURenderer(
|
|
|
3699
4135
|
// a layout from the replacement device.
|
|
3700
4136
|
emptyPostProcessBgl = null;
|
|
3701
4137
|
internals.context.unconfigure();
|
|
4138
|
+
retireGpuPassTimingSession();
|
|
4139
|
+
attachGpuPassTimingSession(internals, undefined);
|
|
3702
4140
|
|
|
3703
4141
|
// Step (b) / B-2 / B-AC-02: shed device-bound RenderSystem state minted by
|
|
3704
4142
|
// the lost device — the render-graph pendingDestroy queue (its PooledTextures
|
|
@@ -3845,8 +4283,12 @@ async function makeWebGPURenderer(
|
|
|
3845
4283
|
const previousDeviceScope = activeDeviceScope;
|
|
3846
4284
|
activeDeviceScope = recoveryScope;
|
|
3847
4285
|
pipelineState = candidatePipelineState;
|
|
4286
|
+
currentReceipts.clear();
|
|
3848
4287
|
gpuStore.bindDeviceScope(activeDeviceScope);
|
|
4288
|
+
renderTargetHost.recover();
|
|
3849
4289
|
previousDeviceScope.retire();
|
|
4290
|
+
establishGpuPassTimingSession();
|
|
4291
|
+
attachGpuPassTimingSession(internals, gpuPassTimingSession);
|
|
3850
4292
|
|
|
3851
4293
|
// Re-evaluate feature capability gates against the replacement device.
|
|
3852
4294
|
// Feature recovery is isolated from renderer recovery: a feature that is
|
|
@@ -3932,7 +4374,7 @@ export function exposeRenderer(renderer: RendererHostImplementation): Renderer {
|
|
|
3932
4374
|
});
|
|
3933
4375
|
|
|
3934
4376
|
return {
|
|
3935
|
-
attach: (world) => {
|
|
4377
|
+
attach: (world: World) => {
|
|
3936
4378
|
const result = renderer.attach(world);
|
|
3937
4379
|
return result.ok
|
|
3938
4380
|
? result
|
|
@@ -3943,6 +4385,12 @@ export function exposeRenderer(renderer: RendererHostImplementation): Renderer {
|
|
|
3943
4385
|
}),
|
|
3944
4386
|
);
|
|
3945
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),
|
|
3946
4394
|
setProfile: (profile) => {
|
|
3947
4395
|
if (!isRendererStateOperational(currentState)) {
|
|
3948
4396
|
return err(
|
|
@@ -3955,7 +4403,7 @@ export function exposeRenderer(renderer: RendererHostImplementation): Renderer {
|
|
|
3955
4403
|
return renderer.setProfile(profile);
|
|
3956
4404
|
},
|
|
3957
4405
|
state: () => currentState,
|
|
3958
|
-
draw: (request) => {
|
|
4406
|
+
draw: (request: RenderFrameInput) => {
|
|
3959
4407
|
const result = renderer.drawFrame(request);
|
|
3960
4408
|
if (!result.ok) return err(drawError(result.error));
|
|
3961
4409
|
emit(
|
|
@@ -3968,8 +4416,9 @@ export function exposeRenderer(renderer: RendererHostImplementation): Renderer {
|
|
|
3968
4416
|
return ok(result.value);
|
|
3969
4417
|
},
|
|
3970
4418
|
inspect: () => renderer.inspect(),
|
|
3971
|
-
observe: (receipt, request) =>
|
|
3972
|
-
|
|
4419
|
+
observe: (receipt: FrameReceipt, request: FrameObservationRequest) =>
|
|
4420
|
+
renderer.observe(receipt, request),
|
|
4421
|
+
subscribe: (listener: RendererEventListener) => {
|
|
3973
4422
|
if (disposed) return () => undefined;
|
|
3974
4423
|
listeners.add(listener);
|
|
3975
4424
|
return () => listeners.delete(listener);
|
|
@@ -4083,17 +4532,11 @@ function structuredRendererCause(cause: unknown, operation: string): RendererOpe
|
|
|
4083
4532
|
const RENDER_PROFILE_KEYS = new Set([
|
|
4084
4533
|
'pipelineId',
|
|
4085
4534
|
'lightCount',
|
|
4086
|
-
'
|
|
4535
|
+
'renderPath',
|
|
4087
4536
|
'shadows',
|
|
4088
4537
|
'pbr',
|
|
4089
4538
|
'ibl',
|
|
4090
4539
|
'ssao',
|
|
4091
|
-
'bloom',
|
|
4092
|
-
'tone',
|
|
4093
|
-
'antialias',
|
|
4094
|
-
'sky',
|
|
4095
|
-
'volumetricFog',
|
|
4096
|
-
'fallback',
|
|
4097
4540
|
'postStages',
|
|
4098
4541
|
]);
|
|
4099
4542
|
|
|
@@ -4107,36 +4550,15 @@ function validateRenderProfile(profile: RenderProfile): string | undefined {
|
|
|
4107
4550
|
if (!STANDARD_LIGHT_COUNTS.some((count) => count === profile.lightCount)) {
|
|
4108
4551
|
return 'RenderProfile.lightCount must be 1, 32, or 256';
|
|
4109
4552
|
}
|
|
4110
|
-
if (profile.
|
|
4111
|
-
return "RenderProfile.
|
|
4553
|
+
if (profile.renderPath !== 'forward' && profile.renderPath !== 'deferred') {
|
|
4554
|
+
return "RenderProfile.renderPath must be 'forward' or 'deferred'";
|
|
4112
4555
|
}
|
|
4113
4556
|
if (profile.shadows !== 'off' && profile.shadows !== 'hard' && profile.shadows !== 'filtered') {
|
|
4114
4557
|
return "RenderProfile.shadows must be 'off', 'hard', or 'filtered'";
|
|
4115
4558
|
}
|
|
4116
|
-
if (profile.fallback !== 'native' && profile.fallback !== 'cpu-webgl2') {
|
|
4117
|
-
return "RenderProfile.fallback must be 'native' or 'cpu-webgl2'";
|
|
4118
|
-
}
|
|
4119
4559
|
for (const field of ['pbr', 'ibl', 'ssao'] as const) {
|
|
4120
4560
|
if (typeof profile[field] !== 'boolean') return `RenderProfile.${field} must be boolean`;
|
|
4121
4561
|
}
|
|
4122
|
-
if (profile.volumetricFog !== undefined) {
|
|
4123
|
-
if (profile.volumetricFog.quality !== 'low' && profile.volumetricFog.quality !== 'high')
|
|
4124
|
-
return "RenderProfile.volumetricFog.quality must be 'low' or 'high'";
|
|
4125
|
-
if (profile.volumetricFog.depth !== 48 && profile.volumetricFog.depth !== 64) {
|
|
4126
|
-
return 'RenderProfile.volumetricFog.depth must be 48 or 64';
|
|
4127
|
-
}
|
|
4128
|
-
if (profile.volumetricFog.tileSize !== 4 && profile.volumetricFog.tileSize !== 16) {
|
|
4129
|
-
return 'RenderProfile.volumetricFog.tileSize must be 4 or 16';
|
|
4130
|
-
}
|
|
4131
|
-
if (profile.volumetricFog.quality === 'low' && profile.volumetricFog.depth !== 48)
|
|
4132
|
-
return 'RenderProfile.volumetricFog.low must use depth 48';
|
|
4133
|
-
if (profile.volumetricFog.quality === 'high' && profile.volumetricFog.depth !== 64)
|
|
4134
|
-
return 'RenderProfile.volumetricFog.high must use depth 64';
|
|
4135
|
-
if (profile.volumetricFog.quality === 'low' && profile.volumetricFog.tileSize !== 16)
|
|
4136
|
-
return 'RenderProfile.volumetricFog.low must use tileSize 16';
|
|
4137
|
-
if (profile.volumetricFog.quality === 'high' && profile.volumetricFog.tileSize !== 4)
|
|
4138
|
-
return 'RenderProfile.volumetricFog.high must use tileSize 4';
|
|
4139
|
-
}
|
|
4140
4562
|
const stages = STANDARD_POST_STAGE_NAMES;
|
|
4141
4563
|
if (
|
|
4142
4564
|
!Array.isArray(profile.postStages) ||
|
|
@@ -4409,23 +4831,20 @@ function makeRhiNotAvailableError(): RhiError {
|
|
|
4409
4831
|
// inverseViewProj mat4x4<f32> (64 B) appended at tail (offset 176, align 16,
|
|
4410
4832
|
// total 176 + 64 = 240 B). Field added to common.wgsl View struct; host
|
|
4411
4833
|
// writes the tail via queue.writeBuffer in render-system-record.
|
|
4412
|
-
// feat-20260613-csm-cascaded-shadow-maps M4 / w16: extended to
|
|
4834
|
+
// feat-20260613-csm-cascaded-shadow-maps M4 / w16: extended to 960 B.
|
|
4413
4835
|
// lightSpaceMatrix replaced by lightViewProj_A at offset 112; inverseViewProj
|
|
4414
4836
|
// stays at offset 176; lightViewProj_B..D + splitPlanes[4] + cascadeCount +
|
|
4415
|
-
// cascadeBlend + tail padding appended after inverseViewProj.
|
|
4416
|
-
// =
|
|
4417
|
-
//
|
|
4418
|
-
//
|
|
4419
|
-
// append at the formerly-free tail pad (bytes 504/508/512, floats 126/127/128).
|
|
4420
|
-
// VIEW_UBO_BYTES stays 592 (the host tail pad shrinks 88 B -> 64 B); the WGSL
|
|
4421
|
-
// 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.
|
|
4422
4841
|
// feat-20260625-spot-light-shadow-mapping w25 (scope-amend webkit-fallback):
|
|
4423
|
-
// grew
|
|
4842
|
+
// grew from the directional 960 B payload to the 1024 B slot. The per-spot fragment-read perspective lightViewProj matrices
|
|
4424
4843
|
// (`array<mat4x4<f32>, 4>` = 256 B) fold from a standalone @group(0) binding 9
|
|
4425
4844
|
// uniform buffer into the View UBO tail (bytes 528..784, 16 B-aligned after
|
|
4426
|
-
//
|
|
4845
|
+
// the directional filter carrier at bytes 512..528). This removes the 12th fragment-stage uniform
|
|
4427
4846
|
// buffer that overflowed GLES 3.0's max_uniform_buffers_per_shader_stage = 11 on
|
|
4428
|
-
// the WebGL2 fallback path. render-system-record writes the full
|
|
4847
|
+
// the WebGL2 fallback path. render-system-record writes the full 240 f32 payload;
|
|
4429
4848
|
// common.wgsl `View.spotLightViewProj` is the matching SSOT.
|
|
4430
4849
|
const VIEW_UBO_BYTES = VIEW_UNIFORM_BUFFER_SIZE;
|
|
4431
4850
|
|
|
@@ -4922,7 +5341,6 @@ async function prepareMaterialShaders(
|
|
|
4922
5341
|
const projectorAvailable =
|
|
4923
5342
|
(rhiDevice.limits.maxSampledTexturesPerShaderStage ?? 0) >=
|
|
4924
5343
|
STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES;
|
|
4925
|
-
|
|
4926
5344
|
// ── Step 1: manifest load ─────────────────────────────────────────────────
|
|
4927
5345
|
const registry = getShader();
|
|
4928
5346
|
const loaded = await registry.loadManifest();
|
|
@@ -4931,12 +5349,11 @@ async function prepareMaterialShaders(
|
|
|
4931
5349
|
}
|
|
4932
5350
|
|
|
4933
5351
|
// ── Step 1b: material shader variant resolution + registration ───────────
|
|
4934
|
-
//
|
|
4935
|
-
//
|
|
4936
|
-
//
|
|
4937
|
-
//
|
|
4938
|
-
|
|
4939
|
-
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();
|
|
4940
5357
|
for (const msEntry of registry.materialShaderManifestEntries()) {
|
|
4941
5358
|
// bug-20260610: skip synthetic non-material engine entries piggy-backing
|
|
4942
5359
|
// on the materialShaders channel for variant surfacing (shadow_caster).
|
|
@@ -4947,6 +5364,11 @@ async function prepareMaterialShaders(
|
|
|
4947
5364
|
const variantDefines: Record<string, boolean> = {
|
|
4948
5365
|
STORAGE_BUFFER_AVAILABLE: storageBufferCapable,
|
|
4949
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
|
+
}
|
|
4950
5372
|
// User material manifests may declare WEBGL2_COMPAT alongside the shared
|
|
4951
5373
|
// mesh-storage axis. The fallback backend must select that authored
|
|
4952
5374
|
// branch at boot; otherwise it asks for the impossible bare
|
|
@@ -4958,7 +5380,7 @@ async function prepareMaterialShaders(
|
|
|
4958
5380
|
// the axis (has a variant with this key), preserving backward compat for
|
|
4959
5381
|
// entries without the pragma.
|
|
4960
5382
|
if (msEntry.variants.some((v) => 'CLUSTER_FORWARD_AVAILABLE' in v.defines)) {
|
|
4961
|
-
variantDefines.CLUSTER_FORWARD_AVAILABLE =
|
|
5383
|
+
variantDefines.CLUSTER_FORWARD_AVAILABLE = bootClusterAxis;
|
|
4962
5384
|
}
|
|
4963
5385
|
if (msEntry.variants.some((v) => 'VERTEX_COLOR_AVAILABLE' in v.defines)) {
|
|
4964
5386
|
variantDefines.VERTEX_COLOR_AVAILABLE = false;
|
|
@@ -5103,6 +5525,11 @@ async function buildReadyWebGPU(
|
|
|
5103
5525
|
* frame while the shared async shader adapter is still warming up.
|
|
5104
5526
|
*/
|
|
5105
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
|
+
}[],
|
|
5106
5533
|
// feat-20260608-mesh-ssbo-dynamic-grow-l1-lift-1024-entity-cap M2 /
|
|
5107
5534
|
// T-M2-05 + M3 / T-M3-04: surface for setting `internals.growMeshSsbo`
|
|
5108
5535
|
// + `internals.meshSsboState` so the record stage's
|
|
@@ -5131,7 +5558,11 @@ async function buildReadyWebGPU(
|
|
|
5131
5558
|
// `getMaterialShaderPipeline` lookup hits a live PSO instead of falling
|
|
5132
5559
|
// into a 1-frame async-compile skip-draw window. Idempotent: caller
|
|
5133
5560
|
// guards against re-seeding when a key already exists.
|
|
5134
|
-
seedMaterialShaderPipelineCache: (
|
|
5561
|
+
seedMaterialShaderPipelineCache: (
|
|
5562
|
+
key: string,
|
|
5563
|
+
pso: RenderPipeline,
|
|
5564
|
+
group2Contract: PipelineGroup2Contract,
|
|
5565
|
+
) => void,
|
|
5135
5566
|
// feat-20260621 M-A3 (D-5): register the engine built-in tonemap onto the
|
|
5136
5567
|
// unified post-process channel once the tonemap manifest entry's composed
|
|
5137
5568
|
// WGSL is resolved. Invoked with the tonemap WGSL source string; the outer
|
|
@@ -5141,6 +5572,8 @@ async function buildReadyWebGPU(
|
|
|
5141
5572
|
// tonemap source resolves inside this async function (after the manifest-load
|
|
5142
5573
|
// await), by which point the synchronous `renderSystem` const is defined.
|
|
5143
5574
|
registerBuiltinTonemap: (source: string) => void,
|
|
5575
|
+
/** Register the special FXAA pass' final-output policy params UBO. */
|
|
5576
|
+
registerBuiltinFxaa: (source: string) => void,
|
|
5144
5577
|
registerBuiltinTemporalPostProcesses: (entries: {
|
|
5145
5578
|
readonly motionBlur?: string;
|
|
5146
5579
|
readonly taaResolve?: string;
|
|
@@ -5167,7 +5600,6 @@ async function buildReadyWebGPU(
|
|
|
5167
5600
|
const projectorAvailable =
|
|
5168
5601
|
(rhiDevice.limits.maxSampledTexturesPerShaderStage ?? 0) >=
|
|
5169
5602
|
STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES;
|
|
5170
|
-
|
|
5171
5603
|
// bug-20260612: choose swap-chain storage / view formats by backend.
|
|
5172
5604
|
// Channel 2 (native WebGPU) follows navigator.gpu.getPreferredCanvasFormat();
|
|
5173
5605
|
// Channel 3 (wgpu-wasm GLES, storageBufferCapable=false) hard-codes
|
|
@@ -5238,6 +5670,10 @@ async function buildReadyWebGPU(
|
|
|
5238
5670
|
let unlitModule: ShaderModule | null = null;
|
|
5239
5671
|
let spriteModule: ShaderModule | null = null;
|
|
5240
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;
|
|
5241
5677
|
let fxaaModule: ShaderModule | null = null;
|
|
5242
5678
|
let skyboxModule: ShaderModule | null = null;
|
|
5243
5679
|
let bloomBrightModule: ShaderModule | null = null;
|
|
@@ -5456,22 +5892,25 @@ async function buildReadyWebGPU(
|
|
|
5456
5892
|
// CLUSTER_FORWARD_AVAILABLE in PR #344) only emits compound keys like
|
|
5457
5893
|
// `CLUSTER_FORWARD_AVAILABLE=false+STORAGE_BUFFER_AVAILABLE=false`, so
|
|
5458
5894
|
// findVariantByKey('STORAGE_BUFFER_AVAILABLE=false') silently misses.
|
|
5459
|
-
// The
|
|
5460
|
-
//
|
|
5461
|
-
//
|
|
5462
|
-
// 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.
|
|
5463
5898
|
const idToVariantWgsl = new Map<string, string>();
|
|
5464
5899
|
for (const ms of registry.materialShaderManifestEntries()) {
|
|
5465
5900
|
const exact = ms.variants.find(
|
|
5466
|
-
(
|
|
5467
|
-
|
|
5468
|
-
(!('CLUSTER_FORWARD_AVAILABLE' in
|
|
5469
|
-
|
|
5470
|
-
(!('VERTEX_COLOR_AVAILABLE' in
|
|
5471
|
-
|
|
5472
|
-
(!('
|
|
5473
|
-
|
|
5474
|
-
|
|
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),
|
|
5475
5914
|
);
|
|
5476
5915
|
const v = exact === undefined ? undefined : findVariantByKey(ms, exact.definesKey);
|
|
5477
5916
|
if (v !== undefined) {
|
|
@@ -5500,8 +5939,8 @@ async function buildReadyWebGPU(
|
|
|
5500
5939
|
}
|
|
5501
5940
|
}
|
|
5502
5941
|
// The flat `registry.entries()` map carries the canonical composed source,
|
|
5503
|
-
// while
|
|
5504
|
-
// matching
|
|
5942
|
+
// while this eager module uses the boot layout contract. Select the
|
|
5943
|
+
// matching boot variant for every eager engine module, including native
|
|
5505
5944
|
// WebGPU. Without this step the canonical PBR/Unlit source declares the
|
|
5506
5945
|
// optional COLOR_0 vertex input while the boot SPEC_CONST layouts for
|
|
5507
5946
|
// ordinary meshes do not provide location 13.
|
|
@@ -5511,13 +5950,16 @@ async function buildReadyWebGPU(
|
|
|
5511
5950
|
(variant) =>
|
|
5512
5951
|
variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
|
|
5513
5952
|
(!('CLUSTER_FORWARD_AVAILABLE' in variant.defines) ||
|
|
5514
|
-
variant.defines.CLUSTER_FORWARD_AVAILABLE ===
|
|
5953
|
+
variant.defines.CLUSTER_FORWARD_AVAILABLE === standardBootClusterAxis()) &&
|
|
5515
5954
|
(!('VERTEX_COLOR_AVAILABLE' in variant.defines) ||
|
|
5516
5955
|
variant.defines.VERTEX_COLOR_AVAILABLE === false) &&
|
|
5517
5956
|
(!('PROJECTOR_AVAILABLE' in variant.defines) ||
|
|
5518
5957
|
variant.defines.PROJECTOR_AVAILABLE === projectorAvailable) &&
|
|
5519
5958
|
(!('SKINNING_DISABLED' in variant.defines) ||
|
|
5520
5959
|
variant.defines.SKINNING_DISABLED === true) &&
|
|
5960
|
+
(!('DIRECTIONAL_PCSS_AVAILABLE' in variant.defines) ||
|
|
5961
|
+
variant.defines.DIRECTIONAL_PCSS_AVAILABLE ===
|
|
5962
|
+
directionalPcssCapable(rhiDevice.caps.backendKind)) &&
|
|
5521
5963
|
(!('TRANSMISSION_AVAILABLE' in variant.defines) ||
|
|
5522
5964
|
variant.defines.TRANSMISSION_AVAILABLE === false),
|
|
5523
5965
|
);
|
|
@@ -5650,6 +6092,27 @@ async function buildReadyWebGPU(
|
|
|
5650
6092
|
seedShaderModule(label, shaderResult.value);
|
|
5651
6093
|
}
|
|
5652
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
|
+
|
|
5653
6116
|
// The volume graph is authored from ECS state and therefore cannot be
|
|
5654
6117
|
// compiled while the static material manifest is assembled. Its pass
|
|
5655
6118
|
// encoder nevertheless uses the same shared lazy shader adapter, so the
|
|
@@ -5735,6 +6198,7 @@ async function buildReadyWebGPU(
|
|
|
5735
6198
|
);
|
|
5736
6199
|
if (!pbrShaderResult.ok) throw pbrShaderResult.error;
|
|
5737
6200
|
pbrModule = pbrShaderResult.value;
|
|
6201
|
+
pbrGroup2Contract = resolvePipelineGroup2Contract(pbrEntry.wgsl);
|
|
5738
6202
|
// feat-20260629-multi-uv-set-support: a real extra-UV mesh creates a
|
|
5739
6203
|
// layout-specific material PSO after the boot-time standard-layout PSO
|
|
5740
6204
|
// has been pre-warmed. Seed the shared shader-module adapter under the
|
|
@@ -5758,6 +6222,7 @@ async function buildReadyWebGPU(
|
|
|
5758
6222
|
? selectStandardPbrTransmissionPrewarmVariants(
|
|
5759
6223
|
pbrManifestEntry,
|
|
5760
6224
|
storageBufferCapable,
|
|
6225
|
+
directionalPcssCapable(rhiDevice.caps.backendKind),
|
|
5761
6226
|
projectorAvailable,
|
|
5762
6227
|
)
|
|
5763
6228
|
: [];
|
|
@@ -5797,6 +6262,7 @@ async function buildReadyWebGPU(
|
|
|
5797
6262
|
pbrManifestEntry,
|
|
5798
6263
|
storageBufferCapable,
|
|
5799
6264
|
transmissionCapable,
|
|
6265
|
+
directionalPcssCapable(rhiDevice.caps.backendKind),
|
|
5800
6266
|
projectorAvailable,
|
|
5801
6267
|
)) {
|
|
5802
6268
|
const moduleLabel = `module-forgeax::default-standard-pbr#${hdrpVariant.definesKey}`;
|
|
@@ -5824,6 +6290,43 @@ async function buildReadyWebGPU(
|
|
|
5824
6290
|
seedShaderModule(moduleLabel, hdrpModule);
|
|
5825
6291
|
}
|
|
5826
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
|
+
|
|
5827
6330
|
// M2-07-c: Materials.standard with an explicit renderState reaches the
|
|
5828
6331
|
// lazy material pipeline path, whose module label includes the requested
|
|
5829
6332
|
// variant set. Reuse the boot-compiled PBR module only for manifest
|
|
@@ -5848,6 +6351,7 @@ async function buildReadyWebGPU(
|
|
|
5848
6351
|
);
|
|
5849
6352
|
if (!unlitShaderResult.ok) throw unlitShaderResult.error;
|
|
5850
6353
|
unlitModule = unlitShaderResult.value;
|
|
6354
|
+
unlitGroup2Contract = resolvePipelineGroup2Contract(unlitEntry.wgsl);
|
|
5851
6355
|
// Seed the shared lazy adapter with the eagerly compiled module so
|
|
5852
6356
|
// prepared color-only PSOs build synchronously on their first frame.
|
|
5853
6357
|
seedShaderModule('unlit', unlitModule);
|
|
@@ -5994,7 +6498,8 @@ async function buildReadyWebGPU(
|
|
|
5994
6498
|
// frames (e.g. the hello-tonemap dawn smoke) would stall on a black frame
|
|
5995
6499
|
// forever. Mirror the shadow_caster prewarm: await-compile the tonemap module
|
|
5996
6500
|
// here and `seedShaderModule` it under the exact label
|
|
5997
|
-
// `buildPostProcessPipeline` requests
|
|
6501
|
+
// `buildPostProcessPipeline` requests the content-addressed module label,
|
|
6502
|
+
// so the
|
|
5998
6503
|
// first `getPostProcessPipeline(STANDARD_OUTPUT_TRANSFORM_FEATURE_ID, …)` hits the module cache
|
|
5999
6504
|
// synchronously and builds the pipeline on frame 1.
|
|
6000
6505
|
const tonemapPrewarm = await runShimStep(
|
|
@@ -6002,11 +6507,17 @@ async function buildReadyWebGPU(
|
|
|
6002
6507
|
asyncCreateShaderModule
|
|
6003
6508
|
? asyncCreateShaderModule(rhiDevice, {
|
|
6004
6509
|
code: tonemapEntry.wgsl,
|
|
6005
|
-
label:
|
|
6510
|
+
label: postProcessShaderModuleLabel(
|
|
6511
|
+
STANDARD_OUTPUT_TRANSFORM_FEATURE_ID,
|
|
6512
|
+
tonemapEntry.wgsl,
|
|
6513
|
+
),
|
|
6006
6514
|
})
|
|
6007
6515
|
: invokeDeviceCreateShaderModule(rhiDevice, {
|
|
6008
6516
|
code: tonemapEntry.wgsl,
|
|
6009
|
-
label:
|
|
6517
|
+
label: postProcessShaderModuleLabel(
|
|
6518
|
+
STANDARD_OUTPUT_TRANSFORM_FEATURE_ID,
|
|
6519
|
+
tonemapEntry.wgsl,
|
|
6520
|
+
),
|
|
6010
6521
|
}),
|
|
6011
6522
|
'shader-compile-failed',
|
|
6012
6523
|
'tonemap shader module compiled (unified post-process prewarm)',
|
|
@@ -6014,7 +6525,7 @@ async function buildReadyWebGPU(
|
|
|
6014
6525
|
);
|
|
6015
6526
|
if (!tonemapPrewarm.ok) throw tonemapPrewarm.error;
|
|
6016
6527
|
seedShaderModule(
|
|
6017
|
-
|
|
6528
|
+
postProcessShaderModuleLabel(STANDARD_OUTPUT_TRANSFORM_FEATURE_ID, tonemapEntry.wgsl),
|
|
6018
6529
|
tonemapPrewarm.value,
|
|
6019
6530
|
);
|
|
6020
6531
|
registerBuiltinTonemap(tonemapEntry.wgsl);
|
|
@@ -6031,18 +6542,18 @@ async function buildReadyWebGPU(
|
|
|
6031
6542
|
asyncCreateShaderModule
|
|
6032
6543
|
? asyncCreateShaderModule(rhiDevice, {
|
|
6033
6544
|
code: entry.wgsl,
|
|
6034
|
-
label:
|
|
6545
|
+
label: postProcessShaderModuleLabel(id, entry.wgsl),
|
|
6035
6546
|
})
|
|
6036
6547
|
: invokeDeviceCreateShaderModule(rhiDevice, {
|
|
6037
6548
|
code: entry.wgsl,
|
|
6038
|
-
label:
|
|
6549
|
+
label: postProcessShaderModuleLabel(id, entry.wgsl),
|
|
6039
6550
|
}),
|
|
6040
6551
|
'shader-compile-failed',
|
|
6041
6552
|
`${id} shader module compiled (unified post-process prewarm)`,
|
|
6042
6553
|
`inspect manifest ${id} entry composed wgsl; check device.features`,
|
|
6043
6554
|
);
|
|
6044
6555
|
if (!prewarm.ok) throw prewarm.error;
|
|
6045
|
-
seedShaderModule(
|
|
6556
|
+
seedShaderModule(postProcessShaderModuleLabel(id, entry.wgsl), prewarm.value);
|
|
6046
6557
|
};
|
|
6047
6558
|
await prewarmTemporal(motionBlurEntry, 'forgeax.motion-blur');
|
|
6048
6559
|
await prewarmTemporal(taaResolveEntry, 'forgeax.taa-resolve');
|
|
@@ -6071,6 +6582,7 @@ async function buildReadyWebGPU(
|
|
|
6071
6582
|
);
|
|
6072
6583
|
if (!spriteShaderResult.ok) throw spriteShaderResult.error;
|
|
6073
6584
|
spriteModule = spriteShaderResult.value;
|
|
6585
|
+
spriteGroup2Contract = resolvePipelineGroup2Contract(spriteEntry.wgsl);
|
|
6074
6586
|
// feat-20260625-refactor-sprite-as-transparent-mesh CI-fix: seed the
|
|
6075
6587
|
// lazy adapter cache under the same label (`module-forgeax::sprite`)
|
|
6076
6588
|
// that `getMaterialShaderPipeline -> buildAndCachePipeline ->
|
|
@@ -6141,7 +6653,44 @@ async function buildReadyWebGPU(
|
|
|
6141
6653
|
);
|
|
6142
6654
|
if (!spriteLitShaderResult.ok) throw spriteLitShaderResult.error;
|
|
6143
6655
|
spriteLitModule = spriteLitShaderResult.value;
|
|
6656
|
+
spriteLitGroup2Contract = resolvePipelineGroup2Contract(spriteLitEntryConst.wgsl);
|
|
6144
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
|
+
}
|
|
6145
6694
|
}
|
|
6146
6695
|
|
|
6147
6696
|
// feat-20260528-fxaa-post-processing: fxaa shader module is optional
|
|
@@ -6164,6 +6713,7 @@ async function buildReadyWebGPU(
|
|
|
6164
6713
|
);
|
|
6165
6714
|
if (!fxaaShaderResult.ok) throw fxaaShaderResult.error;
|
|
6166
6715
|
fxaaModule = fxaaShaderResult.value;
|
|
6716
|
+
registerBuiltinFxaa(fxaaEntry.wgsl);
|
|
6167
6717
|
}
|
|
6168
6718
|
|
|
6169
6719
|
// feat-20260531-skybox-env-background M3 / w15: skybox shader module.
|
|
@@ -6316,15 +6866,14 @@ async function buildReadyWebGPU(
|
|
|
6316
6866
|
// BGL at group(2) so an HDRP-variant PSO validates against the 7-slot
|
|
6317
6867
|
// group(2) bindGroup that the record stage sets via hdrpClusterBindGroup.
|
|
6318
6868
|
// Stays null when createBindGroupLayout / createPipelineLayout fails;
|
|
6319
|
-
// selectPipelineLayoutForVariant
|
|
6320
|
-
//
|
|
6321
|
-
// 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.
|
|
6322
6871
|
let hdrpPbrPipelineLayoutHandle: PipelineLayout | null = null;
|
|
6323
6872
|
// The cluster layout is storage-buffer backed. On the WebGL2/fallback
|
|
6324
6873
|
// capability tier its uniform-buffer projection exceeds the backend's
|
|
6325
6874
|
// fragment-stage limit, and HDRP is not a valid route there anyway.
|
|
6326
6875
|
if (storageBufferCapable) {
|
|
6327
|
-
const hdrpUnifiedBglDesc = createHdrpBindGroupLayoutDescriptor(
|
|
6876
|
+
const hdrpUnifiedBglDesc = createHdrpBindGroupLayoutDescriptor();
|
|
6328
6877
|
const hdrpUnifiedBglRes = rhiDevice.createBindGroupLayout(hdrpUnifiedBglDesc);
|
|
6329
6878
|
if (hdrpUnifiedBglRes.ok) {
|
|
6330
6879
|
const hdrpPlRes = rhiDevice.createPipelineLayout({
|
|
@@ -6395,23 +6944,26 @@ async function buildReadyWebGPU(
|
|
|
6395
6944
|
// bug-20260611-skin-pipeline-layout-mesh-array-bgl-2bindings: boot-time
|
|
6396
6945
|
// build of the skin-variant PipelineLayout. Mirrors the HDRP block above
|
|
6397
6946
|
// (D-2 / D-3 in plan-strategy): reuse view / material / instances BGLs
|
|
6398
|
-
// from `pbrLayouts` and only create a
|
|
6399
|
-
// 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
|
|
6400
6950
|
// `@group(2) @binding(1) palette` declaration validates. Stays null when
|
|
6401
6951
|
// createBindGroupLayout / createPipelineLayout fails;
|
|
6402
6952
|
// selectPipelineLayoutForVariant returns null in that case (charter P3
|
|
6403
6953
|
// explicit failure -- no silent fallback to URP layout, mirroring memory
|
|
6404
6954
|
// anchor `hdrp-active-must-not-fallback-to-urp-pipeline`).
|
|
6405
6955
|
let pbrSkinPipelineLayoutHandle: PipelineLayout | null = null;
|
|
6406
|
-
// feat-20260611 R2 / M8 / w28: capture the
|
|
6956
|
+
// feat-20260611 R2 / M8 / w28: capture the 3-binding skin mesh-array BGL
|
|
6407
6957
|
// produced by `buildPbrSkinLayouts` so the record stage can build a BG
|
|
6408
|
-
// matching `pbr-skin-pl` (pipeline-layout BGL[2] is this
|
|
6958
|
+
// matching `pbr-skin-pl` (pipeline-layout BGL[2] is this 3-entry skin
|
|
6409
6959
|
// BGL, NOT the 1-entry `pbr-mesh-array-bgl`). The BGL handle stays null
|
|
6410
6960
|
// when the skin pipeline layout itself failed to build, keeping the skin
|
|
6411
6961
|
// path explicitly disabled (charter P3 — record-stage falls back to URP
|
|
6412
6962
|
// path which is correct for non-skin entries; skin entries hit the
|
|
6413
6963
|
// explicit-failure branch).
|
|
6414
6964
|
let pbrSkinMeshBindGroupLayoutHandle: BindGroupLayout | null = null;
|
|
6965
|
+
let hdrpSkinPipelineLayoutHandle: PipelineLayout | null = null;
|
|
6966
|
+
let hdrpSkinMeshBindGroupLayoutHandle: BindGroupLayout | null = null;
|
|
6415
6967
|
{
|
|
6416
6968
|
const skinLayoutsResult = runShimSyncStep(
|
|
6417
6969
|
() =>
|
|
@@ -6427,6 +6979,29 @@ async function buildReadyWebGPU(
|
|
|
6427
6979
|
pbrSkinMeshBindGroupLayoutHandle = skinLayoutsResult.value.meshArrayBgl;
|
|
6428
6980
|
}
|
|
6429
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
|
+
}
|
|
6430
7005
|
|
|
6431
7006
|
// feat-20260612-skin-palette-per-frame-upload M1 / m1-2: skin palette
|
|
6432
7007
|
// allocator. Replaces the prior 16320 B identity-seeded UBO stub (PR #353,
|
|
@@ -7303,17 +7878,20 @@ async function buildReadyWebGPU(
|
|
|
7303
7878
|
fragmentEntryPoint?: string;
|
|
7304
7879
|
layout?: unknown;
|
|
7305
7880
|
label?: string;
|
|
7881
|
+
group2Contract?: PipelineGroup2Contract;
|
|
7306
7882
|
}
|
|
7307
7883
|
>();
|
|
7308
7884
|
if (unlitModule !== null)
|
|
7309
7885
|
shaderModuleMap.set('forgeax::default-unlit', {
|
|
7310
7886
|
vertex: unlitModule,
|
|
7311
7887
|
fragment: unlitModule,
|
|
7888
|
+
...(unlitGroup2Contract === undefined ? {} : { group2Contract: unlitGroup2Contract }),
|
|
7312
7889
|
});
|
|
7313
7890
|
if (pbrModule !== null)
|
|
7314
7891
|
shaderModuleMap.set('forgeax::default-standard-pbr', {
|
|
7315
7892
|
vertex: pbrModule,
|
|
7316
7893
|
fragment: pbrModule,
|
|
7894
|
+
...(pbrGroup2Contract === undefined ? {} : { group2Contract: pbrGroup2Contract }),
|
|
7317
7895
|
});
|
|
7318
7896
|
if (spriteModule !== null)
|
|
7319
7897
|
// feat-20260625-refactor-sprite-as-transparent-mesh M3 / w14 (D-7):
|
|
@@ -7325,6 +7903,7 @@ async function buildReadyWebGPU(
|
|
|
7325
7903
|
shaderModuleMap.set('forgeax::sprite', {
|
|
7326
7904
|
vertex: spriteModule,
|
|
7327
7905
|
fragment: spriteModule,
|
|
7906
|
+
...(spriteGroup2Contract === undefined ? {} : { group2Contract: spriteGroup2Contract }),
|
|
7328
7907
|
});
|
|
7329
7908
|
// feat-20260624-sprite-lit-shading-model-pure-2d-lighting M1' / t7:
|
|
7330
7909
|
// sprite-lit shader registers under canonical `forgeax::sprite-lit` —
|
|
@@ -7333,6 +7912,7 @@ async function buildReadyWebGPU(
|
|
|
7333
7912
|
shaderModuleMap.set('forgeax::sprite-lit', {
|
|
7334
7913
|
vertex: spriteLitModule,
|
|
7335
7914
|
fragment: spriteLitModule,
|
|
7915
|
+
...(spriteLitGroup2Contract === undefined ? {} : { group2Contract: spriteLitGroup2Contract }),
|
|
7336
7916
|
});
|
|
7337
7917
|
// M2-T4: fullscreen-post shader modules (tonemap + skybox) are registered
|
|
7338
7918
|
// in their respective blocks after pipeline layout creation — layout is
|
|
@@ -7434,7 +8014,6 @@ async function buildReadyWebGPU(
|
|
|
7434
8014
|
...runtimeSpecConstTable,
|
|
7435
8015
|
...runtimeLinearLdrMaterialSpecTable,
|
|
7436
8016
|
];
|
|
7437
|
-
|
|
7438
8017
|
// Boot-time pre-warm: build SPEC_CONST entries whose shader modules are
|
|
7439
8018
|
// compiled. Entries referencing a missing module are silently skipped
|
|
7440
8019
|
// (the empty-manifest path — D-3). Each build failure for an available
|
|
@@ -7467,7 +8046,7 @@ async function buildReadyWebGPU(
|
|
|
7467
8046
|
// `makeWebGPURenderer` scope) from the prewarmed `pipelineCache` for
|
|
7468
8047
|
// SPEC_CONST entries whose `variantSet !== undefined`. The URP record
|
|
7469
8048
|
// path queries `getMaterialShaderPipeline(...)` keyed off
|
|
7470
|
-
// `cacheKeyOf(spec)` with
|
|
8049
|
+
// `cacheKeyOf(spec)` with the non-clustered boot variant; both caches
|
|
7471
8050
|
// generate keys via the same `cacheKeyOf` so the lookup hits the
|
|
7472
8051
|
// boot-time prewarmed PSO instead of triggering a 1-frame async-compile
|
|
7473
8052
|
// skip-draw. Seeding only variantSet-bearing entries keeps the
|
|
@@ -7477,11 +8056,12 @@ async function buildReadyWebGPU(
|
|
|
7477
8056
|
// explicit instead of collapsing into one map (charter P3).
|
|
7478
8057
|
for (const spec of runtimeSpecConstTable) {
|
|
7479
8058
|
if (spec.shader.variantSet === undefined) continue;
|
|
7480
|
-
|
|
8059
|
+
const modules = shaderModuleMap.get(spec.shader.id);
|
|
8060
|
+
if (modules === undefined) continue;
|
|
7481
8061
|
const key = cacheKeyOf(spec);
|
|
7482
8062
|
const built = pipelineCache.get(key);
|
|
7483
|
-
if (built !== undefined) {
|
|
7484
|
-
seedMaterialShaderPipelineCache(key, built as RenderPipeline);
|
|
8063
|
+
if (built !== undefined && modules.group2Contract !== undefined) {
|
|
8064
|
+
seedMaterialShaderPipelineCache(key, built as RenderPipeline, modules.group2Contract);
|
|
7485
8065
|
}
|
|
7486
8066
|
}
|
|
7487
8067
|
|
|
@@ -7489,11 +8069,12 @@ async function buildReadyWebGPU(
|
|
|
7489
8069
|
// swap-chain view target. Seed every material entry, including no-variant
|
|
7490
8070
|
// unlit, because tonemap=none reaches this cache directly on frame one.
|
|
7491
8071
|
for (const spec of runtimeLinearLdrMaterialSpecTable) {
|
|
7492
|
-
|
|
8072
|
+
const modules = shaderModuleMap.get(spec.shader.id);
|
|
8073
|
+
if (modules === undefined) continue;
|
|
7493
8074
|
const key = cacheKeyOf(spec);
|
|
7494
8075
|
const built = pipelineCache.get(key);
|
|
7495
|
-
if (built !== undefined) {
|
|
7496
|
-
seedMaterialShaderPipelineCache(key, built as RenderPipeline);
|
|
8076
|
+
if (built !== undefined && modules.group2Contract !== undefined) {
|
|
8077
|
+
seedMaterialShaderPipelineCache(key, built as RenderPipeline, modules.group2Contract);
|
|
7497
8078
|
}
|
|
7498
8079
|
}
|
|
7499
8080
|
}
|
|
@@ -7701,15 +8282,15 @@ async function buildReadyWebGPU(
|
|
|
7701
8282
|
|
|
7702
8283
|
// feat-20260528-fxaa-post-processing M2 / w10: FXAA pipeline prebuilt.
|
|
7703
8284
|
// When the manifest contains the fxaa entry (rgb2luma marker, D-5),
|
|
7704
|
-
// construct the
|
|
8285
|
+
// construct the 3-entry BGL (texture + sampler + output-policy UBO),
|
|
7705
8286
|
// pipeline layout, fullscreen render pipeline (vertex = fullscreen
|
|
7706
8287
|
// triangle from fxaa.wgsl, fragment = FXAA 3.11 algorithm), and
|
|
7707
8288
|
// linear clamp-to-edge sampler. Mirrors the tonemap pipeline
|
|
7708
8289
|
// construction pattern directly above.
|
|
7709
8290
|
if (fxaaModule !== null) {
|
|
7710
|
-
// FXAA BindGroupLayout:
|
|
7711
|
-
//
|
|
7712
|
-
//
|
|
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.
|
|
7713
8294
|
const fxaaBglResult = runShimSyncStep(
|
|
7714
8295
|
() =>
|
|
7715
8296
|
rhiDevice.createBindGroupLayout({
|
|
@@ -7725,6 +8306,11 @@ async function buildReadyWebGPU(
|
|
|
7725
8306
|
visibility: GPU_SHADER_STAGE_FRAGMENT,
|
|
7726
8307
|
sampler: { type: 'filtering' },
|
|
7727
8308
|
},
|
|
8309
|
+
{
|
|
8310
|
+
binding: 2,
|
|
8311
|
+
visibility: GPU_SHADER_STAGE_FRAGMENT,
|
|
8312
|
+
buffer: { type: 'uniform' },
|
|
8313
|
+
},
|
|
7728
8314
|
],
|
|
7729
8315
|
}),
|
|
7730
8316
|
'webgpu-runtime-error',
|
|
@@ -8760,6 +9346,7 @@ async function buildReadyWebGPU(
|
|
|
8760
9346
|
}
|
|
8761
9347
|
|
|
8762
9348
|
const pipelineState: PipelineState = {
|
|
9349
|
+
device: rhiDevice,
|
|
8763
9350
|
// feat-20260518-pbr-direct-lighting-mvp M5 / w22.10 (AC-06 dual->triple
|
|
8764
9351
|
// pipeline + D-2 + D-10): three distinct GPU render-pipeline handles
|
|
8765
9352
|
// backed by 2 distinct shader modules (unlit + pbr) x 2 vertex stride
|
|
@@ -8814,7 +9401,7 @@ async function buildReadyWebGPU(
|
|
|
8814
9401
|
hdrpClusterMembershipPipeline,
|
|
8815
9402
|
hdrpClusterMembershipBindGroupLayout,
|
|
8816
9403
|
// bug-20260611-skin-pipeline-layout: skin-variant PipelineLayout (4-BGL
|
|
8817
|
-
// chain with
|
|
9404
|
+
// chain with 3-entry mesh-array BGL substituted for the standard 1-entry
|
|
8818
9405
|
// mesh-array BGL). Built at boot above (see pbrSkinPipelineLayoutHandle);
|
|
8819
9406
|
// gated on the same unlit/pbr module presence so Camera-only and empty-
|
|
8820
9407
|
// manifest paths see null. Selected per call by
|
|
@@ -8826,13 +9413,17 @@ async function buildReadyWebGPU(
|
|
|
8826
9413
|
// Mirror the gating used by `pbrSkinPipelineLayout` -- both fields
|
|
8827
9414
|
// come from the same `buildPbrSkinLayouts` call so they live or die
|
|
8828
9415
|
// together. Surfaced on PipelineState so render-system-record can
|
|
8829
|
-
// build a
|
|
9416
|
+
// build a 3-binding BG matching `pbr-skin-pl` (was: 1-binding
|
|
8830
9417
|
// `pbr-mesh-bg` + `pbr-mesh-array-bgl`, which the device rejected
|
|
8831
9418
|
// every frame with `Bind group layout pbr-skin-mesh-array-bgl ... does
|
|
8832
9419
|
// not match layout pbr-mesh-array-bgl` + Invalid CommandBuffer +
|
|
8833
9420
|
// queue-submit-failed).
|
|
8834
9421
|
pbrSkinMeshBindGroupLayout:
|
|
8835
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,
|
|
8836
9427
|
// feat-20260612-skin-palette-per-frame-upload M1 / m1-2: animator-ready
|
|
8837
9428
|
// skin-palette allocator (replaces the prior identity-buffer stub).
|
|
8838
9429
|
// Same gating as `pbrSkinPipelineLayout` -- `null` when the skin
|