@forgeax/engine-render 0.1.32 → 0.1.34
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 +1374 -142
- package/dist/assembly/analytic-fog-registration.d.ts +3 -0
- package/dist/assembly/analytic-fog-registration.d.ts.map +1 -0
- package/dist/assembly/backend-contract.d.ts +2 -0
- package/dist/assembly/backend-contract.d.ts.map +1 -1
- package/dist/assembly/barrel-distortion-inspection.d.ts +3 -0
- package/dist/assembly/barrel-distortion-inspection.d.ts.map +1 -0
- package/dist/assembly/canvas-draw-guard.d.ts +3 -0
- package/dist/assembly/canvas-draw-guard.d.ts.map +1 -0
- package/dist/assembly/cloud-shadow-entry.d.ts +4 -0
- package/dist/assembly/cloud-shadow-entry.d.ts.map +1 -0
- package/dist/assembly/dynamic-geometry-host.d.ts +4 -1
- package/dist/assembly/dynamic-geometry-host.d.ts.map +1 -1
- package/dist/assembly/dynamic-geometry-runtime.d.ts +1 -1
- package/dist/assembly/dynamic-geometry-runtime.d.ts.map +1 -1
- package/dist/assembly/factory.d.ts +1 -1
- package/dist/assembly/factory.d.ts.map +1 -1
- package/dist/assembly/fxaa-registration.d.ts +4 -0
- package/dist/assembly/fxaa-registration.d.ts.map +1 -0
- package/dist/assembly/host-contract.d.ts +3 -3
- package/dist/assembly/host-contract.d.ts.map +1 -1
- package/dist/assembly/lod-observation.d.ts +4 -0
- package/dist/assembly/lod-observation.d.ts.map +1 -0
- package/dist/assembly/material/assembly.d.ts.map +1 -1
- package/dist/assembly/material/pipeline-helpers.d.ts +5 -0
- package/dist/assembly/material/pipeline-helpers.d.ts.map +1 -1
- package/dist/assembly/material-shader-policy.d.ts +16 -2
- package/dist/assembly/material-shader-policy.d.ts.map +1 -1
- package/dist/assembly/material-shader-prewarm.d.ts.map +1 -1
- package/dist/assembly/motion-blur-registration.d.ts +3 -0
- package/dist/assembly/motion-blur-registration.d.ts.map +1 -0
- package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -1
- package/dist/assembly/recovery/renderer-recover.d.ts +2 -0
- package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -1
- package/dist/assembly/render-feature-post-processes.d.ts +6 -0
- package/dist/assembly/render-feature-post-processes.d.ts.map +1 -0
- package/dist/assembly/renderer-facade.d.ts.map +1 -1
- package/dist/assembly/renderer-feature-inspection.d.ts +1 -1
- package/dist/assembly/renderer-feature-inspection.d.ts.map +1 -1
- package/dist/assembly/renderer-frame-transaction.d.ts +12 -0
- package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
- package/dist/assembly/renderer-inspection.d.ts +11 -0
- package/dist/assembly/renderer-inspection.d.ts.map +1 -1
- package/dist/assembly/temporal-post-process-prewarm.d.ts +13 -0
- package/dist/assembly/temporal-post-process-prewarm.d.ts.map +1 -0
- package/dist/assembly/webgpu-pbr-ready.d.ts +1 -0
- package/dist/assembly/webgpu-pbr-ready.d.ts.map +1 -1
- package/dist/assembly/webgpu-ready-contract.d.ts +4 -3
- package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -1
- package/dist/assembly/webgpu-ready-per-pass.d.ts +39 -0
- package/dist/assembly/webgpu-ready-per-pass.d.ts.map +1 -0
- package/dist/assembly/webgpu-ready.d.ts +2 -2
- package/dist/assembly/webgpu-ready.d.ts.map +1 -1
- package/dist/assembly/webgpu-renderer-bootstrap.d.ts.map +1 -1
- package/dist/assembly/webgpu-renderer-observation-owner.d.ts +35 -0
- package/dist/assembly/webgpu-renderer-observation-owner.d.ts.map +1 -0
- package/dist/assembly/webgpu-renderer.d.ts +1 -1
- package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
- package/dist/authoring.mjs +1 -1
- package/dist/barrel-distortion.d.ts +47 -0
- package/dist/barrel-distortion.d.ts.map +1 -0
- package/dist/bloom-admission.d.ts +13 -0
- package/dist/bloom-admission.d.ts.map +1 -0
- package/dist/{chunk-PA5DRJNI.mjs → chunk-ELJFJ2BU.mjs} +2 -2
- package/dist/chunk-ELJFJ2BU.mjs.map +1 -0
- package/dist/{chunk-JCIRYQ4V.mjs → chunk-HEB564WF.mjs} +10 -4
- package/dist/chunk-HEB564WF.mjs.map +1 -0
- package/dist/{chunk-JRIPCMYC.mjs → chunk-MKDIT2EM.mjs} +1584 -375
- package/dist/chunk-MKDIT2EM.mjs.map +1 -0
- package/dist/{chunk-S665NJNF.mjs → chunk-OCDLEI6I.mjs} +3 -3
- package/dist/{chunk-S665NJNF.mjs.map → chunk-OCDLEI6I.mjs.map} +1 -1
- package/dist/chunk-ODZWR6JQ.mjs +4927 -0
- package/dist/chunk-ODZWR6JQ.mjs.map +1 -0
- package/dist/{chunk-DRR7D3KX.mjs → chunk-SHFUFJ5N.mjs} +192 -18
- package/dist/chunk-SHFUFJ5N.mjs.map +1 -0
- package/dist/{chunk-4NNZQCL3.mjs → chunk-UI5SFODX.mjs} +223 -277
- package/dist/chunk-UI5SFODX.mjs.map +1 -0
- package/dist/{chunk-GWCEMDVR.mjs → chunk-UINHLCZA.mjs} +787 -80
- package/dist/chunk-UINHLCZA.mjs.map +1 -0
- package/dist/{chunk-6TFBXLRJ.mjs → chunk-WEX45HKH.mjs} +222 -337
- package/dist/chunk-WEX45HKH.mjs.map +1 -0
- package/dist/cloud/density.d.ts +55 -0
- package/dist/cloud/density.d.ts.map +1 -0
- package/dist/cloud/extract.d.ts +25 -0
- package/dist/cloud/extract.d.ts.map +1 -0
- package/dist/cloud/feature.d.ts +57 -0
- package/dist/cloud/feature.d.ts.map +1 -0
- package/dist/cloud/frame-context.d.ts +8 -0
- package/dist/cloud/frame-context.d.ts.map +1 -0
- package/dist/cloud/inspection.d.ts +59 -0
- package/dist/cloud/inspection.d.ts.map +1 -0
- package/dist/cloud/optics.d.ts +56 -0
- package/dist/cloud/optics.d.ts.map +1 -0
- package/dist/cloud/parameters.d.ts +73 -0
- package/dist/cloud/parameters.d.ts.map +1 -0
- package/dist/cloud/resources.d.ts +60 -0
- package/dist/cloud/resources.d.ts.map +1 -0
- package/dist/cloud/shadow.d.ts +42 -0
- package/dist/cloud/shadow.d.ts.map +1 -0
- package/dist/cloud/temporal.d.ts +74 -0
- package/dist/cloud/temporal.d.ts.map +1 -0
- package/dist/components/atmosphere.d.ts +2 -0
- package/dist/components/atmosphere.d.ts.map +1 -1
- package/dist/components/barrel-distortion.d.ts +13 -0
- package/dist/components/barrel-distortion.d.ts.map +1 -0
- package/dist/components/camera.d.ts +20 -4
- package/dist/components/camera.d.ts.map +1 -1
- package/dist/components/cloud-layer.d.ts +29 -0
- package/dist/components/cloud-layer.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/instances.d.ts +20 -6
- package/dist/components/instances.d.ts.map +1 -1
- package/dist/components/mesh-filter.d.ts +6 -5
- package/dist/components/mesh-filter.d.ts.map +1 -1
- package/dist/components/motion-blur.d.ts +1 -0
- package/dist/components/motion-blur.d.ts.map +1 -1
- package/dist/construct-renderer.d.ts +1 -1
- package/dist/construct-renderer.d.ts.map +1 -1
- package/dist/construct-renderer.mjs +9751 -3528
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/device/gpu-residency.d.ts +6 -0
- package/dist/device/gpu-residency.d.ts.map +1 -1
- package/dist/dynamic-geometry.d.ts +5 -1
- package/dist/dynamic-geometry.d.ts.map +1 -1
- package/dist/environment/analytic-fog.d.ts +6 -0
- package/dist/environment/analytic-fog.d.ts.map +1 -0
- package/dist/errors/cloud.d.ts +59 -0
- package/dist/errors/cloud.d.ts.map +1 -0
- package/dist/errors/render.d.ts +23 -5
- package/dist/errors/render.d.ts.map +1 -1
- package/dist/examples/single-layer-medium-surface.d.ts +47 -0
- package/dist/examples/single-layer-medium-surface.d.ts.map +1 -0
- package/dist/extract/camera.d.ts.map +1 -1
- package/dist/extract/contracts.d.ts +12 -1
- package/dist/extract/contracts.d.ts.map +1 -1
- package/dist/extract/environment.d.ts +2 -0
- package/dist/extract/environment.d.ts.map +1 -1
- package/dist/features/barrel-distortion.d.ts +29 -0
- package/dist/features/barrel-distortion.d.ts.map +1 -0
- package/dist/features/error-reporting.d.ts +17 -0
- package/dist/features/error-reporting.d.ts.map +1 -0
- package/dist/features/host.d.ts +15 -0
- package/dist/features/host.d.ts.map +1 -1
- package/dist/features/motion-blur/motion-blur-feature.d.ts +20 -2
- package/dist/features/motion-blur/motion-blur-feature.d.ts.map +1 -1
- package/dist/features/motion-blur/motion-blur-params.d.ts +21 -0
- package/dist/features/motion-blur/motion-blur-params.d.ts.map +1 -1
- package/dist/features/motion-blur/motion-blur-runtime.d.ts +54 -0
- package/dist/features/motion-blur/motion-blur-runtime.d.ts.map +1 -0
- package/dist/features/plan.d.ts +15 -4
- package/dist/features/plan.d.ts.map +1 -1
- package/dist/features/prepared-gpu-work.d.ts +9 -5
- package/dist/features/prepared-gpu-work.d.ts.map +1 -1
- package/dist/features/render-graph-compute.d.ts.map +1 -1
- package/dist/features/render-graph-contribution.d.ts +5 -0
- package/dist/features/render-graph-contribution.d.ts.map +1 -1
- package/dist/features/render-graph-raster.d.ts +1 -0
- package/dist/features/render-graph-raster.d.ts.map +1 -1
- package/dist/features/targets.d.ts +2 -0
- package/dist/features/targets.d.ts.map +1 -1
- package/dist/features/types.d.ts +44 -0
- package/dist/features/types.d.ts.map +1 -1
- package/dist/fullscreen-post-process-pass.d.ts +15 -7
- package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
- package/dist/gpu-dirty-ranges.d.ts +11 -0
- package/dist/gpu-dirty-ranges.d.ts.map +1 -0
- package/dist/gpu-driven/batch-topology.d.ts +17 -0
- package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
- package/dist/gpu-driven/material-artifacts.d.ts +6 -0
- package/dist/gpu-driven/material-artifacts.d.ts.map +1 -1
- package/dist/gpu-driven/prepared-draw.d.ts +7 -0
- package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
- package/dist/gpu-driven/production-raster-material.d.ts +178 -0
- package/dist/gpu-driven/production-raster-material.d.ts.map +1 -0
- package/dist/gpu-driven/production-raster.d.ts +50 -4
- package/dist/gpu-driven/production-raster.d.ts.map +1 -1
- package/dist/gpu-driven/resource-allocation.d.ts +31 -0
- package/dist/gpu-driven/resource-allocation.d.ts.map +1 -0
- package/dist/gpu-driven/shadow-ownership.d.ts +14 -0
- package/dist/gpu-driven/shadow-ownership.d.ts.map +1 -0
- package/dist/gpu-driven/view-gpu.d.ts +35 -2
- package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
- package/dist/gpu-scene.d.ts +11 -1
- package/dist/gpu-scene.d.ts.map +1 -1
- package/dist/hdrp-buffers.d.ts +2 -2
- package/dist/hdrp-buffers.d.ts.map +1 -1
- package/dist/ibl/skylight-bind-group.d.ts +10 -1
- package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
- package/dist/index.d.ts +31 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +258 -8
- package/dist/index.mjs.map +1 -1
- package/dist/inspection-types.d.ts +178 -3
- package/dist/inspection-types.d.ts.map +1 -1
- package/dist/instance-buffer-cache.d.ts +4 -4
- package/dist/instance-buffer-cache.d.ts.map +1 -1
- package/dist/instances.d.ts +49 -94
- package/dist/instances.d.ts.map +1 -1
- package/dist/internal.mjs +7 -7
- package/dist/pbr-pipeline.d.ts +19 -1
- package/dist/pbr-pipeline.d.ts.map +1 -1
- package/dist/pipeline/single-layer-medium-passes.d.ts +73 -0
- package/dist/pipeline/single-layer-medium-passes.d.ts.map +1 -0
- package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
- package/dist/pipeline/standard-output/graph.d.ts.map +1 -1
- package/dist/pipeline/standard-output/types.d.ts +4 -2
- package/dist/pipeline/standard-output/types.d.ts.map +1 -1
- package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
- package/dist/pipeline/standard-post.d.ts +28 -19
- package/dist/pipeline/standard-post.d.ts.map +1 -1
- package/dist/pipeline-builder.d.ts +1 -1
- package/dist/pipeline-builder.d.ts.map +1 -1
- package/dist/pipeline-spec.d.ts +2 -2
- package/dist/plugin.d.ts.map +1 -1
- package/dist/prepare/prepared-graphics-resolver.d.ts +2 -0
- package/dist/prepare/prepared-graphics-resolver.d.ts.map +1 -1
- package/dist/record/barrel-distortion-frame.d.ts +15 -0
- package/dist/record/barrel-distortion-frame.d.ts.map +1 -0
- package/dist/record/dynamic-geometry-consumption.d.ts +10 -2
- package/dist/record/dynamic-geometry-consumption.d.ts.map +1 -1
- package/dist/record/fold-buckets.d.ts +95 -0
- package/dist/record/fold-buckets.d.ts.map +1 -0
- package/dist/record/fold-instance-buffer.d.ts +6 -0
- package/dist/record/fold-instance-buffer.d.ts.map +1 -0
- package/dist/record/frame-lighting.d.ts +1 -0
- package/dist/record/frame-lighting.d.ts.map +1 -1
- package/dist/record/frame-snapshot.d.ts +40 -3
- package/dist/record/frame-snapshot.d.ts.map +1 -1
- package/dist/record/frame-targets.d.ts.map +1 -1
- package/dist/record/frame.d.ts +14 -62
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/gpu-draw-receipts.d.ts +11 -0
- package/dist/record/gpu-draw-receipts.d.ts.map +1 -0
- package/dist/record/gpu-timing.d.ts +8 -0
- package/dist/record/gpu-timing.d.ts.map +1 -1
- package/dist/record/main-pass-geometry.d.ts +21 -5
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/main-pass-material.d.ts +7 -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 +13 -2
- package/dist/record/main-pass.d.ts.map +1 -1
- package/dist/record/mesh-ssbo.d.ts +6 -5
- package/dist/record/mesh-ssbo.d.ts.map +1 -1
- package/dist/record/motion-blur-receipt.d.ts +11 -0
- package/dist/record/motion-blur-receipt.d.ts.map +1 -0
- package/dist/record/probe-blend-buffer.d.ts +16 -0
- package/dist/record/probe-blend-buffer.d.ts.map +1 -0
- package/dist/record/recovery-pipeline.d.ts.map +1 -1
- package/dist/record/render-context.d.ts +34 -31
- package/dist/record/render-context.d.ts.map +1 -1
- package/dist/record/shadow-pass.d.ts +2 -0
- package/dist/record/shadow-pass.d.ts.map +1 -1
- package/dist/record/skybox-post-pass.d.ts +6 -4
- package/dist/record/skybox-post-pass.d.ts.map +1 -1
- package/dist/record/typed-frame-graph.d.ts +22 -2
- package/dist/record/typed-frame-graph.d.ts.map +1 -1
- package/dist/record/view-ubo.d.ts +11 -3
- package/dist/record/view-ubo.d.ts.map +1 -1
- package/dist/record/volume-params.d.ts +9 -0
- package/dist/record/volume-params.d.ts.map +1 -0
- package/dist/recovery/render-system-candidate.d.ts +10 -0
- package/dist/recovery/render-system-candidate.d.ts.map +1 -1
- package/dist/reflection/record-owner.d.ts.map +1 -1
- package/dist/render-contract.d.ts +66 -8
- package/dist/render-contract.d.ts.map +1 -1
- package/dist/render-graph-primitives.d.ts.map +1 -1
- package/dist/render-pipeline.d.ts +106 -3
- package/dist/render-pipeline.d.ts.map +1 -1
- package/dist/render-system-extract-tail.d.ts +3 -2
- package/dist/render-system-extract-tail.d.ts.map +1 -1
- package/dist/render-system-extract.d.ts +24 -10
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system-presentation.d.ts +23 -0
- package/dist/render-system-presentation.d.ts.map +1 -0
- package/dist/render-system-producer-roots.d.ts +10 -0
- package/dist/render-system-producer-roots.d.ts.map +1 -0
- package/dist/render-system-residency.d.ts +12 -0
- package/dist/render-system-residency.d.ts.map +1 -0
- package/dist/render-system.d.ts +26 -15
- package/dist/render-system.d.ts.map +1 -1
- package/dist/scene/probe-blend.d.ts +27 -0
- package/dist/scene/probe-blend.d.ts.map +1 -1
- package/dist/scene/render-scene.d.ts +14 -5
- package/dist/scene/render-scene.d.ts.map +1 -1
- package/dist/ssao-buffers.d.ts +9 -0
- package/dist/ssao-buffers.d.ts.map +1 -1
- package/dist/surface/dynamic-input.d.ts +133 -0
- package/dist/surface/dynamic-input.d.ts.map +1 -0
- package/dist/surface/gpu-driven.d.ts +41 -0
- package/dist/surface/gpu-driven.d.ts.map +1 -0
- package/dist/surface/submission-observation.d.ts +136 -0
- package/dist/surface/submission-observation.d.ts.map +1 -0
- package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
- package/dist/temporal/frame-coordinator.d.ts.map +1 -1
- package/dist/temporal/frame.d.ts +7 -0
- package/dist/temporal/frame.d.ts.map +1 -1
- package/dist/temporal/gpu.d.ts +13 -1
- package/dist/temporal/gpu.d.ts.map +1 -1
- package/dist/temporal/index.mjs +5 -5
- package/dist/temporal/inspection.d.ts.map +1 -1
- package/dist/temporal/standard-scene-data.d.ts.map +1 -1
- package/dist/temporal/target-coverage-attachment.d.ts.map +1 -1
- package/dist/temporal/view.d.ts +6 -1
- package/dist/temporal/view.d.ts.map +1 -1
- package/dist/transmission/single-layer-medium.d.ts +89 -0
- package/dist/transmission/single-layer-medium.d.ts.map +1 -0
- package/dist/typed-render-graph-primitives.d.ts +24 -9
- package/dist/typed-render-graph-primitives.d.ts.map +1 -1
- package/dist/volume/component.d.ts +9 -5
- package/dist/volume/component.d.ts.map +1 -1
- package/dist/volume/extract.d.ts +1 -1
- package/dist/volume/extract.d.ts.map +1 -1
- package/dist/volume/inspection.d.ts +2 -0
- package/dist/volume/inspection.d.ts.map +1 -1
- package/dist/volume/passes.d.ts +4 -1
- package/dist/volume/passes.d.ts.map +1 -1
- package/dist/volume/resources.d.ts.map +1 -1
- package/dist/volume/temporal.d.ts +3 -1
- package/dist/volume/temporal.d.ts.map +1 -1
- package/package.json +22 -21
- package/src/__tests__/analytic-fog-graph.unit.test.ts +199 -0
- package/src/__tests__/analytic-fog-optics.unit.test.ts +38 -0
- package/src/__tests__/atmosphere-daylight.dawn.test.ts +72 -0
- package/src/__tests__/barrel-distortion-lkg.integration.test.ts +353 -0
- package/src/__tests__/barrel-distortion.dawn.test.ts +41 -0
- package/src/__tests__/barrel-distortion.unit.test.ts +364 -0
- package/src/__tests__/bloom-exact-zero.unit.test.ts +112 -18
- package/src/__tests__/bloom-lifecycle.integration.test.ts +17 -4
- package/src/__tests__/construct-renderer-identity.unit.test.ts +49 -0
- package/src/__tests__/detached-recovery-graph.unit.test.ts +277 -0
- package/src/__tests__/device-loss-fanout.unit.test.ts +36 -1
- package/src/__tests__/docs-gate-green.unit.test.ts +112 -1
- package/src/__tests__/dynamic-resolution-plugin.integration.test.ts +20 -0
- package/src/__tests__/environment-fog-lanes.integration.test.ts +14 -0
- package/src/__tests__/environment-selection.unit.test.ts +12 -0
- package/src/__tests__/factory-contract.integration.test.ts +626 -113
- package/src/__tests__/feature-depth-input.dawn.test.ts +129 -2
- package/src/__tests__/frame-camera-selection.unit.test.ts +39 -1
- package/src/__tests__/frame-plan-contract.unit.test.ts +2 -0
- package/src/__tests__/fullscreen-feature-plan.unit.test.ts +14 -3
- package/src/__tests__/gpu-dirty-ranges.unit.test.ts +22 -0
- package/src/__tests__/gpu-driven-baseline.characterization.test.ts +16 -3
- package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +12 -0
- package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +2 -1
- package/src/__tests__/gpu-driven-material-artifacts.unit.test.ts +143 -0
- package/src/__tests__/gpu-driven-pbr.dawn.test.ts +2 -2
- package/src/__tests__/gpu-driven-production.integration.test.ts +482 -3
- package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +7 -6
- package/src/__tests__/gpu-driven-shadow-views.integration.test.ts +1 -1
- package/src/__tests__/gpu-driven-view-gpu-evidence.ts +103 -4
- package/src/__tests__/gpu-driven-view-graph.integration.test.ts +18 -2
- package/src/__tests__/gpu-driven-view.browser.test.ts +5 -8
- package/src/__tests__/gpu-driven-view.dawn.test.ts +6 -2
- package/src/__tests__/gpu-driven-view.unit.test.ts +1 -1
- package/src/__tests__/gpu-pass-timing-lifecycle.integration.test.ts +1 -0
- package/src/__tests__/gpu-pass-timing.browser.test.ts +6 -4
- package/src/__tests__/gpu-pass-timing.rhi-null.unit.test.ts +1 -0
- package/src/__tests__/gpu-resource-store-stride.unit.test.ts +12 -0
- package/src/__tests__/gpu-scene.unit.test.ts +210 -9
- package/src/__tests__/instance-allocation-failure.unit.test.ts +57 -0
- package/src/__tests__/instances-culling.integration.test.ts +10 -36
- package/src/__tests__/instances-store.unit.test.ts +105 -238
- package/src/__tests__/instances-world-ownership.integration.test.ts +62 -0
- package/src/__tests__/main-pass-material-medium-selection.unit.test.ts +146 -0
- package/src/__tests__/material-shader-prewarm.unit.test.ts +59 -0
- package/src/__tests__/material-snapshot-mutation.integration.test.ts +39 -1
- package/src/__tests__/mesh-submission-lifetime.unit.test.ts +31 -0
- package/src/__tests__/morph-culling-reentry.dawn.test.ts +5 -5
- package/src/__tests__/morph-visual-evidence.integration.test.ts +8 -7
- package/src/__tests__/motion-blur-compute.dawn.test.ts +589 -0
- package/src/__tests__/motion-blur.pixel.integration.test.ts +2 -2
- package/src/__tests__/nineslice-handle-surface.unit.test.ts +4 -4
- package/src/__tests__/pbr-view-layout.unit.test.ts +9 -0
- package/src/__tests__/physical-clearcoat.integration.test.ts +58 -0
- package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +21 -0
- package/src/__tests__/prepared-graphics-capability.unit.test.ts +66 -2
- package/src/__tests__/prepared-graphics-lifecycle.unit.test.ts +86 -0
- package/src/__tests__/probe-bindings.unit.test.ts +582 -1
- package/src/__tests__/probe-scene-dirty.integration.test.ts +28 -1
- package/src/__tests__/public-surface.test-d.ts +8 -0
- package/src/__tests__/receipt-observation-retirement.unit.test.ts +65 -0
- package/src/__tests__/recovery-candidate-prepare.contract.test.ts +69 -0
- package/src/__tests__/render-error-code-owner.test-d.ts +9 -0
- package/src/__tests__/render-feature-diagnostics.unit.test.ts +15 -2
- package/src/__tests__/render-feature-error-reporting.unit.test.ts +44 -0
- package/src/__tests__/render-feature-prepared-diagnostics.unit.test.ts +10 -1
- package/src/__tests__/render-feature-retry.unit.test.ts +12 -1
- package/src/__tests__/render-feature-stage-fault-recovery.unit.test.ts +23 -0
- package/src/__tests__/render-scene-mixed-updates.integration.test.ts +145 -19
- package/src/__tests__/render-scene-projection.unit.test.ts +36 -11
- package/src/__tests__/render-scene-temporal-retry.unit.test.ts +37 -6
- package/src/__tests__/render-target-probe-soak.perf.test.ts +2 -2
- package/src/__tests__/render-temporal-environment-consumer.browser.test.ts +3 -3
- package/src/__tests__/render-temporal-environment-consumer.dawn.test.ts +5 -2
- package/src/__tests__/renderer-factory-material-contract.unit.test.ts +41 -7
- package/src/__tests__/renderer-frame-generation-fence.integration.test.ts +16 -0
- package/src/__tests__/renderer-frame-transaction.integration.test.ts +33 -11
- package/src/__tests__/resource-allocation.unit.test.ts +123 -0
- package/src/__tests__/shader-manifest-fixture.ts +4 -4
- package/src/__tests__/single-layer-medium-passes.unit.test.ts +275 -0
- package/src/__tests__/skin-motion-regression.unit.test.ts +70 -1
- package/src/__tests__/skin-palette-recycling.dawn.test.ts +97 -0
- package/src/__tests__/skinned-shadow-caster.test.ts +17 -0
- package/src/__tests__/ssr-carrier-public-contract.unit.test.ts +21 -0
- package/src/__tests__/ssr-gpu-dispatch.browser.test.ts +4 -4
- package/src/__tests__/ssr-gpu-dispatch.dawn.test.ts +2 -2
- package/src/__tests__/ssr-gpu-dispatch.ts +1 -1
- package/src/__tests__/ssr-receiver-coverage.dawn.test.ts +6 -2
- package/src/__tests__/standard-lighting-write.unit.test.ts +2 -1
- package/src/__tests__/standard-output-chain-contract.unit.test.ts +28 -0
- package/src/__tests__/standard-pbr-artifact-assembly.unit.test.ts +46 -1
- package/src/__tests__/standard-pipeline.dawn.test.ts +1 -1
- package/src/__tests__/standard-pipeline.integration.test.ts +200 -9
- package/src/__tests__/surface-dynamic-buffer-lifecycle.unit.test.ts +506 -0
- package/src/__tests__/surface-submission-observation.unit.test.ts +812 -0
- package/src/__tests__/temporal-history-lifecycle.unit.test.ts +32 -0
- package/src/__tests__/temporal-performance.integration.test.ts +3 -3
- package/src/__tests__/temporal-reset-matrix.unit.test.ts +10 -0
- package/src/__tests__/temporal-view.unit.test.ts +2 -1
- package/src/__tests__/typed-pipeline-topology.unit.test.ts +20 -4
- package/src/__tests__/visibility-instances.integration.test.ts +7 -19
- package/src/__tests__/visibility-producer-matrix.integration.test.ts +9 -20
- package/src/__tests__/volume-optics-oracle.unit.test.ts +7 -3
- package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +189 -15
- package/src/__tests__/volumetric-fog-errors.unit.test.ts +4 -3
- package/src/__tests__/volumetric-fog-multiple.unit.test.ts +55 -0
- package/src/__tests__/volumetric-fog-spot-frame-optics.integration.test.ts +1 -1
- package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +558 -68
- package/src/__tests__/volumetric-fog-temporal.unit.test.ts +27 -0
- package/src/__tests__/volumetric-fog-topology.unit.test.ts +23 -5
- package/src/assembly/analytic-fog-registration.ts +14 -0
- package/src/assembly/backend-contract.ts +30 -0
- package/src/assembly/barrel-distortion-inspection.ts +22 -0
- package/src/assembly/canvas-draw-guard.ts +11 -0
- package/src/assembly/cloud-shadow-entry.ts +22 -0
- package/src/assembly/dynamic-geometry-host.ts +65 -37
- package/src/assembly/dynamic-geometry-runtime.ts +7 -1
- package/src/assembly/factory.ts +2 -30
- package/src/assembly/fxaa-registration.ts +17 -0
- package/src/assembly/host-contract.ts +3 -3
- package/src/assembly/lod-observation.ts +12 -0
- package/src/assembly/material/assembly.ts +25 -13
- package/src/assembly/material/pipeline-helpers.ts +53 -12
- package/src/assembly/material-shader-policy.ts +55 -0
- package/src/assembly/material-shader-prewarm.ts +21 -10
- package/src/assembly/motion-blur-registration.ts +17 -0
- package/src/assembly/recovery/device-loss-fanout.ts +13 -0
- package/src/assembly/recovery/renderer-recover.ts +41 -18
- package/src/assembly/render-feature-post-processes.ts +54 -0
- package/src/assembly/renderer-facade.ts +6 -1
- package/src/assembly/renderer-feature-inspection.ts +8 -2
- package/src/assembly/renderer-frame-transaction.ts +23 -0
- package/src/assembly/renderer-inspection.ts +28 -0
- package/src/assembly/skin-palette-owner.ts +1 -1
- package/src/assembly/temporal-post-process-prewarm.ts +40 -0
- package/src/assembly/webgpu-pbr-ready.ts +9 -1
- package/src/assembly/webgpu-ready-contract.ts +8 -3
- package/src/assembly/webgpu-ready-per-pass.ts +104 -0
- package/src/assembly/webgpu-ready.ts +296 -306
- package/src/assembly/webgpu-renderer-bootstrap.ts +0 -12
- package/src/assembly/webgpu-renderer-observation-owner.ts +222 -0
- package/src/assembly/webgpu-renderer.ts +228 -281
- package/src/assembly/webgpu-vertex-layouts.ts +1 -1
- package/src/barrel-distortion.ts +247 -0
- package/src/bloom-admission.ts +21 -0
- package/src/cloud/__tests__/cloud-density-parity.dawn.test.ts +173 -0
- package/src/cloud/__tests__/cloud-layer-framebuffer-y.dawn.test.ts +265 -0
- package/src/cloud/__tests__/cloud-layer.browser.test.ts +432 -0
- package/src/cloud/__tests__/cloud-layer.unit.test.ts +634 -0
- package/src/cloud/density.ts +600 -0
- package/src/cloud/extract.ts +110 -0
- package/src/cloud/feature.ts +1810 -0
- package/src/cloud/frame-context.ts +42 -0
- package/src/cloud/inspection.ts +130 -0
- package/src/cloud/optics.ts +285 -0
- package/src/cloud/parameters.ts +255 -0
- package/src/cloud/resources.ts +122 -0
- package/src/cloud/shadow.ts +167 -0
- package/src/cloud/temporal.ts +226 -0
- package/src/components/__tests__/motion-blur.unit.test.ts +1 -0
- package/src/components/atmosphere.ts +4 -0
- package/src/components/barrel-distortion.ts +46 -0
- package/src/components/camera.ts +66 -10
- package/src/components/cloud-layer.ts +47 -0
- package/src/components/index.ts +2 -0
- package/src/components/instances.ts +18 -45
- package/src/components/mesh-filter.ts +11 -9
- package/src/components/motion-blur.ts +1 -0
- package/src/components/sprite-instances.ts +1 -1
- package/src/construct-renderer.ts +1 -1
- package/src/device/gpu-residency.ts +56 -12
- package/src/dynamic-geometry.ts +12 -1
- package/src/environment/analytic-fog.ts +44 -0
- package/src/environment/background.ts +2 -2
- package/src/errors/cloud.ts +103 -0
- package/src/errors/render.ts +58 -8
- package/src/examples/single-layer-medium-surface.ts +59 -0
- package/src/extract/camera.ts +35 -8
- package/src/extract/contracts.ts +12 -0
- package/src/extract/environment.ts +4 -0
- package/src/features/barrel-distortion.ts +172 -0
- package/src/features/error-reporting.ts +98 -0
- package/src/features/host.ts +138 -24
- package/src/features/motion-blur/__tests__/motion-blur-feature.unit.test.ts +111 -1
- package/src/features/motion-blur/__tests__/motion-blur-params.unit.test.ts +58 -3
- package/src/features/motion-blur/motion-blur-feature.ts +1519 -15
- package/src/features/motion-blur/motion-blur-params.ts +70 -2
- package/src/features/motion-blur/motion-blur-runtime.ts +176 -0
- package/src/features/plan.ts +33 -8
- package/src/features/prepared-gpu-work.ts +86 -35
- package/src/features/render-graph-compute.ts +13 -5
- package/src/features/render-graph-contribution.ts +11 -0
- package/src/features/render-graph-raster.ts +45 -3
- package/src/features/targets.ts +66 -0
- package/src/features/types.ts +47 -0
- package/src/fullscreen-post-process-pass.ts +41 -9
- package/src/gpu-dirty-ranges.ts +48 -0
- package/src/gpu-driven/batch-topology.ts +53 -0
- package/src/gpu-driven/material-artifacts.ts +35 -32
- package/src/gpu-driven/prepared-draw.ts +27 -14
- package/src/gpu-driven/production-raster-material.ts +1894 -0
- package/src/gpu-driven/production-raster.ts +904 -787
- package/src/gpu-driven/resource-allocation.ts +142 -0
- package/src/gpu-driven/shadow-ownership.ts +71 -0
- package/src/gpu-driven/view-gpu.ts +181 -10
- package/src/gpu-scene.ts +259 -101
- package/src/hdrp-buffers.ts +4 -3
- package/src/ibl/skylight-bind-group.ts +49 -7
- package/src/index.ts +228 -9
- package/src/inspection-types.ts +222 -2
- package/src/instance-buffer-cache.ts +4 -4
- package/src/instances.ts +151 -492
- package/src/pbr-pipeline.ts +219 -13
- package/src/pipeline/single-layer-medium-passes.ts +267 -0
- package/src/pipeline/standard-forward-lane.ts +313 -57
- package/src/pipeline/standard-output/graph.ts +8 -2
- package/src/pipeline/standard-output/types.ts +12 -1
- package/src/pipeline/standard-pipeline.ts +291 -18
- package/src/pipeline/standard-post.ts +397 -102
- package/src/pipeline-builder.ts +1 -1
- package/src/pipeline-spec.ts +5 -5
- package/src/plugin.ts +4 -0
- package/src/points-lines/__tests__/points-lines.bench.ts +17 -8
- package/src/points-lines/__tests__/view-ubo.unit.test.ts +2 -1
- package/src/prepare/prepared-graphics-resolver.ts +2 -0
- package/src/record/__tests__/main-pass-geometry.unit.test.ts +30 -0
- package/src/record/__tests__/sprite-material-abi.unit.test.ts +1 -1
- package/src/record/__tests__/temporal-payload.unit.test.ts +3 -2
- package/src/record/__tests__/transparent-group2-selection.unit.test.ts +11 -0
- package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +1 -0
- package/src/record/__tests__/view-ubo-layout.unit.test.ts +4 -3
- package/src/record/barrel-distortion-frame.ts +110 -0
- package/src/record/dynamic-geometry-consumption.ts +39 -9
- package/src/record/fold-buckets.ts +852 -0
- package/src/record/fold-instance-buffer.ts +51 -0
- package/src/record/frame-lighting.ts +36 -2
- package/src/record/frame-snapshot.ts +44 -4
- package/src/record/frame-targets.ts +10 -6
- package/src/record/frame.ts +373 -1099
- package/src/record/gpu-draw-receipts.ts +38 -0
- package/src/record/gpu-timing.ts +48 -5
- package/src/record/main-pass-geometry.ts +281 -83
- package/src/record/main-pass-material.ts +66 -4
- package/src/record/main-pass-sprite-draws.ts +13 -66
- package/src/record/main-pass.ts +156 -20
- package/src/record/mesh-ssbo.ts +43 -6
- package/src/record/motion-blur-receipt.ts +91 -0
- package/src/record/probe-blend-buffer.ts +180 -0
- package/src/record/recovery-pipeline.ts +35 -3
- package/src/record/render-context.ts +51 -31
- package/src/record/shadow-pass.ts +36 -2
- package/src/record/skybox-post-pass.ts +150 -200
- package/src/record/typed-frame-graph.ts +824 -44
- package/src/record/view-ubo.ts +26 -3
- package/src/record/volume-params.ts +225 -0
- package/src/recovery/render-system-candidate.ts +257 -52
- package/src/reflection/record-owner.ts +7 -3
- package/src/render-contract.ts +71 -8
- package/src/render-graph-primitives.ts +7 -1
- package/src/render-pipeline.ts +136 -1
- package/src/render-system-extract-tail.ts +72 -60
- package/src/render-system-extract.ts +117 -87
- package/src/render-system-presentation.ts +56 -0
- package/src/render-system-producer-roots.ts +96 -0
- package/src/render-system-residency.ts +159 -0
- package/src/render-system.ts +601 -482
- package/src/scene/probe-blend.ts +96 -4
- package/src/scene/render-scene.ts +431 -281
- package/src/ssao-buffers.ts +95 -2
- package/src/surface/__tests__/dynamic-input.unit.test.ts +374 -0
- package/src/surface/__tests__/gpu-driven.unit.test.ts +87 -0
- package/src/surface/dynamic-input.ts +658 -0
- package/src/surface/gpu-driven.ts +147 -0
- package/src/surface/submission-observation.ts +517 -0
- package/src/systems/skin-palette-allocator.ts +4 -1
- package/src/temporal/__tests__/temporal-frame-transaction.unit.test.ts +36 -0
- package/src/temporal/frame-coordinator.ts +8 -1
- package/src/temporal/frame.ts +20 -0
- package/src/temporal/gpu.ts +101 -17
- package/src/temporal/inspection.ts +7 -1
- package/src/temporal/standard-scene-data.ts +5 -0
- package/src/temporal/target-coverage-attachment.ts +4 -0
- package/src/temporal/view.ts +13 -2
- package/src/transmission/__tests__/single-layer-medium.unit.test.ts +149 -0
- package/src/transmission/__tests__/standard-transmission.dawn.test.ts +12 -11
- package/src/transmission/single-layer-medium.ts +322 -0
- package/src/typed-render-graph-primitives.ts +191 -50
- package/src/volume/component.ts +10 -10
- package/src/volume/extract.ts +11 -5
- package/src/volume/inspection.ts +3 -0
- package/src/volume/passes.ts +95 -8
- package/src/volume/resources.ts +2 -1
- package/src/volume/temporal.ts +11 -0
- package/dist/chunk-4NNZQCL3.mjs.map +0 -1
- package/dist/chunk-6TFBXLRJ.mjs.map +0 -1
- package/dist/chunk-DRR7D3KX.mjs.map +0 -1
- package/dist/chunk-GWCEMDVR.mjs.map +0 -1
- package/dist/chunk-JCIRYQ4V.mjs.map +0 -1
- package/dist/chunk-JRIPCMYC.mjs.map +0 -1
- package/dist/chunk-LULCA3OF.mjs +0 -1713
- package/dist/chunk-LULCA3OF.mjs.map +0 -1
- package/dist/chunk-PA5DRJNI.mjs.map +0 -1
package/src/record/frame.ts
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { encodeAnalyticFogParams } from '../environment/analytic-fog';
|
|
2
|
+
import type { FogFrame } from '../extract/environment';
|
|
3
|
+
import {
|
|
4
|
+
collectGpuDrivenMaterialArtifacts,
|
|
5
|
+
requiresProbeBlendRecord,
|
|
6
|
+
} from '../gpu-driven/material-artifacts';
|
|
7
|
+
import { projectMaterialBindingClasses } from '../gpu-driven/material-bindings';
|
|
8
|
+
import {
|
|
9
|
+
buildDispatchPlan,
|
|
10
|
+
cleanPerFrameCaches,
|
|
11
|
+
computeFoldBuckets,
|
|
12
|
+
validateRenderables,
|
|
13
|
+
} from './fold-buckets';
|
|
3
14
|
import { uploadMaterialUniforms } from './material-uniforms';
|
|
4
15
|
// @forgeax/engine-runtime - RenderSystem record stage: frame.
|
|
5
16
|
// Extracted from render-system-record.ts (feat-20260704 M3/w17, pure move).
|
|
6
17
|
|
|
7
|
-
import {
|
|
8
|
-
import type
|
|
18
|
+
import { resolveAssetHandle } from '@forgeax/engine-assets-runtime';
|
|
19
|
+
import { Time, type World } from '@forgeax/engine-ecs';
|
|
9
20
|
import type { RenderReadLease } from '@forgeax/engine-ecs/projection';
|
|
10
|
-
import { deriveVertexCount } from '@forgeax/engine-geometry';
|
|
11
21
|
import { frustum, mat4, vec3 } from '@forgeax/engine-math';
|
|
12
22
|
import {
|
|
13
23
|
type CurrentFrameObservationLease,
|
|
@@ -23,8 +33,12 @@ import {
|
|
|
23
33
|
type TextureFormat,
|
|
24
34
|
type TextureView,
|
|
25
35
|
} from '@forgeax/engine-rhi';
|
|
26
|
-
import type {
|
|
36
|
+
import type { MaterialShaderArtifact } from '@forgeax/engine-shader';
|
|
37
|
+
import type { MeshAsset } from '@forgeax/engine-types';
|
|
27
38
|
import { BUILTIN_BASE, handleSlot, toShared } from '@forgeax/engine-types';
|
|
39
|
+
import type { ExtractedCloudLayer } from '../cloud/extract';
|
|
40
|
+
import { cloudShadowResolutionForQuality } from '../cloud/parameters';
|
|
41
|
+
import { type CloudShadowProjection, createCloudShadowProjection } from '../cloud/shadow';
|
|
28
42
|
import { CUBE_CAMERA_FACE_ORDER } from '../components/cube-camera';
|
|
29
43
|
import type { DirectionalShadowQuality } from '../components/directional-shadow-filter';
|
|
30
44
|
import type { MeshGpuHandles } from '../device/gpu-residency';
|
|
@@ -33,7 +47,7 @@ import {
|
|
|
33
47
|
type ObservationUnavailableReason,
|
|
34
48
|
RendererOperationError,
|
|
35
49
|
} from '../errors/render';
|
|
36
|
-
import {
|
|
50
|
+
import { BARREL_DISTORTION_POST_PROCESS_ID } from '../features/barrel-distortion';
|
|
37
51
|
import {
|
|
38
52
|
DEPTH_OF_FIELD_MSAA_POST_PROCESS_IDS,
|
|
39
53
|
DEPTH_OF_FIELD_POST_PROCESS_IDS,
|
|
@@ -44,15 +58,9 @@ import {
|
|
|
44
58
|
} from '../features/depth-of-field/depth-of-field-params';
|
|
45
59
|
import type { GpuDrivenProduction, PreparedGpuDrivenFrame } from '../gpu-driven/production-raster';
|
|
46
60
|
import type { ShadowViewIdentity } from '../gpu-driven/shadow-views';
|
|
47
|
-
import {
|
|
61
|
+
import type { GpuDrivenLodSubmitIdentity } from '../gpu-driven/view-gpu';
|
|
48
62
|
import { GPU_TEXTURE_USAGE_COPY_SRC } from '../gpu-texture-usage';
|
|
49
|
-
import {
|
|
50
|
-
GPU_BUFFER_USAGE_COPY_DST,
|
|
51
|
-
GPU_BUFFER_USAGE_MAP_READ,
|
|
52
|
-
GPU_BUFFER_USAGE_UNIFORM,
|
|
53
|
-
GPU_BUFFER_USAGE_VERTEX,
|
|
54
|
-
} from '../gpu-usage';
|
|
55
|
-
import { disposeTransientInstanceBuffers } from '../instance-buffer-cache';
|
|
63
|
+
import { GPU_BUFFER_USAGE_COPY_DST, GPU_BUFFER_USAGE_MAP_READ } from '../gpu-usage';
|
|
56
64
|
import { deriveRenderExtent } from '../pipeline/render-extent';
|
|
57
65
|
import { inspectStandardLighting } from '../pipeline/standard-lighting/inspection';
|
|
58
66
|
import { standardLightingTopologySignature } from '../pipeline/standard-lighting/topology';
|
|
@@ -68,7 +76,6 @@ import type {
|
|
|
68
76
|
DispatchEntry,
|
|
69
77
|
ExtractedLights,
|
|
70
78
|
ExtractedVolumetricFog,
|
|
71
|
-
MaterialSnapshot,
|
|
72
79
|
RenderableSnapshot,
|
|
73
80
|
ShadowCasterMembership,
|
|
74
81
|
SkyboxSnapshot,
|
|
@@ -86,14 +93,10 @@ import {
|
|
|
86
93
|
type SsrSpatialAdmission,
|
|
87
94
|
} from '../ssr/admission';
|
|
88
95
|
import { createSsrHistoryOwner } from '../ssr/history';
|
|
89
|
-
import {
|
|
90
|
-
getTransparentSortConfig,
|
|
91
|
-
TRANSPARENT_SORT_MODE_LAYER_Y,
|
|
92
|
-
TRANSPARENT_SORT_MODE_LAYER_Z,
|
|
93
|
-
} from '../systems/transparent-sort-config';
|
|
96
|
+
import type { SurfaceDynamicInputFrame } from '../surface/dynamic-input';
|
|
94
97
|
import type { TransmissionDemand } from '../transmission/projection';
|
|
95
98
|
import { hasVolumetricFogCapability } from '../volume/capability';
|
|
96
|
-
import { inspectVolumetricFogResources
|
|
99
|
+
import { inspectVolumetricFogResources } from '../volume/resources';
|
|
97
100
|
import { resolveVolumeTemporalReset, type VolumeTemporalSignature } from '../volume/temporal';
|
|
98
101
|
import {
|
|
99
102
|
buildPerFrameBindGroups,
|
|
@@ -111,7 +114,6 @@ import {
|
|
|
111
114
|
type DispatchCounts,
|
|
112
115
|
type FrameObservationSource,
|
|
113
116
|
getTextureIdentity,
|
|
114
|
-
instanceCollectionCacheKey,
|
|
115
117
|
makeZeroCameraFallbackSnapshot,
|
|
116
118
|
type ReflectionFallbackReadbackRequest,
|
|
117
119
|
type RenderFrameState,
|
|
@@ -119,28 +121,22 @@ import {
|
|
|
119
121
|
validateGraphTargetCaptureReadback,
|
|
120
122
|
worldEntityKey,
|
|
121
123
|
} from './frame-snapshot';
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
// entries with equal (Layer.value, sortKey, materialHandle) into FoldBucket
|
|
125
|
-
// descriptors. The drawIndexed swap (1 instanced draw per bucket vs N
|
|
126
|
-
// per-entity draws) hooks into the sprite-pass dispatch loop below
|
|
127
|
-
// (w4-record-swap), using {@link buildFoldDispatchPlan} to translate
|
|
128
|
-
// renderableIndex-keyed buckets into validatedOrdered-index-keyed
|
|
129
|
-
// head/skip maps.
|
|
130
|
-
import {
|
|
131
|
-
buildFoldDispatchPlan,
|
|
132
|
-
evaluateFoldBucketUniformCap,
|
|
133
|
-
type FoldBucket,
|
|
134
|
-
type FoldDispatchPlan,
|
|
135
|
-
foldDispatchBuckets,
|
|
136
|
-
incrementFoldedDrawsMetric,
|
|
137
|
-
} from './mesh-ssbo';
|
|
124
|
+
import { emitGpuDrivenDrawReceipts, type RenderableDrawReceipt } from './gpu-draw-receipts';
|
|
125
|
+
import { ensureProbeBlendRecordBuffer } from './probe-blend-buffer';
|
|
138
126
|
import type {
|
|
139
127
|
_InternalRenderPipelineContext,
|
|
140
128
|
PipelineState,
|
|
141
129
|
RenderSystemInternals,
|
|
142
130
|
} from './render-context';
|
|
143
|
-
|
|
131
|
+
import { prepareVolumetricFogFrame, stageVolumetricFogParams } from './volume-params';
|
|
132
|
+
|
|
133
|
+
export {
|
|
134
|
+
buildDispatchPlan,
|
|
135
|
+
buildMaterialSlotPlan,
|
|
136
|
+
findRenderablePrefixForSlotCapacity,
|
|
137
|
+
materialSlotCountForPrefix,
|
|
138
|
+
validateRenderables,
|
|
139
|
+
} from './fold-buckets';
|
|
144
140
|
export type { FrameObservationSource } from './frame-snapshot';
|
|
145
141
|
|
|
146
142
|
export type FrameObservationReadback = (
|
|
@@ -155,35 +151,10 @@ export type FrameObservationReadback = (
|
|
|
155
151
|
function residencyValidationCacheKey(world: World, worlds: readonly World[]): string {
|
|
156
152
|
return [world.identity, ...worlds.map((candidate) => candidate.identity)].join('\u001f');
|
|
157
153
|
}
|
|
158
|
-
|
|
159
154
|
function renderableIdentity(snapshot: Pick<RenderableSnapshot, 'worldId' | 'entityKey'>): string {
|
|
160
155
|
return `${snapshot.worldId}:${snapshot.entityKey}`;
|
|
161
156
|
}
|
|
162
157
|
|
|
163
|
-
/**
|
|
164
|
-
* A GPU-owned indirect draw has no CPU submit callback. Keep the same
|
|
165
|
-
* record-stage consumption proof for downstream lifecycle owners by checking
|
|
166
|
-
* the concrete draw-key projection that was actually encoded for this frame.
|
|
167
|
-
*/
|
|
168
|
-
function gpuDrivenRenderableConsumed(
|
|
169
|
-
entry: ValidatedRenderable,
|
|
170
|
-
drawKeys: ReadonlySet<string>,
|
|
171
|
-
): boolean {
|
|
172
|
-
const draws = entry.source.gpuDrivenDraws;
|
|
173
|
-
if (draws === undefined || draws.length === 0) return false;
|
|
174
|
-
const worldEntity = worldEntityKey(entry.source.worldId, entry.source.entityKey);
|
|
175
|
-
return draws.some((draw, compactIndex) => {
|
|
176
|
-
const material = entry.source.materials[draw.materialSlot] ?? entry.source.material;
|
|
177
|
-
return drawKeys.has(
|
|
178
|
-
gpuDrivenDrawKey(
|
|
179
|
-
worldEntity,
|
|
180
|
-
material.materialHandle ?? -1,
|
|
181
|
-
gpuDrivenSourceDrawItemIndex(draw, compactIndex),
|
|
182
|
-
),
|
|
183
|
-
);
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
158
|
/**
|
|
188
159
|
* Project one shared validation result into a cull-specific renderable index
|
|
189
160
|
* domain. The retained ShadowCaster projection is the superset; the primary
|
|
@@ -304,8 +275,20 @@ export async function observeCurrentFrame(
|
|
|
304
275
|
}
|
|
305
276
|
|
|
306
277
|
import { acknowledgeRenderFeatureSubmission } from '../features/host';
|
|
278
|
+
import {
|
|
279
|
+
effectiveMotionBlurSampleCount,
|
|
280
|
+
isMotionBlurIntervalValid,
|
|
281
|
+
MOTION_BLUR_PARAMS_BYTE_SIZE,
|
|
282
|
+
motionBlurExposureScale,
|
|
283
|
+
motionBlurSampleDelta,
|
|
284
|
+
motionBlurTemporalDemand,
|
|
285
|
+
} from '../features/motion-blur/motion-blur-params';
|
|
307
286
|
import { createTemporalView, resolveTemporalReset, type TemporalView } from '../temporal/view';
|
|
308
287
|
import { inspectVolumetricFog } from '../volume/inspection';
|
|
288
|
+
import {
|
|
289
|
+
packBarrelDistortionParams,
|
|
290
|
+
resolveSubmittedBarrelMapping,
|
|
291
|
+
} from './barrel-distortion-frame';
|
|
309
292
|
import {
|
|
310
293
|
acquireSwapChainTarget,
|
|
311
294
|
graphExecutionPhase,
|
|
@@ -318,12 +301,11 @@ import {
|
|
|
318
301
|
computeViewMatrix,
|
|
319
302
|
driveLazyEquirectProjection,
|
|
320
303
|
selectLazyEquirectHandle,
|
|
321
|
-
variantSetFromDefines,
|
|
322
304
|
warnMultiSkybox,
|
|
323
305
|
warnMultiSkylight,
|
|
324
306
|
} from './helpers';
|
|
325
307
|
import { computeSplitLdrSprite } from './main-pass-sprite-draws';
|
|
326
|
-
import {
|
|
308
|
+
import { uploadMeshSsboBatch } from './mesh-ssbo';
|
|
327
309
|
import { writeShadowCasterUniforms } from './shadow-pass';
|
|
328
310
|
import {
|
|
329
311
|
type CubeCaptureGraphState,
|
|
@@ -331,6 +313,7 @@ import {
|
|
|
331
313
|
executeCompiledFrameGraph,
|
|
332
314
|
getRenderFeatureGraphState,
|
|
333
315
|
type RenderFeatureGraphCandidate,
|
|
316
|
+
settleBarrelDistortionGraphCandidate,
|
|
334
317
|
settleDepthOfFieldGraphCandidate,
|
|
335
318
|
settleVolumetricFogGraphCandidate,
|
|
336
319
|
} from './typed-frame-graph';
|
|
@@ -394,110 +377,6 @@ export function gpuDrivenMeshesForFrame(
|
|
|
394
377
|
return meshes;
|
|
395
378
|
}
|
|
396
379
|
|
|
397
|
-
/**
|
|
398
|
-
* A frame-local material table. Repeated snapshot objects share one UBO slot;
|
|
399
|
-
* each renderable keeps only the slot index used by each authored material.
|
|
400
|
-
*/
|
|
401
|
-
export interface MaterialSlotPlan<T extends object> {
|
|
402
|
-
readonly slotIndices: readonly (readonly number[])[];
|
|
403
|
-
readonly slots: readonly T[];
|
|
404
|
-
/** Index of the first renderable that referenced each slot. */
|
|
405
|
-
readonly slotOwners: readonly number[];
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* Intern identical material snapshots into one frame-local slot table.
|
|
410
|
-
* Snapshot identity is the extract layer's invalidation token, so this keeps
|
|
411
|
-
* the same correctness boundary while avoiding duplicate payload assembly and
|
|
412
|
-
* upload for repeated scene instances.
|
|
413
|
-
*/
|
|
414
|
-
export function buildMaterialSlotPlan<T extends object>(
|
|
415
|
-
materialGroups: readonly (readonly T[])[],
|
|
416
|
-
bindingClass?: (material: T, owner: number) => string,
|
|
417
|
-
): MaterialSlotPlan<T> {
|
|
418
|
-
const slotByMaterial = new Map<T, Map<string, number>>();
|
|
419
|
-
const slots: T[] = [];
|
|
420
|
-
const slotOwners: number[] = [];
|
|
421
|
-
const slotIndices = materialGroups.map((materials, ownerIndex) =>
|
|
422
|
-
materials.map((material) => {
|
|
423
|
-
const resourceClass = bindingClass?.(material, ownerIndex) ?? '';
|
|
424
|
-
let materialClasses = slotByMaterial.get(material);
|
|
425
|
-
if (materialClasses === undefined) {
|
|
426
|
-
materialClasses = new Map();
|
|
427
|
-
slotByMaterial.set(material, materialClasses);
|
|
428
|
-
}
|
|
429
|
-
const cached = materialClasses.get(resourceClass);
|
|
430
|
-
if (cached !== undefined) return cached;
|
|
431
|
-
const slot = slots.length;
|
|
432
|
-
slots.push(material);
|
|
433
|
-
slotOwners.push(ownerIndex);
|
|
434
|
-
materialClasses.set(resourceClass, slot);
|
|
435
|
-
return slot;
|
|
436
|
-
}),
|
|
437
|
-
);
|
|
438
|
-
return { slotIndices, slots, slotOwners };
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* Return the largest complete renderable prefix supported by a shared
|
|
443
|
-
* mesh/material capacity. Mesh rows consume one slot per renderable; material
|
|
444
|
-
* rows use the deduplicated slot indices assigned in first-seen order.
|
|
445
|
-
*/
|
|
446
|
-
export function findRenderablePrefixForSlotCapacity(
|
|
447
|
-
materialSlotIndices: readonly (readonly number[])[],
|
|
448
|
-
slotCapacity: number,
|
|
449
|
-
): number {
|
|
450
|
-
const capacity = Math.max(0, Math.floor(slotCapacity));
|
|
451
|
-
const maxRenderableCount = Math.min(materialSlotIndices.length, capacity);
|
|
452
|
-
let renderableCount = 0;
|
|
453
|
-
while (renderableCount < maxRenderableCount) {
|
|
454
|
-
const slots = materialSlotIndices[renderableCount] ?? [];
|
|
455
|
-
if (slots.some((slot) => slot >= capacity)) break;
|
|
456
|
-
renderableCount += 1;
|
|
457
|
-
}
|
|
458
|
-
return renderableCount;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
/** Count material slots reachable from a prefix of a first-seen slot plan. */
|
|
462
|
-
export function materialSlotCountForPrefix(
|
|
463
|
-
materialSlotIndices: readonly (readonly number[])[],
|
|
464
|
-
renderableCount: number,
|
|
465
|
-
): number {
|
|
466
|
-
let maxSlot = -1;
|
|
467
|
-
for (let index = 0; index < renderableCount; index += 1) {
|
|
468
|
-
for (const slot of materialSlotIndices[index] ?? []) maxSlot = Math.max(maxSlot, slot);
|
|
469
|
-
}
|
|
470
|
-
return maxSlot + 1;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
type MeshSsboCapacityResult = ReturnType<typeof ensureMeshSsboCapacity>;
|
|
474
|
-
|
|
475
|
-
/**
|
|
476
|
-
* Resolve the actual mesh-SSBO ceiling after the shared grow contract ran.
|
|
477
|
-
* A missing grow controller is the legacy/test surface with no renderer-owned
|
|
478
|
-
* ceiling; a failed grow explicitly exposes the pre-grow capacity and must
|
|
479
|
-
* constrain every row written by this frame, including retained shadow rows.
|
|
480
|
-
*/
|
|
481
|
-
function meshSsboSlotCapacityAfterEnsure(
|
|
482
|
-
internals: Pick<RenderSystemInternals, 'growMeshSsbo' | 'meshSsboState'>,
|
|
483
|
-
neededSlots: number,
|
|
484
|
-
result: MeshSsboCapacityResult,
|
|
485
|
-
): number {
|
|
486
|
-
if (internals.growMeshSsbo === undefined) return Number.POSITIVE_INFINITY;
|
|
487
|
-
if (!result.ok) return Math.max(0, result.degradedToSlotCount);
|
|
488
|
-
return internals.meshSsboState?.slotCount ?? neededSlots;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
/**
|
|
492
|
-
* Keep a row sequence inside the capacity published by the mesh-SSBO owner.
|
|
493
|
-
* The caller has already reserved earlier rows (the primary view), so this
|
|
494
|
-
* helper only accepts the remaining slots for the retained shadow sequence.
|
|
495
|
-
*/
|
|
496
|
-
function limitRowsToMeshSsboCapacity<T>(rows: readonly T[], availableSlots: number): readonly T[] {
|
|
497
|
-
if (availableSlots === Number.POSITIVE_INFINITY) return rows;
|
|
498
|
-
return rows.slice(0, Math.max(0, Math.floor(availableSlots)));
|
|
499
|
-
}
|
|
500
|
-
|
|
501
380
|
export type RecordProfileRunner = <T>(phase: RenderRecordPhase, action: () => T) => T;
|
|
502
381
|
|
|
503
382
|
export interface CubeCaptureFrameInput {
|
|
@@ -727,46 +606,6 @@ async function mapReflectionFallbackReadback(
|
|
|
727
606
|
|
|
728
607
|
const VOLUMETRIC_FOG_PASS_COUNT = 4;
|
|
729
608
|
|
|
730
|
-
type VolumetricFogParamsSlot = 0 | 1;
|
|
731
|
-
|
|
732
|
-
function nextVolumetricFogParamsSlot(frameState: RenderFrameState): VolumetricFogParamsSlot {
|
|
733
|
-
const accepted = frameState.volumetricFogParamsAcceptedSlot;
|
|
734
|
-
const pending = frameState.volumetricFogParamsPendingSlot;
|
|
735
|
-
if (pending !== null && pending !== accepted) return pending;
|
|
736
|
-
return accepted === null ? 0 : accepted === 0 ? 1 : 0;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
function stageVolumetricFogParams(
|
|
740
|
-
internals: RenderSystemInternals,
|
|
741
|
-
frameState: RenderFrameState,
|
|
742
|
-
params: Float32Array,
|
|
743
|
-
): Buffer | undefined {
|
|
744
|
-
const slot = nextVolumetricFogParamsSlot(frameState);
|
|
745
|
-
let buffer = frameState.volumetricFogParamsBuffers[slot];
|
|
746
|
-
if (buffer === null) {
|
|
747
|
-
const created = internals.device.createBuffer({
|
|
748
|
-
label: `volumetric-fog-params-${slot}`,
|
|
749
|
-
size: VOLUMETRIC_FOG_PARAMS_BYTES,
|
|
750
|
-
usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
|
|
751
|
-
mappedAtCreation: false,
|
|
752
|
-
});
|
|
753
|
-
if (!created.ok) {
|
|
754
|
-
internals.errorRegistry.fire(created.error);
|
|
755
|
-
return undefined;
|
|
756
|
-
}
|
|
757
|
-
buffer = created.value;
|
|
758
|
-
frameState.volumetricFogParamsBuffers[slot] = buffer;
|
|
759
|
-
}
|
|
760
|
-
const written = internals.device.queue.writeBuffer(buffer, 0, params);
|
|
761
|
-
if (!written.ok) {
|
|
762
|
-
internals.errorRegistry.fire(written.error);
|
|
763
|
-
return undefined;
|
|
764
|
-
}
|
|
765
|
-
frameState.volumetricFogParamsPendingSlot = slot;
|
|
766
|
-
frameState.volumetricFogPendingParams = new Float32Array(params);
|
|
767
|
-
return buffer;
|
|
768
|
-
}
|
|
769
|
-
|
|
770
609
|
/** Retire parameter buffers only after the submit that stopped using them. */
|
|
771
610
|
function retireVolumetricFogParams(
|
|
772
611
|
internals: RenderSystemInternals,
|
|
@@ -803,6 +642,19 @@ function volumeRevision(values: readonly number[]): number {
|
|
|
803
642
|
return hash;
|
|
804
643
|
}
|
|
805
644
|
|
|
645
|
+
/** Resolve the stable world-space projection shared by cloud and lighting. */
|
|
646
|
+
function cloudShadowProjectionFor(
|
|
647
|
+
cloudLayer: ExtractedCloudLayer | undefined,
|
|
648
|
+
): CloudShadowProjection | undefined {
|
|
649
|
+
if (cloudLayer?.sunDirection === undefined) return undefined;
|
|
650
|
+
return createCloudShadowProjection({
|
|
651
|
+
center: [0, 0, 0],
|
|
652
|
+
sunDirection: cloudLayer.sunDirection,
|
|
653
|
+
range: cloudLayer.params.shadowRange,
|
|
654
|
+
resolution: cloudShadowResolutionForQuality(cloudLayer.params.quality),
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
806
658
|
function volumeTemporalSignature(
|
|
807
659
|
fog: ExtractedVolumetricFog,
|
|
808
660
|
prepared: VolumetricFogFrameContext,
|
|
@@ -824,7 +676,7 @@ function volumeTemporalSignature(
|
|
|
824
676
|
camera.orthoBottom,
|
|
825
677
|
camera.orthoTop,
|
|
826
678
|
]);
|
|
827
|
-
|
|
679
|
+
let fogRevision =
|
|
828
680
|
authored === undefined
|
|
829
681
|
? 0
|
|
830
682
|
: volumeRevision([
|
|
@@ -835,6 +687,8 @@ function volumeTemporalSignature(
|
|
|
835
687
|
...authored.emission,
|
|
836
688
|
authored.anisotropy,
|
|
837
689
|
authored.maxDistance,
|
|
690
|
+
authored.sampling === 'density' ? 1 : 0,
|
|
691
|
+
...Array.from(`${fog.guid}:${fog.digest}`, (char) => char.charCodeAt(0)),
|
|
838
692
|
]);
|
|
839
693
|
const selectedSpot =
|
|
840
694
|
fog.spotLightEntity === undefined
|
|
@@ -852,7 +706,7 @@ function volumeTemporalSignature(
|
|
|
852
706
|
fog.lightKind === 'directional' && directional?.entity === fog.lightEntity
|
|
853
707
|
? directional
|
|
854
708
|
: undefined;
|
|
855
|
-
|
|
709
|
+
let lightRevision =
|
|
856
710
|
selectedSpot === undefined && selectedPoint === undefined && selectedDirectional === undefined
|
|
857
711
|
? 0
|
|
858
712
|
: volumeRevision(
|
|
@@ -891,12 +745,30 @@ function volumeTemporalSignature(
|
|
|
891
745
|
selectedSpot.normalBias ?? 0.05,
|
|
892
746
|
selectedSpot.pcfKernelSize ?? 3,
|
|
893
747
|
selectedSpot.shadowAtlasTile,
|
|
748
|
+
Number(selectedSpot.projectorHandle ?? 0),
|
|
749
|
+
selectedSpot.projectorGeneration ?? 0,
|
|
750
|
+
selectedSpot.projectorRevision ?? 0,
|
|
894
751
|
...(selectedSpot.lightViewProj === undefined
|
|
895
752
|
? []
|
|
896
753
|
: [...selectedSpot.lightViewProj]),
|
|
897
754
|
]),
|
|
898
755
|
],
|
|
899
756
|
);
|
|
757
|
+
for (const [index, member] of (fog.additional ?? []).entries()) {
|
|
758
|
+
const signature = volumeTemporalSignature(
|
|
759
|
+
member,
|
|
760
|
+
{
|
|
761
|
+
...prepared,
|
|
762
|
+
densityGeneration: prepared.additional?.[index]?.densityGeneration ?? 0,
|
|
763
|
+
},
|
|
764
|
+
camera,
|
|
765
|
+
lights,
|
|
766
|
+
width,
|
|
767
|
+
height,
|
|
768
|
+
);
|
|
769
|
+
fogRevision = volumeRevision([fogRevision, signature.fogRevision, signature.densityGeneration]);
|
|
770
|
+
lightRevision = volumeRevision([lightRevision, signature.lightRevision]);
|
|
771
|
+
}
|
|
900
772
|
return {
|
|
901
773
|
cameraRevision,
|
|
902
774
|
fogRevision,
|
|
@@ -904,6 +776,7 @@ function volumeTemporalSignature(
|
|
|
904
776
|
densityGeneration: prepared.densityGeneration,
|
|
905
777
|
width,
|
|
906
778
|
height,
|
|
779
|
+
...(fog.worldTimeSeconds === undefined ? {} : { worldTimeSeconds: fog.worldTimeSeconds }),
|
|
907
780
|
...(fog.lightKind === undefined ? {} : { lightKind: fog.lightKind }),
|
|
908
781
|
...(fog.lightEntity === undefined ? {} : { lightEntity: fog.lightEntity }),
|
|
909
782
|
...(fog.pointLightEntity === undefined ? {} : { pointLightEntity: fog.pointLightEntity }),
|
|
@@ -957,135 +830,6 @@ function resolveVolumetricFogHistoryContext(
|
|
|
957
830
|
};
|
|
958
831
|
}
|
|
959
832
|
|
|
960
|
-
/**
|
|
961
|
-
* Resolve the authored density POD into the renderer-owned GPU resources used
|
|
962
|
-
* by the graph. This is deliberately a pull operation: a missing/stale
|
|
963
|
-
* handle or failed upload produces a structured error and leaves the graph
|
|
964
|
-
* candidate disabled for this frame rather than binding a synthetic density.
|
|
965
|
-
*/
|
|
966
|
-
function prepareVolumetricFogFrame(
|
|
967
|
-
internals: RenderSystemInternals,
|
|
968
|
-
frameState: RenderFrameState,
|
|
969
|
-
world: World,
|
|
970
|
-
worlds: readonly World[],
|
|
971
|
-
fog: ExtractedVolumetricFog | undefined,
|
|
972
|
-
lights: ExtractedLights,
|
|
973
|
-
volumeCapability: boolean,
|
|
974
|
-
projector: SpotLightProjectorFrameContext | undefined,
|
|
975
|
-
): VolumetricFogFrameContext | undefined {
|
|
976
|
-
if (
|
|
977
|
-
!volumeCapability ||
|
|
978
|
-
fog?.status !== 'available' ||
|
|
979
|
-
fog.fog === undefined ||
|
|
980
|
-
fog.densityHandle === undefined ||
|
|
981
|
-
fog.densityAsset === undefined ||
|
|
982
|
-
(fog.lightKind === 'spot'
|
|
983
|
-
? lights.spot.find((light) => light.entity === fog.lightEntity) === undefined
|
|
984
|
-
: fog.lightKind === 'point'
|
|
985
|
-
? lights.point.find((light) => light.entity === fog.lightEntity) === undefined
|
|
986
|
-
: lights.directional === undefined)
|
|
987
|
-
) {
|
|
988
|
-
return undefined;
|
|
989
|
-
}
|
|
990
|
-
const ownerWorld = worlds[fog.worldId ?? 0] ?? world;
|
|
991
|
-
const resident = internals.gpuStore.ensureResident(
|
|
992
|
-
fog.densityHandle,
|
|
993
|
-
fog.densityAsset,
|
|
994
|
-
ownerWorld,
|
|
995
|
-
);
|
|
996
|
-
if (!resident.ok || !('texture' in resident.value)) {
|
|
997
|
-
if (!resident.ok) {
|
|
998
|
-
internals.errorRegistry.fire(
|
|
999
|
-
resident.error instanceof RhiError
|
|
1000
|
-
? resident.error
|
|
1001
|
-
: new RhiError({
|
|
1002
|
-
code: 'webgpu-runtime-error',
|
|
1003
|
-
expected: 'volumetric density residency to resolve a GPU texture',
|
|
1004
|
-
hint: 'repair the authored TextureAsset payload and retry the next frame',
|
|
1005
|
-
detail: { error: resident.error },
|
|
1006
|
-
}),
|
|
1007
|
-
);
|
|
1008
|
-
}
|
|
1009
|
-
return undefined;
|
|
1010
|
-
}
|
|
1011
|
-
const params = new Float32Array(32);
|
|
1012
|
-
params.set(fog.fog.bounds.min, 0);
|
|
1013
|
-
params.set(fog.fog.bounds.max, 4);
|
|
1014
|
-
params.set(fog.fog.extinction, 8);
|
|
1015
|
-
params.set(fog.fog.albedo, 12);
|
|
1016
|
-
params.set(fog.fog.emission, 16);
|
|
1017
|
-
const selectedPoint =
|
|
1018
|
-
fog.pointLightEntity === undefined
|
|
1019
|
-
? undefined
|
|
1020
|
-
: lights.point.find((light) => light.entity === fog.pointLightEntity);
|
|
1021
|
-
const selectedSpot =
|
|
1022
|
-
fog.spotLightEntity === undefined
|
|
1023
|
-
? fog.lightKind === 'spot'
|
|
1024
|
-
? lights.spot.find((light) => light.entity === fog.lightEntity)
|
|
1025
|
-
: undefined
|
|
1026
|
-
: lights.spot.find((light) => light.entity === fog.spotLightEntity);
|
|
1027
|
-
const lightDirection =
|
|
1028
|
-
selectedSpot?.direction ?? lights.directional?.direction ?? vec3.create(0, -1, 0);
|
|
1029
|
-
const lightColor = selectedSpot?.color ?? lights.directional?.color ?? vec3.create(0, 0, 0);
|
|
1030
|
-
if (lightDirection === undefined || lightColor === undefined) return undefined;
|
|
1031
|
-
params.set(lightDirection, 20);
|
|
1032
|
-
// Reuse the vec4 padding lane as the renderer-owned frame phase for
|
|
1033
|
-
// sub-froxel jitter. The temporal resolver consumes it without growing the
|
|
1034
|
-
// public fog component or allocating another uniform buffer.
|
|
1035
|
-
params[23] = frameState.frameNumber % 1024;
|
|
1036
|
-
const selectedPointIndex =
|
|
1037
|
-
selectedPoint === undefined
|
|
1038
|
-
? -1
|
|
1039
|
-
: lights.point.findIndex((light) => light.entity === selectedPoint.entity);
|
|
1040
|
-
const selectedSpotIndex =
|
|
1041
|
-
selectedSpot === undefined
|
|
1042
|
-
? -1
|
|
1043
|
-
: lights.spot.findIndex((light) => light.entity === selectedSpot.entity);
|
|
1044
|
-
// Cluster light_data is one point/spot/rect array. Point slots are emitted
|
|
1045
|
-
// first, so volume stores the global slot rather than a per-kind array index.
|
|
1046
|
-
const selectedSpotClusterIndex =
|
|
1047
|
-
selectedSpotIndex < 0 ? -1 : lights.point.length + selectedSpotIndex;
|
|
1048
|
-
params.set(lightColor, 24);
|
|
1049
|
-
params[19] = selectedPointIndex;
|
|
1050
|
-
params[27] = selectedSpotClusterIndex;
|
|
1051
|
-
params[28] = fog.fog.maxDistance;
|
|
1052
|
-
params[29] = fog.fog.anisotropy;
|
|
1053
|
-
params[30] =
|
|
1054
|
-
selectedPoint !== undefined && selectedSpot !== undefined
|
|
1055
|
-
? 3
|
|
1056
|
-
: selectedPoint !== undefined
|
|
1057
|
-
? 1
|
|
1058
|
-
: selectedSpot !== undefined
|
|
1059
|
-
? 2
|
|
1060
|
-
: 0;
|
|
1061
|
-
// The temporal shader reads this lane after the record stage resolves the
|
|
1062
|
-
// accepted graph/slot. It is overwritten on the next queue-visible frame;
|
|
1063
|
-
// the first candidate deliberately starts with history disabled.
|
|
1064
|
-
params[31] =
|
|
1065
|
-
frameState.temporalFrame !== undefined && frameState.temporalFrame.resetReason === 'none'
|
|
1066
|
-
? 1
|
|
1067
|
-
: 0;
|
|
1068
|
-
|
|
1069
|
-
const paramsBuffer = stageVolumetricFogParams(internals, frameState, params);
|
|
1070
|
-
if (paramsBuffer === undefined) return undefined;
|
|
1071
|
-
return {
|
|
1072
|
-
densityTexture: resident.value.texture.handle,
|
|
1073
|
-
densityView: resident.value.view,
|
|
1074
|
-
paramsBuffer,
|
|
1075
|
-
densityGeneration: resident.value.receipt.generation,
|
|
1076
|
-
historyReadSlot: null,
|
|
1077
|
-
historyWriteSlot: 0,
|
|
1078
|
-
historyValid: false,
|
|
1079
|
-
...(projector === undefined
|
|
1080
|
-
? {}
|
|
1081
|
-
: {
|
|
1082
|
-
projectorTexture: projector.texture,
|
|
1083
|
-
projectorView: projector.view,
|
|
1084
|
-
projectorSampler: projector.sampler,
|
|
1085
|
-
}),
|
|
1086
|
-
};
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
833
|
/**
|
|
1090
834
|
* Resolve the SpotLight-owned cookie independently of volumetric fog. Surface
|
|
1091
835
|
* lighting must keep the authored projector when the fog component is absent;
|
|
@@ -1219,6 +963,8 @@ function mapSsrHistoryResetReason(
|
|
|
1219
963
|
case 'environment-change':
|
|
1220
964
|
case 'fog-change':
|
|
1221
965
|
return 'reflection-generation';
|
|
966
|
+
case 'time-discontinuity':
|
|
967
|
+
return 'camera-cut';
|
|
1222
968
|
case 'device-recover':
|
|
1223
969
|
return 'device-recovery';
|
|
1224
970
|
case 'submit-failure':
|
|
@@ -1227,6 +973,50 @@ function mapSsrHistoryResetReason(
|
|
|
1227
973
|
}
|
|
1228
974
|
}
|
|
1229
975
|
|
|
976
|
+
interface FrameClockSample {
|
|
977
|
+
readonly deltaSeconds: number;
|
|
978
|
+
readonly elapsedSeconds: number | undefined;
|
|
979
|
+
/** Explicit host/replay clock was present but invalid. */
|
|
980
|
+
readonly invalidSampleTime: boolean;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/** Read the render sample clock from the camera's composition world once per frame. */
|
|
984
|
+
function frameClockSample(
|
|
985
|
+
world: World,
|
|
986
|
+
worlds: readonly World[],
|
|
987
|
+
camera: CameraSnapshot,
|
|
988
|
+
sampleTimeSeconds?: number,
|
|
989
|
+
): FrameClockSample {
|
|
990
|
+
const owner = worlds[camera.worldId ?? 0] ?? world;
|
|
991
|
+
const invalidSampleTime = sampleTimeSeconds !== undefined && !Number.isFinite(sampleTimeSeconds);
|
|
992
|
+
try {
|
|
993
|
+
const clock = owner.getResource(Time);
|
|
994
|
+
return {
|
|
995
|
+
deltaSeconds: Number.isFinite(clock.delta) ? Math.max(0, clock.delta) : 0,
|
|
996
|
+
// Keep the raw host sample even when ECS accepted only a clamped
|
|
997
|
+
// simulation delta. Temporal admission compares this value with the
|
|
998
|
+
// last successfully submitted sample and therefore sees real hitches.
|
|
999
|
+
elapsedSeconds:
|
|
1000
|
+
sampleTimeSeconds === undefined
|
|
1001
|
+
? Number.isFinite(clock.elapsed)
|
|
1002
|
+
? clock.elapsed
|
|
1003
|
+
: undefined
|
|
1004
|
+
: Number.isFinite(sampleTimeSeconds)
|
|
1005
|
+
? sampleTimeSeconds
|
|
1006
|
+
: undefined,
|
|
1007
|
+
invalidSampleTime,
|
|
1008
|
+
};
|
|
1009
|
+
} catch {
|
|
1010
|
+
// Test and recovery callers may provide a minimal World-shaped owner. An
|
|
1011
|
+
// absent clock is an invalid temporal pair and therefore safely resets.
|
|
1012
|
+
return {
|
|
1013
|
+
deltaSeconds: 0,
|
|
1014
|
+
elapsedSeconds: Number.isFinite(sampleTimeSeconds) ? sampleTimeSeconds : undefined,
|
|
1015
|
+
invalidSampleTime,
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1230
1020
|
function stageSsrHistoryCandidate(
|
|
1231
1021
|
internals: RenderSystemInternals,
|
|
1232
1022
|
frameState: RenderFrameState,
|
|
@@ -1611,7 +1401,12 @@ export function recordFrame(
|
|
|
1611
1401
|
readonly activeEntityKeys?: ReadonlySet<number>;
|
|
1612
1402
|
readonly activeEntityRevision?: number;
|
|
1613
1403
|
readonly telemetryCandidateCount?: number;
|
|
1404
|
+
readonly telemetrySubmit?: GpuDrivenLodSubmitIdentity;
|
|
1614
1405
|
readonly occlusion?: OcclusionFrameProjection;
|
|
1406
|
+
readonly frameTime?: number;
|
|
1407
|
+
readonly deviceGeneration?: number;
|
|
1408
|
+
readonly surfaceDynamicInput?: SurfaceDynamicInputFrame;
|
|
1409
|
+
readonly geometryLane?: 'automatic' | 'direct';
|
|
1615
1410
|
},
|
|
1616
1411
|
renderReadLeases?: readonly RenderReadLease[],
|
|
1617
1412
|
featureGraphCandidate?: RenderFeatureGraphCandidate,
|
|
@@ -1630,7 +1425,15 @@ export function recordFrame(
|
|
|
1630
1425
|
shadowCasterWorldKeys?: readonly number[],
|
|
1631
1426
|
preparedLighting?: ReturnType<typeof prepareFrameLighting>,
|
|
1632
1427
|
ssrDependencies?: SsrDependenciesInspection,
|
|
1633
|
-
onRenderableDraw?: (
|
|
1428
|
+
onRenderableDraw?: (
|
|
1429
|
+
entry: ValidatedRenderable,
|
|
1430
|
+
submeshIndex?: number,
|
|
1431
|
+
receipt?: RenderableDrawReceipt,
|
|
1432
|
+
) => void,
|
|
1433
|
+
analyticFog?: FogFrame,
|
|
1434
|
+
sampleTimeSeconds?: number,
|
|
1435
|
+
temporalReset = false,
|
|
1436
|
+
cloudLayer?: ExtractedCloudLayer,
|
|
1634
1437
|
): boolean {
|
|
1635
1438
|
void shadowCasterEntityKeys;
|
|
1636
1439
|
frameState.reflectionFallbackObservationSource = undefined;
|
|
@@ -1841,6 +1644,7 @@ export function recordFrame(
|
|
|
1841
1644
|
lights,
|
|
1842
1645
|
volumetricFog,
|
|
1843
1646
|
);
|
|
1647
|
+
const cloudShadowProjection = cloudShadowProjectionFor(cloudLayer);
|
|
1844
1648
|
|
|
1845
1649
|
const preparedVolumetricFog = prepareVolumetricFogFrame(
|
|
1846
1650
|
internals,
|
|
@@ -1887,21 +1691,29 @@ export function recordFrame(
|
|
|
1887
1691
|
(volumetricFog?.status === 'degraded' && acceptedFallback !== undefined);
|
|
1888
1692
|
|
|
1889
1693
|
let preparedGpuDriven: PreparedGpuDrivenFrame | undefined;
|
|
1694
|
+
let acceptedMaterialArtifacts: ReadonlyMap<string, MaterialShaderArtifact> | undefined;
|
|
1695
|
+
let acceptedShadowMaterialArtifacts: ReadonlyMap<string, MaterialShaderArtifact> | undefined;
|
|
1890
1696
|
let materialBindingClasses: ReadonlyMap<string, string> | undefined;
|
|
1891
1697
|
const abortPreparedGpuDriven = (): void => {
|
|
1892
1698
|
preparedGpuDriven?._abortResourceReplacement?.();
|
|
1893
1699
|
};
|
|
1894
1700
|
const cubeCaptureWork = prepareCubeCapture(internals, cubeCapture, camera);
|
|
1895
|
-
const reflectionFallbackCandidate =
|
|
1701
|
+
const reflectionFallbackCandidate =
|
|
1702
|
+
cubeCapture?.reflectionProbes?.fallbackDemand === true ||
|
|
1703
|
+
(ssrDependencies?.requested === true && ssrDependencies.reflectionFallback !== undefined);
|
|
1704
|
+
frameState.surfaceSubmissionObservation?.setActualLaneReason(
|
|
1705
|
+
reflectionFallbackCandidate ? 'reflection-fallback-mrt' : undefined,
|
|
1706
|
+
);
|
|
1707
|
+
// The Standard PBR GPU raster owns one color target. Reflection fallback
|
|
1708
|
+
// adds an MRT attachment, so keep this frame on the ordinary CPU producer
|
|
1709
|
+
// instead of claiming rows that the one-target GPU pipeline cannot draw.
|
|
1710
|
+
const frameGpuDriven = reflectionFallbackCandidate ? undefined : gpuDriven;
|
|
1896
1711
|
// Capture views have independent draw ownership; display-camera GPU work
|
|
1897
1712
|
// remains active while their faces and SSR fallback attachments are recorded.
|
|
1898
|
-
// ProbeBlendRecord is
|
|
1899
|
-
//
|
|
1900
|
-
//
|
|
1901
|
-
//
|
|
1902
|
-
// the scene-index lane carries the same record contract.
|
|
1903
|
-
const probeBlendFrame = renderables.some((entry) => entry.probeBlendRecord !== undefined);
|
|
1904
|
-
const frameGpuDriven = probeBlendFrame ? undefined : gpuDriven;
|
|
1713
|
+
// ProbeBlendRecord is a shared object-level fragment ABI. The prepared
|
|
1714
|
+
// GPU owner receives the renderer-owned record buffer below and binds it
|
|
1715
|
+
// at the same slot used by the direct material path, so probe-bearing rows
|
|
1716
|
+
// remain eligible for the GPU lane.
|
|
1905
1717
|
|
|
1906
1718
|
// Validate first so user meshes are resident in the same frame. The GPU
|
|
1907
1719
|
// owner consumes this producer-owned map together with the builtin mesh
|
|
@@ -2026,16 +1838,37 @@ export function recordFrame(
|
|
|
2026
1838
|
frameState.installedPipelineConfig?.ssao?.enabled === true),
|
|
2027
1839
|
reflectionFallback: frameState.reflectionFallbackDemand === true,
|
|
2028
1840
|
});
|
|
1841
|
+
acceptedMaterialArtifacts = materialArtifacts;
|
|
1842
|
+
acceptedShadowMaterialArtifacts = shadowMaterialArtifacts;
|
|
2029
1843
|
const paletteUploadBytes = validatedForResidency.reduce(
|
|
2030
1844
|
(total, row) => total + (row.source.skin?.uploadBytes ?? 0),
|
|
2031
1845
|
0,
|
|
2032
1846
|
);
|
|
1847
|
+
const gpuProbeBlendRequested = [...materialArtifacts.values()].some((artifact) =>
|
|
1848
|
+
requiresProbeBlendRecord(artifact),
|
|
1849
|
+
);
|
|
1850
|
+
const gpuProbeBlendBuffer = gpuProbeBlendRequested
|
|
1851
|
+
? ensureProbeBlendRecordBuffer(
|
|
1852
|
+
internals.device,
|
|
1853
|
+
frameState,
|
|
1854
|
+
frameGpuDriven.scene?.probeBlend,
|
|
1855
|
+
)
|
|
1856
|
+
: undefined;
|
|
2033
1857
|
const prepared = frameGpuDriven.owner.prepare({
|
|
2034
1858
|
scene: frameGpuDriven.scene,
|
|
2035
1859
|
camera,
|
|
2036
1860
|
meshBySlot: gpuDrivenMeshes,
|
|
2037
1861
|
viewBindGroupLayout: pipelineState.viewBindGroupLayout,
|
|
2038
1862
|
meshResidencyEpoch: internals.gpuStore.meshResidencyEpoch,
|
|
1863
|
+
// `hdrp` remains a compatibility stop for callers that cannot provide
|
|
1864
|
+
// the clustered topology. The production frame supplies the explicit
|
|
1865
|
+
// Standard topology below, so the GPU owner can select a matching
|
|
1866
|
+
// published group(2) artifact and bind the existing cluster owner.
|
|
1867
|
+
hdrp:
|
|
1868
|
+
standard.kind === 'clustered' &&
|
|
1869
|
+
(standard.prepared.local.length > 0 ||
|
|
1870
|
+
frameState.installedPipelineConfig?.ssao?.enabled === true),
|
|
1871
|
+
clustered: standard.kind === 'clustered',
|
|
2039
1872
|
materialBindingClasses,
|
|
2040
1873
|
...(frameGpuDriven.activeEntityKeys === undefined
|
|
2041
1874
|
? {}
|
|
@@ -2046,7 +1879,17 @@ export function recordFrame(
|
|
|
2046
1879
|
...(frameGpuDriven.telemetryCandidateCount === undefined
|
|
2047
1880
|
? {}
|
|
2048
1881
|
: { telemetryCandidateCount: frameGpuDriven.telemetryCandidateCount }),
|
|
1882
|
+
...(frameGpuDriven.telemetrySubmit === undefined
|
|
1883
|
+
? {}
|
|
1884
|
+
: { telemetrySubmit: frameGpuDriven.telemetrySubmit }),
|
|
2049
1885
|
...(frameGpuDriven.occlusion === undefined ? {} : { occlusion: frameGpuDriven.occlusion }),
|
|
1886
|
+
...(frameGpuDriven.frameTime === undefined ? {} : { frameTime: frameGpuDriven.frameTime }),
|
|
1887
|
+
...(frameGpuDriven.deviceGeneration === undefined
|
|
1888
|
+
? {}
|
|
1889
|
+
: { deviceGeneration: frameGpuDriven.deviceGeneration }),
|
|
1890
|
+
...(frameGpuDriven.surfaceDynamicInput === undefined
|
|
1891
|
+
? {}
|
|
1892
|
+
: { surfaceDynamicInput: frameGpuDriven.surfaceDynamicInput }),
|
|
2050
1893
|
...(frameGpuDriven.scene?.structureMetrics === undefined
|
|
2051
1894
|
? {}
|
|
2052
1895
|
: {
|
|
@@ -2059,6 +1902,9 @@ export function recordFrame(
|
|
|
2059
1902
|
...(firstSkinArtifact === undefined ? {} : { materialSkinArtifact: firstSkinArtifact }),
|
|
2060
1903
|
...(materialArtifacts.size === 0 ? {} : { materialArtifacts }),
|
|
2061
1904
|
...(shadowMaterialArtifacts.size === 0 ? {} : { shadowMaterialArtifacts }),
|
|
1905
|
+
...(gpuProbeBlendBuffer === undefined
|
|
1906
|
+
? {}
|
|
1907
|
+
: { probeBlendRecordBuffer: gpuProbeBlendBuffer }),
|
|
2062
1908
|
materialPipelineState: pipelineState,
|
|
2063
1909
|
...(internals.getMaterialShaderPipelineEntry === undefined
|
|
2064
1910
|
? {}
|
|
@@ -2119,7 +1965,12 @@ export function recordFrame(
|
|
|
2119
1965
|
}
|
|
2120
1966
|
}
|
|
2121
1967
|
|
|
2122
|
-
|
|
1968
|
+
// Direct acceptance keeps producer preparation/residency active so its
|
|
1969
|
+
// page/frame/shared-time candidate is validated and committed through the
|
|
1970
|
+
// same submit hooks. Only raster projection changes lanes.
|
|
1971
|
+
const rasterGpuDriven =
|
|
1972
|
+
frameGpuDriven?.geometryLane === 'direct' ? undefined : preparedGpuDriven;
|
|
1973
|
+
const gpuOwnedDrawKeys = rasterGpuDriven?.drawKeys ?? EMPTY_DRAW_KEYS;
|
|
2123
1974
|
// The GPU lane owns only the primary opaque raster today. Directional,
|
|
2124
1975
|
// point, and spot shadow views consume the same CPU-validated rows; they do
|
|
2125
1976
|
// not require a second residency walk because the first validation already
|
|
@@ -2136,6 +1987,7 @@ export function recordFrame(
|
|
|
2136
1987
|
validated,
|
|
2137
1988
|
gpuOwnedDrawKeys,
|
|
2138
1989
|
validationInput.cacheKey,
|
|
1990
|
+
rasterGpuDriven?.worldKeys,
|
|
2139
1991
|
);
|
|
2140
1992
|
const cacheValidated =
|
|
2141
1993
|
validatedShadowForResidency === undefined
|
|
@@ -2178,7 +2030,7 @@ export function recordFrame(
|
|
|
2178
2030
|
Math.max(1, internals.canvas.width),
|
|
2179
2031
|
Math.max(1, internals.canvas.height),
|
|
2180
2032
|
graphShadowMapSize,
|
|
2181
|
-
|
|
2033
|
+
rasterGpuDriven,
|
|
2182
2034
|
featureGraphCandidate,
|
|
2183
2035
|
cubeCapture?.state,
|
|
2184
2036
|
cameras.length === 0,
|
|
@@ -2190,6 +2042,13 @@ export function recordFrame(
|
|
|
2190
2042
|
undefined,
|
|
2191
2043
|
ssrAdmission,
|
|
2192
2044
|
renderExtent,
|
|
2045
|
+
validated.some((entry) =>
|
|
2046
|
+
entry.source.materials.some(
|
|
2047
|
+
(material) => material.surfaceModel === 'single-layer-medium',
|
|
2048
|
+
),
|
|
2049
|
+
),
|
|
2050
|
+
analyticFog !== undefined && analyticFog.density > 0 && analyticFog.maxOpacity > 0,
|
|
2051
|
+
cloudShadowProjection?.resolution,
|
|
2193
2052
|
),
|
|
2194
2053
|
);
|
|
2195
2054
|
if (preflightGraph === null) {
|
|
@@ -2266,8 +2125,47 @@ export function recordFrame(
|
|
|
2266
2125
|
const view = swapTarget.view;
|
|
2267
2126
|
const targetW = swapTarget.targetW;
|
|
2268
2127
|
const targetH = swapTarget.targetH;
|
|
2128
|
+
const submittedBarrelMapping = resolveSubmittedBarrelMapping(
|
|
2129
|
+
frameState,
|
|
2130
|
+
preflightGraph,
|
|
2131
|
+
camera,
|
|
2132
|
+
targetW,
|
|
2133
|
+
targetH,
|
|
2134
|
+
);
|
|
2269
2135
|
|
|
2270
2136
|
const previousTemporalView = frameState.lastSuccessfulTemporalView;
|
|
2137
|
+
const motionBlurDemand =
|
|
2138
|
+
camera.motionBlur === undefined
|
|
2139
|
+
? false
|
|
2140
|
+
: motionBlurTemporalDemand({
|
|
2141
|
+
shutterAngle: camera.motionBlur.shutterAngle,
|
|
2142
|
+
maxRadiusPixels: camera.motionBlur.maxRadiusPixels,
|
|
2143
|
+
sampleCount: camera.motionBlur.sampleCount,
|
|
2144
|
+
targetFps: camera.motionBlur.targetFps ?? 60,
|
|
2145
|
+
});
|
|
2146
|
+
const temporalDemand = camera.antialias === 'taa' || motionBlurDemand;
|
|
2147
|
+
const clock = frameClockSample(world, worlds, camera, sampleTimeSeconds);
|
|
2148
|
+
const deltaSeconds =
|
|
2149
|
+
temporalReset || clock.invalidSampleTime
|
|
2150
|
+
? Number.NaN
|
|
2151
|
+
: motionBlurSampleDelta(
|
|
2152
|
+
clock.elapsedSeconds,
|
|
2153
|
+
previousTemporalView?.sampleTimeSeconds,
|
|
2154
|
+
clock.deltaSeconds,
|
|
2155
|
+
);
|
|
2156
|
+
const intervalValid = isMotionBlurIntervalValid(deltaSeconds);
|
|
2157
|
+
// An off MB-only frame still carries an unjittered temporal view so the
|
|
2158
|
+
// accepted sample clock remains observable. Its history flag is false,
|
|
2159
|
+
// however, and must form a new baseline when the effect is enabled again.
|
|
2160
|
+
// A reset frame already has an explicit reason and is the valid baseline
|
|
2161
|
+
// for the following pair, so it is deliberately excluded here.
|
|
2162
|
+
const motionBlurFirstFrame =
|
|
2163
|
+
motionBlurDemand &&
|
|
2164
|
+
camera.antialias !== 'taa' &&
|
|
2165
|
+
(previousTemporalView === undefined ||
|
|
2166
|
+
(previousTemporalView.mode === 'off' &&
|
|
2167
|
+
!previousTemporalView.historyValid &&
|
|
2168
|
+
previousTemporalView.resetReason === undefined));
|
|
2271
2169
|
const currentUnjitteredViewProjection = mat4.multiply(
|
|
2272
2170
|
mat4.create(),
|
|
2273
2171
|
computeProjectionMatrix(camera),
|
|
@@ -2294,19 +2192,32 @@ export function recordFrame(
|
|
|
2294
2192
|
internalHeight: renderExtent.internalHeight,
|
|
2295
2193
|
}),
|
|
2296
2194
|
...(camera.historyVersion === undefined ? {} : { historyVersion: camera.historyVersion }),
|
|
2195
|
+
...(clock.elapsedSeconds === undefined ? {} : { sampleTimeSeconds: clock.elapsedSeconds }),
|
|
2297
2196
|
historyValid: false,
|
|
2298
2197
|
currentUnjitteredViewProjection,
|
|
2299
2198
|
currentCameraPosition,
|
|
2300
2199
|
});
|
|
2301
2200
|
const resetReason =
|
|
2302
|
-
|
|
2303
|
-
? resolveTemporalReset(previousTemporalView, temporalCandidate)
|
|
2201
|
+
temporalDemand && environmentReady
|
|
2202
|
+
? (resolveTemporalReset(previousTemporalView, temporalCandidate) ??
|
|
2203
|
+
(temporalReset || (motionBlurDemand && !intervalValid)
|
|
2204
|
+
? 'time-discontinuity'
|
|
2205
|
+
: motionBlurFirstFrame
|
|
2206
|
+
? 'first-frame'
|
|
2207
|
+
: undefined))
|
|
2304
2208
|
: undefined;
|
|
2305
2209
|
const canReusePrevious = previousTemporalView !== undefined && resetReason === undefined;
|
|
2306
2210
|
const temporalView = createTemporalView({
|
|
2307
2211
|
...temporalCandidate.input,
|
|
2308
2212
|
frameIndex: resetReason === undefined ? temporalCandidate.input.frameIndex : 0,
|
|
2309
|
-
|
|
2213
|
+
// Motion Blur needs a finite positive pair before it can read the
|
|
2214
|
+
// baseline. TAA-only frames retain their existing temporal contract and
|
|
2215
|
+
// do not depend on the simulation clock being present in a test host.
|
|
2216
|
+
historyValid:
|
|
2217
|
+
temporalDemand &&
|
|
2218
|
+
environmentReady &&
|
|
2219
|
+
canReusePrevious &&
|
|
2220
|
+
(!motionBlurDemand || intervalValid),
|
|
2310
2221
|
resetReason,
|
|
2311
2222
|
...(canReusePrevious && previousTemporalView.currentUnjitteredViewProjection !== undefined
|
|
2312
2223
|
? {
|
|
@@ -2357,17 +2268,26 @@ export function recordFrame(
|
|
|
2357
2268
|
);
|
|
2358
2269
|
activeVolumetricFogContext = resolvedHistory.context;
|
|
2359
2270
|
activeVolumetricFogSignature = resolvedHistory.signature;
|
|
2360
|
-
const
|
|
2271
|
+
const previousTime = frameState.volumetricFogHistorySignature?.worldTimeSeconds;
|
|
2272
|
+
const currentTime = resolvedHistory.signature.worldTimeSeconds;
|
|
2273
|
+
// Adjacent UBO lanes: validity, World time, elapsed time since accepted history.
|
|
2274
|
+
const historyParams = new Float32Array([
|
|
2275
|
+
activeVolumetricFogContext.historyValid ? 1 : 0,
|
|
2276
|
+
currentTime ?? 0,
|
|
2277
|
+
previousTime === undefined || currentTime === undefined
|
|
2278
|
+
? 0
|
|
2279
|
+
: Math.max(0, currentTime - previousTime),
|
|
2280
|
+
]);
|
|
2361
2281
|
if (frameState.volumetricFogPendingParams !== undefined) {
|
|
2362
|
-
frameState.volumetricFogPendingParams
|
|
2282
|
+
frameState.volumetricFogPendingParams.set(historyParams, 31);
|
|
2363
2283
|
}
|
|
2364
|
-
const
|
|
2284
|
+
const historyParamsWritten = internals.device.queue.writeBuffer(
|
|
2365
2285
|
activeVolumetricFogContext.paramsBuffer,
|
|
2366
2286
|
31 * Float32Array.BYTES_PER_ELEMENT,
|
|
2367
|
-
|
|
2287
|
+
historyParams,
|
|
2368
2288
|
);
|
|
2369
|
-
if (!
|
|
2370
|
-
internals.errorRegistry.fire(
|
|
2289
|
+
if (!historyParamsWritten.ok) {
|
|
2290
|
+
internals.errorRegistry.fire(historyParamsWritten.error);
|
|
2371
2291
|
abortPreparedGpuDriven();
|
|
2372
2292
|
return false;
|
|
2373
2293
|
}
|
|
@@ -2520,6 +2440,8 @@ export function recordFrame(
|
|
|
2520
2440
|
// View UBO + CSM/spot-shadow matrix pack: assembled + uploaded in one
|
|
2521
2441
|
// queue.writeBuffer round-trip. Extracted to view-ubo.ts (M3/w18) so
|
|
2522
2442
|
// recordFrame stays an orchestration skeleton (D-2).
|
|
2443
|
+
// The stable projection is carried in the View UBO. Direct-sun shader
|
|
2444
|
+
// consumers sample the renderer-owned map at each receiver position.
|
|
2523
2445
|
writeViewUbo(
|
|
2524
2446
|
internals.device.queue,
|
|
2525
2447
|
pipelineState.viewUniformBuffer,
|
|
@@ -2529,6 +2451,7 @@ export function recordFrame(
|
|
|
2529
2451
|
frameState.spotShadowSnapshots,
|
|
2530
2452
|
temporalView,
|
|
2531
2453
|
preparedSpotLightProjector?.spotIndex,
|
|
2454
|
+
cloudShadowProjection,
|
|
2532
2455
|
);
|
|
2533
2456
|
if (pipelineState.pointsLinesViewBuffer !== undefined) {
|
|
2534
2457
|
writePointsLinesViewUbo(
|
|
@@ -2600,7 +2523,7 @@ export function recordFrame(
|
|
|
2600
2523
|
internals,
|
|
2601
2524
|
frameState,
|
|
2602
2525
|
pipelineState,
|
|
2603
|
-
validated.length > 0 ||
|
|
2526
|
+
validated.length > 0 || rasterGpuDriven !== undefined,
|
|
2604
2527
|
bindGroupCounts,
|
|
2605
2528
|
undefined,
|
|
2606
2529
|
true,
|
|
@@ -2618,6 +2541,7 @@ export function recordFrame(
|
|
|
2618
2541
|
frameState.spotShadowSnapshots,
|
|
2619
2542
|
capture.viewBindGroupDynamicOffset,
|
|
2620
2543
|
preparedSpotLightProjector?.spotIndex,
|
|
2544
|
+
cloudShadowProjection,
|
|
2621
2545
|
);
|
|
2622
2546
|
}
|
|
2623
2547
|
for (const probe of cubeCapture?.reflectionProbes?.graph.work ?? []) {
|
|
@@ -2632,6 +2556,7 @@ export function recordFrame(
|
|
|
2632
2556
|
frameState.spotShadowSnapshots,
|
|
2633
2557
|
probe.viewBindGroupDynamicOffset,
|
|
2634
2558
|
preparedSpotLightProjector?.spotIndex,
|
|
2559
|
+
cloudShadowProjection,
|
|
2635
2560
|
);
|
|
2636
2561
|
}
|
|
2637
2562
|
|
|
@@ -2652,14 +2577,33 @@ export function recordFrame(
|
|
|
2652
2577
|
// builtin motion-blur pass receives its all-zero registration default and
|
|
2653
2578
|
// becomes a no-op even though the temporal scene target is populated.
|
|
2654
2579
|
const framePostProcessParams = new Map(postProcessParams);
|
|
2580
|
+
if (analyticFog !== undefined)
|
|
2581
|
+
framePostProcessParams.set(
|
|
2582
|
+
'forgeax.analytic-fog',
|
|
2583
|
+
encodeAnalyticFogParams(analyticFog, recordCamera),
|
|
2584
|
+
);
|
|
2655
2585
|
if (camera.motionBlur !== undefined) {
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2586
|
+
if (motionBlurDemand) {
|
|
2587
|
+
const motionBlurParams = new Uint8Array(MOTION_BLUR_PARAMS_BYTE_SIZE);
|
|
2588
|
+
const motionBlurView = new DataView(motionBlurParams.buffer);
|
|
2589
|
+
const uniformDeltaSeconds = Number.isFinite(deltaSeconds) ? deltaSeconds : 0;
|
|
2590
|
+
motionBlurView.setFloat32(0, camera.motionBlur.shutterAngle, true);
|
|
2591
|
+
motionBlurView.setFloat32(4, camera.motionBlur.maxRadiusPixels, true);
|
|
2592
|
+
motionBlurView.setUint32(
|
|
2593
|
+
8,
|
|
2594
|
+
effectiveMotionBlurSampleCount(camera.motionBlur.sampleCount),
|
|
2595
|
+
true,
|
|
2596
|
+
);
|
|
2597
|
+
motionBlurView.setUint32(12, temporalView?.historyValid === true ? 0 : 1, true);
|
|
2598
|
+
motionBlurView.setFloat32(16, camera.motionBlur.targetFps ?? 60, true);
|
|
2599
|
+
motionBlurView.setFloat32(
|
|
2600
|
+
20,
|
|
2601
|
+
motionBlurExposureScale(uniformDeltaSeconds, camera.motionBlur.targetFps ?? 60),
|
|
2602
|
+
true,
|
|
2603
|
+
);
|
|
2604
|
+
motionBlurView.setFloat32(24, uniformDeltaSeconds, true);
|
|
2605
|
+
framePostProcessParams.set('forgeax.motion-blur', motionBlurParams);
|
|
2606
|
+
}
|
|
2663
2607
|
}
|
|
2664
2608
|
if (camera.depthOfField !== undefined) {
|
|
2665
2609
|
const dofParams = packDepthOfFieldParams(camera.depthOfField, {
|
|
@@ -2675,7 +2619,18 @@ export function recordFrame(
|
|
|
2675
2619
|
framePostProcessParams.set(id, dofParams);
|
|
2676
2620
|
}
|
|
2677
2621
|
}
|
|
2622
|
+
if (submittedBarrelMapping !== undefined && submittedBarrelMapping.strength > 0) {
|
|
2623
|
+
framePostProcessParams.set(
|
|
2624
|
+
BARREL_DISTORTION_POST_PROCESS_ID,
|
|
2625
|
+
packBarrelDistortionParams(submittedBarrelMapping),
|
|
2626
|
+
);
|
|
2627
|
+
}
|
|
2678
2628
|
|
|
2629
|
+
if (preparedGpuDriven !== undefined) {
|
|
2630
|
+
frameState.surfaceSubmissionObservation?.setResourceGeneration(
|
|
2631
|
+
preparedGpuDriven.standardPbrFrameResources.resourceGeneration,
|
|
2632
|
+
);
|
|
2633
|
+
}
|
|
2679
2634
|
const passCtx: _InternalRenderPipelineContext = {
|
|
2680
2635
|
assets: internals.assets,
|
|
2681
2636
|
world,
|
|
@@ -2738,12 +2693,22 @@ export function recordFrame(
|
|
|
2738
2693
|
...(preparedGpuDriven === undefined
|
|
2739
2694
|
? {}
|
|
2740
2695
|
: {
|
|
2741
|
-
gpuDrivenStandardPbrFrameResources:
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2696
|
+
gpuDrivenStandardPbrFrameResources: {
|
|
2697
|
+
...preparedGpuDriven.standardPbrFrameResources,
|
|
2698
|
+
...(frameState.surfaceSubmissionObservation === undefined
|
|
2699
|
+
? {}
|
|
2700
|
+
: { surfaceSubmissionObservation: frameState.surfaceSubmissionObservation }),
|
|
2701
|
+
},
|
|
2702
|
+
...(rasterGpuDriven === undefined
|
|
2703
|
+
? {}
|
|
2704
|
+
: {
|
|
2705
|
+
gpuDrivenDrawKeys: rasterGpuDriven.drawKeys,
|
|
2706
|
+
gpuDrivenWorldKeys: rasterGpuDriven.worldKeys,
|
|
2707
|
+
gpuDrivenShadowViews: rasterGpuDriven.shadowViewPool,
|
|
2708
|
+
gpuDrivenShadowBatchProjections: rasterGpuDriven.shadowBatchProjections,
|
|
2709
|
+
gpuDrivenShadowDrawKeys: rasterGpuDriven.shadowDrawKeys,
|
|
2710
|
+
gpuDrivenShadowDrawKeysByView: rasterGpuDriven.shadowDrawKeysByView,
|
|
2711
|
+
}),
|
|
2747
2712
|
}),
|
|
2748
2713
|
...(frameGpuDriven?.scene === undefined
|
|
2749
2714
|
? {}
|
|
@@ -2817,6 +2782,12 @@ export function recordFrame(
|
|
|
2817
2782
|
frameState.ssrHistoryCandidate = undefined;
|
|
2818
2783
|
}
|
|
2819
2784
|
preparedGpuDriven?._commitResourceReplacement();
|
|
2785
|
+
const consumed = preparedGpuDriven?._consumeSurfaceDynamicInput?.(
|
|
2786
|
+
frameState.frameNumber,
|
|
2787
|
+
);
|
|
2788
|
+
if (consumed !== undefined && !consumed.ok) {
|
|
2789
|
+
internals.errorRegistry.fire(consumed.error);
|
|
2790
|
+
}
|
|
2820
2791
|
frameGpuDriven?.occlusion?.commit(true, frameState.frameNumber, profilePhase);
|
|
2821
2792
|
},
|
|
2822
2793
|
onAborted: () => {
|
|
@@ -2841,6 +2812,7 @@ export function recordFrame(
|
|
|
2841
2812
|
cubeCaptureWork.length === 0 ? undefined : cubeCaptureWork.map((work) => work.faceIndex),
|
|
2842
2813
|
(done) => {
|
|
2843
2814
|
completion = done;
|
|
2815
|
+
frameState.surfaceSubmissionObservation?.submit(done, frameState.graphGeneration);
|
|
2844
2816
|
if (fallbackReadbackRequest !== undefined) {
|
|
2845
2817
|
fallbackReadback = done.then(
|
|
2846
2818
|
() =>
|
|
@@ -2859,14 +2831,24 @@ export function recordFrame(
|
|
|
2859
2831
|
timingCapture,
|
|
2860
2832
|
),
|
|
2861
2833
|
);
|
|
2862
|
-
if (submitted &&
|
|
2834
|
+
if (submitted && onRenderableDraw !== undefined && rasterGpuDriven?.drawKeys !== undefined) {
|
|
2863
2835
|
for (const entry of validated) {
|
|
2864
|
-
|
|
2865
|
-
onRenderableDraw(entry);
|
|
2866
|
-
}
|
|
2836
|
+
emitGpuDrivenDrawReceipts(entry, rasterGpuDriven.drawKeys, onRenderableDraw);
|
|
2867
2837
|
}
|
|
2868
2838
|
}
|
|
2869
2839
|
if (submitted) {
|
|
2840
|
+
// `ensureCompiledFrameGraph` can deliberately return the accepted LKG
|
|
2841
|
+
// while a topology candidate is still preparing. The submitted graph is
|
|
2842
|
+
// the picture authority: an identity frame publishes an identity
|
|
2843
|
+
// mapping, while a failed enable/disable candidate retains the previous
|
|
2844
|
+
// mapping together with its old picture. Never pair an LKG graph with
|
|
2845
|
+
// the live World's newer camera component.
|
|
2846
|
+
// The exact mapping used to populate the accepted graph's UBO is the
|
|
2847
|
+
// only mapping promoted with its receipt. This keeps an identity frame,
|
|
2848
|
+
// a retained barrel LKG and a normal nonzero frame on one context.
|
|
2849
|
+
if (submittedBarrelMapping !== undefined) {
|
|
2850
|
+
frameState.lastSuccessfulBarrelDistortion = submittedBarrelMapping;
|
|
2851
|
+
}
|
|
2870
2852
|
// Feature plans are submission-sensitive: VFX keeps queued fixed ticks
|
|
2871
2853
|
// until the graph that consumed them is actually submitted. Acknowledge
|
|
2872
2854
|
// the exact accepted graph here so successful frames do not replay every
|
|
@@ -2905,6 +2887,8 @@ export function recordFrame(
|
|
|
2905
2887
|
: null;
|
|
2906
2888
|
}
|
|
2907
2889
|
if (submitted) {
|
|
2890
|
+
frameState.recoveryMaterialArtifacts = acceptedMaterialArtifacts;
|
|
2891
|
+
frameState.recoveryShadowMaterialArtifacts = acceptedShadowMaterialArtifacts;
|
|
2908
2892
|
if (graphVolumetricFog === undefined) {
|
|
2909
2893
|
frameState.volumetricFogAccepted = undefined;
|
|
2910
2894
|
frameState.volumetricFogAcceptedContext = undefined;
|
|
@@ -2957,12 +2941,13 @@ export function recordFrame(
|
|
|
2957
2941
|
frameState.ssrHistoryOwner?.abortFrame(frameState.ssrHistoryCandidate, 'submit');
|
|
2958
2942
|
frameState.ssrHistoryCandidate = undefined;
|
|
2959
2943
|
}
|
|
2960
|
-
// DoF and volume topology replacements are provisional until the
|
|
2961
|
-
// submission succeeds. This runs for every early-return path as
|
|
2962
|
-
// swapchain, encoder, or graph failure cannot leave a
|
|
2963
|
-
// candidate as the next frame's accepted graph.
|
|
2944
|
+
// Output, DoF, and volume topology replacements are provisional until the
|
|
2945
|
+
// queue submission succeeds. This runs for every early-return path as
|
|
2946
|
+
// well, so a swapchain, encoder, or graph failure cannot leave a
|
|
2947
|
+
// non-submitted candidate as the next frame's accepted graph.
|
|
2964
2948
|
settleVolumetricFogGraphCandidate(frameState, submitted);
|
|
2965
2949
|
settleDepthOfFieldGraphCandidate(frameState, submitted);
|
|
2950
|
+
settleBarrelDistortionGraphCandidate(frameState, submitted);
|
|
2966
2951
|
if (volumeInspectionReady && (submitted || volumeSubmissionAttempted)) {
|
|
2967
2952
|
updateVolumetricFogInspection(
|
|
2968
2953
|
frameState,
|
|
@@ -3017,8 +3002,10 @@ function sameWorldState(
|
|
|
3017
3002
|
// atlas rather than reaching through World for a second mutation clock.
|
|
3018
3003
|
return false;
|
|
3019
3004
|
}
|
|
3020
|
-
if (
|
|
3021
|
-
|
|
3005
|
+
if (
|
|
3006
|
+
changes.version.structureEpoch !== cachedState.version.structureEpoch ||
|
|
3007
|
+
changes.world.changedComponentIds.length !== 0
|
|
3008
|
+
) {
|
|
3022
3009
|
return false;
|
|
3023
3010
|
}
|
|
3024
3011
|
}
|
|
@@ -3136,716 +3123,3 @@ function prepareDirectionalShadowCache(
|
|
|
3136
3123
|
},
|
|
3137
3124
|
};
|
|
3138
3125
|
}
|
|
3139
|
-
|
|
3140
|
-
/**
|
|
3141
|
-
* feat-20260704 M3/w18: validate renderable handles + collect the render plan,
|
|
3142
|
-
* extracted verbatim from `recordFrame`. Empty `renderables` input or all-
|
|
3143
|
-
* unregistered handles both yield an empty result (the Case E clear-pass-only
|
|
3144
|
-
* path). Builds per-entry renderState / stencilReference overlays from the
|
|
3145
|
-
* transparent-dispatch entries, resolves each MeshFilter.assetHandle through
|
|
3146
|
-
* the AssetRegistry + GPU store (with the sprite 9-slice mesh swap), and fires
|
|
3147
|
-
* structured `asset-not-registered` errors for handles that fail to resolve.
|
|
3148
|
-
*
|
|
3149
|
-
* @internal
|
|
3150
|
-
*/
|
|
3151
|
-
export function validateRenderables(
|
|
3152
|
-
internals: RenderSystemInternals,
|
|
3153
|
-
world: World,
|
|
3154
|
-
// feat-20260709-editor-world-partition ENGINE-fix-round2 (defect 2): the full
|
|
3155
|
-
// worlds[] list. Each renderable's mesh handle is a user-tier slot in its OWN
|
|
3156
|
-
// world's sharedRefs; resolving against a foreign world either misses
|
|
3157
|
-
// (asset-not-registered) or resolves the wrong slot payload. `world` (the
|
|
3158
|
-
// resource-owner) is retained as the fallback for renderables whose worldId
|
|
3159
|
-
// is out of range (defensive; extractFrames always stamps a valid index).
|
|
3160
|
-
worlds: readonly World[],
|
|
3161
|
-
pipelineState: PipelineState,
|
|
3162
|
-
frameState: RenderFrameState,
|
|
3163
|
-
renderables: readonly RenderableSnapshot[],
|
|
3164
|
-
transparentDispatch: readonly DispatchEntry[],
|
|
3165
|
-
excludedEntityKeys: ReadonlySet<number> = new Set<number>(),
|
|
3166
|
-
worldKeys?: readonly number[],
|
|
3167
|
-
): ValidatedRenderable[] {
|
|
3168
|
-
// bug-20260527-renderstate-pipeline-dispatch-gap D-4:
|
|
3169
|
-
// build a renderableIndex -> renderState map from dispatch entries
|
|
3170
|
-
// so each ValidatedRenderable carries its per-material renderState
|
|
3171
|
-
// override without an O(n^2) back-scan in the draw loop.
|
|
3172
|
-
const renderStateByRenderableIdx = new Map<number, MaterialRenderState | undefined>();
|
|
3173
|
-
// w10: also build a renderableIndex -> stencilReference map from
|
|
3174
|
-
// dispatch entries for per-draw setStencilReference calls.
|
|
3175
|
-
const stencilRefByRenderableIdx = new Map<number, number | undefined>();
|
|
3176
|
-
const variantSetByRenderableIdx = new Map<number, string | undefined>();
|
|
3177
|
-
// Keep these three independent maps because each overlay has its own
|
|
3178
|
-
// consumer and the last dispatch entry remains authoritative. Populate
|
|
3179
|
-
// them in one pass: the old code walked the same dispatch list three times
|
|
3180
|
-
// and repeated the same renderableIndex branch / hash writes.
|
|
3181
|
-
for (const de of transparentDispatch) {
|
|
3182
|
-
const renderableIndex = de.renderableIndex;
|
|
3183
|
-
if (renderableIndex === undefined) continue;
|
|
3184
|
-
renderStateByRenderableIdx.set(renderableIndex, de.renderState);
|
|
3185
|
-
stencilRefByRenderableIdx.set(renderableIndex, de.stencilReference);
|
|
3186
|
-
variantSetByRenderableIdx.set(renderableIndex, variantSetFromDefines(de.defines));
|
|
3187
|
-
}
|
|
3188
|
-
const validated: ValidatedRenderable[] = [];
|
|
3189
|
-
for (let rIdx = 0; rIdx < renderables.length; rIdx++) {
|
|
3190
|
-
const r = renderables[rIdx];
|
|
3191
|
-
if (r === undefined) continue;
|
|
3192
|
-
if (excludedEntityKeys.has(worldEntityKey(worldKeys?.[r.worldId] ?? r.worldId, r.entityKey)))
|
|
3193
|
-
continue;
|
|
3194
|
-
// feat-20260709-editor-world-partition ENGINE-fix-round2 (defect 2): resolve
|
|
3195
|
-
// this renderable's mesh against the world it was EXTRACTED from
|
|
3196
|
-
// (worlds[r.worldId]) — NOT the single resource-owner `world`. Builtin mesh
|
|
3197
|
-
// slots (< BUILTIN_BASE) resolve process-statically regardless of world, so
|
|
3198
|
-
// the per-world pick only matters for user-tier handles, but selecting it
|
|
3199
|
-
// unconditionally keeps a single code path. Falls back to the resource-owner
|
|
3200
|
-
// world if worldId is out of range (defensive; extractFrames always stamps
|
|
3201
|
-
// a valid index into worlds[]).
|
|
3202
|
-
const renderableWorld = worlds[r.worldId] ?? world;
|
|
3203
|
-
const assetRes = resolveAssetHandle<MeshAsset>(
|
|
3204
|
-
renderableWorld,
|
|
3205
|
-
toShared<'MeshAsset'>(r.assetHandle),
|
|
3206
|
-
);
|
|
3207
|
-
if (!assetRes.ok) {
|
|
3208
|
-
internals.errorRegistry.fire(
|
|
3209
|
-
new RhiError({
|
|
3210
|
-
code: 'asset-not-registered',
|
|
3211
|
-
expected: 'MeshFilter.assetHandle in AssetRegistry',
|
|
3212
|
-
hint: 'use HANDLE_CUBE / HANDLE_TRIANGLE imports; custom mesh register path: feat-future-asset-system',
|
|
3213
|
-
detail: { assetHandle: r.assetHandle },
|
|
3214
|
-
}),
|
|
3215
|
-
);
|
|
3216
|
-
continue;
|
|
3217
|
-
}
|
|
3218
|
-
// feat-20260601-device/gpu-residency-extraction M1 (D-1): builtin meshes
|
|
3219
|
-
// (slots through HANDLE_NINESLICE_QUAD) keep the createRenderer step-3
|
|
3220
|
-
// direct-upload + `pipelineState.meshes` path -- they are NOT routed
|
|
3221
|
-
// through `ensureResident`. User-registered meshes pull through the store
|
|
3222
|
-
// on first access (the register->upload push was severed in this M1);
|
|
3223
|
-
// the POD fetched above (assetRes.value) is passed in, store holds no
|
|
3224
|
-
// registry ref (D-2). A first-access miss builds the GPU buffers; later
|
|
3225
|
-
// frames hit the O(1) cache.
|
|
3226
|
-
const meshAssetHandle = toShared<'MeshAsset'>(r.assetHandle);
|
|
3227
|
-
let meshHandles = internals.gpuStore.getMeshGpuHandles(meshAssetHandle, renderableWorld);
|
|
3228
|
-
const lodMeshes = r.lods?.map((lod) => internals.assets.lookup<MeshAsset>(lod.mesh));
|
|
3229
|
-
const resolvedLodMeshes =
|
|
3230
|
-
lodMeshes?.every((lod): lod is MeshAsset => lod?.kind === 'mesh') === true
|
|
3231
|
-
? lodMeshes
|
|
3232
|
-
: undefined;
|
|
3233
|
-
if (meshHandles === undefined && r.assetHandle > handleSlot(HANDLE_NINESLICE_QUAD)) {
|
|
3234
|
-
const residentRes = internals.gpuStore.ensureResident(
|
|
3235
|
-
meshAssetHandle,
|
|
3236
|
-
assetRes.value,
|
|
3237
|
-
renderableWorld,
|
|
3238
|
-
resolvedLodMeshes,
|
|
3239
|
-
);
|
|
3240
|
-
if (residentRes.ok) {
|
|
3241
|
-
meshHandles = residentRes.value;
|
|
3242
|
-
} else if (residentRes.error instanceof RhiError) {
|
|
3243
|
-
internals.errorRegistry.fire(residentRes.error);
|
|
3244
|
-
}
|
|
3245
|
-
}
|
|
3246
|
-
meshHandles = meshHandles ?? pipelineState.meshes.get(r.assetHandle);
|
|
3247
|
-
if (meshHandles === undefined) {
|
|
3248
|
-
internals.errorRegistry.fire(
|
|
3249
|
-
new RhiError({
|
|
3250
|
-
code: 'asset-not-registered',
|
|
3251
|
-
expected: 'GPU mesh buffers uploaded for assetHandle',
|
|
3252
|
-
hint: 'await renderer.initialization before draw([world], { cameraOwner: 0, resourceOwner: 0 }); ensure AssetRegistry.configureGpuDevice ran so user meshes are uploaded',
|
|
3253
|
-
detail: { assetHandle: r.assetHandle },
|
|
3254
|
-
}),
|
|
3255
|
-
);
|
|
3256
|
-
continue;
|
|
3257
|
-
}
|
|
3258
|
-
// feat-20260527-sprite-nineslice M2 / w11 (plan-strategy section D-2):
|
|
3259
|
-
// sprite branch with non-zero `slicesAndMode` (post-w12 paramSnapshot
|
|
3260
|
-
// entry name) overrides the user-supplied mesh handle (typically
|
|
3261
|
-
// HANDLE_QUAD = 3) with the 16-vertex / 54-index HANDLE_NINESLICE_QUAD
|
|
3262
|
-
// (id=5) topology so the vertex shader sees the 4x4 grid required for
|
|
3263
|
-
// 9-region anchor mapping. Default slicesAndMode ([0, 0, 0, 0]) keeps
|
|
3264
|
-
// the legacy HANDLE_QUAD path; a flip from zero to non-zero on the
|
|
3265
|
-
// same entity routes here per-frame so AI users can toggle 9-slice
|
|
3266
|
-
// on the fly without re-spawning the entity (charter F1 minimum
|
|
3267
|
-
// surface). The HANDLE_NINESLICE_QUAD GPU buffers are seeded by
|
|
3268
|
-
// createRenderer step-3.
|
|
3269
|
-
//
|
|
3270
|
-
// feat-20260625-refactor-sprite-as-transparent-mesh M3 / w13: judgement
|
|
3271
|
-
// key migrated from `shadingModel === 'sprite'` to
|
|
3272
|
-
// `materialShaderId === 'forgeax::sprite'` (plan-strategy D-10); slices
|
|
3273
|
-
// sourced from `paramSnapshot.slicesAndMode` (post-w12 UBO-aligned
|
|
3274
|
-
// overlay path).
|
|
3275
|
-
//
|
|
3276
|
-
// feat-20260624-sprite-lit-shading-model-pure-2d-lighting M1' / t7:
|
|
3277
|
-
// sprite-lit shares the sprite paramSchema (5 fields, t4 mirror) so
|
|
3278
|
-
// the 9-slices mesh swap applies identically.
|
|
3279
|
-
let effectiveMeshHandles = meshHandles;
|
|
3280
|
-
if (
|
|
3281
|
-
r.material.materialShaderId === 'forgeax::sprite' ||
|
|
3282
|
-
r.material.materialShaderId === 'forgeax::sprite-lit'
|
|
3283
|
-
) {
|
|
3284
|
-
const slicesArr = r.material.paramSnapshot?.slicesAndMode as readonly number[] | undefined;
|
|
3285
|
-
if (
|
|
3286
|
-
slicesArr !== undefined &&
|
|
3287
|
-
slicesArr.length >= 4 &&
|
|
3288
|
-
(slicesArr[0] !== 0 || slicesArr[1] !== 0 || slicesArr[2] !== 0 || slicesArr[3] !== 0)
|
|
3289
|
-
) {
|
|
3290
|
-
const nineSliceHandles = pipelineState.meshes.get(handleSlot(HANDLE_NINESLICE_QUAD));
|
|
3291
|
-
if (nineSliceHandles !== undefined) {
|
|
3292
|
-
effectiveMeshHandles = nineSliceHandles;
|
|
3293
|
-
}
|
|
3294
|
-
}
|
|
3295
|
-
}
|
|
3296
|
-
if (r.morph !== undefined) {
|
|
3297
|
-
const morphed = prepareMorphMesh(
|
|
3298
|
-
internals,
|
|
3299
|
-
frameState,
|
|
3300
|
-
worldEntityKey(r.worldId, r.entityKey),
|
|
3301
|
-
effectiveMeshHandles,
|
|
3302
|
-
assetRes.value,
|
|
3303
|
-
r.morph,
|
|
3304
|
-
);
|
|
3305
|
-
if (morphed !== undefined) effectiveMeshHandles = morphed;
|
|
3306
|
-
}
|
|
3307
|
-
validated.push({
|
|
3308
|
-
source: r,
|
|
3309
|
-
world: renderableWorld,
|
|
3310
|
-
mesh: effectiveMeshHandles,
|
|
3311
|
-
renderableIndex: rIdx,
|
|
3312
|
-
renderState: renderStateByRenderableIdx.get(rIdx),
|
|
3313
|
-
variantSet: variantSetByRenderableIdx.get(rIdx),
|
|
3314
|
-
stencilReference: stencilRefByRenderableIdx.get(rIdx),
|
|
3315
|
-
});
|
|
3316
|
-
}
|
|
3317
|
-
return validated;
|
|
3318
|
-
}
|
|
3319
|
-
|
|
3320
|
-
// Keep the primary record-stage name explicit at its call site while sharing
|
|
3321
|
-
// the same owner implementation for the complete retained ShadowCaster view.
|
|
3322
|
-
|
|
3323
|
-
function prepareMorphMesh(
|
|
3324
|
-
internals: RenderSystemInternals,
|
|
3325
|
-
frameState: RenderFrameState,
|
|
3326
|
-
cacheKey: number,
|
|
3327
|
-
base: MeshGpuHandles,
|
|
3328
|
-
mesh: MeshAsset,
|
|
3329
|
-
morph: RenderableSnapshot['morph'],
|
|
3330
|
-
): MeshGpuHandles | undefined {
|
|
3331
|
-
if (morph === undefined || frameState.morphBuffers === undefined) return undefined;
|
|
3332
|
-
const targets = mesh.morphTargets;
|
|
3333
|
-
if (targets === undefined || targets.length !== morph.targetCount) return undefined;
|
|
3334
|
-
const position = base.layoutProjection.attributes.find(
|
|
3335
|
-
(attribute) => attribute.key === 'position',
|
|
3336
|
-
);
|
|
3337
|
-
if (
|
|
3338
|
-
position === undefined ||
|
|
3339
|
-
position.offset !== 0 ||
|
|
3340
|
-
position.shaderLocation !== 0 ||
|
|
3341
|
-
position.format !== 'float32x3' ||
|
|
3342
|
-
position.byteLength !== 12
|
|
3343
|
-
) {
|
|
3344
|
-
return undefined;
|
|
3345
|
-
}
|
|
3346
|
-
const vertexCount = deriveVertexCount(mesh.vertices, base.layoutProjection);
|
|
3347
|
-
if (vertexCount === undefined || vertexCount !== base.vertexCount || vertexCount <= 0) {
|
|
3348
|
-
return undefined;
|
|
3349
|
-
}
|
|
3350
|
-
const stride = base.layoutProjection.arrayStride / Float32Array.BYTES_PER_ELEMENT;
|
|
3351
|
-
|
|
3352
|
-
let active = false;
|
|
3353
|
-
for (const weight of morph.weights) {
|
|
3354
|
-
if (weight !== 0) {
|
|
3355
|
-
active = true;
|
|
3356
|
-
break;
|
|
3357
|
-
}
|
|
3358
|
-
}
|
|
3359
|
-
if (!active) {
|
|
3360
|
-
frameState.morphBuffers.get(cacheKey)?.buffer.destroy();
|
|
3361
|
-
frameState.morphBuffers.delete(cacheKey);
|
|
3362
|
-
return undefined;
|
|
3363
|
-
}
|
|
3364
|
-
|
|
3365
|
-
const vertices = new Float32Array(mesh.vertices);
|
|
3366
|
-
for (let targetIndex = 0; targetIndex < targets.length; targetIndex += 1) {
|
|
3367
|
-
const weight = morph.weights[targetIndex] ?? 0;
|
|
3368
|
-
const positions = targets[targetIndex]?.position;
|
|
3369
|
-
if (positions === undefined || positions.length !== base.vertexCount * 3) return undefined;
|
|
3370
|
-
for (let vertex = 0; vertex < base.vertexCount; vertex += 1) {
|
|
3371
|
-
const vertexBase = vertex * stride;
|
|
3372
|
-
const positionBase = vertex * 3;
|
|
3373
|
-
vertices[vertexBase] = (vertices[vertexBase] ?? 0) + (positions[positionBase] ?? 0) * weight;
|
|
3374
|
-
vertices[vertexBase + 1] =
|
|
3375
|
-
(vertices[vertexBase + 1] ?? 0) + (positions[positionBase + 1] ?? 0) * weight;
|
|
3376
|
-
vertices[vertexBase + 2] =
|
|
3377
|
-
(vertices[vertexBase + 2] ?? 0) + (positions[positionBase + 2] ?? 0) * weight;
|
|
3378
|
-
}
|
|
3379
|
-
}
|
|
3380
|
-
|
|
3381
|
-
let cached = frameState.morphBuffers.get(cacheKey);
|
|
3382
|
-
if (cached === undefined || cached.byteLength !== vertices.byteLength) {
|
|
3383
|
-
if (cached !== undefined && !cached.buffer.isDestroyed) {
|
|
3384
|
-
const destroyed = cached.buffer.destroy();
|
|
3385
|
-
if (!destroyed.ok) internals.errorRegistry.fire(destroyed.error);
|
|
3386
|
-
}
|
|
3387
|
-
const created = internals.device.createBuffer({
|
|
3388
|
-
label: `morph-${cacheKey}-vbo`,
|
|
3389
|
-
size: vertices.byteLength,
|
|
3390
|
-
usage: GPU_BUFFER_USAGE_VERTEX | GPU_BUFFER_USAGE_COPY_DST,
|
|
3391
|
-
mappedAtCreation: false,
|
|
3392
|
-
});
|
|
3393
|
-
if (!created.ok) {
|
|
3394
|
-
internals.errorRegistry.fire(created.error);
|
|
3395
|
-
frameState.morphBuffers.delete(cacheKey);
|
|
3396
|
-
return undefined;
|
|
3397
|
-
}
|
|
3398
|
-
const buffer = new GpuBuffer(internals.device, created.value);
|
|
3399
|
-
internals.deviceScope._adopt('buffer', buffer, (owned) => {
|
|
3400
|
-
if (!owned.isDestroyed) owned.destroy();
|
|
3401
|
-
});
|
|
3402
|
-
cached = {
|
|
3403
|
-
buffer,
|
|
3404
|
-
byteLength: vertices.byteLength,
|
|
3405
|
-
};
|
|
3406
|
-
frameState.morphBuffers.set(cacheKey, cached);
|
|
3407
|
-
}
|
|
3408
|
-
const written = internals.device.queue.writeBuffer(cached.buffer.handle, 0, vertices);
|
|
3409
|
-
if (!written.ok) {
|
|
3410
|
-
internals.errorRegistry.fire(written.error);
|
|
3411
|
-
return undefined;
|
|
3412
|
-
}
|
|
3413
|
-
return { ...base, vertexBuffer: cached.buffer };
|
|
3414
|
-
}
|
|
3415
|
-
|
|
3416
|
-
/**
|
|
3417
|
-
* feat-20260704 M3/w18: build the dispatch-ordered render plan, extracted
|
|
3418
|
-
* verbatim from `recordFrame`. (1) M3/w26 dispatch-ordered reorder: reorder
|
|
3419
|
-
* `validated` to follow the transparent-dispatch order (extract-order fallback
|
|
3420
|
-
* for unmatched entries). (2) feat-20260608 mesh-SSBO capacity gate: size the
|
|
3421
|
-
* mesh-SSBO + material-UBO pair to the larger of entity vs cumulative
|
|
3422
|
-
* material-slot count, truncating on ceiling (graceful degradation). (3)
|
|
3423
|
-
* feat-20260622 fold dispatch plan: build + apply the WebGL2 uniform-cap
|
|
3424
|
-
* fallback + bump the folded-draws metric.
|
|
3425
|
-
*
|
|
3426
|
-
* @internal
|
|
3427
|
-
*/
|
|
3428
|
-
export function buildDispatchPlan(
|
|
3429
|
-
internals: RenderSystemInternals,
|
|
3430
|
-
validated: readonly ValidatedRenderable[],
|
|
3431
|
-
transparentDispatch: readonly DispatchEntry[],
|
|
3432
|
-
foldBuckets: readonly FoldBucket[],
|
|
3433
|
-
shadowValidated: readonly ValidatedRenderable[] = [],
|
|
3434
|
-
materialBindingClasses?: ReadonlyMap<string, string>,
|
|
3435
|
-
): {
|
|
3436
|
-
validatedOrdered: readonly ValidatedRenderable[];
|
|
3437
|
-
shadowValidatedOrdered: readonly ValidatedRenderable[];
|
|
3438
|
-
foldDispatchPlan: FoldDispatchPlan | null;
|
|
3439
|
-
materialSlotIndices: readonly (readonly number[])[];
|
|
3440
|
-
materialSlots: readonly MaterialSnapshot[];
|
|
3441
|
-
materialSlotOwners: readonly ValidatedRenderable[];
|
|
3442
|
-
materialSlotCount: number;
|
|
3443
|
-
} {
|
|
3444
|
-
// A production GPU-owned frame has no CPU validated rows by design. The
|
|
3445
|
-
// dispatch list can still contain one entry per authored entity, but no
|
|
3446
|
-
// CPU plan, material table, or fold bucket is consumed in that lane. Return
|
|
3447
|
-
// the empty plan without walking that list so a steady 100k scene remains
|
|
3448
|
-
// bounded by the persistent GPU batches rather than per-entity record work.
|
|
3449
|
-
if (validated.length === 0 && shadowValidated.length === 0) {
|
|
3450
|
-
const meshSsboCapResult = ensureMeshSsboCapacity(internals, shadowValidated.length);
|
|
3451
|
-
const meshSsboSlotCapacity = meshSsboSlotCapacityAfterEnsure(
|
|
3452
|
-
internals,
|
|
3453
|
-
shadowValidated.length,
|
|
3454
|
-
meshSsboCapResult,
|
|
3455
|
-
);
|
|
3456
|
-
return {
|
|
3457
|
-
validatedOrdered: [],
|
|
3458
|
-
shadowValidatedOrdered: limitRowsToMeshSsboCapacity(shadowValidated, meshSsboSlotCapacity),
|
|
3459
|
-
foldDispatchPlan: null,
|
|
3460
|
-
materialSlotIndices: [],
|
|
3461
|
-
materialSlots: [],
|
|
3462
|
-
materialSlotOwners: [],
|
|
3463
|
-
materialSlotCount: 0,
|
|
3464
|
-
};
|
|
3465
|
-
}
|
|
3466
|
-
// M3 / w26: dispatch-ordered render. The dispatch list is pre-sorted
|
|
3467
|
-
// by queue (ascending, stable) by the extract stage per plan-strategy D-3.
|
|
3468
|
-
// Reorder validated renderables to follow the dispatch order, falling
|
|
3469
|
-
// back to extract order for renderables with no matching dispatch entry.
|
|
3470
|
-
let dispatchOrderMatchesValidated = transparentDispatch.length === validated.length;
|
|
3471
|
-
if (dispatchOrderMatchesValidated) {
|
|
3472
|
-
for (let i = 0; i < validated.length; i++) {
|
|
3473
|
-
if (transparentDispatch[i]?.renderableIndex !== validated[i]?.renderableIndex) {
|
|
3474
|
-
dispatchOrderMatchesValidated = false;
|
|
3475
|
-
break;
|
|
3476
|
-
}
|
|
3477
|
-
}
|
|
3478
|
-
}
|
|
3479
|
-
let validatedOrdered: readonly ValidatedRenderable[] = dispatchOrderMatchesValidated
|
|
3480
|
-
? validated
|
|
3481
|
-
: [...validated];
|
|
3482
|
-
if (transparentDispatch.length > 0 && !dispatchOrderMatchesValidated) {
|
|
3483
|
-
const validatedByRenderableIdx = new Map<number, ValidatedRenderable>();
|
|
3484
|
-
const seen = new Set<number>();
|
|
3485
|
-
for (const v of validated) {
|
|
3486
|
-
validatedByRenderableIdx.set(v.renderableIndex, v);
|
|
3487
|
-
}
|
|
3488
|
-
const ordered: ValidatedRenderable[] = [];
|
|
3489
|
-
for (const de of transparentDispatch) {
|
|
3490
|
-
if (de.renderableIndex === undefined) continue;
|
|
3491
|
-
const v = validatedByRenderableIdx.get(de.renderableIndex);
|
|
3492
|
-
if (v !== undefined && !seen.has(de.renderableIndex)) {
|
|
3493
|
-
seen.add(de.renderableIndex);
|
|
3494
|
-
ordered.push(v);
|
|
3495
|
-
}
|
|
3496
|
-
}
|
|
3497
|
-
// Append renderables not in the dispatch list (e.g. default-material entities)
|
|
3498
|
-
for (const v of validated) {
|
|
3499
|
-
if (!seen.has(v.renderableIndex)) {
|
|
3500
|
-
ordered.push(v);
|
|
3501
|
-
}
|
|
3502
|
-
}
|
|
3503
|
-
validatedOrdered = ordered;
|
|
3504
|
-
}
|
|
3505
|
-
|
|
3506
|
-
// Shadow-only/off-camera CPU residuals consume the same material UBO as
|
|
3507
|
-
// visible draws. Their preparation must not depend on main-view visibility.
|
|
3508
|
-
const materialRows = [...validatedOrdered, ...shadowValidated];
|
|
3509
|
-
const materialGroups = materialRows.map((entry): readonly MaterialSnapshot[] => {
|
|
3510
|
-
const shaderId = entry.source.material.materialShaderId;
|
|
3511
|
-
if (shaderId === 'forgeax::sprite' || shaderId === 'forgeax::sprite-lit') {
|
|
3512
|
-
return [entry.source.material];
|
|
3513
|
-
}
|
|
3514
|
-
return entry.source.materials.length > 0 ? entry.source.materials : [entry.source.material];
|
|
3515
|
-
});
|
|
3516
|
-
const materialSlotPlan = buildMaterialSlotPlan(materialGroups, (material, owner) => {
|
|
3517
|
-
const row = materialRows[owner];
|
|
3518
|
-
return row === undefined
|
|
3519
|
-
? ''
|
|
3520
|
-
: (materialBindingClasses?.get(
|
|
3521
|
-
materialBindingKey(row.source, material.materialHandle ?? -1),
|
|
3522
|
-
) ?? '');
|
|
3523
|
-
});
|
|
3524
|
-
let materialSlotIndices = materialSlotPlan.slotIndices;
|
|
3525
|
-
let materialSlots = materialSlotPlan.slots;
|
|
3526
|
-
let materialSlotOwners = materialSlotPlan.slotOwners.map((index) => {
|
|
3527
|
-
const owner = materialRows[index];
|
|
3528
|
-
if (owner === undefined) throw new Error('material slot has no validated owner');
|
|
3529
|
-
return owner;
|
|
3530
|
-
});
|
|
3531
|
-
let materialSlotCount = materialSlots.length;
|
|
3532
|
-
let materialRenderableCount = materialRows.length;
|
|
3533
|
-
|
|
3534
|
-
// feat-20260608-mesh-ssbo-dynamic-grow-l1-lift-1024-entity-cap M3 / T-M3-04:
|
|
3535
|
-
// ensure the mesh-SSBO + material-UBO buffer pair is large enough to hold
|
|
3536
|
-
// the render plan BEFORE the first per-entity writeBuffer.
|
|
3537
|
-
// On `ok:false` the controller has already fired a structured RuntimeError
|
|
3538
|
-
// (`mesh-ssbo-ceiling-reached` / `mesh-ssbo-capacity-exceeded`); we truncate
|
|
3539
|
-
// the draw list to the largest complete entity prefix whose cumulative
|
|
3540
|
-
// material slots fit `degradedToSlotCount` (graceful degradation per
|
|
3541
|
-
// plan-strategy D-2): render the subset that fits, discard overflow, no
|
|
3542
|
-
// black frame. The helper is idempotent across same-frame re-calls (AC-09)
|
|
3543
|
-
// and short-circuits on length=0 / length<=slotCount (boundary table).
|
|
3544
|
-
//
|
|
3545
|
-
// bug-20260609: feat-20260608 M5 amend made the material UBO indexed by
|
|
3546
|
-
// cumulative *material-slot* count (one slot per submesh material),
|
|
3547
|
-
// which is >= entity count once an entity carries `materials.length>1`.
|
|
3548
|
-
// The mesh + material buffer pair share `slotCount` (single allocator),
|
|
3549
|
-
// so we size against the larger of the two requirements: entity count
|
|
3550
|
-
// (mesh-SSBO consumer) vs cumulative material-slot count (material-UBO
|
|
3551
|
-
// consumer). Sprite entities collapse to 1 slot in the material table,
|
|
3552
|
-
// mirroring the same rule (sprite per-submesh OOS-1;
|
|
3553
|
-
// post-w13 judgement key migrated to materialShaderId).
|
|
3554
|
-
//
|
|
3555
|
-
// feat-20260624 M1' / t7: sprite-lit treated identically to sprite
|
|
3556
|
-
// for material-slot accounting (paramSchema mirror, t4).
|
|
3557
|
-
const neededSlots = Math.max(validatedOrdered.length + shadowValidated.length, materialSlotCount);
|
|
3558
|
-
const meshSsboCapResult = ensureMeshSsboCapacity(internals, neededSlots);
|
|
3559
|
-
const meshSsboSlotCapacity = meshSsboSlotCapacityAfterEnsure(
|
|
3560
|
-
internals,
|
|
3561
|
-
neededSlots,
|
|
3562
|
-
meshSsboCapResult,
|
|
3563
|
-
);
|
|
3564
|
-
if (!meshSsboCapResult.ok) {
|
|
3565
|
-
// Graceful degradation: the controller reports slots, but this stage
|
|
3566
|
-
// consumes entities. Find a complete prefix instead of slicing at the
|
|
3567
|
-
// numeric slot count; a multi-material entity can consume several slots.
|
|
3568
|
-
const degradedRenderableCount = findRenderablePrefixForSlotCapacity(
|
|
3569
|
-
materialSlotIndices,
|
|
3570
|
-
meshSsboCapResult.degradedToSlotCount,
|
|
3571
|
-
);
|
|
3572
|
-
materialRenderableCount = degradedRenderableCount;
|
|
3573
|
-
materialSlotCount = materialSlotCountForPrefix(materialSlotIndices, degradedRenderableCount);
|
|
3574
|
-
materialSlotIndices = materialSlotIndices.slice(0, degradedRenderableCount);
|
|
3575
|
-
materialSlots = materialSlots.slice(0, materialSlotCount);
|
|
3576
|
-
materialSlotOwners = materialSlotOwners.slice(0, materialSlotCount);
|
|
3577
|
-
validatedOrdered = validatedOrdered.slice(0, degradedRenderableCount);
|
|
3578
|
-
}
|
|
3579
|
-
const shadowValidatedOrdered = limitRowsToMeshSsboCapacity(
|
|
3580
|
-
shadowValidated,
|
|
3581
|
-
Math.min(meshSsboSlotCapacity, materialRenderableCount) - validatedOrdered.length,
|
|
3582
|
-
);
|
|
3583
|
-
|
|
3584
|
-
// feat-20260622-chunk-gpu-instancing-sprite-tilemap M1 / w4-record-swap
|
|
3585
|
-
// (D-1): build the fold dispatch plan once `validatedOrdered` is final
|
|
3586
|
-
// (post truncation by mesh-SSBO capacity gate). The plan re-keys each
|
|
3587
|
-
// non-singleton bucket from `renderableIndex` to the validated-ordered
|
|
3588
|
-
// index `i` consumed by the dispatch loops; the loops use it to skip
|
|
3589
|
-
// non-head bucket members and emit one instanced drawIndexed per
|
|
3590
|
-
// bucket head. Empty plan (no fold-eligible buckets) is a byte-
|
|
3591
|
-
// identical no-op for the dispatch loops below (charter P3: silent
|
|
3592
|
-
// pass-through, no error path).
|
|
3593
|
-
let renderableToValidatedIdx: Map<number, number> | null = null;
|
|
3594
|
-
let foldDispatchPlan: FoldDispatchPlan | null = null;
|
|
3595
|
-
if (foldBuckets.length > 0) {
|
|
3596
|
-
renderableToValidatedIdx = new Map<number, number>();
|
|
3597
|
-
for (let i = 0; i < validatedOrdered.length; i++) {
|
|
3598
|
-
const e = validatedOrdered[i];
|
|
3599
|
-
if (e === undefined) continue;
|
|
3600
|
-
renderableToValidatedIdx.set(e.renderableIndex, i);
|
|
3601
|
-
}
|
|
3602
|
-
foldDispatchPlan = buildFoldDispatchPlan(foldBuckets, renderableToValidatedIdx);
|
|
3603
|
-
|
|
3604
|
-
// feat-20260622 M2 / w11 (D-2 + D-9 + AC-05): WebGL2 uniform-fallback
|
|
3605
|
-
// per-bucket instance-count cap. When caps.storageBuffer===false AND
|
|
3606
|
-
// a fold bucket carries more than FOLD_UNIFORM_INSTANCE_CAP (128)
|
|
3607
|
-
// instances, fire RhiError({code:'instancing-exceeds-uniform-cap'})
|
|
3608
|
-
// AND remove the bucket from the dispatch plan so its members fall
|
|
3609
|
-
// through to the per-entity drawIndexed exit (the same exit the
|
|
3610
|
-
// mode-gate bypass uses — D-9 "shared fallback exit"). The frame
|
|
3611
|
-
// stays visually correct (charter proposition 9 graceful
|
|
3612
|
-
// degradation: no identity-collapse / black screen) while the cap
|
|
3613
|
-
// event surfaces structurally for AI users (proposition 4 explicit
|
|
3614
|
-
// failure on .code).
|
|
3615
|
-
//
|
|
3616
|
-
// Scope discrimination: tilemap-chunk-extract-system encodes
|
|
3617
|
-
// Layer.value = (layerOrder<<20) | (chunkIndex & 0xfffff), so a
|
|
3618
|
-
// bucket whose head entry carries non-zero low-20-bits is
|
|
3619
|
-
// definitively a tilemap-chunk dispatch site. Plain sprite buckets
|
|
3620
|
-
// use SPRITE_LAYER_VALUE = layerOrder<<20 (low-20 zero) by the
|
|
3621
|
-
// documented convention (apps/hello/asi-world main.ts pattern).
|
|
3622
|
-
// The chunkIndex===0 edge case maps to 'sprite' (the helper's
|
|
3623
|
-
// default branch) — a one-bucket ambiguity per layerOrder that is
|
|
3624
|
-
// acceptable for the AI-user affordance level (the error semantics
|
|
3625
|
-
// — "this bucket exceeded the cap" — is the actionable signal;
|
|
3626
|
-
// scope=sprite vs tilemap-chunk only refines the recovery hint).
|
|
3627
|
-
if (foldDispatchPlan.headBuckets.size > 0 && !internals.device.caps.storageBuffer) {
|
|
3628
|
-
const filteredHeads = new Map<number, FoldBucket>(foldDispatchPlan.headBuckets);
|
|
3629
|
-
const filteredSkips = new Set<number>(foldDispatchPlan.skipIndices);
|
|
3630
|
-
let filteredCount = foldDispatchPlan.foldedBucketCount;
|
|
3631
|
-
for (const [headIdx, bucket] of foldDispatchPlan.headBuckets) {
|
|
3632
|
-
const scope: 'sprite' | 'tilemap-chunk' =
|
|
3633
|
-
(bucket.layer & 0xfffff) !== 0 ? 'tilemap-chunk' : 'sprite';
|
|
3634
|
-
const decision = evaluateFoldBucketUniformCap(bucket, internals.device.caps, scope);
|
|
3635
|
-
if (decision.fallback && decision.error !== undefined) {
|
|
3636
|
-
internals.errorRegistry.fire(decision.error);
|
|
3637
|
-
filteredHeads.delete(headIdx);
|
|
3638
|
-
filteredCount -= 1;
|
|
3639
|
-
for (let j = 1; j < bucket.entries.length; j++) {
|
|
3640
|
-
const memberEntry = bucket.entries[j];
|
|
3641
|
-
if (memberEntry === undefined) continue;
|
|
3642
|
-
const memberValidatedIdx = renderableToValidatedIdx.get(memberEntry.renderableIndex);
|
|
3643
|
-
if (memberValidatedIdx !== undefined) {
|
|
3644
|
-
filteredSkips.delete(memberValidatedIdx);
|
|
3645
|
-
}
|
|
3646
|
-
}
|
|
3647
|
-
}
|
|
3648
|
-
}
|
|
3649
|
-
if (filteredCount !== foldDispatchPlan.foldedBucketCount) {
|
|
3650
|
-
foldDispatchPlan = {
|
|
3651
|
-
headBuckets: filteredHeads,
|
|
3652
|
-
skipIndices: filteredSkips,
|
|
3653
|
-
foldedBucketCount: filteredCount,
|
|
3654
|
-
};
|
|
3655
|
-
}
|
|
3656
|
-
}
|
|
3657
|
-
|
|
3658
|
-
// feat-20260622-chunk-gpu-instancing-sprite-tilemap M3 / w13 (D-3 +
|
|
3659
|
-
// AC-06): increment `render.instancing.foldedDraws` once per fold-
|
|
3660
|
-
// eligible head bucket retained after the cap-fallback filter above.
|
|
3661
|
-
// The metric tracks instanced drawIndexed call count for this frame
|
|
3662
|
-
// — cap-overrun buckets routed through the per-entity fallback exit
|
|
3663
|
-
// are removed from `foldDispatchPlan` and therefore not counted, by
|
|
3664
|
-
// construction (M2 / w11 cap-fallback + plan-strategy D-3 semantics).
|
|
3665
|
-
// Singleton buckets (mode-bypass under D-5, or non-foldable under
|
|
3666
|
-
// mode 0) carry `bucketSize === 1` and never enter `headBuckets`, so
|
|
3667
|
-
// the per-entity drawIndexed path correctly does not count.
|
|
3668
|
-
// SSOT helper lives in this record owner — engine code never
|
|
3669
|
-
// hardcodes the metric key string.
|
|
3670
|
-
incrementFoldedDrawsMetric(foldDispatchPlan, internals.metrics);
|
|
3671
|
-
}
|
|
3672
|
-
|
|
3673
|
-
return {
|
|
3674
|
-
validatedOrdered,
|
|
3675
|
-
shadowValidatedOrdered,
|
|
3676
|
-
foldDispatchPlan,
|
|
3677
|
-
materialSlotIndices,
|
|
3678
|
-
materialSlots,
|
|
3679
|
-
materialSlotOwners,
|
|
3680
|
-
materialSlotCount,
|
|
3681
|
-
};
|
|
3682
|
-
}
|
|
3683
|
-
|
|
3684
|
-
/**
|
|
3685
|
-
* feat-20260704 M3/w18: per-frame cache clean-up (despawn eviction), extracted
|
|
3686
|
-
* verbatim from `recordFrame`.
|
|
3687
|
-
*
|
|
3688
|
-
* feat-20260531-per-frame-bind-group-cache M4 / w14 (D-5): drop per-entity BG
|
|
3689
|
-
* cache entries (materialBgPerEntity / instancesBgPerEntity) + instance buffers
|
|
3690
|
-
* whose outer-Map entityKey (packed Entity u32) is absent from the current
|
|
3691
|
-
* validated set, preventing unbounded growth after entity despawn. view + mesh
|
|
3692
|
-
* caches are frame-shared (keyed by GPU resource handle objects, WeakMap chains
|
|
3693
|
-
* naturally bounded) and are not touched. feat-20260619 M4 / F11: destroy the
|
|
3694
|
-
* GPU instance buffer before Map.delete (D-6 symmetric release); failure fires
|
|
3695
|
-
* errorRegistry + continues the sweep.
|
|
3696
|
-
*
|
|
3697
|
-
* @internal
|
|
3698
|
-
*/
|
|
3699
|
-
function cleanPerFrameCaches(
|
|
3700
|
-
internals: RenderSystemInternals,
|
|
3701
|
-
frameState: RenderFrameState,
|
|
3702
|
-
validated: readonly ValidatedRenderable[],
|
|
3703
|
-
retainedEntityKeys: ReadonlySet<number> = new Set<number>(),
|
|
3704
|
-
): void {
|
|
3705
|
-
if (frameState.transientInstanceBuffers !== undefined) {
|
|
3706
|
-
disposeTransientInstanceBuffers(frameState.transientInstanceBuffers, internals.errorRegistry);
|
|
3707
|
-
}
|
|
3708
|
-
// Build a Set<number> of worldEntityKey composites from the validated
|
|
3709
|
-
// renderables. D-1a #4: validatedEntityKeys are worldEntityKey(worldId, entityKey)
|
|
3710
|
-
// composites matching the write-side keys of #1-#3 — cross-world false eviction
|
|
3711
|
-
// is prevented because worldEntityKey(0, k) !== worldEntityKey(1, k).
|
|
3712
|
-
const validatedEntityKeys = new Set<number>(retainedEntityKeys);
|
|
3713
|
-
const validatedInstanceKeys = new Set<number>(retainedEntityKeys);
|
|
3714
|
-
for (const v of validated) {
|
|
3715
|
-
const entityKey = worldEntityKey(v.source.worldId, v.source.entityKey);
|
|
3716
|
-
validatedEntityKeys.add(entityKey);
|
|
3717
|
-
validatedInstanceKeys.add(entityKey);
|
|
3718
|
-
if (v.source.instances !== undefined) {
|
|
3719
|
-
validatedInstanceKeys.add(instanceCollectionCacheKey(v.source.worldId, v.source.instances));
|
|
3720
|
-
}
|
|
3721
|
-
}
|
|
3722
|
-
|
|
3723
|
-
// Clean per-entity material BG cache: drop outer-Map entries whose
|
|
3724
|
-
// entityKey is absent from the current validated set. The shared and
|
|
3725
|
-
// singleton material caches have no entityKey and are not touched here.
|
|
3726
|
-
cleanPerEntityCache(frameState.materialBgPerEntity, validatedEntityKeys);
|
|
3727
|
-
|
|
3728
|
-
// Clean per-entity instances BG cache.
|
|
3729
|
-
cleanPerEntityCache(frameState.instancesBgPerEntity, validatedEntityKeys);
|
|
3730
|
-
|
|
3731
|
-
// D-5 retrofit: instanceBuffers clean-up. The instanceBuffers Map is
|
|
3732
|
-
// keyed by cacheKey (packed Entity u32, same as entityKey on
|
|
3733
|
-
// RenderableSnapshot). Drop entries whose key is no longer in the
|
|
3734
|
-
// validated set (OQ-3 / R-4). feat-20260619 M4 / F11: destroy the GPU
|
|
3735
|
-
// buffer before Map.delete so despawned entities release their
|
|
3736
|
-
// instance-buffer backing memory symmetrically (D-6).
|
|
3737
|
-
//
|
|
3738
|
-
// D-1a #1: instanceBuffers keys on the positive half (>= 0) are
|
|
3739
|
-
// worldEntityKey composites matching the write side. Negative half
|
|
3740
|
-
// fold-bucket keys (< 0) are NOT worldEntityKey; they are
|
|
3741
|
-
// material-handle-based and cross-world collision is semantically
|
|
3742
|
-
// correct (same material renders in same fold bucket).
|
|
3743
|
-
for (const [key, entry] of frameState.instanceBuffers.entries()) {
|
|
3744
|
-
if (!validatedInstanceKeys.has(key)) {
|
|
3745
|
-
if (!entry.buffer.isDestroyed) {
|
|
3746
|
-
const r = entry.buffer.destroy();
|
|
3747
|
-
if (!r.ok) internals.errorRegistry.fire(r.error);
|
|
3748
|
-
}
|
|
3749
|
-
frameState.instanceBuffers.delete(key);
|
|
3750
|
-
}
|
|
3751
|
-
}
|
|
3752
|
-
|
|
3753
|
-
// Chunk keys are `${worldEntityKey}:${chunkStart}`. Keep the key opaque to
|
|
3754
|
-
// the authoring API, but retain the numeric owner prefix for eviction.
|
|
3755
|
-
if (frameState.instanceBufferChunks !== undefined) {
|
|
3756
|
-
for (const [key, entry] of frameState.instanceBufferChunks.entries()) {
|
|
3757
|
-
const separator = key.indexOf(':');
|
|
3758
|
-
const ownerKey = separator < 0 ? Number.NaN : Number(key.slice(0, separator));
|
|
3759
|
-
if (validatedInstanceKeys.has(ownerKey)) continue;
|
|
3760
|
-
if (!entry.buffer.isDestroyed) {
|
|
3761
|
-
const r = entry.buffer.destroy();
|
|
3762
|
-
if (!r.ok) internals.errorRegistry.fire(r.error);
|
|
3763
|
-
}
|
|
3764
|
-
frameState.instanceBufferChunks.delete(key);
|
|
3765
|
-
}
|
|
3766
|
-
}
|
|
3767
|
-
|
|
3768
|
-
if (frameState.morphBuffers !== undefined) {
|
|
3769
|
-
for (const [key, entry] of frameState.morphBuffers.entries()) {
|
|
3770
|
-
if (validatedEntityKeys.has(key)) continue;
|
|
3771
|
-
if (!entry.buffer.isDestroyed) {
|
|
3772
|
-
const result = entry.buffer.destroy();
|
|
3773
|
-
if (!result.ok) internals.errorRegistry.fire(result.error);
|
|
3774
|
-
}
|
|
3775
|
-
frameState.morphBuffers.delete(key);
|
|
3776
|
-
}
|
|
3777
|
-
}
|
|
3778
|
-
}
|
|
3779
|
-
|
|
3780
|
-
/**
|
|
3781
|
-
* feat-20260704 M3/w18: record-stage fold operator linear scan, extracted
|
|
3782
|
-
* verbatim from `recordFrame`.
|
|
3783
|
-
*
|
|
3784
|
-
* feat-20260622-chunk-gpu-instancing-sprite-tilemap M1 / w4 + w5 (D-1, D-5):
|
|
3785
|
-
* groups transparent-sort-ordered dispatch entries with equal (Layer.value,
|
|
3786
|
-
* sortKey, materialHandle) into fold buckets. Mode-gate (D-5 extended): modes 0
|
|
3787
|
-
* (LAYER_Z) and 1 (LAYER_Y) fold using the pos z/y lanes; modes 2/3 bypass per-entity
|
|
3788
|
-
* (each entry a singleton bucket). Records `frameState.lastFoldBucketCount` =
|
|
3789
|
-
* fold-eligible buckets (bucketSize > 1) for the AC-06 metric. Empty dispatch
|
|
3790
|
-
* short-circuits (test fixtures pass null world).
|
|
3791
|
-
*
|
|
3792
|
-
* @internal
|
|
3793
|
-
*/
|
|
3794
|
-
function computeFoldBuckets(
|
|
3795
|
-
world: World,
|
|
3796
|
-
frameState: RenderFrameState,
|
|
3797
|
-
transparentDispatch: readonly DispatchEntry[],
|
|
3798
|
-
renderables: readonly RenderableSnapshot[],
|
|
3799
|
-
): readonly FoldBucket[] {
|
|
3800
|
-
if (transparentDispatch.length === 0) {
|
|
3801
|
-
frameState.lastFoldBucketCount = 0;
|
|
3802
|
-
return [];
|
|
3803
|
-
}
|
|
3804
|
-
const transparentSortCfg = getTransparentSortConfig(world);
|
|
3805
|
-
|
|
3806
|
-
// Only layer-Z / layer-Y modes can produce non-singleton fold buckets.
|
|
3807
|
-
// The other modes make one inert singleton per entry, which is discarded by
|
|
3808
|
-
// buildFoldDispatchPlan. Returning the same empty plan here avoids allocating
|
|
3809
|
-
// a transform matrix for every renderable when folding is disabled by mode.
|
|
3810
|
-
if (
|
|
3811
|
-
transparentSortCfg.mode !== TRANSPARENT_SORT_MODE_LAYER_Z &&
|
|
3812
|
-
transparentSortCfg.mode !== TRANSPARENT_SORT_MODE_LAYER_Y
|
|
3813
|
-
) {
|
|
3814
|
-
frameState.lastFoldBucketCount = 0;
|
|
3815
|
-
return [];
|
|
3816
|
-
}
|
|
3817
|
-
|
|
3818
|
-
// `transparentDispatch` is the legacy name for the complete sorted dispatch
|
|
3819
|
-
// list. Opaque entries can never participate in the transparent-only fold
|
|
3820
|
-
// plan, yet foldDispatchBuckets must preserve its general helper contract and
|
|
3821
|
-
// therefore materializes an inert singleton (including a Float32Array(16))
|
|
3822
|
-
// for each one. Filter only at this private production call site so the fold
|
|
3823
|
-
// helper keeps its testable semantics while record avoids work whose result
|
|
3824
|
-
// is provably discarded.
|
|
3825
|
-
let transparentCount = 0;
|
|
3826
|
-
for (const entry of transparentDispatch) {
|
|
3827
|
-
if (renderables[entry.renderableIndex]?.material.transparent === true) {
|
|
3828
|
-
transparentCount += 1;
|
|
3829
|
-
}
|
|
3830
|
-
}
|
|
3831
|
-
if (transparentCount === 0) {
|
|
3832
|
-
frameState.lastFoldBucketCount = 0;
|
|
3833
|
-
return [];
|
|
3834
|
-
}
|
|
3835
|
-
const foldCandidates =
|
|
3836
|
-
transparentCount === transparentDispatch.length
|
|
3837
|
-
? transparentDispatch
|
|
3838
|
-
: transparentDispatch.filter(
|
|
3839
|
-
(entry) => renderables[entry.renderableIndex]?.material.transparent === true,
|
|
3840
|
-
);
|
|
3841
|
-
const foldBuckets = foldDispatchBuckets(foldCandidates, transparentSortCfg.mode, renderables);
|
|
3842
|
-
// Count only fold-eligible buckets (bucketSize > 1) so the metric
|
|
3843
|
-
// surfaces fold actually reducing draws — singleton buckets under
|
|
3844
|
-
// mode bypass do not change draw count, so they do not contribute.
|
|
3845
|
-
let foldEligibleCount = 0;
|
|
3846
|
-
for (const b of foldBuckets) {
|
|
3847
|
-
if (b.bucketSize > 1) foldEligibleCount += 1;
|
|
3848
|
-
}
|
|
3849
|
-
frameState.lastFoldBucketCount = foldEligibleCount;
|
|
3850
|
-
return foldBuckets;
|
|
3851
|
-
}
|