@forgeax/engine-render 0.1.32 → 0.1.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1374 -142
- package/dist/assembly/analytic-fog-registration.d.ts +3 -0
- package/dist/assembly/analytic-fog-registration.d.ts.map +1 -0
- package/dist/assembly/backend-contract.d.ts +2 -0
- package/dist/assembly/backend-contract.d.ts.map +1 -1
- package/dist/assembly/barrel-distortion-inspection.d.ts +3 -0
- package/dist/assembly/barrel-distortion-inspection.d.ts.map +1 -0
- package/dist/assembly/canvas-draw-guard.d.ts +3 -0
- package/dist/assembly/canvas-draw-guard.d.ts.map +1 -0
- package/dist/assembly/cloud-shadow-entry.d.ts +4 -0
- package/dist/assembly/cloud-shadow-entry.d.ts.map +1 -0
- package/dist/assembly/dynamic-geometry-host.d.ts +4 -1
- package/dist/assembly/dynamic-geometry-host.d.ts.map +1 -1
- package/dist/assembly/dynamic-geometry-runtime.d.ts +1 -1
- package/dist/assembly/dynamic-geometry-runtime.d.ts.map +1 -1
- package/dist/assembly/factory.d.ts +1 -1
- package/dist/assembly/factory.d.ts.map +1 -1
- package/dist/assembly/fxaa-registration.d.ts +4 -0
- package/dist/assembly/fxaa-registration.d.ts.map +1 -0
- package/dist/assembly/host-contract.d.ts +3 -3
- package/dist/assembly/host-contract.d.ts.map +1 -1
- package/dist/assembly/lod-observation.d.ts +4 -0
- package/dist/assembly/lod-observation.d.ts.map +1 -0
- package/dist/assembly/material/assembly.d.ts.map +1 -1
- package/dist/assembly/material/pipeline-helpers.d.ts +5 -0
- package/dist/assembly/material/pipeline-helpers.d.ts.map +1 -1
- package/dist/assembly/material-shader-policy.d.ts +16 -2
- package/dist/assembly/material-shader-policy.d.ts.map +1 -1
- package/dist/assembly/material-shader-prewarm.d.ts.map +1 -1
- package/dist/assembly/motion-blur-registration.d.ts +3 -0
- package/dist/assembly/motion-blur-registration.d.ts.map +1 -0
- package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -1
- package/dist/assembly/recovery/renderer-recover.d.ts +2 -0
- package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -1
- package/dist/assembly/render-feature-post-processes.d.ts +6 -0
- package/dist/assembly/render-feature-post-processes.d.ts.map +1 -0
- package/dist/assembly/renderer-facade.d.ts.map +1 -1
- package/dist/assembly/renderer-feature-inspection.d.ts +1 -1
- package/dist/assembly/renderer-feature-inspection.d.ts.map +1 -1
- package/dist/assembly/renderer-frame-transaction.d.ts +12 -0
- package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
- package/dist/assembly/renderer-inspection.d.ts +11 -0
- package/dist/assembly/renderer-inspection.d.ts.map +1 -1
- package/dist/assembly/temporal-post-process-prewarm.d.ts +13 -0
- package/dist/assembly/temporal-post-process-prewarm.d.ts.map +1 -0
- package/dist/assembly/webgpu-pbr-ready.d.ts +1 -0
- package/dist/assembly/webgpu-pbr-ready.d.ts.map +1 -1
- package/dist/assembly/webgpu-ready-contract.d.ts +4 -3
- package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -1
- package/dist/assembly/webgpu-ready-per-pass.d.ts +39 -0
- package/dist/assembly/webgpu-ready-per-pass.d.ts.map +1 -0
- package/dist/assembly/webgpu-ready.d.ts +2 -2
- package/dist/assembly/webgpu-ready.d.ts.map +1 -1
- package/dist/assembly/webgpu-renderer-bootstrap.d.ts.map +1 -1
- package/dist/assembly/webgpu-renderer-observation-owner.d.ts +35 -0
- package/dist/assembly/webgpu-renderer-observation-owner.d.ts.map +1 -0
- package/dist/assembly/webgpu-renderer.d.ts +1 -1
- package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
- package/dist/authoring.mjs +1 -1
- package/dist/barrel-distortion.d.ts +47 -0
- package/dist/barrel-distortion.d.ts.map +1 -0
- package/dist/bloom-admission.d.ts +13 -0
- package/dist/bloom-admission.d.ts.map +1 -0
- package/dist/{chunk-PA5DRJNI.mjs → chunk-ELJFJ2BU.mjs} +2 -2
- package/dist/chunk-ELJFJ2BU.mjs.map +1 -0
- package/dist/{chunk-JCIRYQ4V.mjs → chunk-HEB564WF.mjs} +10 -4
- package/dist/chunk-HEB564WF.mjs.map +1 -0
- package/dist/{chunk-JRIPCMYC.mjs → chunk-MKDIT2EM.mjs} +1584 -375
- package/dist/chunk-MKDIT2EM.mjs.map +1 -0
- package/dist/{chunk-S665NJNF.mjs → chunk-OCDLEI6I.mjs} +3 -3
- package/dist/{chunk-S665NJNF.mjs.map → chunk-OCDLEI6I.mjs.map} +1 -1
- package/dist/chunk-ODZWR6JQ.mjs +4927 -0
- package/dist/chunk-ODZWR6JQ.mjs.map +1 -0
- package/dist/{chunk-DRR7D3KX.mjs → chunk-SHFUFJ5N.mjs} +192 -18
- package/dist/chunk-SHFUFJ5N.mjs.map +1 -0
- package/dist/{chunk-4NNZQCL3.mjs → chunk-UI5SFODX.mjs} +223 -277
- package/dist/chunk-UI5SFODX.mjs.map +1 -0
- package/dist/{chunk-GWCEMDVR.mjs → chunk-UINHLCZA.mjs} +787 -80
- package/dist/chunk-UINHLCZA.mjs.map +1 -0
- package/dist/{chunk-6TFBXLRJ.mjs → chunk-WEX45HKH.mjs} +222 -337
- package/dist/chunk-WEX45HKH.mjs.map +1 -0
- package/dist/cloud/density.d.ts +55 -0
- package/dist/cloud/density.d.ts.map +1 -0
- package/dist/cloud/extract.d.ts +25 -0
- package/dist/cloud/extract.d.ts.map +1 -0
- package/dist/cloud/feature.d.ts +57 -0
- package/dist/cloud/feature.d.ts.map +1 -0
- package/dist/cloud/frame-context.d.ts +8 -0
- package/dist/cloud/frame-context.d.ts.map +1 -0
- package/dist/cloud/inspection.d.ts +59 -0
- package/dist/cloud/inspection.d.ts.map +1 -0
- package/dist/cloud/optics.d.ts +56 -0
- package/dist/cloud/optics.d.ts.map +1 -0
- package/dist/cloud/parameters.d.ts +73 -0
- package/dist/cloud/parameters.d.ts.map +1 -0
- package/dist/cloud/resources.d.ts +60 -0
- package/dist/cloud/resources.d.ts.map +1 -0
- package/dist/cloud/shadow.d.ts +42 -0
- package/dist/cloud/shadow.d.ts.map +1 -0
- package/dist/cloud/temporal.d.ts +74 -0
- package/dist/cloud/temporal.d.ts.map +1 -0
- package/dist/components/atmosphere.d.ts +2 -0
- package/dist/components/atmosphere.d.ts.map +1 -1
- package/dist/components/barrel-distortion.d.ts +13 -0
- package/dist/components/barrel-distortion.d.ts.map +1 -0
- package/dist/components/camera.d.ts +20 -4
- package/dist/components/camera.d.ts.map +1 -1
- package/dist/components/cloud-layer.d.ts +29 -0
- package/dist/components/cloud-layer.d.ts.map +1 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/instances.d.ts +20 -6
- package/dist/components/instances.d.ts.map +1 -1
- package/dist/components/mesh-filter.d.ts +6 -5
- package/dist/components/mesh-filter.d.ts.map +1 -1
- package/dist/components/motion-blur.d.ts +1 -0
- package/dist/components/motion-blur.d.ts.map +1 -1
- package/dist/construct-renderer.d.ts +1 -1
- package/dist/construct-renderer.d.ts.map +1 -1
- package/dist/construct-renderer.mjs +9751 -3528
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/device/gpu-residency.d.ts +6 -0
- package/dist/device/gpu-residency.d.ts.map +1 -1
- package/dist/dynamic-geometry.d.ts +5 -1
- package/dist/dynamic-geometry.d.ts.map +1 -1
- package/dist/environment/analytic-fog.d.ts +6 -0
- package/dist/environment/analytic-fog.d.ts.map +1 -0
- package/dist/errors/cloud.d.ts +59 -0
- package/dist/errors/cloud.d.ts.map +1 -0
- package/dist/errors/render.d.ts +23 -5
- package/dist/errors/render.d.ts.map +1 -1
- package/dist/examples/single-layer-medium-surface.d.ts +47 -0
- package/dist/examples/single-layer-medium-surface.d.ts.map +1 -0
- package/dist/extract/camera.d.ts.map +1 -1
- package/dist/extract/contracts.d.ts +12 -1
- package/dist/extract/contracts.d.ts.map +1 -1
- package/dist/extract/environment.d.ts +2 -0
- package/dist/extract/environment.d.ts.map +1 -1
- package/dist/features/barrel-distortion.d.ts +29 -0
- package/dist/features/barrel-distortion.d.ts.map +1 -0
- package/dist/features/error-reporting.d.ts +17 -0
- package/dist/features/error-reporting.d.ts.map +1 -0
- package/dist/features/host.d.ts +15 -0
- package/dist/features/host.d.ts.map +1 -1
- package/dist/features/motion-blur/motion-blur-feature.d.ts +20 -2
- package/dist/features/motion-blur/motion-blur-feature.d.ts.map +1 -1
- package/dist/features/motion-blur/motion-blur-params.d.ts +21 -0
- package/dist/features/motion-blur/motion-blur-params.d.ts.map +1 -1
- package/dist/features/motion-blur/motion-blur-runtime.d.ts +54 -0
- package/dist/features/motion-blur/motion-blur-runtime.d.ts.map +1 -0
- package/dist/features/plan.d.ts +15 -4
- package/dist/features/plan.d.ts.map +1 -1
- package/dist/features/prepared-gpu-work.d.ts +9 -5
- package/dist/features/prepared-gpu-work.d.ts.map +1 -1
- package/dist/features/render-graph-compute.d.ts.map +1 -1
- package/dist/features/render-graph-contribution.d.ts +5 -0
- package/dist/features/render-graph-contribution.d.ts.map +1 -1
- package/dist/features/render-graph-raster.d.ts +1 -0
- package/dist/features/render-graph-raster.d.ts.map +1 -1
- package/dist/features/targets.d.ts +2 -0
- package/dist/features/targets.d.ts.map +1 -1
- package/dist/features/types.d.ts +44 -0
- package/dist/features/types.d.ts.map +1 -1
- package/dist/fullscreen-post-process-pass.d.ts +15 -7
- package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
- package/dist/gpu-dirty-ranges.d.ts +11 -0
- package/dist/gpu-dirty-ranges.d.ts.map +1 -0
- package/dist/gpu-driven/batch-topology.d.ts +17 -0
- package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
- package/dist/gpu-driven/material-artifacts.d.ts +6 -0
- package/dist/gpu-driven/material-artifacts.d.ts.map +1 -1
- package/dist/gpu-driven/prepared-draw.d.ts +7 -0
- package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
- package/dist/gpu-driven/production-raster-material.d.ts +178 -0
- package/dist/gpu-driven/production-raster-material.d.ts.map +1 -0
- package/dist/gpu-driven/production-raster.d.ts +50 -4
- package/dist/gpu-driven/production-raster.d.ts.map +1 -1
- package/dist/gpu-driven/resource-allocation.d.ts +31 -0
- package/dist/gpu-driven/resource-allocation.d.ts.map +1 -0
- package/dist/gpu-driven/shadow-ownership.d.ts +14 -0
- package/dist/gpu-driven/shadow-ownership.d.ts.map +1 -0
- package/dist/gpu-driven/view-gpu.d.ts +35 -2
- package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
- package/dist/gpu-scene.d.ts +11 -1
- package/dist/gpu-scene.d.ts.map +1 -1
- package/dist/hdrp-buffers.d.ts +2 -2
- package/dist/hdrp-buffers.d.ts.map +1 -1
- package/dist/ibl/skylight-bind-group.d.ts +10 -1
- package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
- package/dist/index.d.ts +31 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +258 -8
- package/dist/index.mjs.map +1 -1
- package/dist/inspection-types.d.ts +178 -3
- package/dist/inspection-types.d.ts.map +1 -1
- package/dist/instance-buffer-cache.d.ts +4 -4
- package/dist/instance-buffer-cache.d.ts.map +1 -1
- package/dist/instances.d.ts +49 -94
- package/dist/instances.d.ts.map +1 -1
- package/dist/internal.mjs +7 -7
- package/dist/pbr-pipeline.d.ts +19 -1
- package/dist/pbr-pipeline.d.ts.map +1 -1
- package/dist/pipeline/single-layer-medium-passes.d.ts +73 -0
- package/dist/pipeline/single-layer-medium-passes.d.ts.map +1 -0
- package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
- package/dist/pipeline/standard-output/graph.d.ts.map +1 -1
- package/dist/pipeline/standard-output/types.d.ts +4 -2
- package/dist/pipeline/standard-output/types.d.ts.map +1 -1
- package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
- package/dist/pipeline/standard-post.d.ts +28 -19
- package/dist/pipeline/standard-post.d.ts.map +1 -1
- package/dist/pipeline-builder.d.ts +1 -1
- package/dist/pipeline-builder.d.ts.map +1 -1
- package/dist/pipeline-spec.d.ts +2 -2
- package/dist/plugin.d.ts.map +1 -1
- package/dist/prepare/prepared-graphics-resolver.d.ts +2 -0
- package/dist/prepare/prepared-graphics-resolver.d.ts.map +1 -1
- package/dist/record/barrel-distortion-frame.d.ts +15 -0
- package/dist/record/barrel-distortion-frame.d.ts.map +1 -0
- package/dist/record/dynamic-geometry-consumption.d.ts +10 -2
- package/dist/record/dynamic-geometry-consumption.d.ts.map +1 -1
- package/dist/record/fold-buckets.d.ts +95 -0
- package/dist/record/fold-buckets.d.ts.map +1 -0
- package/dist/record/fold-instance-buffer.d.ts +6 -0
- package/dist/record/fold-instance-buffer.d.ts.map +1 -0
- package/dist/record/frame-lighting.d.ts +1 -0
- package/dist/record/frame-lighting.d.ts.map +1 -1
- package/dist/record/frame-snapshot.d.ts +40 -3
- package/dist/record/frame-snapshot.d.ts.map +1 -1
- package/dist/record/frame-targets.d.ts.map +1 -1
- package/dist/record/frame.d.ts +14 -62
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/gpu-draw-receipts.d.ts +11 -0
- package/dist/record/gpu-draw-receipts.d.ts.map +1 -0
- package/dist/record/gpu-timing.d.ts +8 -0
- package/dist/record/gpu-timing.d.ts.map +1 -1
- package/dist/record/main-pass-geometry.d.ts +21 -5
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/main-pass-material.d.ts +7 -1
- package/dist/record/main-pass-material.d.ts.map +1 -1
- package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
- package/dist/record/main-pass.d.ts +13 -2
- package/dist/record/main-pass.d.ts.map +1 -1
- package/dist/record/mesh-ssbo.d.ts +6 -5
- package/dist/record/mesh-ssbo.d.ts.map +1 -1
- package/dist/record/motion-blur-receipt.d.ts +11 -0
- package/dist/record/motion-blur-receipt.d.ts.map +1 -0
- package/dist/record/probe-blend-buffer.d.ts +16 -0
- package/dist/record/probe-blend-buffer.d.ts.map +1 -0
- package/dist/record/recovery-pipeline.d.ts.map +1 -1
- package/dist/record/render-context.d.ts +34 -31
- package/dist/record/render-context.d.ts.map +1 -1
- package/dist/record/shadow-pass.d.ts +2 -0
- package/dist/record/shadow-pass.d.ts.map +1 -1
- package/dist/record/skybox-post-pass.d.ts +6 -4
- package/dist/record/skybox-post-pass.d.ts.map +1 -1
- package/dist/record/typed-frame-graph.d.ts +22 -2
- package/dist/record/typed-frame-graph.d.ts.map +1 -1
- package/dist/record/view-ubo.d.ts +11 -3
- package/dist/record/view-ubo.d.ts.map +1 -1
- package/dist/record/volume-params.d.ts +9 -0
- package/dist/record/volume-params.d.ts.map +1 -0
- package/dist/recovery/render-system-candidate.d.ts +10 -0
- package/dist/recovery/render-system-candidate.d.ts.map +1 -1
- package/dist/reflection/record-owner.d.ts.map +1 -1
- package/dist/render-contract.d.ts +66 -8
- package/dist/render-contract.d.ts.map +1 -1
- package/dist/render-graph-primitives.d.ts.map +1 -1
- package/dist/render-pipeline.d.ts +106 -3
- package/dist/render-pipeline.d.ts.map +1 -1
- package/dist/render-system-extract-tail.d.ts +3 -2
- package/dist/render-system-extract-tail.d.ts.map +1 -1
- package/dist/render-system-extract.d.ts +24 -10
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system-presentation.d.ts +23 -0
- package/dist/render-system-presentation.d.ts.map +1 -0
- package/dist/render-system-producer-roots.d.ts +10 -0
- package/dist/render-system-producer-roots.d.ts.map +1 -0
- package/dist/render-system-residency.d.ts +12 -0
- package/dist/render-system-residency.d.ts.map +1 -0
- package/dist/render-system.d.ts +26 -15
- package/dist/render-system.d.ts.map +1 -1
- package/dist/scene/probe-blend.d.ts +27 -0
- package/dist/scene/probe-blend.d.ts.map +1 -1
- package/dist/scene/render-scene.d.ts +14 -5
- package/dist/scene/render-scene.d.ts.map +1 -1
- package/dist/ssao-buffers.d.ts +9 -0
- package/dist/ssao-buffers.d.ts.map +1 -1
- package/dist/surface/dynamic-input.d.ts +133 -0
- package/dist/surface/dynamic-input.d.ts.map +1 -0
- package/dist/surface/gpu-driven.d.ts +41 -0
- package/dist/surface/gpu-driven.d.ts.map +1 -0
- package/dist/surface/submission-observation.d.ts +136 -0
- package/dist/surface/submission-observation.d.ts.map +1 -0
- package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
- package/dist/temporal/frame-coordinator.d.ts.map +1 -1
- package/dist/temporal/frame.d.ts +7 -0
- package/dist/temporal/frame.d.ts.map +1 -1
- package/dist/temporal/gpu.d.ts +13 -1
- package/dist/temporal/gpu.d.ts.map +1 -1
- package/dist/temporal/index.mjs +5 -5
- package/dist/temporal/inspection.d.ts.map +1 -1
- package/dist/temporal/standard-scene-data.d.ts.map +1 -1
- package/dist/temporal/target-coverage-attachment.d.ts.map +1 -1
- package/dist/temporal/view.d.ts +6 -1
- package/dist/temporal/view.d.ts.map +1 -1
- package/dist/transmission/single-layer-medium.d.ts +89 -0
- package/dist/transmission/single-layer-medium.d.ts.map +1 -0
- package/dist/typed-render-graph-primitives.d.ts +24 -9
- package/dist/typed-render-graph-primitives.d.ts.map +1 -1
- package/dist/volume/component.d.ts +9 -5
- package/dist/volume/component.d.ts.map +1 -1
- package/dist/volume/extract.d.ts +1 -1
- package/dist/volume/extract.d.ts.map +1 -1
- package/dist/volume/inspection.d.ts +2 -0
- package/dist/volume/inspection.d.ts.map +1 -1
- package/dist/volume/passes.d.ts +4 -1
- package/dist/volume/passes.d.ts.map +1 -1
- package/dist/volume/resources.d.ts.map +1 -1
- package/dist/volume/temporal.d.ts +3 -1
- package/dist/volume/temporal.d.ts.map +1 -1
- package/package.json +22 -21
- package/src/__tests__/analytic-fog-graph.unit.test.ts +199 -0
- package/src/__tests__/analytic-fog-optics.unit.test.ts +38 -0
- package/src/__tests__/atmosphere-daylight.dawn.test.ts +72 -0
- package/src/__tests__/barrel-distortion-lkg.integration.test.ts +353 -0
- package/src/__tests__/barrel-distortion.dawn.test.ts +41 -0
- package/src/__tests__/barrel-distortion.unit.test.ts +364 -0
- package/src/__tests__/bloom-exact-zero.unit.test.ts +112 -18
- package/src/__tests__/bloom-lifecycle.integration.test.ts +17 -4
- package/src/__tests__/construct-renderer-identity.unit.test.ts +49 -0
- package/src/__tests__/detached-recovery-graph.unit.test.ts +277 -0
- package/src/__tests__/device-loss-fanout.unit.test.ts +36 -1
- package/src/__tests__/docs-gate-green.unit.test.ts +112 -1
- package/src/__tests__/dynamic-resolution-plugin.integration.test.ts +20 -0
- package/src/__tests__/environment-fog-lanes.integration.test.ts +14 -0
- package/src/__tests__/environment-selection.unit.test.ts +12 -0
- package/src/__tests__/factory-contract.integration.test.ts +626 -113
- package/src/__tests__/feature-depth-input.dawn.test.ts +129 -2
- package/src/__tests__/frame-camera-selection.unit.test.ts +39 -1
- package/src/__tests__/frame-plan-contract.unit.test.ts +2 -0
- package/src/__tests__/fullscreen-feature-plan.unit.test.ts +14 -3
- package/src/__tests__/gpu-dirty-ranges.unit.test.ts +22 -0
- package/src/__tests__/gpu-driven-baseline.characterization.test.ts +16 -3
- package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +12 -0
- package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +2 -1
- package/src/__tests__/gpu-driven-material-artifacts.unit.test.ts +143 -0
- package/src/__tests__/gpu-driven-pbr.dawn.test.ts +2 -2
- package/src/__tests__/gpu-driven-production.integration.test.ts +482 -3
- package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +7 -6
- package/src/__tests__/gpu-driven-shadow-views.integration.test.ts +1 -1
- package/src/__tests__/gpu-driven-view-gpu-evidence.ts +103 -4
- package/src/__tests__/gpu-driven-view-graph.integration.test.ts +18 -2
- package/src/__tests__/gpu-driven-view.browser.test.ts +5 -8
- package/src/__tests__/gpu-driven-view.dawn.test.ts +6 -2
- package/src/__tests__/gpu-driven-view.unit.test.ts +1 -1
- package/src/__tests__/gpu-pass-timing-lifecycle.integration.test.ts +1 -0
- package/src/__tests__/gpu-pass-timing.browser.test.ts +6 -4
- package/src/__tests__/gpu-pass-timing.rhi-null.unit.test.ts +1 -0
- package/src/__tests__/gpu-resource-store-stride.unit.test.ts +12 -0
- package/src/__tests__/gpu-scene.unit.test.ts +210 -9
- package/src/__tests__/instance-allocation-failure.unit.test.ts +57 -0
- package/src/__tests__/instances-culling.integration.test.ts +10 -36
- package/src/__tests__/instances-store.unit.test.ts +105 -238
- package/src/__tests__/instances-world-ownership.integration.test.ts +62 -0
- package/src/__tests__/main-pass-material-medium-selection.unit.test.ts +146 -0
- package/src/__tests__/material-shader-prewarm.unit.test.ts +59 -0
- package/src/__tests__/material-snapshot-mutation.integration.test.ts +39 -1
- package/src/__tests__/mesh-submission-lifetime.unit.test.ts +31 -0
- package/src/__tests__/morph-culling-reentry.dawn.test.ts +5 -5
- package/src/__tests__/morph-visual-evidence.integration.test.ts +8 -7
- package/src/__tests__/motion-blur-compute.dawn.test.ts +589 -0
- package/src/__tests__/motion-blur.pixel.integration.test.ts +2 -2
- package/src/__tests__/nineslice-handle-surface.unit.test.ts +4 -4
- package/src/__tests__/pbr-view-layout.unit.test.ts +9 -0
- package/src/__tests__/physical-clearcoat.integration.test.ts +58 -0
- package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +21 -0
- package/src/__tests__/prepared-graphics-capability.unit.test.ts +66 -2
- package/src/__tests__/prepared-graphics-lifecycle.unit.test.ts +86 -0
- package/src/__tests__/probe-bindings.unit.test.ts +582 -1
- package/src/__tests__/probe-scene-dirty.integration.test.ts +28 -1
- package/src/__tests__/public-surface.test-d.ts +8 -0
- package/src/__tests__/receipt-observation-retirement.unit.test.ts +65 -0
- package/src/__tests__/recovery-candidate-prepare.contract.test.ts +69 -0
- package/src/__tests__/render-error-code-owner.test-d.ts +9 -0
- package/src/__tests__/render-feature-diagnostics.unit.test.ts +15 -2
- package/src/__tests__/render-feature-error-reporting.unit.test.ts +44 -0
- package/src/__tests__/render-feature-prepared-diagnostics.unit.test.ts +10 -1
- package/src/__tests__/render-feature-retry.unit.test.ts +12 -1
- package/src/__tests__/render-feature-stage-fault-recovery.unit.test.ts +23 -0
- package/src/__tests__/render-scene-mixed-updates.integration.test.ts +145 -19
- package/src/__tests__/render-scene-projection.unit.test.ts +36 -11
- package/src/__tests__/render-scene-temporal-retry.unit.test.ts +37 -6
- package/src/__tests__/render-target-probe-soak.perf.test.ts +2 -2
- package/src/__tests__/render-temporal-environment-consumer.browser.test.ts +3 -3
- package/src/__tests__/render-temporal-environment-consumer.dawn.test.ts +5 -2
- package/src/__tests__/renderer-factory-material-contract.unit.test.ts +41 -7
- package/src/__tests__/renderer-frame-generation-fence.integration.test.ts +16 -0
- package/src/__tests__/renderer-frame-transaction.integration.test.ts +33 -11
- package/src/__tests__/resource-allocation.unit.test.ts +123 -0
- package/src/__tests__/shader-manifest-fixture.ts +4 -4
- package/src/__tests__/single-layer-medium-passes.unit.test.ts +275 -0
- package/src/__tests__/skin-motion-regression.unit.test.ts +70 -1
- package/src/__tests__/skin-palette-recycling.dawn.test.ts +97 -0
- package/src/__tests__/skinned-shadow-caster.test.ts +17 -0
- package/src/__tests__/ssr-carrier-public-contract.unit.test.ts +21 -0
- package/src/__tests__/ssr-gpu-dispatch.browser.test.ts +4 -4
- package/src/__tests__/ssr-gpu-dispatch.dawn.test.ts +2 -2
- package/src/__tests__/ssr-gpu-dispatch.ts +1 -1
- package/src/__tests__/ssr-receiver-coverage.dawn.test.ts +6 -2
- package/src/__tests__/standard-lighting-write.unit.test.ts +2 -1
- package/src/__tests__/standard-output-chain-contract.unit.test.ts +28 -0
- package/src/__tests__/standard-pbr-artifact-assembly.unit.test.ts +46 -1
- package/src/__tests__/standard-pipeline.dawn.test.ts +1 -1
- package/src/__tests__/standard-pipeline.integration.test.ts +200 -9
- package/src/__tests__/surface-dynamic-buffer-lifecycle.unit.test.ts +506 -0
- package/src/__tests__/surface-submission-observation.unit.test.ts +812 -0
- package/src/__tests__/temporal-history-lifecycle.unit.test.ts +32 -0
- package/src/__tests__/temporal-performance.integration.test.ts +3 -3
- package/src/__tests__/temporal-reset-matrix.unit.test.ts +10 -0
- package/src/__tests__/temporal-view.unit.test.ts +2 -1
- package/src/__tests__/typed-pipeline-topology.unit.test.ts +20 -4
- package/src/__tests__/visibility-instances.integration.test.ts +7 -19
- package/src/__tests__/visibility-producer-matrix.integration.test.ts +9 -20
- package/src/__tests__/volume-optics-oracle.unit.test.ts +7 -3
- package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +189 -15
- package/src/__tests__/volumetric-fog-errors.unit.test.ts +4 -3
- package/src/__tests__/volumetric-fog-multiple.unit.test.ts +55 -0
- package/src/__tests__/volumetric-fog-spot-frame-optics.integration.test.ts +1 -1
- package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +558 -68
- package/src/__tests__/volumetric-fog-temporal.unit.test.ts +27 -0
- package/src/__tests__/volumetric-fog-topology.unit.test.ts +23 -5
- package/src/assembly/analytic-fog-registration.ts +14 -0
- package/src/assembly/backend-contract.ts +30 -0
- package/src/assembly/barrel-distortion-inspection.ts +22 -0
- package/src/assembly/canvas-draw-guard.ts +11 -0
- package/src/assembly/cloud-shadow-entry.ts +22 -0
- package/src/assembly/dynamic-geometry-host.ts +65 -37
- package/src/assembly/dynamic-geometry-runtime.ts +7 -1
- package/src/assembly/factory.ts +2 -30
- package/src/assembly/fxaa-registration.ts +17 -0
- package/src/assembly/host-contract.ts +3 -3
- package/src/assembly/lod-observation.ts +12 -0
- package/src/assembly/material/assembly.ts +25 -13
- package/src/assembly/material/pipeline-helpers.ts +53 -12
- package/src/assembly/material-shader-policy.ts +55 -0
- package/src/assembly/material-shader-prewarm.ts +21 -10
- package/src/assembly/motion-blur-registration.ts +17 -0
- package/src/assembly/recovery/device-loss-fanout.ts +13 -0
- package/src/assembly/recovery/renderer-recover.ts +41 -18
- package/src/assembly/render-feature-post-processes.ts +54 -0
- package/src/assembly/renderer-facade.ts +6 -1
- package/src/assembly/renderer-feature-inspection.ts +8 -2
- package/src/assembly/renderer-frame-transaction.ts +23 -0
- package/src/assembly/renderer-inspection.ts +28 -0
- package/src/assembly/skin-palette-owner.ts +1 -1
- package/src/assembly/temporal-post-process-prewarm.ts +40 -0
- package/src/assembly/webgpu-pbr-ready.ts +9 -1
- package/src/assembly/webgpu-ready-contract.ts +8 -3
- package/src/assembly/webgpu-ready-per-pass.ts +104 -0
- package/src/assembly/webgpu-ready.ts +296 -306
- package/src/assembly/webgpu-renderer-bootstrap.ts +0 -12
- package/src/assembly/webgpu-renderer-observation-owner.ts +222 -0
- package/src/assembly/webgpu-renderer.ts +228 -281
- package/src/assembly/webgpu-vertex-layouts.ts +1 -1
- package/src/barrel-distortion.ts +247 -0
- package/src/bloom-admission.ts +21 -0
- package/src/cloud/__tests__/cloud-density-parity.dawn.test.ts +173 -0
- package/src/cloud/__tests__/cloud-layer-framebuffer-y.dawn.test.ts +265 -0
- package/src/cloud/__tests__/cloud-layer.browser.test.ts +432 -0
- package/src/cloud/__tests__/cloud-layer.unit.test.ts +634 -0
- package/src/cloud/density.ts +600 -0
- package/src/cloud/extract.ts +110 -0
- package/src/cloud/feature.ts +1810 -0
- package/src/cloud/frame-context.ts +42 -0
- package/src/cloud/inspection.ts +130 -0
- package/src/cloud/optics.ts +285 -0
- package/src/cloud/parameters.ts +255 -0
- package/src/cloud/resources.ts +122 -0
- package/src/cloud/shadow.ts +167 -0
- package/src/cloud/temporal.ts +226 -0
- package/src/components/__tests__/motion-blur.unit.test.ts +1 -0
- package/src/components/atmosphere.ts +4 -0
- package/src/components/barrel-distortion.ts +46 -0
- package/src/components/camera.ts +66 -10
- package/src/components/cloud-layer.ts +47 -0
- package/src/components/index.ts +2 -0
- package/src/components/instances.ts +18 -45
- package/src/components/mesh-filter.ts +11 -9
- package/src/components/motion-blur.ts +1 -0
- package/src/components/sprite-instances.ts +1 -1
- package/src/construct-renderer.ts +1 -1
- package/src/device/gpu-residency.ts +56 -12
- package/src/dynamic-geometry.ts +12 -1
- package/src/environment/analytic-fog.ts +44 -0
- package/src/environment/background.ts +2 -2
- package/src/errors/cloud.ts +103 -0
- package/src/errors/render.ts +58 -8
- package/src/examples/single-layer-medium-surface.ts +59 -0
- package/src/extract/camera.ts +35 -8
- package/src/extract/contracts.ts +12 -0
- package/src/extract/environment.ts +4 -0
- package/src/features/barrel-distortion.ts +172 -0
- package/src/features/error-reporting.ts +98 -0
- package/src/features/host.ts +138 -24
- package/src/features/motion-blur/__tests__/motion-blur-feature.unit.test.ts +111 -1
- package/src/features/motion-blur/__tests__/motion-blur-params.unit.test.ts +58 -3
- package/src/features/motion-blur/motion-blur-feature.ts +1519 -15
- package/src/features/motion-blur/motion-blur-params.ts +70 -2
- package/src/features/motion-blur/motion-blur-runtime.ts +176 -0
- package/src/features/plan.ts +33 -8
- package/src/features/prepared-gpu-work.ts +86 -35
- package/src/features/render-graph-compute.ts +13 -5
- package/src/features/render-graph-contribution.ts +11 -0
- package/src/features/render-graph-raster.ts +45 -3
- package/src/features/targets.ts +66 -0
- package/src/features/types.ts +47 -0
- package/src/fullscreen-post-process-pass.ts +41 -9
- package/src/gpu-dirty-ranges.ts +48 -0
- package/src/gpu-driven/batch-topology.ts +53 -0
- package/src/gpu-driven/material-artifacts.ts +35 -32
- package/src/gpu-driven/prepared-draw.ts +27 -14
- package/src/gpu-driven/production-raster-material.ts +1894 -0
- package/src/gpu-driven/production-raster.ts +904 -787
- package/src/gpu-driven/resource-allocation.ts +142 -0
- package/src/gpu-driven/shadow-ownership.ts +71 -0
- package/src/gpu-driven/view-gpu.ts +181 -10
- package/src/gpu-scene.ts +259 -101
- package/src/hdrp-buffers.ts +4 -3
- package/src/ibl/skylight-bind-group.ts +49 -7
- package/src/index.ts +228 -9
- package/src/inspection-types.ts +222 -2
- package/src/instance-buffer-cache.ts +4 -4
- package/src/instances.ts +151 -492
- package/src/pbr-pipeline.ts +219 -13
- package/src/pipeline/single-layer-medium-passes.ts +267 -0
- package/src/pipeline/standard-forward-lane.ts +313 -57
- package/src/pipeline/standard-output/graph.ts +8 -2
- package/src/pipeline/standard-output/types.ts +12 -1
- package/src/pipeline/standard-pipeline.ts +291 -18
- package/src/pipeline/standard-post.ts +397 -102
- package/src/pipeline-builder.ts +1 -1
- package/src/pipeline-spec.ts +5 -5
- package/src/plugin.ts +4 -0
- package/src/points-lines/__tests__/points-lines.bench.ts +17 -8
- package/src/points-lines/__tests__/view-ubo.unit.test.ts +2 -1
- package/src/prepare/prepared-graphics-resolver.ts +2 -0
- package/src/record/__tests__/main-pass-geometry.unit.test.ts +30 -0
- package/src/record/__tests__/sprite-material-abi.unit.test.ts +1 -1
- package/src/record/__tests__/temporal-payload.unit.test.ts +3 -2
- package/src/record/__tests__/transparent-group2-selection.unit.test.ts +11 -0
- package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +1 -0
- package/src/record/__tests__/view-ubo-layout.unit.test.ts +4 -3
- package/src/record/barrel-distortion-frame.ts +110 -0
- package/src/record/dynamic-geometry-consumption.ts +39 -9
- package/src/record/fold-buckets.ts +852 -0
- package/src/record/fold-instance-buffer.ts +51 -0
- package/src/record/frame-lighting.ts +36 -2
- package/src/record/frame-snapshot.ts +44 -4
- package/src/record/frame-targets.ts +10 -6
- package/src/record/frame.ts +373 -1099
- package/src/record/gpu-draw-receipts.ts +38 -0
- package/src/record/gpu-timing.ts +48 -5
- package/src/record/main-pass-geometry.ts +281 -83
- package/src/record/main-pass-material.ts +66 -4
- package/src/record/main-pass-sprite-draws.ts +13 -66
- package/src/record/main-pass.ts +156 -20
- package/src/record/mesh-ssbo.ts +43 -6
- package/src/record/motion-blur-receipt.ts +91 -0
- package/src/record/probe-blend-buffer.ts +180 -0
- package/src/record/recovery-pipeline.ts +35 -3
- package/src/record/render-context.ts +51 -31
- package/src/record/shadow-pass.ts +36 -2
- package/src/record/skybox-post-pass.ts +150 -200
- package/src/record/typed-frame-graph.ts +824 -44
- package/src/record/view-ubo.ts +26 -3
- package/src/record/volume-params.ts +225 -0
- package/src/recovery/render-system-candidate.ts +257 -52
- package/src/reflection/record-owner.ts +7 -3
- package/src/render-contract.ts +71 -8
- package/src/render-graph-primitives.ts +7 -1
- package/src/render-pipeline.ts +136 -1
- package/src/render-system-extract-tail.ts +72 -60
- package/src/render-system-extract.ts +117 -87
- package/src/render-system-presentation.ts +56 -0
- package/src/render-system-producer-roots.ts +96 -0
- package/src/render-system-residency.ts +159 -0
- package/src/render-system.ts +601 -482
- package/src/scene/probe-blend.ts +96 -4
- package/src/scene/render-scene.ts +431 -281
- package/src/ssao-buffers.ts +95 -2
- package/src/surface/__tests__/dynamic-input.unit.test.ts +374 -0
- package/src/surface/__tests__/gpu-driven.unit.test.ts +87 -0
- package/src/surface/dynamic-input.ts +658 -0
- package/src/surface/gpu-driven.ts +147 -0
- package/src/surface/submission-observation.ts +517 -0
- package/src/systems/skin-palette-allocator.ts +4 -1
- package/src/temporal/__tests__/temporal-frame-transaction.unit.test.ts +36 -0
- package/src/temporal/frame-coordinator.ts +8 -1
- package/src/temporal/frame.ts +20 -0
- package/src/temporal/gpu.ts +101 -17
- package/src/temporal/inspection.ts +7 -1
- package/src/temporal/standard-scene-data.ts +5 -0
- package/src/temporal/target-coverage-attachment.ts +4 -0
- package/src/temporal/view.ts +13 -2
- package/src/transmission/__tests__/single-layer-medium.unit.test.ts +149 -0
- package/src/transmission/__tests__/standard-transmission.dawn.test.ts +12 -11
- package/src/transmission/single-layer-medium.ts +322 -0
- package/src/typed-render-graph-primitives.ts +191 -50
- package/src/volume/component.ts +10 -10
- package/src/volume/extract.ts +11 -5
- package/src/volume/inspection.ts +3 -0
- package/src/volume/passes.ts +95 -8
- package/src/volume/resources.ts +2 -1
- package/src/volume/temporal.ts +11 -0
- package/dist/chunk-4NNZQCL3.mjs.map +0 -1
- package/dist/chunk-6TFBXLRJ.mjs.map +0 -1
- package/dist/chunk-DRR7D3KX.mjs.map +0 -1
- package/dist/chunk-GWCEMDVR.mjs.map +0 -1
- package/dist/chunk-JCIRYQ4V.mjs.map +0 -1
- package/dist/chunk-JRIPCMYC.mjs.map +0 -1
- package/dist/chunk-LULCA3OF.mjs +0 -1713
- package/dist/chunk-LULCA3OF.mjs.map +0 -1
- package/dist/chunk-PA5DRJNI.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# `@forgeax/engine-render`
|
|
2
2
|
|
|
3
|
+
> [!NOTE]
|
|
4
|
+
> This package-owner README contains physical `@forgeax/engine-render` imports
|
|
5
|
+
> in internal ownership examples. Game-facing code should use the public
|
|
6
|
+
> facade `@forgeax/engine/render`; the Bloom consumer route is documented in
|
|
7
|
+
> [hello-bloom](../../apps/hello/bloom/README.md).
|
|
8
|
+
|
|
3
9
|
> [!IMPORTANT]
|
|
4
10
|
> The Standard pipeline keeps dark-gradient color in `rgba16float` until one
|
|
5
11
|
> `outputTransform` reaches the `surface.storage.raw` endpoint; `inspect()` and
|
|
@@ -8,6 +14,77 @@
|
|
|
8
14
|
|
|
9
15
|
## Public frame contract
|
|
10
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
|
+
|
|
43
|
+
`Fog` provides distance fog with exponential height density through one
|
|
44
|
+
finite-depth fullscreen pass before temporal resolve, Bloom and tone mapping.
|
|
45
|
+
`density`, `heightFalloff`, linear `color` and `maxOpacity` are selected from
|
|
46
|
+
the frame's resource owner. Disabled or zero-density fog allocates no fog target;
|
|
47
|
+
there is no 3D texture, froxel grid or temporal history. The authored sky remains
|
|
48
|
+
unchanged. FXAA, TAA and no-AA use single-sample depth; MSAA is rejected until a
|
|
49
|
+
matching depth resolve is available. Translucent surfaces that do not write depth
|
|
50
|
+
use the opaque background depth. Use `VolumetricFog` for spatial density and
|
|
51
|
+
lighting/scattering effects; the meadow-style distance haze needs only `Fog`.
|
|
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
|
+
|
|
11
88
|
## 灯光最短入口
|
|
12
89
|
|
|
13
90
|
三条最短入口:
|
|
@@ -57,6 +134,10 @@ payload; the first-draw guard still rejects lazy cold uploads or pipeline builds
|
|
|
57
134
|
inside rendering. Preparation does not make an old-generation receipt current.
|
|
58
135
|
Recovery prepares the last successfully submitted workset; the next draw still
|
|
59
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.
|
|
60
141
|
|
|
61
142
|
`inspect().recovery` is always present. Its `phase` is `null` outside an active
|
|
62
143
|
attempt and otherwise follows `quiesce`, `acquire-adapter`, `acquire-device`,
|
|
@@ -76,10 +157,39 @@ uninitialized until a successful receipt; a neutral target or last-known-good
|
|
|
76
157
|
fallback is not proof of real recovery. Public consumers never receive graph
|
|
77
158
|
nodes, devices, history textures, or prepared handles.
|
|
78
159
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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.
|
|
83
193
|
|
|
84
194
|
## RenderFeature: the producer seam (first-read index)
|
|
85
195
|
|
|
@@ -92,6 +202,589 @@ fallback material. The projection preserves `layoutIdentity`,
|
|
|
92
202
|
`programIdentity`, `cookIdentity`, and `materialPublicationIdentity` so a
|
|
93
203
|
stale draw can be traced to the first producer divergence.
|
|
94
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
|
+
|
|
95
788
|
The public route is one `RenderFeature<FrameData>` through the Standard
|
|
96
789
|
Pipeline and the active RenderGraph pass. In the examples below, `type FrameData`
|
|
97
790
|
is the producer-owned extracted value. A feature extracts one frame value,
|
|
@@ -109,6 +802,14 @@ that was still pending. This is a receipt of admitted commands, not a GPU
|
|
|
109
802
|
readback or proof that an indirect draw produced non-zero instances; producers
|
|
110
803
|
that need those facts must use their existing readback or tape evidence.
|
|
111
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
|
+
|
|
112
813
|
## RenderFeature: the producer seam (first-read index)
|
|
113
814
|
|
|
114
815
|
The public route is one `RenderFeature<FrameData>` through the active RenderGraph
|
|
@@ -207,6 +908,8 @@ index path, not a second schema.
|
|
|
207
908
|
import {
|
|
208
909
|
ANTIALIAS_TAA,
|
|
209
910
|
Atmosphere,
|
|
911
|
+
BLOOM_DISABLED,
|
|
912
|
+
CAMERA_EXPOSURE_MODE_MANUAL,
|
|
210
913
|
Camera,
|
|
211
914
|
Fog,
|
|
212
915
|
type FramePlan,
|
|
@@ -229,6 +932,17 @@ code-specific `detail`; they do not create a second registry or silently pick
|
|
|
229
932
|
the first entity. Frame facts contain IDs, revisions, and POD values only, not
|
|
230
933
|
textures, buffers, devices, or other live GPU objects.
|
|
231
934
|
|
|
935
|
+
An Atmosphere also requires exactly one `DirectionalLight` sun. A direct render
|
|
936
|
+
extraction with no sun routes `SunCardinalityError` through the World-owned
|
|
937
|
+
error boundary (`console.error` with the structured error) before extraction
|
|
938
|
+
throws; that internal route is deliberately separate from `app.onError`.
|
|
939
|
+
`app.onError` receives errors returned by the App frame loop and renderer event
|
|
940
|
+
stream. Inspect `error.code` and `error.detail.value`, repair the owning World,
|
|
941
|
+
then retry the same extraction or the next frame. For zero suns, spawn one
|
|
942
|
+
`DirectionalLight`; for multiple suns, remove the extras so the Atmosphere has
|
|
943
|
+
one owner. The renderer does not manufacture a fallback sun or retain a
|
|
944
|
+
partially selected environment.
|
|
945
|
+
|
|
232
946
|
The Standard graph renders an Atmosphere source as a 128-by-128, six-face
|
|
233
947
|
`rgba16float` sky cube and a background pass before scene geometry. The selected
|
|
234
948
|
DirectionalLight supplies the sun direction, color, and illuminance; the sun
|
|
@@ -240,6 +954,52 @@ Its color and intensity remain the lighting controls; no Skylight means no
|
|
|
240
954
|
global ambient contribution. An explicit equirect keeps its image source.
|
|
241
955
|
Use a neutral Skylight tint when comparing local captures against the sky.
|
|
242
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
|
+
|
|
967
|
+
### Atmosphere controls and fixed-exposure comparisons
|
|
968
|
+
|
|
969
|
+
`Atmosphere` keeps the visible disc and the analytic sky response as separate
|
|
970
|
+
controls. `circumsolarStrength` and `circumsolarWidth` affect only the Mie
|
|
971
|
+
forward lobe used while producing the cached sky cube. The defaults preserve the
|
|
972
|
+
baseline response; `sunAngularRadius` still controls the separate disc in the
|
|
973
|
+
background pass and does not grow a halo.
|
|
974
|
+
|
|
975
|
+
| Field | Default | Valid range | Meaning |
|
|
976
|
+
|:--|:--:|:--|:--|
|
|
977
|
+
| `circumsolarStrength` | `1` | `[0, 4]` | Multiplier for the circumsolar Mie lobe |
|
|
978
|
+
| `circumsolarWidth` | `1` | `[0.25, 4]` | Relative lobe width; larger values broaden it, smaller values narrow it |
|
|
979
|
+
| `sunAngularRadius` | `0.004675` rad | `[0, +∞)` | Radius of the separate visible sun disc; `0` disables the disc |
|
|
980
|
+
|
|
981
|
+
For an angular or sunset sweep, hold the camera exposure in manual mode and
|
|
982
|
+
disable Bloom so the measurement belongs to the Atmosphere response. Vary the
|
|
983
|
+
two circumsolar fields while leaving `sunAngularRadius` unchanged:
|
|
984
|
+
|
|
985
|
+
```ts
|
|
986
|
+
world.set(atmosphere, Atmosphere, {
|
|
987
|
+
circumsolarStrength: 1.5,
|
|
988
|
+
circumsolarWidth: 2,
|
|
989
|
+
sunAngularRadius: 0.004675,
|
|
990
|
+
}).unwrap();
|
|
991
|
+
world.set(camera, Camera, {
|
|
992
|
+
exposureMode: CAMERA_EXPOSURE_MODE_MANUAL,
|
|
993
|
+
exposure: 1,
|
|
994
|
+
bloom: BLOOM_DISABLED,
|
|
995
|
+
}).unwrap();
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
The fixed exposure and Bloom setting are comparison setup, not additional
|
|
999
|
+
Atmosphere state. Use the renderer's submitted frame or a real Browser/Dawn
|
|
1000
|
+
readback for visual claims; source values and a sky-cube upload do not prove a
|
|
1001
|
+
pixel response.
|
|
1002
|
+
|
|
243
1003
|
The cube has 786,432 texture payload bytes. Its 16-square irradiance cube and
|
|
244
1004
|
64-square, five-mip prefilter add 274,176 bytes; parameters and vertices total
|
|
245
1005
|
1,560 bytes, before backend allocation alignment. DeviceScope owns one cached
|
|
@@ -347,6 +1107,315 @@ before publication; an invalid update keeps the renderer's last-known-good
|
|
|
347
1107
|
fog frame and exposes the structured failure through the existing inspection
|
|
348
1108
|
path. There is no app-local fog state.
|
|
349
1109
|
|
|
1110
|
+
### VolumetricFog authoring, World time, and recovery
|
|
1111
|
+
|
|
1112
|
+
`VolumetricFog` is an authored, one-owner component backed by a linear 3D
|
|
1113
|
+
`TextureAsset`. The selected light must be a live same-World
|
|
1114
|
+
`DirectionalLight`, `PointLight`, or `SpotLight`; Point/Spot selection also
|
|
1115
|
+
requires the corresponding `Transform`. Validate the authoring POD before
|
|
1116
|
+
spawning the component, then let the existing RenderSystem extract it. The
|
|
1117
|
+
World `Time` resource is the only simulation clock: `world.update(delta)`
|
|
1118
|
+
advances it, and the volume parameter block receives that elapsed value.
|
|
1119
|
+
|
|
1120
|
+
```ts
|
|
1121
|
+
import { Time, World } from '@forgeax/engine-ecs';
|
|
1122
|
+
import {
|
|
1123
|
+
Atmosphere,
|
|
1124
|
+
BLOOM_DISABLED,
|
|
1125
|
+
CAMERA_EXPOSURE_MODE_MANUAL,
|
|
1126
|
+
Camera,
|
|
1127
|
+
DirectionalLight,
|
|
1128
|
+
VolumetricFog,
|
|
1129
|
+
extractVolumetricFog,
|
|
1130
|
+
perspective,
|
|
1131
|
+
resolveSelectedVolumetricLight,
|
|
1132
|
+
validateVolumetricFog,
|
|
1133
|
+
type VolumetricFogAuthoring,
|
|
1134
|
+
type Renderer,
|
|
1135
|
+
} from '@forgeax/engine-render';
|
|
1136
|
+
import { Transform } from '@forgeax/engine-scene';
|
|
1137
|
+
import type { TextureAsset } from '@forgeax/engine-types';
|
|
1138
|
+
|
|
1139
|
+
const world = new World();
|
|
1140
|
+
const densityAsset: TextureAsset = {
|
|
1141
|
+
kind: 'texture',
|
|
1142
|
+
shape: { viewDimension: '3d', extent: { width: 4, height: 4, depth: 4 } },
|
|
1143
|
+
format: 'r8unorm',
|
|
1144
|
+
colorSpace: 'linear',
|
|
1145
|
+
mips: { kind: 'none' },
|
|
1146
|
+
data: new Uint8Array(4 * 4 * 4).fill(32),
|
|
1147
|
+
};
|
|
1148
|
+
const density = world.allocSharedRef('TextureAsset', densityAsset);
|
|
1149
|
+
// A production project normally obtains this TextureAsset from its existing
|
|
1150
|
+
// Pack/Catalog load path, then allocates the same shared handle in this World.
|
|
1151
|
+
// The inline payload keeps this authoring example executable and deterministic.
|
|
1152
|
+
const camera = world.spawn(
|
|
1153
|
+
{ component: Transform, data: { pos: [0, 0, 3] } },
|
|
1154
|
+
{ component: Camera, data: {
|
|
1155
|
+
...perspective({ fov: Math.PI / 4, aspect: 16 / 9 }),
|
|
1156
|
+
exposureMode: CAMERA_EXPOSURE_MODE_MANUAL,
|
|
1157
|
+
exposure: 1,
|
|
1158
|
+
bloom: BLOOM_DISABLED,
|
|
1159
|
+
} },
|
|
1160
|
+
).unwrap();
|
|
1161
|
+
const sun = world.spawn({
|
|
1162
|
+
component: DirectionalLight,
|
|
1163
|
+
data: { direction: [-0.4, -0.8, -0.3], castShadow: true },
|
|
1164
|
+
}).unwrap();
|
|
1165
|
+
const atmosphere = world.spawn({
|
|
1166
|
+
component: Atmosphere,
|
|
1167
|
+
data: { circumsolarStrength: 1, circumsolarWidth: 1, sunAngularRadius: 0.004675 },
|
|
1168
|
+
}).unwrap();
|
|
1169
|
+
|
|
1170
|
+
const authored: VolumetricFogAuthoring = {
|
|
1171
|
+
light: sun,
|
|
1172
|
+
density: {
|
|
1173
|
+
guid: 'density-demo',
|
|
1174
|
+
generation: 1,
|
|
1175
|
+
shape: { viewDimension: '3d', extent: { width: 4, height: 4, depth: 4 } },
|
|
1176
|
+
format: 'r8unorm',
|
|
1177
|
+
colorSpace: 'linear',
|
|
1178
|
+
},
|
|
1179
|
+
bounds: { min: [-1, -1, -1], max: [1, 1, 1] },
|
|
1180
|
+
extinction: [0.2, 0.2, 0.2],
|
|
1181
|
+
albedo: [0.8, 0.8, 0.8],
|
|
1182
|
+
emission: [0, 0, 0],
|
|
1183
|
+
anisotropy: 0,
|
|
1184
|
+
maxDistance: 50,
|
|
1185
|
+
};
|
|
1186
|
+
const checked = validateVolumetricFog(authored);
|
|
1187
|
+
if (!checked.ok) throw checked.error;
|
|
1188
|
+
const extracted = extractVolumetricFog([checked.value]);
|
|
1189
|
+
if (!extracted.ok) throw extracted.error;
|
|
1190
|
+
const selected = resolveSelectedVolumetricLight(world, sun);
|
|
1191
|
+
if (selected.status === 'unresolved') {
|
|
1192
|
+
throw new Error(`${selected.reason}: ${selected.hint}`);
|
|
1193
|
+
}
|
|
1194
|
+
world.spawn({
|
|
1195
|
+
component: VolumetricFog,
|
|
1196
|
+
data: {
|
|
1197
|
+
light: sun,
|
|
1198
|
+
density,
|
|
1199
|
+
boundsMin: authored.bounds.min,
|
|
1200
|
+
boundsMax: authored.bounds.max,
|
|
1201
|
+
extinction: authored.extinction,
|
|
1202
|
+
albedo: authored.albedo,
|
|
1203
|
+
emission: authored.emission,
|
|
1204
|
+
anisotropy: authored.anisotropy,
|
|
1205
|
+
maxDistance: authored.maxDistance,
|
|
1206
|
+
},
|
|
1207
|
+
}).unwrap();
|
|
1208
|
+
|
|
1209
|
+
world.update(1 / 60).unwrap();
|
|
1210
|
+
const elapsed = world.getResource(Time).elapsed; // the projected volume time
|
|
1211
|
+
void camera;
|
|
1212
|
+
void atmosphere;
|
|
1213
|
+
void elapsed;
|
|
1214
|
+
```
|
|
1215
|
+
|
|
1216
|
+
If validation fails, read its structured `code`, `expected`, `hint`, and typed
|
|
1217
|
+
`detail`. If light resolution is unresolved, read its `reason`, `expected`,
|
|
1218
|
+
`actual`, and `hint`; repair the owning authoring field, and extract again.
|
|
1219
|
+
Do not manufacture a light or a density fallback. After a submitted frame,
|
|
1220
|
+
`renderer.inspect().volumetricFog` reports `status`, `resourceStage`, the
|
|
1221
|
+
selected-light facts, and candidate/LKG generations. For a renderer device
|
|
1222
|
+
loss, wait for the host recovery boundary, call `await renderer.recover()`,
|
|
1223
|
+
then submit the same World again and inspect the replacement generation:
|
|
1224
|
+
|
|
1225
|
+
```ts
|
|
1226
|
+
import type { World } from '@forgeax/engine-ecs';
|
|
1227
|
+
import type { Renderer } from '@forgeax/engine-render';
|
|
1228
|
+
|
|
1229
|
+
// After the existing createRenderer(canvas) happy path returns a Renderer,
|
|
1230
|
+
// call `await recoverAndDraw(renderer, world)` with the World above.
|
|
1231
|
+
async function recoverAndDraw(renderer: Renderer, world: World): Promise<void> {
|
|
1232
|
+
const attached = renderer.attach(world);
|
|
1233
|
+
if (!attached.ok) throw attached.error;
|
|
1234
|
+
const lease = attached.value;
|
|
1235
|
+
if (renderer.state() === 'device-lost') {
|
|
1236
|
+
const recovered = await renderer.recover();
|
|
1237
|
+
if (!recovered.ok) throw recovered.error;
|
|
1238
|
+
}
|
|
1239
|
+
const retry = renderer.draw({
|
|
1240
|
+
leases: [lease],
|
|
1241
|
+
camera: { lease },
|
|
1242
|
+
environment: { lease },
|
|
1243
|
+
});
|
|
1244
|
+
if (!retry.ok) throw retry.error;
|
|
1245
|
+
const completed = await retry.value.completed;
|
|
1246
|
+
if (!completed.ok) throw completed.error;
|
|
1247
|
+
const volume = renderer.inspect().volumetricFog;
|
|
1248
|
+
console.log(volume?.status, volume?.resourceStage, volume?.generation);
|
|
1249
|
+
}
|
|
1250
|
+
```
|
|
1251
|
+
|
|
1252
|
+
The recovery call rebuilds renderer-owned resources; it is not a second volume
|
|
1253
|
+
clock or registry. Pausing the simulation means withholding `world.update`.
|
|
1254
|
+
Equal time and normal monotonic World ticks up to 100 ms retain eligible
|
|
1255
|
+
volume history. Clock rollback, a larger jump, or changed time availability
|
|
1256
|
+
invalidates it. Camera, light, medium, size and resource changes still invalidate
|
|
1257
|
+
history. Integration and shadow visibility use stable cell centers, so a reset
|
|
1258
|
+
does not expose render-frame random noise. The ray uses 96 base intervals;
|
|
1259
|
+
segments within four interval lengths of a selected punctual source use four
|
|
1260
|
+
midpoints to resolve its inverse-square peak and spot cone. This local refinement
|
|
1261
|
+
keeps the density expression and packed shadow resolution unchanged. A real GPU
|
|
1262
|
+
punctual-scattering oracle protects spatial accuracy alongside the continuous-frame
|
|
1263
|
+
browser check; homogeneous transmittance alone cannot detect light-band artifacts.
|
|
1264
|
+
History is clipped to the current
|
|
1265
|
+
neighborhood and its weight decays with elapsed World time (at most 0.875 per
|
|
1266
|
+
frame, about 87 ms half-life at 60 Hz or slower), limiting trails from animated
|
|
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.
|
|
1418
|
+
|
|
350
1419
|
Point shadows use one renderer-owned cube-array `ShadowAtlas`. The public
|
|
351
1420
|
`SHADOW_ATLAS_DEFAULT_FACE_SIZE` and `SHADOW_ATLAS_DEFAULT_LAYERS` constants
|
|
352
1421
|
are the only capacity owner; extract assigns `shadowAtlasLayer: -1` to
|
|
@@ -496,7 +1565,7 @@ switch (spatial.status) {
|
|
|
496
1565
|
`history.state`, `history.bytes`, `history.resetCount`, `passRoster`, and
|
|
497
1566
|
`fallbackSource` are bounded POD facts. They are evidence about the current
|
|
498
1567
|
owner state, not an acceptance claim: M1/M2 remain an implementation checkpoint
|
|
499
|
-
until the paired Browser/Dawn
|
|
1568
|
+
until the paired Browser/Dawn 60-frame carrier, readback, falsifiers, and
|
|
500
1569
|
performance gates are present.
|
|
501
1570
|
|
|
502
1571
|
SSR temporal feedback and presentation use distinct coordinates without another
|
|
@@ -749,7 +1818,7 @@ that a physical Browser/Dawn workload or timing gate completed.
|
|
|
749
1818
|
`renderer.inspect()` and `renderer.observe()` never carry ROI pixels, scanlines,
|
|
750
1819
|
brightness or level metrics. The hello-fxaa `hello-fxaa/dark-gradient/v1`
|
|
751
1820
|
fixture report owns those fields and records the fixed camera, low-light scene,
|
|
752
|
-
800x600 resolution,
|
|
1821
|
+
800x600 resolution, 60-frame schedule, ROI, scanline, backend/lane, surface
|
|
753
1822
|
formats, pixel hash, validation errors and mutation falsifiers.
|
|
754
1823
|
|
|
755
1824
|
The report gates are explicit: unique-color ratio $r_U \ge 0.75$, level ratio
|
|
@@ -911,20 +1980,131 @@ not from inspecting shader source text for a particular output structure.
|
|
|
911
1980
|
|
|
912
1981
|
## Motion Blur temporal consumer
|
|
913
1982
|
|
|
914
|
-
The Standard camera
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1983
|
+
The Standard camera enables Motion Blur by carrying the presence-enabled
|
|
1984
|
+
component. Authoring stays bounded and validated at the component boundary:
|
|
1985
|
+
|
|
1986
|
+
```ts
|
|
1987
|
+
import { MotionBlur } from '@forgeax/engine-render';
|
|
1988
|
+
|
|
1989
|
+
const result = world.addComponent(cameraEntity, {
|
|
1990
|
+
component: MotionBlur,
|
|
1991
|
+
data: { shutterAngle: 180, maxRadiusPixels: 32, sampleCount: 8, targetFps: 60 },
|
|
1992
|
+
});
|
|
1993
|
+
if (!result.ok) throw result.error;
|
|
1994
|
+
```
|
|
1995
|
+
|
|
1996
|
+
| Input / state | Contract |
|
|
1997
|
+
|:--|:--|
|
|
1998
|
+
| Parameters | `shutterAngle ∈ [0, 360]`, `maxRadiusPixels ∈ [0, 64]`, integer `sampleCount ∈ [4, 16]`, integer `targetFps ∈ [0, 240]`; zero shutter/radius is zero work. |
|
|
1999
|
+
| Temporal source | One renderer-owned `scene-data-temporal` sampled target. Motion validity and color reactivity are separate fields. |
|
|
2000
|
+
| Post order | TAA resolve → Motion Blur → auto exposure/Bloom/tone output. Motion Blur never writes TAA history. |
|
|
2001
|
+
| WebGPU/native lane | One 16×16 tile summary dispatch plus one fused reconstruction dispatch. Long vectors use an even-pixel half-resolution source lattice with depth guidance. |
|
|
2002
|
+
| Bounded work | Effective color tiers are exactly `0|4|8|16`; metadata probes are fixed and color work is linear in the tier. There is no radius-squared neighborhood, per-pixel scatter, or atomic accumulation. |
|
|
2003
|
+
| WebGL2 fallback | One directional raster gather, explicitly reported as `raster-limited`; it does not claim compute quality or timing parity. |
|
|
2004
|
+
|
|
2005
|
+
The authored integer sample count maps to one of the four execution tiers:
|
|
2006
|
+
`4..7 → 4`, `8..15 → 8`, and `16 → 16`; a zero-work bypass is `0`. The
|
|
2007
|
+
selected tier is the total color budget for every direction, including a
|
|
2008
|
+
bounded fallback when all candidates are rejected. There is no implicit
|
|
2009
|
+
center tap added to the budget.
|
|
2010
|
+
|
|
2011
|
+
For an uncovered moving support, accepted foreground coverage reconstructs a
|
|
2012
|
+
bounded portion of an opaque silhouette edge rather than averaging rejected
|
|
2013
|
+
support as black. The matching empty-background trail is attenuated by the
|
|
2014
|
+
same fixed factor, so the correction redistributes energy and does not add
|
|
2015
|
+
another sample. If no source is accepted, the receiver's center color remains
|
|
2016
|
+
the conservative fallback. A recovered midpoint anchor is still only partial
|
|
2017
|
+
evidence. Compute and raster-limited lanes share the rule without an extra
|
|
2018
|
+
pass or radius-sized neighborhood.
|
|
2019
|
+
|
|
2020
|
+
Reset and recovery remain observable through the existing temporal owner:
|
|
2021
|
+
|
|
2022
|
+
```ts
|
|
2023
|
+
const inspection = renderer.inspect();
|
|
2024
|
+
if (
|
|
2025
|
+
inspection.motionBlur?.status === 'reset' &&
|
|
2026
|
+
inspection.motionBlur.resetReason === 'time-discontinuity' &&
|
|
2027
|
+
inspection.temporal.resetReason === 'time-discontinuity'
|
|
2028
|
+
) {
|
|
2029
|
+
// The next successful draw commits the new baseline; retry the same renderer.
|
|
2030
|
+
renderer.draw(frameInput());
|
|
2031
|
+
}
|
|
2032
|
+
```
|
|
2033
|
+
|
|
2034
|
+
After a rejected graph or queue submit, `motionBlur.lastFailure` is
|
|
2035
|
+
`'submit-failure'` while the previous successful temporal view remains the
|
|
2036
|
+
baseline. A `'scene-data-unavailable'` failure means the shared
|
|
2037
|
+
`standard-scene-data` producer or its `rgba16float` capability was not
|
|
2038
|
+
admitted; repair that producer/capability and retry the same frame. It never
|
|
2039
|
+
creates a private Motion Blur history target.
|
|
2040
|
+
|
|
2041
|
+
The renderer carries the raw host render sample timestamp through App/Worker
|
|
2042
|
+
and accepts it only after a successful submit. A gap greater than `100 ms`
|
|
2043
|
+
creates a `time-discontinuity` reset; a normal `30 Hz` interval remains valid,
|
|
2044
|
+
and a failed submit cannot advance the accepted temporal clock. Pause/resume
|
|
2045
|
+
also marks the next frame as a new baseline. Instance pose updates preserve
|
|
2046
|
+
the last submitted transform. Collection replacement mints new identity
|
|
2047
|
+
generations by default; a producer that keeps an instance through compaction
|
|
2048
|
+
or reorder can pass its prior generation tokens in the new order, allowing the
|
|
2049
|
+
record stage to pair each current transform with the real prior transform.
|
|
2050
|
+
|
|
2051
|
+
`renderer.inspect().motionBlur` exposes detached POD facts: status (`off`,
|
|
2052
|
+
`active`, `reset`, `limited`, or `invalid`), selected lane, effective tier,
|
|
2053
|
+
pass count, reset reason, last failure, and `historyWrites: 0`. Invalid public
|
|
2054
|
+
parameters return `MotionBlurValidationError` with code
|
|
2055
|
+
`motion-blur-invalid-params`; repair the named component field or capability
|
|
2056
|
+
and retry the same frame. The typed cause is preserved by the public draw
|
|
2057
|
+
result, so a caller can repair the named field without parsing `Error.message`:
|
|
2058
|
+
|
|
2059
|
+
```ts
|
|
2060
|
+
const result = renderer.draw(frameInput);
|
|
2061
|
+
if (!result.ok && result.error.code === 'motion-blur-invalid-params') {
|
|
2062
|
+
console.error(result.error.detail.field, result.error.detail.value);
|
|
2063
|
+
// Repair the component field, then retry the same frame.
|
|
2064
|
+
}
|
|
2065
|
+
```
|
|
2066
|
+
|
|
2067
|
+
The hello-taa carrier contains the Dawn, Browser, WebGL2, RhiNull, performance,
|
|
2068
|
+
and source-level falsifier paths.
|
|
2069
|
+
|
|
2070
|
+
## Multiscale HDR Bloom
|
|
2071
|
+
|
|
2072
|
+
Bloom is a Camera-owned Standard post feature. `bloom` selects the enabled
|
|
2073
|
+
path, while `bloomIntensity` is bounded to `[0, 8]`, `bloomThreshold` to
|
|
2074
|
+
`[0, 65504]`, `bloomSoftKnee` to `[0, 1]`, and `bloomScatter` to `[0, 0.95]`.
|
|
2075
|
+
The defaults are intensity `1`, threshold `1`, soft knee `0.5`, and scatter
|
|
2076
|
+
`0.7`; the disabled value and intensity `0` are exact zero-work cases.
|
|
2077
|
+
|
|
2078
|
+
Game authors import this contract through the public umbrella facade:
|
|
2079
|
+
|
|
2080
|
+
```ts
|
|
2081
|
+
import {
|
|
2082
|
+
BLOOM_ENABLED,
|
|
2083
|
+
Camera,
|
|
2084
|
+
TONEMAP_REINHARD_EXTENDED,
|
|
2085
|
+
} from '@forgeax/engine/render';
|
|
2086
|
+
```
|
|
2087
|
+
|
|
2088
|
+
The physical `@forgeax/engine-render` package remains the repository ownership
|
|
2089
|
+
unit; it is not the recommended consumer import path.
|
|
2090
|
+
|
|
2091
|
+
An enabled frame stays in the linear HDR domain and uses one typed graph:
|
|
2092
|
+
five ceil-halved downsample levels `D0..D4`, four tent upsample levels
|
|
2093
|
+
`U3..U0`, then one HDR composite before tone mapping. `D0` applies the soft
|
|
2094
|
+
threshold per source texel and weights each texel by its exact source/target
|
|
2095
|
+
rectangle overlap, including odd extents. `D1..D4` use the shared 13-tap
|
|
2096
|
+
reduction; each upsample blends the current level with the next coarser level
|
|
2097
|
+
using the authored scatter. Composite adds only RGB bloom and preserves the
|
|
2098
|
+
scene alpha.
|
|
2099
|
+
|
|
2100
|
+
`renderer.inspect().bloom` is detached POD evidence from the compiled graph.
|
|
2101
|
+
Its level dimensions, `targetBytes`, pass topology, and
|
|
2102
|
+
`residentChildBytes` come from validated descriptors and active resource
|
|
2103
|
+
ownership, not from an expected-size calculation. A missing, forged, or
|
|
2104
|
+
invalid descriptor graph reports `invalid`; an off camera reports `empty` and
|
|
2105
|
+
zero target/resident bytes. Candidate generations remain behind the existing
|
|
2106
|
+
LKG fence and are released only after the submit fence; device recovery
|
|
2107
|
+
rehydrates the same Bloom bundle before a new receipt is accepted.
|
|
928
2108
|
|
|
929
2109
|
> [!IMPORTANT]
|
|
930
2110
|
> Render consumes the effective MaterialAsset snapshot produced by extract. Each texture slot carries its own coordinate set and transform into the built-in PBR binding layout; render records do not reinterpret authoring fields or manufacture shader artifacts. The effective `passes` are already validated.
|
|
@@ -962,7 +2142,7 @@ base-only root keeps `forward + deferred + shadow`, while declaring any
|
|
|
962
2142
|
second-stage layer or physical texture selects `forward + shadow`.
|
|
963
2143
|
|
|
964
2144
|
```ts
|
|
965
|
-
import { Materials } from '@forgeax/engine
|
|
2145
|
+
import { Materials } from '@forgeax/engine/render';
|
|
966
2146
|
|
|
967
2147
|
const material = Materials.standard({
|
|
968
2148
|
baseColor: [0.72, 0.48, 0.22, 1],
|
|
@@ -1065,113 +2245,75 @@ infer state from URLs, or repair a producer failure. Aggregate counts are
|
|
|
1065
2245
|
derived from the observations, so an AI can inspect first, repair or recook the
|
|
1066
2246
|
producer, and retry without guessing at hidden renderer state.
|
|
1067
2247
|
|
|
1068
|
-
###
|
|
2248
|
+
### World-owned Instances and CPU bounds
|
|
1069
2249
|
|
|
1070
|
-
`Instances`
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
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.
|
|
1074
2253
|
|
|
1075
2254
|
```ts
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
matrices[offset + 10] = 1;
|
|
1084
|
-
matrices[offset + 15] = 1;
|
|
1085
|
-
matrices[offset + 12] = (index % 100) * 2;
|
|
1086
|
-
matrices[offset + 14] = Math.floor(index / 100) * 2;
|
|
1087
|
-
}
|
|
1088
|
-
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;
|
|
1089
2262
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
transforms: identityMatrices(instanceCount),
|
|
1093
|
-
}).unwrap();
|
|
1094
|
-
world.spawn(
|
|
2263
|
+
const entity = world.spawn(
|
|
2264
|
+
{ component: Transform, data: {} },
|
|
1095
2265
|
{ component: MeshFilter, data: { assetHandle: cube } },
|
|
1096
2266
|
{ component: MeshRenderer, data: {} },
|
|
1097
|
-
{ component: Instances, data: {
|
|
1098
|
-
);
|
|
1099
|
-
|
|
1100
|
-
const patch = identityMatrices(1);
|
|
1101
|
-
patch[12] = 4;
|
|
1102
|
-
renderer.instances.update(collection.collectionId, {
|
|
1103
|
-
start: 0,
|
|
1104
|
-
transforms: patch,
|
|
1105
|
-
});
|
|
2267
|
+
{ component: Instances, data: { transforms } },
|
|
2268
|
+
).unwrap();
|
|
1106
2269
|
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
const current = renderer.instances.inspect(collection.collectionId).unwrap();
|
|
1110
|
-
const detached = renderer.instances.snapshot(collection.collectionId).unwrap();
|
|
1111
|
-
// A read-only observation does not advance the renderer upload cursor.
|
|
1112
|
-
const observed = renderer.instances.peek(collection.collectionId).unwrap();
|
|
1113
|
-
renderer.instances.release(collection.collectionId);
|
|
2270
|
+
transforms[12] = 4;
|
|
2271
|
+
world.set(entity, Instances, { transforms }).unwrap();
|
|
1114
2272
|
```
|
|
1115
2273
|
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
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.
|
|
1136
2297
|
|
|
1137
2298
|
#### Instance inspection and recovery
|
|
1138
2299
|
|
|
1139
|
-
`renderer.inspect().instanceCollections`
|
|
1140
|
-
|
|
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.
|
|
1141
2303
|
|
|
1142
2304
|
| Field | Meaning |
|
|
1143
2305
|
|:--|:--|
|
|
1144
|
-
| `collectionId`, `count`, `
|
|
1145
|
-
| `residentGeneration`, `lane` | Device generation and
|
|
1146
|
-
| `
|
|
1147
|
-
| `
|
|
1148
|
-
| `
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
```ts
|
|
1156
|
-
const result = renderer.instances.create({ transforms: new Float32Array(17) });
|
|
1157
|
-
if (!result.ok) {
|
|
1158
|
-
const { code, expected, hint, detail, facts } = result.error;
|
|
1159
|
-
// facts expose requestedBytes, supportedBytes, backend, owner, cause, and
|
|
1160
|
-
// recovery; detail retains operation metadata. Repair the payload and retry.
|
|
1161
|
-
void code;
|
|
1162
|
-
void expected;
|
|
1163
|
-
void hint;
|
|
1164
|
-
void detail;
|
|
1165
|
-
void facts;
|
|
1166
|
-
}
|
|
1167
|
-
```
|
|
1168
|
-
|
|
1169
|
-
After a `device-lost` transition, call `await renderer.recover()` and retry the
|
|
1170
|
-
same draw request. The renderer drops generation-owned residents, keeps the
|
|
1171
|
-
canonical collection, and uploads every matrix into the new resident before
|
|
1172
|
-
recording; it does not reuse an old partial dirty range for uninitialized GPU
|
|
1173
|
-
memory. If inspection reports `unavailable`, follow its `error.detail.recovery`
|
|
1174
|
-
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.
|
|
1175
2317
|
|
|
1176
2318
|
The large-instance smoke exercises the real Dawn path for all admitted sizes:
|
|
1177
2319
|
|
|
@@ -1189,7 +2331,7 @@ transport through the same production fixture and parameterizes the same three
|
|
|
1189
2331
|
populations:
|
|
1190
2332
|
|
|
1191
2333
|
```bash
|
|
1192
|
-
pnpm exec vitest run --config vitest.browser.config.ts --project=browser \
|
|
2334
|
+
pnpm exec vitest run --config config/vitest.browser.config.ts --project=browser \
|
|
1193
2335
|
apps/parity/instancing-static/src/__tests__/instances.browser.test.ts
|
|
1194
2336
|
```
|
|
1195
2337
|
|
|
@@ -1350,7 +2492,9 @@ same forward-only policy. This section is about the Standard lane, not the
|
|
|
1350
2492
|
separate Points/Lines admission rule above. See the
|
|
1351
2493
|
[`shader Surface contract`](../shader/README.md#standard-surface-contract) and
|
|
1352
2494
|
the [`game-3d` import example](../../templates/game-3d/README.md#import-first-surface-material-example)
|
|
1353
|
-
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.
|
|
1354
2498
|
|
|
1355
2499
|
The compiler and renderer share one pure `StandardLayerPlan`:
|
|
1356
2500
|
`deriveStandardLayerPlan(effectiveParameters)`. It is the only derivation used
|
|
@@ -1412,8 +2556,8 @@ separate cache/recovery authority.
|
|
|
1412
2556
|
> Owner: render vocabulary and the extract → prepare → record frame boundary. Runtime selects concrete services and calls this package; it does not re-own these tokens.
|
|
1413
2557
|
|
|
1414
2558
|
```ts
|
|
1415
|
-
import { Camera, MeshFilter, MeshRenderer } from '@forgeax/engine
|
|
1416
|
-
import { createRenderer } from '@forgeax/engine
|
|
2559
|
+
import { Camera, MeshFilter, MeshRenderer } from '@forgeax/engine/render';
|
|
2560
|
+
import { createRenderer } from '@forgeax/engine/runtime';
|
|
1417
2561
|
|
|
1418
2562
|
const renderer = await createRenderer(canvas);
|
|
1419
2563
|
const attached = renderer.attach(world);
|
|
@@ -1435,23 +2579,31 @@ state published by `World.update()`.
|
|
|
1435
2579
|
## Persistent render scene
|
|
1436
2580
|
|
|
1437
2581
|
Every attached World composition bootstraps one renderer-owned CPU projection,
|
|
1438
|
-
then consumes each World's
|
|
2582
|
+
then consumes each World's current membership and component block revisions. One identity-based update
|
|
1439
2583
|
publication merges content, root transforms, and instance changes, including
|
|
1440
2584
|
when all occur in the same frame. Its GPU projection compares affected matrix
|
|
1441
|
-
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
|
|
1442
2588
|
snapshot; unrelated gameplay component writes do not invalidate render state.
|
|
1443
2589
|
There is no exclusive transform/instance scene admission followed by a separate
|
|
1444
2590
|
rebuild implementation. Missing producer evidence causes conservative source
|
|
1445
2591
|
extraction into the same retained projection. World reordering and catalog
|
|
1446
2592
|
reconciliation preserve surviving slots and their submitted temporal history.
|
|
1447
2593
|
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
source
|
|
1454
|
-
|
|
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.
|
|
1455
2607
|
|
|
1456
2608
|
Instance collection revisions refresh only their consumers. Visibility and
|
|
1457
2609
|
parent changes refresh the affected subtree, and joint changes refresh the
|
|
@@ -1502,6 +2654,13 @@ The shortest public declaration uses the same `Materials.standard` producer as
|
|
|
1502
2654
|
the runtime and imported-skin carriers. `alphaCutoff` is the Alpha Mask
|
|
1503
2655
|
contract; `castShadow` publishes the matching ShadowCaster pass.
|
|
1504
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
|
+
|
|
1505
2664
|
```ts
|
|
1506
2665
|
import { Materials } from '@forgeax/engine/render';
|
|
1507
2666
|
|
|
@@ -1903,6 +3062,10 @@ consume the structured `error.detail` context.
|
|
|
1903
3062
|
Use `renderer.inspect()` for a read-only snapshot. Call `renderer.dispose()`
|
|
1904
3063
|
once or repeatedly; disposal is idempotent. Feature plans are assembled once
|
|
1905
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.
|
|
1906
3069
|
|
|
1907
3070
|
For a temporary presentation-owner handoff, call `renderer.releaseSurface()`.
|
|
1908
3071
|
It unconfigures the canvas and makes `draw()` fail closed without disposing the
|
|
@@ -1927,10 +3090,10 @@ ownership is [`@forgeax/engine-render-graph`](../render-graph/README.md).
|
|
|
1927
3090
|
Dynamic consumers use the same boundary explicitly:
|
|
1928
3091
|
|
|
1929
3092
|
```ts
|
|
1930
|
-
const { Camera, MeshFilter, MeshRenderer } = await import('@forgeax/engine
|
|
3093
|
+
const { Camera, MeshFilter, MeshRenderer } = await import('@forgeax/engine/render');
|
|
1931
3094
|
```
|
|
1932
3095
|
|
|
1933
|
-
`@forgeax/engine
|
|
3096
|
+
`@forgeax/engine/runtime` remains the host assembly entry for `createRenderer` and backend policy. Import `Materials` from `@forgeax/engine/render`. Runtime is not a compatibility barrel for render components.
|
|
1934
3097
|
|
|
1935
3098
|
Optional text, tilemap, and sprite authoring is intentionally isolated from the
|
|
1936
3099
|
base vocabulary:
|
|
@@ -1949,6 +3112,12 @@ consumer that composes sprites with an existing 3D game can call
|
|
|
1949
3112
|
`TransparentSort.configure(world, { mode: TransparentSort.layerY, yzAlpha: 1 })`;
|
|
1950
3113
|
it should not reach into `/internal`.
|
|
1951
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
|
+
|
|
1952
3121
|
The root barrel does not expose frame stores or extract/plan/record owners.
|
|
1953
3122
|
Applications contribute work through `RenderFeature` plans; the Standard
|
|
1954
3123
|
renderer owns graph compilation and submission.
|
|
@@ -2049,7 +3218,7 @@ before promoting that status.
|
|
|
2049
3218
|
|
|
2050
3219
|
The canonical paired carrier is `apps/hello/ssr`. It runs the same fixture at
|
|
2051
3220
|
`http://127.0.0.1:4173/?forgeax-evidence=ssr` and
|
|
2052
|
-
`dawn://hello/ssr?forgeax-evidence=ssr`, records
|
|
3221
|
+
`dawn://hello/ssr?forgeax-evidence=ssr`, records 60-frame identity-bound
|
|
2053
3222
|
readbacks, and keeps visual rows in the form `observed` / `verdict` /
|
|
2054
3223
|
`confidence`. Its performance lane derives the 1920x1080 descriptor and checks
|
|
2055
3224
|
it against `estimateSsrSpatialMemory`; timestamp or paired-lane absence remains
|
|
@@ -2309,19 +3478,24 @@ existing observable state; resource disposal is distinct from JavaScript GC.
|
|
|
2309
3478
|
|
|
2310
3479
|
### Volumetric density coordinates
|
|
2311
3480
|
|
|
2312
|
-
`VolumetricFog.density` is a linear
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
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
|
|
3483
|
+
same three scales used by the approved fog fixture, and uses World time for the
|
|
3484
|
+
continuous advection term. The renderer keeps the source texture resident; it
|
|
3485
|
+
does not rebuild noise data for each frame. Its source expression is mapped to a
|
|
3486
|
+
signed density and clamped only at the optical-depth boundary so Beer-Lambert
|
|
3487
|
+
extinction remains non-negative.
|
|
3488
|
+
|
|
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
|
|
3494
|
+
receive four local midpoints to resolve the inverse-square peak without making
|
|
3495
|
+
the whole ray uniformly expensive. Frame identity does not rotate density
|
|
3496
|
+
samples. Shadow visibility uses matching froxel centers without per-frame XY
|
|
3497
|
+
jitter or stochastic byte dithering; shared PCF shadow filtering remains in
|
|
3498
|
+
effect.
|
|
2325
3499
|
|
|
2326
3500
|
## Camera Depth of Field
|
|
2327
3501
|
|
|
@@ -2370,3 +3544,61 @@ orthographic camera stay on the normal camera snapshot path with structured
|
|
|
2370
3544
|
The Engine Preview/game capability lab uses this same component on its ordinary
|
|
2371
3545
|
mesh camera, and its settings control toggles component presence through the
|
|
2372
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.
|