@forgeax/engine-render 0.1.4 → 0.1.6
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 +130 -641
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/device-scope-lifecycle.unit.test.d.ts +2 -0
- package/dist/__tests__/device-scope-lifecycle.unit.test.d.ts.map +1 -0
- package/dist/__tests__/device-scope-stale-matrix.unit.test.d.ts +2 -0
- package/dist/__tests__/device-scope-stale-matrix.unit.test.d.ts.map +1 -0
- package/dist/__tests__/fullscreen-feature-plan.unit.test.d.ts +2 -0
- package/dist/__tests__/fullscreen-feature-plan.unit.test.d.ts.map +1 -0
- package/dist/__tests__/gpu-driven-indirect-raster-evidence.d.ts.map +1 -1
- package/dist/__tests__/gpu-driven-view-gpu-evidence.d.ts +6 -0
- package/dist/__tests__/gpu-driven-view-gpu-evidence.d.ts.map +1 -1
- package/dist/{gpu-driven/view-reference.d.ts → __tests__/gpu-driven-view-reference.d.ts} +4 -4
- package/dist/__tests__/gpu-driven-view-reference.d.ts.map +1 -0
- package/dist/__tests__/gpu-scene-render-graph-gpu.d.ts.map +1 -1
- package/dist/__tests__/ibl/face-uniforms.unit.test.d.ts +2 -0
- package/dist/__tests__/ibl/face-uniforms.unit.test.d.ts.map +1 -0
- package/dist/__tests__/morph-feature-plan.unit.test.d.ts +2 -0
- package/dist/__tests__/morph-feature-plan.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-feature-plan.test-d.d.ts +2 -0
- package/dist/__tests__/render-feature-plan.test-d.d.ts.map +1 -0
- package/dist/__tests__/render-feature-plan.unit.test.d.ts +2 -0
- package/dist/__tests__/render-feature-plan.unit.test.d.ts.map +1 -0
- package/dist/__tests__/ssao-config.unit.test.d.ts +2 -0
- package/dist/__tests__/ssao-config.unit.test.d.ts.map +1 -0
- package/dist/__tests__/standard-clustered-pipeline.unit.test.d.ts +2 -0
- package/dist/__tests__/standard-clustered-pipeline.unit.test.d.ts.map +1 -0
- package/dist/__tests__/standard-descriptors.test-d.d.ts +2 -0
- package/dist/__tests__/standard-descriptors.test-d.d.ts.map +1 -0
- package/dist/__tests__/standard-pipeline.dawn.test.d.ts +2 -0
- package/dist/__tests__/standard-pipeline.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/standard-pipeline.integration.test.d.ts +2 -0
- package/dist/__tests__/standard-pipeline.integration.test.d.ts.map +1 -0
- package/dist/__tests__/standard-profile-oracle.unit.test.d.ts +2 -0
- package/dist/__tests__/standard-profile-oracle.unit.test.d.ts.map +1 -0
- package/dist/__tests__/visibility-resource-short-circuit.unit.test.d.ts +2 -0
- package/dist/__tests__/visibility-resource-short-circuit.unit.test.d.ts.map +1 -0
- package/dist/{renderer/backend-selection.d.ts → assembly/backend-contract.d.ts} +15 -4
- package/dist/assembly/backend-contract.d.ts.map +1 -0
- package/dist/assembly/factory.d.ts +207 -0
- package/dist/assembly/factory.d.ts.map +1 -0
- package/dist/assembly/host-contract.d.ts +84 -0
- package/dist/assembly/host-contract.d.ts.map +1 -0
- package/dist/{renderer → assembly}/material/assembly.d.ts +0 -1
- package/dist/{renderer → assembly}/material/assembly.d.ts.map +1 -1
- package/dist/assembly/material/pipeline-projection.d.ts +30 -0
- package/dist/assembly/material/pipeline-projection.d.ts.map +1 -0
- package/dist/authoring.d.ts +10 -11
- package/dist/authoring.d.ts.map +1 -1
- package/dist/authoring.mjs +25 -3
- package/dist/authoring.mjs.map +1 -1
- package/dist/chunk-CQXZC5XI.mjs +182 -0
- package/dist/chunk-CQXZC5XI.mjs.map +1 -0
- package/dist/chunk-QC4VW3XJ.mjs +79 -0
- package/dist/chunk-QC4VW3XJ.mjs.map +1 -0
- package/dist/chunk-SCRSAK5B.mjs +480 -0
- package/dist/chunk-SCRSAK5B.mjs.map +1 -0
- package/dist/components/__tests__/schema-contract.unit.test.d.ts +2 -0
- package/dist/components/__tests__/schema-contract.unit.test.d.ts.map +1 -0
- package/dist/components/glyph-text.d.ts +1 -1
- package/dist/components/glyph-text.d.ts.map +1 -1
- package/dist/components/index.d.ts +2 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/instances.d.ts +1 -1
- package/dist/components/instances.d.ts.map +1 -1
- package/dist/components/light-helpers.d.ts +1 -1
- package/dist/components/light-helpers.d.ts.map +1 -1
- package/dist/components/mesh-filter.d.ts +7 -7
- package/dist/components/mesh-filter.d.ts.map +1 -1
- package/dist/components/scene-instance.d.ts +11 -11
- package/dist/components/skybox-background.d.ts +56 -0
- package/dist/components/skybox-background.d.ts.map +1 -0
- package/dist/components/skylight.d.ts +55 -0
- package/dist/components/skylight.d.ts.map +1 -0
- package/dist/components/sprite-animation.d.ts +2 -3
- package/dist/components/sprite-animation.d.ts.map +1 -1
- package/dist/components/sprite-instances.d.ts +1 -1
- package/dist/components/sprite-playback-mode.d.ts +6 -1
- package/dist/components/sprite-playback-mode.d.ts.map +1 -1
- package/dist/components/sprite-region-override.d.ts +2 -3
- package/dist/components/sprite-region-override.d.ts.map +1 -1
- package/dist/components/tile-layer.d.ts +5 -0
- package/dist/components/tile-layer.d.ts.map +1 -1
- package/dist/construct-renderer.d.ts +18 -1
- package/dist/construct-renderer.d.ts.map +1 -1
- package/dist/construct-renderer.mjs +27008 -4
- package/dist/construct-renderer.mjs.map +1 -1
- package/dist/debug-draw-glue.d.ts +2 -53
- package/dist/debug-draw-glue.d.ts.map +1 -1
- package/dist/device/device-scope.d.ts +83 -0
- package/dist/device/device-scope.d.ts.map +1 -0
- package/dist/{gpu-resource-store.d.ts → device/gpu-residency.d.ts} +91 -48
- package/dist/device/gpu-residency.d.ts.map +1 -0
- package/dist/device/resource-types.d.ts +8 -0
- package/dist/device/resource-types.d.ts.map +1 -0
- package/dist/engine-metrics.d.ts +2 -2
- package/dist/engine-metrics.d.ts.map +1 -1
- package/dist/errors/recover.d.ts +28 -1
- package/dist/errors/recover.d.ts.map +1 -1
- package/dist/errors/render.d.ts +249 -258
- package/dist/errors/render.d.ts.map +1 -1
- package/dist/features/fullscreen.d.ts +19 -0
- package/dist/features/fullscreen.d.ts.map +1 -0
- package/dist/features/host.d.ts +32 -26
- package/dist/features/host.d.ts.map +1 -1
- package/dist/features/morph/morph-feature.d.ts +39 -5
- package/dist/features/morph/morph-feature.d.ts.map +1 -1
- package/dist/features/plan.d.ts +182 -0
- package/dist/features/plan.d.ts.map +1 -0
- package/dist/features/prepared-gpu-work.d.ts +21 -17
- package/dist/features/prepared-gpu-work.d.ts.map +1 -1
- package/dist/features/prepared-graphics-store.d.ts +2 -1
- package/dist/features/prepared-graphics-store.d.ts.map +1 -1
- package/dist/features/prepared-graphics.d.ts +11 -21
- package/dist/features/prepared-graphics.d.ts.map +1 -1
- package/dist/features/render-graph-compute.d.ts +2 -2
- package/dist/features/render-graph-compute.d.ts.map +1 -1
- package/dist/features/render-graph-contribution.d.ts +4 -7
- package/dist/features/render-graph-contribution.d.ts.map +1 -1
- package/dist/features/render-graph-raster.d.ts +2 -2
- package/dist/features/render-graph-raster.d.ts.map +1 -1
- package/dist/features/render-graph-resources.d.ts +6 -6
- package/dist/features/render-graph-resources.d.ts.map +1 -1
- package/dist/features/targets.d.ts +5 -5
- package/dist/features/targets.d.ts.map +1 -1
- package/dist/features/types.d.ts +11 -190
- package/dist/features/types.d.ts.map +1 -1
- package/dist/features/vocabulary.d.ts +14 -0
- package/dist/features/vocabulary.d.ts.map +1 -0
- package/dist/fullscreen-post-process-pass.d.ts +4 -25
- package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
- package/dist/glyph-text-layout-system.d.ts +3 -2
- package/dist/glyph-text-layout-system.d.ts.map +1 -1
- package/dist/gpu-driven/batch-topology.d.ts +6 -75
- package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
- package/dist/gpu-driven/production-raster.d.ts +13 -112
- package/dist/gpu-driven/production-raster.d.ts.map +1 -1
- package/dist/gpu-driven/view-gpu.d.ts +10 -66
- package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
- package/dist/gpu-resource.d.ts +3 -3
- package/dist/gpu-scene-schema.d.ts +1 -94
- package/dist/gpu-scene-schema.d.ts.map +1 -1
- package/dist/gpu-scene.d.ts +7 -33
- package/dist/gpu-scene.d.ts.map +1 -1
- package/dist/hdrp-buffers.d.ts +3 -53
- package/dist/hdrp-buffers.d.ts.map +1 -1
- package/dist/ibl/IblPipelineCache.d.ts +74 -180
- package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
- package/dist/ibl/skylight-bind-group.d.ts +1 -1
- package/dist/index.d.ts +13 -43
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +165 -3
- package/dist/index.mjs.map +1 -1
- package/dist/inspection-types.d.ts +69 -0
- package/dist/inspection-types.d.ts.map +1 -0
- package/dist/instance-buffer-cache.d.ts +1 -4
- package/dist/instance-buffer-cache.d.ts.map +1 -1
- package/dist/internal.d.ts +1 -73
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.mjs +1 -43
- package/dist/internal.mjs.map +1 -1
- package/dist/lifecycle.d.ts +36 -2
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/materials.d.ts +0 -3
- package/dist/materials.d.ts.map +1 -1
- package/dist/mesh-material-bindings.d.ts +0 -1
- package/dist/mesh-material-bindings.d.ts.map +1 -1
- package/dist/pbr-pipeline.d.ts +75 -29
- package/dist/pbr-pipeline.d.ts.map +1 -1
- package/dist/pipeline/standard-forward-lane.d.ts +5 -0
- package/dist/pipeline/standard-forward-lane.d.ts.map +1 -0
- package/dist/pipeline/standard-pipeline.d.ts +31 -0
- package/dist/pipeline/standard-pipeline.d.ts.map +1 -0
- package/dist/pipeline/standard-profile.d.ts +40 -0
- package/dist/pipeline/standard-profile.d.ts.map +1 -0
- package/dist/pipeline-builder.d.ts +3 -3
- package/dist/pipeline-builder.d.ts.map +1 -1
- package/dist/pipeline-spec-types.d.ts +24 -0
- package/dist/pipeline-spec-types.d.ts.map +1 -0
- package/dist/pipeline-spec.d.ts +16 -155
- package/dist/pipeline-spec.d.ts.map +1 -1
- package/dist/plugin.d.ts +1 -14
- package/dist/plugin.d.ts.map +1 -1
- package/dist/post-process-errors.d.ts +18 -18
- package/dist/post-process-errors.d.ts.map +1 -1
- package/dist/prepare/prepared-graphics-resolver.d.ts +2 -5
- package/dist/prepare/prepared-graphics-resolver.d.ts.map +1 -1
- package/dist/record/__tests__/typed-frame-graph-plan.unit.test.d.ts +2 -0
- package/dist/record/__tests__/typed-frame-graph-plan.unit.test.d.ts.map +1 -0
- package/dist/record/frame-lighting.d.ts +7 -73
- package/dist/record/frame-lighting.d.ts.map +1 -1
- package/dist/record/frame-snapshot.d.ts +44 -44
- package/dist/record/frame-snapshot.d.ts.map +1 -1
- package/dist/record/frame-targets.d.ts +5 -38
- package/dist/record/frame-targets.d.ts.map +1 -1
- package/dist/record/frame.d.ts +64 -52
- package/dist/record/frame.d.ts.map +1 -1
- package/dist/record/helpers.d.ts +3 -2
- package/dist/record/helpers.d.ts.map +1 -1
- package/dist/record/main-pass-geometry.d.ts +5 -4
- package/dist/record/main-pass-geometry.d.ts.map +1 -1
- package/dist/record/main-pass-material.d.ts +90 -59
- package/dist/record/main-pass-material.d.ts.map +1 -1
- package/dist/record/main-pass-sprite-draws.d.ts +28 -3
- package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
- package/dist/record/main-pass.d.ts +1 -1
- package/dist/record/main-pass.d.ts.map +1 -1
- package/dist/record/mesh-ssbo.d.ts +276 -3
- package/dist/record/mesh-ssbo.d.ts.map +1 -1
- package/dist/record/render-context.d.ts +235 -0
- package/dist/record/render-context.d.ts.map +1 -0
- package/dist/record/shadow-pass.d.ts +15 -88
- package/dist/record/shadow-pass.d.ts.map +1 -1
- package/dist/record/skybox-post-pass.d.ts +1 -1
- package/dist/record/skybox-post-pass.d.ts.map +1 -1
- package/dist/record/typed-frame-graph.d.ts +29 -7
- package/dist/record/typed-frame-graph.d.ts.map +1 -1
- package/dist/record/view-ubo.d.ts +3 -3
- package/dist/record/view-ubo.d.ts.map +1 -1
- package/dist/render-contract.d.ts +416 -0
- package/dist/render-contract.d.ts.map +1 -0
- package/dist/render-data.d.ts.map +1 -1
- package/dist/render-graph-primitives.d.ts +14 -362
- package/dist/render-graph-primitives.d.ts.map +1 -1
- package/dist/render-pipeline.d.ts +25 -64
- package/dist/render-pipeline.d.ts.map +1 -1
- package/dist/render-system-extract.d.ts +107 -338
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts +21 -713
- package/dist/render-system.d.ts.map +1 -1
- package/dist/scene/render-scene-types.d.ts +58 -0
- package/dist/scene/render-scene-types.d.ts.map +1 -0
- package/dist/scene/render-scene.d.ts +105 -0
- package/dist/scene/render-scene.d.ts.map +1 -0
- package/dist/scene-instances/post-spawn-resolve-joints.d.ts +2 -5
- package/dist/scene-instances/post-spawn-resolve-joints.d.ts.map +1 -1
- package/dist/ssao-buffers.d.ts +22 -3
- package/dist/ssao-buffers.d.ts.map +1 -1
- package/dist/systems/skin-palette-allocator.d.ts +2 -4
- package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
- package/dist/systems/skin-palette-types.d.ts +14 -0
- package/dist/systems/skin-palette-types.d.ts.map +1 -0
- package/dist/systems/transparent-sort-config.d.ts +23 -2
- package/dist/systems/transparent-sort-config.d.ts.map +1 -1
- package/dist/tilemap-chunk-extract-system.d.ts +4 -3
- package/dist/tilemap-chunk-extract-system.d.ts.map +1 -1
- package/dist/typed-render-graph-primitives.d.ts +2 -4
- package/dist/typed-render-graph-primitives.d.ts.map +1 -1
- package/dist/typed-shadow-passes.d.ts +3 -3
- package/dist/typed-shadow-passes.d.ts.map +1 -1
- package/package.json +23 -23
- package/src/__tests__/backend-selection-rhi-capture.test-d.ts +1 -1
- package/src/__tests__/callback-inference.test-d.ts +6 -3
- package/src/__tests__/cluster-grid-default-surface.unit.test.ts +7 -4
- package/src/__tests__/construct-renderer-identity.unit.test.ts +15 -2
- package/src/__tests__/device-scope-lifecycle.unit.test.ts +151 -0
- package/src/__tests__/device-scope-stale-matrix.unit.test.ts +42 -0
- package/src/__tests__/direct-extract-consumer-contract.test-d.ts +8 -19
- package/src/__tests__/direct-light-snapshot.unit.test.ts +3 -13
- package/src/__tests__/factory-contract.integration.test.ts +158 -209
- package/src/__tests__/fallback-row-stride-surface.unit.test.ts +1 -4
- package/src/__tests__/frame-observation.contract.test.ts +17 -8
- package/src/__tests__/fullscreen-feature-plan.unit.test.ts +32 -0
- package/src/__tests__/glyph-text-layout-recovery.unit.test.ts +6 -11
- package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +9 -16
- package/src/__tests__/gpu-driven-production.integration.test.ts +32 -205
- package/src/__tests__/gpu-driven-scaling.unit.test.ts +4 -7
- package/src/__tests__/gpu-driven-view-gpu-evidence.ts +87 -8
- package/src/__tests__/gpu-driven-view-graph.integration.test.ts +170 -8
- package/src/{gpu-driven/view-reference.ts → __tests__/gpu-driven-view-reference.ts} +4 -3
- package/src/__tests__/gpu-driven-view.browser.test.ts +12 -1
- package/src/__tests__/gpu-driven-view.dawn.test.ts +12 -1
- package/src/__tests__/gpu-driven-view.unit.test.ts +11 -267
- package/src/__tests__/gpu-resource-store-stride.unit.test.ts +3 -3
- package/src/__tests__/gpu-scene-render-graph-gpu.ts +2 -3
- package/src/__tests__/gpu-scene.dawn.test.ts +2 -6
- package/src/__tests__/gpu-scene.unit.test.ts +10 -24
- package/src/__tests__/ibl/face-uniforms.unit.test.ts +46 -0
- package/src/__tests__/infinite-grid-no-vertex-probe.unit.test.ts +34 -66
- package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +14 -5
- package/src/__tests__/light-buffer-layout.unit.test.ts +4 -49
- package/src/__tests__/material-cooked-projection.unit.test.ts +2 -51
- package/src/__tests__/material-snapshot-mutation.integration.test.ts +10 -15
- package/src/__tests__/material-static-dynamic-route.unit.test.ts +4 -23
- package/src/__tests__/material-stride-surface.unit.test.ts +2 -2
- package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +7 -8
- package/src/__tests__/morph-culling-reentry.dawn.test.ts +29 -38
- package/src/__tests__/morph-feature-gpu-work.integration.test.ts +45 -43
- package/src/__tests__/morph-feature-plan.unit.test.ts +53 -0
- package/src/__tests__/morph-visual-evidence.integration.test.ts +47 -33
- package/src/__tests__/nineslice-handle-surface.unit.test.ts +11 -6
- package/src/__tests__/post-process-error-policy-owner.unit.test.ts +6 -13
- package/src/__tests__/prepared-graphics-capability.unit.test.ts +6 -81
- package/src/__tests__/prepared-graphics-generation.unit.test.ts +3 -17
- package/src/__tests__/prepared-graphics-lifecycle.unit.test.ts +6 -74
- package/src/__tests__/prepared-graphics-resolver.unit.test.ts +1 -3
- package/src/__tests__/prepared-graphics-snapshot.unit.test.ts +2 -2
- package/src/__tests__/profiler-default-off.test.ts +1 -1
- package/src/__tests__/profiler-phase-catalog.test.ts +2 -2
- package/src/__tests__/profiler-phase-integration.test.ts +5 -13
- package/src/__tests__/profiler-phase-token.test.ts +3 -8
- package/src/__tests__/public-surface.test-d.ts +65 -3
- package/src/__tests__/recover-error-policy-owner.unit.test.ts +1 -21
- package/src/__tests__/render-error-code-owner.test-d.ts +18 -23
- package/src/__tests__/render-error-detail-ssot.test-d.ts +0 -95
- package/src/__tests__/render-error-exhaustive.test-d.ts +8 -1
- package/src/__tests__/render-feature-assembly.test-d.ts +8 -16
- package/src/__tests__/render-feature-context.test-d.ts +16 -40
- package/src/__tests__/render-feature-contract.test-d.ts +9 -18
- package/src/__tests__/render-feature-diagnostics.unit.test.ts +6 -8
- package/src/__tests__/render-feature-docs.unit.test.ts +8 -7
- package/src/__tests__/render-feature-gpu-work.integration.test.ts +14 -75
- package/src/__tests__/render-feature-plan.test-d.ts +161 -0
- package/src/__tests__/render-feature-plan.unit.test.ts +170 -0
- package/src/__tests__/render-feature-prepared-diagnostics.unit.test.ts +8 -19
- package/src/__tests__/render-feature-prepared-errors.test-d.ts +3 -6
- package/src/__tests__/render-feature-prepared-graphics-docs.unit.test.ts +10 -71
- package/src/__tests__/render-feature-prepared-graphics-isolation.unit.test.ts +11 -61
- package/src/__tests__/render-feature-prepared-graphics-negative.test-d.ts +20 -43
- package/src/__tests__/render-feature-prepared-graphics.test-d.ts +56 -78
- package/src/__tests__/render-feature-public-surface.test-d.ts +6 -24
- package/src/__tests__/render-feature-retry.unit.test.ts +7 -48
- package/src/__tests__/render-feature-scheduling.unit.test.ts +15 -27
- package/src/__tests__/render-feature-stage-failed-policy-owner.unit.test.ts +0 -9
- package/src/__tests__/render-feature-stage-fault-recovery.unit.test.ts +19 -109
- package/src/__tests__/render-feature-visibility-context.test-d.ts +1 -6
- package/src/__tests__/render-feature-visibility-context.unit.test.ts +1 -2
- package/src/__tests__/render-feature-zero-work.unit.test.ts +14 -20
- package/src/__tests__/render-scene-projection.unit.test.ts +73 -111
- package/src/__tests__/renderer-factory-material-contract.unit.test.ts +44 -95
- package/src/__tests__/renderer-lifecycle.integration.test.ts +49 -1
- package/src/__tests__/shader-stage-surface.unit.test.ts +14 -9
- package/src/__tests__/ssao-config.unit.test.ts +36 -0
- package/src/__tests__/ssao-kernel-surface.unit.test.ts +4 -5
- package/src/__tests__/standard-clustered-pipeline.unit.test.ts +28 -0
- package/src/__tests__/standard-descriptors.test-d.ts +22 -0
- package/src/__tests__/standard-pipeline.dawn.test.ts +61 -0
- package/src/__tests__/standard-pipeline.integration.test.ts +124 -0
- package/src/__tests__/standard-profile-oracle.unit.test.ts +62 -0
- package/src/__tests__/surface-configuration.unit.test.ts +1 -2
- package/src/__tests__/texture-usage-surface.unit.test.ts +3 -6
- package/src/__tests__/typed-pipeline-topology.unit.test.ts +40 -206
- package/src/__tests__/urp-pipeline-targets.unit.test.ts +1 -11
- package/src/__tests__/urp-pipeline.unit.test.ts +2 -2
- package/src/__tests__/vertex-color-pipeline.unit.test.ts +10 -2
- package/src/__tests__/vertex-layout-consumer.unit.test.ts +1 -1
- package/src/__tests__/visibility-diagnostics.integration.test.ts +7 -11
- package/src/__tests__/visibility-extract.integration.test.ts +20 -11
- package/src/__tests__/visibility-extract.perf.test.ts +66 -32
- package/src/__tests__/visibility-feature-dedup.integration.test.ts +2 -4
- package/src/__tests__/visibility-hidden-report.unit.test.ts +1 -2
- package/src/__tests__/visibility-instances.integration.test.ts +7 -11
- package/src/__tests__/visibility-multi-world.integration.test.ts +38 -6
- package/src/__tests__/visibility-producer-matrix.integration.test.ts +18 -41
- package/src/__tests__/visibility-public-surface.test-d.ts +10 -9
- package/src/__tests__/visibility-resolver.unit.test.ts +2 -1
- package/src/__tests__/visibility-resource-short-circuit.unit.test.ts +80 -0
- package/src/__tests__/visibility-snapshot-contract.unit.test.ts +2 -1
- package/src/__tests__/vocabulary.unit.test.ts +1 -1
- package/src/assembly/backend-contract.ts +57 -0
- package/src/{renderer/renderer-factory.ts → assembly/factory.ts} +1663 -2690
- package/src/assembly/host-contract.ts +101 -0
- package/src/{renderer → assembly}/material/assembly.ts +0 -2
- package/src/assembly/material/pipeline-projection.ts +63 -0
- package/src/authoring.ts +14 -22
- package/src/components/__tests__/schema-contract.unit.test.ts +10 -0
- package/src/components/glyph-text.ts +3 -2
- package/src/components/index.ts +7 -9
- package/src/components/instances.ts +4 -5
- package/src/components/light-helpers.ts +1 -1
- package/src/components/mesh-filter.ts +11 -10
- package/src/components/post-process-params.ts +3 -3
- package/src/components/scene-instance.ts +12 -12
- package/src/components/skybox-background.ts +103 -0
- package/src/components/{environment.ts → skylight.ts} +2 -110
- package/src/components/sprite-animation.ts +3 -4
- package/src/components/sprite-instances.ts +2 -2
- package/src/components/sprite-playback-mode.ts +7 -1
- package/src/components/sprite-region-override.ts +2 -3
- package/src/components/tile-layer.ts +8 -2
- package/src/construct-renderer.ts +67 -4
- package/src/debug-draw-glue.ts +8 -129
- package/src/device/device-scope.ts +306 -0
- package/src/{gpu-resource-store.ts → device/gpu-residency.ts} +540 -639
- package/src/device/resource-types.ts +20 -0
- package/src/engine-metrics.ts +12 -19
- package/src/errors/recover.ts +45 -3
- package/src/errors/render.ts +357 -460
- package/src/features/fullscreen.ts +46 -0
- package/src/features/host.ts +575 -309
- package/src/features/morph/morph-feature.ts +195 -105
- package/src/features/plan.ts +447 -0
- package/src/features/prepared-gpu-work.ts +72 -15
- package/src/features/prepared-graphics-store.ts +20 -6
- package/src/features/prepared-graphics.ts +12 -71
- package/src/features/render-graph-compute.ts +14 -5
- package/src/features/render-graph-contribution.ts +43 -63
- package/src/features/render-graph-raster.ts +11 -6
- package/src/features/render-graph-resources.ts +25 -23
- package/src/features/targets.ts +8 -21
- package/src/features/types.ts +39 -227
- package/src/features/vocabulary.ts +24 -0
- package/src/fullscreen-post-process-pass.ts +14 -57
- package/src/glyph-text-layout-system.ts +70 -96
- package/src/gpu-driven/batch-topology.ts +24 -202
- package/src/gpu-driven/production-raster.ts +93 -1108
- package/src/gpu-driven/view-gpu.ts +46 -192
- package/src/gpu-resource.ts +4 -4
- package/src/gpu-scene-schema.ts +11 -99
- package/src/gpu-scene.ts +231 -364
- package/src/hdrp-buffers.ts +12 -221
- package/src/ibl/IblPipelineCache.ts +471 -711
- package/src/ibl/skylight-bind-group.ts +4 -4
- package/src/index.ts +58 -217
- package/src/inspection-types.ts +76 -0
- package/src/instance-buffer-cache.ts +1 -29
- package/src/internal.ts +6 -85
- package/src/lifecycle.ts +64 -19
- package/src/materials.ts +7 -29
- package/src/mesh-material-bindings.ts +0 -1
- package/src/pbr-pipeline.ts +434 -53
- package/src/{urp-pipeline.ts → pipeline/standard-forward-lane.ts} +38 -149
- package/src/{hdrp-pipeline.ts → pipeline/standard-pipeline.ts} +221 -191
- package/src/pipeline/standard-profile.ts +65 -0
- package/src/pipeline-builder.ts +60 -64
- package/src/pipeline-spec-types.ts +28 -0
- package/src/pipeline-spec.ts +72 -609
- package/src/plugin.ts +3 -75
- package/src/post-process-errors.ts +31 -33
- package/src/prepare/prepared-graphics-resolver.ts +5 -4
- package/src/record/__tests__/material-slot-plan.unit.test.ts +1 -33
- package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +41 -0
- package/src/record/__tests__/video-texture-view.unit.test.ts +1 -1
- package/src/record/frame-lighting.ts +74 -195
- package/src/record/frame-snapshot.ts +45 -60
- package/src/record/frame-targets.ts +8 -243
- package/src/record/frame.ts +509 -796
- package/src/record/helpers.ts +6 -11
- package/src/record/main-pass-geometry.ts +106 -147
- package/src/record/main-pass-material.ts +517 -190
- package/src/record/main-pass-sprite-draws.ts +198 -117
- package/src/record/main-pass.ts +162 -143
- package/src/record/mesh-ssbo.ts +517 -38
- package/src/record/render-context.ts +364 -0
- package/src/record/shadow-pass.ts +138 -1838
- package/src/record/skybox-post-pass.ts +12 -14
- package/src/record/typed-frame-graph.ts +215 -254
- package/src/record/view-ubo.ts +6 -37
- package/src/render-contract.ts +571 -0
- package/src/render-data.ts +3 -3
- package/src/render-graph-primitives.ts +76 -851
- package/src/render-pipeline.ts +104 -188
- package/src/render-system-extract.ts +1048 -1729
- package/src/render-system.ts +330 -1957
- package/src/scene/render-scene-types.ts +64 -0
- package/src/scene/render-scene.ts +1032 -0
- package/src/scene-instances/post-spawn-resolve-joints.ts +4 -7
- package/src/ssao-buffers.ts +167 -8
- package/src/systems/skin-palette-allocator.ts +21 -64
- package/src/systems/skin-palette-types.ts +14 -0
- package/src/systems/transparent-sort-config.ts +25 -3
- package/src/tilemap-chunk-extract-system.ts +53 -70
- package/src/typed-render-graph-primitives.ts +35 -24
- package/src/typed-shadow-passes.ts +26 -56
- package/dist/__tests__/analytic-sky-contract.test.d.ts +0 -2
- package/dist/__tests__/analytic-sky-contract.test.d.ts.map +0 -1
- package/dist/__tests__/analytic-sky-falsify.test.d.ts +0 -2
- package/dist/__tests__/analytic-sky-falsify.test.d.ts.map +0 -1
- package/dist/__tests__/assets-runtime-handoff.integration.test.d.ts +0 -2
- package/dist/__tests__/assets-runtime-handoff.integration.test.d.ts.map +0 -1
- package/dist/__tests__/backend-selection.integration.test.d.ts +0 -2
- package/dist/__tests__/backend-selection.integration.test.d.ts.map +0 -1
- package/dist/__tests__/camera-temporal.test-d.d.ts +0 -2
- package/dist/__tests__/camera-temporal.test-d.d.ts.map +0 -1
- package/dist/__tests__/camera-temporal.test.d.ts +0 -2
- package/dist/__tests__/camera-temporal.test.d.ts.map +0 -1
- package/dist/__tests__/directional-shadow-probe-atlas-layout.unit.test.d.ts +0 -2
- package/dist/__tests__/directional-shadow-probe-atlas-layout.unit.test.d.ts.map +0 -1
- package/dist/__tests__/environment-inspection.test.d.ts +0 -2
- package/dist/__tests__/environment-inspection.test.d.ts.map +0 -1
- package/dist/__tests__/fog-ai-user.fixture.d.ts +0 -2
- package/dist/__tests__/fog-ai-user.fixture.d.ts.map +0 -1
- package/dist/__tests__/fog-custom-render-feature.fixture.d.ts +0 -16
- package/dist/__tests__/fog-custom-render-feature.fixture.d.ts.map +0 -1
- package/dist/__tests__/fog-custom-render-feature.integration.test.d.ts +0 -2
- package/dist/__tests__/fog-custom-render-feature.integration.test.d.ts.map +0 -1
- package/dist/__tests__/fog-inspection.integration.test.d.ts +0 -2
- package/dist/__tests__/fog-inspection.integration.test.d.ts.map +0 -1
- package/dist/__tests__/fog-owner-state.integration.test.d.ts +0 -2
- package/dist/__tests__/fog-owner-state.integration.test.d.ts.map +0 -1
- package/dist/__tests__/fog-pipeline-topology.integration.test.d.ts +0 -2
- package/dist/__tests__/fog-pipeline-topology.integration.test.d.ts.map +0 -1
- package/dist/__tests__/fog-public-surface.integration.test.d.ts +0 -2
- package/dist/__tests__/fog-public-surface.integration.test.d.ts.map +0 -1
- package/dist/__tests__/fog-ray-contract.integration.test.d.ts +0 -2
- package/dist/__tests__/fog-ray-contract.integration.test.d.ts.map +0 -1
- package/dist/__tests__/fog-source-absence.integration.test.d.ts +0 -2
- package/dist/__tests__/fog-source-absence.integration.test.d.ts.map +0 -1
- package/dist/__tests__/fog-temporal.integration.test.d.ts +0 -2
- package/dist/__tests__/fog-temporal.integration.test.d.ts.map +0 -1
- package/dist/__tests__/frame-chain.unit.test.d.ts +0 -2
- package/dist/__tests__/frame-chain.unit.test.d.ts.map +0 -1
- package/dist/__tests__/gpu-scene-field-type-owner.test-d.d.ts +0 -2
- package/dist/__tests__/gpu-scene-field-type-owner.test-d.d.ts.map +0 -1
- package/dist/__tests__/hdrp-pipeline.unit.test.d.ts +0 -2
- package/dist/__tests__/hdrp-pipeline.unit.test.d.ts.map +0 -1
- package/dist/__tests__/ibl/ibl-fallback.unit.test.d.ts +0 -2
- package/dist/__tests__/ibl/ibl-fallback.unit.test.d.ts.map +0 -1
- package/dist/__tests__/material-derived-error-stop-draw.integration.test.d.ts +0 -2
- package/dist/__tests__/material-derived-error-stop-draw.integration.test.d.ts.map +0 -1
- package/dist/__tests__/material-derived-payload.integration.test.d.ts +0 -2
- package/dist/__tests__/material-derived-payload.integration.test.d.ts.map +0 -1
- package/dist/__tests__/materials.unit.test.d.ts +0 -2
- package/dist/__tests__/materials.unit.test.d.ts.map +0 -1
- package/dist/__tests__/param-schema-projection-census.unit.test.d.ts +0 -2
- package/dist/__tests__/param-schema-projection-census.unit.test.d.ts.map +0 -1
- package/dist/__tests__/persistent-render-scene.unit.test.d.ts +0 -2
- package/dist/__tests__/persistent-render-scene.unit.test.d.ts.map +0 -1
- package/dist/__tests__/pipeline-error-policy-owner.unit.test.d.ts +0 -2
- package/dist/__tests__/pipeline-error-policy-owner.unit.test.d.ts.map +0 -1
- package/dist/__tests__/pipeline-pass-trace.test.d.ts +0 -2
- package/dist/__tests__/pipeline-pass-trace.test.d.ts.map +0 -1
- package/dist/__tests__/pipeline-stage-owner.test-d.d.ts +0 -2
- package/dist/__tests__/pipeline-stage-owner.test-d.d.ts.map +0 -1
- package/dist/__tests__/plugin.test.d.ts +0 -2
- package/dist/__tests__/plugin.test.d.ts.map +0 -1
- package/dist/__tests__/prepared-graphics-backend-matrix.unit.test.d.ts +0 -2
- package/dist/__tests__/prepared-graphics-backend-matrix.unit.test.d.ts.map +0 -1
- package/dist/__tests__/prepared-graphics-resolver-recording.unit.test.d.ts +0 -2
- package/dist/__tests__/prepared-graphics-resolver-recording.unit.test.d.ts.map +0 -1
- package/dist/__tests__/public-pipeline-id.test-d.d.ts +0 -2
- package/dist/__tests__/public-pipeline-id.test-d.d.ts.map +0 -1
- package/dist/__tests__/render-dispatch-membership.unit.test.d.ts +0 -2
- package/dist/__tests__/render-dispatch-membership.unit.test.d.ts.map +0 -1
- package/dist/__tests__/render-feature-construction.unit.test.d.ts +0 -2
- package/dist/__tests__/render-feature-construction.unit.test.d.ts.map +0 -1
- package/dist/__tests__/render-feature-error-code-owner.test-d.d.ts +0 -2
- package/dist/__tests__/render-feature-error-code-owner.test-d.d.ts.map +0 -1
- package/dist/__tests__/render-feature-gpu-buffer-usage-owner.test-d.d.ts +0 -2
- package/dist/__tests__/render-feature-gpu-buffer-usage-owner.test-d.d.ts.map +0 -1
- package/dist/__tests__/render-feature-graph.integration.test.d.ts +0 -2
- package/dist/__tests__/render-feature-graph.integration.test.d.ts.map +0 -1
- package/dist/__tests__/render-feature-graphics-staging.unit.test.d.ts +0 -2
- package/dist/__tests__/render-feature-graphics-staging.unit.test.d.ts.map +0 -1
- package/dist/__tests__/render-feature-order.unit.test.d.ts +0 -2
- package/dist/__tests__/render-feature-order.unit.test.d.ts.map +0 -1
- package/dist/__tests__/render-feature-prepared-graphics-recording.unit.test.d.ts +0 -2
- package/dist/__tests__/render-feature-prepared-graphics-recording.unit.test.d.ts.map +0 -1
- package/dist/__tests__/render-feature-staging.unit.test.d.ts +0 -2
- package/dist/__tests__/render-feature-staging.unit.test.d.ts.map +0 -1
- package/dist/__tests__/renderer-render-scene-public.test-d.d.ts +0 -2
- package/dist/__tests__/renderer-render-scene-public.test-d.d.ts.map +0 -1
- package/dist/__tests__/scene-temporal-coverage.unit.test.d.ts +0 -2
- package/dist/__tests__/scene-temporal-coverage.unit.test.d.ts.map +0 -1
- package/dist/__tests__/sun-snapshot-consumers.test.d.ts +0 -2
- package/dist/__tests__/sun-snapshot-consumers.test.d.ts.map +0 -1
- package/dist/__tests__/surface-backend-kind-owner.test-d.d.ts +0 -2
- package/dist/__tests__/surface-backend-kind-owner.test-d.d.ts.map +0 -1
- package/dist/__tests__/taa-history-store.unit.test.d.ts +0 -2
- package/dist/__tests__/taa-history-store.unit.test.d.ts.map +0 -1
- package/dist/__tests__/temporal-frame-transaction.integration.test.d.ts +0 -2
- package/dist/__tests__/temporal-frame-transaction.integration.test.d.ts.map +0 -1
- package/dist/__tests__/temporal-inspection.unit.test.d.ts +0 -2
- package/dist/__tests__/temporal-inspection.unit.test.d.ts.map +0 -1
- package/dist/assets/__tests__/asset-decoders.unit.test.d.ts +0 -2
- package/dist/assets/__tests__/asset-decoders.unit.test.d.ts.map +0 -1
- package/dist/assets/__tests__/mipmap-generator.unit.test.d.ts +0 -2
- package/dist/assets/__tests__/mipmap-generator.unit.test.d.ts.map +0 -1
- package/dist/assets/__tests__/render-asset-port.unit.test.d.ts +0 -2
- package/dist/assets/__tests__/render-asset-port.unit.test.d.ts.map +0 -1
- package/dist/assets/__tests__/resolve-render-asset.unit.test.d.ts +0 -2
- package/dist/assets/__tests__/resolve-render-asset.unit.test.d.ts.map +0 -1
- package/dist/chunk-DJIMFYX6.mjs +0 -1439
- package/dist/chunk-DJIMFYX6.mjs.map +0 -1
- package/dist/chunk-F2SIOCRM.mjs +0 -7423
- package/dist/chunk-F2SIOCRM.mjs.map +0 -1
- package/dist/chunk-JMSB7AR4.mjs +0 -3
- package/dist/chunk-JMSB7AR4.mjs.map +0 -1
- package/dist/chunk-KHI6K4C6.mjs +0 -659
- package/dist/chunk-KHI6K4C6.mjs.map +0 -1
- package/dist/chunk-PORZOL2V.mjs +0 -569
- package/dist/chunk-PORZOL2V.mjs.map +0 -1
- package/dist/chunk-UOJGMDGP.mjs +0 -29913
- package/dist/chunk-UOJGMDGP.mjs.map +0 -1
- package/dist/components/__tests__/atmosphere-red.test.d.ts +0 -2
- package/dist/components/__tests__/atmosphere-red.test.d.ts.map +0 -1
- package/dist/components/__tests__/atmosphere.test.d.ts +0 -2
- package/dist/components/__tests__/atmosphere.test.d.ts.map +0 -1
- package/dist/components/__tests__/fog.test-d.d.ts +0 -2
- package/dist/components/__tests__/fog.test-d.d.ts.map +0 -1
- package/dist/components/__tests__/fog.test.d.ts +0 -2
- package/dist/components/__tests__/fog.test.d.ts.map +0 -1
- package/dist/components/atmosphere.d.ts +0 -18
- package/dist/components/atmosphere.d.ts.map +0 -1
- package/dist/components/environment.d.ts +0 -115
- package/dist/components/environment.d.ts.map +0 -1
- package/dist/deferred-lighting.d.ts +0 -13
- package/dist/deferred-lighting.d.ts.map +0 -1
- package/dist/dynamic-texture-store.d.ts +0 -42
- package/dist/dynamic-texture-store.d.ts.map +0 -1
- package/dist/environment/__tests__/failure-injection.test.d.ts +0 -2
- package/dist/environment/__tests__/failure-injection.test.d.ts.map +0 -1
- package/dist/environment/__tests__/generation-red.test.d.ts +0 -2
- package/dist/environment/__tests__/generation-red.test.d.ts.map +0 -1
- package/dist/environment/__tests__/generation.test.d.ts +0 -2
- package/dist/environment/__tests__/generation.test.d.ts.map +0 -1
- package/dist/environment/__tests__/graph-transaction.test.d.ts +0 -2
- package/dist/environment/__tests__/graph-transaction.test.d.ts.map +0 -1
- package/dist/environment/__tests__/inspection-red.test.d.ts +0 -2
- package/dist/environment/__tests__/inspection-red.test.d.ts.map +0 -1
- package/dist/environment/__tests__/lifecycle-evidence.test.d.ts +0 -2
- package/dist/environment/__tests__/lifecycle-evidence.test.d.ts.map +0 -1
- package/dist/environment/__tests__/pipeline-topology.test.d.ts +0 -2
- package/dist/environment/__tests__/pipeline-topology.test.d.ts.map +0 -1
- package/dist/environment/__tests__/readiness.test.d.ts +0 -2
- package/dist/environment/__tests__/readiness.test.d.ts.map +0 -1
- package/dist/environment/__tests__/recovery.test.d.ts +0 -2
- package/dist/environment/__tests__/recovery.test.d.ts.map +0 -1
- package/dist/environment/__tests__/resize-dispose.test.d.ts +0 -2
- package/dist/environment/__tests__/resize-dispose.test.d.ts.map +0 -1
- package/dist/environment/__tests__/signature-red.test.d.ts +0 -2
- package/dist/environment/__tests__/signature-red.test.d.ts.map +0 -1
- package/dist/environment/__tests__/signature.test.d.ts +0 -2
- package/dist/environment/__tests__/signature.test.d.ts.map +0 -1
- package/dist/environment/descriptors.d.ts +0 -2
- package/dist/environment/descriptors.d.ts.map +0 -1
- package/dist/environment/extracted-environment.d.ts +0 -53
- package/dist/environment/extracted-environment.d.ts.map +0 -1
- package/dist/environment/generation.d.ts +0 -62
- package/dist/environment/generation.d.ts.map +0 -1
- package/dist/environment/gpu-bundle.d.ts +0 -23
- package/dist/environment/gpu-bundle.d.ts.map +0 -1
- package/dist/environment/graph.d.ts +0 -108
- package/dist/environment/graph.d.ts.map +0 -1
- package/dist/environment/inspection.d.ts +0 -48
- package/dist/environment/inspection.d.ts.map +0 -1
- package/dist/environment/lifecycle.d.ts +0 -22
- package/dist/environment/lifecycle.d.ts.map +0 -1
- package/dist/environment/readiness.d.ts +0 -29
- package/dist/environment/readiness.d.ts.map +0 -1
- package/dist/environment/signature.d.ts +0 -70
- package/dist/environment/signature.d.ts.map +0 -1
- package/dist/extract/__tests__/directional-csm-domain.unit.test.d.ts +0 -2
- package/dist/extract/__tests__/directional-csm-domain.unit.test.d.ts.map +0 -1
- package/dist/extract/__tests__/environment-source.test.d.ts +0 -2
- package/dist/extract/__tests__/environment-source.test.d.ts.map +0 -1
- package/dist/extract/__tests__/schema-readers.unit.test.d.ts +0 -2
- package/dist/extract/__tests__/schema-readers.unit.test.d.ts.map +0 -1
- package/dist/extract/directional-csm.d.ts +0 -129
- package/dist/extract/directional-csm.d.ts.map +0 -1
- package/dist/extract/index.d.ts +0 -5
- package/dist/extract/index.d.ts.map +0 -1
- package/dist/extract/material-interface.d.ts +0 -56
- package/dist/extract/material-interface.d.ts.map +0 -1
- package/dist/extract/schema-readers.d.ts +0 -20
- package/dist/extract/schema-readers.d.ts.map +0 -1
- package/dist/features/graph-contribution.d.ts +0 -64
- package/dist/features/graph-contribution.d.ts.map +0 -1
- package/dist/features/index.d.ts +0 -21
- package/dist/features/index.d.ts.map +0 -1
- package/dist/features/morph/index.d.ts +0 -3
- package/dist/features/morph/index.d.ts.map +0 -1
- package/dist/features/morph/morph-bounds.d.ts +0 -42
- package/dist/features/morph/morph-bounds.d.ts.map +0 -1
- package/dist/features/scene-temporal.d.ts +0 -45
- package/dist/features/scene-temporal.d.ts.map +0 -1
- package/dist/features/taa-resolve.d.ts +0 -21
- package/dist/features/taa-resolve.d.ts.map +0 -1
- package/dist/frame-chain.d.ts +0 -14
- package/dist/frame-chain.d.ts.map +0 -1
- package/dist/gpu-driven/view-reference.d.ts.map +0 -1
- package/dist/gpu-resource-store.d.ts.map +0 -1
- package/dist/hdrp-cluster-membership.d.ts +0 -2
- package/dist/hdrp-cluster-membership.d.ts.map +0 -1
- package/dist/hdrp-pipeline.d.ts +0 -36
- package/dist/hdrp-pipeline.d.ts.map +0 -1
- package/dist/ibl/cubemap-projection.d.ts +0 -23
- package/dist/ibl/cubemap-projection.d.ts.map +0 -1
- package/dist/ibl/face-uniforms.d.ts +0 -65
- package/dist/ibl/face-uniforms.d.ts.map +0 -1
- package/dist/mesh-update-errors.d.ts +0 -71
- package/dist/mesh-update-errors.d.ts.map +0 -1
- package/dist/mipmap-generator.d.ts +0 -39
- package/dist/mipmap-generator.d.ts.map +0 -1
- package/dist/param-schema-projection-census.d.ts +0 -18
- package/dist/param-schema-projection-census.d.ts.map +0 -1
- package/dist/persistent-render-scene.d.ts +0 -158
- package/dist/persistent-render-scene.d.ts.map +0 -1
- package/dist/pipeline/index.d.ts +0 -11
- package/dist/pipeline/index.d.ts.map +0 -1
- package/dist/pipeline-errors.d.ts +0 -110
- package/dist/pipeline-errors.d.ts.map +0 -1
- package/dist/prepare/__tests__/schema-readers.unit.test.d.ts +0 -2
- package/dist/prepare/__tests__/schema-readers.unit.test.d.ts.map +0 -1
- package/dist/prepare/index.d.ts +0 -4
- package/dist/prepare/index.d.ts.map +0 -1
- package/dist/prepare/schema-readers.d.ts +0 -11
- package/dist/prepare/schema-readers.d.ts.map +0 -1
- package/dist/record/__tests__/environment-record.test.d.ts +0 -2
- package/dist/record/__tests__/environment-record.test.d.ts.map +0 -1
- package/dist/record/__tests__/frame-targets.unit.test.d.ts +0 -2
- package/dist/record/__tests__/frame-targets.unit.test.d.ts.map +0 -1
- package/dist/record/__tests__/membership-timing.unit.test.d.ts +0 -2
- package/dist/record/__tests__/membership-timing.unit.test.d.ts.map +0 -1
- package/dist/record/__tests__/render-submission.integration.test.d.ts +0 -2
- package/dist/record/__tests__/render-submission.integration.test.d.ts.map +0 -1
- package/dist/record/__tests__/render-submission.perf.test.d.ts +0 -2
- package/dist/record/__tests__/render-submission.perf.test.d.ts.map +0 -1
- package/dist/record/__tests__/shadow-submit-mode.unit.test.d.ts +0 -2
- package/dist/record/__tests__/shadow-submit-mode.unit.test.d.ts.map +0 -1
- package/dist/record/__tests__/view-ubo-layout.unit.test.d.ts +0 -2
- package/dist/record/__tests__/view-ubo-layout.unit.test.d.ts.map +0 -1
- package/dist/record/frame-observation.d.ts +0 -38
- package/dist/record/frame-observation.d.ts.map +0 -1
- package/dist/record/index.d.ts +0 -15
- package/dist/record/index.d.ts.map +0 -1
- package/dist/record/main-pass-skin.d.ts +0 -46
- package/dist/record/main-pass-skin.d.ts.map +0 -1
- package/dist/record/main-pass-sprite.d.ts +0 -69
- package/dist/record/main-pass-sprite.d.ts.map +0 -1
- package/dist/record/material-payload.d.ts +0 -43
- package/dist/record/material-payload.d.ts.map +0 -1
- package/dist/record/material-slot-plan.d.ts +0 -26
- package/dist/record/material-slot-plan.d.ts.map +0 -1
- package/dist/record/membership-timing.d.ts +0 -232
- package/dist/record/membership-timing.d.ts.map +0 -1
- package/dist/record/shadow-caster-uniform.d.ts +0 -8
- package/dist/record/shadow-caster-uniform.d.ts.map +0 -1
- package/dist/record/shadow-submit-mode.d.ts +0 -66
- package/dist/record/shadow-submit-mode.d.ts.map +0 -1
- package/dist/render-dispatch-membership.d.ts +0 -47
- package/dist/render-dispatch-membership.d.ts.map +0 -1
- package/dist/render-pipeline-context.d.ts +0 -289
- package/dist/render-pipeline-context.d.ts.map +0 -1
- package/dist/render-pipeline-target.d.ts +0 -16
- package/dist/render-pipeline-target.d.ts.map +0 -1
- package/dist/render-scene-projection.d.ts +0 -174
- package/dist/render-scene-projection.d.ts.map +0 -1
- package/dist/render-system-fold.d.ts +0 -266
- package/dist/render-system-fold.d.ts.map +0 -1
- package/dist/renderer/asset-service.d.ts +0 -10
- package/dist/renderer/asset-service.d.ts.map +0 -1
- package/dist/renderer/backend-selection.d.ts.map +0 -1
- package/dist/renderer/environment-classify.d.ts +0 -6
- package/dist/renderer/environment-classify.d.ts.map +0 -1
- package/dist/renderer/environment-error.d.ts +0 -13
- package/dist/renderer/environment-error.d.ts.map +0 -1
- package/dist/renderer/material/pipeline-projection.d.ts +0 -91
- package/dist/renderer/material/pipeline-projection.d.ts.map +0 -1
- package/dist/renderer/renderer-factory.d.ts +0 -289
- package/dist/renderer/renderer-factory.d.ts.map +0 -1
- package/dist/renderer.d.ts +0 -771
- package/dist/renderer.d.ts.map +0 -1
- package/dist/scene-utils/collect-subtree.d.ts +0 -11
- package/dist/scene-utils/collect-subtree.d.ts.map +0 -1
- package/dist/ssao-data.d.ts +0 -34
- package/dist/ssao-data.d.ts.map +0 -1
- package/dist/surface-format.d.ts +0 -10
- package/dist/surface-format.d.ts.map +0 -1
- package/dist/taa-history-store.d.ts +0 -88
- package/dist/taa-history-store.d.ts.map +0 -1
- package/dist/temporal-coverage.d.ts +0 -35
- package/dist/temporal-coverage.d.ts.map +0 -1
- package/dist/temporal-frame-transaction.d.ts +0 -92
- package/dist/temporal-frame-transaction.d.ts.map +0 -1
- package/dist/temporal-inspection.d.ts +0 -57
- package/dist/temporal-inspection.d.ts.map +0 -1
- package/dist/temporal-view.d.ts +0 -43
- package/dist/temporal-view.d.ts.map +0 -1
- package/dist/tilemap/resolve-tileset-runtime.d.ts +0 -23
- package/dist/tilemap/resolve-tileset-runtime.d.ts.map +0 -1
- package/dist/urp-pipeline.d.ts +0 -4
- package/dist/urp-pipeline.d.ts.map +0 -1
- package/src/__tests__/analytic-sky-contract.test.ts +0 -27
- package/src/__tests__/analytic-sky-falsify-carriers.mjs +0 -43
- package/src/__tests__/analytic-sky-falsify.test.ts +0 -94
- package/src/__tests__/assets-runtime-handoff.integration.test.ts +0 -36
- package/src/__tests__/backend-selection.integration.test.ts +0 -11
- package/src/__tests__/camera-temporal.test-d.ts +0 -8
- package/src/__tests__/camera-temporal.test.ts +0 -188
- package/src/__tests__/directional-shadow-probe-atlas-layout.unit.test.ts +0 -18
- package/src/__tests__/environment-inspection.test.ts +0 -45
- package/src/__tests__/fog-ai-user.fixture.ts +0 -350
- package/src/__tests__/fog-custom-render-feature.fixture.ts +0 -33
- package/src/__tests__/fog-custom-render-feature.integration.test.ts +0 -76
- package/src/__tests__/fog-inspection.integration.test.ts +0 -127
- package/src/__tests__/fog-owner-state.integration.test.ts +0 -112
- package/src/__tests__/fog-pipeline-topology.integration.test.ts +0 -93
- package/src/__tests__/fog-public-surface.integration.test.ts +0 -60
- package/src/__tests__/fog-ray-contract.integration.test.ts +0 -73
- package/src/__tests__/fog-source-absence.integration.test.ts +0 -51
- package/src/__tests__/fog-temporal.integration.test.ts +0 -115
- package/src/__tests__/frame-chain.unit.test.ts +0 -26
- package/src/__tests__/gpu-scene-field-type-owner.test-d.ts +0 -27
- package/src/__tests__/hdrp-pipeline.unit.test.ts +0 -100
- package/src/__tests__/ibl/ibl-fallback.unit.test.ts +0 -23
- package/src/__tests__/material-derived-error-stop-draw.integration.test.ts +0 -69
- package/src/__tests__/material-derived-payload.integration.test.ts +0 -164
- package/src/__tests__/materials.unit.test.ts +0 -25
- package/src/__tests__/param-schema-projection-census.unit.test.ts +0 -40
- package/src/__tests__/persistent-render-scene.unit.test.ts +0 -1311
- package/src/__tests__/pipeline-error-policy-owner.unit.test.ts +0 -167
- package/src/__tests__/pipeline-pass-trace.test.ts +0 -92
- package/src/__tests__/pipeline-stage-owner.test-d.ts +0 -13
- package/src/__tests__/plugin.test.ts +0 -25
- package/src/__tests__/prepared-graphics-backend-matrix.unit.test.ts +0 -129
- package/src/__tests__/prepared-graphics-resolver-recording.unit.test.ts +0 -180
- package/src/__tests__/public-pipeline-id.test-d.ts +0 -6
- package/src/__tests__/render-dispatch-membership.unit.test.ts +0 -117
- package/src/__tests__/render-feature-construction.unit.test.ts +0 -124
- package/src/__tests__/render-feature-error-code-owner.test-d.ts +0 -27
- package/src/__tests__/render-feature-gpu-buffer-usage-owner.test-d.ts +0 -46
- package/src/__tests__/render-feature-graph.integration.test.ts +0 -205
- package/src/__tests__/render-feature-graphics-staging.unit.test.ts +0 -200
- package/src/__tests__/render-feature-order.unit.test.ts +0 -66
- package/src/__tests__/render-feature-prepared-graphics-recording.unit.test.ts +0 -107
- package/src/__tests__/render-feature-staging.unit.test.ts +0 -94
- package/src/__tests__/renderer-render-scene-public.test-d.ts +0 -27
- package/src/__tests__/scene-temporal-coverage.unit.test.ts +0 -259
- package/src/__tests__/sun-snapshot-consumers.test.ts +0 -35
- package/src/__tests__/surface-backend-kind-owner.test-d.ts +0 -42
- package/src/__tests__/taa-history-store.unit.test.ts +0 -196
- package/src/__tests__/temporal-frame-transaction.integration.test.ts +0 -537
- package/src/__tests__/temporal-inspection.unit.test.ts +0 -138
- package/src/assets/__tests__/asset-decoders.unit.test.ts +0 -92
- package/src/assets/__tests__/mipmap-generator.unit.test.ts +0 -52
- package/src/assets/__tests__/render-asset-port.unit.test.ts +0 -54
- package/src/assets/__tests__/resolve-render-asset.unit.test.ts +0 -47
- package/src/components/__tests__/atmosphere-red.test.ts +0 -51
- package/src/components/__tests__/atmosphere.test.ts +0 -79
- package/src/components/__tests__/fog.test-d.ts +0 -49
- package/src/components/__tests__/fog.test.ts +0 -60
- package/src/components/atmosphere.ts +0 -64
- package/src/deferred-lighting.ts +0 -92
- package/src/dynamic-texture-store.ts +0 -111
- package/src/environment/README.md +0 -76
- package/src/environment/__tests__/failure-injection.test.ts +0 -50
- package/src/environment/__tests__/generation-red.test.ts +0 -64
- package/src/environment/__tests__/generation.test.ts +0 -37
- package/src/environment/__tests__/graph-transaction.test.ts +0 -85
- package/src/environment/__tests__/inspection-red.test.ts +0 -53
- package/src/environment/__tests__/lifecycle-evidence.test.ts +0 -40
- package/src/environment/__tests__/pipeline-topology.test.ts +0 -33
- package/src/environment/__tests__/readiness.test.ts +0 -42
- package/src/environment/__tests__/recovery.test.ts +0 -62
- package/src/environment/__tests__/resize-dispose.test.ts +0 -30
- package/src/environment/__tests__/signature-red.test.ts +0 -45
- package/src/environment/__tests__/signature.test.ts +0 -60
- package/src/environment/descriptors.ts +0 -7
- package/src/environment/extracted-environment.ts +0 -239
- package/src/environment/generation.ts +0 -158
- package/src/environment/gpu-bundle.ts +0 -144
- package/src/environment/graph.ts +0 -588
- package/src/environment/inspection.ts +0 -107
- package/src/environment/lifecycle.ts +0 -124
- package/src/environment/readiness.ts +0 -76
- package/src/environment/signature.ts +0 -185
- package/src/extract/__tests__/directional-csm-domain.unit.test.ts +0 -391
- package/src/extract/__tests__/environment-source.test.ts +0 -105
- package/src/extract/__tests__/schema-readers.unit.test.ts +0 -16
- package/src/extract/directional-csm.ts +0 -959
- package/src/extract/index.ts +0 -10
- package/src/extract/material-interface.ts +0 -604
- package/src/extract/schema-readers.ts +0 -27
- package/src/features/graph-contribution.ts +0 -577
- package/src/features/index.ts +0 -100
- package/src/features/morph/index.ts +0 -24
- package/src/features/morph/morph-bounds.ts +0 -123
- package/src/features/scene-temporal.ts +0 -274
- package/src/features/taa-resolve.ts +0 -244
- package/src/frame-chain.ts +0 -21
- package/src/hdrp-cluster-membership.ts +0 -70
- package/src/ibl/cubemap-projection.ts +0 -60
- package/src/ibl/face-uniforms.ts +0 -150
- package/src/mesh-update-errors.ts +0 -113
- package/src/mipmap-generator.ts +0 -244
- package/src/param-schema-projection-census.ts +0 -73
- package/src/persistent-render-scene.ts +0 -1752
- package/src/pipeline/index.ts +0 -40
- package/src/pipeline-errors.ts +0 -172
- package/src/prepare/__tests__/schema-readers.unit.test.ts +0 -16
- package/src/prepare/index.ts +0 -8
- package/src/prepare/schema-readers.ts +0 -20
- package/src/record/__tests__/environment-record.test.ts +0 -35
- package/src/record/__tests__/frame-targets.unit.test.ts +0 -54
- package/src/record/__tests__/membership-timing.unit.test.ts +0 -524
- package/src/record/__tests__/render-submission.integration.test.ts +0 -76
- package/src/record/__tests__/render-submission.perf.test.ts +0 -141
- package/src/record/__tests__/shadow-submit-mode.unit.test.ts +0 -88
- package/src/record/__tests__/view-ubo-layout.unit.test.ts +0 -101
- package/src/record/frame-observation.ts +0 -122
- package/src/record/index.ts +0 -15
- package/src/record/main-pass-skin.ts +0 -51
- package/src/record/main-pass-sprite.ts +0 -166
- package/src/record/material-payload.ts +0 -292
- package/src/record/material-slot-plan.ts +0 -68
- package/src/record/membership-timing.ts +0 -1012
- package/src/record/shadow-caster-uniform.ts +0 -37
- package/src/record/shadow-submit-mode.ts +0 -277
- package/src/render-dispatch-membership.ts +0 -155
- package/src/render-pipeline-context.ts +0 -332
- package/src/render-pipeline-target.ts +0 -96
- package/src/render-scene-projection.ts +0 -1738
- package/src/render-system-fold.ts +0 -531
- package/src/renderer/asset-service.ts +0 -32
- package/src/renderer/backend-selection.ts +0 -121
- package/src/renderer/environment-classify.ts +0 -30
- package/src/renderer/environment-error.ts +0 -19
- package/src/renderer/material/pipeline-projection.ts +0 -140
- package/src/renderer.ts +0 -940
- package/src/scene-utils/collect-subtree.ts +0 -40
- package/src/ssao-data.ts +0 -106
- package/src/surface-format.ts +0 -37
- package/src/taa-history-store.ts +0 -429
- package/src/temporal-coverage.ts +0 -106
- package/src/temporal-frame-transaction.ts +0 -301
- package/src/temporal-inspection.ts +0 -194
- package/src/temporal-view.ts +0 -174
- package/src/tilemap/resolve-tileset-runtime.ts +0 -72
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
// arrays consumed by the Record stage.
|
|
5
5
|
//
|
|
6
6
|
// Carve-out from render-system.ts (review round 1 finding #3 - 505 line cap
|
|
7
|
-
// fallback split into 3 files; main + extract + record).
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// fallback split into 3 files; main + extract + record). The public API remains
|
|
8
|
+
// limited to the renderer contract; this module is an owner-local
|
|
9
|
+
// implementation seam consumed through relative imports.
|
|
10
10
|
//
|
|
11
11
|
// w15 (feat-20260511-asset-system-v1 M5 / plan-strategy D-P4) +
|
|
12
12
|
// feat-20260513-component-naming-bevy-align M3 / D-2 (merged) +
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
//
|
|
78
78
|
// feat-20260514-ecs-children-instances-managed-buffer-array M3 / w15:
|
|
79
79
|
// The legacy `Instances { buffer, count }` form (cross-coupled with the
|
|
80
|
-
// deleted `
|
|
80
|
+
// deleted `AssetRegistry.createInstancedBuffer` triplet + the retired
|
|
81
81
|
// `InstancedBufferAsset` POD) is gone. Per-entity instance transforms now
|
|
82
82
|
// live inside the ECS `Instances { transforms: 'array<f32>' }` column;
|
|
83
83
|
// extract materialises a `Float32Array` snapshot per Instances-bearing
|
|
@@ -97,16 +97,23 @@
|
|
|
97
97
|
// Layer-3 ErrorHandler and the renderable is skipped. The record stage
|
|
98
98
|
// trusts the invariant and does not re-check.
|
|
99
99
|
|
|
100
|
-
import type {
|
|
100
|
+
import type { AssetRegistry } from '@forgeax/engine-assets-runtime';
|
|
101
|
+
import {
|
|
102
|
+
resolveAssetHandle,
|
|
103
|
+
walkMaterialPassesOverSharedRefs,
|
|
104
|
+
} from '@forgeax/engine-assets-runtime';
|
|
105
|
+
import type { Component, ComponentSchema, EntityHandle, World } from '@forgeax/engine-ecs';
|
|
101
106
|
import {
|
|
102
107
|
InstanceTransformsStrideMismatchError,
|
|
108
|
+
readRenderArrayView,
|
|
103
109
|
routeWorldError,
|
|
110
|
+
SpawnLightInvalidBoundsError,
|
|
104
111
|
SpriteInstancesCountMismatchError,
|
|
105
112
|
SpriteInstancesMutuallyExclusiveWithInstancesError,
|
|
106
113
|
SpriteInstancesRequiresSpriteShaderError,
|
|
107
114
|
} from '@forgeax/engine-ecs/projection';
|
|
108
115
|
import { box3, frustum, type Mat4, mat4, type Vec3, vec3 } from '@forgeax/engine-math';
|
|
109
|
-
import { AssetGuid } from '@forgeax/engine-pack/guid';
|
|
116
|
+
import { AssetGuid, type AssetGuid as AssetGuidBytes } from '@forgeax/engine-pack/guid';
|
|
110
117
|
import { RhiError } from '@forgeax/engine-rhi';
|
|
111
118
|
import {
|
|
112
119
|
MorphWeights,
|
|
@@ -114,7 +121,6 @@ import {
|
|
|
114
121
|
type SceneHierarchySnapshot,
|
|
115
122
|
Transform,
|
|
116
123
|
} from '@forgeax/engine-scene';
|
|
117
|
-
import { DEFAULT_UNLIT_PARAM_SCHEMA } from '@forgeax/engine-shader';
|
|
118
124
|
import {
|
|
119
125
|
JointCountMismatchError,
|
|
120
126
|
JointEntityDanglingError,
|
|
@@ -124,11 +130,9 @@ import {
|
|
|
124
130
|
} from '@forgeax/engine-skinning';
|
|
125
131
|
import type {
|
|
126
132
|
Asset,
|
|
127
|
-
DerivedMaterialInterface,
|
|
128
133
|
Handle,
|
|
129
134
|
MaterialAsset,
|
|
130
135
|
MaterialColorParameterSchema,
|
|
131
|
-
MaterialError,
|
|
132
136
|
MaterialParameter,
|
|
133
137
|
MaterialPass,
|
|
134
138
|
MaterialRenderState,
|
|
@@ -138,36 +142,21 @@ import type {
|
|
|
138
142
|
ParamSchemaEntry,
|
|
139
143
|
PrimitiveTopology,
|
|
140
144
|
SkeletonAsset,
|
|
141
|
-
TextureAsset,
|
|
142
145
|
} from '@forgeax/engine-types';
|
|
143
146
|
import {
|
|
144
147
|
ASSET_ERROR_HINTS,
|
|
145
148
|
AssetError,
|
|
146
149
|
derive,
|
|
147
|
-
err,
|
|
148
150
|
materialGuidText,
|
|
149
151
|
materialValuesToLinearRuntime,
|
|
150
|
-
ok,
|
|
151
|
-
RenderQueue,
|
|
152
|
-
type Result,
|
|
153
152
|
toShared,
|
|
154
153
|
} from '@forgeax/engine-types';
|
|
155
|
-
import type { RenderAssetPort } from './assets/render-asset-port';
|
|
156
|
-
import { resolveRenderAsset } from './assets/resolve-render-asset';
|
|
157
|
-
|
|
158
|
-
function reportRenderError(error: unknown, _context?: unknown): never {
|
|
159
|
-
throw error;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
154
|
import {
|
|
163
|
-
type Antialias,
|
|
164
155
|
antialiasFromF32,
|
|
165
|
-
type BloomEnabled,
|
|
166
156
|
bloomEnabledFromF32,
|
|
167
157
|
Camera,
|
|
168
158
|
cameraProjectionFromF32,
|
|
169
159
|
DirectionalLight,
|
|
170
|
-
Fog,
|
|
171
160
|
Instances,
|
|
172
161
|
Layer,
|
|
173
162
|
MeshFilter,
|
|
@@ -180,327 +169,33 @@ import {
|
|
|
180
169
|
SpotLight,
|
|
181
170
|
SpriteInstances,
|
|
182
171
|
SpriteRegionOverride,
|
|
183
|
-
type Tonemap,
|
|
184
172
|
tonemapFromF32,
|
|
185
173
|
tonemapToU32,
|
|
186
174
|
} from './components';
|
|
187
175
|
import { GlyphText } from './components/glyph-text';
|
|
176
|
+
import { computeInvRangeSquared, degToCos } from './components/light-helpers';
|
|
188
177
|
import {
|
|
189
|
-
computeInvRangeSquared,
|
|
190
|
-
degToCos,
|
|
191
|
-
validateDirectionalLightData,
|
|
192
|
-
validatePointLightData,
|
|
193
|
-
validatePointLightShadowData,
|
|
194
|
-
validateSpotLightData,
|
|
195
|
-
} from './components/light-helpers';
|
|
196
|
-
import {
|
|
197
|
-
type ExtractedEnvironment,
|
|
198
|
-
selectEnvironmentSource,
|
|
199
|
-
} from './environment/extracted-environment';
|
|
200
|
-
import {
|
|
201
|
-
FogOwnerConflictError,
|
|
202
|
-
FogParametersInvalidError,
|
|
203
178
|
MaterialSkinAttrMissingError,
|
|
204
|
-
|
|
179
|
+
ShadowInvalidConfigError,
|
|
205
180
|
SkinMaterialMismatchError,
|
|
206
181
|
} from './errors/render';
|
|
207
|
-
import {
|
|
208
|
-
type CsmCameraData,
|
|
209
|
-
computeDirectionalCsm,
|
|
210
|
-
type DirectionalCsmConfig,
|
|
211
|
-
type DirectionalCsmResult,
|
|
212
|
-
pssmSplit,
|
|
213
|
-
} from './extract/directional-csm';
|
|
214
|
-
import {
|
|
215
|
-
createMaterialInterfaceOwner,
|
|
216
|
-
type DerivedMaterialSlotProjection,
|
|
217
|
-
defaultMaterialTextureCoordinates,
|
|
218
|
-
deriveMaterialSlotProjection,
|
|
219
|
-
hasShadowCasterPass,
|
|
220
|
-
resolveMaterialFromPort,
|
|
221
|
-
} from './extract/material-interface';
|
|
222
|
-
import {
|
|
223
|
-
deriveParamSchemaForCensus,
|
|
224
|
-
type ParamSchemaProjectionCensus,
|
|
225
|
-
} from './param-schema-projection-census';
|
|
226
|
-
import {
|
|
227
|
-
buildRenderCandidateDomain,
|
|
228
|
-
type RenderCandidateDomain,
|
|
229
|
-
type RenderDispatchMembership,
|
|
230
|
-
renderCandidateCsmCandidates,
|
|
231
|
-
} from './render-dispatch-membership';
|
|
232
|
-
|
|
233
|
-
export {
|
|
234
|
-
type CascadeVolume,
|
|
235
|
-
type CascadeVolumeSet,
|
|
236
|
-
CascadeVolumeSetBuilder,
|
|
237
|
-
type CsmCameraData,
|
|
238
|
-
computeDirectionalCsm,
|
|
239
|
-
type DirectionalCsmConfig,
|
|
240
|
-
type DirectionalCsmResult,
|
|
241
|
-
directionalCascadeAtlasSize,
|
|
242
|
-
directionalCascadeRasterViewport,
|
|
243
|
-
pssmSplit,
|
|
244
|
-
} from './extract/directional-csm';
|
|
245
|
-
|
|
246
182
|
import { resolveVisibility, type VisibilitySnapshot } from './extract/visibility';
|
|
247
183
|
import type {
|
|
248
184
|
RenderFeatureHiddenEntityReport,
|
|
249
185
|
RenderFeatureWorldVisibilitySnapshot,
|
|
250
186
|
} from './features/types';
|
|
251
187
|
import { ensureGlyphMeshMaterialSlots } from './glyph-text-layout-system';
|
|
252
|
-
import type { RenderSceneFactInput } from './gpu-scene-schema';
|
|
253
188
|
import {
|
|
254
189
|
type MeshMaterialBindingDiagnostic,
|
|
255
190
|
type MeshMaterialBindingSource,
|
|
256
191
|
resolveMeshMaterialBindings,
|
|
257
192
|
} from './mesh-material-bindings';
|
|
193
|
+
import { isStandardPbrMaterialShader } from './pbr-pipeline';
|
|
194
|
+
import { type CameraSnapshot, STANDARD_TONEMAP_FEATURE_ID } from './render-contract';
|
|
258
195
|
import { getActiveCamera, selectActiveCameraIndex } from './systems/active-camera';
|
|
259
196
|
import { selectPasses } from './systems/pass-selector';
|
|
260
197
|
import type { SkinPaletteAllocator } from './systems/skin-palette-allocator';
|
|
261
|
-
|
|
262
|
-
export interface CameraSnapshot {
|
|
263
|
-
/** Renderer-local World attachment identity. */
|
|
264
|
-
readonly worldId: number;
|
|
265
|
-
/** Packed ECS entity handle; its high bits retain the entity generation. */
|
|
266
|
-
readonly entityKey: number;
|
|
267
|
-
/** World-space camera translation (mat4.getTranslation of Transform.world). */
|
|
268
|
-
readonly position: Vec3;
|
|
269
|
-
/**
|
|
270
|
-
* Resolved world-space camera mat4 (column-major 16 floats), copied from the
|
|
271
|
-
* entity's `Transform.world` view. The record stage derives the view matrix
|
|
272
|
-
* as `mat4.invert(world)` (feat-20260601 D-3: read world mat4 -> invert; no
|
|
273
|
-
* recompose from decomposed TRS).
|
|
274
|
-
*/
|
|
275
|
-
readonly world: Float32Array;
|
|
276
|
-
readonly fov: number;
|
|
277
|
-
readonly aspect: number;
|
|
278
|
-
readonly near: number;
|
|
279
|
-
readonly far: number;
|
|
280
|
-
/**
|
|
281
|
-
* Camera projection variant (`'perspective'` | `'orthographic'`),
|
|
282
|
-
* surfaced through the closed string-literal union narrowed from the
|
|
283
|
-
* Camera schema's `projection` f32 discriminator. Drives the CSM
|
|
284
|
-
* frustum-corner builder in render-system-extract: perspective uses
|
|
285
|
-
* mat4.perspective(fov, aspect, ...); orthographic uses mat4.ortho
|
|
286
|
-
* (left, right, bottom, top, ...). Without this discrimination an
|
|
287
|
-
* orthographic camera (fov=0) would feed a degenerate matrix into
|
|
288
|
-
* the CSM AABB-fit and the shadow atlas would never be written
|
|
289
|
-
* (feat-20260613 M6 / w20 fix).
|
|
290
|
-
*/
|
|
291
|
-
readonly projection: 'perspective' | 'orthographic';
|
|
292
|
-
/** Orthographic left frustum plane (only consumed when projection === 'orthographic'). */
|
|
293
|
-
readonly orthoLeft: number;
|
|
294
|
-
/** Orthographic right frustum plane. */
|
|
295
|
-
readonly orthoRight: number;
|
|
296
|
-
/** Orthographic bottom frustum plane. */
|
|
297
|
-
readonly orthoBottom: number;
|
|
298
|
-
/** Orthographic top frustum plane. */
|
|
299
|
-
readonly orthoTop: number;
|
|
300
|
-
/**
|
|
301
|
-
* feat-20260519-tonemap-reinhard-mvp / M3 / T-M3.1: tone-map mode
|
|
302
|
-
* surfaced through the closed string-literal union (`tonemapFromF32`
|
|
303
|
-
* narrows the schema's `f32` discriminator). The record stage branches
|
|
304
|
-
* on this field to pick the geometry pipeline target format
|
|
305
|
-
* (`bgra8unorm-srgb` for `'none'` / `'rgba16float'` for
|
|
306
|
-
* `'reinhard-extended'`) and to decide whether to emit the post-process
|
|
307
|
-
* tonemap fullscreen pass.
|
|
308
|
-
*/
|
|
309
|
-
readonly tonemap: Tonemap;
|
|
310
|
-
/** Linear pre-multiplier applied before the luminance compute (T-M3.3). */
|
|
311
|
-
readonly exposure: number;
|
|
312
|
-
/** Bright-end break point Lw for the extended Reinhard curve (T-M3.3). */
|
|
313
|
-
readonly whitePoint: number;
|
|
314
|
-
/**
|
|
315
|
-
* feat-20260528-fxaa-post-processing / w3: anti-alias mode surfaced
|
|
316
|
-
* through the closed string-literal union (antialiasFromF32 narrows
|
|
317
|
-
* the schema's f32 discriminator). The record stage branches on this
|
|
318
|
-
* field to decide whether to emit the FXAA post-process pass.
|
|
319
|
-
*/
|
|
320
|
-
readonly antialias: Antialias;
|
|
321
|
-
/** Explicit authoring fence for discontinuous view changes. */
|
|
322
|
-
readonly historyVersion: number;
|
|
323
|
-
/**
|
|
324
|
-
* feat-20260531-bloom-first-declarative-render-graph-pass / w4: bloom
|
|
325
|
-
* enabled discriminator surfaced through the closed string-literal union
|
|
326
|
-
* (bloomEnabledFromF32 narrows the schema's f32 discriminator). The record
|
|
327
|
-
* stage branches on this field to decide whether to emit the bloom
|
|
328
|
-
* post-process pipeline.
|
|
329
|
-
*/
|
|
330
|
-
readonly bloom: BloomEnabled;
|
|
331
|
-
/** HDR luminance threshold for bright-pass extraction (D-3 default 1.0). */
|
|
332
|
-
readonly bloomThreshold: number;
|
|
333
|
-
/** Multiplier applied to the blurred bloom contribution in composite (D-3 default 1.0). */
|
|
334
|
-
readonly bloomIntensity: number;
|
|
335
|
-
/** Gaussian blur kernel radius, clamped [1.0, 4.0] in shader (D-3 default 4.0). */
|
|
336
|
-
readonly bloomBlurRadius: number;
|
|
337
|
-
/**
|
|
338
|
-
* feat-20260709 M3 / D-3: clear-color RGBA sourced from the Camera entity's
|
|
339
|
-
* inline `array<f32,4>` column (first-archetype-hit per OOS-2). Collapsed
|
|
340
|
-
* from the feat-20260608 clearR/G/B/A snapshot quartet into one field aligned
|
|
341
|
-
* with the light snapshot shape. AI users override per-camera by setting
|
|
342
|
-
* `clearColor` when spawning the Camera entity; default is opaque black
|
|
343
|
-
* `[0, 0, 0, 1]` (D-5). The record stage reads these values verbatim into
|
|
344
|
-
* the LoadOp::Clear color slot.
|
|
345
|
-
*/
|
|
346
|
-
readonly clearColor: readonly [number, number, number, number];
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
export type RenderVisibilityViewKind = 'main' | 'cascade';
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* One of the four CPU visibility views. Cascade membership is copied from the
|
|
353
|
-
* canonical CascadeVolumeSet; this descriptor never derives a second volume.
|
|
354
|
-
*/
|
|
355
|
-
export interface RenderVisibilityViewDescriptor {
|
|
356
|
-
readonly viewId: string;
|
|
357
|
-
readonly kind: RenderVisibilityViewKind;
|
|
358
|
-
readonly cascadeIndex: number | undefined;
|
|
359
|
-
readonly planes: Float32Array;
|
|
360
|
-
/** Conservative world AABB used by the persistent spatial owner. */
|
|
361
|
-
readonly broadphaseWorldAabb?: Float32Array | undefined;
|
|
362
|
-
/** Canonical raster matrix for a cascade; absent for the main view. */
|
|
363
|
-
readonly lightViewProj?: Float32Array | undefined;
|
|
364
|
-
readonly casterCandidateIndices: ReadonlySet<number>;
|
|
365
|
-
readonly volumeRevision: number | undefined;
|
|
366
|
-
readonly spatialIndexRevision: number | undefined;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function cameraVisibilityPlanes(camera: CameraSnapshot): Float32Array {
|
|
370
|
-
if (
|
|
371
|
-
(camera.projection === 'perspective' && (camera.fov <= 0 || camera.aspect <= 0)) ||
|
|
372
|
-
!Number.isFinite(camera.near) ||
|
|
373
|
-
!Number.isFinite(camera.far) ||
|
|
374
|
-
camera.near >= camera.far
|
|
375
|
-
) {
|
|
376
|
-
return new Float32Array(0);
|
|
377
|
-
}
|
|
378
|
-
const projection = mat4.create();
|
|
379
|
-
if (camera.projection === 'orthographic') {
|
|
380
|
-
mat4.orthographic(
|
|
381
|
-
projection,
|
|
382
|
-
camera.orthoLeft,
|
|
383
|
-
camera.orthoRight,
|
|
384
|
-
camera.orthoBottom,
|
|
385
|
-
camera.orthoTop,
|
|
386
|
-
camera.near,
|
|
387
|
-
camera.far,
|
|
388
|
-
);
|
|
389
|
-
} else {
|
|
390
|
-
mat4.perspective(projection, camera.fov, camera.aspect, camera.near, camera.far);
|
|
391
|
-
}
|
|
392
|
-
const view = mat4.invert(mat4.create(), camera.world);
|
|
393
|
-
return frustum.fromViewProjection(
|
|
394
|
-
frustum.create(),
|
|
395
|
-
mat4.multiply(mat4.create(), projection, view),
|
|
396
|
-
);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function cameraVisibilityBroadphaseAabb(camera: CameraSnapshot): Float32Array | undefined {
|
|
400
|
-
if (
|
|
401
|
-
(camera.projection === 'perspective' && (camera.fov <= 0 || camera.aspect <= 0)) ||
|
|
402
|
-
!Number.isFinite(camera.near) ||
|
|
403
|
-
!Number.isFinite(camera.far) ||
|
|
404
|
-
camera.near >= camera.far
|
|
405
|
-
) {
|
|
406
|
-
return undefined;
|
|
407
|
-
}
|
|
408
|
-
const projection = mat4.create();
|
|
409
|
-
if (camera.projection === 'orthographic') {
|
|
410
|
-
mat4.orthographic(
|
|
411
|
-
projection,
|
|
412
|
-
camera.orthoLeft,
|
|
413
|
-
camera.orthoRight,
|
|
414
|
-
camera.orthoBottom,
|
|
415
|
-
camera.orthoTop,
|
|
416
|
-
camera.near,
|
|
417
|
-
camera.far,
|
|
418
|
-
);
|
|
419
|
-
} else {
|
|
420
|
-
mat4.perspective(projection, camera.fov, camera.aspect, camera.near, camera.far);
|
|
421
|
-
}
|
|
422
|
-
const view = mat4.invert(mat4.create(), camera.world);
|
|
423
|
-
const inverseViewProjection = mat4.invert(
|
|
424
|
-
mat4.create(),
|
|
425
|
-
mat4.multiply(mat4.create(), projection, view),
|
|
426
|
-
);
|
|
427
|
-
const span = camera.far - camera.near;
|
|
428
|
-
const nearNdc =
|
|
429
|
-
camera.projection === 'orthographic'
|
|
430
|
-
? 0
|
|
431
|
-
: (camera.far * (camera.near - camera.near)) / (camera.near * span);
|
|
432
|
-
const farNdc =
|
|
433
|
-
camera.projection === 'orthographic'
|
|
434
|
-
? 1
|
|
435
|
-
: (camera.far * (camera.far - camera.near)) / (camera.far * span);
|
|
436
|
-
let minX = Infinity;
|
|
437
|
-
let minY = Infinity;
|
|
438
|
-
let minZ = Infinity;
|
|
439
|
-
let maxX = -Infinity;
|
|
440
|
-
let maxY = -Infinity;
|
|
441
|
-
let maxZ = -Infinity;
|
|
442
|
-
for (const ndcZ of [nearNdc, farNdc]) {
|
|
443
|
-
for (const ndcX of [-1, 1]) {
|
|
444
|
-
for (const ndcY of [-1, 1]) {
|
|
445
|
-
const point = mat4.unproject(
|
|
446
|
-
vec3.create(),
|
|
447
|
-
new Float32Array([ndcX, ndcY, ndcZ]),
|
|
448
|
-
inverseViewProjection,
|
|
449
|
-
);
|
|
450
|
-
if (![point[0], point[1], point[2]].every(Number.isFinite)) return undefined;
|
|
451
|
-
minX = Math.min(minX, point[0] ?? 0);
|
|
452
|
-
minY = Math.min(minY, point[1] ?? 0);
|
|
453
|
-
minZ = Math.min(minZ, point[2] ?? 0);
|
|
454
|
-
maxX = Math.max(maxX, point[0] ?? 0);
|
|
455
|
-
maxY = Math.max(maxY, point[1] ?? 0);
|
|
456
|
-
maxZ = Math.max(maxZ, point[2] ?? 0);
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
return new Float32Array([minX, minY, minZ, maxX, maxY, maxZ]);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
/** Build the main camera plus exactly three CSM view descriptors. */
|
|
464
|
-
export function buildRenderVisibilityViewDescriptors(input: {
|
|
465
|
-
readonly camera: CameraSnapshot | undefined;
|
|
466
|
-
readonly directionalCsm: DirectionalCsmResult | undefined;
|
|
467
|
-
readonly spatialIndexRevision?: number | undefined;
|
|
468
|
-
}): readonly RenderVisibilityViewDescriptor[] {
|
|
469
|
-
const main = {
|
|
470
|
-
viewId: 'main',
|
|
471
|
-
kind: 'main' as const,
|
|
472
|
-
cascadeIndex: undefined,
|
|
473
|
-
planes: input.camera === undefined ? new Float32Array(0) : cameraVisibilityPlanes(input.camera),
|
|
474
|
-
broadphaseWorldAabb:
|
|
475
|
-
input.camera === undefined ? undefined : cameraVisibilityBroadphaseAabb(input.camera),
|
|
476
|
-
lightViewProj: undefined,
|
|
477
|
-
casterCandidateIndices: new Set<number>(),
|
|
478
|
-
volumeRevision: undefined,
|
|
479
|
-
spatialIndexRevision: input.spatialIndexRevision,
|
|
480
|
-
} satisfies RenderVisibilityViewDescriptor;
|
|
481
|
-
const cascades = Array.from({ length: 3 }, (_, cascadeIndex) => {
|
|
482
|
-
const volume = input.directionalCsm?.volumes[cascadeIndex];
|
|
483
|
-
return {
|
|
484
|
-
viewId: `cascade-${cascadeIndex}`,
|
|
485
|
-
kind: 'cascade' as const,
|
|
486
|
-
cascadeIndex,
|
|
487
|
-
// CascadeVolumeSet owns the exact boundary and membership. The CPU
|
|
488
|
-
// executor consumes the stable candidate IDs below instead of fitting a
|
|
489
|
-
// second frustum or re-running CSM math.
|
|
490
|
-
planes:
|
|
491
|
-
volume === undefined
|
|
492
|
-
? new Float32Array(0)
|
|
493
|
-
: frustum.fromViewProjection(frustum.create(), volume.lightViewProj),
|
|
494
|
-
broadphaseWorldAabb:
|
|
495
|
-
volume === undefined ? undefined : new Float32Array(volume.casterBroadphaseWorldAabb),
|
|
496
|
-
lightViewProj: volume === undefined ? undefined : new Float32Array(volume.lightViewProj),
|
|
497
|
-
casterCandidateIndices: new Set(volume?.casterRenderableIndices ?? []),
|
|
498
|
-
volumeRevision: volume?.revision,
|
|
499
|
-
spatialIndexRevision: input.spatialIndexRevision,
|
|
500
|
-
} satisfies RenderVisibilityViewDescriptor;
|
|
501
|
-
});
|
|
502
|
-
return Object.freeze([main, ...cascades]);
|
|
503
|
-
}
|
|
198
|
+
import type { SkinPaletteSlice } from './systems/skin-palette-types';
|
|
504
199
|
|
|
505
200
|
/**
|
|
506
201
|
* DirectionalLightSnapshot — sun-like infinite light variant of the
|
|
@@ -613,8 +308,6 @@ export interface PointShadowSnapshot {
|
|
|
613
308
|
*/
|
|
614
309
|
export interface SpotLightSnapshot {
|
|
615
310
|
readonly kind: 'spot';
|
|
616
|
-
/** ECS entity handle in the source World; diagnostic identity only. */
|
|
617
|
-
readonly entity: number;
|
|
618
311
|
readonly position: Vec3;
|
|
619
312
|
readonly direction: Vec3;
|
|
620
313
|
readonly color: Vec3;
|
|
@@ -744,10 +437,6 @@ export interface ExtractedLights {
|
|
|
744
437
|
* when castShadow=false or no directional light.
|
|
745
438
|
*/
|
|
746
439
|
readonly directionalCsmDirection: Vec3 | undefined;
|
|
747
|
-
/** Canonical receiver/caster plan consumed by persistent scene and record. */
|
|
748
|
-
readonly directionalCsm?: DirectionalCsmResult;
|
|
749
|
-
/** Internal compatibility projection; the plan above remains canonical. */
|
|
750
|
-
readonly cascades?: DirectionalCsmResult['cascades'];
|
|
751
440
|
}
|
|
752
441
|
|
|
753
442
|
/**
|
|
@@ -757,7 +446,7 @@ export interface ExtractedLights {
|
|
|
757
446
|
* `equirectHandle` carries the packed u32 handle for
|
|
758
447
|
* `Handle<EquirectAsset, 'shared'>`; the record stage drives the internal
|
|
759
448
|
* lazy cubemap projection from it and resolves the projected GPU cubemap via
|
|
760
|
-
* `
|
|
449
|
+
* `GpuResidencyCache.getCubemapGpuView(...)` and related helpers
|
|
761
450
|
* (feat-20260630 M3 / w16-w18). The snapshot carries NO projection status
|
|
762
451
|
* field: the status truth lives in the store's CubemapGpuEntry (D-3 SSOT);
|
|
763
452
|
* record queries it once per frame.
|
|
@@ -781,128 +470,13 @@ export interface SkylightSnapshot {
|
|
|
781
470
|
readonly entityHandle: number;
|
|
782
471
|
}
|
|
783
472
|
|
|
784
|
-
/** Retained scene-radiance Fog facts; parameters remain outside inspection. */
|
|
785
|
-
export interface FogSnapshot {
|
|
786
|
-
readonly color: readonly [number, number, number];
|
|
787
|
-
readonly density: number;
|
|
788
|
-
readonly heightFalloff: number;
|
|
789
|
-
readonly maxOpacity: number;
|
|
790
|
-
readonly revision: number;
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
/** Internal owner facts needed to publish a Fog snapshot transactionally. */
|
|
794
|
-
export interface FogSelection {
|
|
795
|
-
readonly snapshot: FogSnapshot | undefined;
|
|
796
|
-
readonly resourceOwner: number;
|
|
797
|
-
readonly entity: EntityHandle | undefined;
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
export type FogSelectionError = Extract<
|
|
801
|
-
RenderError,
|
|
802
|
-
FogOwnerConflictError | FogParametersInvalidError
|
|
803
|
-
>;
|
|
804
|
-
|
|
805
|
-
export type FogSelectionResult = Result<FogSelection, FogSelectionError>;
|
|
806
|
-
|
|
807
|
-
const FOG_ENTITY_DETAIL_LIMIT = 8;
|
|
808
|
-
|
|
809
|
-
function fogRevision(world: World, resourceOwner: number, entity: EntityHandle): number {
|
|
810
|
-
let hash = 2166136261 >>> 0;
|
|
811
|
-
const fog = world.get(entity, Fog);
|
|
812
|
-
const source = fog.ok
|
|
813
|
-
? `${world.identity}:${resourceOwner}:${entity}:${Array.from(fog.value.color).join(',')}:${fog.value.density}:${fog.value.heightFalloff}:${fog.value.maxOpacity}`
|
|
814
|
-
: `${world.identity}:${resourceOwner}:${entity}`;
|
|
815
|
-
for (let index = 0; index < source.length; index += 1) {
|
|
816
|
-
hash ^= source.charCodeAt(index);
|
|
817
|
-
hash = Math.imul(hash, 16777619) >>> 0;
|
|
818
|
-
}
|
|
819
|
-
return hash === 0 ? 1 : hash;
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
function fogInvalid(
|
|
823
|
-
entity: EntityHandle,
|
|
824
|
-
field: string,
|
|
825
|
-
expected: string,
|
|
826
|
-
actual: unknown,
|
|
827
|
-
): FogSelectionResult {
|
|
828
|
-
return err(new FogParametersInvalidError(entity as number, field, expected, actual));
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
/** Select and validate the single Fog authoring entity from resourceOwner. */
|
|
832
|
-
export function selectFog(world: World | undefined, resourceOwner: number): FogSelectionResult {
|
|
833
|
-
if (world === undefined) return ok({ snapshot: undefined, resourceOwner, entity: undefined });
|
|
834
|
-
const entities: EntityHandle[] = [];
|
|
835
|
-
const query = world.query({ read: [Fog] }).unwrap();
|
|
836
|
-
for (const row of query) entities.push(row.entity);
|
|
837
|
-
if (entities.length === 0) {
|
|
838
|
-
return ok({ snapshot: undefined, resourceOwner, entity: undefined });
|
|
839
|
-
}
|
|
840
|
-
if (entities.length > 1) {
|
|
841
|
-
const bounded = entities.slice(0, FOG_ENTITY_DETAIL_LIMIT).map((entity) => entity as number);
|
|
842
|
-
return err(
|
|
843
|
-
new FogOwnerConflictError(
|
|
844
|
-
resourceOwner,
|
|
845
|
-
bounded,
|
|
846
|
-
Math.max(0, entities.length - bounded.length),
|
|
847
|
-
),
|
|
848
|
-
);
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
const entity = entities[0];
|
|
852
|
-
if (entity === undefined) return ok({ snapshot: undefined, resourceOwner, entity: undefined });
|
|
853
|
-
const read = world.get(entity, Fog);
|
|
854
|
-
if (!read.ok) return ok({ snapshot: undefined, resourceOwner, entity: undefined });
|
|
855
|
-
const color = read.value.color;
|
|
856
|
-
if (
|
|
857
|
-
color.length !== 3 ||
|
|
858
|
-
color.some((value) => typeof value !== 'number' || !Number.isFinite(value) || value < 0)
|
|
859
|
-
) {
|
|
860
|
-
return fogInvalid(
|
|
861
|
-
entity,
|
|
862
|
-
'color',
|
|
863
|
-
'a three-component finite non-negative array',
|
|
864
|
-
Array.from(color),
|
|
865
|
-
);
|
|
866
|
-
}
|
|
867
|
-
const density = read.value.density;
|
|
868
|
-
if (!Number.isFinite(density) || density < 0) {
|
|
869
|
-
return fogInvalid(entity, 'density', 'a finite non-negative number', density);
|
|
870
|
-
}
|
|
871
|
-
const heightFalloff = read.value.heightFalloff;
|
|
872
|
-
if (!Number.isFinite(heightFalloff) || heightFalloff < 0) {
|
|
873
|
-
return fogInvalid(entity, 'heightFalloff', 'a finite non-negative number', heightFalloff);
|
|
874
|
-
}
|
|
875
|
-
const maxOpacity = read.value.maxOpacity;
|
|
876
|
-
if (!Number.isFinite(maxOpacity) || maxOpacity < 0 || maxOpacity > 1) {
|
|
877
|
-
return fogInvalid(entity, 'maxOpacity', 'a finite number in [0, 1]', maxOpacity);
|
|
878
|
-
}
|
|
879
|
-
const normalizedColor: [number, number, number] = [color[0] ?? 0, color[1] ?? 0, color[2] ?? 0];
|
|
880
|
-
const snapshot: FogSnapshot = Object.freeze({
|
|
881
|
-
color: normalizedColor,
|
|
882
|
-
density,
|
|
883
|
-
heightFalloff,
|
|
884
|
-
maxOpacity,
|
|
885
|
-
revision: fogRevision(world, resourceOwner, entity),
|
|
886
|
-
});
|
|
887
|
-
return ok({ snapshot, resourceOwner, entity });
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
function routeFogSelectionError(world: World | undefined, error: FogSelectionError): void {
|
|
891
|
-
if (world === undefined) return;
|
|
892
|
-
try {
|
|
893
|
-
routeWorldError(world, error, { systemName: 'RenderSystem.extractFog' });
|
|
894
|
-
} catch {
|
|
895
|
-
// World error policy owns whether a routed error throws.
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
|
|
899
473
|
/**
|
|
900
474
|
* SkyboxSnapshot -- extract-stage view of one SkyboxBackground entity
|
|
901
475
|
* (feat-20260531-skybox-env-background M2 / w5).
|
|
902
476
|
*
|
|
903
477
|
* `equirectHandle` carries the packed u32 handle for
|
|
904
478
|
* `Handle<EquirectAsset, 'shared'>`; the record stage resolves the projected
|
|
905
|
-
* GPU cubemap via `
|
|
479
|
+
* GPU cubemap via `GpuResidencyCache.getCubemapGpuView(...)`.
|
|
906
480
|
*
|
|
907
481
|
* `mode` carries the raw `f32` column value (`SKYBOX_MODE_CUBEMAP = 0`).
|
|
908
482
|
* First hit wins per plan-strategy D-6; multi-entity once-warn in record stage.
|
|
@@ -921,27 +495,9 @@ export interface SkyboxSnapshot {
|
|
|
921
495
|
export interface RenderableSnapshot {
|
|
922
496
|
readonly assetHandle: number;
|
|
923
497
|
readonly transform: TransformSnapshot;
|
|
924
|
-
/**
|
|
925
|
-
* Bounds-only transform derived from authored local TRS when the resolved
|
|
926
|
-
* world column is still its identity default. This preserves the producer
|
|
927
|
-
* bounds source for direct extract-before-propagate paths without changing
|
|
928
|
-
* the render SSOT, which remains `transform.world`.
|
|
929
|
-
*/
|
|
930
|
-
readonly boundsWorld?: Float32Array;
|
|
931
|
-
/** Candidate-domain copy of all instance transforms, retained before cull. */
|
|
932
|
-
readonly candidateInstanceTransforms?: Float32Array;
|
|
933
498
|
/** Finite producer-owned local bounds retained for persistent CPU view work. */
|
|
934
499
|
readonly localAabb?: Float32Array;
|
|
935
|
-
/**
|
|
936
|
-
* Optional authoring/pack-owned scene facts. The projection supplies
|
|
937
|
-
* explicit `unknown` defaults when this field is absent; it never infers
|
|
938
|
-
* staticness or shadow eligibility from frame-local behavior.
|
|
939
|
-
*/
|
|
940
|
-
readonly sceneFact?: RenderSceneFactInput;
|
|
941
|
-
/**
|
|
942
|
-
* Forward raster item facts consumed by the first GPU-driven rigid lane.
|
|
943
|
-
* Each entry carries its source submesh, pass identity, and resolved queue.
|
|
944
|
-
*/
|
|
500
|
+
/** Indexed single-submesh facts consumed by the first GPU-driven rigid lane. */
|
|
945
501
|
readonly gpuDrivenDraws?: readonly GpuDrivenDrawSnapshot[];
|
|
946
502
|
/**
|
|
947
503
|
* feat-20260608 M5 amend / w11-a: the entity's representative (first)
|
|
@@ -997,8 +553,6 @@ export interface RenderableSnapshot {
|
|
|
997
553
|
* not the bare entityKey alone (D-1).
|
|
998
554
|
*/
|
|
999
555
|
readonly entityKey: number;
|
|
1000
|
-
/** Stable pre-main-cull candidate index used by the directional CSM plan. */
|
|
1001
|
-
readonly candidateIndex?: number;
|
|
1002
556
|
/**
|
|
1003
557
|
* feat-20260514 M3 (w15): when the entity carries an `Instances` component
|
|
1004
558
|
* the extract stage materialises a fresh `Float32Array` snapshot of the
|
|
@@ -1038,41 +592,20 @@ export interface RenderableSnapshot {
|
|
|
1038
592
|
* Absent (`undefined`) means the entity is not skinned.
|
|
1039
593
|
*/
|
|
1040
594
|
readonly skin?: SkinPaletteSlice;
|
|
1041
|
-
/**
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
595
|
+
/**
|
|
596
|
+
* ECS-owned morph weights. Presence selects the Standard Pipeline CPU
|
|
597
|
+
* specialized deformation lane; the asset remains the source of target
|
|
598
|
+
* deltas and the record stage only receives this frozen POD snapshot.
|
|
599
|
+
*/
|
|
600
|
+
readonly morph?: MorphSnapshot;
|
|
1045
601
|
}
|
|
1046
602
|
|
|
1047
|
-
export
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
| 'new-slot'
|
|
1051
|
-
| 'generation-reuse'
|
|
1052
|
-
| 'reentered'
|
|
1053
|
-
| 'geometry-revision'
|
|
1054
|
-
| 'material-revision'
|
|
1055
|
-
| 'skinning-deformation'
|
|
1056
|
-
| 'morph-deformation';
|
|
1057
|
-
|
|
1058
|
-
export interface RenderableTemporalSnapshot {
|
|
1059
|
-
readonly previousEpoch: number | undefined;
|
|
1060
|
-
readonly previousSource: RenderablePreviousSource;
|
|
1061
|
-
readonly reactive: boolean;
|
|
1062
|
-
readonly reactiveReasons: readonly RenderableReactiveReason[];
|
|
1063
|
-
readonly previousTransform: TransformSnapshot;
|
|
1064
|
-
readonly previousInstances: InstancesSnapshot | undefined;
|
|
1065
|
-
readonly previousSkin: SkinPaletteSlice | undefined;
|
|
1066
|
-
readonly previousMorphWeights: Float32Array | undefined;
|
|
603
|
+
export interface MorphSnapshot {
|
|
604
|
+
readonly weights: Float32Array;
|
|
605
|
+
readonly targetCount: number;
|
|
1067
606
|
}
|
|
1068
607
|
|
|
1069
608
|
export interface GpuDrivenDrawSnapshot {
|
|
1070
|
-
/** Index into the resolved mesh submesh array. */
|
|
1071
|
-
readonly submeshIndex: number;
|
|
1072
|
-
/** Index of the corresponding material pass in the resolved pass list. */
|
|
1073
|
-
readonly passIndex: number;
|
|
1074
|
-
/** Effective queue copied from that pass; always resolved at extract time. */
|
|
1075
|
-
readonly queue: number;
|
|
1076
609
|
readonly kind: 'indexed' | 'non-indexed';
|
|
1077
610
|
readonly first: number;
|
|
1078
611
|
readonly count: number;
|
|
@@ -1083,41 +616,6 @@ export interface GpuDrivenDrawSnapshot {
|
|
|
1083
616
|
readonly materialResourceClass: string;
|
|
1084
617
|
}
|
|
1085
618
|
|
|
1086
|
-
/**
|
|
1087
|
-
* feat-20260523-skin-skeleton-animation M2 / T-21 + T-24:
|
|
1088
|
-
* per-draw palette slice metadata. Joint count + byte offset into the
|
|
1089
|
-
* shared skin palette storage buffer.
|
|
1090
|
-
*
|
|
1091
|
-
* plan-strategy D-10: SkinPaletteSlice naming mirroring Bevy SkinByteOffset
|
|
1092
|
-
* but with forgeax vocabulary (byteOffset not offset, jointCount not joint_count).
|
|
1093
|
-
*/
|
|
1094
|
-
export interface SkinPaletteSlice {
|
|
1095
|
-
readonly jointCount: number;
|
|
1096
|
-
/** CPU snapshot of the exact palette written for this extract epoch. */
|
|
1097
|
-
readonly jointMatrices: Float32Array;
|
|
1098
|
-
/**
|
|
1099
|
-
* Byte offset INTO `buffer` for this slice. On the storage path the
|
|
1100
|
-
* allocator returns one shared buffer + a per-entity cursor offset; on
|
|
1101
|
-
* the uniform fallback path each slice owns its own GPU buffer (cap
|
|
1102
|
-
* `maxUniformBufferBindingSize` 16 KiB cannot host shared dynOffset
|
|
1103
|
-
* windows for >1 entity), so `byteOffset === 0` for every slice and
|
|
1104
|
-
* `buffer` is the per-slice handle. Record stage reads both fields and
|
|
1105
|
-
* does not branch on the path -- BG cache key includes `buffer`, so
|
|
1106
|
-
* two paths converge at the same code.
|
|
1107
|
-
*/
|
|
1108
|
-
readonly byteOffset: number;
|
|
1109
|
-
/**
|
|
1110
|
-
* Per-slice GPU buffer handle. M6: feat-20260612 collapsed the prior
|
|
1111
|
-
* shared-buffer assumption (record-stage read `pipelineState
|
|
1112
|
-
* .skinPaletteAllocator.buffer`) into a per-slice carrier so the
|
|
1113
|
-
* uniform fallback path can return a per-entity buffer without
|
|
1114
|
-
* branching the record stage.
|
|
1115
|
-
*/
|
|
1116
|
-
readonly buffer: import('@forgeax/engine-rhi').Buffer;
|
|
1117
|
-
/** Previous successful-submit palette, paired with {@link buffer}. */
|
|
1118
|
-
readonly previousBuffer?: import('@forgeax/engine-rhi').Buffer | undefined;
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
619
|
export interface InstancesSnapshot {
|
|
1122
620
|
/** Packed column-major mat4 transforms (16 f32 per instance). */
|
|
1123
621
|
readonly transforms: Float32Array;
|
|
@@ -1177,56 +675,6 @@ export interface TransformSnapshot {
|
|
|
1177
675
|
readonly world: Float32Array;
|
|
1178
676
|
}
|
|
1179
677
|
|
|
1180
|
-
function authoredBoundsWorld(
|
|
1181
|
-
world: ArrayLike<number>,
|
|
1182
|
-
local:
|
|
1183
|
-
| {
|
|
1184
|
-
readonly pos: ArrayLike<number>;
|
|
1185
|
-
readonly quat: ArrayLike<number>;
|
|
1186
|
-
readonly scale: ArrayLike<number>;
|
|
1187
|
-
}
|
|
1188
|
-
| undefined,
|
|
1189
|
-
): Float32Array | undefined {
|
|
1190
|
-
if (local === undefined || world.length < 16) return undefined;
|
|
1191
|
-
let worldIsIdentity = true;
|
|
1192
|
-
const identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
1193
|
-
for (let index = 0; index < 16; index += 1) {
|
|
1194
|
-
const value = world[index];
|
|
1195
|
-
if (
|
|
1196
|
-
value === undefined ||
|
|
1197
|
-
!Number.isFinite(value) ||
|
|
1198
|
-
Math.abs(value - (identity[index] ?? 0)) > 1e-6
|
|
1199
|
-
) {
|
|
1200
|
-
worldIsIdentity = false;
|
|
1201
|
-
break;
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
if (!worldIsIdentity || local.pos.length < 3 || local.quat.length < 4 || local.scale.length < 3) {
|
|
1205
|
-
return undefined;
|
|
1206
|
-
}
|
|
1207
|
-
const hasAuthoredTransform =
|
|
1208
|
-
(local.pos[0] ?? 0) !== 0 ||
|
|
1209
|
-
(local.pos[1] ?? 0) !== 0 ||
|
|
1210
|
-
(local.pos[2] ?? 0) !== 0 ||
|
|
1211
|
-
(local.quat[0] ?? 0) !== 0 ||
|
|
1212
|
-
(local.quat[1] ?? 0) !== 0 ||
|
|
1213
|
-
(local.quat[2] ?? 0) !== 0 ||
|
|
1214
|
-
(local.quat[3] ?? 1) !== 1 ||
|
|
1215
|
-
(local.scale[0] ?? 1) !== 1 ||
|
|
1216
|
-
(local.scale[1] ?? 1) !== 1 ||
|
|
1217
|
-
(local.scale[2] ?? 1) !== 1;
|
|
1218
|
-
if (!hasAuthoredTransform) return undefined;
|
|
1219
|
-
for (const values of [local.pos, local.quat, local.scale]) {
|
|
1220
|
-
for (let index = 0; index < values.length; index += 1) {
|
|
1221
|
-
const value = values[index];
|
|
1222
|
-
if (value === undefined || !Number.isFinite(value)) return undefined;
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
const composed = mat4.create();
|
|
1226
|
-
mat4.compose(composed, local.pos, local.quat, local.scale);
|
|
1227
|
-
return composed;
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
678
|
/**
|
|
1231
679
|
* MaterialSnapshot: extract-stage view of one entity's material asset (M2 / w6
|
|
1232
680
|
* of feat-20260517-merge-mesh-renderer-material-renderer; plan-strategy section 2.3).
|
|
@@ -1267,16 +715,12 @@ export interface MaterialSnapshot {
|
|
|
1267
715
|
* discriminator (M4-T06).
|
|
1268
716
|
*/
|
|
1269
717
|
readonly materialShaderId?: string | undefined;
|
|
1270
|
-
/** Canonical RenderAssetPort GUID used to join the production readiness tuple. */
|
|
1271
|
-
readonly materialGuid?: string | undefined;
|
|
1272
718
|
/**
|
|
1273
719
|
* Stable source handle for the material asset. Record uses this only for
|
|
1274
720
|
* same-frame assembly reuse; GPU bind-group validity still comes from the
|
|
1275
721
|
* resource-identity cache.
|
|
1276
722
|
*/
|
|
1277
723
|
readonly materialHandle?: number | undefined;
|
|
1278
|
-
/** Whether the resolved material owns a ShadowCaster pass. */
|
|
1279
|
-
readonly shadowCaster?: boolean | undefined;
|
|
1280
724
|
/**
|
|
1281
725
|
* The selected forward-pass state for this exact material. A mesh can bind
|
|
1282
726
|
* different materials to its submeshes, so pipeline selection must read the
|
|
@@ -1292,10 +736,6 @@ export interface MaterialSnapshot {
|
|
|
1292
736
|
readonly paramSnapshot?: Readonly<Record<string, number | number[] | string>> | undefined;
|
|
1293
737
|
/** Effective MaterialAsset parameter contract used to lay out this snapshot. */
|
|
1294
738
|
readonly materialParamSchema?: readonly ParamSchemaEntry[] | undefined;
|
|
1295
|
-
/** Typed per-slot projection produced from the effective root schema. */
|
|
1296
|
-
readonly derivedMaterialSlot?: DerivedMaterialSlotProjection | undefined;
|
|
1297
|
-
/** Structured extract failure that the record stage must propagate and stop. */
|
|
1298
|
-
readonly derivedMaterialError?: MaterialError | undefined;
|
|
1299
739
|
/** Authored per-slot UV set and KHR texture transform metadata. */
|
|
1300
740
|
readonly textureCoordinates?: ReadonlyMap<string, MaterialTextureCoordinates> | undefined;
|
|
1301
741
|
/**
|
|
@@ -1303,7 +743,7 @@ export interface MaterialSnapshot {
|
|
|
1303
743
|
* (feat-20260621-learn-render-5-5-parallax M2 / w7). The SSOT carrier for
|
|
1304
744
|
* EVERY texture the shader's `derive(paramSchema).textureFieldNames`
|
|
1305
745
|
* declares — `baseColorTexture` / `metallicRoughnessTexture` /
|
|
1306
|
-
* `normalTexture` for built-in standard-PBR, plus
|
|
746
|
+
* `normalTexture` for built-in standard-PBR, plus each custom field such as
|
|
1307
747
|
* `heightTexture` (LO 5.5 parallax). The record stage iterates this map to
|
|
1308
748
|
* assemble the user-region bind group per the per-shader BGL (w8), so a 4th
|
|
1309
749
|
* (or Nth) texture flows end-to-end without a hardcoded field list.
|
|
@@ -1322,7 +762,7 @@ export interface MaterialSnapshot {
|
|
|
1322
762
|
* occupies the same texture2d values slot a static texture would (P4:
|
|
1323
763
|
* one binding shape), but extract routes it here instead of `textureHandles`
|
|
1324
764
|
* so the record stage pulls the current-frame view from the transient
|
|
1325
|
-
* DynamicTextureStore (D-3) instead of `
|
|
765
|
+
* DynamicTextureStore (D-3) instead of `GpuResidencyCache.ensureResident`
|
|
1326
766
|
* (which has no `video` arm; AC-08). Each entry also carries the resolved
|
|
1327
767
|
* clip handle so the record stage can key the per-frame upload.
|
|
1328
768
|
*
|
|
@@ -1403,7 +843,7 @@ export interface MaterialSnapshotCacheEntry {
|
|
|
1403
843
|
readonly passes: readonly MaterialPass[];
|
|
1404
844
|
/** Root/chain identities are present only on cross-frame-safe entries. */
|
|
1405
845
|
readonly chain?: readonly MaterialCacheChainIdentity[];
|
|
1406
|
-
/**
|
|
846
|
+
/** AssetRegistry epoch at which the root/parent chain was last validated. */
|
|
1407
847
|
readonly catalogEpoch?: number;
|
|
1408
848
|
/** True only when every object reachable from the authored chain is frozen. */
|
|
1409
849
|
readonly crossFrameSafe: boolean;
|
|
@@ -1426,39 +866,6 @@ function gpuDrivenMaterialResourceClass(material: MaterialSnapshot): string {
|
|
|
1426
866
|
});
|
|
1427
867
|
}
|
|
1428
868
|
|
|
1429
|
-
function gpuDrivenForwardPass(
|
|
1430
|
-
material: MaterialSnapshot,
|
|
1431
|
-
cache: MaterialSnapshotCache,
|
|
1432
|
-
): { readonly passIndex: number; readonly queue: number } | undefined {
|
|
1433
|
-
// Test-only snapshots and the engine default material do not have a cached
|
|
1434
|
-
// authored pass list. Their forward pass is the canonical Geometry pass.
|
|
1435
|
-
if (material.materialHandle === undefined) {
|
|
1436
|
-
return { passIndex: 0, queue: RenderQueue.Geometry };
|
|
1437
|
-
}
|
|
1438
|
-
if (material.materialHandle === 0) {
|
|
1439
|
-
// The synthesized default dispatch keeps ShadowCaster at index 0 and
|
|
1440
|
-
// Forward at index 1; use the same identity for GPU/CPU ownership.
|
|
1441
|
-
return { passIndex: 1, queue: RenderQueue.Geometry };
|
|
1442
|
-
}
|
|
1443
|
-
const passes = cache.get(material.materialHandle)?.passes;
|
|
1444
|
-
if (passes === undefined) return undefined;
|
|
1445
|
-
for (let passIndex = 0; passIndex < passes.length; passIndex += 1) {
|
|
1446
|
-
const pass = passes[passIndex];
|
|
1447
|
-
if (pass === undefined) continue;
|
|
1448
|
-
const state = pass.renderState as
|
|
1449
|
-
| (Readonly<Record<string, unknown>> & {
|
|
1450
|
-
readonly tags?: Readonly<Record<string, string>>;
|
|
1451
|
-
readonly queue?: unknown;
|
|
1452
|
-
})
|
|
1453
|
-
| undefined;
|
|
1454
|
-
const lightMode = state?.tags?.LightMode;
|
|
1455
|
-
if (lightMode !== 'Forward' && pass.name !== 'forward') continue;
|
|
1456
|
-
const queue = typeof state?.queue === 'number' ? state.queue : RenderQueue.Geometry;
|
|
1457
|
-
return { passIndex, queue };
|
|
1458
|
-
}
|
|
1459
|
-
return undefined;
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
869
|
// === DispatchEntry — M3 / w26 single dispatch list (feat-20260526-material-asset-multipass-renderstate) ===
|
|
1463
870
|
//
|
|
1464
871
|
// Plan-strategy D-3: single dispatch list sorted by queue value,
|
|
@@ -1507,6 +914,12 @@ export function sortDispatchByQueue<E extends { readonly queue: number }>(
|
|
|
1507
914
|
return entries.slice().sort((a, b) => a.queue - b.queue);
|
|
1508
915
|
}
|
|
1509
916
|
|
|
917
|
+
const DEFAULT_FORWARD_PASS: MaterialPass = {
|
|
918
|
+
name: 'forward',
|
|
919
|
+
program: { module: 'forgeax::default-unlit', vertexEntry: 'vs_main', fragmentEntry: 'fs_main' },
|
|
920
|
+
renderState: { tags: { LightMode: 'Forward' }, queue: 2000 },
|
|
921
|
+
};
|
|
922
|
+
|
|
1510
923
|
function appendMaterialDispatchEntries(
|
|
1511
924
|
pendingDispatch: DispatchEntry[],
|
|
1512
925
|
passes: readonly MaterialPass[],
|
|
@@ -1515,6 +928,7 @@ function appendMaterialDispatchEntries(
|
|
|
1515
928
|
renderableIndex: number,
|
|
1516
929
|
layer: number,
|
|
1517
930
|
paramSnapshot: Readonly<Record<string, number | number[] | string>> | undefined,
|
|
931
|
+
passIndexOffset = 0,
|
|
1518
932
|
): void {
|
|
1519
933
|
const matchedPasses = selectPasses(passes, {});
|
|
1520
934
|
for (let pIdx = 0; pIdx < matchedPasses.length; pIdx++) {
|
|
@@ -1529,8 +943,8 @@ function appendMaterialDispatchEntries(
|
|
|
1529
943
|
entityIndex: entity,
|
|
1530
944
|
materialHandle,
|
|
1531
945
|
renderableIndex,
|
|
1532
|
-
passIndex: pIdx,
|
|
1533
|
-
queue: passState.queue ??
|
|
946
|
+
passIndex: passIndexOffset + pIdx,
|
|
947
|
+
queue: passState.queue ?? 2000,
|
|
1534
948
|
layer,
|
|
1535
949
|
tags: passState.tags ?? {},
|
|
1536
950
|
renderState: pipelineRenderState(passState),
|
|
@@ -1550,23 +964,16 @@ export interface ExtractedFrame {
|
|
|
1550
964
|
readonly cameras: CameraSnapshot[];
|
|
1551
965
|
readonly lights: ExtractedLights;
|
|
1552
966
|
readonly renderables: RenderableSnapshot[];
|
|
1553
|
-
/** Author-visible snapshots retained before the main-view frustum cull. */
|
|
1554
|
-
readonly candidateRenderables?: readonly RenderableSnapshot[];
|
|
1555
|
-
/** Pass dispatch for the same pre-main-cull candidate domain. */
|
|
1556
|
-
readonly candidateDispatch?: readonly DispatchEntry[];
|
|
1557
|
-
/** Shared membership owner for the candidate dispatch domain. */
|
|
1558
|
-
readonly candidateDispatchMembership?: RenderDispatchMembership;
|
|
1559
|
-
/** Shared immutable candidate array/dispatch/index owner. */
|
|
1560
|
-
readonly candidateDomain?: RenderCandidateDomain;
|
|
1561
|
-
/** Resource-owner Fog snapshot retained for direct extract consumers. */
|
|
1562
|
-
readonly fog?: FogSnapshot | undefined;
|
|
1563
967
|
/**
|
|
1564
968
|
* Single dispatch list sorted by queue value (ascending, stable sort).
|
|
1565
969
|
* Replaces the old opaqueDispatch / transparentDispatch / overlayDispatch
|
|
1566
970
|
* three-bucket model per plan-strategy D-3.
|
|
1567
971
|
*/
|
|
1568
972
|
readonly dispatch: DispatchEntry[];
|
|
1569
|
-
readonly
|
|
973
|
+
readonly skylight: SkylightSnapshot | undefined;
|
|
974
|
+
readonly skylightCount: number;
|
|
975
|
+
readonly skybox: SkyboxSnapshot | undefined;
|
|
976
|
+
readonly skyboxCount: number;
|
|
1570
977
|
/**
|
|
1571
978
|
* feat-20260528-frustum-culling M3 / w11: frustum culling statistics
|
|
1572
979
|
* collected during the extract phase. `total` is the count of entities
|
|
@@ -1593,53 +1000,82 @@ export interface ExtractedFrame {
|
|
|
1593
1000
|
}
|
|
1594
1001
|
|
|
1595
1002
|
/**
|
|
1596
|
-
*
|
|
1597
|
-
*
|
|
1598
|
-
*
|
|
1599
|
-
*
|
|
1600
|
-
* - `layer` — primary sort key (signed i32 from `Layer.value`; default 0
|
|
1601
|
-
* for entities without a Layer component).
|
|
1602
|
-
* - `posX` / `posY` / `posZ` — world position from the entity's Transform.
|
|
1603
|
-
* - `pivotY` / `sizeY` — sprite's Y-axis foot offset inputs. `pivotY =
|
|
1604
|
-
* SpriteMaterialAsset.pivot[1]` (default 0.5); `sizeY` = Transform's
|
|
1605
|
-
* world scale Y (sprite quad's world-space height in unit-quad world
|
|
1606
|
-
* space — the unit quad is 1x1 so `Transform.scaleY` directly maps to
|
|
1607
|
-
* world height). The foot-Y formula `posY - pivotY * sizeY` is the
|
|
1608
|
-
* SSOT for the JRPG Y-sort path (mode=1, requirements §AC-10).
|
|
1609
|
-
* - `sortKey` — optional per-entity override from `SortKey.value`. When
|
|
1610
|
-
* present, `transparentSortEntries` uses it INSTEAD of the mode formula
|
|
1611
|
-
* for this entry (still gated by `layer` as the primary key).
|
|
1612
|
-
*
|
|
1613
|
-
* Plan-strategy §3.3 interface example + §AC-10 sort path + §AC-19
|
|
1614
|
-
* derivation row 13 (this is the true new POD shape `@new-surface`; not a
|
|
1615
|
-
* mere rename of an existing snapshot).
|
|
1003
|
+
* Internal world surface used by extract for archetype-graph traversal and
|
|
1004
|
+
* error routing. The packed `Entity` handle for a row is read directly from
|
|
1005
|
+
* the essential id=0 `Entity` column (`arch.columns.get(Entity.id).get('self')`)
|
|
1006
|
+
* -- no generation lookup / encodeEntity rebuild (feat-20260602 M2).
|
|
1616
1007
|
*/
|
|
1617
|
-
|
|
1618
|
-
readonly
|
|
1619
|
-
|
|
1620
|
-
readonly
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1008
|
+
type RenderErrorContext = {
|
|
1009
|
+
readonly systemName: string;
|
|
1010
|
+
/** Retained as diagnostic metadata for legacy render call sites. */
|
|
1011
|
+
readonly severity?: 'error' | 'warning';
|
|
1012
|
+
};
|
|
1013
|
+
|
|
1014
|
+
type ArrayFieldView = ArrayLike<number>;
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* Render's old zero-copy ECS calls now use the explicit public projection
|
|
1018
|
+
* boundary. The optimized ECS owner keeps storage behind `World`; render
|
|
1019
|
+
* receives a detached array snapshot and routes failures through the same
|
|
1020
|
+
* projection owner.
|
|
1021
|
+
*/
|
|
1022
|
+
type WorldInternalView = {
|
|
1023
|
+
_routeError(error: unknown, ctx: RenderErrorContext): void;
|
|
1024
|
+
_getArrayView<N extends string, S extends ComponentSchema>(
|
|
1025
|
+
entity: EntityHandle,
|
|
1026
|
+
component: Component<N, S>,
|
|
1027
|
+
fieldName: string,
|
|
1028
|
+
): ArrayFieldView | undefined;
|
|
1029
|
+
};
|
|
1030
|
+
|
|
1031
|
+
const Severity = Object.freeze({ Error: 'error', Warning: 'warning' } as const);
|
|
1032
|
+
|
|
1033
|
+
function createWorldInternalView(world: World): WorldInternalView {
|
|
1034
|
+
return {
|
|
1035
|
+
_routeError(error, ctx) {
|
|
1036
|
+
routeWorldError(world, error, { systemName: ctx.systemName });
|
|
1037
|
+
},
|
|
1038
|
+
_getArrayView(entity, component, fieldName) {
|
|
1039
|
+
return readRenderArrayView(world, entity, component, fieldName) as ArrayFieldView | undefined;
|
|
1040
|
+
},
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* The user-region texture fields the built-in standard-PBR material declares.
|
|
1046
|
+
* Used as the fallback texture-field set when the shader id is not registered
|
|
1047
|
+
* (cross-worktree shader-late-register, plan R-4) so a built-in material still
|
|
1048
|
+
* resolves its user-region textures. Mirrors `derive(default-standard-pbr).textureFieldNames`.
|
|
1049
|
+
*/
|
|
1050
|
+
const BUILTIN_USER_REGION_TEXTURE_FIELDS: readonly string[] = [
|
|
1051
|
+
'baseColorTexture',
|
|
1052
|
+
'metallicRoughnessTexture',
|
|
1053
|
+
'normalTexture',
|
|
1054
|
+
'specularTintTexture',
|
|
1055
|
+
];
|
|
1056
|
+
const BUILTIN_USER_REGION_TEXTURE_FIELD_SET = new Set(BUILTIN_USER_REGION_TEXTURE_FIELDS);
|
|
1057
|
+
const BUILTIN_BASE_COLOR_TEXTURE_FIELD_SET = new Set(['baseColorTexture']);
|
|
1058
|
+
|
|
1059
|
+
function materialTextureFields(
|
|
1060
|
+
shaderId: string | undefined,
|
|
1061
|
+
fields: ReadonlySet<string> | undefined,
|
|
1062
|
+
): ReadonlySet<string> | undefined {
|
|
1063
|
+
if (fields !== undefined && fields.size > 0) return fields;
|
|
1064
|
+
if (
|
|
1065
|
+
shaderId === 'forgeax::default-standard-pbr' ||
|
|
1066
|
+
shaderId === 'forgeax::default-standard-pbr-skin' ||
|
|
1067
|
+
shaderId === 'forgeax::pbr-skin'
|
|
1068
|
+
) {
|
|
1069
|
+
return BUILTIN_USER_REGION_TEXTURE_FIELD_SET;
|
|
1070
|
+
}
|
|
1071
|
+
if (
|
|
1072
|
+
shaderId === 'forgeax::default-unlit' ||
|
|
1073
|
+
shaderId === 'forgeax::sprite' ||
|
|
1074
|
+
shaderId === 'forgeax::sprite-lit'
|
|
1075
|
+
) {
|
|
1076
|
+
return BUILTIN_BASE_COLOR_TEXTURE_FIELD_SET;
|
|
1077
|
+
}
|
|
1078
|
+
return fields;
|
|
1643
1079
|
}
|
|
1644
1080
|
|
|
1645
1081
|
/**
|
|
@@ -1724,12 +1160,12 @@ function pipelineRenderState(
|
|
|
1724
1160
|
|
|
1725
1161
|
/**
|
|
1726
1162
|
* tweak-20260627-model-loading-smoke-build-perf M4: per-World intern cache for
|
|
1727
|
-
* the
|
|
1163
|
+
* the loadByGuid texture/sampler resolution path. A MaterialAsset's
|
|
1728
1164
|
* texture/sampler values remain embedded GUID strings (dash-form) after
|
|
1729
|
-
*
|
|
1165
|
+
* loadByGuid; the extract stage re-resolves each GUID to a column handle every
|
|
1730
1166
|
* frame. Before this cache each resolution called `world.allocSharedRef`, which
|
|
1731
1167
|
* mints a NEW monotonically-increasing slot id per call. Because the GPU
|
|
1732
|
-
* residency cache (`
|
|
1168
|
+
* residency cache (`GpuResidencyCache.textureGpuHandles`) is keyed on
|
|
1733
1169
|
* `handleSlot(handle)`, a fresh slot every frame meant the residency check
|
|
1734
1170
|
* ALWAYS missed -> all textures re-uploaded to the GPU every frame, old GPU
|
|
1735
1171
|
* textures never freed (refcount never hits 0). Unbounded GPU memory +
|
|
@@ -1740,11 +1176,10 @@ function pipelineRenderState(
|
|
|
1740
1176
|
* across frames (architecture-principle §6 idempotency: same GUID resolved N
|
|
1741
1177
|
* times yields the same handle). The handle is intentionally long-lived -- it
|
|
1742
1178
|
* lives as long as the World references the material, which is exactly the
|
|
1743
|
-
* desired lifetime
|
|
1744
|
-
*
|
|
1745
|
-
* owns GPU disposal.
|
|
1179
|
+
* desired lifetime; the `onLastRelease` -> `gpuStore.evictTexture` wiring stays
|
|
1180
|
+
* coherent because the handle is no longer churned per frame.
|
|
1746
1181
|
*
|
|
1747
|
-
* Invariant boundary (asset-registry.ts:1958-1962): the
|
|
1182
|
+
* Invariant boundary (asset-registry.ts:1958-1962): the AssetRegistry is a
|
|
1748
1183
|
* GUID -> payload catalogue with NO handle/World concept -- it cannot mint a
|
|
1749
1184
|
* column handle. So this intern cache lives in the extract/render layer, keyed
|
|
1750
1185
|
* per-World via a WeakMap (the World owns the SharedRefStore that mints slots).
|
|
@@ -1767,7 +1202,7 @@ const guidHandleInternByWorld = new WeakMap<World, Map<string, GuidHandleInternE
|
|
|
1767
1202
|
|
|
1768
1203
|
function internSharedRefFromGuid<B extends string>(
|
|
1769
1204
|
world: World,
|
|
1770
|
-
assetsRef:
|
|
1205
|
+
assetsRef: AssetRegistry,
|
|
1771
1206
|
guid: string,
|
|
1772
1207
|
brand: B,
|
|
1773
1208
|
): Handle<B, 'shared'> | undefined {
|
|
@@ -1780,19 +1215,19 @@ function internSharedRefFromGuid<B extends string>(
|
|
|
1780
1215
|
const cached = perWorld.get(key);
|
|
1781
1216
|
const payload = assetsRef.lookup(guid);
|
|
1782
1217
|
if (cached !== undefined && cached.payload === payload) {
|
|
1783
|
-
const cachedHandle = cached.handle
|
|
1218
|
+
const cachedHandle = toShared<B>(cached.handle);
|
|
1784
1219
|
if (world.sharedRefs.resolve(cachedHandle).ok) return cachedHandle;
|
|
1785
1220
|
}
|
|
1786
1221
|
if (cached !== undefined) {
|
|
1787
|
-
world.sharedRefs.release(cached.handle
|
|
1222
|
+
world.sharedRefs.release(toShared<B>(cached.handle));
|
|
1788
1223
|
perWorld.delete(key);
|
|
1789
1224
|
}
|
|
1790
1225
|
if (payload === undefined) return undefined;
|
|
1791
|
-
// Mint exactly once per (world, guid, brand).
|
|
1792
|
-
// publishes
|
|
1793
|
-
//
|
|
1794
|
-
const handle = world.
|
|
1795
|
-
perWorld.set(key, { handle
|
|
1226
|
+
// Mint exactly once per (world, guid, brand). ECS owns the allocation grant
|
|
1227
|
+
// and publishes release evidence through SharedRefStore; render does not
|
|
1228
|
+
// attach a second deleter lifecycle to the World.
|
|
1229
|
+
const handle = world.internSharedRef(brand, payload);
|
|
1230
|
+
perWorld.set(key, { handle, payload });
|
|
1796
1231
|
return handle;
|
|
1797
1232
|
}
|
|
1798
1233
|
|
|
@@ -1817,7 +1252,7 @@ function internSharedRefFromGuid<B extends string>(
|
|
|
1817
1252
|
function resolveVideoFieldHandle(
|
|
1818
1253
|
value: unknown,
|
|
1819
1254
|
world: World,
|
|
1820
|
-
assetsRef:
|
|
1255
|
+
assetsRef: AssetRegistry,
|
|
1821
1256
|
): Handle<'VideoAsset', 'shared'> | undefined {
|
|
1822
1257
|
const texture = materialTextureValue(value);
|
|
1823
1258
|
const textureGuid = assetReferenceText(texture?.texture ?? value);
|
|
@@ -1843,11 +1278,14 @@ function materialTextureValue(value: unknown): MaterialTextureValue | undefined
|
|
|
1843
1278
|
|
|
1844
1279
|
function assetReferenceText(value: unknown): string | undefined {
|
|
1845
1280
|
if (typeof value === 'string') return value;
|
|
1846
|
-
if (value instanceof Uint8Array && value
|
|
1847
|
-
return AssetGuid.format(value as never);
|
|
1281
|
+
if (value instanceof Uint8Array && isAssetGuidBytes(value)) return AssetGuid.format(value);
|
|
1848
1282
|
return undefined;
|
|
1849
1283
|
}
|
|
1850
1284
|
|
|
1285
|
+
function isAssetGuidBytes(value: Uint8Array): value is AssetGuidBytes {
|
|
1286
|
+
return value.byteLength === 16;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1851
1289
|
function materialNormalScale(values: Readonly<Record<string, unknown>>): number {
|
|
1852
1290
|
for (const [field, value] of Object.entries(values)) {
|
|
1853
1291
|
if (field !== 'normalTexture') continue;
|
|
@@ -1863,14 +1301,16 @@ function materialTextureRef(value: unknown): unknown {
|
|
|
1863
1301
|
|
|
1864
1302
|
function collectMaterialTextureCoordinates(
|
|
1865
1303
|
values: Readonly<Record<string, unknown>>,
|
|
1866
|
-
schema: readonly ParamSchemaEntry[],
|
|
1867
|
-
derivedInterface?: DerivedMaterialInterface,
|
|
1868
|
-
paramSchemaProjectionCensus?: ParamSchemaProjectionCensus,
|
|
1869
1304
|
): Map<string, MaterialTextureCoordinates> {
|
|
1870
1305
|
const out = new Map<string, MaterialTextureCoordinates>();
|
|
1871
|
-
for (const field of
|
|
1872
|
-
|
|
1873
|
-
|
|
1306
|
+
for (const field of [
|
|
1307
|
+
'baseColorTexture',
|
|
1308
|
+
'metallicRoughnessTexture',
|
|
1309
|
+
'normalTexture',
|
|
1310
|
+
'specularTintTexture',
|
|
1311
|
+
'emissiveTexture',
|
|
1312
|
+
'occlusionTexture',
|
|
1313
|
+
]) {
|
|
1874
1314
|
const coordinates = materialTextureValue(values[field])?.coordinates;
|
|
1875
1315
|
if (coordinates !== undefined) out.set(field, coordinates);
|
|
1876
1316
|
}
|
|
@@ -1897,14 +1337,14 @@ function collectMaterialTextureSamplers(
|
|
|
1897
1337
|
* feat-20260621-learn-render-5-5-parallax M2 / w7 (D-3): collect the
|
|
1898
1338
|
* user-region texture handles for a material by iterating the shader's
|
|
1899
1339
|
* `derive(paramSchema).textureFieldNames` SSOT (via
|
|
1900
|
-
* `
|
|
1340
|
+
* `AssetRegistry.materialShaderTextureFieldNames`). Each declared texture
|
|
1901
1341
|
* field whose paramValue resolves to a handle lands in the returned map keyed
|
|
1902
|
-
* by field name; this is the single path through which
|
|
1342
|
+
* by field name; this is the single path through which an arbitrary number of user-region
|
|
1903
1343
|
* textures (4 standard or 5+ custom, e.g. parallax `heightTexture`) flow.
|
|
1904
1344
|
*
|
|
1905
|
-
* When the
|
|
1906
|
-
* `emissiveTexture` / `occlusionTexture`
|
|
1907
|
-
* textures and are NOT part of this set.
|
|
1345
|
+
* When the shader is not registered the built-in 4-field set is used so
|
|
1346
|
+
* standard materials still resolve. `emissiveTexture` / `occlusionTexture`
|
|
1347
|
+
* are engine-injection (lightmap) textures and are NOT part of this set.
|
|
1908
1348
|
*
|
|
1909
1349
|
* feat-20260623-world-space-video-asset M4 / w14 (D-5): a field whose paramValue
|
|
1910
1350
|
* catalogues to a VideoAsset is routed into `videoOut` (a VideoAsset-branded
|
|
@@ -1918,8 +1358,7 @@ function collectUserRegionTextureHandles(
|
|
|
1918
1358
|
pv: Readonly<Record<string, unknown>>,
|
|
1919
1359
|
shaderId: string | undefined,
|
|
1920
1360
|
paramSchema: readonly ParamSchemaEntry[] | undefined,
|
|
1921
|
-
|
|
1922
|
-
assetsRef: RenderAssetPort,
|
|
1361
|
+
assetsRef: AssetRegistry,
|
|
1923
1362
|
world: World,
|
|
1924
1363
|
resolveTex: (
|
|
1925
1364
|
value: unknown,
|
|
@@ -1928,38 +1367,16 @@ function collectUserRegionTextureHandles(
|
|
|
1928
1367
|
videoOut: Map<string, Handle<'VideoAsset', 'shared'>>,
|
|
1929
1368
|
): Map<string, Handle<'TextureAsset', 'shared'>> {
|
|
1930
1369
|
const fields =
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1370
|
+
materialTextureFields(
|
|
1371
|
+
shaderId,
|
|
1372
|
+
paramSchema !== undefined
|
|
1934
1373
|
? derive(paramSchema).textureFieldNames
|
|
1935
|
-
:
|
|
1936
|
-
?
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
if (materialTextureValue(value) !== undefined) return true;
|
|
1940
|
-
if (typeof value === 'number') {
|
|
1941
|
-
const resolved = resolveRenderAsset<Asset>(
|
|
1942
|
-
world,
|
|
1943
|
-
value as Handle<string, 'shared'>,
|
|
1944
|
-
);
|
|
1945
|
-
return (
|
|
1946
|
-
resolved.ok &&
|
|
1947
|
-
(resolved.value.kind === 'texture' || resolved.value.kind === 'video')
|
|
1948
|
-
);
|
|
1949
|
-
}
|
|
1950
|
-
if (typeof value === 'string') {
|
|
1951
|
-
const payload = assetsRef.lookup(value);
|
|
1952
|
-
return payload?.kind === 'texture' || payload?.kind === 'video';
|
|
1953
|
-
}
|
|
1954
|
-
return false;
|
|
1955
|
-
})
|
|
1956
|
-
.map(([field]) => field),
|
|
1957
|
-
)
|
|
1958
|
-
: shaderId !== undefined
|
|
1959
|
-
? assetsRef.materialShaderTextureFieldNames(shaderId)
|
|
1960
|
-
: undefined;
|
|
1374
|
+
: shaderId !== undefined
|
|
1375
|
+
? assetsRef.materialShaderTextureFieldNames(shaderId)
|
|
1376
|
+
: undefined,
|
|
1377
|
+
) ?? BUILTIN_USER_REGION_TEXTURE_FIELDS;
|
|
1961
1378
|
const out = new Map<string, Handle<'TextureAsset', 'shared'>>();
|
|
1962
|
-
for (const field of fields
|
|
1379
|
+
for (const field of fields) {
|
|
1963
1380
|
// D-5: a video-kind paramValue is routed to the transient path (videoOut),
|
|
1964
1381
|
// NOT minted as a TextureAsset (which would crash the record-stage
|
|
1965
1382
|
// ensureResident, AC-08). A static field falls through to resolveTex.
|
|
@@ -1974,34 +1391,32 @@ function collectUserRegionTextureHandles(
|
|
|
1974
1391
|
return out;
|
|
1975
1392
|
}
|
|
1976
1393
|
|
|
1394
|
+
const ENGINE_INJECTED_TEXTURE_FIELDS = new Set(['emissiveTexture', 'occlusionTexture']);
|
|
1395
|
+
|
|
1396
|
+
function isEngineInjectedTextureField(shaderId: string | undefined, fieldName: string): boolean {
|
|
1397
|
+
return isStandardPbrMaterialShader(shaderId) && ENGINE_INJECTED_TEXTURE_FIELDS.has(fieldName);
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1977
1400
|
function materialParametersToParamSchema(
|
|
1978
1401
|
parameters: readonly MaterialParameter[],
|
|
1979
1402
|
shaderId: string | undefined,
|
|
1980
|
-
assets: RenderAssetPort,
|
|
1981
1403
|
): readonly ParamSchemaEntry[] {
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
}
|
|
1404
|
+
const engineInjectedTextureFields = isStandardPbrMaterialShader(shaderId)
|
|
1405
|
+
? ENGINE_INJECTED_TEXTURE_FIELDS
|
|
1406
|
+
: undefined;
|
|
1986
1407
|
return parameters.flatMap((parameter): ParamSchemaEntry[] => {
|
|
1987
1408
|
// Static values select a cooked specialization and boolean material
|
|
1988
1409
|
// values have no runtime UBO representation in ParamSchemaEntry.
|
|
1989
1410
|
if (parameter.static || parameter.type === 'bool') return [];
|
|
1990
1411
|
if (parameter.type === 'texture') {
|
|
1412
|
+
if (engineInjectedTextureFields?.has(parameter.name) === true) return [];
|
|
1991
1413
|
return [{ name: parameter.name, type: 'texture2d' }];
|
|
1992
1414
|
}
|
|
1993
|
-
const defaultValue = parameter.default;
|
|
1994
|
-
const numericDefault =
|
|
1995
|
-
typeof defaultValue === 'number' ||
|
|
1996
|
-
(Array.isArray(defaultValue) && defaultValue.every((item) => typeof item === 'number'))
|
|
1997
|
-
? { default: defaultValue }
|
|
1998
|
-
: {};
|
|
1999
1415
|
return [
|
|
2000
1416
|
{
|
|
2001
1417
|
name: parameter.name,
|
|
2002
1418
|
type: parameter.type,
|
|
2003
1419
|
...(parameter.colorSpace === undefined ? {} : { colorSpace: parameter.colorSpace }),
|
|
2004
|
-
...numericDefault,
|
|
2005
1420
|
},
|
|
2006
1421
|
];
|
|
2007
1422
|
});
|
|
@@ -2016,7 +1431,7 @@ function materialParametersToParamSchema(
|
|
|
2016
1431
|
function materialColorParameterSchema(
|
|
2017
1432
|
parameters: readonly MaterialParameter[],
|
|
2018
1433
|
shaderId: string | undefined,
|
|
2019
|
-
assets:
|
|
1434
|
+
assets: AssetRegistry,
|
|
2020
1435
|
): readonly MaterialColorParameterSchema[] {
|
|
2021
1436
|
if (shaderId === undefined) return parameters;
|
|
2022
1437
|
const shader = assets.shaderRegistry.findMaterialArtifact(shaderId);
|
|
@@ -2044,13 +1459,13 @@ function isDeepFrozen(value: unknown, seen = new Set<object>()): boolean {
|
|
|
2044
1459
|
|
|
2045
1460
|
/**
|
|
2046
1461
|
* Capture the object identities that make a resolved material stable. The
|
|
2047
|
-
* renderer owns this cache, but the
|
|
1462
|
+
* renderer owns this cache, but the AssetRegistry owns parent GUID lookup, so
|
|
2048
1463
|
* validation must walk the same chain on every cross-frame read. A malformed
|
|
2049
1464
|
* or cyclic chain is deliberately not cacheable.
|
|
2050
1465
|
*/
|
|
2051
1466
|
function captureMaterialCacheChain(
|
|
2052
1467
|
root: MaterialAsset,
|
|
2053
|
-
assets:
|
|
1468
|
+
assets: AssetRegistry,
|
|
2054
1469
|
): readonly MaterialCacheChainIdentity[] | undefined {
|
|
2055
1470
|
const chain: MaterialCacheChainIdentity[] = [];
|
|
2056
1471
|
const visitedParentGuids = new Set<string>();
|
|
@@ -2063,7 +1478,7 @@ function captureMaterialCacheChain(
|
|
|
2063
1478
|
if (parentRef === undefined) return undefined;
|
|
2064
1479
|
if (visitedParentGuids.has(parentGuid)) return undefined;
|
|
2065
1480
|
visitedParentGuids.add(parentGuid);
|
|
2066
|
-
const parent = assets.lookup<Asset>(
|
|
1481
|
+
const parent = assets.lookup<Asset>(parentRef);
|
|
2067
1482
|
if (parent?.kind !== 'material') return undefined;
|
|
2068
1483
|
current = parent;
|
|
2069
1484
|
}
|
|
@@ -2073,11 +1488,15 @@ function captureMaterialCacheChain(
|
|
|
2073
1488
|
function readPersistentMaterialSnapshot(
|
|
2074
1489
|
cache: MaterialSnapshotCache | undefined,
|
|
2075
1490
|
handleRaw: number,
|
|
2076
|
-
assets:
|
|
1491
|
+
assets: AssetRegistry,
|
|
2077
1492
|
): MaterialSnapshotCacheEntry | undefined {
|
|
2078
1493
|
const entry = cache?.get(handleRaw);
|
|
2079
1494
|
if (entry === undefined) return undefined;
|
|
2080
|
-
if (
|
|
1495
|
+
if (
|
|
1496
|
+
entry.crossFrameSafe &&
|
|
1497
|
+
entry.chain !== undefined &&
|
|
1498
|
+
entry.catalogEpoch === assets.catalogEpoch
|
|
1499
|
+
)
|
|
2081
1500
|
return entry;
|
|
2082
1501
|
// The snapshot contains resolved texture and sampler handles as well as the
|
|
2083
1502
|
// material parent chain. Any registry mutation can replace one of those
|
|
@@ -2096,12 +1515,12 @@ function readPersistentMaterialSnapshot(
|
|
|
2096
1515
|
function readStablePersistentMaterialSnapshot(
|
|
2097
1516
|
cache: MaterialSnapshotCache | undefined,
|
|
2098
1517
|
handleRaw: number,
|
|
2099
|
-
assets:
|
|
1518
|
+
assets: AssetRegistry,
|
|
2100
1519
|
): MaterialSnapshotCacheEntry | undefined {
|
|
2101
1520
|
const entry = cache?.get(handleRaw);
|
|
2102
1521
|
return entry?.crossFrameSafe === true &&
|
|
2103
1522
|
entry.chain !== undefined &&
|
|
2104
|
-
entry.catalogEpoch === assets.
|
|
1523
|
+
entry.catalogEpoch === assets.catalogEpoch
|
|
2105
1524
|
? entry
|
|
2106
1525
|
: undefined;
|
|
2107
1526
|
}
|
|
@@ -2112,7 +1531,7 @@ function storeMaterialSnapshot(
|
|
|
2112
1531
|
snapshot: MaterialSnapshot,
|
|
2113
1532
|
passes: readonly MaterialPass[],
|
|
2114
1533
|
root: MaterialAsset,
|
|
2115
|
-
assets:
|
|
1534
|
+
assets: AssetRegistry,
|
|
2116
1535
|
): MaterialSnapshotCacheEntry {
|
|
2117
1536
|
const chain = captureMaterialCacheChain(root, assets);
|
|
2118
1537
|
const entry: MaterialSnapshotCacheEntry = {
|
|
@@ -2120,7 +1539,7 @@ function storeMaterialSnapshot(
|
|
|
2120
1539
|
passes,
|
|
2121
1540
|
crossFrameSafe: chain?.every(({ asset }) => isDeepFrozen(asset)) === true,
|
|
2122
1541
|
...(chain === undefined ? {} : { chain }),
|
|
2123
|
-
...(chain === undefined ? {} : { catalogEpoch: assets.
|
|
1542
|
+
...(chain === undefined ? {} : { catalogEpoch: assets.catalogEpoch }),
|
|
2124
1543
|
};
|
|
2125
1544
|
cache.set(handleRaw, entry);
|
|
2126
1545
|
return entry;
|
|
@@ -2136,124 +1555,211 @@ function storeMaterialSnapshot(
|
|
|
2136
1555
|
* UBO upload, so this helper produces a plain MaterialSnapshot only).
|
|
2137
1556
|
*
|
|
2138
1557
|
* Returns `defaultMaterialSnapshot()` (mid-grey unlit) for handle=0 (case-B
|
|
2139
|
-
* sentinel, mirroring the inline path) and on
|
|
1558
|
+
* sentinel, mirroring the inline path) and on each unresolved / non-material
|
|
2140
1559
|
* asset (so a partially-mis-registered multi-material entity still renders
|
|
2141
1560
|
* the resolvable submeshes; the count-mismatch validator already filtered
|
|
2142
1561
|
* the count-disagreement case earlier).
|
|
2143
1562
|
*/
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
readStablePersistentMaterialSnapshot,
|
|
2147
|
-
readPersistentMaterialSnapshot,
|
|
2148
|
-
storeMaterialSnapshot,
|
|
2149
|
-
runtimeMaterialShaderId,
|
|
2150
|
-
pipelineRenderState,
|
|
2151
|
-
materialColorParameterSchema,
|
|
2152
|
-
materialNormalScale,
|
|
2153
|
-
materialParametersToParamSchema,
|
|
2154
|
-
collectUserRegionTextureHandles,
|
|
2155
|
-
collectMaterialTextureSamplers,
|
|
2156
|
-
materialTextureRef,
|
|
2157
|
-
collectMaterialTextureCoordinates,
|
|
2158
|
-
internSharedRefFromGuid,
|
|
2159
|
-
});
|
|
2160
|
-
|
|
2161
|
-
export const resolveMaterialSnapshot = materialInterfaceOwner.resolveMaterialSnapshot;
|
|
2162
|
-
|
|
2163
|
-
function materialPassTopologySignature(passes: readonly MaterialPass[]): string {
|
|
2164
|
-
return JSON.stringify(
|
|
2165
|
-
passes.map((pass) => {
|
|
2166
|
-
const state = pass.renderState as
|
|
2167
|
-
| (MaterialRenderState & {
|
|
2168
|
-
readonly tags?: Readonly<Record<string, string>>;
|
|
2169
|
-
readonly queue?: number;
|
|
2170
|
-
readonly stencilReference?: number;
|
|
2171
|
-
})
|
|
2172
|
-
| undefined;
|
|
2173
|
-
return {
|
|
2174
|
-
name: pass.name,
|
|
2175
|
-
module: pass.program.module,
|
|
2176
|
-
vertexEntry: pass.program.vertexEntry,
|
|
2177
|
-
fragmentEntry: pass.program.fragmentEntry,
|
|
2178
|
-
moduleSlots: pass.program.moduleSlots,
|
|
2179
|
-
queue: state?.queue,
|
|
2180
|
-
tags: state?.tags,
|
|
2181
|
-
renderState: pipelineRenderState(state),
|
|
2182
|
-
stencilReference: state?.stencilReference,
|
|
2183
|
-
};
|
|
2184
|
-
}),
|
|
2185
|
-
);
|
|
2186
|
-
}
|
|
2187
|
-
|
|
2188
|
-
export function refreshRenderableMaterialSnapshot(
|
|
2189
|
-
renderable: RenderableSnapshot,
|
|
2190
|
-
materialHandle: number,
|
|
1563
|
+
export function resolveMaterialSnapshot(
|
|
1564
|
+
handleRaw: number,
|
|
2191
1565
|
world: World,
|
|
2192
|
-
|
|
2193
|
-
|
|
1566
|
+
assetsRef: AssetRegistry,
|
|
1567
|
+
materialSnapshotCache?: MaterialSnapshotCache,
|
|
2194
1568
|
persistentMaterialSnapshotCache?: MaterialSnapshotCache,
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
const
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
? undefined
|
|
2201
|
-
: materialPassTopologySignature(previousCacheEntry.passes);
|
|
2202
|
-
persistentMaterialSnapshotCache?.delete(materialHandle);
|
|
2203
|
-
const refreshed = resolveMaterialSnapshot(
|
|
2204
|
-
materialHandle,
|
|
2205
|
-
world,
|
|
2206
|
-
assets,
|
|
2207
|
-
gpuStore,
|
|
2208
|
-
new Map(),
|
|
1569
|
+
): MaterialSnapshot {
|
|
1570
|
+
if (handleRaw === 0) return defaultMaterialSnapshot(handleRaw);
|
|
1571
|
+
const cached = materialSnapshotCache?.get(handleRaw);
|
|
1572
|
+
if (cached !== undefined) return cached.snapshot;
|
|
1573
|
+
const stablePersistentCached = readStablePersistentMaterialSnapshot(
|
|
2209
1574
|
persistentMaterialSnapshotCache,
|
|
2210
|
-
|
|
1575
|
+
handleRaw,
|
|
1576
|
+
assetsRef,
|
|
2211
1577
|
);
|
|
2212
|
-
|
|
2213
|
-
(
|
|
1578
|
+
if (stablePersistentCached !== undefined) {
|
|
1579
|
+
materialSnapshotCache?.set(handleRaw, stablePersistentCached);
|
|
1580
|
+
return stablePersistentCached.snapshot;
|
|
1581
|
+
}
|
|
1582
|
+
const tagged = toShared<'MaterialAsset'>(handleRaw);
|
|
1583
|
+
const res = resolveAssetHandle(world, tagged);
|
|
1584
|
+
if (!res.ok) return defaultMaterialSnapshot(handleRaw);
|
|
1585
|
+
const asset = res.value;
|
|
1586
|
+
if (asset.kind !== 'material') return defaultMaterialSnapshot(handleRaw);
|
|
1587
|
+
const persistentCached = readPersistentMaterialSnapshot(
|
|
1588
|
+
persistentMaterialSnapshotCache,
|
|
1589
|
+
handleRaw,
|
|
1590
|
+
assetsRef,
|
|
2214
1591
|
);
|
|
2215
|
-
if (
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
1592
|
+
if (persistentCached !== undefined) {
|
|
1593
|
+
materialSnapshotCache?.set(handleRaw, persistentCached);
|
|
1594
|
+
return persistentCached.snapshot;
|
|
1595
|
+
}
|
|
1596
|
+
const resolvedResult = walkMaterialPassesOverSharedRefs(world, tagged, assetsRef);
|
|
1597
|
+
if (!resolvedResult.ok) return defaultMaterialSnapshot(handleRaw);
|
|
1598
|
+
const resolved = resolvedResult.value;
|
|
1599
|
+
const allPasses = resolved.passes;
|
|
1600
|
+
const firstPassShader =
|
|
1601
|
+
allPasses.length > 0
|
|
1602
|
+
? runtimeMaterialShaderId(allPasses[0]?.program.module, allPasses[0]?.name)
|
|
1603
|
+
: undefined;
|
|
1604
|
+
const pv = materialValuesToLinearRuntime(
|
|
1605
|
+
resolved.values,
|
|
1606
|
+
materialColorParameterSchema(resolved.parameters ?? [], firstPassShader, assetsRef),
|
|
1607
|
+
resolved.colorSpace,
|
|
1608
|
+
) as Readonly<Record<string, unknown>>;
|
|
1609
|
+
const baseColorPv = pv.baseColor as readonly number[] | undefined;
|
|
1610
|
+
const baseColor = vec3.create(
|
|
1611
|
+
baseColorPv?.[0] ?? 1,
|
|
1612
|
+
baseColorPv?.[1] ?? 1,
|
|
1613
|
+
baseColorPv?.[2] ?? 1,
|
|
1614
|
+
);
|
|
1615
|
+
const metallicPv = typeof pv.metallic === 'number' ? pv.metallic : 0;
|
|
1616
|
+
const roughnessPv = typeof pv.roughness === 'number' ? pv.roughness : 0.5;
|
|
1617
|
+
const clearcoatPv = typeof pv.clearcoat === 'number' ? pv.clearcoat : 0;
|
|
1618
|
+
const clearcoatRoughnessPv =
|
|
1619
|
+
typeof pv.clearcoatRoughness === 'number' ? pv.clearcoatRoughness : 0.5;
|
|
1620
|
+
const specularTintPv = pv.specularTint as readonly number[] | undefined;
|
|
1621
|
+
const normalScalePv = materialNormalScale(pv);
|
|
1622
|
+
|
|
1623
|
+
const paramSnap: Record<string, number | number[] | string> = {};
|
|
1624
|
+
for (const [k, v] of Object.entries(pv)) {
|
|
1625
|
+
if (typeof v === 'number') paramSnap[k] = v;
|
|
1626
|
+
else if (typeof v === 'string') paramSnap[k] = v;
|
|
1627
|
+
else if (Array.isArray(v) && v.every((x) => typeof x === 'number')) {
|
|
1628
|
+
paramSnap[k] = v as number[];
|
|
2227
1629
|
}
|
|
2228
1630
|
}
|
|
2229
|
-
const
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
1631
|
+
const materialParamSchema = materialParametersToParamSchema(
|
|
1632
|
+
resolved.parameters ?? [],
|
|
1633
|
+
firstPassShader,
|
|
1634
|
+
);
|
|
1635
|
+
// feat-20260614 M8 (D-19): texture / sampler values are embedded GUIDs
|
|
1636
|
+
// (dash-form strings) after loadByGuid. Resolve each to a user-tier column
|
|
1637
|
+
// handle by looking up the catalogued payload and minting via
|
|
1638
|
+
// world.allocSharedRef; a numeric value (already a column handle from a
|
|
1639
|
+
// directly-minted material) passes through unchanged.
|
|
1640
|
+
const resolveTexLike = <B extends string>(
|
|
1641
|
+
value: unknown,
|
|
1642
|
+
brand: B,
|
|
1643
|
+
): Handle<B, 'shared'> | undefined => {
|
|
1644
|
+
if (typeof value === 'number') return toShared<B>(value);
|
|
1645
|
+
if (typeof value === 'string') {
|
|
1646
|
+
// M4: intern the GUID -> column-handle resolution so each unique
|
|
1647
|
+
// (world, guid, brand) mints exactly ONE stable handle reused across
|
|
1648
|
+
// frames (stops the per-frame slot churn that defeated the GPU
|
|
1649
|
+
// residency cache). feat-20260619 M2 / w8: TextureAsset brand wires
|
|
1650
|
+
// onLastRelease -> gpuStore.evictTexture; other brands (SamplerAsset)
|
|
1651
|
+
// bypass (releaseUnreferenced-fallback lifecycle).
|
|
1652
|
+
return internSharedRefFromGuid(world, assetsRef, value, brand);
|
|
1653
|
+
}
|
|
1654
|
+
return undefined;
|
|
1655
|
+
};
|
|
1656
|
+
// feat-20260621-learn-render-5-5-parallax M2 / w7 (D-3): iterate the
|
|
1657
|
+
// shader's derive(paramSchema).textureFieldNames SSOT instead of a hardcoded
|
|
1658
|
+
// user-region field list, so an Nth texture (e.g. parallax heightTexture)
|
|
1659
|
+
// resolves through the same path. emissive/occlusion are engine-injection
|
|
1660
|
+
// (lightmap) textures, NOT in textureFieldNames, so they keep named reads.
|
|
1661
|
+
const videoTextureFields = new Map<string, Handle<'VideoAsset', 'shared'>>();
|
|
1662
|
+
const textureHandles = collectUserRegionTextureHandles(
|
|
1663
|
+
pv,
|
|
1664
|
+
firstPassShader,
|
|
1665
|
+
materialParamSchema.length > 0 ? materialParamSchema : undefined,
|
|
1666
|
+
assetsRef,
|
|
1667
|
+
world,
|
|
1668
|
+
resolveTexLike,
|
|
1669
|
+
videoTextureFields,
|
|
1670
|
+
);
|
|
1671
|
+
const samplerHandles = collectMaterialTextureSamplers(pv, (value) =>
|
|
1672
|
+
resolveTexLike(materialTextureRef(value), 'SamplerAsset'),
|
|
1673
|
+
);
|
|
1674
|
+
const emissiveTextureHandle = resolveTexLike(
|
|
1675
|
+
materialTextureRef(pv.emissiveTexture),
|
|
1676
|
+
'TextureAsset',
|
|
1677
|
+
);
|
|
1678
|
+
const occlusionTextureHandle = resolveTexLike(
|
|
1679
|
+
materialTextureRef(pv.occlusionTexture),
|
|
1680
|
+
'TextureAsset',
|
|
1681
|
+
);
|
|
1682
|
+
const textureCoordinates = collectMaterialTextureCoordinates(pv);
|
|
1683
|
+
// Named user-region fields are derived from the map (sprite + legacy reads).
|
|
1684
|
+
const baseColorTextureHandle = textureHandles.get('baseColorTexture');
|
|
1685
|
+
const metallicRoughnessTextureHandle = textureHandles.get('metallicRoughnessTexture');
|
|
1686
|
+
const normalTextureHandle = textureHandles.get('normalTexture');
|
|
1687
|
+
const emissivePv = pv.emissive as readonly number[] | undefined;
|
|
1688
|
+
const snapshot: MaterialSnapshot = {
|
|
1689
|
+
baseColor,
|
|
1690
|
+
metallic: metallicPv,
|
|
1691
|
+
roughness: roughnessPv,
|
|
1692
|
+
clearcoat: clearcoatPv,
|
|
1693
|
+
clearcoatRoughness: clearcoatRoughnessPv,
|
|
1694
|
+
...(specularTintPv !== undefined && {
|
|
1695
|
+
specularTint: [
|
|
1696
|
+
specularTintPv[0] ?? 1,
|
|
1697
|
+
specularTintPv[1] ?? 1,
|
|
1698
|
+
specularTintPv[2] ?? 1,
|
|
1699
|
+
] as readonly [number, number, number],
|
|
1700
|
+
}),
|
|
1701
|
+
normalScale: normalScalePv,
|
|
1702
|
+
materialShaderId: firstPassShader,
|
|
1703
|
+
materialHandle: handleRaw,
|
|
1704
|
+
renderState: pipelineRenderState(allPasses[0]?.renderState),
|
|
1705
|
+
paramSnapshot: paramSnap,
|
|
1706
|
+
...(materialParamSchema.length > 0 && { materialParamSchema }),
|
|
1707
|
+
...(textureCoordinates.size > 0 && { textureCoordinates }),
|
|
1708
|
+
...(textureHandles.size > 0 && { textureHandles }),
|
|
1709
|
+
...(videoTextureFields.size > 0 && { videoTextureFields }),
|
|
1710
|
+
...(samplerHandles.size > 0 && { samplerHandles }),
|
|
1711
|
+
...(baseColorTextureHandle !== undefined && { baseColorTexture: baseColorTextureHandle }),
|
|
1712
|
+
...(metallicRoughnessTextureHandle !== undefined && {
|
|
1713
|
+
metallicRoughnessTexture: metallicRoughnessTextureHandle,
|
|
1714
|
+
}),
|
|
1715
|
+
...(normalTextureHandle !== undefined && { normalTexture: normalTextureHandle }),
|
|
1716
|
+
...(emissivePv !== undefined && {
|
|
1717
|
+
emissive: [emissivePv[0] ?? 0, emissivePv[1] ?? 0, emissivePv[2] ?? 0] as readonly [
|
|
1718
|
+
number,
|
|
1719
|
+
number,
|
|
1720
|
+
number,
|
|
1721
|
+
],
|
|
1722
|
+
}),
|
|
1723
|
+
...(typeof pv.emissiveIntensity === 'number' && { emissiveIntensity: pv.emissiveIntensity }),
|
|
1724
|
+
...(emissiveTextureHandle !== undefined && { emissiveTexture: emissiveTextureHandle }),
|
|
1725
|
+
...(occlusionTextureHandle !== undefined && { occlusionTexture: occlusionTextureHandle }),
|
|
1726
|
+
...(typeof pv.occlusionStrength === 'number' && { occlusionStrength: pv.occlusionStrength }),
|
|
1727
|
+
// feat-city-glb Bug 5 (per-submesh transparency): derive `transparent`
|
|
1728
|
+
// from the first pass's `renderState.blend` presence, identical to the
|
|
1729
|
+
// entity-level snapshot builder (extractFrame archetype loop). Without
|
|
1730
|
+
// this, per-submesh materials (materials[i>=1], e.g. a glTF BLEND decal
|
|
1731
|
+
// submesh on a multi-material mesh) never carry the transparent flag, so
|
|
1732
|
+
// the record stage's LDR split + blend routing treats them as opaque and
|
|
1733
|
+
// their alpha=0 texels composite as black.
|
|
1734
|
+
transparent: allPasses[0]?.renderState?.blend !== undefined,
|
|
2256
1735
|
};
|
|
1736
|
+
const stored =
|
|
1737
|
+
materialSnapshotCache !== undefined
|
|
1738
|
+
? storeMaterialSnapshot(
|
|
1739
|
+
materialSnapshotCache,
|
|
1740
|
+
handleRaw,
|
|
1741
|
+
snapshot,
|
|
1742
|
+
allPasses,
|
|
1743
|
+
asset,
|
|
1744
|
+
assetsRef,
|
|
1745
|
+
)
|
|
1746
|
+
: undefined;
|
|
1747
|
+
const cacheEntry =
|
|
1748
|
+
stored ??
|
|
1749
|
+
(persistentMaterialSnapshotCache !== undefined
|
|
1750
|
+
? storeMaterialSnapshot(
|
|
1751
|
+
persistentMaterialSnapshotCache,
|
|
1752
|
+
handleRaw,
|
|
1753
|
+
snapshot,
|
|
1754
|
+
allPasses,
|
|
1755
|
+
asset,
|
|
1756
|
+
assetsRef,
|
|
1757
|
+
)
|
|
1758
|
+
: undefined);
|
|
1759
|
+
if (cacheEntry?.crossFrameSafe === true) {
|
|
1760
|
+
persistentMaterialSnapshotCache?.set(handleRaw, cacheEntry);
|
|
1761
|
+
}
|
|
1762
|
+
return snapshot;
|
|
2257
1763
|
}
|
|
2258
1764
|
|
|
2259
1765
|
/**
|
|
@@ -2269,6 +1775,132 @@ export interface ExtractPipelineSurface {
|
|
|
2269
1775
|
readonly skinPaletteAllocator: SkinPaletteAllocator | null;
|
|
2270
1776
|
}
|
|
2271
1777
|
|
|
1778
|
+
/**
|
|
1779
|
+
* PSSM (Parallel-Split Shadow Maps) split plane computation.
|
|
1780
|
+
*
|
|
1781
|
+
* Formula (plan-strategy D-8, research F1):
|
|
1782
|
+
* C_i = λ·n·(f/n)^(i/m) + (1-λ)·(n + i/m·(f-n))
|
|
1783
|
+
* where i = 1..m, n = near, f = far, m = cascadeCount
|
|
1784
|
+
*
|
|
1785
|
+
* The coverage range is [camera near, DirectionalLight.shadowDistance] — the
|
|
1786
|
+
* near end derives from the active camera, the far end is the shadowDistance
|
|
1787
|
+
* knob. Returns strictly monotonic view-space z depths (positive).
|
|
1788
|
+
*
|
|
1789
|
+
* When `far <= near + ε` (ε=1e-6), throws ShadowInvalidConfigError
|
|
1790
|
+
* (charter P3 explicit failure — research F2 guarantees formula stability
|
|
1791
|
+
* but degenerate configuration must be surfaced to the caller).
|
|
1792
|
+
*
|
|
1793
|
+
* @param nearPlane - near of the shadow casting range (from the camera near)
|
|
1794
|
+
* @param farPlane - far of the shadow casting range (DirectionalLight.shadowDistance)
|
|
1795
|
+
* @param cascadeCount - number of cascades (1..4)
|
|
1796
|
+
* @param splitLambda - PSSM split weight (0 = pure uniform, 1 = pure log)
|
|
1797
|
+
* @returns Array of m split depths (view-space z); length = cascadeCount,
|
|
1798
|
+
* guaranteed strictly monotonic, last element = far.
|
|
1799
|
+
*
|
|
1800
|
+
* @internal exported for w6/w7 testing only; production callers route through
|
|
1801
|
+
* extractFrame which validates cascadeCount/splitLambda via component schema.
|
|
1802
|
+
*/
|
|
1803
|
+
/**
|
|
1804
|
+
* Compute the 8 world-space corner points of a camera frustum slice.
|
|
1805
|
+
*
|
|
1806
|
+
* Uses mat4.unproject to map NDC cube corners back to world space.
|
|
1807
|
+
* The slice is defined by view-space nearZ and farZ depths.
|
|
1808
|
+
*
|
|
1809
|
+
* For a WebGPU perspective matrix, ndcZ(viewZ) = camFar * (viewZ - camNear) /
|
|
1810
|
+
* (viewZ * (camFar - camNear)). We compute the ndcZ for each slice boundary
|
|
1811
|
+
* and unproject directly.
|
|
1812
|
+
*
|
|
1813
|
+
* @param vp - camera view-projection matrix (column-major 16 floats)
|
|
1814
|
+
* @param camNear - camera near plane (used to derive NDC mapping)
|
|
1815
|
+
* @param camFar - camera far plane (used to derive NDC mapping)
|
|
1816
|
+
* @param nearZ - near depth of the frustum slice (view-space, positive)
|
|
1817
|
+
* @param farZ - far depth of the frustum slice (view-space, positive)
|
|
1818
|
+
* @returns Array of 8 Vec3 world-space corner positions.
|
|
1819
|
+
*/
|
|
1820
|
+
function computeFrustumCorners(
|
|
1821
|
+
vp: Mat4,
|
|
1822
|
+
camNear: number,
|
|
1823
|
+
camFar: number,
|
|
1824
|
+
nearZ: number,
|
|
1825
|
+
farZ: number,
|
|
1826
|
+
projection: 'perspective' | 'orthographic',
|
|
1827
|
+
): Vec3[] {
|
|
1828
|
+
const invVP = mat4.create();
|
|
1829
|
+
mat4.invert(invVP, vp);
|
|
1830
|
+
const corners: Vec3[] = [];
|
|
1831
|
+
|
|
1832
|
+
// NDC z mapping is projection-dependent (WebGPU clip-space z in [0,1]):
|
|
1833
|
+
// perspective: ndc(z) = camFar * (z - camNear) / (z * (camFar - camNear))
|
|
1834
|
+
// orthographic: ndc(z) = (z - camNear) / (camFar - camNear)
|
|
1835
|
+
// feat-20260613-csm M6 / w22: orthographic cameras silently produced
|
|
1836
|
+
// garbage NDC z (perspective formula divides by viewZ, but ortho NDC is
|
|
1837
|
+
// linear), which mapped the cascade slab back to a degenerate world-space
|
|
1838
|
+
// corner set and the AABB-fit collapsed to near zero -- shadow_caster
|
|
1839
|
+
// wrote its triangles outside the [-1,1] clip volume so the depth
|
|
1840
|
+
// attachment stayed at clear=1.0 (root cause for shadow-m2 / shadow-m3 /
|
|
1841
|
+
// shadow-opt-out dawn red surfaced after w20's host-side ortho fix).
|
|
1842
|
+
const span = camFar - camNear;
|
|
1843
|
+
const ndcNear =
|
|
1844
|
+
projection === 'orthographic'
|
|
1845
|
+
? (nearZ - camNear) / span
|
|
1846
|
+
: (camFar * (nearZ - camNear)) / (nearZ * span);
|
|
1847
|
+
const ndcFar =
|
|
1848
|
+
projection === 'orthographic'
|
|
1849
|
+
? (farZ - camNear) / span
|
|
1850
|
+
: (camFar * (farZ - camNear)) / (farZ * span);
|
|
1851
|
+
|
|
1852
|
+
const signs = [-1, 1];
|
|
1853
|
+
for (const sx of signs) {
|
|
1854
|
+
for (const sy of signs) {
|
|
1855
|
+
corners.push(unprojectNDC(invVP, sx, sy, ndcNear));
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
for (const sx of signs) {
|
|
1859
|
+
for (const sy of signs) {
|
|
1860
|
+
corners.push(unprojectNDC(invVP, sx, sy, ndcFar));
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
return corners;
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
/**
|
|
1868
|
+
* Unproject a single NDC point to world space.
|
|
1869
|
+
*/
|
|
1870
|
+
function unprojectNDC(invVP: Mat4, ndcX: number, ndcY: number, ndcZ: number): Vec3 {
|
|
1871
|
+
const ndc = vec3.create(ndcX, ndcY, ndcZ);
|
|
1872
|
+
const ws = vec3.create();
|
|
1873
|
+
mat4.unproject(ws, ndc, invVP);
|
|
1874
|
+
return ws;
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
export function pssmSplit(
|
|
1878
|
+
nearPlane: number,
|
|
1879
|
+
farPlane: number,
|
|
1880
|
+
cascadeCount: number,
|
|
1881
|
+
splitLambda: number,
|
|
1882
|
+
): Float32Array {
|
|
1883
|
+
const EPS = 1e-6;
|
|
1884
|
+
if (farPlane <= nearPlane + EPS) {
|
|
1885
|
+
throw new ShadowInvalidConfigError('shadowDistance', farPlane, nearPlane + EPS);
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
const result = new Float32Array(cascadeCount);
|
|
1889
|
+
const m = cascadeCount;
|
|
1890
|
+
const n = nearPlane;
|
|
1891
|
+
const f = farPlane;
|
|
1892
|
+
const ratio = f / n;
|
|
1893
|
+
|
|
1894
|
+
for (let i = 1; i <= m; i++) {
|
|
1895
|
+
const t = i / m;
|
|
1896
|
+
const logPart = n * ratio ** t;
|
|
1897
|
+
const uniformPart = n + t * (f - n);
|
|
1898
|
+
result[i - 1] = splitLambda * logPart + (1 - splitLambda) * uniformPart;
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
return result;
|
|
1902
|
+
}
|
|
1903
|
+
|
|
2272
1904
|
// feat-20260612-point-light-shadows-urp-hdrp M1 / T-M1-5 (plan-strategy §D-3,
|
|
2273
1905
|
// requirements §5.3): 6-face view-proj matrix table for one omnidirectional
|
|
2274
1906
|
// shadow caster.
|
|
@@ -2342,8 +1974,196 @@ export function buildPointShadowMatrices(lightPos: Vec3, near: number, far: numb
|
|
|
2342
1974
|
return out;
|
|
2343
1975
|
}
|
|
2344
1976
|
|
|
2345
|
-
|
|
2346
|
-
|
|
1977
|
+
/**
|
|
1978
|
+
* bug-20260710-editor-cross-world-shadow: the camera-frustum subset of
|
|
1979
|
+
* {@link CameraSnapshot} that the directional-CSM matrix builder consumes.
|
|
1980
|
+
* Extracted so the builder can run both inside {@link extractFrame} (single
|
|
1981
|
+
* world) AND at the {@link extractFrames} merge layer, where the camera and the
|
|
1982
|
+
* directional light may originate in DIFFERENT worlds (the editor
|
|
1983
|
+
* editorWorld/sceneWorld super-composite). A cameraless world produces zero
|
|
1984
|
+
* lightViewProj matrices, so the merge layer MUST recompute using the surfaced
|
|
1985
|
+
* camera — see {@link computeDirectionalCsm}.
|
|
1986
|
+
*/
|
|
1987
|
+
export interface CsmCameraData {
|
|
1988
|
+
readonly world: Float32Array;
|
|
1989
|
+
readonly fov: number;
|
|
1990
|
+
readonly aspect: number;
|
|
1991
|
+
readonly near: number;
|
|
1992
|
+
readonly far: number;
|
|
1993
|
+
readonly projection: 'perspective' | 'orthographic';
|
|
1994
|
+
readonly orthoLeft: number;
|
|
1995
|
+
readonly orthoRight: number;
|
|
1996
|
+
readonly orthoBottom: number;
|
|
1997
|
+
readonly orthoTop: number;
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
/**
|
|
2001
|
+
* bug-20260710-editor-cross-world-shadow: the raw directional-shadow config a
|
|
2002
|
+
* DirectionalLight carries, threaded onto {@link ExtractedLights} so the merge
|
|
2003
|
+
* layer can recompute CSM matrices against the surfaced camera. Distinct from
|
|
2004
|
+
* the *computed* outputs (lightViewProj / splitPlanes) also on ExtractedLights.
|
|
2005
|
+
*/
|
|
2006
|
+
export interface DirectionalCsmConfig {
|
|
2007
|
+
readonly cascadeCount: number;
|
|
2008
|
+
readonly splitLambda: number;
|
|
2009
|
+
readonly cascadeBlend: number;
|
|
2010
|
+
readonly mapSize: number;
|
|
2011
|
+
readonly shadowDistance: number;
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
/**
|
|
2015
|
+
* bug-20260710-editor-cross-world-shadow: computed directional-CSM output —
|
|
2016
|
+
* the per-cascade light-view-projection matrices plus the derived cascade
|
|
2017
|
+
* metadata. Returned by {@link computeDirectionalCsm}; `undefined` when the
|
|
2018
|
+
* config or camera is missing (caller leaves the fields undefined).
|
|
2019
|
+
*/
|
|
2020
|
+
export interface DirectionalCsmResult {
|
|
2021
|
+
readonly lightViewProj: Float32Array[];
|
|
2022
|
+
readonly splitPlanes: Float32Array;
|
|
2023
|
+
readonly cascadeCount: number;
|
|
2024
|
+
readonly cascadeBlend: number;
|
|
2025
|
+
readonly shadowMapSize: number;
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
/**
|
|
2029
|
+
* bug-20260710-editor-cross-world-shadow: pure directional-CSM matrix builder,
|
|
2030
|
+
* extracted verbatim from the former inline block in {@link extractFrame} (the
|
|
2031
|
+
* PSSM split + per-cascade frustum-slice AABB fit + orthographic light
|
|
2032
|
+
* projection). It is now a free function so BOTH the per-world extract and the
|
|
2033
|
+
* cross-world merge layer can call it — the merge layer is the only place that
|
|
2034
|
+
* pairs the first-hit directional light of one world with the surfaced camera
|
|
2035
|
+
* of another (editor super-composite). Returns `null` when there is no camera
|
|
2036
|
+
* to fit against (a cameraless world's directional light yields no matrices —
|
|
2037
|
+
* the caller leaves lightViewProj undefined rather than emitting zero matrices
|
|
2038
|
+
* that the WGSL reader would sample as NaN → "fully lit").
|
|
2039
|
+
*
|
|
2040
|
+
* Byte-identical to the prior inline computation for the single-world case
|
|
2041
|
+
* (same PSSM lambda, same toward-light Z reach RC-2 fix, same clip-space
|
|
2042
|
+
* matrix / tile-placement-in-shader split).
|
|
2043
|
+
*/
|
|
2044
|
+
export function computeDirectionalCsm(
|
|
2045
|
+
direction: Vec3,
|
|
2046
|
+
config: DirectionalCsmConfig,
|
|
2047
|
+
cameraData: CsmCameraData | undefined,
|
|
2048
|
+
): DirectionalCsmResult | null {
|
|
2049
|
+
const cascadeCount = Math.round(config.cascadeCount);
|
|
2050
|
+
// Coverage range: near from the active camera near (no separate near knob);
|
|
2051
|
+
// far is the component's shadowDistance. Fallback near (0.1) only when no
|
|
2052
|
+
// camera exists — but matrices are gated on cameraData below anyway.
|
|
2053
|
+
const sNear = cameraData?.near ?? 0.1;
|
|
2054
|
+
const sFar = config.shadowDistance;
|
|
2055
|
+
|
|
2056
|
+
// PSSM split planes: [camera near, shadowDistance], not the camera far.
|
|
2057
|
+
const splits = pssmSplit(sNear, sFar, cascadeCount, config.splitLambda);
|
|
2058
|
+
const paddedSplits = new Float32Array(4);
|
|
2059
|
+
for (let i = 0; i < splits.length; i++) paddedSplits[i] = splits[i] ?? 0;
|
|
2060
|
+
|
|
2061
|
+
// Light view matrix: camera at origin, looking along the light direction.
|
|
2062
|
+
const lightDirN = vec3.normalize(vec3.create(), direction);
|
|
2063
|
+
const lightTarget = vec3.create(lightDirN[0] ?? 0, lightDirN[1] ?? 0, lightDirN[2] ?? 0);
|
|
2064
|
+
const lightView = mat4.create();
|
|
2065
|
+
mat4.lookAt(lightView, vec3.create(0, 0, 0), lightTarget, vec3.create(0, 1, 0));
|
|
2066
|
+
|
|
2067
|
+
// Camera view-projection for frustum-corner computation. Without a camera the
|
|
2068
|
+
// frustum cannot be fit — bail so the caller emits no matrices (undefined),
|
|
2069
|
+
// NOT zero matrices (which read as NaN in the shadow shader → no shadow).
|
|
2070
|
+
if (cameraData === undefined) return null;
|
|
2071
|
+
const camProj = mat4.create();
|
|
2072
|
+
if (cameraData.projection === 'orthographic') {
|
|
2073
|
+
mat4.orthographic(
|
|
2074
|
+
camProj,
|
|
2075
|
+
cameraData.orthoLeft,
|
|
2076
|
+
cameraData.orthoRight,
|
|
2077
|
+
cameraData.orthoBottom,
|
|
2078
|
+
cameraData.orthoTop,
|
|
2079
|
+
cameraData.near,
|
|
2080
|
+
cameraData.far,
|
|
2081
|
+
);
|
|
2082
|
+
} else {
|
|
2083
|
+
mat4.perspective(camProj, cameraData.fov, cameraData.aspect, cameraData.near, cameraData.far);
|
|
2084
|
+
}
|
|
2085
|
+
const camView = mat4.create();
|
|
2086
|
+
mat4.invert(camView, cameraData.world);
|
|
2087
|
+
const cameraVP = mat4.create();
|
|
2088
|
+
mat4.multiply(cameraVP, camProj, camView);
|
|
2089
|
+
|
|
2090
|
+
const resultLightViewProjs: Float32Array[] = [];
|
|
2091
|
+
|
|
2092
|
+
// bug-20260619 RC-2 (AC-05): toward-light Z reach. Extend the near (toward-
|
|
2093
|
+
// light) bound of EVERY cascade to the toward-light extreme of the WHOLE
|
|
2094
|
+
// shadow frustum (sNear..sFar) so casters between the light and a slice are
|
|
2095
|
+
// admitted; X/Y stays per-cascade tight. Larger light-space z == closer to
|
|
2096
|
+
// the light (lookAt forward = eye-target), so the full-frustum max-z is the
|
|
2097
|
+
// toward-light reach used as -maxZ (the ortho near plane) per cascade.
|
|
2098
|
+
let lightSpaceMaxZFull = -Infinity;
|
|
2099
|
+
const fullCorners = computeFrustumCorners(
|
|
2100
|
+
cameraVP,
|
|
2101
|
+
cameraData.near,
|
|
2102
|
+
cameraData.far,
|
|
2103
|
+
sNear,
|
|
2104
|
+
sFar,
|
|
2105
|
+
cameraData.projection,
|
|
2106
|
+
);
|
|
2107
|
+
for (const ws of fullCorners) {
|
|
2108
|
+
const ls = vec3.create();
|
|
2109
|
+
mat4.transformVec3(ls, lightView, ws);
|
|
2110
|
+
if ((ls[2] ?? 0) > lightSpaceMaxZFull) lightSpaceMaxZFull = ls[2] ?? 0;
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
// Pre-allocate 4-cascade array; fill in the effective cascades.
|
|
2114
|
+
for (let cIdx = 0; cIdx < 4; cIdx++) {
|
|
2115
|
+
if (cIdx >= cascadeCount) {
|
|
2116
|
+
resultLightViewProjs.push(new Float32Array(16));
|
|
2117
|
+
continue;
|
|
2118
|
+
}
|
|
2119
|
+
const cascadeNear = cIdx === 0 ? sNear : (splits[cIdx - 1] ?? sFar);
|
|
2120
|
+
const cascadeFar = splits[cIdx] ?? sFar;
|
|
2121
|
+
const corners = computeFrustumCorners(
|
|
2122
|
+
cameraVP,
|
|
2123
|
+
cameraData.near,
|
|
2124
|
+
cameraData.far,
|
|
2125
|
+
cascadeNear,
|
|
2126
|
+
cascadeFar,
|
|
2127
|
+
cameraData.projection,
|
|
2128
|
+
);
|
|
2129
|
+
const lightMVP = mat4.clone(lightView);
|
|
2130
|
+
let minX = Infinity;
|
|
2131
|
+
let maxX = -Infinity;
|
|
2132
|
+
let minY = Infinity;
|
|
2133
|
+
let maxY = -Infinity;
|
|
2134
|
+
let minZ = Infinity;
|
|
2135
|
+
let maxZ = -Infinity;
|
|
2136
|
+
for (const ws of corners) {
|
|
2137
|
+
const ls = vec3.create();
|
|
2138
|
+
mat4.transformVec3(ls, lightMVP, ws);
|
|
2139
|
+
if ((ls[0] ?? 0) < minX) minX = ls[0] ?? 0;
|
|
2140
|
+
if ((ls[0] ?? 0) > maxX) maxX = ls[0] ?? 0;
|
|
2141
|
+
if ((ls[1] ?? 0) < minY) minY = ls[1] ?? 0;
|
|
2142
|
+
if ((ls[1] ?? 0) > maxY) maxY = ls[1] ?? 0;
|
|
2143
|
+
if ((ls[2] ?? 0) < minZ) minZ = ls[2] ?? 0;
|
|
2144
|
+
if ((ls[2] ?? 0) > maxZ) maxZ = ls[2] ?? 0;
|
|
2145
|
+
}
|
|
2146
|
+
// Orthographic projection from light-space AABB. RC-2: whole-frustum
|
|
2147
|
+
// toward-light extreme for the near bound; per-cascade far (minZ) + X/Y.
|
|
2148
|
+
const nearZ = Math.max(maxZ, lightSpaceMaxZFull);
|
|
2149
|
+
const orthoProj = mat4.create();
|
|
2150
|
+
mat4.orthographic(orthoProj, minX, maxX, minY, maxY, -nearZ, -minZ);
|
|
2151
|
+
// lightViewProj = orthoProj * lightView — pure clip-space [-1,1]. Atlas
|
|
2152
|
+
// tile placement is handled by the per-cascade viewport + fragment-side UV
|
|
2153
|
+
// math (evalDirectional), so shadow_caster.gl_Position stays clip-space.
|
|
2154
|
+
void cIdx;
|
|
2155
|
+
resultLightViewProjs.push(new Float32Array(mat4.multiply(mat4.create(), orthoProj, lightView)));
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
return {
|
|
2159
|
+
lightViewProj: resultLightViewProjs,
|
|
2160
|
+
splitPlanes: paddedSplits,
|
|
2161
|
+
cascadeCount,
|
|
2162
|
+
cascadeBlend: config.cascadeBlend,
|
|
2163
|
+
shadowMapSize: config.mapSize,
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2347
2167
|
/**
|
|
2348
2168
|
* feat-20260709-editor-world-partition M1 / w4 (AC-08, plan-strategy §2 D-3):
|
|
2349
2169
|
* the owner index that previously served BOTH cameras and singleton render
|
|
@@ -2360,42 +2180,29 @@ export interface ExtractFramesOwner {
|
|
|
2360
2180
|
}
|
|
2361
2181
|
|
|
2362
2182
|
export interface PreparedExtractContext {
|
|
2363
|
-
|
|
2364
|
-
readonly worldId?: number | undefined;
|
|
2365
|
-
readonly assets: RenderAssetPort | null | undefined;
|
|
2183
|
+
readonly assets: AssetRegistry | null | undefined;
|
|
2366
2184
|
readonly pipelineState: ExtractPipelineSurface | null | undefined;
|
|
2367
|
-
readonly gpuStore: import('./gpu-resource-store').GpuResourceStore | undefined;
|
|
2368
2185
|
readonly materialSnapshotCache: MaterialSnapshotCache | undefined;
|
|
2369
2186
|
readonly cull: 'self' | 'none' | 'external';
|
|
2370
2187
|
readonly cullCameras: readonly CameraSnapshot[] | undefined;
|
|
2371
2188
|
readonly hierarchy: SceneHierarchySnapshot;
|
|
2372
2189
|
readonly visibility: VisibilitySnapshot;
|
|
2373
|
-
/** @internal Debug-only ParamSchema compatibility census. */
|
|
2374
|
-
readonly paramSchemaProjectionCensus?: ParamSchemaProjectionCensus | undefined;
|
|
2375
|
-
/** @internal Refresh only compact lighting resources over an existing frame. */
|
|
2376
|
-
readonly resourceRefreshBase?: ExtractedFrame;
|
|
2377
|
-
/**
|
|
2378
|
-
* @internal Restrict the MeshRenderer row source to known entities. The
|
|
2379
|
-
* query is addressed through Query.at(), so this path does not iterate the
|
|
2380
|
-
* world's renderable tables. Resource queries remain unchanged.
|
|
2381
|
-
*/
|
|
2382
|
-
readonly targetRenderableEntities?: readonly EntityHandle[];
|
|
2383
2190
|
}
|
|
2384
2191
|
|
|
2385
2192
|
export function extractCameraSnapshots(world: World): CameraSnapshot[] {
|
|
2193
|
+
const worldInternal = createWorldInternalView(world);
|
|
2386
2194
|
const cameras: CameraSnapshot[] = [];
|
|
2387
2195
|
const cameraEntities: number[] = [];
|
|
2388
|
-
const cameraQuery = world.query({ read: [Camera, Transform] }).unwrap();
|
|
2196
|
+
const cameraQuery = world.query({ read: [Camera], with: [Transform] }).unwrap();
|
|
2389
2197
|
for (const row of cameraQuery) {
|
|
2390
2198
|
const cam = row.get(Camera);
|
|
2391
2199
|
const entity = row.entity;
|
|
2392
|
-
const
|
|
2393
|
-
if (
|
|
2394
|
-
const worldMat = new Float32Array(
|
|
2200
|
+
const view = worldInternal._getArrayView(entity, Transform, 'world');
|
|
2201
|
+
if (view === undefined) continue;
|
|
2202
|
+
const worldMat = new Float32Array(view);
|
|
2395
2203
|
cameras.push({
|
|
2396
|
-
worldId: 0,
|
|
2397
2204
|
entityKey: entity as number,
|
|
2398
|
-
position: mat4.getTranslation(vec3.create(), worldMat
|
|
2205
|
+
position: mat4.getTranslation(vec3.create(), worldMat),
|
|
2399
2206
|
world: worldMat,
|
|
2400
2207
|
fov: cam.fov,
|
|
2401
2208
|
aspect: cam.aspect,
|
|
@@ -2410,7 +2217,6 @@ export function extractCameraSnapshots(world: World): CameraSnapshot[] {
|
|
|
2410
2217
|
exposure: cam.exposure,
|
|
2411
2218
|
whitePoint: cam.whitePoint,
|
|
2412
2219
|
antialias: antialiasFromF32(cam.antialias),
|
|
2413
|
-
historyVersion: cam.historyVersion,
|
|
2414
2220
|
bloom: bloomEnabledFromF32(cam.bloom),
|
|
2415
2221
|
bloomThreshold: cam.bloomThreshold,
|
|
2416
2222
|
bloomIntensity: cam.bloomIntensity,
|
|
@@ -2440,114 +2246,35 @@ function tonemapParams(camera: CameraSnapshot): Uint8Array {
|
|
|
2440
2246
|
return new Uint8Array(bytes);
|
|
2441
2247
|
}
|
|
2442
2248
|
|
|
2443
|
-
function extractPostProcessParams(
|
|
2444
|
-
world: World,
|
|
2445
|
-
cameras: readonly CameraSnapshot[],
|
|
2446
|
-
): Map<string, Uint8Array> {
|
|
2447
|
-
const postProcessParams = new Map<string, Uint8Array>();
|
|
2448
|
-
const query = world.query({ with: [PostProcessParams] }).unwrap();
|
|
2449
|
-
for (const row of query) {
|
|
2450
|
-
const read = world.get(row.entity, PostProcessParams);
|
|
2451
|
-
if (!read.ok) continue;
|
|
2452
|
-
postProcessParams.set(read.value.shader, read.value.data);
|
|
2453
|
-
}
|
|
2454
|
-
const tonemapCamera = cameras[0];
|
|
2455
|
-
if (tonemapCamera !== undefined) {
|
|
2456
|
-
postProcessParams.set('forgeax::tonemap', tonemapParams(tonemapCamera));
|
|
2457
|
-
}
|
|
2458
|
-
return postProcessParams;
|
|
2459
|
-
}
|
|
2460
|
-
|
|
2461
|
-
export function refreshExtractedFrameCameras(world: World, frame: ExtractedFrame): ExtractedFrame {
|
|
2462
|
-
const cameras = extractCameraSnapshots(world);
|
|
2463
|
-
const camera = cameras[0];
|
|
2464
|
-
const config = frame.lights.directionalCsmConfig;
|
|
2465
|
-
const direction = frame.lights.directionalCsmDirection;
|
|
2466
|
-
const csm =
|
|
2467
|
-
camera === undefined || config === undefined || direction === undefined
|
|
2468
|
-
? null
|
|
2469
|
-
: computeDirectionalCsm(direction, config, camera);
|
|
2470
|
-
const postProcessParams = new Map(frame.postProcessParams);
|
|
2471
|
-
postProcessParams.delete('forgeax::tonemap');
|
|
2472
|
-
if (camera !== undefined) postProcessParams.set('forgeax::tonemap', tonemapParams(camera));
|
|
2473
|
-
return {
|
|
2474
|
-
...frame,
|
|
2475
|
-
cameras,
|
|
2476
|
-
lights: {
|
|
2477
|
-
...frame.lights,
|
|
2478
|
-
lightViewProj: csm?.lightViewProj,
|
|
2479
|
-
splitPlanes: csm?.splitPlanes,
|
|
2480
|
-
cascadeCount: csm?.cascadeCount,
|
|
2481
|
-
cascadeBlend: csm?.cascadeBlend,
|
|
2482
|
-
shadowMapSize: csm?.shadowMapSize,
|
|
2483
|
-
},
|
|
2484
|
-
postProcessParams,
|
|
2485
|
-
};
|
|
2486
|
-
}
|
|
2487
|
-
|
|
2488
|
-
/**
|
|
2489
|
-
* Refresh only the resource-owned PostProcessParams map after an ECS journal
|
|
2490
|
-
* update. Post-process parameter entities are not render-domain slots; making
|
|
2491
|
-
* their transient byte payload visible must not rebuild candidates, dispatch,
|
|
2492
|
-
* topology, or the GPU scene.
|
|
2493
|
-
*/
|
|
2494
|
-
export function refreshExtractedFramePostProcessParams(
|
|
2495
|
-
world: World,
|
|
2496
|
-
frame: ExtractedFrame,
|
|
2497
|
-
): ExtractedFrame {
|
|
2498
|
-
const postProcessParams = new Map<string, Uint8Array>();
|
|
2499
|
-
const query = world.query({ with: [PostProcessParams] }).unwrap();
|
|
2500
|
-
for (const row of query) {
|
|
2501
|
-
const read = world.get(row.entity, PostProcessParams);
|
|
2502
|
-
if (!read.ok) continue;
|
|
2503
|
-
postProcessParams.set(read.value.shader, read.value.data);
|
|
2504
|
-
}
|
|
2505
|
-
const tonemap = frame.postProcessParams.get('forgeax::tonemap');
|
|
2506
|
-
if (tonemap !== undefined) postProcessParams.set('forgeax::tonemap', tonemap);
|
|
2507
|
-
return { ...frame, postProcessParams };
|
|
2508
|
-
}
|
|
2509
|
-
|
|
2510
2249
|
export function prepareExtractContext(
|
|
2511
2250
|
world: World,
|
|
2512
2251
|
options: {
|
|
2513
|
-
readonly
|
|
2514
|
-
readonly assets?: RenderAssetPort | null;
|
|
2252
|
+
readonly assets?: AssetRegistry | null;
|
|
2515
2253
|
readonly pipelineState?: ExtractPipelineSurface | null;
|
|
2516
|
-
readonly gpuStore?: import('./gpu-resource-store').GpuResourceStore;
|
|
2517
2254
|
readonly materialSnapshotCache?: MaterialSnapshotCache;
|
|
2518
2255
|
readonly cull?: 'self' | 'none' | 'external';
|
|
2519
2256
|
readonly cullCameras?: readonly CameraSnapshot[];
|
|
2520
|
-
readonly paramSchemaProjectionCensus?: ParamSchemaProjectionCensus;
|
|
2521
2257
|
} = {},
|
|
2522
2258
|
): PreparedExtractContext {
|
|
2523
2259
|
const hierarchy = projectHierarchy(world);
|
|
2524
2260
|
return {
|
|
2525
|
-
worldId: options.worldId ?? 0,
|
|
2526
2261
|
assets: options.assets,
|
|
2527
2262
|
pipelineState: options.pipelineState,
|
|
2528
|
-
gpuStore: options.gpuStore,
|
|
2529
2263
|
materialSnapshotCache: options.materialSnapshotCache,
|
|
2530
2264
|
cull: options.cull ?? 'self',
|
|
2531
2265
|
cullCameras: options.cullCameras,
|
|
2532
2266
|
hierarchy,
|
|
2533
2267
|
visibility: resolveVisibility(world, hierarchy),
|
|
2534
|
-
...(options.paramSchemaProjectionCensus === undefined
|
|
2535
|
-
? {}
|
|
2536
|
-
: { paramSchemaProjectionCensus: options.paramSchemaProjectionCensus }),
|
|
2537
2268
|
};
|
|
2538
2269
|
}
|
|
2539
2270
|
|
|
2540
2271
|
export function extractFrames(
|
|
2541
2272
|
worlds: readonly World[],
|
|
2542
2273
|
owner: number | ExtractFramesOwner,
|
|
2543
|
-
assets?:
|
|
2274
|
+
assets?: AssetRegistry | null,
|
|
2544
2275
|
pipelineState?: ExtractPipelineSurface | null,
|
|
2545
|
-
gpuStore?: import('./gpu-resource-store').GpuResourceStore,
|
|
2546
2276
|
materialSnapshotCachesByWorld?: MaterialSnapshotCachesByWorld,
|
|
2547
|
-
options: {
|
|
2548
|
-
readonly cull?: 'normal' | 'none';
|
|
2549
|
-
readonly paramSchemaProjectionCensus?: ParamSchemaProjectionCensus;
|
|
2550
|
-
} = {},
|
|
2277
|
+
options: { readonly cull?: 'normal' | 'none' } = {},
|
|
2551
2278
|
): ExtractedFrame {
|
|
2552
2279
|
// w4: normalize the owner argument. A bare number is the legacy single-owner
|
|
2553
2280
|
// form (cameraOwner === resourceOwner); an object carries the two split
|
|
@@ -2559,7 +2286,7 @@ export function extractFrames(
|
|
|
2559
2286
|
// ── D-2: frame-level side effects live here ────────────────────────────
|
|
2560
2287
|
//
|
|
2561
2288
|
// resetForFrame is called exactly once per frame, at the extractFrames
|
|
2562
|
-
// entry. The skinPaletteAllocator cursor is reset before
|
|
2289
|
+
// entry. The skinPaletteAllocator cursor is reset before each per-world
|
|
2563
2290
|
// extract runs, so sequential per-world allocation yields non-overlapping
|
|
2564
2291
|
// palette slices (AC-08).
|
|
2565
2292
|
const skinPaletteAllocator = pipelineState?.skinPaletteAllocator ?? null;
|
|
@@ -2567,55 +2294,7 @@ export function extractFrames(
|
|
|
2567
2294
|
skinPaletteAllocator.resetForFrame();
|
|
2568
2295
|
}
|
|
2569
2296
|
|
|
2570
|
-
// The ordinary game path owns exactly one World. extractFrame already emits
|
|
2571
|
-
// the canonical single-world shape: worldId=0, sorted dispatch, owner
|
|
2572
|
-
// cameras/resources, visibility projections, and CSM matrices fitted to its
|
|
2573
|
-
// own camera. Returning that snapshot directly avoids routing every frame
|
|
2574
|
-
// through the multi-world reconciliation machinery below (ordering arrays,
|
|
2575
|
-
// Maps, and per-renderable/dispatch object copies). This is an intra-frame
|
|
2576
|
-
// structural fast path, not a cross-frame cache; every ECS query and derived
|
|
2577
|
-
// snapshot is still refreshed on every call.
|
|
2578
|
-
const onlyWorld = worlds.length === 1 ? worlds[0] : undefined;
|
|
2579
2297
|
const failedWorlds = new Set<World>();
|
|
2580
|
-
if (onlyWorld !== undefined && cameraOwner === 0 && resourceOwner === 0) {
|
|
2581
|
-
try {
|
|
2582
|
-
const prepared = prepareExtractContext(onlyWorld, {
|
|
2583
|
-
...(assets !== undefined ? { assets } : {}),
|
|
2584
|
-
...(pipelineState !== undefined ? { pipelineState } : {}),
|
|
2585
|
-
...(gpuStore !== undefined ? { gpuStore } : {}),
|
|
2586
|
-
...(materialSnapshotCachesByWorld === undefined
|
|
2587
|
-
? {}
|
|
2588
|
-
: {
|
|
2589
|
-
materialSnapshotCache:
|
|
2590
|
-
materialSnapshotCachesByWorld.get(onlyWorld) ??
|
|
2591
|
-
(() => {
|
|
2592
|
-
const cache: MaterialSnapshotCache = new Map();
|
|
2593
|
-
materialSnapshotCachesByWorld.set(onlyWorld, cache);
|
|
2594
|
-
return cache;
|
|
2595
|
-
})(),
|
|
2596
|
-
}),
|
|
2597
|
-
...(options.cull === 'none' ? { cull: 'none' as const } : {}),
|
|
2598
|
-
...(options.paramSchemaProjectionCensus === undefined
|
|
2599
|
-
? {}
|
|
2600
|
-
: { paramSchemaProjectionCensus: options.paramSchemaProjectionCensus }),
|
|
2601
|
-
});
|
|
2602
|
-
const frame = extractFrame(onlyWorld, prepared);
|
|
2603
|
-
const fog = selectFog(onlyWorld, 0);
|
|
2604
|
-
if (!fog.ok) {
|
|
2605
|
-
routeFogSelectionError(onlyWorld, fog.error);
|
|
2606
|
-
return frame;
|
|
2607
|
-
}
|
|
2608
|
-
return { ...frame, fog: fog.value.snapshot };
|
|
2609
|
-
} catch (err) {
|
|
2610
|
-
try {
|
|
2611
|
-
reportRenderError(err);
|
|
2612
|
-
} catch {
|
|
2613
|
-
// The World error channel may throw under the default policy.
|
|
2614
|
-
}
|
|
2615
|
-
failedWorlds.add(onlyWorld);
|
|
2616
|
-
}
|
|
2617
|
-
}
|
|
2618
|
-
|
|
2619
2298
|
// ── D-2: per-world extract with error isolation ────────────────────────
|
|
2620
2299
|
//
|
|
2621
2300
|
// Each world runs extractFrame over the final state published by
|
|
@@ -2644,10 +2323,8 @@ export function extractFrames(
|
|
|
2644
2323
|
const isCameraOwner = wi === cameraOwner;
|
|
2645
2324
|
const cameraOwnerFrame = framesByWorld.get(cameraOwner);
|
|
2646
2325
|
const prepared = prepareExtractContext(world, {
|
|
2647
|
-
worldId: wi,
|
|
2648
2326
|
...(assets !== undefined ? { assets } : {}),
|
|
2649
2327
|
...(pipelineState !== undefined ? { pipelineState } : {}),
|
|
2650
|
-
...(gpuStore !== undefined ? { gpuStore } : {}),
|
|
2651
2328
|
...(materialSnapshotCachesByWorld === undefined
|
|
2652
2329
|
? {}
|
|
2653
2330
|
: {
|
|
@@ -2661,9 +2338,6 @@ export function extractFrames(
|
|
|
2661
2338
|
}),
|
|
2662
2339
|
cull: options.cull === 'none' ? 'none' : isCameraOwner ? 'self' : 'external',
|
|
2663
2340
|
...(cameraOwnerFrame === undefined ? {} : { cullCameras: cameraOwnerFrame.cameras }),
|
|
2664
|
-
...(options.paramSchemaProjectionCensus === undefined
|
|
2665
|
-
? {}
|
|
2666
|
-
: { paramSchemaProjectionCensus: options.paramSchemaProjectionCensus }),
|
|
2667
2341
|
});
|
|
2668
2342
|
const frame = extractFrame(world, prepared);
|
|
2669
2343
|
|
|
@@ -2671,7 +2345,10 @@ export function extractFrames(
|
|
|
2671
2345
|
} catch (err) {
|
|
2672
2346
|
// Per-world failure: route to world's own error handler, skip contribution.
|
|
2673
2347
|
try {
|
|
2674
|
-
|
|
2348
|
+
createWorldInternalView(world)._routeError(err, {
|
|
2349
|
+
severity: Severity.Error,
|
|
2350
|
+
systemName: `RenderSystem.extractFrames(world[${wi}])`,
|
|
2351
|
+
});
|
|
2675
2352
|
} catch {
|
|
2676
2353
|
// If _routeError itself throws, the world already failed — skip silently.
|
|
2677
2354
|
}
|
|
@@ -2690,9 +2367,7 @@ export function extractFrames(
|
|
|
2690
2367
|
|
|
2691
2368
|
// AC-04: renderables — concat by worlds[] order, stamp worldId.
|
|
2692
2369
|
const renderables: RenderableSnapshot[] = [];
|
|
2693
|
-
const candidateRenderables: RenderableSnapshot[] = [];
|
|
2694
2370
|
const dispatchEntries: DispatchEntry[] = [];
|
|
2695
|
-
const candidateDispatchEntries: DispatchEntry[] = [];
|
|
2696
2371
|
const visibilitySnapshots: VisibilitySnapshot[] = [];
|
|
2697
2372
|
const featureVisibilitySnapshots: RenderFeatureWorldVisibilitySnapshot[] = [];
|
|
2698
2373
|
const hiddenEntityReports: RenderFeatureHiddenEntityReport[] = [];
|
|
@@ -2703,7 +2378,6 @@ export function extractFrames(
|
|
|
2703
2378
|
if (f === undefined || wId === undefined) continue;
|
|
2704
2379
|
|
|
2705
2380
|
const base = renderables.length;
|
|
2706
|
-
const candidateBase = candidateRenderables.length;
|
|
2707
2381
|
const visibilitySnapshot = f.visibilitySnapshots[0];
|
|
2708
2382
|
const world = worlds[wId];
|
|
2709
2383
|
if (visibilitySnapshot !== undefined) {
|
|
@@ -2713,44 +2387,17 @@ export function extractFrames(
|
|
|
2713
2387
|
}
|
|
2714
2388
|
hiddenEntityReports.push(...f.hiddenEntityReports);
|
|
2715
2389
|
for (const r of f.renderables) {
|
|
2716
|
-
renderables.push({
|
|
2717
|
-
...r,
|
|
2718
|
-
worldId: wId,
|
|
2719
|
-
...(r.candidateIndex !== undefined
|
|
2720
|
-
? { candidateIndex: candidateBase + r.candidateIndex }
|
|
2721
|
-
: {}),
|
|
2722
|
-
});
|
|
2723
|
-
}
|
|
2724
|
-
for (const r of f.candidateRenderables ?? f.renderables) {
|
|
2725
|
-
candidateRenderables.push({
|
|
2726
|
-
...r,
|
|
2727
|
-
worldId: wId,
|
|
2728
|
-
...(r.candidateIndex !== undefined
|
|
2729
|
-
? { candidateIndex: candidateBase + r.candidateIndex }
|
|
2730
|
-
: {}),
|
|
2731
|
-
});
|
|
2390
|
+
renderables.push({ ...r, worldId: wId });
|
|
2732
2391
|
}
|
|
2733
2392
|
|
|
2734
2393
|
// D-3: dispatch — per-world renderableIndex rebased by base offset.
|
|
2735
2394
|
for (const d of f.dispatch) {
|
|
2736
2395
|
dispatchEntries.push({ ...d, renderableIndex: (d.renderableIndex ?? 0) + base });
|
|
2737
2396
|
}
|
|
2738
|
-
for (const d of f.candidateDispatch ?? f.dispatch) {
|
|
2739
|
-
candidateDispatchEntries.push({
|
|
2740
|
-
...d,
|
|
2741
|
-
renderableIndex: (d.renderableIndex ?? 0) + candidateBase,
|
|
2742
|
-
});
|
|
2743
|
-
}
|
|
2744
2397
|
}
|
|
2745
2398
|
|
|
2746
2399
|
// Stable sort dispatch by queue value.
|
|
2747
2400
|
dispatchEntries.sort((a, b) => (a.queue ?? 0) - (b.queue ?? 0));
|
|
2748
|
-
candidateDispatchEntries.sort((a, b) => (a.queue ?? 0) - (b.queue ?? 0));
|
|
2749
|
-
const candidateDomain = buildRenderCandidateDomain(
|
|
2750
|
-
candidateRenderables,
|
|
2751
|
-
candidateDispatchEntries,
|
|
2752
|
-
);
|
|
2753
|
-
const candidateDispatchMembership = candidateDomain.membership;
|
|
2754
2401
|
|
|
2755
2402
|
// AC-04: lights — point[]/spot[] concat; directional first-hit in
|
|
2756
2403
|
// succeededFrames order (which preserves worlds[] order for successful
|
|
@@ -2767,7 +2414,6 @@ export function extractFrames(
|
|
|
2767
2414
|
let depthBias: number | undefined;
|
|
2768
2415
|
let normalBias: number | undefined;
|
|
2769
2416
|
let pcfKernelSize: number | undefined;
|
|
2770
|
-
let directionalCsm: DirectionalCsmResult | undefined;
|
|
2771
2417
|
const pointShadow: PointShadowSnapshot[] = [];
|
|
2772
2418
|
// bug-20260710-editor-cross-world-shadow: carry the raw CSM config +
|
|
2773
2419
|
// direction of the first-hit directional so the merge layer can recompute
|
|
@@ -2807,10 +2453,7 @@ export function extractFrames(
|
|
|
2807
2453
|
if (succeededIndices[fi] === cameraOwner) cameraOwnerFrame = succeededFrames[fi];
|
|
2808
2454
|
if (succeededIndices[fi] === resourceOwner) resourceOwnerFrame = succeededFrames[fi];
|
|
2809
2455
|
}
|
|
2810
|
-
const cameras =
|
|
2811
|
-
cameraOwnerFrame !== undefined
|
|
2812
|
-
? cameraOwnerFrame.cameras.map((camera) => ({ ...camera, worldId: cameraOwner }))
|
|
2813
|
-
: [];
|
|
2456
|
+
const cameras = cameraOwnerFrame !== undefined ? [...cameraOwnerFrame.cameras] : [];
|
|
2814
2457
|
|
|
2815
2458
|
// bug-20260710-editor-cross-world-shadow: RECOMPUTE directional CSM matrices
|
|
2816
2459
|
// against the surfaced camera. In a single-world app the per-world extract
|
|
@@ -2842,10 +2485,8 @@ export function extractFrames(
|
|
|
2842
2485
|
directionalCsmDirection,
|
|
2843
2486
|
directionalCsmConfig,
|
|
2844
2487
|
mergeCameraData,
|
|
2845
|
-
renderCandidateCsmCandidates(candidateDomain),
|
|
2846
2488
|
);
|
|
2847
2489
|
if (csm !== null) {
|
|
2848
|
-
directionalCsm = csm;
|
|
2849
2490
|
lightViewProj = csm.lightViewProj;
|
|
2850
2491
|
splitPlanes = csm.splitPlanes;
|
|
2851
2492
|
cascadeCount = csm.cascadeCount;
|
|
@@ -2872,35 +2513,30 @@ export function extractFrames(
|
|
|
2872
2513
|
pointShadow,
|
|
2873
2514
|
directionalCsmConfig,
|
|
2874
2515
|
directionalCsmDirection,
|
|
2875
|
-
...(directionalCsm !== undefined ? { directionalCsm, cascades: directionalCsm.cascades } : {}),
|
|
2876
2516
|
};
|
|
2877
|
-
const
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
worldId: resourceOwner,
|
|
2882
|
-
skylightCount: 0,
|
|
2883
|
-
skyboxCount: 0,
|
|
2884
|
-
} satisfies ExtractedEnvironment);
|
|
2517
|
+
const skylight = resourceOwnerFrame?.skylight;
|
|
2518
|
+
const skylightCount = resourceOwnerFrame?.skylightCount ?? 0;
|
|
2519
|
+
const skybox = resourceOwnerFrame?.skybox;
|
|
2520
|
+
const skyboxCount = resourceOwnerFrame?.skyboxCount ?? 0;
|
|
2885
2521
|
// User-authored PostProcessParams entities are singleton scene resources, so
|
|
2886
2522
|
// they come from the resourceOwner world (holistic snapshot selection).
|
|
2887
2523
|
const postProcessParams = new Map(resourceOwnerFrame?.postProcessParams);
|
|
2888
2524
|
// feat-20260709-editor-world-partition ENGINE-fix-round2 (defect 1): the
|
|
2889
|
-
// engine built-in
|
|
2525
|
+
// engine built-in Standard tonemap param is NOT a scene resource — the
|
|
2890
2526
|
// per-world extractFrame bridges it from that world's own `cameras[0]`
|
|
2891
2527
|
// (Camera.exposure / whitePoint / tonemap is the SSOT). It therefore lives on
|
|
2892
2528
|
// the CAMERA-owner frame, not the resource-owner frame. In the split-owner
|
|
2893
2529
|
// editor topology the resourceOwner world has no Camera, so its frame carries
|
|
2894
|
-
// no
|
|
2530
|
+
// no Standard tonemap entry; taking postProcessParams from resourceOwner
|
|
2895
2531
|
// alone drops it, the tonemap pass's params UBO stays zero-filled
|
|
2896
2532
|
// (exposure=0 => tonemapped output is uniformly black), and the whole frame
|
|
2897
2533
|
// reads black even though geometry drew into the HDR target. Overlay the
|
|
2898
2534
|
// camera-owner frame's tonemap param (its SSOT source) so the surfaced
|
|
2899
2535
|
// camera's exposure/whitePoint/mode reach the tonemap pass. When
|
|
2900
2536
|
// cameraOwner === resourceOwner this is a no-op (identical entry). The 'na'
|
|
2901
|
-
//
|
|
2902
|
-
// bottom of extractFrame (SSOT: same
|
|
2903
|
-
const TONEMAP_PARAM_KEY =
|
|
2537
|
+
// Standard tonemap identity mirrors the engine provider key set at the
|
|
2538
|
+
// bottom of extractFrame (SSOT: same identity, same 16B layout).
|
|
2539
|
+
const TONEMAP_PARAM_KEY = STANDARD_TONEMAP_FEATURE_ID;
|
|
2904
2540
|
const cameraTonemapParam = cameraOwnerFrame?.postProcessParams.get(TONEMAP_PARAM_KEY);
|
|
2905
2541
|
if (cameraTonemapParam !== undefined) {
|
|
2906
2542
|
postProcessParams.set(TONEMAP_PARAM_KEY, cameraTonemapParam);
|
|
@@ -2920,21 +2556,16 @@ export function extractFrames(
|
|
|
2920
2556
|
const visibilityStats = {
|
|
2921
2557
|
explicitlyHidden: succeededFrames.reduce((s, f) => s + f.visibilityStats.explicitlyHidden, 0),
|
|
2922
2558
|
};
|
|
2923
|
-
const resourceWorld = worlds[resourceOwner];
|
|
2924
|
-
const fog = selectFog(resourceWorld, resourceOwner);
|
|
2925
|
-
if (!fog.ok) routeFogSelectionError(resourceWorld, fog.error);
|
|
2926
2559
|
|
|
2927
2560
|
return {
|
|
2928
2561
|
cameras,
|
|
2929
2562
|
lights,
|
|
2930
2563
|
renderables,
|
|
2931
2564
|
dispatch: dispatchEntries,
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
fog: fog.ok ? fog.value.snapshot : undefined,
|
|
2937
|
-
environment,
|
|
2565
|
+
skylight,
|
|
2566
|
+
skylightCount,
|
|
2567
|
+
skybox,
|
|
2568
|
+
skyboxCount,
|
|
2938
2569
|
frustumStats,
|
|
2939
2570
|
visibilityStats,
|
|
2940
2571
|
postProcessParams,
|
|
@@ -2982,6 +2613,36 @@ function hasFiniteOrderedLocalAabb(aabb: Float32Array | undefined): aabb is Floa
|
|
|
2982
2613
|
return minX <= maxX && minY <= maxY && minZ <= maxZ;
|
|
2983
2614
|
}
|
|
2984
2615
|
|
|
2616
|
+
function morphSnapshotFor(
|
|
2617
|
+
mesh: MeshAsset,
|
|
2618
|
+
weights: ArrayLike<number> | undefined,
|
|
2619
|
+
): MorphSnapshot | undefined {
|
|
2620
|
+
const targets = mesh.morphTargets;
|
|
2621
|
+
if (targets === undefined || targets.length === 0 || weights === undefined) return undefined;
|
|
2622
|
+
if (weights.length !== targets.length) return undefined;
|
|
2623
|
+
const firstPositions = targets[0]?.position;
|
|
2624
|
+
if (
|
|
2625
|
+
firstPositions === undefined ||
|
|
2626
|
+
firstPositions.length === 0 ||
|
|
2627
|
+
firstPositions.length % 3 !== 0
|
|
2628
|
+
) {
|
|
2629
|
+
return undefined;
|
|
2630
|
+
}
|
|
2631
|
+
const vertexCount = firstPositions.length / 3;
|
|
2632
|
+
if (mesh.vertices.length % vertexCount !== 0) return undefined;
|
|
2633
|
+
for (let targetIndex = 0; targetIndex < targets.length; targetIndex += 1) {
|
|
2634
|
+
const target = targets[targetIndex];
|
|
2635
|
+
if (target?.position?.length !== firstPositions.length) return undefined;
|
|
2636
|
+
}
|
|
2637
|
+
const copied = new Float32Array(weights.length);
|
|
2638
|
+
for (let index = 0; index < weights.length; index += 1) {
|
|
2639
|
+
const weight = weights[index] ?? Number.NaN;
|
|
2640
|
+
if (!Number.isFinite(weight)) return undefined;
|
|
2641
|
+
copied[index] = weight;
|
|
2642
|
+
}
|
|
2643
|
+
return { weights: copied, targetCount: targets.length };
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2985
2646
|
export function extractFrame(world: World, context: PreparedExtractContext): ExtractedFrame {
|
|
2986
2647
|
// feat-20260708-composited-multi-world-rendering M2 / D-2: resetForFrame
|
|
2987
2648
|
// has been lifted to extractFrames (the frame-level entry point).
|
|
@@ -2990,16 +2651,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
2990
2651
|
const {
|
|
2991
2652
|
assets,
|
|
2992
2653
|
pipelineState,
|
|
2993
|
-
gpuStore,
|
|
2994
2654
|
materialSnapshotCache: persistentMaterialSnapshotCache,
|
|
2995
2655
|
cull: cullMode,
|
|
2996
2656
|
} = context;
|
|
2997
|
-
const reportRenderError = (
|
|
2998
|
-
error: unknown,
|
|
2999
|
-
errorContext?: { readonly systemName: string },
|
|
3000
|
-
): void => {
|
|
3001
|
-
routeWorldError(world, error, errorContext);
|
|
3002
|
-
};
|
|
3003
2657
|
const visibility = context.visibility.hasAnyHiddenIntent ? context.visibility : undefined;
|
|
3004
2658
|
const skinPaletteAllocator = pipelineState?.skinPaletteAllocator ?? null;
|
|
3005
2659
|
|
|
@@ -3011,11 +2665,13 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3011
2665
|
//
|
|
3012
2666
|
// Each segment routes through a World-owned Query with explicit read and
|
|
3013
2667
|
// optional roles. The packed entity handle for `readWorldMat4Copy` /
|
|
3014
|
-
//
|
|
2668
|
+
// `_getArrayView` reads comes from `row.entity` -- the
|
|
3015
2669
|
// archetype-graph back-door (`graph.archetypes` / `arch.components.some`) is gone.
|
|
3016
2670
|
// Plan-decisions K-2 sniffing scheme B (archetype-edge sniff once via
|
|
3017
|
-
// `row.get(X) !== undefined`); K-3 invariant preserved (
|
|
2671
|
+
// `row.get(X) !== undefined`); K-3 invariant preserved (`_getArrayView`
|
|
3018
2672
|
// calls survive untouched, only the entity source changes).
|
|
2673
|
+
const worldInternal = createWorldInternalView(world);
|
|
2674
|
+
|
|
3019
2675
|
const cameras = extractCameraSnapshots(world);
|
|
3020
2676
|
|
|
3021
2677
|
// Three-query union (M2 / w16 / AC-03): directional has no Transform
|
|
@@ -3042,14 +2698,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3042
2698
|
| undefined;
|
|
3043
2699
|
for (const row of directionalLightQuery) {
|
|
3044
2700
|
const l = row.get(DirectionalLight);
|
|
3045
|
-
const validation = validateDirectionalLightData(
|
|
3046
|
-
l as unknown as Readonly<Record<string, unknown>>,
|
|
3047
|
-
);
|
|
3048
|
-
if (!validation.ok) {
|
|
3049
|
-
reportRenderError(validation.error, {
|
|
3050
|
-
systemName: 'RenderSystem.extract (directional-light)',
|
|
3051
|
-
});
|
|
3052
|
-
}
|
|
3053
2701
|
directionalCount += 1;
|
|
3054
2702
|
const intensity = l.intensity;
|
|
3055
2703
|
const snapshot: DirectionalLightSnapshot = {
|
|
@@ -3092,24 +2740,21 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3092
2740
|
.unwrap();
|
|
3093
2741
|
for (const row of pointLightQuery) {
|
|
3094
2742
|
const p = row.get(PointLight);
|
|
3095
|
-
const validation = validatePointLightData(p as unknown as Readonly<Record<string, unknown>>);
|
|
3096
|
-
if (!validation.ok) {
|
|
3097
|
-
reportRenderError(validation.error, { systemName: 'RenderSystem.extract (point-light)' });
|
|
3098
|
-
}
|
|
3099
2743
|
// K-2 scheme B: archetype-edge sniff -- `bundle.Transform` key is absent
|
|
3100
2744
|
// when the archetype does not carry the Transform column.
|
|
3101
|
-
const
|
|
2745
|
+
const hasTransform = row.get(Transform) !== undefined;
|
|
3102
2746
|
const intensity = p.intensity;
|
|
3103
2747
|
const range = p.range;
|
|
3104
2748
|
const entityId = row.entity;
|
|
3105
2749
|
// Position = world-space translation extracted from Transform.world.
|
|
3106
2750
|
// A point light archetype without a Transform column sits at the origin.
|
|
3107
2751
|
let worldMat: Float32Array | undefined;
|
|
3108
|
-
if (
|
|
2752
|
+
if (hasTransform) {
|
|
2753
|
+
const view = worldInternal._getArrayView(entityId, Transform, 'world');
|
|
2754
|
+
if (view !== undefined) worldMat = new Float32Array(view);
|
|
2755
|
+
}
|
|
3109
2756
|
const position =
|
|
3110
|
-
worldMat !== undefined
|
|
3111
|
-
? mat4.getTranslation(vec3.create(), worldMat as unknown as mat4.Mat4Like)
|
|
3112
|
-
: vec3.create(0, 0, 0);
|
|
2757
|
+
worldMat !== undefined ? mat4.getTranslation(vec3.create(), worldMat) : vec3.create(0, 0, 0);
|
|
3113
2758
|
pointSnapshots.push({
|
|
3114
2759
|
kind: 'point',
|
|
3115
2760
|
position,
|
|
@@ -3137,21 +2782,19 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3137
2782
|
let spotTileNext = 0;
|
|
3138
2783
|
for (const row of spotLightQuery) {
|
|
3139
2784
|
const s = row.get(SpotLight);
|
|
3140
|
-
const
|
|
3141
|
-
if (!validation.ok) {
|
|
3142
|
-
reportRenderError(validation.error, { systemName: 'RenderSystem.extract (spot-light)' });
|
|
3143
|
-
}
|
|
3144
|
-
const transform = row.get(Transform);
|
|
2785
|
+
const hasTransform = row.get(Transform) !== undefined;
|
|
3145
2786
|
const intensity = s.intensity;
|
|
3146
2787
|
const range = s.range;
|
|
3147
2788
|
const innerConeDeg = s.innerConeDeg;
|
|
3148
2789
|
const outerConeDeg = s.outerConeDeg;
|
|
3149
2790
|
let worldMat: Float32Array | undefined;
|
|
3150
|
-
if (
|
|
2791
|
+
if (hasTransform) {
|
|
2792
|
+
const entity = row.entity;
|
|
2793
|
+
const view = worldInternal._getArrayView(entity, Transform, 'world');
|
|
2794
|
+
if (view !== undefined) worldMat = new Float32Array(view);
|
|
2795
|
+
}
|
|
3151
2796
|
const position =
|
|
3152
|
-
worldMat !== undefined
|
|
3153
|
-
? mat4.getTranslation(vec3.create(), worldMat as unknown as mat4.Mat4Like)
|
|
3154
|
-
: vec3.create(0, 0, 0);
|
|
2797
|
+
worldMat !== undefined ? mat4.getTranslation(vec3.create(), worldMat) : vec3.create(0, 0, 0);
|
|
3155
2798
|
const dir = vec3.create(s.direction[0] ?? 0, s.direction[1] ?? -1, s.direction[2] ?? 0);
|
|
3156
2799
|
|
|
3157
2800
|
// Extract is the single direction-normalization owner for direct-light
|
|
@@ -3161,6 +2804,19 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3161
2804
|
);
|
|
3162
2805
|
const EPSILON = 1e-6;
|
|
3163
2806
|
const hasValidDirection = dirLen > EPSILON;
|
|
2807
|
+
if (!hasValidDirection) {
|
|
2808
|
+
worldInternal._routeError(
|
|
2809
|
+
new SpawnLightInvalidBoundsError('SpotLight', 'direction', [
|
|
2810
|
+
dir[0] ?? 0,
|
|
2811
|
+
dir[1] ?? 0,
|
|
2812
|
+
dir[2] ?? 0,
|
|
2813
|
+
]),
|
|
2814
|
+
{
|
|
2815
|
+
severity: Severity.Error,
|
|
2816
|
+
systemName: 'RenderSystem.extract (spot-direction)',
|
|
2817
|
+
},
|
|
2818
|
+
);
|
|
2819
|
+
}
|
|
3164
2820
|
const dirN = vec3.create(
|
|
3165
2821
|
hasValidDirection ? (dir[0] ?? 0) / dirLen : (dir[0] ?? 0),
|
|
3166
2822
|
hasValidDirection ? (dir[1] ?? 0) / dirLen : (dir[1] ?? 0),
|
|
@@ -3203,7 +2859,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3203
2859
|
|
|
3204
2860
|
spotSnapshots.push({
|
|
3205
2861
|
kind: 'spot',
|
|
3206
|
-
entity: row.entity,
|
|
3207
2862
|
// D-6: position reflects world transform; direction stays sourced
|
|
3208
2863
|
// from SpotLight.direction (NOT rotated by the parent).
|
|
3209
2864
|
position,
|
|
@@ -3276,7 +2931,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3276
2931
|
cascadeBlend: sf.cascadeBlend,
|
|
3277
2932
|
mapSize: sf.mapSize,
|
|
3278
2933
|
shadowDistance: sf.shadowDistance,
|
|
3279
|
-
pcfKernelSize: sf.pcfKernelSize,
|
|
3280
2934
|
};
|
|
3281
2935
|
directionalCsmDirection = dirSnapshot.direction;
|
|
3282
2936
|
const csm = computeDirectionalCsm(dirSnapshot.direction, directionalCsmConfig, cameraData);
|
|
@@ -3327,14 +2981,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3327
2981
|
for (const row of pointShadowQuery) {
|
|
3328
2982
|
const t = row.get(Transform);
|
|
3329
2983
|
const ps = row.get(PointLightShadow);
|
|
3330
|
-
const validation = validatePointLightShadowData(
|
|
3331
|
-
ps as unknown as Readonly<Record<string, unknown>>,
|
|
3332
|
-
);
|
|
3333
|
-
if (!validation.ok) {
|
|
3334
|
-
reportRenderError(validation.error, {
|
|
3335
|
-
systemName: 'RenderSystem.extract (point-light-shadow)',
|
|
3336
|
-
});
|
|
3337
|
-
}
|
|
3338
2984
|
// Read world-space position from Transform.world (mat4 column-major;
|
|
3339
2985
|
// translation lives at indices 12..14, mirroring CameraSnapshot.world
|
|
3340
2986
|
// semantics in this file).
|
|
@@ -3400,7 +3046,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3400
3046
|
}
|
|
3401
3047
|
}
|
|
3402
3048
|
|
|
3403
|
-
|
|
3049
|
+
const lights: ExtractedLights = {
|
|
3404
3050
|
directional,
|
|
3405
3051
|
directionalCount,
|
|
3406
3052
|
point: pointSnapshots,
|
|
@@ -3485,56 +3131,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3485
3131
|
}
|
|
3486
3132
|
}
|
|
3487
3133
|
|
|
3488
|
-
const environmentResult = selectEnvironmentSource({
|
|
3489
|
-
resourceOwner: world,
|
|
3490
|
-
worldId: context.worldId ?? 0,
|
|
3491
|
-
skylight,
|
|
3492
|
-
skylightCount,
|
|
3493
|
-
skybox,
|
|
3494
|
-
skyboxCount,
|
|
3495
|
-
});
|
|
3496
|
-
if (!environmentResult.ok) throw environmentResult.error;
|
|
3497
|
-
const environment = environmentResult.value;
|
|
3498
|
-
|
|
3499
|
-
const resourceRefreshBase = context.resourceRefreshBase;
|
|
3500
|
-
if (resourceRefreshBase !== undefined) {
|
|
3501
|
-
const candidateDomain =
|
|
3502
|
-
resourceRefreshBase.candidateDomain ??
|
|
3503
|
-
buildRenderCandidateDomain(
|
|
3504
|
-
resourceRefreshBase.candidateRenderables ?? resourceRefreshBase.renderables,
|
|
3505
|
-
resourceRefreshBase.candidateDispatch ?? resourceRefreshBase.dispatch,
|
|
3506
|
-
);
|
|
3507
|
-
const finalizedDirectionalCsm =
|
|
3508
|
-
directionalCsmConfig !== undefined && directionalCsmDirection !== undefined
|
|
3509
|
-
? computeDirectionalCsm(
|
|
3510
|
-
directionalCsmDirection,
|
|
3511
|
-
directionalCsmConfig,
|
|
3512
|
-
cameraData,
|
|
3513
|
-
renderCandidateCsmCandidates(candidateDomain),
|
|
3514
|
-
)
|
|
3515
|
-
: undefined;
|
|
3516
|
-
const refreshedLights =
|
|
3517
|
-
finalizedDirectionalCsm === undefined || finalizedDirectionalCsm === null
|
|
3518
|
-
? lights
|
|
3519
|
-
: {
|
|
3520
|
-
...lights,
|
|
3521
|
-
lightViewProj: finalizedDirectionalCsm.lightViewProj,
|
|
3522
|
-
splitPlanes: finalizedDirectionalCsm.splitPlanes,
|
|
3523
|
-
cascadeCount: finalizedDirectionalCsm.cascadeCount,
|
|
3524
|
-
cascadeBlend: finalizedDirectionalCsm.cascadeBlend,
|
|
3525
|
-
shadowMapSize: finalizedDirectionalCsm.shadowMapSize,
|
|
3526
|
-
directionalCsm: finalizedDirectionalCsm,
|
|
3527
|
-
cascades: finalizedDirectionalCsm.cascades,
|
|
3528
|
-
};
|
|
3529
|
-
return {
|
|
3530
|
-
...resourceRefreshBase,
|
|
3531
|
-
cameras,
|
|
3532
|
-
lights: refreshedLights,
|
|
3533
|
-
environment,
|
|
3534
|
-
postProcessParams: extractPostProcessParams(world, cameras),
|
|
3535
|
-
};
|
|
3536
|
-
}
|
|
3537
|
-
|
|
3538
3134
|
// feat-20260528-frustum-culling M3 / w10: precompute per-camera frustum
|
|
3539
3135
|
// planes so entities can be tested against all cameras in the inner loop.
|
|
3540
3136
|
// Cameras with degenerate projection parameters (e.g. zero fov, zero aspect)
|
|
@@ -3580,7 +3176,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3580
3176
|
// carried in the world basis columns; the cull frustum uses the same view
|
|
3581
3177
|
// the record stage derives, so cull stays same-source with render (AC-05).
|
|
3582
3178
|
const view = mat4.create();
|
|
3583
|
-
mat4.invert(view, cam.world
|
|
3179
|
+
mat4.invert(view, cam.world);
|
|
3584
3180
|
const vp = mat4.create();
|
|
3585
3181
|
mat4.multiply(vp, proj, view);
|
|
3586
3182
|
const f = frustum.create();
|
|
@@ -3591,7 +3187,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3591
3187
|
|
|
3592
3188
|
const renderables: RenderableSnapshot[] = [];
|
|
3593
3189
|
// feat-20260528-frustum-culling M3 / w11: frustum culling counters.
|
|
3594
|
-
const candidateRenderables: RenderableSnapshot[] = [];
|
|
3595
3190
|
let frustumCulled = 0;
|
|
3596
3191
|
let frustumTotal = 0;
|
|
3597
3192
|
const explicitlyHidden = new Set<EntityHandle>();
|
|
@@ -3614,7 +3209,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3614
3209
|
// (`row.get(X) !== undefined` edge sniff) replaces the prior
|
|
3615
3210
|
// `arch.components.some` row-internal back-door. K-3 invariant: the
|
|
3616
3211
|
// variable-length array reads (`MeshRenderer.materials`,
|
|
3617
|
-
// `Instances.transforms`) still flow through
|
|
3212
|
+
// `Instances.transforms`) still flow through `_getArrayView` /
|
|
3618
3213
|
// `world.get(e, Instances)` -- only the `entity` source switches to
|
|
3619
3214
|
// `row.entity`.
|
|
3620
3215
|
//
|
|
@@ -3637,46 +3232,50 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3637
3232
|
MeshFilter,
|
|
3638
3233
|
Instances,
|
|
3639
3234
|
Skin,
|
|
3640
|
-
MorphWeights,
|
|
3641
3235
|
Layer,
|
|
3236
|
+
MorphWeights,
|
|
3642
3237
|
SpriteRegionOverride,
|
|
3643
3238
|
SpriteInstances,
|
|
3644
3239
|
],
|
|
3645
3240
|
})
|
|
3646
3241
|
.unwrap();
|
|
3647
|
-
// QueryRow deliberately exposes component data, not
|
|
3648
|
-
//
|
|
3649
|
-
//
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3242
|
+
// QueryRow deliberately exposes component data, not an archetype version.
|
|
3243
|
+
// The optimized ECS owner keeps table storage private, so instance-buffer
|
|
3244
|
+
// invalidation uses a content fingerprint of the managed arrays instead.
|
|
3245
|
+
const resolveArchVersion = (entity: EntityHandle): number => {
|
|
3246
|
+
const values = [
|
|
3247
|
+
worldInternal._getArrayView(entity, Instances, 'transforms'),
|
|
3248
|
+
worldInternal._getArrayView(entity, SpriteInstances, 'transforms'),
|
|
3249
|
+
worldInternal._getArrayView(entity, SpriteInstances, 'regions'),
|
|
3250
|
+
];
|
|
3251
|
+
let hash = 2166136261;
|
|
3252
|
+
for (const value of values) {
|
|
3253
|
+
hash = Math.imul(hash ^ (value?.length ?? 0), 16777619) >>> 0;
|
|
3254
|
+
if (value === undefined) continue;
|
|
3255
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
3256
|
+
hash = Math.imul(hash ^ Math.fround(value[index] ?? 0), 16777619) >>> 0;
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
return hash;
|
|
3260
|
+
};
|
|
3261
|
+
// Pending entries are created with the current renderables.length, which is
|
|
3262
|
+
// exactly the slot this entity receives if it survives culling. No other
|
|
3263
|
+
// renderable can be pushed between staging and this entity's push, so publish
|
|
3264
|
+
// the original fresh entries instead of cloning every submesh descriptor.
|
|
3265
|
+
// A culled entity still discards its private pending array unchanged.
|
|
3659
3266
|
const flushPendingDispatch = (pending: readonly DispatchEntry[]): void => {
|
|
3660
|
-
for (const entry of pending)
|
|
3267
|
+
for (const entry of pending) dispatch.push(entry);
|
|
3661
3268
|
};
|
|
3662
|
-
const
|
|
3663
|
-
context.targetRenderableEntities === undefined
|
|
3664
|
-
? meshRendererQuery
|
|
3665
|
-
: context.targetRenderableEntities.flatMap((entity) => {
|
|
3666
|
-
const row = meshRendererQuery.at(entity);
|
|
3667
|
-
return row === undefined ? [] : [row];
|
|
3668
|
-
});
|
|
3669
|
-
for (const row of meshRendererRows) {
|
|
3269
|
+
for (const row of meshRendererQuery) {
|
|
3670
3270
|
// K-2 archetype-edge sniff (scheme B): a missing optional component
|
|
3671
3271
|
// surfaces as an absent bundle key, not a row-internal optional chain.
|
|
3672
3272
|
// Presence-only checks use QueryRow.has so large array-bearing components
|
|
3673
3273
|
// are not materialised merely to answer a boolean question.
|
|
3674
|
-
const
|
|
3675
|
-
const hasTransform = transformData !== undefined;
|
|
3274
|
+
const hasTransform = row.has(Transform);
|
|
3676
3275
|
const meshFilter = row.get(MeshFilter);
|
|
3677
3276
|
const hasInstances = row.has(Instances);
|
|
3678
3277
|
const skin = row.get(Skin);
|
|
3679
|
-
const
|
|
3278
|
+
const morphWeightsView = worldInternal._getArrayView(row.entity, MorphWeights, 'weights');
|
|
3680
3279
|
const hasMeshFilter = meshFilter !== undefined;
|
|
3681
3280
|
const hasSkin = skin !== undefined;
|
|
3682
3281
|
// feat-20260625-sprite-instances-and-tilemap-terrain-static-batch M3 / w10:
|
|
@@ -3705,8 +3304,8 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3705
3304
|
// feat-20260608-tilemap-object-layer-rendering M3 / m3-t5: tilemap-spawned
|
|
3706
3305
|
// per-cell render entities (the ones `tilemap-chunk-extract-system`
|
|
3707
3306
|
// pushes via `spawnDerivedRenderEntities`) reach this loop via the same
|
|
3708
|
-
// archetype edge that carries
|
|
3709
|
-
//
|
|
3307
|
+
// archetype edge that carries a sprite entity -- they all wear
|
|
3308
|
+
// `MeshFilter.assetHandle === HANDLE_QUAD` + a `forgeax::sprite`-shaded
|
|
3710
3309
|
// material asset + the sprite-bucket `values.region` rectangle.
|
|
3711
3310
|
// For the per-entity Y-sort path (requirements §AC-12 / §AC-13):
|
|
3712
3311
|
//
|
|
@@ -3721,7 +3320,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3721
3320
|
// bucket types therefore feed one `transparentSortEntries` argsort
|
|
3722
3321
|
// step + share the `argsortInPlace` radix LSD primitive (plan-strategy
|
|
3723
3322
|
// §D-1 / §D-3). The detection lives on the material side -- detect a
|
|
3724
|
-
// tilemap-spawned entity by
|
|
3323
|
+
// tilemap-spawned entity by `MeshFilter.assetHandle === HANDLE_QUAD`
|
|
3725
3324
|
// plus the `forgeax::sprite` shader id on `MeshRenderer.material`'s
|
|
3726
3325
|
// first pass + non-empty `values.region`; no new public ECS
|
|
3727
3326
|
// marker component lands (charter F1 minimum surface).
|
|
@@ -3739,10 +3338,10 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3739
3338
|
// sprite reduces the anchor budget to 0.5 rather than the asset's 1.0.
|
|
3740
3339
|
//
|
|
3741
3340
|
// SpriteRegionOverride.region is a fixed stride-4 value. Per-row reads
|
|
3742
|
-
// route through
|
|
3341
|
+
// route through `_getArrayView` for the zero-copy row window
|
|
3743
3342
|
// row-window slice (consistent with the variable-length array column
|
|
3744
|
-
// reads -- K-3 carve-out keeps
|
|
3745
|
-
// record for
|
|
3343
|
+
// reads -- K-3 carve-out keeps `_getArrayView` as the row-accessor of
|
|
3344
|
+
// record for each non-scalar column).
|
|
3746
3345
|
const hasSpriteRegionOverride = row.has(SpriteRegionOverride);
|
|
3747
3346
|
// feat-20260523-skin-skeleton-animation M2 / T-21: Skin component
|
|
3748
3347
|
// column views for coexistence check + joint despawn fail-fast.
|
|
@@ -3754,11 +3353,11 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3754
3353
|
if (hasInstances || hasSpriteInstances) {
|
|
3755
3354
|
// All rows in this callback share one archetype. Only these two
|
|
3756
3355
|
// instance-bearing paths consume the version in their cache key.
|
|
3757
|
-
archVersion =
|
|
3356
|
+
archVersion = resolveArchVersion(row.entity);
|
|
3758
3357
|
}
|
|
3759
3358
|
|
|
3760
3359
|
{
|
|
3761
|
-
// feat-20260608 M2 / w11: read materials array via
|
|
3360
|
+
// feat-20260608 M2 / w11: read materials array via _getArrayView
|
|
3762
3361
|
const entity = row.entity;
|
|
3763
3362
|
if (isRenderable && visibility?.effective(entity) === 'hidden') {
|
|
3764
3363
|
explicitlyHidden.add(entity);
|
|
@@ -3777,9 +3376,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3777
3376
|
// the deferred-shading smoke (PR #598 CI). Pure ordering fix; cull
|
|
3778
3377
|
// logic and MeshRenderer contract unchanged.
|
|
3779
3378
|
const pendingDispatch: DispatchEntry[] = [];
|
|
3780
|
-
const
|
|
3781
|
-
|
|
3782
|
-
|
|
3379
|
+
const materialsView = worldInternal._getArrayView(entity, MeshRenderer, 'materials') as
|
|
3380
|
+
| Uint32Array
|
|
3381
|
+
| undefined;
|
|
3783
3382
|
const materialCount = materialsView?.length ?? 0;
|
|
3784
3383
|
let materialHandles = Array.from(materialsView ?? []);
|
|
3785
3384
|
let materialBindingSources: MeshMaterialBindingSource[] = materialHandles.map(
|
|
@@ -3798,7 +3397,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3798
3397
|
assets !== null
|
|
3799
3398
|
) {
|
|
3800
3399
|
const meshHandle = toShared<'MeshAsset'>(fAssetHandleVal);
|
|
3801
|
-
const meshRes =
|
|
3400
|
+
const meshRes = resolveAssetHandle<Asset>(world, meshHandle);
|
|
3802
3401
|
if (meshRes.ok && meshRes.value.kind === 'mesh') {
|
|
3803
3402
|
const meshAsset = meshRes.value as MeshAsset;
|
|
3804
3403
|
gpuDrivenSubmeshes = meshAsset.submeshes;
|
|
@@ -3813,7 +3412,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3813
3412
|
const guid = (meshRes.value as { guid?: string }).guid ?? '<no-guid>';
|
|
3814
3413
|
const resolvedBindings = resolveMeshMaterialBindings(meshAsset, materialsView ?? [], {
|
|
3815
3414
|
isValidOverride(handle) {
|
|
3816
|
-
const resolved =
|
|
3415
|
+
const resolved = resolveAssetHandle(world, toShared<'MaterialAsset'>(handle));
|
|
3817
3416
|
return resolved.ok && resolved.value.kind === 'material';
|
|
3818
3417
|
},
|
|
3819
3418
|
resolveMeshDefault(defaultGuid) {
|
|
@@ -3826,7 +3425,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3826
3425
|
});
|
|
3827
3426
|
if (!resolvedBindings.ok) {
|
|
3828
3427
|
if (resolvedBindings.code === 'mesh-material-slots-missing') {
|
|
3829
|
-
|
|
3428
|
+
worldInternal._routeError(
|
|
3830
3429
|
new AssetError({
|
|
3831
3430
|
code: 'load-failed',
|
|
3832
3431
|
expected: 'every MeshAsset producer supplies materialSlots[]',
|
|
@@ -3837,19 +3436,20 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3837
3436
|
subAssetGuid: '<material-slots-missing>',
|
|
3838
3437
|
sourceField: { fieldName: 'materialSlots' },
|
|
3839
3438
|
},
|
|
3840
|
-
})
|
|
3439
|
+
}),
|
|
3841
3440
|
{
|
|
3441
|
+
severity: Severity.Error,
|
|
3842
3442
|
systemName: 'RenderSystem.extract (mesh-material-slots-missing)',
|
|
3843
3443
|
},
|
|
3844
3444
|
);
|
|
3845
3445
|
continue;
|
|
3846
3446
|
}
|
|
3847
3447
|
const materialGuid = AssetGuid.format(resolvedBindings.defaultMaterial);
|
|
3848
|
-
|
|
3448
|
+
worldInternal._routeError(
|
|
3849
3449
|
new AssetError({
|
|
3850
3450
|
code: 'load-failed',
|
|
3851
3451
|
expected: `MeshAsset materialSlots[${resolvedBindings.slotIndex}] default ${materialGuid} is ready and a MaterialAsset`,
|
|
3852
|
-
hint: `
|
|
3452
|
+
hint: `loadByGuid(meshGuid) must recursively load the declared default material; mesh=${guid}, slot=${resolvedBindings.slotIndex}, material=${materialGuid}`,
|
|
3853
3453
|
detail: {
|
|
3854
3454
|
referencedByGuid: guid,
|
|
3855
3455
|
referencedByKind: 'mesh',
|
|
@@ -3859,8 +3459,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3859
3459
|
arrayIndex: resolvedBindings.slotIndex,
|
|
3860
3460
|
},
|
|
3861
3461
|
},
|
|
3862
|
-
})
|
|
3462
|
+
}),
|
|
3863
3463
|
{
|
|
3464
|
+
severity: Severity.Error,
|
|
3864
3465
|
systemName: 'RenderSystem.extract (mesh-default-not-ready)',
|
|
3865
3466
|
},
|
|
3866
3467
|
);
|
|
@@ -3884,7 +3485,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3884
3485
|
},
|
|
3885
3486
|
}));
|
|
3886
3487
|
for (const diagnostic of resolvedBindings.diagnostics) {
|
|
3887
|
-
|
|
3488
|
+
worldInternal._routeError(
|
|
3888
3489
|
new AssetError({
|
|
3889
3490
|
code: diagnostic.code,
|
|
3890
3491
|
expected:
|
|
@@ -3904,8 +3505,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3904
3505
|
slotIndex: diagnostic.slotIndex,
|
|
3905
3506
|
handle: diagnostic.handle ?? 0,
|
|
3906
3507
|
},
|
|
3907
|
-
})
|
|
3508
|
+
}),
|
|
3908
3509
|
{
|
|
3510
|
+
severity: Severity.Warning,
|
|
3909
3511
|
systemName: `RenderSystem.extract (${diagnostic.code})`,
|
|
3910
3512
|
},
|
|
3911
3513
|
);
|
|
@@ -3941,7 +3543,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3941
3543
|
cachedMaterial.passes,
|
|
3942
3544
|
entity,
|
|
3943
3545
|
handleRaw,
|
|
3944
|
-
|
|
3546
|
+
renderables.length,
|
|
3945
3547
|
layerVal,
|
|
3946
3548
|
materialSnap.paramSnapshot,
|
|
3947
3549
|
);
|
|
@@ -3968,22 +3570,23 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3968
3570
|
stablePersistentCached.passes,
|
|
3969
3571
|
entity,
|
|
3970
3572
|
handleRaw,
|
|
3971
|
-
|
|
3573
|
+
renderables.length,
|
|
3972
3574
|
layerVal,
|
|
3973
3575
|
materialSnap.paramSnapshot,
|
|
3974
3576
|
);
|
|
3975
3577
|
}
|
|
3976
3578
|
} else {
|
|
3977
|
-
const res =
|
|
3579
|
+
const res = resolveAssetHandle(world, tagged);
|
|
3978
3580
|
if (!res.ok) {
|
|
3979
3581
|
if (isRenderable) {
|
|
3980
3582
|
const rhiErr = new RhiError({
|
|
3981
3583
|
code: 'asset-not-registered',
|
|
3982
|
-
expected: 'MeshRenderer.material in
|
|
3584
|
+
expected: 'MeshRenderer.material in AssetRegistry',
|
|
3983
3585
|
hint: 'catalog the material via assetRegistry.catalog(guid, asset) + world.allocSharedRef before spawn, or remove the material field to fall back to default',
|
|
3984
3586
|
detail: { assetHandle: handleRaw },
|
|
3985
3587
|
});
|
|
3986
|
-
|
|
3588
|
+
worldInternal._routeError(rhiErr, {
|
|
3589
|
+
severity: Severity.Error,
|
|
3987
3590
|
systemName: 'RenderSystem.extract (material asset-not-registered)',
|
|
3988
3591
|
});
|
|
3989
3592
|
}
|
|
@@ -3993,12 +3596,11 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3993
3596
|
if (asset.kind !== 'material') {
|
|
3994
3597
|
materialSnap = defaultMaterialSnapshot(handleRaw);
|
|
3995
3598
|
} else {
|
|
3996
|
-
const materialGuid = assets.guidOf(asset);
|
|
3997
3599
|
// feat-20260529 M3 / w11: material parent chain inheritance via
|
|
3998
3600
|
// read-through _materialWalk accessor (plan-strategy D-6).
|
|
3999
3601
|
// The old direct asset.passes / asset.values read never
|
|
4000
3602
|
// walked the parent chain, causing broken-inheritance (root cause).
|
|
4001
|
-
const resolvedResult =
|
|
3603
|
+
const resolvedResult = walkMaterialPassesOverSharedRefs(world, tagged, assets);
|
|
4002
3604
|
if (!resolvedResult.ok) {
|
|
4003
3605
|
// AC-09 / S-7 / q8=A: passes-empty or cycle must fire structured
|
|
4004
3606
|
// error through _routeError (same routing as asset-not-registered
|
|
@@ -4011,26 +3613,29 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4011
3613
|
case 'material-value-unknown':
|
|
4012
3614
|
case 'material-value-type-mismatch':
|
|
4013
3615
|
case 'material-contract-program-mismatch':
|
|
4014
|
-
|
|
3616
|
+
worldInternal._routeError(err, {
|
|
3617
|
+
severity: Severity.Error,
|
|
4015
3618
|
systemName: `RenderSystem.extract (${err.code})`,
|
|
4016
3619
|
});
|
|
4017
3620
|
break;
|
|
4018
3621
|
case 'material-circular-inheritance':
|
|
4019
|
-
|
|
3622
|
+
worldInternal._routeError(err, {
|
|
3623
|
+
severity: Severity.Error,
|
|
4020
3624
|
systemName: 'RenderSystem.extract (material-circular-inheritance)',
|
|
4021
3625
|
});
|
|
4022
3626
|
break;
|
|
4023
3627
|
default:
|
|
4024
3628
|
// Exhaustive guard: unhandled error codes from _materialWalk
|
|
4025
3629
|
// surface an internal assertion to avoid silent continuation.
|
|
4026
|
-
|
|
3630
|
+
worldInternal._routeError(err, {
|
|
3631
|
+
severity: Severity.Error,
|
|
4027
3632
|
systemName: `RenderSystem.extract (_materialWalk: ${err.code})`,
|
|
4028
3633
|
});
|
|
4029
3634
|
}
|
|
4030
3635
|
continue;
|
|
4031
3636
|
}
|
|
4032
3637
|
const resolved = resolvedResult.value;
|
|
4033
|
-
const allPasses = resolved.passes
|
|
3638
|
+
const allPasses = resolved.passes;
|
|
4034
3639
|
const firstPassShader =
|
|
4035
3640
|
allPasses.length > 0
|
|
4036
3641
|
? runtimeMaterialShaderId(allPasses[0]?.program.module, allPasses[0]?.name)
|
|
@@ -4064,50 +3669,10 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4064
3669
|
}
|
|
4065
3670
|
}
|
|
4066
3671
|
|
|
4067
|
-
const
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
const registeredProjection =
|
|
4072
|
-
registeredShader?.ok === true
|
|
4073
|
-
? registeredShader.value.paramSchemaProjection
|
|
4074
|
-
: undefined;
|
|
4075
|
-
const materialParamSchema =
|
|
4076
|
-
registeredProjection?.schema ??
|
|
4077
|
-
materialParametersToParamSchema(resolved.parameters ?? [], firstPassShader, assets);
|
|
4078
|
-
// An admitted empty schema is valid for vertex-only/shadow
|
|
4079
|
-
// shaders, but older authored material payloads can still carry
|
|
4080
|
-
// texture values while their manifest schema is empty. Keep that
|
|
4081
|
-
// compatibility inference alive; a non-empty admitted schema is
|
|
4082
|
-
// always the sole runtime projection owner.
|
|
4083
|
-
const derivedMaterialInterface =
|
|
4084
|
-
registeredProjection !== undefined && registeredProjection.schema.length > 0
|
|
4085
|
-
? registeredProjection.derivedInterface
|
|
4086
|
-
: materialParamSchema.length > 0
|
|
4087
|
-
? deriveParamSchemaForCensus(
|
|
4088
|
-
materialParamSchema,
|
|
4089
|
-
context.paramSchemaProjectionCensus,
|
|
4090
|
-
'entity-extract',
|
|
4091
|
-
)
|
|
4092
|
-
: undefined;
|
|
4093
|
-
if (
|
|
4094
|
-
context.paramSchemaProjectionCensus !== undefined &&
|
|
4095
|
-
registeredProjection !== undefined
|
|
4096
|
-
) {
|
|
4097
|
-
context.paramSchemaProjectionCensus.record(
|
|
4098
|
-
'registered-projection-read',
|
|
4099
|
-
'entity-extract',
|
|
4100
|
-
);
|
|
4101
|
-
if (
|
|
4102
|
-
registeredProjection.schema.length === 0 &&
|
|
4103
|
-
Object.keys(resolved.values ?? {}).some((name) => name.endsWith('Texture'))
|
|
4104
|
-
) {
|
|
4105
|
-
context.paramSchemaProjectionCensus.record(
|
|
4106
|
-
'empty-schema-legacy-inference',
|
|
4107
|
-
'entity-extract',
|
|
4108
|
-
);
|
|
4109
|
-
}
|
|
4110
|
-
}
|
|
3672
|
+
const materialParamSchema = materialParametersToParamSchema(
|
|
3673
|
+
resolved.parameters ?? [],
|
|
3674
|
+
firstPassShader,
|
|
3675
|
+
);
|
|
4111
3676
|
// feat-20260611-fox-skinning-vertex-attribute-chain M4 / w17 (D-5):
|
|
4112
3677
|
// bidirectional Skin <-> pbr-skin material fail-fast at extract.
|
|
4113
3678
|
// Skin component without a forgeax::pbr-skin first-pass material
|
|
@@ -4126,20 +3691,15 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4126
3691
|
skinSkeletonView !== 0;
|
|
4127
3692
|
const isPbrSkinMaterial = firstPassShader === 'forgeax::pbr-skin';
|
|
4128
3693
|
if (hasSkinSkel && !isPbrSkinMaterial) {
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
) as unknown as Error,
|
|
4134
|
-
{
|
|
4135
|
-
systemName: 'RenderSystem.extract (skin-material-mismatch)',
|
|
4136
|
-
},
|
|
4137
|
-
);
|
|
3694
|
+
worldInternal._routeError(new SkinMaterialMismatchError(entity, firstPassShader), {
|
|
3695
|
+
severity: Severity.Error,
|
|
3696
|
+
systemName: 'RenderSystem.extract (skin-material-mismatch)',
|
|
3697
|
+
});
|
|
4138
3698
|
continue;
|
|
4139
3699
|
}
|
|
4140
3700
|
if (isPbrSkinMaterial && fAssetHandleVal !== undefined && fAssetHandleVal !== 0) {
|
|
4141
3701
|
const meshHandleForSkinCheck = toShared<'MeshAsset'>(fAssetHandleVal);
|
|
4142
|
-
const meshResForSkinCheck =
|
|
3702
|
+
const meshResForSkinCheck = resolveAssetHandle<MeshAsset>(
|
|
4143
3703
|
world,
|
|
4144
3704
|
meshHandleForSkinCheck,
|
|
4145
3705
|
);
|
|
@@ -4150,15 +3710,10 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4150
3710
|
if (!hasSkinIdx || !hasSkinWt) {
|
|
4151
3711
|
const missing: 'skinIndex' | 'skinWeight' | 'both' =
|
|
4152
3712
|
!hasSkinIdx && !hasSkinWt ? 'both' : !hasSkinIdx ? 'skinIndex' : 'skinWeight';
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
) as unknown as Error,
|
|
4158
|
-
{
|
|
4159
|
-
systemName: 'RenderSystem.extract (material-skin-attr-missing)',
|
|
4160
|
-
},
|
|
4161
|
-
);
|
|
3713
|
+
worldInternal._routeError(new MaterialSkinAttrMissingError(entity, missing), {
|
|
3714
|
+
severity: Severity.Error,
|
|
3715
|
+
systemName: 'RenderSystem.extract (material-skin-attr-missing)',
|
|
3716
|
+
});
|
|
4162
3717
|
continue;
|
|
4163
3718
|
}
|
|
4164
3719
|
}
|
|
@@ -4183,32 +3738,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4183
3738
|
const isSprite =
|
|
4184
3739
|
firstPassShader === 'forgeax::sprite' || firstPassShader === 'forgeax::sprite-lit';
|
|
4185
3740
|
|
|
4186
|
-
const emptySchemaTextureFields =
|
|
4187
|
-
materialParamSchema.length === 0
|
|
4188
|
-
? new Set(
|
|
4189
|
-
Object.entries(pv)
|
|
4190
|
-
.filter(([, value]) => {
|
|
4191
|
-
if (materialTextureValue(value) !== undefined) return true;
|
|
4192
|
-
if (typeof value === 'number' && assets !== null && assets !== undefined) {
|
|
4193
|
-
const resolved = resolveRenderAsset<Asset>(
|
|
4194
|
-
world,
|
|
4195
|
-
value as Handle<string, 'shared'>,
|
|
4196
|
-
);
|
|
4197
|
-
return (
|
|
4198
|
-
resolved.ok &&
|
|
4199
|
-
(resolved.value.kind === 'texture' || resolved.value.kind === 'video')
|
|
4200
|
-
);
|
|
4201
|
-
}
|
|
4202
|
-
if (typeof value === 'string' && assets !== null && assets !== undefined) {
|
|
4203
|
-
const payload = assets.lookup(value);
|
|
4204
|
-
return payload?.kind === 'texture' || payload?.kind === 'video';
|
|
4205
|
-
}
|
|
4206
|
-
return false;
|
|
4207
|
-
})
|
|
4208
|
-
.map(([field]) => field),
|
|
4209
|
-
)
|
|
4210
|
-
: undefined;
|
|
4211
|
-
|
|
4212
3741
|
// feat-20260613-material-paramschema-driven-binding M4 / w23
|
|
4213
3742
|
// (D-5 graceful): paramSchema-driven texture-field validation.
|
|
4214
3743
|
// For each handle-shaped paramValue (typeof === 'number'),
|
|
@@ -4232,7 +3761,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4232
3761
|
if (assets === null || assets === undefined) return undefined;
|
|
4233
3762
|
// M4: intern so the GUID mints one stable handle per World
|
|
4234
3763
|
// instead of a fresh slot every frame (GPU residency relies on
|
|
4235
|
-
// a stable handleSlot).
|
|
3764
|
+
// a stable handleSlot). onLastRelease -> gpuStore.evictTexture.
|
|
4236
3765
|
const interned = internSharedRefFromGuid(
|
|
4237
3766
|
world,
|
|
4238
3767
|
assets,
|
|
@@ -4242,34 +3771,34 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4242
3771
|
if (interned === undefined) return undefined;
|
|
4243
3772
|
handle = interned;
|
|
4244
3773
|
} else if (typeof textureRef === 'number') {
|
|
4245
|
-
handle =
|
|
3774
|
+
handle = toShared<'TextureAsset'>(textureRef);
|
|
4246
3775
|
} else {
|
|
4247
3776
|
return undefined;
|
|
4248
3777
|
}
|
|
4249
3778
|
if (assets === null || assets === undefined) return handle;
|
|
4250
|
-
const declaredFields =
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
3779
|
+
const declaredFields = materialTextureFields(
|
|
3780
|
+
firstPassShader,
|
|
3781
|
+
materialParamSchema.length > 0
|
|
3782
|
+
? derive(materialParamSchema).textureFieldNames
|
|
3783
|
+
: firstPassShader !== undefined
|
|
3784
|
+
? assets.materialShaderTextureFieldNames(firstPassShader)
|
|
3785
|
+
: undefined,
|
|
3786
|
+
);
|
|
4257
3787
|
// Shader not registered (R-4 cross-worktree path) -> trust the
|
|
4258
|
-
// raw handle and let the record stage / GPU layer surface
|
|
3788
|
+
// raw handle and let the record stage / GPU layer surface each
|
|
4259
3789
|
// mismatch via MISSING_TEXTURE_HANDLE.
|
|
4260
3790
|
if (declaredFields === undefined) return handle;
|
|
4261
3791
|
// Field is not declared as a texture by the shader -> the
|
|
4262
3792
|
// loader's "try every int" fallback misclassified a scalar;
|
|
4263
3793
|
// drop the slot so the record stage uses the default white.
|
|
4264
|
-
|
|
4265
|
-
declaredFields
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
if (!declared) {
|
|
3794
|
+
if (
|
|
3795
|
+
!declaredFields.has(fieldName) &&
|
|
3796
|
+
!isEngineInjectedTextureField(firstPassShader, fieldName)
|
|
3797
|
+
) {
|
|
4269
3798
|
return undefined;
|
|
4270
3799
|
}
|
|
4271
3800
|
// Field declared as texture: verify the handle's asset kind.
|
|
4272
|
-
const assetRes =
|
|
3801
|
+
const assetRes = resolveAssetHandle(world, handle);
|
|
4273
3802
|
if (!assetRes.ok) return undefined;
|
|
4274
3803
|
const kind = (assetRes.value as { kind?: string }).kind;
|
|
4275
3804
|
if (kind !== 'texture') return undefined;
|
|
@@ -4283,7 +3812,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4283
3812
|
brand: B,
|
|
4284
3813
|
): Handle<B, 'shared'> | undefined => {
|
|
4285
3814
|
const value = materialTextureRef(raw);
|
|
4286
|
-
if (typeof value === 'number') return
|
|
3815
|
+
if (typeof value === 'number') return toShared<B>(value);
|
|
4287
3816
|
const guid = assetReferenceText(value);
|
|
4288
3817
|
if (guid !== undefined) {
|
|
4289
3818
|
if (assets === null || assets === undefined) return undefined;
|
|
@@ -4299,15 +3828,17 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4299
3828
|
// carried, replacing the hardcoded 3-field list. validateTextureHandle
|
|
4300
3829
|
// already drops fields a shader doesn't declare as a texture.
|
|
4301
3830
|
const userRegionFields =
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
(
|
|
3831
|
+
materialTextureFields(
|
|
3832
|
+
firstPassShader,
|
|
3833
|
+
materialParamSchema.length > 0
|
|
3834
|
+
? derive(materialParamSchema).textureFieldNames
|
|
3835
|
+
: firstPassShader !== undefined && assets !== null && assets !== undefined
|
|
4306
3836
|
? assets.materialShaderTextureFieldNames(firstPassShader)
|
|
4307
|
-
: undefined
|
|
3837
|
+
: undefined,
|
|
3838
|
+
) ?? BUILTIN_USER_REGION_TEXTURE_FIELDS;
|
|
4308
3839
|
const textureHandles = new Map<string, Handle<'TextureAsset', 'shared'>>();
|
|
4309
3840
|
const videoTextureFields = new Map<string, Handle<'VideoAsset', 'shared'>>();
|
|
4310
|
-
for (const field of userRegionFields
|
|
3841
|
+
for (const field of userRegionFields) {
|
|
4311
3842
|
// D-5: a video-kind paramValue routes to the transient path
|
|
4312
3843
|
// (videoTextureFields), NOT validateTextureHandle (which drops
|
|
4313
3844
|
// kind!=='texture', the R-7 silent-fail path). Static fields fall
|
|
@@ -4329,20 +3860,15 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4329
3860
|
const samplerHandles = collectMaterialTextureSamplers(pv, (value) =>
|
|
4330
3861
|
resolveParamHandle(materialTextureRef(value), 'SamplerAsset'),
|
|
4331
3862
|
);
|
|
4332
|
-
const emissiveTextureHandle =
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
pv,
|
|
4336
|
-
materialParamSchema,
|
|
4337
|
-
derivedMaterialInterface,
|
|
3863
|
+
const emissiveTextureHandle = validateTextureHandle(
|
|
3864
|
+
'emissiveTexture',
|
|
3865
|
+
pv.emissiveTexture,
|
|
4338
3866
|
);
|
|
4339
|
-
const
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
}
|
|
4345
|
-
}
|
|
3867
|
+
const occlusionTextureHandle = validateTextureHandle(
|
|
3868
|
+
'occlusionTexture',
|
|
3869
|
+
pv.occlusionTexture,
|
|
3870
|
+
);
|
|
3871
|
+
const textureCoordinates = collectMaterialTextureCoordinates(pv);
|
|
4346
3872
|
const emissivePv = pv.emissive as readonly number[] | undefined;
|
|
4347
3873
|
// feat-20260625 M2 / w6: first-pass transparency flag folds into
|
|
4348
3874
|
// MaterialSnapshot.transparent so the record stage can drive the
|
|
@@ -4369,10 +3895,11 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4369
3895
|
// SpriteRegionOverride: per-entity per-frame region displacement.
|
|
4370
3896
|
let overrideRegion: readonly [number, number, number, number] | undefined;
|
|
4371
3897
|
if (hasSpriteRegionOverride) {
|
|
4372
|
-
const
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
3898
|
+
const overrideView = worldInternal._getArrayView(
|
|
3899
|
+
entity,
|
|
3900
|
+
SpriteRegionOverride,
|
|
3901
|
+
'region',
|
|
3902
|
+
) as Float32Array | undefined;
|
|
4376
3903
|
if (overrideView !== undefined && overrideView.length >= 4) {
|
|
4377
3904
|
overrideRegion = [
|
|
4378
3905
|
overrideView[0] ?? 0,
|
|
@@ -4402,36 +3929,18 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4402
3929
|
regionY += regionW;
|
|
4403
3930
|
regionW = -regionW;
|
|
4404
3931
|
}
|
|
4405
|
-
paramSnap.region = [regionX, regionY, regionZ, regionW]
|
|
3932
|
+
paramSnap.region = [regionX, regionY, regionZ, regionW];
|
|
4406
3933
|
// Guard: slicesAndMode must be present and zero for non-9-slice
|
|
4407
3934
|
// sprites so the record-stage UBO writer (applyParamSnapshotToUbo)
|
|
4408
|
-
// writes [0,0,0,0]
|
|
4409
|
-
//
|
|
4410
|
-
//
|
|
4411
|
-
// geometry
|
|
3935
|
+
// writes [0,0,0,0] at offset 48 instead of leaving the
|
|
3936
|
+
// buildPbrMaterialUboPayload PBR baseline (e.g. occlusionStrength=1
|
|
3937
|
+
// at that slot). A non-zero slicesAndMode trips `useSlices=true`
|
|
3938
|
+
// in sprite.wgsl, which degenerates HANDLE_QUAD geometry → invisible.
|
|
4412
3939
|
if (!('slicesAndMode' in paramSnap)) {
|
|
4413
3940
|
(paramSnap as Record<string, unknown>).slicesAndMode = [0, 0, 0, 0];
|
|
4414
3941
|
}
|
|
4415
3942
|
}
|
|
4416
3943
|
|
|
4417
|
-
// Sprite-specific region/slice values are folded into paramSnap
|
|
4418
|
-
// above. Build the derived projection afterwards so the record
|
|
4419
|
-
// stage writes those values through the same schema-owned payload
|
|
4420
|
-
// as every other material parameter.
|
|
4421
|
-
const derivedMaterialSlot =
|
|
4422
|
-
materialParamSchema.length > 0
|
|
4423
|
-
? deriveMaterialSlotProjection({
|
|
4424
|
-
material: String(handleRaw),
|
|
4425
|
-
schema: materialParamSchema,
|
|
4426
|
-
...(derivedMaterialInterface === undefined
|
|
4427
|
-
? {}
|
|
4428
|
-
: { derivedInterface: derivedMaterialInterface }),
|
|
4429
|
-
values: { ...pv, ...paramSnap },
|
|
4430
|
-
coordinates: textureCoordinates,
|
|
4431
|
-
textures: textureFacts,
|
|
4432
|
-
})
|
|
4433
|
-
: undefined;
|
|
4434
|
-
|
|
4435
3944
|
// Generic materialShaderId snapshot --- sprite included now flows
|
|
4436
3945
|
// through this single branch (plan-strategy D-3 / AC-01 / AC-02 /
|
|
4437
3946
|
// AC-07). The sprite block above only writes paramSnap.region (D-8
|
|
@@ -4452,18 +3961,10 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4452
3961
|
}),
|
|
4453
3962
|
normalScale: normalScalePv,
|
|
4454
3963
|
materialShaderId: firstPassShader,
|
|
4455
|
-
...(materialGuid === undefined ? {} : { materialGuid }),
|
|
4456
3964
|
materialHandle: handleRaw,
|
|
4457
|
-
shadowCaster: hasShadowCasterPass(allPasses),
|
|
4458
3965
|
renderState: pipelineRenderState(allPasses[0]?.renderState),
|
|
4459
3966
|
paramSnapshot: paramSnap,
|
|
4460
3967
|
...(materialParamSchema.length > 0 && { materialParamSchema }),
|
|
4461
|
-
...(derivedMaterialSlot?.ok === true && {
|
|
4462
|
-
derivedMaterialSlot: derivedMaterialSlot.value,
|
|
4463
|
-
}),
|
|
4464
|
-
...(derivedMaterialSlot?.ok === false && {
|
|
4465
|
-
derivedMaterialError: derivedMaterialSlot.error,
|
|
4466
|
-
}),
|
|
4467
3968
|
...(textureCoordinates.size > 0 && { textureCoordinates }),
|
|
4468
3969
|
...(samplerHandles.size > 0 && { samplerHandles }),
|
|
4469
3970
|
...(textureHandles.size > 0 && { textureHandles }),
|
|
@@ -4516,7 +4017,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4516
4017
|
allPasses,
|
|
4517
4018
|
entity,
|
|
4518
4019
|
handleRaw,
|
|
4519
|
-
|
|
4020
|
+
renderables.length,
|
|
4520
4021
|
layerVal,
|
|
4521
4022
|
paramSnap,
|
|
4522
4023
|
);
|
|
@@ -4550,7 +4051,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4550
4051
|
// handleRaw===0 guard alone preserves the exact same dispatch shape.
|
|
4551
4052
|
if (isRenderable && handleRaw === 0) {
|
|
4552
4053
|
const shadowCasterTags: Record<string, string> = { LightMode: 'ShadowCaster' };
|
|
4553
|
-
const nextRenderableIndex =
|
|
4054
|
+
const nextRenderableIndex = renderables.length;
|
|
4554
4055
|
// M2.5: stage into pendingDispatch; flushed at the renderable push
|
|
4555
4056
|
// site below only when the entity survives cull.
|
|
4556
4057
|
pendingDispatch.push({
|
|
@@ -4558,7 +4059,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4558
4059
|
materialHandle: 0,
|
|
4559
4060
|
renderableIndex: nextRenderableIndex,
|
|
4560
4061
|
passIndex: 0,
|
|
4561
|
-
queue:
|
|
4062
|
+
queue: 2000,
|
|
4562
4063
|
layer: layerVal,
|
|
4563
4064
|
tags: shadowCasterTags,
|
|
4564
4065
|
renderState: undefined,
|
|
@@ -4570,22 +4071,16 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4570
4071
|
});
|
|
4571
4072
|
// Also add a Forward pass entry so the entity renders in the
|
|
4572
4073
|
// main scene pass (mirrors Materials.unlit default).
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
defines: undefined,
|
|
4584
|
-
vertexEntry: 'vs_main',
|
|
4585
|
-
fragmentEntry: 'fs_main',
|
|
4586
|
-
materialShaderId: 'forgeax::default-unlit',
|
|
4587
|
-
paramSnapshot: {},
|
|
4588
|
-
});
|
|
4074
|
+
appendMaterialDispatchEntries(
|
|
4075
|
+
pendingDispatch,
|
|
4076
|
+
[DEFAULT_FORWARD_PASS],
|
|
4077
|
+
entity,
|
|
4078
|
+
0,
|
|
4079
|
+
nextRenderableIndex,
|
|
4080
|
+
layerVal,
|
|
4081
|
+
{},
|
|
4082
|
+
1,
|
|
4083
|
+
);
|
|
4589
4084
|
}
|
|
4590
4085
|
|
|
4591
4086
|
if (isRenderable) {
|
|
@@ -4605,30 +4100,21 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4605
4100
|
) {
|
|
4606
4101
|
// Skin + Instances coexistence is forbidden (D-10).
|
|
4607
4102
|
if (hasInstances) {
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
{
|
|
4613
|
-
systemName: 'RenderSystem.extract (skin-instances-coexist)',
|
|
4614
|
-
},
|
|
4615
|
-
);
|
|
4103
|
+
worldInternal._routeError(new SkinInstancesCoexistForbiddenError(entity), {
|
|
4104
|
+
severity: Severity.Error,
|
|
4105
|
+
systemName: 'RenderSystem.extract (skin-instances-coexist)',
|
|
4106
|
+
});
|
|
4616
4107
|
|
|
4617
4108
|
continue;
|
|
4618
4109
|
}
|
|
4619
4110
|
// (a) Resolve skeleton asset; on failure -> skeleton-resolve-failed.
|
|
4620
4111
|
const skeletonHandle = toShared<'SkeletonAsset'>(skeletonHandleRaw);
|
|
4621
|
-
const skeletonRes =
|
|
4112
|
+
const skeletonRes = resolveAssetHandle<SkeletonAsset>(world, skeletonHandle);
|
|
4622
4113
|
if (!skeletonRes.ok || skeletonRes.value.kind !== 'skeleton') {
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
) as unknown as Error,
|
|
4628
|
-
{
|
|
4629
|
-
systemName: 'RenderSystem.extract (skeleton-resolve-failed)',
|
|
4630
|
-
},
|
|
4631
|
-
);
|
|
4114
|
+
worldInternal._routeError(new SkeletonResolveFailedError(entity, skeletonHandleRaw), {
|
|
4115
|
+
severity: Severity.Error,
|
|
4116
|
+
systemName: 'RenderSystem.extract (skeleton-resolve-failed)',
|
|
4117
|
+
});
|
|
4632
4118
|
continue;
|
|
4633
4119
|
}
|
|
4634
4120
|
const skeleton = skeletonRes.value;
|
|
@@ -4638,20 +4124,17 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4638
4124
|
if (skinJoints === undefined) continue;
|
|
4639
4125
|
const jointsLength = skinJoints.length;
|
|
4640
4126
|
if (jointsLength !== skeleton.jointCount) {
|
|
4641
|
-
|
|
4642
|
-
new JointCountMismatchError(
|
|
4643
|
-
entity as unknown as number,
|
|
4644
|
-
skeleton.jointCount,
|
|
4645
|
-
jointsLength,
|
|
4646
|
-
) as unknown as Error,
|
|
4127
|
+
worldInternal._routeError(
|
|
4128
|
+
new JointCountMismatchError(entity, skeleton.jointCount, jointsLength),
|
|
4647
4129
|
{
|
|
4130
|
+
severity: Severity.Error,
|
|
4648
4131
|
systemName: 'RenderSystem.extract (joint-count-mismatch)',
|
|
4649
4132
|
},
|
|
4650
4133
|
);
|
|
4651
4134
|
continue;
|
|
4652
4135
|
}
|
|
4653
4136
|
// (c) Resolve only each joint's Transform.world column. The
|
|
4654
|
-
// transient view is consumed before
|
|
4137
|
+
// transient view is consumed before a structural mutation and
|
|
4655
4138
|
// therefore preserves the same dangling-joint behavior without
|
|
4656
4139
|
// constructing every other Transform field.
|
|
4657
4140
|
// Build mat4 list eagerly so write happens once per entity (no
|
|
@@ -4660,27 +4143,24 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4660
4143
|
let jointDangling = -1;
|
|
4661
4144
|
for (let jIdx = 0; jIdx < skeleton.jointCount; jIdx++) {
|
|
4662
4145
|
const jointEntityRaw = skinJoints[jIdx] ?? 0;
|
|
4663
|
-
const jointEntity = jointEntityRaw as
|
|
4664
|
-
const
|
|
4665
|
-
if (
|
|
4146
|
+
const jointEntity = jointEntityRaw as EntityHandle;
|
|
4147
|
+
const jointWorld = worldInternal._getArrayView(jointEntity, Transform, 'world');
|
|
4148
|
+
if (jointWorld === undefined) {
|
|
4666
4149
|
jointDangling = jIdx;
|
|
4667
4150
|
break;
|
|
4668
4151
|
}
|
|
4669
|
-
// The
|
|
4670
|
-
//
|
|
4671
|
-
//
|
|
4672
|
-
|
|
4152
|
+
// The allocator consumes the canonical Mat4 brand. Copy the
|
|
4153
|
+
// column view into that owner-created value so the ECS storage
|
|
4154
|
+
// view never crosses the math brand boundary by assertion.
|
|
4155
|
+
const jointWorldMat = mat4.create();
|
|
4156
|
+
jointWorldMat.set(jointWorld);
|
|
4157
|
+
jointWorlds[jIdx] = jointWorldMat;
|
|
4673
4158
|
}
|
|
4674
4159
|
if (jointDangling >= 0) {
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
) as unknown as Error,
|
|
4680
|
-
{
|
|
4681
|
-
systemName: 'RenderSystem.extract (joint-entity-dangling)',
|
|
4682
|
-
},
|
|
4683
|
-
);
|
|
4160
|
+
worldInternal._routeError(new JointEntityDanglingError(entity, jointDangling), {
|
|
4161
|
+
severity: Severity.Error,
|
|
4162
|
+
systemName: 'RenderSystem.extract (joint-entity-dangling)',
|
|
4163
|
+
});
|
|
4684
4164
|
continue;
|
|
4685
4165
|
}
|
|
4686
4166
|
// (d) Slice the IBM flat Float32Array into per-joint Float32Arrays.
|
|
@@ -4699,7 +4179,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4699
4179
|
skinPaletteAllocator.writeJointPalette(slice, ibms, jointWorlds);
|
|
4700
4180
|
skinSlice = {
|
|
4701
4181
|
jointCount: slice.jointCount,
|
|
4702
|
-
jointMatrices: new Float32Array(slice.jointMatrices),
|
|
4703
4182
|
byteOffset: slice.byteOffset,
|
|
4704
4183
|
buffer: slice.buffer,
|
|
4705
4184
|
};
|
|
@@ -4712,14 +4191,11 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4712
4191
|
// record stage copies these 16 floats into the mesh SSBO with zero
|
|
4713
4192
|
// per-snapshot `mat4.compose` (AC-07). A stale slot (generation gone)
|
|
4714
4193
|
// skips the renderable, mirroring the Instances dangling-row sweep.
|
|
4715
|
-
// tweak-20260611 M1 / K-3:
|
|
4194
|
+
// tweak-20260611 M1 / K-3: `_getArrayView` call survives untouched;
|
|
4716
4195
|
// only the `entity` source switched to `bundle.Entity.self[i]`.
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
const
|
|
4720
|
-
const candidateInstanceTransforms = hasInstances
|
|
4721
|
-
? (row.get(Instances)?.transforms as unknown as Float32Array | undefined)
|
|
4722
|
-
: undefined;
|
|
4196
|
+
const worldView = worldInternal._getArrayView(entity, Transform, 'world');
|
|
4197
|
+
if (worldView === undefined) continue;
|
|
4198
|
+
const worldMat = new Float32Array(worldView);
|
|
4723
4199
|
const transformSnap: TransformSnapshot = { world: worldMat };
|
|
4724
4200
|
// feat-20260608 M5 amend / w11-a: per-submesh `materials[]` array
|
|
4725
4201
|
// aligned 1-1 with `MeshAsset.submeshes[]`. materials[0] === the
|
|
@@ -4741,14 +4217,30 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4741
4217
|
subHandle,
|
|
4742
4218
|
world,
|
|
4743
4219
|
assets,
|
|
4744
|
-
gpuStore,
|
|
4745
4220
|
materialSnapshotCache,
|
|
4746
4221
|
persistentMaterialSnapshotCache,
|
|
4747
|
-
context.paramSchemaProjectionCensus,
|
|
4748
4222
|
),
|
|
4749
4223
|
);
|
|
4750
4224
|
}
|
|
4751
4225
|
}
|
|
4226
|
+
if (isRenderable && assets !== undefined && assets !== null) {
|
|
4227
|
+
for (let mi = 1; mi < materialHandles.length; mi++) {
|
|
4228
|
+
const subHandle = materialHandles[mi] ?? 0;
|
|
4229
|
+
if (subHandle === handleRaw) continue;
|
|
4230
|
+
const subEntry =
|
|
4231
|
+
materialSnapshotCache.get(subHandle) ??
|
|
4232
|
+
readPersistentMaterialSnapshot(persistentMaterialSnapshotCache, subHandle, assets);
|
|
4233
|
+
appendMaterialDispatchEntries(
|
|
4234
|
+
pendingDispatch,
|
|
4235
|
+
subEntry?.passes ?? (subHandle === 0 ? [DEFAULT_FORWARD_PASS] : []),
|
|
4236
|
+
entity,
|
|
4237
|
+
subHandle,
|
|
4238
|
+
renderables.length,
|
|
4239
|
+
layerVal,
|
|
4240
|
+
materialsArr[mi]?.paramSnapshot,
|
|
4241
|
+
);
|
|
4242
|
+
}
|
|
4243
|
+
}
|
|
4752
4244
|
|
|
4753
4245
|
// feat-20260625-sprite-instances-and-tilemap-terrain-static-batch M3 /
|
|
4754
4246
|
// w10: SpriteInstances validation + snapshot materialisation.
|
|
@@ -4758,11 +4250,10 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4758
4250
|
if (hasSpriteInstances) {
|
|
4759
4251
|
// (1) mutually exclusive with Instances (peers — pick one).
|
|
4760
4252
|
if (hasInstances) {
|
|
4761
|
-
|
|
4762
|
-
new SpriteInstancesMutuallyExclusiveWithInstancesError(
|
|
4763
|
-
entity as unknown as number,
|
|
4764
|
-
) as unknown as Error,
|
|
4253
|
+
worldInternal._routeError(
|
|
4254
|
+
new SpriteInstancesMutuallyExclusiveWithInstancesError(entity),
|
|
4765
4255
|
{
|
|
4256
|
+
severity: Severity.Error,
|
|
4766
4257
|
systemName: 'RenderSystem.extract (sprite-instances-mutually-exclusive)',
|
|
4767
4258
|
},
|
|
4768
4259
|
);
|
|
@@ -4781,12 +4272,13 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4781
4272
|
materialSnap.materialShaderId !== 'forgeax::sprite' &&
|
|
4782
4273
|
materialSnap.materialShaderId !== 'forgeax::sprite-lit'
|
|
4783
4274
|
) {
|
|
4784
|
-
|
|
4275
|
+
worldInternal._routeError(
|
|
4785
4276
|
new SpriteInstancesRequiresSpriteShaderError(
|
|
4786
|
-
entity
|
|
4277
|
+
entity,
|
|
4787
4278
|
materialSnap.materialShaderId ?? 'undefined',
|
|
4788
|
-
)
|
|
4279
|
+
),
|
|
4789
4280
|
{
|
|
4281
|
+
severity: Severity.Error,
|
|
4790
4282
|
systemName: 'RenderSystem.extract (sprite-instances-requires-sprite-shader)',
|
|
4791
4283
|
},
|
|
4792
4284
|
);
|
|
@@ -4795,9 +4287,8 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4795
4287
|
// (3) count mismatch — transforms.length / 16 === regions.length / 4
|
|
4796
4288
|
// (transforms.length=0 + regions.length=0 is the zero-instance lawful
|
|
4797
4289
|
// boundary; both derivations are 0 and equality holds, so no fire).
|
|
4798
|
-
const
|
|
4799
|
-
const
|
|
4800
|
-
const regions = spriteValue?.regions as unknown as Float32Array | undefined;
|
|
4290
|
+
const transforms = worldInternal._getArrayView(entity, SpriteInstances, 'transforms');
|
|
4291
|
+
const regions = worldInternal._getArrayView(entity, SpriteInstances, 'regions');
|
|
4801
4292
|
if (transforms !== undefined && regions !== undefined) {
|
|
4802
4293
|
const transformsLength = transforms.length;
|
|
4803
4294
|
const regionsLength = regions.length;
|
|
@@ -4808,12 +4299,10 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4808
4299
|
const tCount = transformsLength / 16;
|
|
4809
4300
|
const rCount = regionsLength / 4;
|
|
4810
4301
|
if (transformsLength % 16 !== 0 || regionsLength % 4 !== 0 || tCount !== rCount) {
|
|
4811
|
-
|
|
4812
|
-
new SpriteInstancesCountMismatchError(
|
|
4813
|
-
transformsLength,
|
|
4814
|
-
regionsLength,
|
|
4815
|
-
) as unknown as Error,
|
|
4302
|
+
worldInternal._routeError(
|
|
4303
|
+
new SpriteInstancesCountMismatchError(transformsLength, regionsLength),
|
|
4816
4304
|
{
|
|
4305
|
+
severity: Severity.Error,
|
|
4817
4306
|
systemName: 'RenderSystem.extract (sprite-instances-count-mismatch)',
|
|
4818
4307
|
},
|
|
4819
4308
|
);
|
|
@@ -4828,35 +4317,39 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4828
4317
|
transforms: transformsCopy,
|
|
4829
4318
|
regions: regionsCopy,
|
|
4830
4319
|
instanceCount: tCount,
|
|
4831
|
-
cacheKey: entity
|
|
4320
|
+
cacheKey: entity,
|
|
4832
4321
|
archVersion,
|
|
4833
4322
|
};
|
|
4834
4323
|
}
|
|
4835
4324
|
}
|
|
4836
4325
|
|
|
4837
4326
|
let localAabb: Float32Array | undefined;
|
|
4327
|
+
let morph: MorphSnapshot | undefined;
|
|
4838
4328
|
const assetHandleRaw = Math.round(fAssetHandle ?? 0);
|
|
4839
4329
|
if (assetHandleRaw !== 0) {
|
|
4840
|
-
const meshRes =
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4330
|
+
const meshRes = resolveAssetHandle(world, toShared<'MeshAsset'>(assetHandleRaw));
|
|
4331
|
+
if (meshRes.ok && meshRes.value.kind === 'mesh') {
|
|
4332
|
+
const meshAsset = meshRes.value as MeshAsset;
|
|
4333
|
+
morph = morphSnapshotFor(meshAsset, morphWeightsView);
|
|
4334
|
+
const meshAabb = meshAsset.aabb;
|
|
4335
|
+
// Morph targets can expand the authored bounds. Keep the
|
|
4336
|
+
// specialized lane conservative until a target-aware bounds
|
|
4337
|
+
// projection is available; a false-positive draw is preferable
|
|
4338
|
+
// to culling a valid deformed vertex.
|
|
4339
|
+
if (morph === undefined && hasFiniteOrderedLocalAabb(meshAabb)) {
|
|
4340
|
+
localAabb = new Float32Array(meshAabb);
|
|
4341
|
+
}
|
|
4342
|
+
}
|
|
4846
4343
|
}
|
|
4847
4344
|
|
|
4848
4345
|
let nonIndexedFirst = 0;
|
|
4849
|
-
const gpuDrivenDraws = gpuDrivenSubmeshes.flatMap((submesh
|
|
4346
|
+
const gpuDrivenDraws = gpuDrivenSubmeshes.flatMap((submesh) => {
|
|
4850
4347
|
const drawMaterial = materialsArr[submesh.materialSlot] ?? materialSnap;
|
|
4851
|
-
const forwardPass = gpuDrivenForwardPass(drawMaterial, materialSnapshotCache);
|
|
4852
4348
|
const first = gpuDrivenIndexed ? submesh.indexOffset : nonIndexedFirst;
|
|
4853
4349
|
nonIndexedFirst += submesh.vertexCount;
|
|
4854
|
-
if (drawMaterial.transparent === true
|
|
4350
|
+
if (drawMaterial.transparent === true) return [];
|
|
4855
4351
|
return [
|
|
4856
4352
|
{
|
|
4857
|
-
submeshIndex,
|
|
4858
|
-
passIndex: forwardPass.passIndex,
|
|
4859
|
-
queue: forwardPass.queue,
|
|
4860
4353
|
kind: gpuDrivenIndexed ? ('indexed' as const) : ('non-indexed' as const),
|
|
4861
4354
|
first,
|
|
4862
4355
|
count: gpuDrivenIndexed ? submesh.indexCount : submesh.vertexCount,
|
|
@@ -4871,53 +4364,19 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4871
4364
|
const baseRenderable: RenderableSnapshot = {
|
|
4872
4365
|
assetHandle: Math.round(fAssetHandle ?? 0),
|
|
4873
4366
|
transform: transformSnap,
|
|
4874
|
-
...(boundsWorld !== undefined ? { boundsWorld } : {}),
|
|
4875
|
-
...(candidateInstanceTransforms !== undefined
|
|
4876
|
-
? { candidateInstanceTransforms: new Float32Array(candidateInstanceTransforms) }
|
|
4877
|
-
: {}),
|
|
4878
4367
|
...(localAabb !== undefined ? { localAabb } : {}),
|
|
4879
4368
|
material: materialSnap,
|
|
4880
4369
|
materials: materialsArr,
|
|
4881
4370
|
materialBindingSources,
|
|
4882
4371
|
materialBindingDiagnostics,
|
|
4883
|
-
...(gpuDrivenDraws.length
|
|
4372
|
+
...(morph === undefined && gpuDrivenDraws.length > 0 ? { gpuDrivenDraws } : {}),
|
|
4884
4373
|
worldId: 0,
|
|
4885
|
-
entityKey: entity
|
|
4886
|
-
candidateIndex,
|
|
4374
|
+
entityKey: entity,
|
|
4887
4375
|
...(skinSlice !== undefined ? { skin: skinSlice } : {}),
|
|
4888
|
-
...(
|
|
4376
|
+
...(morph !== undefined ? { morph } : {}),
|
|
4889
4377
|
...(spriteInstancesSnap !== undefined ? { spriteInstances: spriteInstancesSnap } : {}),
|
|
4890
4378
|
};
|
|
4891
4379
|
|
|
4892
|
-
let publishedRenderable = baseRenderable;
|
|
4893
|
-
if (hasInstances) {
|
|
4894
|
-
const instanceValue = row.get(Instances);
|
|
4895
|
-
const transforms = instanceValue?.transforms as unknown as Float32Array | undefined;
|
|
4896
|
-
if (transforms !== undefined) {
|
|
4897
|
-
const actualLength = transforms.length;
|
|
4898
|
-
if (actualLength % 16 !== 0) {
|
|
4899
|
-
reportRenderError(new InstanceTransformsStrideMismatchError(actualLength), {
|
|
4900
|
-
systemName: 'RenderSystem.extract (Instances stride)',
|
|
4901
|
-
});
|
|
4902
|
-
|
|
4903
|
-
continue;
|
|
4904
|
-
}
|
|
4905
|
-
publishedRenderable = {
|
|
4906
|
-
...baseRenderable,
|
|
4907
|
-
instances: {
|
|
4908
|
-
transforms: new Float32Array(transforms),
|
|
4909
|
-
instanceCount: Math.max(1, Math.floor(actualLength / 16)),
|
|
4910
|
-
cacheKey: entity as unknown as number,
|
|
4911
|
-
archVersion,
|
|
4912
|
-
},
|
|
4913
|
-
};
|
|
4914
|
-
}
|
|
4915
|
-
}
|
|
4916
|
-
|
|
4917
|
-
// Publish only the final, validated candidate snapshot. Its index is
|
|
4918
|
-
// the current candidate-domain slot used by the staged dispatch.
|
|
4919
|
-
candidateRenderables.push(publishedRenderable);
|
|
4920
|
-
|
|
4921
4380
|
// feat-20260528-frustum-culling M3 / w10: frustum culling check.
|
|
4922
4381
|
// Skip the entity if a valid AABB exists AND ALL cameras' frusta
|
|
4923
4382
|
// reject the world-space AABB. Missing or malformed AABBs are
|
|
@@ -4929,13 +4388,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4929
4388
|
// directly (no compose) -- same source the record stage feeds
|
|
4930
4389
|
// the mesh SSBO, so cull stays same-source with render (AC-05).
|
|
4931
4390
|
const worldAabb = box3.create();
|
|
4932
|
-
box3.transformBox3(
|
|
4933
|
-
worldAabb,
|
|
4934
|
-
localAabb,
|
|
4935
|
-
transformSnap.world as unknown as Parameters<typeof box3.transformBox3>[2],
|
|
4936
|
-
);
|
|
4391
|
+
box3.transformBox3(worldAabb, localAabb, transformSnap.world);
|
|
4937
4392
|
|
|
4938
|
-
// Test against all cameras. Entity is visible if
|
|
4393
|
+
// Test against all cameras. Entity is visible if one camera
|
|
4939
4394
|
// frustum intersects the world-space AABB (or planes are empty
|
|
4940
4395
|
// from degenerate projection).
|
|
4941
4396
|
frustumTotal += 1;
|
|
@@ -4953,13 +4408,42 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4953
4408
|
}
|
|
4954
4409
|
if (!visible) {
|
|
4955
4410
|
frustumCulled += 1;
|
|
4956
|
-
for (const entry of pendingDispatch) candidateDispatch.push(entry);
|
|
4957
4411
|
continue;
|
|
4958
4412
|
}
|
|
4959
4413
|
}
|
|
4960
4414
|
|
|
4961
|
-
|
|
4962
|
-
|
|
4415
|
+
if (hasInstances) {
|
|
4416
|
+
const transforms = worldInternal._getArrayView(entity, Instances, 'transforms');
|
|
4417
|
+
if (transforms === undefined) {
|
|
4418
|
+
flushPendingDispatch(pendingDispatch);
|
|
4419
|
+
renderables.push(baseRenderable);
|
|
4420
|
+
} else {
|
|
4421
|
+
const actualLength = transforms.length;
|
|
4422
|
+
if (actualLength % 16 !== 0) {
|
|
4423
|
+
worldInternal._routeError(new InstanceTransformsStrideMismatchError(actualLength), {
|
|
4424
|
+
severity: Severity.Error,
|
|
4425
|
+
systemName: 'RenderSystem.extract (Instances stride)',
|
|
4426
|
+
});
|
|
4427
|
+
|
|
4428
|
+
continue;
|
|
4429
|
+
}
|
|
4430
|
+
const snapshotCopy = new Float32Array(transforms);
|
|
4431
|
+
const instanceCount = Math.max(1, Math.floor(actualLength / 16));
|
|
4432
|
+
flushPendingDispatch(pendingDispatch);
|
|
4433
|
+
renderables.push({
|
|
4434
|
+
...baseRenderable,
|
|
4435
|
+
instances: {
|
|
4436
|
+
transforms: snapshotCopy,
|
|
4437
|
+
instanceCount,
|
|
4438
|
+
cacheKey: entity,
|
|
4439
|
+
archVersion,
|
|
4440
|
+
},
|
|
4441
|
+
});
|
|
4442
|
+
}
|
|
4443
|
+
} else {
|
|
4444
|
+
flushPendingDispatch(pendingDispatch);
|
|
4445
|
+
renderables.push(baseRenderable);
|
|
4446
|
+
}
|
|
4963
4447
|
}
|
|
4964
4448
|
|
|
4965
4449
|
// feat-20260520-2d-sprite-layer-mvp M-3 / w22 + w25: finalise the
|
|
@@ -4973,53 +4457,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4973
4457
|
}
|
|
4974
4458
|
}
|
|
4975
4459
|
|
|
4976
|
-
// M3 / w26: sort dispatch entries by queue (ascending, stable sort)
|
|
4977
|
-
//
|
|
4978
|
-
|
|
4979
|
-
const visibleByCandidate = new Map<number, number>();
|
|
4980
|
-
for (let index = 0; index < renderables.length; index += 1) {
|
|
4981
|
-
const candidateIndex = renderables[index]?.candidateIndex;
|
|
4982
|
-
if (candidateIndex !== undefined) visibleByCandidate.set(candidateIndex, index);
|
|
4983
|
-
}
|
|
4984
|
-
const sortedCandidateDispatch = sortDispatchByQueue(candidateDispatch);
|
|
4985
|
-
const candidateDomain = buildRenderCandidateDomain(candidateRenderables, sortedCandidateDispatch);
|
|
4986
|
-
const candidateDispatchMembership = candidateDomain.membership;
|
|
4987
|
-
dispatch = sortDispatchByQueue(
|
|
4988
|
-
sortedCandidateDispatch.flatMap((entry) => {
|
|
4989
|
-
const renderableIndex = visibleByCandidate.get(entry.renderableIndex);
|
|
4990
|
-
return renderableIndex === undefined ? [] : [{ ...entry, renderableIndex }];
|
|
4991
|
-
}),
|
|
4992
|
-
);
|
|
4993
|
-
const finalizedDirectionalCsm =
|
|
4994
|
-
directionalCsmConfig !== undefined && directionalCsmDirection !== undefined
|
|
4995
|
-
? computeDirectionalCsm(
|
|
4996
|
-
directionalCsmDirection,
|
|
4997
|
-
directionalCsmConfig,
|
|
4998
|
-
cameraData,
|
|
4999
|
-
renderCandidateCsmCandidates(candidateDomain),
|
|
5000
|
-
)
|
|
5001
|
-
: undefined;
|
|
5002
|
-
if (finalizedDirectionalCsm !== null && finalizedDirectionalCsm !== undefined) {
|
|
5003
|
-
lightViewProj = finalizedDirectionalCsm.lightViewProj;
|
|
5004
|
-
splitPlanes = finalizedDirectionalCsm.splitPlanes;
|
|
5005
|
-
cascadeCount = finalizedDirectionalCsm.cascadeCount;
|
|
5006
|
-
cascadeBlend = finalizedDirectionalCsm.cascadeBlend;
|
|
5007
|
-
shadowMapSize = finalizedDirectionalCsm.shadowMapSize;
|
|
5008
|
-
}
|
|
5009
|
-
lights = {
|
|
5010
|
-
...lights,
|
|
5011
|
-
lightViewProj,
|
|
5012
|
-
splitPlanes: splitPlanes !== undefined ? paddedSplitPlanes : undefined,
|
|
5013
|
-
cascadeCount,
|
|
5014
|
-
cascadeBlend,
|
|
5015
|
-
shadowMapSize,
|
|
5016
|
-
...(finalizedDirectionalCsm !== null && finalizedDirectionalCsm !== undefined
|
|
5017
|
-
? {
|
|
5018
|
-
directionalCsm: finalizedDirectionalCsm,
|
|
5019
|
-
cascades: finalizedDirectionalCsm.cascades,
|
|
5020
|
-
}
|
|
5021
|
-
: {}),
|
|
5022
|
-
};
|
|
4460
|
+
// M3 / w26: sort dispatch entries by queue (ascending, stable sort)
|
|
4461
|
+
// per plan-strategy D-3.
|
|
4462
|
+
dispatch = sortDispatchByQueue(dispatch);
|
|
5023
4463
|
|
|
5024
4464
|
// D-1: collect PostProcessParams entities into Map<shaderId, Uint8Array>.
|
|
5025
4465
|
// Last-one-wins when multiple entities bear the same shader id (mirrors
|
|
@@ -5037,17 +4477,17 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
5037
4477
|
// provider. The engine bridges the active camera's `Camera.exposure /
|
|
5038
4478
|
// whitePoint / tonemap` onto the SAME unified params channel custom
|
|
5039
4479
|
// post-processes use — `Camera.exposure` stays the AI-user-facing SSOT (D-5),
|
|
5040
|
-
// the engine itself acts as the provider for the
|
|
4480
|
+
// the engine itself acts as the provider for the Standard tonemap shader
|
|
5041
4481
|
// id. The 16B layout is byte-identical to the prior recordTonemapPass packing
|
|
5042
4482
|
// (render-system-record.ts pre-w14): Float32 [exposure, whitePoint, _, pad]
|
|
5043
4483
|
// with the mode u32 occupying the third 4-byte slot via tonemapToU32 (SSOT in
|
|
5044
4484
|
// camera.ts). Run AFTER the user-entity collection above so the engine's
|
|
5045
4485
|
// built-in provider is authoritative for its own reserved key (a user entity
|
|
5046
|
-
// can never shadow
|
|
4486
|
+
// can never shadow the Standard tonemap identity. The single active camera mirrors
|
|
5047
4487
|
// recordFrame's `activeCameras[0]` selection.
|
|
5048
4488
|
const tonemapCamera = cameras[0];
|
|
5049
4489
|
if (tonemapCamera !== undefined) {
|
|
5050
|
-
postProcessParams.set(
|
|
4490
|
+
postProcessParams.set(STANDARD_TONEMAP_FEATURE_ID, tonemapParams(tonemapCamera));
|
|
5051
4491
|
}
|
|
5052
4492
|
|
|
5053
4493
|
return {
|
|
@@ -5055,11 +4495,10 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
5055
4495
|
lights,
|
|
5056
4496
|
renderables,
|
|
5057
4497
|
dispatch,
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
environment,
|
|
4498
|
+
skylight,
|
|
4499
|
+
skylightCount,
|
|
4500
|
+
skybox,
|
|
4501
|
+
skyboxCount,
|
|
5063
4502
|
frustumStats: { culled: frustumCulled, total: frustumTotal },
|
|
5064
4503
|
visibilityStats: { explicitlyHidden: explicitlyHidden.size },
|
|
5065
4504
|
postProcessParams,
|
|
@@ -5069,135 +4508,15 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
5069
4508
|
};
|
|
5070
4509
|
}
|
|
5071
4510
|
|
|
5072
|
-
/**
|
|
5073
|
-
* Extract the complete render-domain facts for one known entity.
|
|
5074
|
-
*
|
|
5075
|
-
* This deliberately reuses extractFrame's MeshRenderer row body instead of
|
|
5076
|
-
* maintaining a second material/skin/sprite implementation. Query.at() makes
|
|
5077
|
-
* the row source identity-addressed; callers must supply a visibility snapshot
|
|
5078
|
-
* appropriate for the current persistent owner and must keep hierarchy-affecting
|
|
5079
|
-
* changes on the conservative rebuild lane.
|
|
5080
|
-
*/
|
|
5081
|
-
export interface ExtractedRenderableEntity {
|
|
5082
|
-
readonly snapshot: RenderableSnapshot;
|
|
5083
|
-
readonly candidateDispatch: readonly DispatchEntry[];
|
|
5084
|
-
}
|
|
5085
|
-
|
|
5086
|
-
export function extractRenderableEntity(
|
|
5087
|
-
world: World,
|
|
5088
|
-
entity: EntityHandle,
|
|
5089
|
-
context: PreparedExtractContext,
|
|
5090
|
-
): ExtractedRenderableEntity | undefined {
|
|
5091
|
-
const frame = extractFrame(world, {
|
|
5092
|
-
...context,
|
|
5093
|
-
cull: 'none',
|
|
5094
|
-
cullCameras: undefined,
|
|
5095
|
-
targetRenderableEntities: [entity],
|
|
5096
|
-
});
|
|
5097
|
-
const domain =
|
|
5098
|
-
frame.candidateDomain ??
|
|
5099
|
-
buildRenderCandidateDomain(
|
|
5100
|
-
frame.candidateRenderables ?? frame.renderables,
|
|
5101
|
-
frame.candidateDispatch ?? frame.dispatch,
|
|
5102
|
-
);
|
|
5103
|
-
const candidateIndex = domain.indexByIdentity.get(`0:${entity}`);
|
|
5104
|
-
if (candidateIndex === undefined) return undefined;
|
|
5105
|
-
const snapshot = domain.renderables[candidateIndex];
|
|
5106
|
-
if (snapshot === undefined) return undefined;
|
|
5107
|
-
return {
|
|
5108
|
-
snapshot,
|
|
5109
|
-
candidateDispatch: domain.dispatch.filter((entry) => entry.renderableIndex === candidateIndex),
|
|
5110
|
-
};
|
|
5111
|
-
}
|
|
5112
|
-
|
|
5113
|
-
const RESOURCE_REFRESH_HIERARCHY: SceneHierarchySnapshot = {
|
|
5114
|
-
parentOf: new Map(),
|
|
5115
|
-
diagnostics: [],
|
|
5116
|
-
getParent: () => undefined,
|
|
5117
|
-
};
|
|
5118
|
-
|
|
5119
|
-
const RESOURCE_REFRESH_VISIBILITY: VisibilitySnapshot = {
|
|
5120
|
-
diagnostics: [],
|
|
5121
|
-
hasAnyIntent: false,
|
|
5122
|
-
hasAnyHiddenIntent: false,
|
|
5123
|
-
get: () => undefined,
|
|
5124
|
-
effective: () => 'visible',
|
|
5125
|
-
};
|
|
5126
|
-
|
|
5127
|
-
/**
|
|
5128
|
-
* Refresh only light, shadow, sky, and post-process resources over an existing
|
|
5129
|
-
* persistent frame. The resource path intentionally skips hierarchy,
|
|
5130
|
-
* renderable, material, cull, dispatch, and candidate-domain extraction.
|
|
5131
|
-
*/
|
|
5132
|
-
export function refreshExtractedFrameLights(world: World, frame: ExtractedFrame): ExtractedFrame {
|
|
5133
|
-
const refreshed = extractFrame(world, {
|
|
5134
|
-
assets: undefined,
|
|
5135
|
-
pipelineState: undefined,
|
|
5136
|
-
gpuStore: undefined,
|
|
5137
|
-
materialSnapshotCache: undefined,
|
|
5138
|
-
cull: 'none',
|
|
5139
|
-
cullCameras: undefined,
|
|
5140
|
-
hierarchy: RESOURCE_REFRESH_HIERARCHY,
|
|
5141
|
-
visibility: frame.visibilitySnapshots[0] ?? RESOURCE_REFRESH_VISIBILITY,
|
|
5142
|
-
resourceRefreshBase: frame,
|
|
5143
|
-
});
|
|
5144
|
-
const fog = selectFog(world, 0);
|
|
5145
|
-
if (!fog.ok) {
|
|
5146
|
-
routeFogSelectionError(world, fog.error);
|
|
5147
|
-
return refreshed;
|
|
5148
|
-
}
|
|
5149
|
-
return { ...refreshed, fog: fog.value.snapshot };
|
|
5150
|
-
}
|
|
5151
|
-
|
|
5152
|
-
export function defaultTransformSnapshot(): TransformSnapshot {
|
|
5153
|
-
// Identity world mat4 (column-major 16 floats).
|
|
5154
|
-
return {
|
|
5155
|
-
world: new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
|
|
5156
|
-
};
|
|
5157
|
-
}
|
|
5158
|
-
|
|
5159
|
-
// The default material path is hit once per renderable. Deriving its schema
|
|
5160
|
-
// also hashes the layout, so retain the immutable projection instead of doing
|
|
5161
|
-
// that work for every entity in a frame.
|
|
5162
|
-
const DEFAULT_UNLIT_DERIVED_MATERIAL_SLOT = (() => {
|
|
5163
|
-
const derived = deriveMaterialSlotProjection({
|
|
5164
|
-
material: '0',
|
|
5165
|
-
schema: DEFAULT_UNLIT_PARAM_SCHEMA,
|
|
5166
|
-
values: { baseColor: [0.5, 0.5, 0.5, 1], alphaCutoff: 0 },
|
|
5167
|
-
coordinates: defaultMaterialTextureCoordinates(DEFAULT_UNLIT_PARAM_SCHEMA),
|
|
5168
|
-
});
|
|
5169
|
-
if (!derived.ok) throw new Error('engine default material schema must derive');
|
|
5170
|
-
return derived.value;
|
|
5171
|
-
})();
|
|
5172
|
-
const DEFAULT_MATERIAL_SNAPSHOT: MaterialSnapshot = Object.freeze({
|
|
5173
|
-
baseColor: vec3.create(0.5, 0.5, 0.5),
|
|
5174
|
-
metallic: 0,
|
|
5175
|
-
roughness: 1,
|
|
5176
|
-
materialHandle: 0,
|
|
5177
|
-
materialParamSchema: DEFAULT_UNLIT_PARAM_SCHEMA,
|
|
5178
|
-
derivedMaterialSlot: DEFAULT_UNLIT_DERIVED_MATERIAL_SLOT,
|
|
5179
|
-
});
|
|
5180
|
-
|
|
5181
4511
|
export function defaultMaterialSnapshot(materialHandle = 0): MaterialSnapshot {
|
|
5182
4512
|
// Mid-grey unlit fallback (D-Q7 case B + extract-stage missing-spec),
|
|
5183
4513
|
// matching the pre-w6 visual outcome where the record stage's force-cast
|
|
5184
4514
|
// read of `firstMaterial.baseColorTexture` returned undefined and the
|
|
5185
4515
|
// unlit fallback shader was selected.
|
|
5186
|
-
const materialParamSchema = DEFAULT_UNLIT_PARAM_SCHEMA;
|
|
5187
|
-
if (materialHandle === 0) return DEFAULT_MATERIAL_SNAPSHOT;
|
|
5188
|
-
const derivedMaterialSlot = deriveMaterialSlotProjection({
|
|
5189
|
-
material: String(materialHandle),
|
|
5190
|
-
schema: materialParamSchema,
|
|
5191
|
-
values: { baseColor: [0.5, 0.5, 0.5, 1], alphaCutoff: 0 },
|
|
5192
|
-
coordinates: defaultMaterialTextureCoordinates(materialParamSchema),
|
|
5193
|
-
}).unwrap();
|
|
5194
4516
|
return {
|
|
5195
4517
|
baseColor: vec3.create(0.5, 0.5, 0.5),
|
|
5196
4518
|
metallic: 0,
|
|
5197
4519
|
roughness: 1,
|
|
5198
4520
|
materialHandle,
|
|
5199
|
-
shadowCaster: true,
|
|
5200
|
-
materialParamSchema,
|
|
5201
|
-
derivedMaterialSlot,
|
|
5202
4521
|
};
|
|
5203
4522
|
}
|