@forgeax/engine-render 0.1.33 → 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 +1010 -124
- 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/host-contract.d.ts +3 -3
- package/dist/assembly/host-contract.d.ts.map +1 -1
- 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/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/webgpu-pbr-ready.d.ts +1 -0
- package/dist/assembly/webgpu-pbr-ready.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 +1 -1
- 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/{chunk-PA5DRJNI.mjs → chunk-ELJFJ2BU.mjs} +2 -2
- package/dist/chunk-ELJFJ2BU.mjs.map +1 -0
- package/dist/{chunk-ZDQWGQW7.mjs → chunk-HEB564WF.mjs} +4 -4
- package/dist/{chunk-ZDQWGQW7.mjs.map → chunk-HEB564WF.mjs.map} +1 -1
- package/dist/{chunk-VOZWDM3L.mjs → chunk-MKDIT2EM.mjs} +1307 -188
- package/dist/chunk-MKDIT2EM.mjs.map +1 -0
- package/dist/{chunk-LEAXOMYJ.mjs → chunk-OCDLEI6I.mjs} +3 -3
- package/dist/{chunk-LEAXOMYJ.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-3RTCJTH4.mjs → chunk-SHFUFJ5N.mjs} +184 -13
- package/dist/chunk-SHFUFJ5N.mjs.map +1 -0
- package/dist/{chunk-OO3DRYK7.mjs → chunk-UI5SFODX.mjs} +176 -96
- package/dist/chunk-UI5SFODX.mjs.map +1 -0
- package/dist/{chunk-PLAXOOX5.mjs → chunk-UINHLCZA.mjs} +561 -79
- package/dist/chunk-UINHLCZA.mjs.map +1 -0
- package/dist/{chunk-NB2S7ZRX.mjs → chunk-WEX45HKH.mjs} +127 -386
- 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/barrel-distortion.d.ts +13 -0
- package/dist/components/barrel-distortion.d.ts.map +1 -0
- 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/construct-renderer.d.ts +1 -1
- package/dist/construct-renderer.d.ts.map +1 -1
- package/dist/construct-renderer.mjs +6759 -2875
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/dynamic-geometry.d.ts +5 -1
- package/dist/dynamic-geometry.d.ts.map +1 -1
- package/dist/errors/cloud.d.ts +59 -0
- package/dist/errors/cloud.d.ts.map +1 -0
- package/dist/errors/render.d.ts +18 -4
- 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 +5 -1
- package/dist/extract/contracts.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 +6 -2
- package/dist/features/error-reporting.d.ts.map +1 -1
- package/dist/features/host.d.ts +10 -0
- package/dist/features/host.d.ts.map +1 -1
- package/dist/features/plan.d.ts +8 -0
- package/dist/features/plan.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.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 +47 -3
- 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 +26 -2
- package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
- package/dist/gpu-scene.d.ts +5 -0
- 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 +27 -7
- 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 +135 -1
- 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 +48 -100
- 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.map +1 -1
- package/dist/pipeline-builder.d.ts +1 -1
- package/dist/pipeline-builder.d.ts.map +1 -1
- 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 +37 -3
- package/dist/record/frame-snapshot.d.ts.map +1 -1
- package/dist/record/frame.d.ts +10 -61
- 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/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 +22 -3
- 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 +1 -1
- 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 +42 -4
- 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 +103 -2
- 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 +16 -9
- 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 +19 -14
- 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 +12 -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/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/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 +14 -2
- package/dist/typed-render-graph-primitives.d.ts.map +1 -1
- package/dist/volume/component.d.ts +9 -4
- 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/package.json +22 -21
- 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__/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__/factory-contract.integration.test.ts +295 -45
- package/src/__tests__/feature-depth-input.dawn.test.ts +129 -2
- 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-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 +6 -5
- package/src/__tests__/gpu-driven-view-gpu-evidence.ts +94 -6
- 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-scene.unit.test.ts +119 -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 +97 -305
- 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__/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 +13 -7
- 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-graphics-capability.unit.test.ts +66 -2
- package/src/__tests__/probe-bindings.unit.test.ts +582 -1
- package/src/__tests__/probe-scene-dirty.integration.test.ts +28 -1
- 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 +7 -0
- package/src/__tests__/render-feature-diagnostics.unit.test.ts +15 -2
- package/src/__tests__/render-feature-error-reporting.unit.test.ts +21 -1
- 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-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 +20 -20
- 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 +26 -10
- package/src/__tests__/resource-allocation.unit.test.ts +123 -0
- 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 +15 -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-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.integration.test.ts +182 -1
- 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__/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 +4 -2
- package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +30 -20
- 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 +113 -9
- package/src/__tests__/volumetric-fog-topology.unit.test.ts +5 -3
- 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/host-contract.ts +3 -3
- 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/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 +4 -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 +3 -3
- package/src/assembly/webgpu-pbr-ready.ts +9 -1
- package/src/assembly/webgpu-ready-per-pass.ts +104 -0
- package/src/assembly/webgpu-ready.ts +115 -61
- 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 +214 -252
- package/src/assembly/webgpu-vertex-layouts.ts +1 -1
- package/src/barrel-distortion.ts +247 -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/barrel-distortion.ts +46 -0
- 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/sprite-instances.ts +1 -1
- package/src/construct-renderer.ts +1 -1
- package/src/dynamic-geometry.ts +12 -1
- package/src/errors/cloud.ts +103 -0
- package/src/errors/render.ts +44 -7
- package/src/examples/single-layer-medium-surface.ts +59 -0
- package/src/extract/camera.ts +15 -1
- package/src/extract/contracts.ts +5 -0
- package/src/features/barrel-distortion.ts +172 -0
- package/src/features/error-reporting.ts +26 -6
- package/src/features/host.ts +115 -14
- package/src/features/plan.ts +8 -0
- package/src/features/render-graph-contribution.ts +9 -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 +33 -25
- package/src/gpu-driven/prepared-draw.ts +12 -4
- package/src/gpu-driven/production-raster-material.ts +1894 -0
- package/src/gpu-driven/production-raster.ts +825 -784
- 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 +164 -10
- package/src/gpu-scene.ts +75 -84
- package/src/hdrp-buffers.ts +4 -3
- package/src/ibl/skylight-bind-group.ts +49 -7
- package/src/index.ts +196 -9
- package/src/inspection-types.ts +162 -0
- package/src/instance-buffer-cache.ts +4 -4
- package/src/instances.ts +149 -569
- 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 +90 -24
- package/src/pipeline-builder.ts +1 -1
- package/src/plugin.ts +4 -0
- package/src/points-lines/__tests__/points-lines.bench.ts +17 -8
- 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 +1 -1
- 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 +39 -3
- package/src/record/frame.ts +217 -1084
- package/src/record/gpu-draw-receipts.ts +38 -0
- package/src/record/gpu-timing.ts +48 -5
- package/src/record/main-pass-geometry.ts +251 -82
- package/src/record/main-pass-material.ts +66 -4
- package/src/record/main-pass-sprite-draws.ts +7 -65
- package/src/record/main-pass.ts +156 -20
- 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 +39 -3
- package/src/record/shadow-pass.ts +30 -1
- package/src/record/skybox-post-pass.ts +28 -4
- package/src/record/typed-frame-graph.ts +770 -134
- package/src/record/view-ubo.ts +26 -3
- package/src/record/volume-params.ts +225 -0
- package/src/recovery/render-system-candidate.ts +235 -42
- package/src/reflection/record-owner.ts +7 -3
- package/src/render-contract.ts +48 -4
- package/src/render-graph-primitives.ts +7 -1
- package/src/render-pipeline.ts +130 -0
- package/src/render-system-extract-tail.ts +57 -61
- package/src/render-system-extract.ts +74 -76
- 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 +347 -363
- package/src/scene/probe-blend.ts +96 -4
- package/src/scene/render-scene.ts +349 -280
- 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/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/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 +70 -2
- package/src/volume/component.ts +10 -9
- package/src/volume/extract.ts +11 -5
- package/src/volume/inspection.ts +3 -0
- package/src/volume/passes.ts +87 -4
- package/src/volume/resources.ts +2 -1
- package/dist/chunk-3RTCJTH4.mjs.map +0 -1
- package/dist/chunk-NB2S7ZRX.mjs.map +0 -1
- package/dist/chunk-NNQL7CB3.mjs +0 -1713
- package/dist/chunk-NNQL7CB3.mjs.map +0 -1
- package/dist/chunk-OO3DRYK7.mjs.map +0 -1
- package/dist/chunk-PA5DRJNI.mjs.map +0 -1
- package/dist/chunk-PLAXOOX5.mjs.map +0 -1
- package/dist/chunk-VOZWDM3L.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -14,6 +14,32 @@
|
|
|
14
14
|
|
|
15
15
|
## Public frame contract
|
|
16
16
|
|
|
17
|
+
Every accepted `FrameReceipt` with a submitted camera context carries an
|
|
18
|
+
immutable `barrelDistortion` mapping, including the exact identity mapping when
|
|
19
|
+
the camera has no active distortion. The optional property is a transport and
|
|
20
|
+
pre-first-frame shape: `undefined` means that no accepted submitted display
|
|
21
|
+
context exists. It is fail-closed, never an identity guess; display consumers
|
|
22
|
+
must wait for a new submitted frame.
|
|
23
|
+
The mapping is tied to the submitted output extent, camera matrices, device
|
|
24
|
+
generation, graph generation, and frame identity. Consumers must keep those
|
|
25
|
+
facts together; a mapping from a newer World edit cannot be applied to an older
|
|
26
|
+
picture. The renderer publishes the mapping only after queue submission, and a
|
|
27
|
+
candidate that fails validation keeps the previous accepted picture and
|
|
28
|
+
mapping. The public projection is deeply frozen and contains no device, graph,
|
|
29
|
+
texture, or mutable resource handle.
|
|
30
|
+
|
|
31
|
+
`renderer.inspect().barrelDistortion` projects the same accepted context as
|
|
32
|
+
`effectiveMapping`, `extent`, `frameId`, `deviceGeneration`,
|
|
33
|
+
`graphGeneration`, and `lastKnownGood`. Before the first accepted frame, after
|
|
34
|
+
surface retirement, or during device loss, `effectiveMapping` is unavailable;
|
|
35
|
+
display consumers must wait for a new submitted frame. An invalid authoring
|
|
36
|
+
value returns `barrel-distortion-invalid-parameter` with its field and bound.
|
|
37
|
+
Unsupported float attachment capability is reported only for an active plan;
|
|
38
|
+
an empty or zero-strength plan stays zero-work. A pending or failed disable
|
|
39
|
+
keeps the old nonzero mapping with its picture, while a successful disable
|
|
40
|
+
publishes identity and retires the old feature resources through the normal
|
|
41
|
+
in-flight fence.
|
|
42
|
+
|
|
17
43
|
`Fog` provides distance fog with exponential height density through one
|
|
18
44
|
finite-depth fullscreen pass before temporal resolve, Bloom and tone mapping.
|
|
19
45
|
`density`, `heightFalloff`, linear `color` and `maxOpacity` are selected from
|
|
@@ -24,6 +50,41 @@ matching depth resolve is available. Translucent surfaces that do not write dept
|
|
|
24
50
|
use the opaque background depth. Use `VolumetricFog` for spatial density and
|
|
25
51
|
lighting/scattering effects; the meadow-style distance haze needs only `Fog`.
|
|
26
52
|
|
|
53
|
+
### Local volumetric fog
|
|
54
|
+
|
|
55
|
+
Spawn one `VolumetricFog` per local medium in the rendered environment World.
|
|
56
|
+
Each owner selects its own light, 3D density texture, world-space bounds, and
|
|
57
|
+
optical coefficients. Up to `MAX_VOLUMETRIC_FOG_OWNERS` (8) owners share the
|
|
58
|
+
renderer-owned inject, integrate, temporal, and composite passes. Overlapping
|
|
59
|
+
media add coefficients before integration; removing one owner keeps the others.
|
|
60
|
+
Removing the last owner retires the volume graph resources.
|
|
61
|
+
|
|
62
|
+
| Author field | Contract |
|
|
63
|
+
|:--|:--|
|
|
64
|
+
| `density` | Shared linear `TextureAsset`, `viewDimension: '3d'` |
|
|
65
|
+
| `sampling` | ECS enum: `VolumetricFogSamplingValue.noise` (default animated atmospheric grain), or `.density` (local normalized bounds sample) |
|
|
66
|
+
| `boundsMin`, `boundsMax` | Finite world coordinates, strictly increasing on each axis; not transformed by `Transform` |
|
|
67
|
+
| `extinction`, `albedo`, `emission` | Nonnegative RGB; albedo additionally at most 1 |
|
|
68
|
+
| `anisotropy`, `maxDistance` | Finite, respectively `(-1, 1)` and positive world units |
|
|
69
|
+
| `light`, `spotLight` | Same-World selected light and optional Point/Spot pair, as in the existing single-volume contract |
|
|
70
|
+
|
|
71
|
+
Pure `VolumetricFogAuthoring` validation uses the string sampling labels, not
|
|
72
|
+
the ECS numeric values. `extractVolumetricFog()` returns an available `fogs`
|
|
73
|
+
collection and validates every member atomically. Overflow returns
|
|
74
|
+
`volume-owner-conflict` with `detail.ownerCount`; invalid author parameters
|
|
75
|
+
retain the existing structured error contract.
|
|
76
|
+
|
|
77
|
+
Read `renderer.inspect().volumetricFog.ownerCount` after a completed frame for
|
|
78
|
+
the accepted collection, alongside status, pass count, memory, and generation.
|
|
79
|
+
It is not an echo of a rejected candidate. Light projectors retain the shared
|
|
80
|
+
renderer-selected projector contract; only its selected light samples the map.
|
|
81
|
+
|
|
82
|
+
> [!IMPORTANT]
|
|
83
|
+
> This is local-medium integration against scene depth, not a claim of
|
|
84
|
+
> depth-aware fogging of every transparent or VFX feature. Those consumers
|
|
85
|
+
> require their own rendered integration evidence. Software GPU readback is
|
|
86
|
+
> correctness evidence, not hardware performance evidence.
|
|
87
|
+
|
|
27
88
|
## 灯光最短入口
|
|
28
89
|
|
|
29
90
|
三条最短入口:
|
|
@@ -73,6 +134,10 @@ payload; the first-draw guard still rejects lazy cold uploads or pipeline builds
|
|
|
73
134
|
inside rendering. Preparation does not make an old-generation receipt current.
|
|
74
135
|
Recovery prepares the last successfully submitted workset; the next draw still
|
|
75
136
|
consumes current World edits and admits new resources through normal residency.
|
|
137
|
+
Fullscreen recovery warms only active declared post-process identities; graph-local
|
|
138
|
+
resource aliases are resolved by the prepared feature and are not separate pipelines.
|
|
139
|
+
Ordinary graph replacement retains the previous graph until submission settles,
|
|
140
|
+
using the same retirement boundary for resize and post-process changes.
|
|
76
141
|
|
|
77
142
|
`inspect().recovery` is always present. Its `phase` is `null` outside an active
|
|
78
143
|
attempt and otherwise follows `quiesce`, `acquire-adapter`, `acquire-device`,
|
|
@@ -92,10 +157,39 @@ uninitialized until a successful receipt; a neutral target or last-known-good
|
|
|
92
157
|
fallback is not proof of real recovery. Public consumers never receive graph
|
|
93
158
|
nodes, devices, history textures, or prepared handles.
|
|
94
159
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
160
|
+
The current-source manifest and schema identify `source`, `build`, `backend`,
|
|
161
|
+
`runner`, and `frameIdentity`. Structural graph receipts, Browser/Dawn
|
|
162
|
+
readback or PNG evidence, and historical oracle data are separate evidence
|
|
163
|
+
classes. An unavailable backend is reported as unavailable.
|
|
164
|
+
|
|
165
|
+
### Renderer-wide graph allocation inspection
|
|
166
|
+
|
|
167
|
+
`renderer.inspect().renderGraphGenerationAllocation` is the renderer-owned
|
|
168
|
+
logical allocation receipt across active, volumetric candidate, retiring, and
|
|
169
|
+
detached recovery graph generations:
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
const allocation = renderer.inspect().renderGraphGenerationAllocation;
|
|
173
|
+
if (allocation?.availability === 'complete') {
|
|
174
|
+
console.log(allocation.liveBytes, allocation.pendingRetirementBytes, allocation.peakBytes);
|
|
175
|
+
for (const entry of allocation.entries) {
|
|
176
|
+
console.log(entry.generation, entry.roles, entry.retirement, entry.allocation);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`roles` identifies `active`, `candidate`, and `retiring` ownership; `retirement`
|
|
182
|
+
is `active`, `pending`, or `failed`. `liveBytes` and
|
|
183
|
+
`pendingRetirementBytes` are current logical bytes, while `peakBytes` is the
|
|
184
|
+
simultaneous logical high-water mark captured at compile, replacement, and
|
|
185
|
+
retirement events. `failedRetirementCount` and `failedRetirementBytes` keep a
|
|
186
|
+
destroy refusal, throw, or fence failure visible until its owner is repaired.
|
|
187
|
+
`availability` is `complete`, `partial`, or `unavailable`, with
|
|
188
|
+
`unavailableGenerationCount` explaining omitted graph facts. `unit` is
|
|
189
|
+
`engine-allocation-bytes` and `physicalResidency` is always `unknown`: imported
|
|
190
|
+
resources remain with their importing owner and logical byte size is not a VRAM
|
|
191
|
+
measurement. After a device-loss recovery, inspect the replacement generation
|
|
192
|
+
and retry the frame; a retained failed or pending row is recovery evidence.
|
|
99
193
|
|
|
100
194
|
## RenderFeature: the producer seam (first-read index)
|
|
101
195
|
|
|
@@ -108,6 +202,589 @@ fallback material. The projection preserves `layoutIdentity`,
|
|
|
108
202
|
`programIdentity`, `cookIdentity`, and `materialPublicationIdentity` so a
|
|
109
203
|
stale draw can be traced to the first producer divergence.
|
|
110
204
|
|
|
205
|
+
### Single-layer medium Surface
|
|
206
|
+
|
|
207
|
+
`MaterialAsset.surface` selects the Engine template and one imported Surface
|
|
208
|
+
implementation. A Standard Surface imports
|
|
209
|
+
`forgeax_material::surface_v1::{SurfaceInput, SurfaceData}` and exports
|
|
210
|
+
`evaluate_surface(SurfaceInput) -> SurfaceData`. A
|
|
211
|
+
`model: 'single-layer-medium'` Surface instead imports
|
|
212
|
+
`forgeax_material::single_layer_medium_surface_v1::{SingleLayerMediumSurfaceInput,
|
|
213
|
+
SingleLayerMediumSurfaceData}` and exports that separate input/data ABI. The
|
|
214
|
+
Surface returns normal, roughness, coverage, foam, absorption, scattering, IOR,
|
|
215
|
+
and phase facts; Render performs the finite Beer-Lambert/single-scatter
|
|
216
|
+
integral, one Fresnel allocation, and consumes renderer-owned paired
|
|
217
|
+
color/depth frame facts. `maxDistanceMeters` is a finite authored upper bound
|
|
218
|
+
in metres for the sky-miss case. A valid value is carried through the Surface
|
|
219
|
+
ABI and Cook output, while invalid or absent input uses the conservative Engine
|
|
220
|
+
bound; it never turns an unavailable depth producer into a synthetic sample.
|
|
221
|
+
The Surface does not declare a stage, binding, attachment, BRDF, or vertex
|
|
222
|
+
displacement.
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
import { definePack, definePackageId } from '@forgeax/engine/pack/source';
|
|
226
|
+
import { ok, type MaterialAsset } from '@forgeax/engine/types';
|
|
227
|
+
|
|
228
|
+
const packageId = definePackageId('01900000-0000-7000-8000-000000000001');
|
|
229
|
+
|
|
230
|
+
const material = {
|
|
231
|
+
kind: 'material',
|
|
232
|
+
surface: {
|
|
233
|
+
model: 'single-layer-medium',
|
|
234
|
+
module: 'game::water_surface_a',
|
|
235
|
+
dynamicInput: {
|
|
236
|
+
name: 'waterEvents',
|
|
237
|
+
fields: [
|
|
238
|
+
{ name: 'position', type: 'vec3<f32>' },
|
|
239
|
+
{ name: 'time', type: 'f32' },
|
|
240
|
+
{ name: 'eventId', type: 'u32' },
|
|
241
|
+
],
|
|
242
|
+
maxRecords: 64,
|
|
243
|
+
maxDomains: 8,
|
|
244
|
+
maxPageBytes: 2048,
|
|
245
|
+
maxBindings: 1,
|
|
246
|
+
maxEventsPerSample: 8,
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
passes: [{ name: 'color', program: { module: 'forgeax::single-layer-medium' } }],
|
|
250
|
+
parameters: [
|
|
251
|
+
{ name: 'coverage', type: 'f32' },
|
|
252
|
+
{ name: 'absorption', type: 'vec3' },
|
|
253
|
+
],
|
|
254
|
+
values: { coverage: 0.85, absorption: [0.22, 0.07, 0.025] },
|
|
255
|
+
} satisfies MaterialAsset;
|
|
256
|
+
|
|
257
|
+
export default definePack({
|
|
258
|
+
schemaVersion: '2.0.0',
|
|
259
|
+
packageId,
|
|
260
|
+
name: 'Water',
|
|
261
|
+
build: () => ok({ 'material/water': material }),
|
|
262
|
+
});
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
The authored module reads that schema through the compiler-generated accessor;
|
|
266
|
+
parameter names are fields on `material`, not free uniforms or string lookups:
|
|
267
|
+
|
|
268
|
+
```wgsl
|
|
269
|
+
#define_import_path game::water_surface_a
|
|
270
|
+
#import forgeax_material::parameters::{material}
|
|
271
|
+
#import forgeax_material::single_layer_medium_surface_v1::{SingleLayerMediumSurfaceInput, SingleLayerMediumSurfaceData}
|
|
272
|
+
|
|
273
|
+
fn evaluate_surface(input: SingleLayerMediumSurfaceInput) -> SingleLayerMediumSurfaceData {
|
|
274
|
+
return SingleLayerMediumSurfaceData(
|
|
275
|
+
normalize(input.geometricNormalWS),
|
|
276
|
+
0.16,
|
|
277
|
+
material.coverage,
|
|
278
|
+
0.0,
|
|
279
|
+
material.absorption,
|
|
280
|
+
vec3<f32>(0.018, 0.04, 0.085),
|
|
281
|
+
1.333,
|
|
282
|
+
0.24,
|
|
283
|
+
900.0,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Cook validates this accessor against `parameters`, the Pack transport writes
|
|
289
|
+
the cooked material through JSON while preserving its GUID, and runtime loads
|
|
290
|
+
that GUID before allocating the `MaterialAsset` handle used by `MeshRenderer`.
|
|
291
|
+
The executable [LightProbe to ProbeBlend to Surface example](#lightprobe-to-probeblend-to-surface-example)
|
|
292
|
+
shows the `loadByGuid<MaterialAsset>` → World → `MeshRenderer` → `draw` half of
|
|
293
|
+
the same route.
|
|
294
|
+
|
|
295
|
+
The generated dynamic page is a bounded, read-only storage record. Create one
|
|
296
|
+
`ReadonlyDynamicInputPage`, write only changed records, then reserve an explicit
|
|
297
|
+
`{ domain, recordStart, recordCount, instanceIndex, member }` range for each draw.
|
|
298
|
+
`member` is the stable public address
|
|
299
|
+
`{ worldIdentity: world.identity, entityKey, drawItemIndex, instanceOrdinal }`.
|
|
300
|
+
Its
|
|
301
|
+
`contentRevision`, `bufferGeneration`, and `deviceGeneration` are separate
|
|
302
|
+
facts; stale ranges and consumption before an upload return a structured error.
|
|
303
|
+
The page uses the existing instance bind group's free binding and never uses
|
|
304
|
+
skinning's `customDataStart`. Publish the page to the existing Renderer owner
|
|
305
|
+
before drawing:
|
|
306
|
+
`renderer.setSurfaceDynamicInput({ page, ranges, projectionRevision, frameTime })`.
|
|
307
|
+
Advance `projectionRevision` only when range membership or addresses change;
|
|
308
|
+
record values and World time keep the current revision so stable frames reuse
|
|
309
|
+
the retained validation and consumption projection.
|
|
310
|
+
Range order is irrelevant: Render joins each range to the admitted candidate by
|
|
311
|
+
`member` and rejects missing, duplicate, or non-admitted identities.
|
|
312
|
+
|
|
313
|
+
#### Linear HDR optical oracle
|
|
314
|
+
|
|
315
|
+
The physical oracle observes the real Pack → GUID → World → medium nearest/color
|
|
316
|
+
path before exposure, tone mapping, and anti-aliasing. Arm the next successful
|
|
317
|
+
submit with `renderer.requestObservation(['linear-hdr'])`, keep its `FrameReceipt`,
|
|
318
|
+
then call `renderer.observe(receipt, { include: ['linear-hdr'] })`. The returned
|
|
319
|
+
bytes are padded-row `rgba16float`; `frameId`, device and graph generations,
|
|
320
|
+
texture identity, and readback identity bind them to that exact completed frame.
|
|
321
|
+
When no observation is requested, the renderer does not copy, map, or synchronously
|
|
322
|
+
read this attachment.
|
|
323
|
+
|
|
324
|
+
> [!IMPORTANT]
|
|
325
|
+
> Compare optical math in this linear attachment. A canvas screenshot is an
|
|
326
|
+
> exposed, tone-mapped, anti-aliased presentation artifact and cannot prove the
|
|
327
|
+
> Beer-Lambert, scatter, or Fresnel contract.
|
|
328
|
+
|
|
329
|
+
The permanent fixture loads authored water through the Preview Pack, freezes the
|
|
330
|
+
camera, medium parameters, paired background, light, event time, and fixed ROI,
|
|
331
|
+
then compares the observed half-floats with an independent double-precision
|
|
332
|
+
screen-space reference. Its evidence also carries the actual nearest/color
|
|
333
|
+
program and resource generations plus the completed submission receipt.
|
|
334
|
+
|
|
335
|
+
Author 4x coverage through the Camera component. ECS fields retain their numeric
|
|
336
|
+
schema representation; `ANTIALIAS_MSAA` extracts to the public closed
|
|
337
|
+
`antialias: 'msaa'` render fact:
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
import { ANTIALIAS_MSAA, Camera, orthographic } from '@forgeax/engine/render';
|
|
341
|
+
|
|
342
|
+
world.spawn({
|
|
343
|
+
component: Camera,
|
|
344
|
+
data: {
|
|
345
|
+
...orthographic({ left: -3, right: 3, bottom: -2, top: 2 }),
|
|
346
|
+
antialias: ANTIALIAS_MSAA,
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
The edge oracle independently projects the authored rod, wall, shoreline, and
|
|
352
|
+
rough-water rectangles onto the canonical four sample positions. Fully covered
|
|
353
|
+
control regions calibrate each authored linear-HDR endpoint. For opaque edges,
|
|
354
|
+
the nearest depth sample selects its matching color before the full-coverage
|
|
355
|
+
water pass. For partial water coverage, the final hardware resolve averages the
|
|
356
|
+
independently predicted covered samples. The oracle rejects the opposite policy,
|
|
357
|
+
checks a small moving-camera interval, repeats after an 800-by-450 backing resize,
|
|
358
|
+
and keeps the physical maximum per-channel error at `0.05`. Its pure four-sample
|
|
359
|
+
counterexample separately proves why averaging color and then choosing the
|
|
360
|
+
nearest depth loses the color/depth pair.
|
|
361
|
+
|
|
362
|
+
```sh
|
|
363
|
+
FORGEAX_SURFACE_LANE_PARITY=1 pnpm exec vitest run --project=dawn --retry=0 \
|
|
364
|
+
--maxWorkers=1 --no-file-parallelism \
|
|
365
|
+
packages/runtime/src/__tests__/surface-standard-pipeline.dawn.test.ts
|
|
366
|
+
|
|
367
|
+
FORGEAX_SURFACE_LANE_PARITY=1 pnpm exec vitest run \
|
|
368
|
+
--config vitest.browser.config.ts --project=browser --retry=0 \
|
|
369
|
+
--maxWorkers=1 --no-file-parallelism \
|
|
370
|
+
packages/runtime/src/__tests__/surface-standard-pipeline.browser.test.ts
|
|
371
|
+
|
|
372
|
+
FORGEAX_SURFACE_MSAA4X=1 FORGEAX_SURFACE_LANE_PARITY=1 \
|
|
373
|
+
pnpm exec vitest run --project=dawn --retry=0 --maxWorkers=1 \
|
|
374
|
+
--no-file-parallelism \
|
|
375
|
+
packages/runtime/src/__tests__/surface-standard-pipeline.dawn.test.ts
|
|
376
|
+
|
|
377
|
+
VITE_FORGEAX_SURFACE_MSAA4X=1 FORGEAX_SURFACE_LANE_PARITY=1 \
|
|
378
|
+
pnpm exec vitest run --project=browser --retry=0 --maxWorkers=1 \
|
|
379
|
+
--no-file-parallelism \
|
|
380
|
+
packages/runtime/src/__tests__/surface-standard-pipeline.browser.test.ts
|
|
381
|
+
|
|
382
|
+
# Public App lifecycle: World-time water events plus a cooked GPU splash.
|
|
383
|
+
FORGEAX_SURFACE_APP_LIFECYCLE_ONLY=1 pnpm exec vitest run --project=dawn \
|
|
384
|
+
--retry=0 --maxWorkers=1 --no-file-parallelism \
|
|
385
|
+
packages/runtime/src/__tests__/surface-standard-pipeline.dawn.test.ts
|
|
386
|
+
|
|
387
|
+
VITE_FORGEAX_SURFACE_APP_LIFECYCLE_ONLY=1 pnpm exec vitest run --project=browser \
|
|
388
|
+
--retry=0 --maxWorkers=1 --no-file-parallelism \
|
|
389
|
+
packages/runtime/src/__tests__/surface-standard-pipeline.browser.test.ts
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
The lifecycle gate enqueues two deduplicated impacts into one attached World.
|
|
393
|
+
Its Update system takes one `Time.elapsed` snapshot, stamps both Surface records,
|
|
394
|
+
and starts the matching `ParticleEffectPlayer` edges. The splash is the authored
|
|
395
|
+
`surface-water-splash.pack.json` asset: the Pack transport resolves its GUID,
|
|
396
|
+
`loadVfxGpuEffect` loads its cooked program, and `createVfxRuntimeHost` contributes
|
|
397
|
+
the production renderer feature. The gate rejects an effect without a renderer;
|
|
398
|
+
an intent-only program is not visible evidence.
|
|
399
|
+
|
|
400
|
+
Before each target submit, the gate arms the existing `linear-hdr` observation.
|
|
401
|
+
It reads pre-frozen 16-by-16 water-A, water-B, and above-water splash ROIs from
|
|
402
|
+
that completed receipt, and compares every pixel. The mask never depends on
|
|
403
|
+
brightness, medians, or observed error. Evidence carries the frame, device,
|
|
404
|
+
graph, texture, and readback identities. The active splash ROI must change and
|
|
405
|
+
return to its baseline after the effect expires; the VFX host must also retire
|
|
406
|
+
both players. `rgba16float` samples remain unclamped linear values, including
|
|
407
|
+
values above 1, and use the same maximum per-channel error of `0.05` at every
|
|
408
|
+
lifecycle stage. The oracle does not round them through an 8-bit domain.
|
|
409
|
+
|
|
410
|
+
A real wait while paused preserves World time, committed VFX state, receipt
|
|
411
|
+
count, and the exact active completed image: `paused` reuses the active receipt
|
|
412
|
+
and does not submit a zero-delta frame. `resume()` continues from retained World
|
|
413
|
+
time without adding the paused host interval or replaying either splash. The
|
|
414
|
+
later `stepFrame(0.05)` calls are explicit deterministic simulation steps used
|
|
415
|
+
to reach camera coverage and expiry; they advance World time and therefore are
|
|
416
|
+
not evidence that a paused App remains frozen. In Dawn only, a timer supplies
|
|
417
|
+
the missing host scheduling primitive; the same App remains the sole owner of
|
|
418
|
+
World update and Renderer draw.
|
|
419
|
+
|
|
420
|
+
### LightProbe to ProbeBlend to Surface example
|
|
421
|
+
|
|
422
|
+
The following scene uses one authored water material for two entities. Each
|
|
423
|
+
entity selects a different local `LightProbe` domain, while both consume the
|
|
424
|
+
same bounded dynamic-input page. The renderer derives `ProbeBlendRecord`, slot,
|
|
425
|
+
generation, and the direct or scene-index consumer lane from the attached
|
|
426
|
+
World; application code does not build a second probe table.
|
|
427
|
+
|
|
428
|
+
`drawProbeLitWater` receives an already-constructed Runtime `Renderer`.
|
|
429
|
+
Construct it at the Runtime boundary with `await createRenderer(canvas)`, check
|
|
430
|
+
the returned `Result` (`if (!result.ok) throw result.error`), and pass
|
|
431
|
+
`result.value` to this helper. Runtime completes renderer initialization before
|
|
432
|
+
returning that value, so the render helper does not own a second initialization
|
|
433
|
+
promise.
|
|
434
|
+
|
|
435
|
+
```ts
|
|
436
|
+
import { Time, World } from '@forgeax/engine-ecs';
|
|
437
|
+
import { createPlaneGeometry } from '@forgeax/engine-geometry';
|
|
438
|
+
import {
|
|
439
|
+
Camera,
|
|
440
|
+
DirectionalLight,
|
|
441
|
+
LightProbe,
|
|
442
|
+
MeshFilter,
|
|
443
|
+
MeshRenderer,
|
|
444
|
+
ReadonlyDynamicInputPage,
|
|
445
|
+
type Renderer,
|
|
446
|
+
} from '@forgeax/engine-render';
|
|
447
|
+
import { Transform } from '@forgeax/engine-scene';
|
|
448
|
+
import { type AssetRegistry } from '@forgeax/engine-assets-runtime';
|
|
449
|
+
import type { AssetGuid, MaterialAsset } from '@forgeax/engine-types';
|
|
450
|
+
|
|
451
|
+
export async function drawProbeLitWater(
|
|
452
|
+
renderer: Renderer,
|
|
453
|
+
assets: AssetRegistry,
|
|
454
|
+
waterGuid: AssetGuid,
|
|
455
|
+
) {
|
|
456
|
+
// The Pack producer cooks the authored MaterialAsset and its Surface module;
|
|
457
|
+
// runtime consumers load only the GUID-addressed publication.
|
|
458
|
+
const loaded = await assets.loadByGuid<MaterialAsset>(waterGuid);
|
|
459
|
+
if (!loaded.ok) throw loaded.error;
|
|
460
|
+
const water = loaded.value;
|
|
461
|
+
const dynamicInput = water.surface?.dynamicInput;
|
|
462
|
+
if (water.surface?.model !== 'single-layer-medium' || dynamicInput === undefined) {
|
|
463
|
+
throw new Error('expected an authored single-layer-medium Surface');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const world = new World();
|
|
467
|
+
const plane = createPlaneGeometry(1.8, 1.8).unwrap();
|
|
468
|
+
const mesh = world.allocSharedRef('MeshAsset', plane);
|
|
469
|
+
const material = world.allocSharedRef('MaterialAsset', water);
|
|
470
|
+
const waterEntities = [-1.2, 1.2].map((x) =>
|
|
471
|
+
world
|
|
472
|
+
.spawn(
|
|
473
|
+
{ component: Transform, data: { pos: [x, 0, 0] } },
|
|
474
|
+
{ component: MeshFilter, data: { assetHandle: mesh } },
|
|
475
|
+
{ component: MeshRenderer, data: { materials: [material] } },
|
|
476
|
+
)
|
|
477
|
+
.unwrap(),
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
const red = new Float32Array(27);
|
|
481
|
+
red[0] = 1.8;
|
|
482
|
+
const blue = new Float32Array(27);
|
|
483
|
+
blue[2] = 1.8;
|
|
484
|
+
world.spawn(
|
|
485
|
+
{ component: Transform, data: { pos: [-1.2, 0, 0] } },
|
|
486
|
+
{ component: LightProbe, data: { irradiance: red, radius: 1 } },
|
|
487
|
+
);
|
|
488
|
+
world.spawn(
|
|
489
|
+
{ component: Transform, data: { pos: [1.2, 0, 0] } },
|
|
490
|
+
{ component: LightProbe, data: { irradiance: blue, radius: 1 } },
|
|
491
|
+
);
|
|
492
|
+
world.spawn(
|
|
493
|
+
{ component: Transform, data: { pos: [0, 0, 6] } },
|
|
494
|
+
{
|
|
495
|
+
component: Camera,
|
|
496
|
+
data: { fov: Math.PI / 4, aspect: 16 / 9, near: 0.1, far: 20 },
|
|
497
|
+
},
|
|
498
|
+
);
|
|
499
|
+
world.spawn({
|
|
500
|
+
component: DirectionalLight,
|
|
501
|
+
data: { direction: [0, 0, -1], color: [1, 1, 1], intensity: 1 },
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
const page = ReadonlyDynamicInputPage.create({
|
|
505
|
+
sourceId: 'water-events',
|
|
506
|
+
pageId: 1,
|
|
507
|
+
schema: dynamicInput,
|
|
508
|
+
}).unwrap();
|
|
509
|
+
const eventLifetimeSeconds = 0.45;
|
|
510
|
+
const events = [
|
|
511
|
+
{ position: [-1.2, 0, 0] as const, time: 0, eventId: 1 },
|
|
512
|
+
{ position: [1.2, 0, 0] as const, time: 0, eventId: 2 },
|
|
513
|
+
] as const;
|
|
514
|
+
let publishedEventCount = 0;
|
|
515
|
+
let projectionRevision = 1;
|
|
516
|
+
let publishedDeviceGeneration = renderer.inspect().frame.deviceGeneration;
|
|
517
|
+
page.reconfigureDevice(publishedDeviceGeneration).unwrap();
|
|
518
|
+
|
|
519
|
+
const reserveFreshRanges = () =>
|
|
520
|
+
waterEntities.map((entityKey, index) =>
|
|
521
|
+
page
|
|
522
|
+
.reserveRange({
|
|
523
|
+
domain: index === 0 ? 'water-left' : 'water-right',
|
|
524
|
+
recordStart: index,
|
|
525
|
+
recordCount: 1,
|
|
526
|
+
instanceIndex: index,
|
|
527
|
+
member: {
|
|
528
|
+
worldIdentity: world.identity,
|
|
529
|
+
entityKey,
|
|
530
|
+
drawItemIndex: 0,
|
|
531
|
+
instanceOrdinal: 0,
|
|
532
|
+
},
|
|
533
|
+
})
|
|
534
|
+
.unwrap(),
|
|
535
|
+
);
|
|
536
|
+
let ranges = reserveFreshRanges();
|
|
537
|
+
let deviceLost = renderer.state() === 'device-lost';
|
|
538
|
+
|
|
539
|
+
const publishDynamicInput = (now: number): void => {
|
|
540
|
+
const activeEvents = events.filter(
|
|
541
|
+
(event) => now >= event.time && now - event.time < eventLifetimeSeconds,
|
|
542
|
+
);
|
|
543
|
+
const recordsToRewrite = Math.max(publishedEventCount, activeEvents.length);
|
|
544
|
+
for (let index = 0; index < recordsToRewrite; index += 1) {
|
|
545
|
+
page
|
|
546
|
+
.writeRecord(
|
|
547
|
+
index,
|
|
548
|
+
activeEvents[index] ?? { position: [0, 0, 0], time: -1_000, eventId: 0 },
|
|
549
|
+
)
|
|
550
|
+
.unwrap();
|
|
551
|
+
}
|
|
552
|
+
publishedEventCount = activeEvents.length;
|
|
553
|
+
renderer.setSurfaceDynamicInput({ page, ranges, projectionRevision, frameTime: now });
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
const recoverAndRepublish = async (): Promise<number> => {
|
|
557
|
+
if (renderer.state() !== 'device-lost') {
|
|
558
|
+
throw new Error('water recovery requires the Renderer device-lost state');
|
|
559
|
+
}
|
|
560
|
+
const recovered = await renderer.recover();
|
|
561
|
+
if (!recovered.ok) throw recovered.error;
|
|
562
|
+
const nextDeviceGeneration = renderer.inspect().frame.deviceGeneration;
|
|
563
|
+
if (nextDeviceGeneration <= publishedDeviceGeneration) {
|
|
564
|
+
throw new Error('water recovery did not publish a new device generation');
|
|
565
|
+
}
|
|
566
|
+
const previousRanges = ranges;
|
|
567
|
+
page.reconfigureDevice(nextDeviceGeneration).unwrap();
|
|
568
|
+
ranges = reserveFreshRanges();
|
|
569
|
+
if (
|
|
570
|
+
ranges.some(
|
|
571
|
+
(range, index) =>
|
|
572
|
+
range === previousRanges[index] ||
|
|
573
|
+
range.deviceGeneration !== nextDeviceGeneration ||
|
|
574
|
+
range.bufferGeneration === previousRanges[index]?.bufferGeneration,
|
|
575
|
+
)
|
|
576
|
+
) {
|
|
577
|
+
throw new Error('water recovery reused a pre-recovery dynamic range');
|
|
578
|
+
}
|
|
579
|
+
publishedDeviceGeneration = nextDeviceGeneration;
|
|
580
|
+
projectionRevision += 1;
|
|
581
|
+
publishDynamicInput(world.getResource(Time).elapsed);
|
|
582
|
+
deviceLost = false;
|
|
583
|
+
return nextDeviceGeneration;
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
const unsubscribe = renderer.subscribe((event) => {
|
|
587
|
+
if (event.kind === 'state-changed' && event.current === 'device-lost') {
|
|
588
|
+
deviceLost = true;
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
try {
|
|
592
|
+
const attachment = renderer.attach(world);
|
|
593
|
+
if (!attachment.ok) throw attachment.error;
|
|
594
|
+
const attached = attachment.value;
|
|
595
|
+
publishDynamicInput(world.getResource(Time).elapsed);
|
|
596
|
+
world.update(1 / 60).unwrap();
|
|
597
|
+
const submitAndObserve = async () => {
|
|
598
|
+
// The Renderer keeps this logical World lease valid across recovery;
|
|
599
|
+
// retry the same lease after the page and ranges are revalidated.
|
|
600
|
+
const submitted = renderer.draw({
|
|
601
|
+
leases: [attached],
|
|
602
|
+
camera: { lease: attached },
|
|
603
|
+
environment: { lease: attached },
|
|
604
|
+
});
|
|
605
|
+
if (!submitted.ok) throw submitted.error;
|
|
606
|
+
const completed = await submitted.value.completed;
|
|
607
|
+
if (!completed.ok) throw completed.error;
|
|
608
|
+
const observed = await renderer.observe(submitted.value, { include: ['draws'] });
|
|
609
|
+
if (!observed.ok) throw observed.error;
|
|
610
|
+
return submitted.value;
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
let submitted: Awaited<ReturnType<typeof submitAndObserve>>;
|
|
614
|
+
try {
|
|
615
|
+
submitted = await submitAndObserve();
|
|
616
|
+
} catch (cause) {
|
|
617
|
+
// A first frame can fail before it yields a receipt. Recover only when
|
|
618
|
+
// the public lifecycle reports the actual device-lost state.
|
|
619
|
+
if (renderer.state() !== 'device-lost') throw cause;
|
|
620
|
+
await recoverAndRepublish();
|
|
621
|
+
submitted = await submitAndObserve();
|
|
622
|
+
}
|
|
623
|
+
if (deviceLost || renderer.state() === 'device-lost') {
|
|
624
|
+
await recoverAndRepublish();
|
|
625
|
+
submitted = await submitAndObserve();
|
|
626
|
+
}
|
|
627
|
+
const inspection = renderer.inspect();
|
|
628
|
+
if (submitted.deviceGeneration !== publishedDeviceGeneration) {
|
|
629
|
+
throw new Error('water draw receipt does not match the published device generation');
|
|
630
|
+
}
|
|
631
|
+
return {
|
|
632
|
+
world,
|
|
633
|
+
attached,
|
|
634
|
+
page,
|
|
635
|
+
receipt: submitted,
|
|
636
|
+
submission: inspection.renderScene.submission,
|
|
637
|
+
deviceGeneration: inspection.frame.deviceGeneration,
|
|
638
|
+
};
|
|
639
|
+
} finally {
|
|
640
|
+
unsubscribe();
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
The repository exercises this route through real Pack/GUID publication and a
|
|
646
|
+
physical texture or compositor readback:
|
|
647
|
+
|
|
648
|
+
```bash
|
|
649
|
+
pnpm exec vitest run --project=dawn \
|
|
650
|
+
packages/runtime/src/__tests__/surface-standard-pipeline.dawn.test.ts --retry=0
|
|
651
|
+
pnpm exec vitest run --config vitest.browser.config.ts --project=browser \
|
|
652
|
+
packages/runtime/src/__tests__/surface-standard-pipeline.browser.test.ts --retry=0
|
|
653
|
+
|
|
654
|
+
# Same material, scene, time, events, ProbeBlend domains, exposure, and ROI;
|
|
655
|
+
# cold direct -> GPU -> direct. The original 0.05 RGB oracle remains active.
|
|
656
|
+
FORGEAX_SURFACE_LANE_PARITY=1 FORGEAX_DAWN_LIGHTWEIGHT=1 \
|
|
657
|
+
pnpm exec vitest run --project=dawn \
|
|
658
|
+
packages/runtime/src/__tests__/surface-standard-pipeline.dawn.test.ts --retry=0
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
Direct medium draws use a dedicated group(3): ordinary instance transforms,
|
|
662
|
+
the retained ProbeBlend record, dynamic page, per-member frame rows, shared
|
|
663
|
+
frame time, and a producer-owned per-draw frame-base uniform. The vertex shader
|
|
664
|
+
addresses `frameBase + instance_index`; neither GUID order nor a scene-index
|
|
665
|
+
visible slot is used to infer that address. Missing member identity, page,
|
|
666
|
+
generation, or frame-base publication fails the draw through the structured
|
|
667
|
+
Render error path.
|
|
668
|
+
|
|
669
|
+
After a successful submission, `renderer.inspect().renderScene.gpuDriven.surfaceArtifact`
|
|
670
|
+
exposes the selected direct/scene-index entries and artifact/resource
|
|
671
|
+
generation. `renderer.inspect().renderScene.submission` separates
|
|
672
|
+
`requestedLane` from the command-derived `actualLane` and publishes only
|
|
673
|
+
nearest/color passes that encoded a real `draw*` or `draw*Indirect` command.
|
|
674
|
+
When the current Standard frame carries the reflection fallback MRT, the same
|
|
675
|
+
submission projection reports `actualLane: 'direct'` with
|
|
676
|
+
`actualLaneReason: 'reflection-fallback-mrt'`. This is a pass-ownership
|
|
677
|
+
constraint, not a device-capability result; the reason is absent again when a
|
|
678
|
+
later frame resumes the ordinary GPU-driven lane.
|
|
679
|
+
Direct rows contain the actual command range (`firstInstance` remains `0`),
|
|
680
|
+
the producer's separate `surfaceFrameBase`, and the member identities bound by
|
|
681
|
+
that command. Every command reports the exact selected artifact's
|
|
682
|
+
`receiptIdentity`/`receiptGeneration`; `programEvidence: 'missing'` means the
|
|
683
|
+
recorder could not prove that producer identity. Indirect rows contain the
|
|
684
|
+
actual indirect buffer identity and byte offset. `status: 'completed'` is published only after the existing queue
|
|
685
|
+
completion fence resolves for the same device generation.
|
|
686
|
+
|
|
687
|
+
GPU member identity is opt-in because it requires readback. Call
|
|
688
|
+
`await renderer.observe(frameReceipt, { include: ['draws'] })` before reading
|
|
689
|
+
the inspection row. Until that call, an indirect pass reports
|
|
690
|
+
`memberEvidence: 'indirect-readback-required'` and omits members. The observe
|
|
691
|
+
path reuses the selector telemetry copy, reads its actual visible rows, and
|
|
692
|
+
then reports `indirect-visible-readback`. The readback carries its recording
|
|
693
|
+
sequence, frame/device/resource/view identity, and per-pass indirect ranges;
|
|
694
|
+
an older or reordered shared-buffer result is rejected even within one device
|
|
695
|
+
generation. Nearest and color share the same
|
|
696
|
+
visible member set because both consume the same submitted selector/indirect
|
|
697
|
+
projection; each pass still has its own encoded command receipt. CPU candidate
|
|
698
|
+
lists are never substituted for this GPU readback. The lane-parity gates pair
|
|
699
|
+
these command facts with completed fixed-ROI pixels at the unchanged `0.05`
|
|
700
|
+
RGB epsilon.
|
|
701
|
+
|
|
702
|
+
The public consumer sequence is `draw` -> `observe` -> `inspect`:
|
|
703
|
+
|
|
704
|
+
```ts
|
|
705
|
+
import type { SurfaceGpuIndirectParameters } from '@forgeax/engine/render';
|
|
706
|
+
|
|
707
|
+
const receipt = renderer.draw(frameInput);
|
|
708
|
+
if (!receipt.ok) throw receipt.error;
|
|
709
|
+
const observed = await renderer.observe(receipt.value, { include: ['draws'] });
|
|
710
|
+
if (!observed.ok) throw observed.error;
|
|
711
|
+
const inspection = renderer.inspect();
|
|
712
|
+
const completePasses = (inspection.renderScene.submission?.passes ?? []).filter(
|
|
713
|
+
(pass) => pass.memberEvidence === 'indirect-visible-readback',
|
|
714
|
+
);
|
|
715
|
+
const indirectParameters: readonly SurfaceGpuIndirectParameters[] = completePasses.flatMap(
|
|
716
|
+
(pass) => {
|
|
717
|
+
if (pass.indirectParameters === undefined) throw new Error('complete GPU readback is missing');
|
|
718
|
+
return pass.indirectParameters;
|
|
719
|
+
},
|
|
720
|
+
);
|
|
721
|
+
void indirectParameters;
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
`pass.indirectParameters` appears only after the requested observation resolves
|
|
725
|
+
and the renderer accepts a complete matching GPU readback. A pass before
|
|
726
|
+
readback, with no complete identity match, reports
|
|
727
|
+
`memberEvidence: 'indirect-readback-required'` and has no parameters. A valid
|
|
728
|
+
readback whose bounded command sample is truncated reports
|
|
729
|
+
`memberEvidence: 'indirect-visible-readback-truncated'` and also has no
|
|
730
|
+
parameters; its saved and dropped counts remain diagnostic facts. GPU-byte
|
|
731
|
+
truncation, a stale/reordered copy, or an identity mismatch is rejected and
|
|
732
|
+
leaves the pass in `indirect-readback-required`. Only a complete valid copy
|
|
733
|
+
reports `memberEvidence: 'indirect-visible-readback'` with parameters. These
|
|
734
|
+
states are diagnostic facts and never proof when the required evidence is
|
|
735
|
+
absent. Each record is bound to the same
|
|
736
|
+
`sequence`, `frameId`, `deviceGeneration`, `resourceGeneration`, `viewIdentity`,
|
|
737
|
+
and Surface `pass` as the published submission, plus the exact indirect
|
|
738
|
+
`indirectBufferIdentity` and `indirectOffset`. For indexed commands, `count`,
|
|
739
|
+
`first`, `instanceCount`, signed `baseVertex`, and `firstInstance` decode the
|
|
740
|
+
five WebGPU indirect words. For non-indexed commands, `count`, `first`,
|
|
741
|
+
`instanceCount`, and `firstInstance` decode the four words and `baseVertex` is
|
|
742
|
+
zero by definition.
|
|
743
|
+
|
|
744
|
+
Each pass exposes `totalCommandCount`, bounded `savedCommandCount`,
|
|
745
|
+
`droppedCommandCount`, and `truncated`. A truncated indirect row is diagnostic
|
|
746
|
+
evidence, never a complete member set; a valid readback labels it
|
|
747
|
+
`indirect-visible-readback-truncated` and omits `indirectParameters`. A direct
|
|
748
|
+
row uses `direct-command-members-truncated` for the same bounded-sample limit.
|
|
749
|
+
|
|
750
|
+
The Render owner validates the cooked layout, source/page identity, generation,
|
|
751
|
+
range bounds, and device storage limit. It begins a detached upload and commits
|
|
752
|
+
the page revision only after every queue write succeeds; graph submission then
|
|
753
|
+
consumes the validated ranges. The same input is carried through GPU
|
|
754
|
+
preparation and the recovery candidate. In the scene-index lane, the renderer
|
|
755
|
+
maps each GPU-written visible member's stable candidate row to a frame row and
|
|
756
|
+
keeps each producer `instanceIndex` in the row; the atomic compact draw slot is
|
|
757
|
+
never used as a dynamic-input address.
|
|
758
|
+
|
|
759
|
+
On a device with compute, storage-buffer, and indirect-drawing capabilities,
|
|
760
|
+
GPU-driven preparation records the published Surface submission and binds the
|
|
761
|
+
Surface page and frame records through the existing GPU-driven instance owner.
|
|
762
|
+
The Standard Forward and Deferred graph owners allocate the two
|
|
763
|
+
`nearest-layer`/`color` targets and share the transmission backdrop producer.
|
|
764
|
+
The backdrop is the paired opaque color input. At 1x, both Standard paths copy
|
|
765
|
+
the completed depth into an `r32float` sampled target. At 4x MSAA, Render does
|
|
766
|
+
not use the hardware-averaged color resolve as the medium backdrop. A typed
|
|
767
|
+
fullscreen producer examines all four depth samples, selects the nearest
|
|
768
|
+
sample, and publishes both that depth and the color at the same sample index.
|
|
769
|
+
The nearest-water pass uses 4x color and depth attachments and the identical
|
|
770
|
+
paired rule before the color pass consumes it. Both resolved depth facts are
|
|
771
|
+
`r32float` with an unfilterable-float/non-filtering binding, so the color pass
|
|
772
|
+
never samples a depth attachment while writing scene depth. Inputs other than
|
|
773
|
+
the admitted 1x or paired 4x forms fail with `resource-descriptor-invalid`;
|
|
774
|
+
`addSingleLayerMediumPasses` never creates a synthetic depth value. This policy
|
|
775
|
+
is a nearest-sample coverage resolve, not an average of already-resolved color.
|
|
776
|
+
The color pass binds the copied depth, the preceding nearest-layer color/depth
|
|
777
|
+
pair, and the shared backdrop through the existing material bind-group owner.
|
|
778
|
+
GPU-driven recording reuses the existing Standard raster
|
|
779
|
+
dispatch with a Surface-family filter; devices without the required
|
|
780
|
+
capability use the existing CPU record lane and its structured admission
|
|
781
|
+
reason. These tests prove the owner boundary and fail-closed behavior;
|
|
782
|
+
Browser, Dawn, and physical GPU frame evidence remain separate gates. A
|
|
783
|
+
missing Surface ABI, pass, resource, or prepared generation is a
|
|
784
|
+
producer/preparation error. The public paired math and two example Surface
|
|
785
|
+
modules live in
|
|
786
|
+
[`src/examples/single-layer-medium-surface.ts`](src/examples/single-layer-medium-surface.ts).
|
|
787
|
+
|
|
111
788
|
The public route is one `RenderFeature<FrameData>` through the Standard
|
|
112
789
|
Pipeline and the active RenderGraph pass. In the examples below, `type FrameData`
|
|
113
790
|
is the producer-owned extracted value. A feature extracts one frame value,
|
|
@@ -125,6 +802,14 @@ that was still pending. This is a receipt of admitted commands, not a GPU
|
|
|
125
802
|
readback or proof that an indirect draw produced non-zero instances; producers
|
|
126
803
|
that need those facts must use their existing readback or tape evidence.
|
|
127
804
|
|
|
805
|
+
`BarrelDistortion` is supplied by the ordinary ordered RenderFeature host. A
|
|
806
|
+
missing or zero-strength component produces no allocation, upload, or pass;
|
|
807
|
+
the feature still declares its shader for production validation. A positive
|
|
808
|
+
component adds one fullscreen pass after linear-LDR LUT output and before FXAA
|
|
809
|
+
and final encoding. There is no separate built-in registration path, so feature
|
|
810
|
+
ordering and retirement use the same graph and in-flight lifetime rules as
|
|
811
|
+
other post-process features.
|
|
812
|
+
|
|
128
813
|
## RenderFeature: the producer seam (first-read index)
|
|
129
814
|
|
|
130
815
|
The public route is one `RenderFeature<FrameData>` through the active RenderGraph
|
|
@@ -269,6 +954,16 @@ Its color and intensity remain the lighting controls; no Skylight means no
|
|
|
269
954
|
global ambient contribution. An explicit equirect keeps its image source.
|
|
270
955
|
Use a neutral Skylight tint when comparing local captures against the sky.
|
|
271
956
|
|
|
957
|
+
The cube uses one bounded analytic Rayleigh/Mie daylight evaluator. Rayleigh
|
|
958
|
+
and Mie controls affect their spectral scattering and extinction, while solar
|
|
959
|
+
radiance scales linearly with the selected light. A 1.5 exponent shapes the
|
|
960
|
+
Rayleigh color response before solar scaling; the Mie lobe remains additive.
|
|
961
|
+
A smooth effective air-mass
|
|
962
|
+
bound (four zenith columns) avoids a saturated neutral horizon. This is an
|
|
963
|
+
explicit game-oriented clear-sky approximation, not the retired Perez fit or a
|
|
964
|
+
full spherical multiple-scattering solution. The existing cache owns all work;
|
|
965
|
+
background pixels still sample the cube, and the separate sun disc is unchanged.
|
|
966
|
+
|
|
272
967
|
### Atmosphere controls and fixed-exposure comparisons
|
|
273
968
|
|
|
274
969
|
`Atmosphere` keeps the visible disc and the analytic sky response as separate
|
|
@@ -339,8 +1034,6 @@ import { createRenderer } from '@forgeax/engine-runtime';
|
|
|
339
1034
|
const created = await createRenderer(canvas, { gpuPassTiming: {} });
|
|
340
1035
|
if (!created.ok) throw created.error;
|
|
341
1036
|
const renderer = created.value;
|
|
342
|
-
const ready = await renderer.initialization;
|
|
343
|
-
if (!ready.ok) throw ready.error;
|
|
344
1037
|
const attached = renderer.attach(world);
|
|
345
1038
|
if (!attached.ok) throw attached.error;
|
|
346
1039
|
const drawn = renderer.draw({
|
|
@@ -536,8 +1229,6 @@ import type { Renderer } from '@forgeax/engine-render';
|
|
|
536
1229
|
// After the existing createRenderer(canvas) happy path returns a Renderer,
|
|
537
1230
|
// call `await recoverAndDraw(renderer, world)` with the World above.
|
|
538
1231
|
async function recoverAndDraw(renderer: Renderer, world: World): Promise<void> {
|
|
539
|
-
const ready = await renderer.initialization;
|
|
540
|
-
if (!ready.ok) throw ready.error;
|
|
541
1232
|
const attached = renderer.attach(world);
|
|
542
1233
|
if (!attached.ok) throw attached.error;
|
|
543
1234
|
const lease = attached.value;
|
|
@@ -573,8 +1264,157 @@ browser check; homogeneous transmittance alone cannot detect light-band artifact
|
|
|
573
1264
|
History is clipped to the current
|
|
574
1265
|
neighborhood and its weight decays with elapsed World time (at most 0.875 per
|
|
575
1266
|
frame, about 87 ms half-life at 60 Hz or slower), limiting trails from animated
|
|
576
|
-
density.
|
|
577
|
-
not
|
|
1267
|
+
density. `CloudLayer` is the renderer-owned procedural cloud route described
|
|
1268
|
+
below; it does not change the volumetric fog component or add a second clock.
|
|
1269
|
+
|
|
1270
|
+
### CloudLayer authoring and derived transport
|
|
1271
|
+
|
|
1272
|
+
`CloudLayer` is a single World-authored component. Its schema keeps the seed,
|
|
1273
|
+
layer bounds, noise scale, coverage, density, wind and quality as serializable
|
|
1274
|
+
source facts. `renderComponentsPlugin()` registers it with the normal ECS
|
|
1275
|
+
component lease. Cache bytes, light-space shadow projections, per-view history
|
|
1276
|
+
and GPU handles remain Render-derived state and can be reconstructed from the
|
|
1277
|
+
validated source key.
|
|
1278
|
+
|
|
1279
|
+
Coverage is monotonic: zero clears the layer and increasing it admits more of
|
|
1280
|
+
its weather-shaped body. Detail erodes this body after altitude shaping; it
|
|
1281
|
+
cannot create detached density outside the body. Formation cache version 5
|
|
1282
|
+
uses the same integer hash and density composition as both analytic WGSL
|
|
1283
|
+
paths, with center-aligned periodic interpolation. Layer thickness controls
|
|
1284
|
+
the physical height of two vertical body cells; scale controls four horizontal
|
|
1285
|
+
cells per world-space period. Choose comparable body dimensions for cumulus
|
|
1286
|
+
rather than compressing a broad cloud into a thin layer. The broad field owns
|
|
1287
|
+
85% of body shaping; attenuated high-frequency octaves and bounded subtractive
|
|
1288
|
+
erosion preserve connected masses instead of dense detached fragments.
|
|
1289
|
+
|
|
1290
|
+
Install the producer through the existing RenderFeature seam when a scene uses
|
|
1291
|
+
clouds:
|
|
1292
|
+
|
|
1293
|
+
```ts
|
|
1294
|
+
import { World } from '@forgeax/engine-ecs';
|
|
1295
|
+
import { createRenderer } from '@forgeax/engine-runtime';
|
|
1296
|
+
import {
|
|
1297
|
+
Camera,
|
|
1298
|
+
CloudLayer,
|
|
1299
|
+
CloudQualityValue,
|
|
1300
|
+
DirectionalLight,
|
|
1301
|
+
createCloudLayerFeature,
|
|
1302
|
+
perspective,
|
|
1303
|
+
} from '@forgeax/engine-render';
|
|
1304
|
+
import { Transform } from '@forgeax/engine-scene';
|
|
1305
|
+
|
|
1306
|
+
const canvas = document.querySelector('canvas');
|
|
1307
|
+
if (!(canvas instanceof HTMLCanvasElement)) throw new Error('canvas is required');
|
|
1308
|
+
const world = new World();
|
|
1309
|
+
const camera = world.spawn(
|
|
1310
|
+
{ component: Transform, data: { pos: [0, 140, 240] } },
|
|
1311
|
+
{ component: Camera, data: { ...perspective({ fov: Math.PI / 4, aspect: 16 / 9 }) } },
|
|
1312
|
+
).unwrap();
|
|
1313
|
+
const sun = world.spawn({
|
|
1314
|
+
component: DirectionalLight,
|
|
1315
|
+
data: { direction: [-0.4, -0.8, -0.3], color: [1, 0.95, 0.9], intensity: 2 },
|
|
1316
|
+
}).unwrap();
|
|
1317
|
+
const cloud = world.spawn({
|
|
1318
|
+
component: CloudLayer,
|
|
1319
|
+
data: {
|
|
1320
|
+
seed: 1337,
|
|
1321
|
+
baseHeight: 120,
|
|
1322
|
+
thickness: 80,
|
|
1323
|
+
scale: 0.004,
|
|
1324
|
+
coverage: 0.48,
|
|
1325
|
+
density: 1,
|
|
1326
|
+
wind: [8, 0, 2],
|
|
1327
|
+
quality: CloudQualityValue.medium,
|
|
1328
|
+
shadowRange: 512,
|
|
1329
|
+
},
|
|
1330
|
+
}).unwrap();
|
|
1331
|
+
const created = await createRenderer(canvas, { features: [createCloudLayerFeature()] });
|
|
1332
|
+
if (!created.ok) throw created.error;
|
|
1333
|
+
const renderer = created.value;
|
|
1334
|
+
const attached = renderer.attach(world);
|
|
1335
|
+
if (!attached.ok) throw attached.error;
|
|
1336
|
+
world.update(1 / 60).unwrap();
|
|
1337
|
+
const drawn = renderer.draw({
|
|
1338
|
+
leases: [attached.value],
|
|
1339
|
+
camera: { lease: attached.value },
|
|
1340
|
+
environment: { lease: attached.value },
|
|
1341
|
+
});
|
|
1342
|
+
if (!drawn.ok) throw drawn.error;
|
|
1343
|
+
console.log(camera, sun, cloud, renderer.inspect().cloudLayer);
|
|
1344
|
+
|
|
1345
|
+
world.set(cloud, CloudLayer, { wind: [10, 0, 2] }).unwrap();
|
|
1346
|
+
const changed = renderer.draw({
|
|
1347
|
+
leases: [attached.value],
|
|
1348
|
+
camera: { lease: attached.value },
|
|
1349
|
+
environment: { lease: attached.value },
|
|
1350
|
+
});
|
|
1351
|
+
if (!changed.ok) {
|
|
1352
|
+
console.error(renderer.state(), renderer.inspect().cloudLayer, changed.error);
|
|
1353
|
+
throw changed.error;
|
|
1354
|
+
}
|
|
1355
|
+
```
|
|
1356
|
+
|
|
1357
|
+
If the capability report does not admit the cloud lane, disable that feature at
|
|
1358
|
+
assembly time and keep the rest of the renderer running:
|
|
1359
|
+
|
|
1360
|
+
```ts
|
|
1361
|
+
const created = await createRenderer(canvas, {
|
|
1362
|
+
features: [createCloudLayerFeature({ enabled: false })],
|
|
1363
|
+
});
|
|
1364
|
+
```
|
|
1365
|
+
|
|
1366
|
+
The runtime `createRenderer()` result is returned after the renderer's internal
|
|
1367
|
+
initialization barrier has completed, so the public `Renderer` can attach and
|
|
1368
|
+
draw immediately after the `Result` check above. The `initialization` Promise
|
|
1369
|
+
belongs to the lower-level internal construction seam.
|
|
1370
|
+
|
|
1371
|
+
The feature extracts the authored component, World `Time`, and the selected
|
|
1372
|
+
directional sun through the normal frame owner. It declares a bounded packed
|
|
1373
|
+
`u32` 3D formation cache with three contiguous R8 planes (`weather`, `body`,
|
|
1374
|
+
`erosion`; `ceil(3 * R8 texels / 4)` elements and `elements * 4` storage
|
|
1375
|
+
bytes), then composites scene-linear HDR color with inverse-view-projection
|
|
1376
|
+
rays, scene depth termination, bounded Beer extinction, solar-column
|
|
1377
|
+
transmittance, and cloud-interior phase scattering. The same formation and
|
|
1378
|
+
coverage contract is used by the CPU reference and the production WGSL path;
|
|
1379
|
+
coverage, density and wind are evaluation inputs and do not rebuild the
|
|
1380
|
+
reusable planes. The erosion plane repeats three times across the body period,
|
|
1381
|
+
adding smaller boundary structure without increasing cache bytes or density
|
|
1382
|
+
reads. Direct and approximate multiple scattering use the same light/phase unit
|
|
1383
|
+
convention as the volume path and CPU optical reference; view and solar
|
|
1384
|
+
sample limits are unchanged. The exported CPU helpers (`buildCloudDensityCache`,
|
|
1385
|
+
`integrateCloudCameraPath`,
|
|
1386
|
+
`integrateCloudSolarColumn`, `integrateCloudInterior`, and
|
|
1387
|
+
`sampleCloudShadow`) remain deterministic reconstruction and recovery tools.
|
|
1388
|
+
|
|
1389
|
+
The cloud shadow target is a square, quality-derived `rgba16float` map whose
|
|
1390
|
+
world-space projection is texel-snapped around the selected sun anchor. The
|
|
1391
|
+
cloud view, shadow and interior consumers share that projection and attenuate
|
|
1392
|
+
direct solar transport once. A cloud-enabled view opts into six additional
|
|
1393
|
+
`rgba16float` temporal surfaces: radiance, transmittance and representative
|
|
1394
|
+
cloud depth, each with current/previous ping-pong slots at `ceil(surface / 2)`
|
|
1395
|
+
resolution. One half-resolution transport raster writes the three surfaces as
|
|
1396
|
+
an MRT from the same camera integral. A full-resolution resolve upsamples those
|
|
1397
|
+
fields, rejects history by world depth, clamps radiance and transmittance to a
|
|
1398
|
+
four-neighbour history envelope, and performs the one HDR composite. The
|
|
1399
|
+
transport samples the world-space cloud-shadow cache for remaining solar
|
|
1400
|
+
optical depth; invalid projection or out-of-range samples use the bounded
|
|
1401
|
+
analytic column fallback. There is no second full-resolution raymarch solely
|
|
1402
|
+
for cloud depth.
|
|
1403
|
+
|
|
1404
|
+
Temporal slots are created only when a view demands them; submit advances the
|
|
1405
|
+
transaction from the resolve pass, while abort, resize, recovery and unload
|
|
1406
|
+
retire the old generation through the normal queue-fence owner.
|
|
1407
|
+
|
|
1408
|
+
Transparent geometry remains depth-read-only unless its authored render state
|
|
1409
|
+
explicitly enables `depthWriteEnabled`; this keeps ordinary alpha smoke from
|
|
1410
|
+
truncating later volume/cloud segments while allowing an intentional cutout or
|
|
1411
|
+
water surface to publish its own depth. Use `inspectCloudLayer()` and the
|
|
1412
|
+
renderer's normal feature inspection to read capability, generation,
|
|
1413
|
+
temporal-reset and resource facts. Inspection distinguishes CPU cache bytes,
|
|
1414
|
+
declared GPU target bytes, and measured GPU bytes; physical adapter timing and
|
|
1415
|
+
residency are reported as unavailable until a prepared adapter submits those
|
|
1416
|
+
receipts. Unavailable compute or storage support is data rather than a hidden
|
|
1417
|
+
fallback texture.
|
|
578
1418
|
|
|
579
1419
|
Point shadows use one renderer-owned cube-array `ShadowAtlas`. The public
|
|
580
1420
|
`SHADOW_ATLAS_DEFAULT_FACE_SIZE` and `SHADOW_ATLAS_DEFAULT_LAYERS` constants
|
|
@@ -612,8 +1452,6 @@ rebuild or cold-cook its source, then retry the same request.
|
|
|
612
1452
|
const created = await createRenderer(canvas);
|
|
613
1453
|
if (!created.ok) throw created.error;
|
|
614
1454
|
const renderer = created.value;
|
|
615
|
-
const ready = await renderer.initialization;
|
|
616
|
-
if (!ready.ok) throw ready.error;
|
|
617
1455
|
const attached = renderer.attach(world);
|
|
618
1456
|
if (!attached.ok) throw attached.error;
|
|
619
1457
|
const frame = renderer.draw({
|
|
@@ -727,7 +1565,7 @@ switch (spatial.status) {
|
|
|
727
1565
|
`history.state`, `history.bytes`, `history.resetCount`, `passRoster`, and
|
|
728
1566
|
`fallbackSource` are bounded POD facts. They are evidence about the current
|
|
729
1567
|
owner state, not an acceptance claim: M1/M2 remain an implementation checkpoint
|
|
730
|
-
until the paired Browser/Dawn
|
|
1568
|
+
until the paired Browser/Dawn 60-frame carrier, readback, falsifiers, and
|
|
731
1569
|
performance gates are present.
|
|
732
1570
|
|
|
733
1571
|
SSR temporal feedback and presentation use distinct coordinates without another
|
|
@@ -980,7 +1818,7 @@ that a physical Browser/Dawn workload or timing gate completed.
|
|
|
980
1818
|
`renderer.inspect()` and `renderer.observe()` never carry ROI pixels, scanlines,
|
|
981
1819
|
brightness or level metrics. The hello-fxaa `hello-fxaa/dark-gradient/v1`
|
|
982
1820
|
fixture report owns those fields and records the fixed camera, low-light scene,
|
|
983
|
-
800x600 resolution,
|
|
1821
|
+
800x600 resolution, 60-frame schedule, ROI, scanline, backend/lane, surface
|
|
984
1822
|
formats, pixel hash, validation errors and mutation falsifiers.
|
|
985
1823
|
|
|
986
1824
|
The report gates are explicit: unique-color ratio $r_U \ge 0.75$, level ratio
|
|
@@ -1407,115 +2245,75 @@ infer state from URLs, or repair a producer failure. Aggregate counts are
|
|
|
1407
2245
|
derived from the observations, so an AI can inspect first, repair or recook the
|
|
1408
2246
|
producer, and retry without guessing at hidden renderer state.
|
|
1409
2247
|
|
|
1410
|
-
###
|
|
2248
|
+
### World-owned Instances and CPU bounds
|
|
1411
2249
|
|
|
1412
|
-
`Instances`
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
revision/capacity/dirty ranges, and returns detached inspection snapshots.
|
|
2250
|
+
`Instances.transforms` holds packed column-major mat4 values in World-managed
|
|
2251
|
+
storage. Author the same field in keyed Scene assets, glTF imports, or ECS;
|
|
2252
|
+
neither creation nor saving requires a Renderer.
|
|
1416
2253
|
|
|
1417
2254
|
```ts
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
matrices[offset + 10] = 1;
|
|
1426
|
-
matrices[offset + 15] = 1;
|
|
1427
|
-
matrices[offset + 12] = (index % 100) * 2;
|
|
1428
|
-
matrices[offset + 14] = Math.floor(index / 100) * 2;
|
|
1429
|
-
}
|
|
1430
|
-
return matrices;
|
|
2255
|
+
const transforms = new Float32Array(20_000 * 16);
|
|
2256
|
+
for (let index = 0; index < 20_000; index++) {
|
|
2257
|
+
const offset = index * 16;
|
|
2258
|
+
transforms[offset] = transforms[offset + 5] =
|
|
2259
|
+
transforms[offset + 10] = transforms[offset + 15] = 1;
|
|
2260
|
+
transforms[offset + 12] = (index % 100) * 2;
|
|
2261
|
+
transforms[offset + 14] = Math.floor(index / 100) * 2;
|
|
1431
2262
|
}
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
transforms: identityMatrices(instanceCount),
|
|
1435
|
-
}).unwrap();
|
|
1436
|
-
world.spawn(
|
|
2263
|
+
const entity = world.spawn(
|
|
2264
|
+
{ component: Transform, data: {} },
|
|
1437
2265
|
{ component: MeshFilter, data: { assetHandle: cube } },
|
|
1438
2266
|
{ component: MeshRenderer, data: {} },
|
|
1439
|
-
{ component: Instances, data: {
|
|
1440
|
-
);
|
|
1441
|
-
|
|
1442
|
-
const patch = identityMatrices(1);
|
|
1443
|
-
patch[12] = 4;
|
|
1444
|
-
renderer.instances.update(collection.collectionId, {
|
|
1445
|
-
start: 0,
|
|
1446
|
-
transforms: patch,
|
|
1447
|
-
});
|
|
2267
|
+
{ component: Instances, data: { transforms } },
|
|
2268
|
+
).unwrap();
|
|
1448
2269
|
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
// omitted tokens intentionally seed replacement rows.
|
|
1452
|
-
renderer.instances.replace(collection.collectionId, identityMatrices(instanceCount + 1));
|
|
1453
|
-
const current = renderer.instances.inspect(collection.collectionId).unwrap();
|
|
1454
|
-
const detached = renderer.instances.snapshot(collection.collectionId).unwrap();
|
|
1455
|
-
// A read-only observation does not advance the renderer upload cursor.
|
|
1456
|
-
const observed = renderer.instances.peek(collection.collectionId).unwrap();
|
|
1457
|
-
renderer.instances.release(collection.collectionId);
|
|
2270
|
+
transforms[12] = 4;
|
|
2271
|
+
world.set(entity, Instances, { transforms }).unwrap();
|
|
1458
2272
|
```
|
|
1459
2273
|
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
2274
|
+
World copies external input and publishes changes through its normal component
|
|
2275
|
+
write path. Mutating the input array after spawn/set does not edit the World.
|
|
2276
|
+
Use `world.set` or a declared writable query; do not mutate inspection snapshots.
|
|
2277
|
+
|
|
2278
|
+
| Boundary | Owner and invariant |
|
|
2279
|
+
|:--|:--|
|
|
2280
|
+
| Authoring and Scene save | World/Scene retains matrices, not renderer-local IDs. Saving and reopening preserves the layout. |
|
|
2281
|
+
| Renderer projection | Current World change evidence drives the existing persistent RenderScene. Independent Renderers accept independently; stable matrices reuse the detached snapshot and revision. |
|
|
2282
|
+
| CPU bounds and drawing | Mesh bounds, holder world transform and instance-local matrices feed one projection shared by direct, GPU-driven and capability fallback lanes. |
|
|
2283
|
+
| Temporal identity | Ordinals retain identity across pose edits. Count changes seed new identities; previous matrices come from the last successful submission. |
|
|
2284
|
+
| GPU lifetime | Record owns buffer capacity, upload revisions, internal chunks and device generation. No game-side collection manager is required. |
|
|
2285
|
+
|
|
2286
|
+
Render derives CPU union bounds from the mesh AABB and instance-local matrices,
|
|
2287
|
+
then applies the holder's world transform. Bounds are not a second authored
|
|
2288
|
+
field. A missing or invalid mesh AABB yields a conservative no-cull result;
|
|
2289
|
+
repair the mesh producer and reload the same asset instead of inventing a
|
|
2290
|
+
game-side bounds override.
|
|
2291
|
+
|
|
2292
|
+
An empty array means zero instances. Invalid stride reports
|
|
2293
|
+
`instance-transforms-stride-mismatch`; non-finite values report
|
|
2294
|
+
`instance-transforms-invalid` before rendering that entity. Repair the World or
|
|
2295
|
+
source Scene data and retry. All-zero matrices are finite but have zero
|
|
2296
|
+
homogeneous `w`; initialize valid transforms for visible geometry.
|
|
1480
2297
|
|
|
1481
2298
|
#### Instance inspection and recovery
|
|
1482
2299
|
|
|
1483
|
-
`renderer.inspect().instanceCollections`
|
|
1484
|
-
|
|
2300
|
+
`renderer.inspect().instanceCollections` reports detached runtime evidence.
|
|
2301
|
+
Its `collectionId` is a private projection identity, not a Scene field or an
|
|
2302
|
+
authoring handle. There is no public `renderer.instances` mutation API.
|
|
1485
2303
|
|
|
1486
2304
|
| Field | Meaning |
|
|
1487
2305
|
|:--|:--|
|
|
1488
|
-
| `collectionId`, `count`, `
|
|
1489
|
-
| `residentGeneration`, `lane` | Device generation and
|
|
1490
|
-
| `
|
|
1491
|
-
| `
|
|
1492
|
-
| `
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
```ts
|
|
1500
|
-
const result = renderer.instances.create({ transforms: new Float32Array(17) });
|
|
1501
|
-
if (!result.ok) {
|
|
1502
|
-
const { code, expected, hint, detail, facts } = result.error;
|
|
1503
|
-
// facts expose requestedBytes, supportedBytes, backend, owner, cause, and
|
|
1504
|
-
// recovery; detail retains operation metadata. Repair the payload and retry.
|
|
1505
|
-
void code;
|
|
1506
|
-
void expected;
|
|
1507
|
-
void hint;
|
|
1508
|
-
void detail;
|
|
1509
|
-
void facts;
|
|
1510
|
-
}
|
|
1511
|
-
```
|
|
1512
|
-
|
|
1513
|
-
After a `device-lost` transition, call `await renderer.recover()` and retry the
|
|
1514
|
-
same draw request. The renderer drops generation-owned residents, keeps the
|
|
1515
|
-
canonical collection, and uploads every matrix into the new resident before
|
|
1516
|
-
recording; it does not reuse an old partial dirty range for uninitialized GPU
|
|
1517
|
-
memory. If inspection reports `unavailable`, follow its `error.detail.recovery`
|
|
1518
|
-
and repair the named capability/producer before retrying.
|
|
2306
|
+
| `collectionId`, `count`, `revision` | Renderer-local projection identity, matrix count and accepted content revision. |
|
|
2307
|
+
| `residentGeneration`, `lane` | Device generation and direct/chunked storage/uniform residency, or unresident/unavailable state. |
|
|
2308
|
+
| `uploadRanges`, `uploadedBytes` | Upload work observed in the latest rendered frame; stable frames report zero. |
|
|
2309
|
+
| `requestedBytes`, `supportedBytes`, `backend`, `owner` | Admission facts. The diagnostic owner label `renderer.instances` names internal residency, not a public API. |
|
|
2310
|
+
| `error` | Structured record failure with `code`, `expected`, `hint` and `detail`. |
|
|
2311
|
+
|
|
2312
|
+
After `device-lost`, call `await renderer.recover()` and retry the same draw.
|
|
2313
|
+
Recovery prepares new device resources from the retained CPU projection, without
|
|
2314
|
+
rescanning World or reusing old GPU handles. Failed candidates release their
|
|
2315
|
+
instance buffers. Writes made while the device is lost remain World-owned and
|
|
2316
|
+
are observed on the next draw. Disposing the Renderer never deletes author data.
|
|
1519
2317
|
|
|
1520
2318
|
The large-instance smoke exercises the real Dawn path for all admitted sizes:
|
|
1521
2319
|
|
|
@@ -1533,7 +2331,7 @@ transport through the same production fixture and parameterizes the same three
|
|
|
1533
2331
|
populations:
|
|
1534
2332
|
|
|
1535
2333
|
```bash
|
|
1536
|
-
pnpm exec vitest run --config vitest.browser.config.ts --project=browser \
|
|
2334
|
+
pnpm exec vitest run --config config/vitest.browser.config.ts --project=browser \
|
|
1537
2335
|
apps/parity/instancing-static/src/__tests__/instances.browser.test.ts
|
|
1538
2336
|
```
|
|
1539
2337
|
|
|
@@ -1694,7 +2492,9 @@ same forward-only policy. This section is about the Standard lane, not the
|
|
|
1694
2492
|
separate Points/Lines admission rule above. See the
|
|
1695
2493
|
[`shader Surface contract`](../shader/README.md#standard-surface-contract) and
|
|
1696
2494
|
the [`game-3d` import example](../../templates/game-3d/README.md#import-first-surface-material-example)
|
|
1697
|
-
for the source and Pack route.
|
|
2495
|
+
for the source and Pack route. Water and other participating media use the
|
|
2496
|
+
separate [Single-layer medium Surface](#single-layer-medium-surface) ABI and
|
|
2497
|
+
runtime route.
|
|
1698
2498
|
|
|
1699
2499
|
The compiler and renderer share one pure `StandardLayerPlan`:
|
|
1700
2500
|
`deriveStandardLayerPlan(effectiveParameters)`. It is the only derivation used
|
|
@@ -1779,23 +2579,31 @@ state published by `World.update()`.
|
|
|
1779
2579
|
## Persistent render scene
|
|
1780
2580
|
|
|
1781
2581
|
Every attached World composition bootstraps one renderer-owned CPU projection,
|
|
1782
|
-
then consumes each World's
|
|
2582
|
+
then consumes each World's current membership and component block revisions. One identity-based update
|
|
1783
2583
|
publication merges content, root transforms, and instance changes, including
|
|
1784
2584
|
when all occur in the same frame. Its GPU projection compares affected matrix
|
|
1785
|
-
and metadata rows before uploading them.
|
|
2585
|
+
and metadata rows before uploading them. Shared material rows are packed once per
|
|
2586
|
+
GPU synchronization; Primitive, DrawTemplate, and Material rows upload only when
|
|
2587
|
+
their packed bytes change. Failed uploads retain their dirty rows for retry. An unchanged frame retains its existing
|
|
1786
2588
|
snapshot; unrelated gameplay component writes do not invalidate render state.
|
|
1787
2589
|
There is no exclusive transform/instance scene admission followed by a separate
|
|
1788
2590
|
rebuild implementation. Missing producer evidence causes conservative source
|
|
1789
2591
|
extraction into the same retained projection. World reordering and catalog
|
|
1790
2592
|
reconciliation preserve surviving slots and their submitted temporal history.
|
|
1791
2593
|
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
source
|
|
1798
|
-
|
|
2594
|
+
Shared runtime material parameters and mesh buffers are authored through
|
|
2595
|
+
`RuntimeMaterialValue` and `RuntimeMeshVertices` from `assets-runtime`, using
|
|
2596
|
+
ordinary managed `world.set` writes. The shared handle remains identity-only.
|
|
2597
|
+
Renderer reverse dependencies wake all users of both the old and new handle
|
|
2598
|
+
when content is rebound or removed. Each Renderer independently accepts its
|
|
2599
|
+
source candidate; one consumer cannot drain another's updates. See the
|
|
2600
|
+
[content contract](../assets-runtime/README.md#runtime-content).
|
|
2601
|
+
|
|
2602
|
+
Camera, light, and environment facts refresh independently of geometry.
|
|
2603
|
+
Initial attachment and updates use the same block candidate discovery and direct
|
|
2604
|
+
entity lookup. GPU dirty row addresses use fixed-width radix ordering before
|
|
2605
|
+
adjacent ranges are coalesced. A failed GPU upload retires that resident; source
|
|
2606
|
+
acceptance and submitted temporal history remain separate boundaries.
|
|
1799
2607
|
|
|
1800
2608
|
Instance collection revisions refresh only their consumers. Visibility and
|
|
1801
2609
|
parent changes refresh the affected subtree, and joint changes refresh the
|
|
@@ -1846,6 +2654,13 @@ The shortest public declaration uses the same `Materials.standard` producer as
|
|
|
1846
2654
|
the runtime and imported-skin carriers. `alphaCutoff` is the Alpha Mask
|
|
1847
2655
|
contract; `castShadow` publishes the matching ShadowCaster pass.
|
|
1848
2656
|
|
|
2657
|
+
Frame preparation and device recovery share the material-artifact collector.
|
|
2658
|
+
It indexes ShadowCaster dispatch once by renderable index and material handle;
|
|
2659
|
+
the selected last matching dispatch owns both the program identity and its
|
|
2660
|
+
vertex/fragment entries. Per-draw lookup does not rescan the dispatch roster,
|
|
2661
|
+
and the index is rebuilt for each collection so replacement publications are
|
|
2662
|
+
observed without a persistent cache.
|
|
2663
|
+
|
|
1849
2664
|
```ts
|
|
1850
2665
|
import { Materials } from '@forgeax/engine/render';
|
|
1851
2666
|
|
|
@@ -2247,6 +3062,10 @@ consume the structured `error.detail` context.
|
|
|
2247
3062
|
Use `renderer.inspect()` for a read-only snapshot. Call `renderer.dispose()`
|
|
2248
3063
|
once or repeatedly; disposal is idempotent. Feature plans are assembled once
|
|
2249
3064
|
and the Standard host owns graph replacement and last-known-good recovery.
|
|
3065
|
+
Repeated recoverable preparation errors are reported at most once per 60 frames.
|
|
3066
|
+
A changed stage exception name/message is reported immediately, retaining one
|
|
3067
|
+
report record per feature/error owner. Normal asynchronous resource warm-up
|
|
3068
|
+
remains silent.
|
|
2250
3069
|
|
|
2251
3070
|
For a temporary presentation-owner handoff, call `renderer.releaseSurface()`.
|
|
2252
3071
|
It unconfigures the canvas and makes `draw()` fail closed without disposing the
|
|
@@ -2293,6 +3112,12 @@ consumer that composes sprites with an existing 3D game can call
|
|
|
2293
3112
|
`TransparentSort.configure(world, { mode: TransparentSort.layerY, yzAlpha: 1 })`;
|
|
2294
3113
|
it should not reach into `/internal`.
|
|
2295
3114
|
|
|
3115
|
+
Eligible sprite buckets share one fold instance buffer across the typed transparent
|
|
3116
|
+
geometry pass and the legacy sprite pass. Both consume the same head/skip plan;
|
|
3117
|
+
10,000 equal-depth sprites remain one instanced draw rather than 10,000 submissions.
|
|
3118
|
+
The sprite-atlas Dawn smoke checks this for 60 frames, and its browser probe
|
|
3119
|
+
checks the instance count, typed upload payload and WebGPU validation.
|
|
3120
|
+
|
|
2296
3121
|
The root barrel does not expose frame stores or extract/plan/record owners.
|
|
2297
3122
|
Applications contribute work through `RenderFeature` plans; the Standard
|
|
2298
3123
|
renderer owns graph compilation and submission.
|
|
@@ -2393,7 +3218,7 @@ before promoting that status.
|
|
|
2393
3218
|
|
|
2394
3219
|
The canonical paired carrier is `apps/hello/ssr`. It runs the same fixture at
|
|
2395
3220
|
`http://127.0.0.1:4173/?forgeax-evidence=ssr` and
|
|
2396
|
-
`dawn://hello/ssr?forgeax-evidence=ssr`, records
|
|
3221
|
+
`dawn://hello/ssr?forgeax-evidence=ssr`, records 60-frame identity-bound
|
|
2397
3222
|
readbacks, and keeps visual rows in the form `observed` / `verdict` /
|
|
2398
3223
|
`confidence`. Its performance lane derives the 1920x1080 descriptor and checks
|
|
2399
3224
|
it against `estimateSsrSpatialMemory`; timestamp or paired-lane absence remains
|
|
@@ -2653,16 +3478,19 @@ existing observable state; resource disposal is distinct from JavaScript GC.
|
|
|
2653
3478
|
|
|
2654
3479
|
### Volumetric density coordinates
|
|
2655
3480
|
|
|
2656
|
-
`VolumetricFog.density` is a producer-owned linear 3D source tile.
|
|
2657
|
-
|
|
3481
|
+
`VolumetricFog.density` is a producer-owned linear 3D source tile. In the default
|
|
3482
|
+
`noise` mode the integrator samples repeated world-space coordinates, combines the
|
|
2658
3483
|
same three scales used by the approved fog fixture, and uses World time for the
|
|
2659
3484
|
continuous advection term. The renderer keeps the source texture resident; it
|
|
2660
3485
|
does not rebuild noise data for each frame. Its source expression is mapped to a
|
|
2661
3486
|
signed density and clamped only at the optical-depth boundary so Beer-Lambert
|
|
2662
3487
|
extinction remains non-negative.
|
|
2663
3488
|
|
|
2664
|
-
|
|
2665
|
-
|
|
3489
|
+
The local `density` mode samples normalized owner bounds directly. Each owner
|
|
3490
|
+
retains its own texture and optics; overlap sums coefficients before integration.
|
|
3491
|
+
Density integration partitions at owner entry/exit boundaries and uses 96
|
|
3492
|
+
fixed midpoint steps per interval, including the clipped final segment.
|
|
3493
|
+
Segments near a selected PointLight or SpotLight
|
|
2666
3494
|
receive four local midpoints to resolve the inverse-square peak without making
|
|
2667
3495
|
the whole ray uniformly expensive. Frame identity does not rotate density
|
|
2668
3496
|
samples. Shadow visibility uses matching froxel centers without per-frame XY
|
|
@@ -2716,3 +3544,61 @@ orthographic camera stay on the normal camera snapshot path with structured
|
|
|
2716
3544
|
The Engine Preview/game capability lab uses this same component on its ordinary
|
|
2717
3545
|
mesh camera, and its settings control toggles component presence through the
|
|
2718
3546
|
normal World path.
|
|
3547
|
+
|
|
3548
|
+
## Standard camera barrel distortion
|
|
3549
|
+
|
|
3550
|
+
`BarrelDistortion` is the bounded output-space camera companion. Its
|
|
3551
|
+
`strength` is finite in `[0, 0.35]`; `centerX` and `centerY` are finite fractions
|
|
3552
|
+
in `[0, 1]` with a top-left origin. The Standard stage samples linear-LDR color
|
|
3553
|
+
after LUT and before FXAA and the single output encoding. Positive strength uses
|
|
3554
|
+
the automatic crop rule, so the fixed-FOV camera sees a narrower region. Missing
|
|
3555
|
+
or zero strength preserves the exact zero-work path.
|
|
3556
|
+
|
|
3557
|
+
If a DOM or `OffscreenCanvas` drawing buffer is temporarily zero-sized (for
|
|
3558
|
+
example while hidden or detached), `renderer.draw()` stops before configuring
|
|
3559
|
+
or submitting a zero-sized swapchain texture. The public result is a structured
|
|
3560
|
+
`device-operation-failed` whose nested cause is `rhi-not-available`; the last
|
|
3561
|
+
accepted `FrameReceipt` remains the LKG and a later positive-size draw resumes
|
|
3562
|
+
the same camera mapping without a GPU validation cascade.
|
|
3563
|
+
|
|
3564
|
+
```ts
|
|
3565
|
+
import { BarrelDistortion } from '@forgeax/engine-render';
|
|
3566
|
+
|
|
3567
|
+
world.addComponent(camera, {
|
|
3568
|
+
component: BarrelDistortion,
|
|
3569
|
+
data: { strength: 0.2, centerX: 0.5, centerY: 0.5 },
|
|
3570
|
+
});
|
|
3571
|
+
```
|
|
3572
|
+
|
|
3573
|
+
Invalid authoring is a closed error-code path. Narrow on `error.code` and read
|
|
3574
|
+
the structured fields; do not parse `error.message`:
|
|
3575
|
+
|
|
3576
|
+
```ts
|
|
3577
|
+
import { createBarrelDistortionMapping } from '@forgeax/engine-render';
|
|
3578
|
+
|
|
3579
|
+
const mapping = createBarrelDistortionMapping(1920, 1080, { strength: 0.4 });
|
|
3580
|
+
if (!mapping.ok && mapping.error.code === 'barrel-distortion-invalid-parameter') {
|
|
3581
|
+
const error = mapping.error;
|
|
3582
|
+
const field = error.detail.field;
|
|
3583
|
+
const value = error.detail.value;
|
|
3584
|
+
const detailExpected = error.detail.expected;
|
|
3585
|
+
const expected = error.expected;
|
|
3586
|
+
const hint = error.hint;
|
|
3587
|
+
console.error({ field, value, detailExpected, expected, hint });
|
|
3588
|
+
}
|
|
3589
|
+
```
|
|
3590
|
+
|
|
3591
|
+
In the design language, `actual` and `bound` map to the public Barrel members
|
|
3592
|
+
`error.detail.value` and `error.detail.expected`. Barrel errors do not expose
|
|
3593
|
+
`error.detail.actual` or `error.detail.bound`; those member names remain owned by
|
|
3594
|
+
`shadow-invalid-config` only.
|
|
3595
|
+
|
|
3596
|
+
`createBarrelDistortionMapping(outputWidth, outputHeight, data)` derives the
|
|
3597
|
+
immutable effective mapping. Reuse the mapping attached to the submitted
|
|
3598
|
+
`FrameReceipt` or the serialized worker frame signal. `mapDisplayToScene` and
|
|
3599
|
+
`mapSceneToDisplay` write continuous output-viewport physical pixels through
|
|
3600
|
+
out-parameters; inverse points cropped outside the display rectangle return
|
|
3601
|
+
`false`. The public `@forgeax/engine-picking` display entrypoints use this
|
|
3602
|
+
mapping once, while legacy picking continues to accept unwarped viewport
|
|
3603
|
+
coordinates. DOM/ShadowRoot HUD layout remains unchanged; world labels and
|
|
3604
|
+
vertex radius sorting must use the inverse mapping in display pixels.
|