@forgeax/engine-render 0.1.4 → 0.1.7
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 +268 -640
- 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 +214 -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 +11 -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-HCOGPHKS.mjs +93 -0
- package/dist/chunk-HCOGPHKS.mjs.map +1 -0
- package/dist/chunk-YEJIUW7D.mjs +1152 -0
- package/dist/chunk-YEJIUW7D.mjs.map +1 -0
- package/dist/components/__tests__/points-lines-admission.unit.test.d.ts +2 -0
- package/dist/components/__tests__/points-lines-admission.unit.test.d.ts.map +1 -0
- package/dist/components/__tests__/points-lines-schema.unit.test.d.ts +2 -0
- package/dist/components/__tests__/points-lines-schema.unit.test.d.ts.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 +4 -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/lines.d.ts +5 -0
- package/dist/components/lines.d.ts.map +1 -0
- package/dist/components/mesh-filter.d.ts +7 -7
- package/dist/components/mesh-filter.d.ts.map +1 -1
- package/dist/components/points.d.ts +15 -0
- package/dist/components/points.d.ts.map +1 -0
- 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 +27463 -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 +320 -248
- 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 +7 -3
- 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 +17 -43
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +167 -3
- package/dist/index.mjs.map +1 -1
- package/dist/inspection-types.d.ts +72 -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 +77 -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/points-lines/__tests__/bounds.unit.test.d.ts +2 -0
- package/dist/points-lines/__tests__/bounds.unit.test.d.ts.map +1 -0
- package/dist/points-lines/__tests__/cache-recovery.unit.test.d.ts +2 -0
- package/dist/points-lines/__tests__/cache-recovery.unit.test.d.ts.map +1 -0
- package/dist/points-lines/__tests__/inspection-lanes.integration.test.d.ts +2 -0
- package/dist/points-lines/__tests__/inspection-lanes.integration.test.d.ts.map +1 -0
- package/dist/points-lines/__tests__/inspection.unit.test.d.ts +2 -0
- package/dist/points-lines/__tests__/inspection.unit.test.d.ts.map +1 -0
- package/dist/points-lines/__tests__/lane-structural.integration.test.d.ts +2 -0
- package/dist/points-lines/__tests__/lane-structural.integration.test.d.ts.map +1 -0
- package/dist/points-lines/__tests__/points-lines.bench.d.ts +2 -0
- package/dist/points-lines/__tests__/points-lines.bench.d.ts.map +1 -0
- package/dist/points-lines/__tests__/raster-lane.integration.test.d.ts +2 -0
- package/dist/points-lines/__tests__/raster-lane.integration.test.d.ts.map +1 -0
- package/dist/points-lines/__tests__/snapshot.unit.test.d.ts +2 -0
- package/dist/points-lines/__tests__/snapshot.unit.test.d.ts.map +1 -0
- package/dist/points-lines/__tests__/view-ubo.unit.test.d.ts +2 -0
- package/dist/points-lines/__tests__/view-ubo.unit.test.d.ts.map +1 -0
- package/dist/points-lines/admission.d.ts +36 -0
- package/dist/points-lines/admission.d.ts.map +1 -0
- package/dist/points-lines/bounds.d.ts +8 -0
- package/dist/points-lines/bounds.d.ts.map +1 -0
- package/dist/points-lines/expansion-cache.d.ts +28 -0
- package/dist/points-lines/expansion-cache.d.ts.map +1 -0
- package/dist/points-lines/inspection.d.ts +53 -0
- package/dist/points-lines/inspection.d.ts.map +1 -0
- package/dist/points-lines/prepare.d.ts +67 -0
- package/dist/points-lines/prepare.d.ts.map +1 -0
- package/dist/points-lines/record.d.ts +60 -0
- package/dist/points-lines/record.d.ts.map +1 -0
- package/dist/points-lines/snapshot.d.ts +49 -0
- package/dist/points-lines/snapshot.d.ts.map +1 -0
- 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 +12 -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 +252 -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 +9 -3
- package/dist/record/view-ubo.d.ts.map +1 -1
- package/dist/render-contract.d.ts +424 -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 +110 -338
- package/dist/render-system-extract.d.ts.map +1 -1
- package/dist/render-system.d.ts +24 -713
- package/dist/render-system.d.ts.map +1 -1
- package/dist/scene/render-scene-types.d.ts +61 -0
- package/dist/scene/render-scene-types.d.ts.map +1 -0
- package/dist/scene/render-scene.d.ts +112 -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__/authoring-path.unit.test.ts +9 -0
- 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 +21 -18
- 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 +31 -58
- package/src/__tests__/material-snapshot-mutation.integration.test.ts +10 -15
- package/src/__tests__/material-static-dynamic-route.unit.test.ts +31 -50
- 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 +24 -23
- package/src/__tests__/render-error-detail-ssot.test-d.ts +0 -95
- package/src/__tests__/render-error-exhaustive.test-d.ts +27 -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 +82 -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} +1741 -2699
- package/src/assembly/host-contract.ts +101 -0
- package/src/{renderer → assembly}/material/assembly.ts +2 -7
- package/src/assembly/material/pipeline-projection.ts +63 -0
- package/src/authoring.ts +15 -22
- package/src/components/__tests__/points-lines-admission.unit.test.ts +219 -0
- package/src/components/__tests__/points-lines-schema.unit.test.ts +56 -0
- package/src/components/__tests__/schema-contract.unit.test.ts +10 -0
- package/src/components/glyph-text.ts +3 -2
- package/src/components/index.ts +14 -9
- package/src/components/instances.ts +4 -5
- package/src/components/light-helpers.ts +1 -1
- package/src/components/lines.ts +18 -0
- package/src/components/mesh-filter.ts +11 -10
- package/src/components/points.ts +40 -0
- 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 +469 -429
- 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 +32 -8
- 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 +99 -1109
- 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 +75 -217
- package/src/inspection-types.ts +80 -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 +441 -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 +7 -75
- package/src/points-lines/__tests__/bounds.unit.test.ts +21 -0
- package/src/points-lines/__tests__/cache-recovery.unit.test.ts +157 -0
- package/src/points-lines/__tests__/inspection-lanes.integration.test.ts +146 -0
- package/src/points-lines/__tests__/inspection.unit.test.ts +173 -0
- package/src/points-lines/__tests__/lane-structural.integration.test.ts +122 -0
- package/src/points-lines/__tests__/points-lines.bench.ts +400 -0
- package/src/points-lines/__tests__/raster-lane.integration.test.ts +44 -0
- package/src/points-lines/__tests__/snapshot.unit.test.ts +101 -0
- package/src/points-lines/__tests__/view-ubo.unit.test.ts +73 -0
- package/src/points-lines/admission.ts +238 -0
- package/src/points-lines/bounds.ts +23 -0
- package/src/points-lines/expansion-cache.ts +215 -0
- package/src/points-lines/inspection.ts +97 -0
- package/src/points-lines/prepare.ts +177 -0
- package/src/points-lines/record.ts +125 -0
- package/src/points-lines/snapshot.ts +130 -0
- 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 +88 -197
- package/src/record/frame-snapshot.ts +45 -60
- package/src/record/frame-targets.ts +8 -243
- package/src/record/frame.ts +521 -797
- package/src/record/helpers.ts +6 -11
- package/src/record/main-pass-geometry.ts +198 -147
- package/src/record/main-pass-material.ts +517 -190
- package/src/record/main-pass-sprite-draws.ts +199 -118
- package/src/record/main-pass.ts +166 -144
- package/src/record/mesh-ssbo.ts +517 -38
- package/src/record/render-context.ts +384 -0
- package/src/record/shadow-pass.ts +154 -1843
- package/src/record/skybox-post-pass.ts +14 -15
- package/src/record/typed-frame-graph.ts +224 -262
- package/src/record/view-ubo.ts +53 -37
- package/src/render-contract.ts +582 -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 +1114 -1732
- package/src/render-system.ts +614 -1939
- package/src/scene/render-scene-types.ts +67 -0
- package/src/scene/render-scene.ts +1087 -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,369 +142,66 @@ 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,
|
|
162
|
+
Lines,
|
|
173
163
|
MeshFilter,
|
|
174
164
|
MeshRenderer,
|
|
175
165
|
PointLight,
|
|
176
166
|
PointLightShadow,
|
|
167
|
+
Points,
|
|
177
168
|
PostProcessParams,
|
|
169
|
+
pointShapeFromU32,
|
|
178
170
|
SkyboxBackground,
|
|
179
171
|
Skylight,
|
|
172
|
+
SortKey,
|
|
180
173
|
SpotLight,
|
|
181
174
|
SpriteInstances,
|
|
182
175
|
SpriteRegionOverride,
|
|
183
|
-
type Tonemap,
|
|
184
176
|
tonemapFromF32,
|
|
185
177
|
tonemapToU32,
|
|
186
178
|
} from './components';
|
|
187
179
|
import { GlyphText } from './components/glyph-text';
|
|
180
|
+
import { computeInvRangeSquared, degToCos } from './components/light-helpers';
|
|
188
181
|
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
182
|
MaterialSkinAttrMissingError,
|
|
204
|
-
|
|
183
|
+
ShadowInvalidConfigError,
|
|
205
184
|
SkinMaterialMismatchError,
|
|
206
185
|
} 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
186
|
import { resolveVisibility, type VisibilitySnapshot } from './extract/visibility';
|
|
247
187
|
import type {
|
|
248
188
|
RenderFeatureHiddenEntityReport,
|
|
249
189
|
RenderFeatureWorldVisibilitySnapshot,
|
|
250
190
|
} from './features/types';
|
|
251
191
|
import { ensureGlyphMeshMaterialSlots } from './glyph-text-layout-system';
|
|
252
|
-
import type { RenderSceneFactInput } from './gpu-scene-schema';
|
|
253
192
|
import {
|
|
254
193
|
type MeshMaterialBindingDiagnostic,
|
|
255
194
|
type MeshMaterialBindingSource,
|
|
256
195
|
resolveMeshMaterialBindings,
|
|
257
196
|
} from './mesh-material-bindings';
|
|
197
|
+
import { isStandardPbrMaterialShader } from './pbr-pipeline';
|
|
198
|
+
import { expandPointsLinesBounds } from './points-lines/bounds';
|
|
199
|
+
import type { PointsLinesRetainedSnapshot, PointsLinesStyle } from './points-lines/snapshot';
|
|
200
|
+
import { type CameraSnapshot, STANDARD_TONEMAP_FEATURE_ID } from './render-contract';
|
|
258
201
|
import { getActiveCamera, selectActiveCameraIndex } from './systems/active-camera';
|
|
259
202
|
import { selectPasses } from './systems/pass-selector';
|
|
260
203
|
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
|
-
}
|
|
204
|
+
import type { SkinPaletteSlice } from './systems/skin-palette-types';
|
|
504
205
|
|
|
505
206
|
/**
|
|
506
207
|
* DirectionalLightSnapshot — sun-like infinite light variant of the
|
|
@@ -613,8 +314,6 @@ export interface PointShadowSnapshot {
|
|
|
613
314
|
*/
|
|
614
315
|
export interface SpotLightSnapshot {
|
|
615
316
|
readonly kind: 'spot';
|
|
616
|
-
/** ECS entity handle in the source World; diagnostic identity only. */
|
|
617
|
-
readonly entity: number;
|
|
618
317
|
readonly position: Vec3;
|
|
619
318
|
readonly direction: Vec3;
|
|
620
319
|
readonly color: Vec3;
|
|
@@ -744,10 +443,6 @@ export interface ExtractedLights {
|
|
|
744
443
|
* when castShadow=false or no directional light.
|
|
745
444
|
*/
|
|
746
445
|
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
446
|
}
|
|
752
447
|
|
|
753
448
|
/**
|
|
@@ -757,7 +452,7 @@ export interface ExtractedLights {
|
|
|
757
452
|
* `equirectHandle` carries the packed u32 handle for
|
|
758
453
|
* `Handle<EquirectAsset, 'shared'>`; the record stage drives the internal
|
|
759
454
|
* lazy cubemap projection from it and resolves the projected GPU cubemap via
|
|
760
|
-
* `
|
|
455
|
+
* `GpuResidencyCache.getCubemapGpuView(...)` and related helpers
|
|
761
456
|
* (feat-20260630 M3 / w16-w18). The snapshot carries NO projection status
|
|
762
457
|
* field: the status truth lives in the store's CubemapGpuEntry (D-3 SSOT);
|
|
763
458
|
* record queries it once per frame.
|
|
@@ -781,128 +476,13 @@ export interface SkylightSnapshot {
|
|
|
781
476
|
readonly entityHandle: number;
|
|
782
477
|
}
|
|
783
478
|
|
|
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
479
|
/**
|
|
900
480
|
* SkyboxSnapshot -- extract-stage view of one SkyboxBackground entity
|
|
901
481
|
* (feat-20260531-skybox-env-background M2 / w5).
|
|
902
482
|
*
|
|
903
483
|
* `equirectHandle` carries the packed u32 handle for
|
|
904
484
|
* `Handle<EquirectAsset, 'shared'>`; the record stage resolves the projected
|
|
905
|
-
* GPU cubemap via `
|
|
485
|
+
* GPU cubemap via `GpuResidencyCache.getCubemapGpuView(...)`.
|
|
906
486
|
*
|
|
907
487
|
* `mode` carries the raw `f32` column value (`SKYBOX_MODE_CUBEMAP = 0`).
|
|
908
488
|
* First hit wins per plan-strategy D-6; multi-entity once-warn in record stage.
|
|
@@ -921,27 +501,9 @@ export interface SkyboxSnapshot {
|
|
|
921
501
|
export interface RenderableSnapshot {
|
|
922
502
|
readonly assetHandle: number;
|
|
923
503
|
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
504
|
/** Finite producer-owned local bounds retained for persistent CPU view work. */
|
|
934
505
|
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
|
-
*/
|
|
506
|
+
/** Indexed single-submesh facts consumed by the first GPU-driven rigid lane. */
|
|
945
507
|
readonly gpuDrivenDraws?: readonly GpuDrivenDrawSnapshot[];
|
|
946
508
|
/**
|
|
947
509
|
* feat-20260608 M5 amend / w11-a: the entity's representative (first)
|
|
@@ -997,8 +559,6 @@ export interface RenderableSnapshot {
|
|
|
997
559
|
* not the bare entityKey alone (D-1).
|
|
998
560
|
*/
|
|
999
561
|
readonly entityKey: number;
|
|
1000
|
-
/** Stable pre-main-cull candidate index used by the directional CSM plan. */
|
|
1001
|
-
readonly candidateIndex?: number;
|
|
1002
562
|
/**
|
|
1003
563
|
* feat-20260514 M3 (w15): when the entity carries an `Instances` component
|
|
1004
564
|
* the extract stage materialises a fresh `Float32Array` snapshot of the
|
|
@@ -1038,41 +598,22 @@ export interface RenderableSnapshot {
|
|
|
1038
598
|
* Absent (`undefined`) means the entity is not skinned.
|
|
1039
599
|
*/
|
|
1040
600
|
readonly skin?: SkinPaletteSlice;
|
|
1041
|
-
/**
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
601
|
+
/**
|
|
602
|
+
* ECS-owned morph weights. Presence selects the Standard Pipeline CPU
|
|
603
|
+
* specialized deformation lane; the asset remains the source of target
|
|
604
|
+
* deltas and the record stage only receives this frozen POD snapshot.
|
|
605
|
+
*/
|
|
606
|
+
readonly morph?: MorphSnapshot;
|
|
607
|
+
/** Detached M2 authoring facts for the optional Points/Lines projection. */
|
|
608
|
+
readonly pointsLines?: PointsLinesRetainedSnapshot;
|
|
1045
609
|
}
|
|
1046
610
|
|
|
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;
|
|
611
|
+
export interface MorphSnapshot {
|
|
612
|
+
readonly weights: Float32Array;
|
|
613
|
+
readonly targetCount: number;
|
|
1067
614
|
}
|
|
1068
615
|
|
|
1069
616
|
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
617
|
readonly kind: 'indexed' | 'non-indexed';
|
|
1077
618
|
readonly first: number;
|
|
1078
619
|
readonly count: number;
|
|
@@ -1083,41 +624,6 @@ export interface GpuDrivenDrawSnapshot {
|
|
|
1083
624
|
readonly materialResourceClass: string;
|
|
1084
625
|
}
|
|
1085
626
|
|
|
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
627
|
export interface InstancesSnapshot {
|
|
1122
628
|
/** Packed column-major mat4 transforms (16 f32 per instance). */
|
|
1123
629
|
readonly transforms: Float32Array;
|
|
@@ -1177,56 +683,6 @@ export interface TransformSnapshot {
|
|
|
1177
683
|
readonly world: Float32Array;
|
|
1178
684
|
}
|
|
1179
685
|
|
|
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
686
|
/**
|
|
1231
687
|
* MaterialSnapshot: extract-stage view of one entity's material asset (M2 / w6
|
|
1232
688
|
* of feat-20260517-merge-mesh-renderer-material-renderer; plan-strategy section 2.3).
|
|
@@ -1267,16 +723,12 @@ export interface MaterialSnapshot {
|
|
|
1267
723
|
* discriminator (M4-T06).
|
|
1268
724
|
*/
|
|
1269
725
|
readonly materialShaderId?: string | undefined;
|
|
1270
|
-
/** Canonical RenderAssetPort GUID used to join the production readiness tuple. */
|
|
1271
|
-
readonly materialGuid?: string | undefined;
|
|
1272
726
|
/**
|
|
1273
727
|
* Stable source handle for the material asset. Record uses this only for
|
|
1274
728
|
* same-frame assembly reuse; GPU bind-group validity still comes from the
|
|
1275
729
|
* resource-identity cache.
|
|
1276
730
|
*/
|
|
1277
731
|
readonly materialHandle?: number | undefined;
|
|
1278
|
-
/** Whether the resolved material owns a ShadowCaster pass. */
|
|
1279
|
-
readonly shadowCaster?: boolean | undefined;
|
|
1280
732
|
/**
|
|
1281
733
|
* The selected forward-pass state for this exact material. A mesh can bind
|
|
1282
734
|
* different materials to its submeshes, so pipeline selection must read the
|
|
@@ -1292,10 +744,6 @@ export interface MaterialSnapshot {
|
|
|
1292
744
|
readonly paramSnapshot?: Readonly<Record<string, number | number[] | string>> | undefined;
|
|
1293
745
|
/** Effective MaterialAsset parameter contract used to lay out this snapshot. */
|
|
1294
746
|
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
747
|
/** Authored per-slot UV set and KHR texture transform metadata. */
|
|
1300
748
|
readonly textureCoordinates?: ReadonlyMap<string, MaterialTextureCoordinates> | undefined;
|
|
1301
749
|
/**
|
|
@@ -1303,7 +751,7 @@ export interface MaterialSnapshot {
|
|
|
1303
751
|
* (feat-20260621-learn-render-5-5-parallax M2 / w7). The SSOT carrier for
|
|
1304
752
|
* EVERY texture the shader's `derive(paramSchema).textureFieldNames`
|
|
1305
753
|
* declares — `baseColorTexture` / `metallicRoughnessTexture` /
|
|
1306
|
-
* `normalTexture` for built-in standard-PBR, plus
|
|
754
|
+
* `normalTexture` for built-in standard-PBR, plus each custom field such as
|
|
1307
755
|
* `heightTexture` (LO 5.5 parallax). The record stage iterates this map to
|
|
1308
756
|
* assemble the user-region bind group per the per-shader BGL (w8), so a 4th
|
|
1309
757
|
* (or Nth) texture flows end-to-end without a hardcoded field list.
|
|
@@ -1322,7 +770,7 @@ export interface MaterialSnapshot {
|
|
|
1322
770
|
* occupies the same texture2d values slot a static texture would (P4:
|
|
1323
771
|
* one binding shape), but extract routes it here instead of `textureHandles`
|
|
1324
772
|
* so the record stage pulls the current-frame view from the transient
|
|
1325
|
-
* DynamicTextureStore (D-3) instead of `
|
|
773
|
+
* DynamicTextureStore (D-3) instead of `GpuResidencyCache.ensureResident`
|
|
1326
774
|
* (which has no `video` arm; AC-08). Each entry also carries the resolved
|
|
1327
775
|
* clip handle so the record stage can key the per-frame upload.
|
|
1328
776
|
*
|
|
@@ -1403,7 +851,7 @@ export interface MaterialSnapshotCacheEntry {
|
|
|
1403
851
|
readonly passes: readonly MaterialPass[];
|
|
1404
852
|
/** Root/chain identities are present only on cross-frame-safe entries. */
|
|
1405
853
|
readonly chain?: readonly MaterialCacheChainIdentity[];
|
|
1406
|
-
/**
|
|
854
|
+
/** AssetRegistry epoch at which the root/parent chain was last validated. */
|
|
1407
855
|
readonly catalogEpoch?: number;
|
|
1408
856
|
/** True only when every object reachable from the authored chain is frozen. */
|
|
1409
857
|
readonly crossFrameSafe: boolean;
|
|
@@ -1426,44 +874,11 @@ function gpuDrivenMaterialResourceClass(material: MaterialSnapshot): string {
|
|
|
1426
874
|
});
|
|
1427
875
|
}
|
|
1428
876
|
|
|
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
877
|
// === DispatchEntry — M3 / w26 single dispatch list (feat-20260526-material-asset-multipass-renderstate) ===
|
|
1463
878
|
//
|
|
1464
879
|
// Plan-strategy D-3: single dispatch list sorted by queue value,
|
|
1465
880
|
// replacing the old three-bucket opaque/transparent/overlay dispatch.
|
|
1466
|
-
// Each entry carries the per-pass render-state
|
|
881
|
+
// Each entry carries the per-pass render-state and entry-point
|
|
1467
882
|
// data from the resolved MaterialPass so the record stage
|
|
1468
883
|
// reads them without re-resolving the material.
|
|
1469
884
|
|
|
@@ -1507,6 +922,12 @@ export function sortDispatchByQueue<E extends { readonly queue: number }>(
|
|
|
1507
922
|
return entries.slice().sort((a, b) => a.queue - b.queue);
|
|
1508
923
|
}
|
|
1509
924
|
|
|
925
|
+
const DEFAULT_FORWARD_PASS: MaterialPass = {
|
|
926
|
+
name: 'forward',
|
|
927
|
+
program: { module: 'forgeax::default-unlit', vertexEntry: 'vs_main', fragmentEntry: 'fs_main' },
|
|
928
|
+
renderState: { tags: { LightMode: 'Forward' }, queue: 2000 },
|
|
929
|
+
};
|
|
930
|
+
|
|
1510
931
|
function appendMaterialDispatchEntries(
|
|
1511
932
|
pendingDispatch: DispatchEntry[],
|
|
1512
933
|
passes: readonly MaterialPass[],
|
|
@@ -1515,6 +936,7 @@ function appendMaterialDispatchEntries(
|
|
|
1515
936
|
renderableIndex: number,
|
|
1516
937
|
layer: number,
|
|
1517
938
|
paramSnapshot: Readonly<Record<string, number | number[] | string>> | undefined,
|
|
939
|
+
passIndexOffset = 0,
|
|
1518
940
|
): void {
|
|
1519
941
|
const matchedPasses = selectPasses(passes, {});
|
|
1520
942
|
for (let pIdx = 0; pIdx < matchedPasses.length; pIdx++) {
|
|
@@ -1529,12 +951,14 @@ function appendMaterialDispatchEntries(
|
|
|
1529
951
|
entityIndex: entity,
|
|
1530
952
|
materialHandle,
|
|
1531
953
|
renderableIndex,
|
|
1532
|
-
passIndex: pIdx,
|
|
1533
|
-
queue: passState.queue ??
|
|
954
|
+
passIndex: passIndexOffset + pIdx,
|
|
955
|
+
queue: passState.queue ?? 2000,
|
|
1534
956
|
layer,
|
|
1535
957
|
tags: passState.tags ?? {},
|
|
1536
958
|
renderState: pipelineRenderState(passState),
|
|
1537
|
-
|
|
959
|
+
// Material module identity is already closed in the cooked program.
|
|
960
|
+
// It must never become a draw-time define map.
|
|
961
|
+
defines: undefined,
|
|
1538
962
|
vertexEntry: pass.program.vertexEntry,
|
|
1539
963
|
fragmentEntry: pass.program.fragmentEntry,
|
|
1540
964
|
materialShaderId: runtimeMaterialShaderId(pass.program.module, pass.name),
|
|
@@ -1550,23 +974,16 @@ export interface ExtractedFrame {
|
|
|
1550
974
|
readonly cameras: CameraSnapshot[];
|
|
1551
975
|
readonly lights: ExtractedLights;
|
|
1552
976
|
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
977
|
/**
|
|
1564
978
|
* Single dispatch list sorted by queue value (ascending, stable sort).
|
|
1565
979
|
* Replaces the old opaqueDispatch / transparentDispatch / overlayDispatch
|
|
1566
980
|
* three-bucket model per plan-strategy D-3.
|
|
1567
981
|
*/
|
|
1568
982
|
readonly dispatch: DispatchEntry[];
|
|
1569
|
-
readonly
|
|
983
|
+
readonly skylight: SkylightSnapshot | undefined;
|
|
984
|
+
readonly skylightCount: number;
|
|
985
|
+
readonly skybox: SkyboxSnapshot | undefined;
|
|
986
|
+
readonly skyboxCount: number;
|
|
1570
987
|
/**
|
|
1571
988
|
* feat-20260528-frustum-culling M3 / w11: frustum culling statistics
|
|
1572
989
|
* collected during the extract phase. `total` is the count of entities
|
|
@@ -1593,53 +1010,82 @@ export interface ExtractedFrame {
|
|
|
1593
1010
|
}
|
|
1594
1011
|
|
|
1595
1012
|
/**
|
|
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).
|
|
1013
|
+
* Internal world surface used by extract for archetype-graph traversal and
|
|
1014
|
+
* error routing. The packed `Entity` handle for a row is read directly from
|
|
1015
|
+
* the essential id=0 `Entity` column (`arch.columns.get(Entity.id).get('self')`)
|
|
1016
|
+
* -- no generation lookup / encodeEntity rebuild (feat-20260602 M2).
|
|
1616
1017
|
*/
|
|
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
|
-
|
|
1018
|
+
type RenderErrorContext = {
|
|
1019
|
+
readonly systemName: string;
|
|
1020
|
+
/** Retained as diagnostic metadata for legacy render call sites. */
|
|
1021
|
+
readonly severity?: 'error' | 'warning';
|
|
1022
|
+
};
|
|
1023
|
+
|
|
1024
|
+
type ArrayFieldView = ArrayLike<number>;
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* Render's old zero-copy ECS calls now use the explicit public projection
|
|
1028
|
+
* boundary. The optimized ECS owner keeps storage behind `World`; render
|
|
1029
|
+
* receives a detached array snapshot and routes failures through the same
|
|
1030
|
+
* projection owner.
|
|
1031
|
+
*/
|
|
1032
|
+
type WorldInternalView = {
|
|
1033
|
+
_routeError(error: unknown, ctx: RenderErrorContext): void;
|
|
1034
|
+
_getArrayView<N extends string, S extends ComponentSchema>(
|
|
1035
|
+
entity: EntityHandle,
|
|
1036
|
+
component: Component<N, S>,
|
|
1037
|
+
fieldName: string,
|
|
1038
|
+
): ArrayFieldView | undefined;
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
const Severity = Object.freeze({ Error: 'error', Warning: 'warning' } as const);
|
|
1042
|
+
|
|
1043
|
+
function createWorldInternalView(world: World): WorldInternalView {
|
|
1044
|
+
return {
|
|
1045
|
+
_routeError(error, ctx) {
|
|
1046
|
+
routeWorldError(world, error, { systemName: ctx.systemName });
|
|
1047
|
+
},
|
|
1048
|
+
_getArrayView(entity, component, fieldName) {
|
|
1049
|
+
return readRenderArrayView(world, entity, component, fieldName) as ArrayFieldView | undefined;
|
|
1050
|
+
},
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* The user-region texture fields the built-in standard-PBR material declares.
|
|
1056
|
+
* Used as the fallback texture-field set when the shader id is not registered
|
|
1057
|
+
* (cross-worktree shader-late-register, plan R-4) so a built-in material still
|
|
1058
|
+
* resolves its user-region textures. Mirrors `derive(default-standard-pbr).textureFieldNames`.
|
|
1059
|
+
*/
|
|
1060
|
+
const BUILTIN_USER_REGION_TEXTURE_FIELDS: readonly string[] = [
|
|
1061
|
+
'baseColorTexture',
|
|
1062
|
+
'metallicRoughnessTexture',
|
|
1063
|
+
'normalTexture',
|
|
1064
|
+
'specularTintTexture',
|
|
1065
|
+
];
|
|
1066
|
+
const BUILTIN_USER_REGION_TEXTURE_FIELD_SET = new Set(BUILTIN_USER_REGION_TEXTURE_FIELDS);
|
|
1067
|
+
const BUILTIN_BASE_COLOR_TEXTURE_FIELD_SET = new Set(['baseColorTexture']);
|
|
1068
|
+
|
|
1069
|
+
function materialTextureFields(
|
|
1070
|
+
shaderId: string | undefined,
|
|
1071
|
+
fields: ReadonlySet<string> | undefined,
|
|
1072
|
+
): ReadonlySet<string> | undefined {
|
|
1073
|
+
if (fields !== undefined && fields.size > 0) return fields;
|
|
1074
|
+
if (
|
|
1075
|
+
shaderId === 'forgeax::default-standard-pbr' ||
|
|
1076
|
+
shaderId === 'forgeax::default-standard-pbr-skin' ||
|
|
1077
|
+
shaderId === 'forgeax::pbr-skin'
|
|
1078
|
+
) {
|
|
1079
|
+
return BUILTIN_USER_REGION_TEXTURE_FIELD_SET;
|
|
1080
|
+
}
|
|
1081
|
+
if (
|
|
1082
|
+
shaderId === 'forgeax::default-unlit' ||
|
|
1083
|
+
shaderId === 'forgeax::sprite' ||
|
|
1084
|
+
shaderId === 'forgeax::sprite-lit'
|
|
1085
|
+
) {
|
|
1086
|
+
return BUILTIN_BASE_COLOR_TEXTURE_FIELD_SET;
|
|
1087
|
+
}
|
|
1088
|
+
return fields;
|
|
1643
1089
|
}
|
|
1644
1090
|
|
|
1645
1091
|
/**
|
|
@@ -1724,12 +1170,12 @@ function pipelineRenderState(
|
|
|
1724
1170
|
|
|
1725
1171
|
/**
|
|
1726
1172
|
* tweak-20260627-model-loading-smoke-build-perf M4: per-World intern cache for
|
|
1727
|
-
* the
|
|
1173
|
+
* the loadByGuid texture/sampler resolution path. A MaterialAsset's
|
|
1728
1174
|
* texture/sampler values remain embedded GUID strings (dash-form) after
|
|
1729
|
-
*
|
|
1175
|
+
* loadByGuid; the extract stage re-resolves each GUID to a column handle every
|
|
1730
1176
|
* frame. Before this cache each resolution called `world.allocSharedRef`, which
|
|
1731
1177
|
* mints a NEW monotonically-increasing slot id per call. Because the GPU
|
|
1732
|
-
* residency cache (`
|
|
1178
|
+
* residency cache (`GpuResidencyCache.textureGpuHandles`) is keyed on
|
|
1733
1179
|
* `handleSlot(handle)`, a fresh slot every frame meant the residency check
|
|
1734
1180
|
* ALWAYS missed -> all textures re-uploaded to the GPU every frame, old GPU
|
|
1735
1181
|
* textures never freed (refcount never hits 0). Unbounded GPU memory +
|
|
@@ -1740,11 +1186,10 @@ function pipelineRenderState(
|
|
|
1740
1186
|
* across frames (architecture-principle §6 idempotency: same GUID resolved N
|
|
1741
1187
|
* times yields the same handle). The handle is intentionally long-lived -- it
|
|
1742
1188
|
* lives as long as the World references the material, which is exactly the
|
|
1743
|
-
* desired lifetime
|
|
1744
|
-
*
|
|
1745
|
-
* owns GPU disposal.
|
|
1189
|
+
* desired lifetime; the `onLastRelease` -> `gpuStore.evictTexture` wiring stays
|
|
1190
|
+
* coherent because the handle is no longer churned per frame.
|
|
1746
1191
|
*
|
|
1747
|
-
* Invariant boundary (asset-registry.ts:1958-1962): the
|
|
1192
|
+
* Invariant boundary (asset-registry.ts:1958-1962): the AssetRegistry is a
|
|
1748
1193
|
* GUID -> payload catalogue with NO handle/World concept -- it cannot mint a
|
|
1749
1194
|
* column handle. So this intern cache lives in the extract/render layer, keyed
|
|
1750
1195
|
* per-World via a WeakMap (the World owns the SharedRefStore that mints slots).
|
|
@@ -1767,7 +1212,7 @@ const guidHandleInternByWorld = new WeakMap<World, Map<string, GuidHandleInternE
|
|
|
1767
1212
|
|
|
1768
1213
|
function internSharedRefFromGuid<B extends string>(
|
|
1769
1214
|
world: World,
|
|
1770
|
-
assetsRef:
|
|
1215
|
+
assetsRef: AssetRegistry,
|
|
1771
1216
|
guid: string,
|
|
1772
1217
|
brand: B,
|
|
1773
1218
|
): Handle<B, 'shared'> | undefined {
|
|
@@ -1780,19 +1225,19 @@ function internSharedRefFromGuid<B extends string>(
|
|
|
1780
1225
|
const cached = perWorld.get(key);
|
|
1781
1226
|
const payload = assetsRef.lookup(guid);
|
|
1782
1227
|
if (cached !== undefined && cached.payload === payload) {
|
|
1783
|
-
const cachedHandle = cached.handle
|
|
1228
|
+
const cachedHandle = toShared<B>(cached.handle);
|
|
1784
1229
|
if (world.sharedRefs.resolve(cachedHandle).ok) return cachedHandle;
|
|
1785
1230
|
}
|
|
1786
1231
|
if (cached !== undefined) {
|
|
1787
|
-
world.sharedRefs.release(cached.handle
|
|
1232
|
+
world.sharedRefs.release(toShared<B>(cached.handle));
|
|
1788
1233
|
perWorld.delete(key);
|
|
1789
1234
|
}
|
|
1790
1235
|
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
|
|
1236
|
+
// Mint exactly once per (world, guid, brand). ECS owns the allocation grant
|
|
1237
|
+
// and publishes release evidence through SharedRefStore; render does not
|
|
1238
|
+
// attach a second deleter lifecycle to the World.
|
|
1239
|
+
const handle = world.internSharedRef(brand, payload);
|
|
1240
|
+
perWorld.set(key, { handle, payload });
|
|
1796
1241
|
return handle;
|
|
1797
1242
|
}
|
|
1798
1243
|
|
|
@@ -1817,7 +1262,7 @@ function internSharedRefFromGuid<B extends string>(
|
|
|
1817
1262
|
function resolveVideoFieldHandle(
|
|
1818
1263
|
value: unknown,
|
|
1819
1264
|
world: World,
|
|
1820
|
-
assetsRef:
|
|
1265
|
+
assetsRef: AssetRegistry,
|
|
1821
1266
|
): Handle<'VideoAsset', 'shared'> | undefined {
|
|
1822
1267
|
const texture = materialTextureValue(value);
|
|
1823
1268
|
const textureGuid = assetReferenceText(texture?.texture ?? value);
|
|
@@ -1843,11 +1288,14 @@ function materialTextureValue(value: unknown): MaterialTextureValue | undefined
|
|
|
1843
1288
|
|
|
1844
1289
|
function assetReferenceText(value: unknown): string | undefined {
|
|
1845
1290
|
if (typeof value === 'string') return value;
|
|
1846
|
-
if (value instanceof Uint8Array && value
|
|
1847
|
-
return AssetGuid.format(value as never);
|
|
1291
|
+
if (value instanceof Uint8Array && isAssetGuidBytes(value)) return AssetGuid.format(value);
|
|
1848
1292
|
return undefined;
|
|
1849
1293
|
}
|
|
1850
1294
|
|
|
1295
|
+
function isAssetGuidBytes(value: Uint8Array): value is AssetGuidBytes {
|
|
1296
|
+
return value.byteLength === 16;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1851
1299
|
function materialNormalScale(values: Readonly<Record<string, unknown>>): number {
|
|
1852
1300
|
for (const [field, value] of Object.entries(values)) {
|
|
1853
1301
|
if (field !== 'normalTexture') continue;
|
|
@@ -1863,14 +1311,16 @@ function materialTextureRef(value: unknown): unknown {
|
|
|
1863
1311
|
|
|
1864
1312
|
function collectMaterialTextureCoordinates(
|
|
1865
1313
|
values: Readonly<Record<string, unknown>>,
|
|
1866
|
-
schema: readonly ParamSchemaEntry[],
|
|
1867
|
-
derivedInterface?: DerivedMaterialInterface,
|
|
1868
|
-
paramSchemaProjectionCensus?: ParamSchemaProjectionCensus,
|
|
1869
1314
|
): Map<string, MaterialTextureCoordinates> {
|
|
1870
1315
|
const out = new Map<string, MaterialTextureCoordinates>();
|
|
1871
|
-
for (const field of
|
|
1872
|
-
|
|
1873
|
-
|
|
1316
|
+
for (const field of [
|
|
1317
|
+
'baseColorTexture',
|
|
1318
|
+
'metallicRoughnessTexture',
|
|
1319
|
+
'normalTexture',
|
|
1320
|
+
'specularTintTexture',
|
|
1321
|
+
'emissiveTexture',
|
|
1322
|
+
'occlusionTexture',
|
|
1323
|
+
]) {
|
|
1874
1324
|
const coordinates = materialTextureValue(values[field])?.coordinates;
|
|
1875
1325
|
if (coordinates !== undefined) out.set(field, coordinates);
|
|
1876
1326
|
}
|
|
@@ -1897,14 +1347,14 @@ function collectMaterialTextureSamplers(
|
|
|
1897
1347
|
* feat-20260621-learn-render-5-5-parallax M2 / w7 (D-3): collect the
|
|
1898
1348
|
* user-region texture handles for a material by iterating the shader's
|
|
1899
1349
|
* `derive(paramSchema).textureFieldNames` SSOT (via
|
|
1900
|
-
* `
|
|
1350
|
+
* `AssetRegistry.materialShaderTextureFieldNames`). Each declared texture
|
|
1901
1351
|
* field whose paramValue resolves to a handle lands in the returned map keyed
|
|
1902
|
-
* by field name; this is the single path through which
|
|
1352
|
+
* by field name; this is the single path through which an arbitrary number of user-region
|
|
1903
1353
|
* textures (4 standard or 5+ custom, e.g. parallax `heightTexture`) flow.
|
|
1904
1354
|
*
|
|
1905
|
-
* When the
|
|
1906
|
-
* `emissiveTexture` / `occlusionTexture`
|
|
1907
|
-
* textures and are NOT part of this set.
|
|
1355
|
+
* When the shader is not registered the built-in 4-field set is used so
|
|
1356
|
+
* standard materials still resolve. `emissiveTexture` / `occlusionTexture`
|
|
1357
|
+
* are engine-injection (lightmap) textures and are NOT part of this set.
|
|
1908
1358
|
*
|
|
1909
1359
|
* feat-20260623-world-space-video-asset M4 / w14 (D-5): a field whose paramValue
|
|
1910
1360
|
* catalogues to a VideoAsset is routed into `videoOut` (a VideoAsset-branded
|
|
@@ -1918,8 +1368,7 @@ function collectUserRegionTextureHandles(
|
|
|
1918
1368
|
pv: Readonly<Record<string, unknown>>,
|
|
1919
1369
|
shaderId: string | undefined,
|
|
1920
1370
|
paramSchema: readonly ParamSchemaEntry[] | undefined,
|
|
1921
|
-
|
|
1922
|
-
assetsRef: RenderAssetPort,
|
|
1371
|
+
assetsRef: AssetRegistry,
|
|
1923
1372
|
world: World,
|
|
1924
1373
|
resolveTex: (
|
|
1925
1374
|
value: unknown,
|
|
@@ -1928,38 +1377,16 @@ function collectUserRegionTextureHandles(
|
|
|
1928
1377
|
videoOut: Map<string, Handle<'VideoAsset', 'shared'>>,
|
|
1929
1378
|
): Map<string, Handle<'TextureAsset', 'shared'>> {
|
|
1930
1379
|
const fields =
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1380
|
+
materialTextureFields(
|
|
1381
|
+
shaderId,
|
|
1382
|
+
paramSchema !== undefined
|
|
1934
1383
|
? 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;
|
|
1384
|
+
: shaderId !== undefined
|
|
1385
|
+
? assetsRef.materialShaderTextureFieldNames(shaderId)
|
|
1386
|
+
: undefined,
|
|
1387
|
+
) ?? BUILTIN_USER_REGION_TEXTURE_FIELDS;
|
|
1961
1388
|
const out = new Map<string, Handle<'TextureAsset', 'shared'>>();
|
|
1962
|
-
for (const field of fields
|
|
1389
|
+
for (const field of fields) {
|
|
1963
1390
|
// D-5: a video-kind paramValue is routed to the transient path (videoOut),
|
|
1964
1391
|
// NOT minted as a TextureAsset (which would crash the record-stage
|
|
1965
1392
|
// ensureResident, AC-08). A static field falls through to resolveTex.
|
|
@@ -1974,34 +1401,31 @@ function collectUserRegionTextureHandles(
|
|
|
1974
1401
|
return out;
|
|
1975
1402
|
}
|
|
1976
1403
|
|
|
1404
|
+
const ENGINE_INJECTED_TEXTURE_FIELDS = new Set(['emissiveTexture', 'occlusionTexture']);
|
|
1405
|
+
|
|
1406
|
+
function isEngineInjectedTextureField(shaderId: string | undefined, fieldName: string): boolean {
|
|
1407
|
+
return isStandardPbrMaterialShader(shaderId) && ENGINE_INJECTED_TEXTURE_FIELDS.has(fieldName);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1977
1410
|
function materialParametersToParamSchema(
|
|
1978
1411
|
parameters: readonly MaterialParameter[],
|
|
1979
1412
|
shaderId: string | undefined,
|
|
1980
|
-
assets: RenderAssetPort,
|
|
1981
1413
|
): readonly ParamSchemaEntry[] {
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
}
|
|
1414
|
+
const engineInjectedTextureFields = isStandardPbrMaterialShader(shaderId)
|
|
1415
|
+
? ENGINE_INJECTED_TEXTURE_FIELDS
|
|
1416
|
+
: undefined;
|
|
1986
1417
|
return parameters.flatMap((parameter): ParamSchemaEntry[] => {
|
|
1987
|
-
//
|
|
1988
|
-
|
|
1989
|
-
if (parameter.static || parameter.type === 'bool') return [];
|
|
1418
|
+
// Boolean material values are runtime-only and have no UBO representation.
|
|
1419
|
+
if (parameter.type === 'bool') return [];
|
|
1990
1420
|
if (parameter.type === 'texture') {
|
|
1421
|
+
if (engineInjectedTextureFields?.has(parameter.name) === true) return [];
|
|
1991
1422
|
return [{ name: parameter.name, type: 'texture2d' }];
|
|
1992
1423
|
}
|
|
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
1424
|
return [
|
|
2000
1425
|
{
|
|
2001
1426
|
name: parameter.name,
|
|
2002
1427
|
type: parameter.type,
|
|
2003
1428
|
...(parameter.colorSpace === undefined ? {} : { colorSpace: parameter.colorSpace }),
|
|
2004
|
-
...numericDefault,
|
|
2005
1429
|
},
|
|
2006
1430
|
];
|
|
2007
1431
|
});
|
|
@@ -2016,7 +1440,7 @@ function materialParametersToParamSchema(
|
|
|
2016
1440
|
function materialColorParameterSchema(
|
|
2017
1441
|
parameters: readonly MaterialParameter[],
|
|
2018
1442
|
shaderId: string | undefined,
|
|
2019
|
-
assets:
|
|
1443
|
+
assets: AssetRegistry,
|
|
2020
1444
|
): readonly MaterialColorParameterSchema[] {
|
|
2021
1445
|
if (shaderId === undefined) return parameters;
|
|
2022
1446
|
const shader = assets.shaderRegistry.findMaterialArtifact(shaderId);
|
|
@@ -2044,13 +1468,13 @@ function isDeepFrozen(value: unknown, seen = new Set<object>()): boolean {
|
|
|
2044
1468
|
|
|
2045
1469
|
/**
|
|
2046
1470
|
* Capture the object identities that make a resolved material stable. The
|
|
2047
|
-
* renderer owns this cache, but the
|
|
1471
|
+
* renderer owns this cache, but the AssetRegistry owns parent GUID lookup, so
|
|
2048
1472
|
* validation must walk the same chain on every cross-frame read. A malformed
|
|
2049
1473
|
* or cyclic chain is deliberately not cacheable.
|
|
2050
1474
|
*/
|
|
2051
1475
|
function captureMaterialCacheChain(
|
|
2052
1476
|
root: MaterialAsset,
|
|
2053
|
-
assets:
|
|
1477
|
+
assets: AssetRegistry,
|
|
2054
1478
|
): readonly MaterialCacheChainIdentity[] | undefined {
|
|
2055
1479
|
const chain: MaterialCacheChainIdentity[] = [];
|
|
2056
1480
|
const visitedParentGuids = new Set<string>();
|
|
@@ -2063,7 +1487,7 @@ function captureMaterialCacheChain(
|
|
|
2063
1487
|
if (parentRef === undefined) return undefined;
|
|
2064
1488
|
if (visitedParentGuids.has(parentGuid)) return undefined;
|
|
2065
1489
|
visitedParentGuids.add(parentGuid);
|
|
2066
|
-
const parent = assets.lookup<Asset>(
|
|
1490
|
+
const parent = assets.lookup<Asset>(parentRef);
|
|
2067
1491
|
if (parent?.kind !== 'material') return undefined;
|
|
2068
1492
|
current = parent;
|
|
2069
1493
|
}
|
|
@@ -2073,11 +1497,15 @@ function captureMaterialCacheChain(
|
|
|
2073
1497
|
function readPersistentMaterialSnapshot(
|
|
2074
1498
|
cache: MaterialSnapshotCache | undefined,
|
|
2075
1499
|
handleRaw: number,
|
|
2076
|
-
assets:
|
|
1500
|
+
assets: AssetRegistry,
|
|
2077
1501
|
): MaterialSnapshotCacheEntry | undefined {
|
|
2078
1502
|
const entry = cache?.get(handleRaw);
|
|
2079
1503
|
if (entry === undefined) return undefined;
|
|
2080
|
-
if (
|
|
1504
|
+
if (
|
|
1505
|
+
entry.crossFrameSafe &&
|
|
1506
|
+
entry.chain !== undefined &&
|
|
1507
|
+
entry.catalogEpoch === assets.catalogEpoch
|
|
1508
|
+
)
|
|
2081
1509
|
return entry;
|
|
2082
1510
|
// The snapshot contains resolved texture and sampler handles as well as the
|
|
2083
1511
|
// material parent chain. Any registry mutation can replace one of those
|
|
@@ -2096,12 +1524,12 @@ function readPersistentMaterialSnapshot(
|
|
|
2096
1524
|
function readStablePersistentMaterialSnapshot(
|
|
2097
1525
|
cache: MaterialSnapshotCache | undefined,
|
|
2098
1526
|
handleRaw: number,
|
|
2099
|
-
assets:
|
|
1527
|
+
assets: AssetRegistry,
|
|
2100
1528
|
): MaterialSnapshotCacheEntry | undefined {
|
|
2101
1529
|
const entry = cache?.get(handleRaw);
|
|
2102
1530
|
return entry?.crossFrameSafe === true &&
|
|
2103
1531
|
entry.chain !== undefined &&
|
|
2104
|
-
entry.catalogEpoch === assets.
|
|
1532
|
+
entry.catalogEpoch === assets.catalogEpoch
|
|
2105
1533
|
? entry
|
|
2106
1534
|
: undefined;
|
|
2107
1535
|
}
|
|
@@ -2112,7 +1540,7 @@ function storeMaterialSnapshot(
|
|
|
2112
1540
|
snapshot: MaterialSnapshot,
|
|
2113
1541
|
passes: readonly MaterialPass[],
|
|
2114
1542
|
root: MaterialAsset,
|
|
2115
|
-
assets:
|
|
1543
|
+
assets: AssetRegistry,
|
|
2116
1544
|
): MaterialSnapshotCacheEntry {
|
|
2117
1545
|
const chain = captureMaterialCacheChain(root, assets);
|
|
2118
1546
|
const entry: MaterialSnapshotCacheEntry = {
|
|
@@ -2120,7 +1548,7 @@ function storeMaterialSnapshot(
|
|
|
2120
1548
|
passes,
|
|
2121
1549
|
crossFrameSafe: chain?.every(({ asset }) => isDeepFrozen(asset)) === true,
|
|
2122
1550
|
...(chain === undefined ? {} : { chain }),
|
|
2123
|
-
...(chain === undefined ? {} : { catalogEpoch: assets.
|
|
1551
|
+
...(chain === undefined ? {} : { catalogEpoch: assets.catalogEpoch }),
|
|
2124
1552
|
};
|
|
2125
1553
|
cache.set(handleRaw, entry);
|
|
2126
1554
|
return entry;
|
|
@@ -2136,124 +1564,211 @@ function storeMaterialSnapshot(
|
|
|
2136
1564
|
* UBO upload, so this helper produces a plain MaterialSnapshot only).
|
|
2137
1565
|
*
|
|
2138
1566
|
* Returns `defaultMaterialSnapshot()` (mid-grey unlit) for handle=0 (case-B
|
|
2139
|
-
* sentinel, mirroring the inline path) and on
|
|
1567
|
+
* sentinel, mirroring the inline path) and on each unresolved / non-material
|
|
2140
1568
|
* asset (so a partially-mis-registered multi-material entity still renders
|
|
2141
1569
|
* the resolvable submeshes; the count-mismatch validator already filtered
|
|
2142
1570
|
* the count-disagreement case earlier).
|
|
2143
1571
|
*/
|
|
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,
|
|
1572
|
+
export function resolveMaterialSnapshot(
|
|
1573
|
+
handleRaw: number,
|
|
2191
1574
|
world: World,
|
|
2192
|
-
|
|
2193
|
-
|
|
1575
|
+
assetsRef: AssetRegistry,
|
|
1576
|
+
materialSnapshotCache?: MaterialSnapshotCache,
|
|
2194
1577
|
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(),
|
|
1578
|
+
): MaterialSnapshot {
|
|
1579
|
+
if (handleRaw === 0) return defaultMaterialSnapshot(handleRaw);
|
|
1580
|
+
const cached = materialSnapshotCache?.get(handleRaw);
|
|
1581
|
+
if (cached !== undefined) return cached.snapshot;
|
|
1582
|
+
const stablePersistentCached = readStablePersistentMaterialSnapshot(
|
|
2209
1583
|
persistentMaterialSnapshotCache,
|
|
2210
|
-
|
|
1584
|
+
handleRaw,
|
|
1585
|
+
assetsRef,
|
|
2211
1586
|
);
|
|
2212
|
-
|
|
2213
|
-
(
|
|
1587
|
+
if (stablePersistentCached !== undefined) {
|
|
1588
|
+
materialSnapshotCache?.set(handleRaw, stablePersistentCached);
|
|
1589
|
+
return stablePersistentCached.snapshot;
|
|
1590
|
+
}
|
|
1591
|
+
const tagged = toShared<'MaterialAsset'>(handleRaw);
|
|
1592
|
+
const res = resolveAssetHandle(world, tagged);
|
|
1593
|
+
if (!res.ok) return defaultMaterialSnapshot(handleRaw);
|
|
1594
|
+
const asset = res.value;
|
|
1595
|
+
if (asset.kind !== 'material') return defaultMaterialSnapshot(handleRaw);
|
|
1596
|
+
const persistentCached = readPersistentMaterialSnapshot(
|
|
1597
|
+
persistentMaterialSnapshotCache,
|
|
1598
|
+
handleRaw,
|
|
1599
|
+
assetsRef,
|
|
2214
1600
|
);
|
|
2215
|
-
if (
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
1601
|
+
if (persistentCached !== undefined) {
|
|
1602
|
+
materialSnapshotCache?.set(handleRaw, persistentCached);
|
|
1603
|
+
return persistentCached.snapshot;
|
|
1604
|
+
}
|
|
1605
|
+
const resolvedResult = walkMaterialPassesOverSharedRefs(world, tagged, assetsRef);
|
|
1606
|
+
if (!resolvedResult.ok) return defaultMaterialSnapshot(handleRaw);
|
|
1607
|
+
const resolved = resolvedResult.value;
|
|
1608
|
+
const allPasses = resolved.passes;
|
|
1609
|
+
const firstPassShader =
|
|
1610
|
+
allPasses.length > 0
|
|
1611
|
+
? runtimeMaterialShaderId(allPasses[0]?.program.module, allPasses[0]?.name)
|
|
1612
|
+
: undefined;
|
|
1613
|
+
const pv = materialValuesToLinearRuntime(
|
|
1614
|
+
resolved.values,
|
|
1615
|
+
materialColorParameterSchema(resolved.parameters ?? [], firstPassShader, assetsRef),
|
|
1616
|
+
resolved.colorSpace,
|
|
1617
|
+
) as Readonly<Record<string, unknown>>;
|
|
1618
|
+
const baseColorPv = pv.baseColor as readonly number[] | undefined;
|
|
1619
|
+
const baseColor = vec3.create(
|
|
1620
|
+
baseColorPv?.[0] ?? 1,
|
|
1621
|
+
baseColorPv?.[1] ?? 1,
|
|
1622
|
+
baseColorPv?.[2] ?? 1,
|
|
1623
|
+
);
|
|
1624
|
+
const metallicPv = typeof pv.metallic === 'number' ? pv.metallic : 0;
|
|
1625
|
+
const roughnessPv = typeof pv.roughness === 'number' ? pv.roughness : 0.5;
|
|
1626
|
+
const clearcoatPv = typeof pv.clearcoat === 'number' ? pv.clearcoat : 0;
|
|
1627
|
+
const clearcoatRoughnessPv =
|
|
1628
|
+
typeof pv.clearcoatRoughness === 'number' ? pv.clearcoatRoughness : 0.5;
|
|
1629
|
+
const specularTintPv = pv.specularTint as readonly number[] | undefined;
|
|
1630
|
+
const normalScalePv = materialNormalScale(pv);
|
|
1631
|
+
|
|
1632
|
+
const paramSnap: Record<string, number | number[] | string> = {};
|
|
1633
|
+
for (const [k, v] of Object.entries(pv)) {
|
|
1634
|
+
if (typeof v === 'number') paramSnap[k] = v;
|
|
1635
|
+
else if (typeof v === 'string') paramSnap[k] = v;
|
|
1636
|
+
else if (Array.isArray(v) && v.every((x) => typeof x === 'number')) {
|
|
1637
|
+
paramSnap[k] = v as number[];
|
|
2227
1638
|
}
|
|
2228
1639
|
}
|
|
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
|
-
}),
|
|
1640
|
+
const materialParamSchema = materialParametersToParamSchema(
|
|
1641
|
+
resolved.parameters ?? [],
|
|
1642
|
+
firstPassShader,
|
|
1643
|
+
);
|
|
1644
|
+
// feat-20260614 M8 (D-19): texture / sampler values are embedded GUIDs
|
|
1645
|
+
// (dash-form strings) after loadByGuid. Resolve each to a user-tier column
|
|
1646
|
+
// handle by looking up the catalogued payload and minting via
|
|
1647
|
+
// world.allocSharedRef; a numeric value (already a column handle from a
|
|
1648
|
+
// directly-minted material) passes through unchanged.
|
|
1649
|
+
const resolveTexLike = <B extends string>(
|
|
1650
|
+
value: unknown,
|
|
1651
|
+
brand: B,
|
|
1652
|
+
): Handle<B, 'shared'> | undefined => {
|
|
1653
|
+
if (typeof value === 'number') return toShared<B>(value);
|
|
1654
|
+
if (typeof value === 'string') {
|
|
1655
|
+
// M4: intern the GUID -> column-handle resolution so each unique
|
|
1656
|
+
// (world, guid, brand) mints exactly ONE stable handle reused across
|
|
1657
|
+
// frames (stops the per-frame slot churn that defeated the GPU
|
|
1658
|
+
// residency cache). feat-20260619 M2 / w8: TextureAsset brand wires
|
|
1659
|
+
// onLastRelease -> gpuStore.evictTexture; other brands (SamplerAsset)
|
|
1660
|
+
// bypass (releaseUnreferenced-fallback lifecycle).
|
|
1661
|
+
return internSharedRefFromGuid(world, assetsRef, value, brand);
|
|
1662
|
+
}
|
|
1663
|
+
return undefined;
|
|
2256
1664
|
};
|
|
1665
|
+
// feat-20260621-learn-render-5-5-parallax M2 / w7 (D-3): iterate the
|
|
1666
|
+
// shader's derive(paramSchema).textureFieldNames SSOT instead of a hardcoded
|
|
1667
|
+
// user-region field list, so an Nth texture (e.g. parallax heightTexture)
|
|
1668
|
+
// resolves through the same path. emissive/occlusion are engine-injection
|
|
1669
|
+
// (lightmap) textures, NOT in textureFieldNames, so they keep named reads.
|
|
1670
|
+
const videoTextureFields = new Map<string, Handle<'VideoAsset', 'shared'>>();
|
|
1671
|
+
const textureHandles = collectUserRegionTextureHandles(
|
|
1672
|
+
pv,
|
|
1673
|
+
firstPassShader,
|
|
1674
|
+
materialParamSchema.length > 0 ? materialParamSchema : undefined,
|
|
1675
|
+
assetsRef,
|
|
1676
|
+
world,
|
|
1677
|
+
resolveTexLike,
|
|
1678
|
+
videoTextureFields,
|
|
1679
|
+
);
|
|
1680
|
+
const samplerHandles = collectMaterialTextureSamplers(pv, (value) =>
|
|
1681
|
+
resolveTexLike(materialTextureRef(value), 'SamplerAsset'),
|
|
1682
|
+
);
|
|
1683
|
+
const emissiveTextureHandle = resolveTexLike(
|
|
1684
|
+
materialTextureRef(pv.emissiveTexture),
|
|
1685
|
+
'TextureAsset',
|
|
1686
|
+
);
|
|
1687
|
+
const occlusionTextureHandle = resolveTexLike(
|
|
1688
|
+
materialTextureRef(pv.occlusionTexture),
|
|
1689
|
+
'TextureAsset',
|
|
1690
|
+
);
|
|
1691
|
+
const textureCoordinates = collectMaterialTextureCoordinates(pv);
|
|
1692
|
+
// Named user-region fields are derived from the map (sprite + legacy reads).
|
|
1693
|
+
const baseColorTextureHandle = textureHandles.get('baseColorTexture');
|
|
1694
|
+
const metallicRoughnessTextureHandle = textureHandles.get('metallicRoughnessTexture');
|
|
1695
|
+
const normalTextureHandle = textureHandles.get('normalTexture');
|
|
1696
|
+
const emissivePv = pv.emissive as readonly number[] | undefined;
|
|
1697
|
+
const snapshot: MaterialSnapshot = {
|
|
1698
|
+
baseColor,
|
|
1699
|
+
metallic: metallicPv,
|
|
1700
|
+
roughness: roughnessPv,
|
|
1701
|
+
clearcoat: clearcoatPv,
|
|
1702
|
+
clearcoatRoughness: clearcoatRoughnessPv,
|
|
1703
|
+
...(specularTintPv !== undefined && {
|
|
1704
|
+
specularTint: [
|
|
1705
|
+
specularTintPv[0] ?? 1,
|
|
1706
|
+
specularTintPv[1] ?? 1,
|
|
1707
|
+
specularTintPv[2] ?? 1,
|
|
1708
|
+
] as readonly [number, number, number],
|
|
1709
|
+
}),
|
|
1710
|
+
normalScale: normalScalePv,
|
|
1711
|
+
materialShaderId: firstPassShader,
|
|
1712
|
+
materialHandle: handleRaw,
|
|
1713
|
+
renderState: pipelineRenderState(allPasses[0]?.renderState),
|
|
1714
|
+
paramSnapshot: paramSnap,
|
|
1715
|
+
...(materialParamSchema.length > 0 && { materialParamSchema }),
|
|
1716
|
+
...(textureCoordinates.size > 0 && { textureCoordinates }),
|
|
1717
|
+
...(textureHandles.size > 0 && { textureHandles }),
|
|
1718
|
+
...(videoTextureFields.size > 0 && { videoTextureFields }),
|
|
1719
|
+
...(samplerHandles.size > 0 && { samplerHandles }),
|
|
1720
|
+
...(baseColorTextureHandle !== undefined && { baseColorTexture: baseColorTextureHandle }),
|
|
1721
|
+
...(metallicRoughnessTextureHandle !== undefined && {
|
|
1722
|
+
metallicRoughnessTexture: metallicRoughnessTextureHandle,
|
|
1723
|
+
}),
|
|
1724
|
+
...(normalTextureHandle !== undefined && { normalTexture: normalTextureHandle }),
|
|
1725
|
+
...(emissivePv !== undefined && {
|
|
1726
|
+
emissive: [emissivePv[0] ?? 0, emissivePv[1] ?? 0, emissivePv[2] ?? 0] as readonly [
|
|
1727
|
+
number,
|
|
1728
|
+
number,
|
|
1729
|
+
number,
|
|
1730
|
+
],
|
|
1731
|
+
}),
|
|
1732
|
+
...(typeof pv.emissiveIntensity === 'number' && { emissiveIntensity: pv.emissiveIntensity }),
|
|
1733
|
+
...(emissiveTextureHandle !== undefined && { emissiveTexture: emissiveTextureHandle }),
|
|
1734
|
+
...(occlusionTextureHandle !== undefined && { occlusionTexture: occlusionTextureHandle }),
|
|
1735
|
+
...(typeof pv.occlusionStrength === 'number' && { occlusionStrength: pv.occlusionStrength }),
|
|
1736
|
+
// feat-city-glb Bug 5 (per-submesh transparency): derive `transparent`
|
|
1737
|
+
// from the first pass's `renderState.blend` presence, identical to the
|
|
1738
|
+
// entity-level snapshot builder (extractFrame archetype loop). Without
|
|
1739
|
+
// this, per-submesh materials (materials[i>=1], e.g. a glTF BLEND decal
|
|
1740
|
+
// submesh on a multi-material mesh) never carry the transparent flag, so
|
|
1741
|
+
// the record stage's LDR split + blend routing treats them as opaque and
|
|
1742
|
+
// their alpha=0 texels composite as black.
|
|
1743
|
+
transparent: allPasses[0]?.renderState?.blend !== undefined,
|
|
1744
|
+
};
|
|
1745
|
+
const stored =
|
|
1746
|
+
materialSnapshotCache !== undefined
|
|
1747
|
+
? storeMaterialSnapshot(
|
|
1748
|
+
materialSnapshotCache,
|
|
1749
|
+
handleRaw,
|
|
1750
|
+
snapshot,
|
|
1751
|
+
allPasses,
|
|
1752
|
+
asset,
|
|
1753
|
+
assetsRef,
|
|
1754
|
+
)
|
|
1755
|
+
: undefined;
|
|
1756
|
+
const cacheEntry =
|
|
1757
|
+
stored ??
|
|
1758
|
+
(persistentMaterialSnapshotCache !== undefined
|
|
1759
|
+
? storeMaterialSnapshot(
|
|
1760
|
+
persistentMaterialSnapshotCache,
|
|
1761
|
+
handleRaw,
|
|
1762
|
+
snapshot,
|
|
1763
|
+
allPasses,
|
|
1764
|
+
asset,
|
|
1765
|
+
assetsRef,
|
|
1766
|
+
)
|
|
1767
|
+
: undefined);
|
|
1768
|
+
if (cacheEntry?.crossFrameSafe === true) {
|
|
1769
|
+
persistentMaterialSnapshotCache?.set(handleRaw, cacheEntry);
|
|
1770
|
+
}
|
|
1771
|
+
return snapshot;
|
|
2257
1772
|
}
|
|
2258
1773
|
|
|
2259
1774
|
/**
|
|
@@ -2269,6 +1784,132 @@ export interface ExtractPipelineSurface {
|
|
|
2269
1784
|
readonly skinPaletteAllocator: SkinPaletteAllocator | null;
|
|
2270
1785
|
}
|
|
2271
1786
|
|
|
1787
|
+
/**
|
|
1788
|
+
* PSSM (Parallel-Split Shadow Maps) split plane computation.
|
|
1789
|
+
*
|
|
1790
|
+
* Formula (plan-strategy D-8, research F1):
|
|
1791
|
+
* C_i = λ·n·(f/n)^(i/m) + (1-λ)·(n + i/m·(f-n))
|
|
1792
|
+
* where i = 1..m, n = near, f = far, m = cascadeCount
|
|
1793
|
+
*
|
|
1794
|
+
* The coverage range is [camera near, DirectionalLight.shadowDistance] — the
|
|
1795
|
+
* near end derives from the active camera, the far end is the shadowDistance
|
|
1796
|
+
* knob. Returns strictly monotonic view-space z depths (positive).
|
|
1797
|
+
*
|
|
1798
|
+
* When `far <= near + ε` (ε=1e-6), throws ShadowInvalidConfigError
|
|
1799
|
+
* (charter P3 explicit failure — research F2 guarantees formula stability
|
|
1800
|
+
* but degenerate configuration must be surfaced to the caller).
|
|
1801
|
+
*
|
|
1802
|
+
* @param nearPlane - near of the shadow casting range (from the camera near)
|
|
1803
|
+
* @param farPlane - far of the shadow casting range (DirectionalLight.shadowDistance)
|
|
1804
|
+
* @param cascadeCount - number of cascades (1..4)
|
|
1805
|
+
* @param splitLambda - PSSM split weight (0 = pure uniform, 1 = pure log)
|
|
1806
|
+
* @returns Array of m split depths (view-space z); length = cascadeCount,
|
|
1807
|
+
* guaranteed strictly monotonic, last element = far.
|
|
1808
|
+
*
|
|
1809
|
+
* @internal exported for w6/w7 testing only; production callers route through
|
|
1810
|
+
* extractFrame which validates cascadeCount/splitLambda via component schema.
|
|
1811
|
+
*/
|
|
1812
|
+
/**
|
|
1813
|
+
* Compute the 8 world-space corner points of a camera frustum slice.
|
|
1814
|
+
*
|
|
1815
|
+
* Uses mat4.unproject to map NDC cube corners back to world space.
|
|
1816
|
+
* The slice is defined by view-space nearZ and farZ depths.
|
|
1817
|
+
*
|
|
1818
|
+
* For a WebGPU perspective matrix, ndcZ(viewZ) = camFar * (viewZ - camNear) /
|
|
1819
|
+
* (viewZ * (camFar - camNear)). We compute the ndcZ for each slice boundary
|
|
1820
|
+
* and unproject directly.
|
|
1821
|
+
*
|
|
1822
|
+
* @param vp - camera view-projection matrix (column-major 16 floats)
|
|
1823
|
+
* @param camNear - camera near plane (used to derive NDC mapping)
|
|
1824
|
+
* @param camFar - camera far plane (used to derive NDC mapping)
|
|
1825
|
+
* @param nearZ - near depth of the frustum slice (view-space, positive)
|
|
1826
|
+
* @param farZ - far depth of the frustum slice (view-space, positive)
|
|
1827
|
+
* @returns Array of 8 Vec3 world-space corner positions.
|
|
1828
|
+
*/
|
|
1829
|
+
function computeFrustumCorners(
|
|
1830
|
+
vp: Mat4,
|
|
1831
|
+
camNear: number,
|
|
1832
|
+
camFar: number,
|
|
1833
|
+
nearZ: number,
|
|
1834
|
+
farZ: number,
|
|
1835
|
+
projection: 'perspective' | 'orthographic',
|
|
1836
|
+
): Vec3[] {
|
|
1837
|
+
const invVP = mat4.create();
|
|
1838
|
+
mat4.invert(invVP, vp);
|
|
1839
|
+
const corners: Vec3[] = [];
|
|
1840
|
+
|
|
1841
|
+
// NDC z mapping is projection-dependent (WebGPU clip-space z in [0,1]):
|
|
1842
|
+
// perspective: ndc(z) = camFar * (z - camNear) / (z * (camFar - camNear))
|
|
1843
|
+
// orthographic: ndc(z) = (z - camNear) / (camFar - camNear)
|
|
1844
|
+
// feat-20260613-csm M6 / w22: orthographic cameras silently produced
|
|
1845
|
+
// garbage NDC z (perspective formula divides by viewZ, but ortho NDC is
|
|
1846
|
+
// linear), which mapped the cascade slab back to a degenerate world-space
|
|
1847
|
+
// corner set and the AABB-fit collapsed to near zero -- shadow_caster
|
|
1848
|
+
// wrote its triangles outside the [-1,1] clip volume so the depth
|
|
1849
|
+
// attachment stayed at clear=1.0 (root cause for shadow-m2 / shadow-m3 /
|
|
1850
|
+
// shadow-opt-out dawn red surfaced after w20's host-side ortho fix).
|
|
1851
|
+
const span = camFar - camNear;
|
|
1852
|
+
const ndcNear =
|
|
1853
|
+
projection === 'orthographic'
|
|
1854
|
+
? (nearZ - camNear) / span
|
|
1855
|
+
: (camFar * (nearZ - camNear)) / (nearZ * span);
|
|
1856
|
+
const ndcFar =
|
|
1857
|
+
projection === 'orthographic'
|
|
1858
|
+
? (farZ - camNear) / span
|
|
1859
|
+
: (camFar * (farZ - camNear)) / (farZ * span);
|
|
1860
|
+
|
|
1861
|
+
const signs = [-1, 1];
|
|
1862
|
+
for (const sx of signs) {
|
|
1863
|
+
for (const sy of signs) {
|
|
1864
|
+
corners.push(unprojectNDC(invVP, sx, sy, ndcNear));
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
for (const sx of signs) {
|
|
1868
|
+
for (const sy of signs) {
|
|
1869
|
+
corners.push(unprojectNDC(invVP, sx, sy, ndcFar));
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
return corners;
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
/**
|
|
1877
|
+
* Unproject a single NDC point to world space.
|
|
1878
|
+
*/
|
|
1879
|
+
function unprojectNDC(invVP: Mat4, ndcX: number, ndcY: number, ndcZ: number): Vec3 {
|
|
1880
|
+
const ndc = vec3.create(ndcX, ndcY, ndcZ);
|
|
1881
|
+
const ws = vec3.create();
|
|
1882
|
+
mat4.unproject(ws, ndc, invVP);
|
|
1883
|
+
return ws;
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
export function pssmSplit(
|
|
1887
|
+
nearPlane: number,
|
|
1888
|
+
farPlane: number,
|
|
1889
|
+
cascadeCount: number,
|
|
1890
|
+
splitLambda: number,
|
|
1891
|
+
): Float32Array {
|
|
1892
|
+
const EPS = 1e-6;
|
|
1893
|
+
if (farPlane <= nearPlane + EPS) {
|
|
1894
|
+
throw new ShadowInvalidConfigError('shadowDistance', farPlane, nearPlane + EPS);
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
const result = new Float32Array(cascadeCount);
|
|
1898
|
+
const m = cascadeCount;
|
|
1899
|
+
const n = nearPlane;
|
|
1900
|
+
const f = farPlane;
|
|
1901
|
+
const ratio = f / n;
|
|
1902
|
+
|
|
1903
|
+
for (let i = 1; i <= m; i++) {
|
|
1904
|
+
const t = i / m;
|
|
1905
|
+
const logPart = n * ratio ** t;
|
|
1906
|
+
const uniformPart = n + t * (f - n);
|
|
1907
|
+
result[i - 1] = splitLambda * logPart + (1 - splitLambda) * uniformPart;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
return result;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
2272
1913
|
// feat-20260612-point-light-shadows-urp-hdrp M1 / T-M1-5 (plan-strategy §D-3,
|
|
2273
1914
|
// requirements §5.3): 6-face view-proj matrix table for one omnidirectional
|
|
2274
1915
|
// shadow caster.
|
|
@@ -2342,8 +1983,196 @@ export function buildPointShadowMatrices(lightPos: Vec3, near: number, far: numb
|
|
|
2342
1983
|
return out;
|
|
2343
1984
|
}
|
|
2344
1985
|
|
|
2345
|
-
|
|
2346
|
-
|
|
1986
|
+
/**
|
|
1987
|
+
* bug-20260710-editor-cross-world-shadow: the camera-frustum subset of
|
|
1988
|
+
* {@link CameraSnapshot} that the directional-CSM matrix builder consumes.
|
|
1989
|
+
* Extracted so the builder can run both inside {@link extractFrame} (single
|
|
1990
|
+
* world) AND at the {@link extractFrames} merge layer, where the camera and the
|
|
1991
|
+
* directional light may originate in DIFFERENT worlds (the editor
|
|
1992
|
+
* editorWorld/sceneWorld super-composite). A cameraless world produces zero
|
|
1993
|
+
* lightViewProj matrices, so the merge layer MUST recompute using the surfaced
|
|
1994
|
+
* camera — see {@link computeDirectionalCsm}.
|
|
1995
|
+
*/
|
|
1996
|
+
export interface CsmCameraData {
|
|
1997
|
+
readonly world: Float32Array;
|
|
1998
|
+
readonly fov: number;
|
|
1999
|
+
readonly aspect: number;
|
|
2000
|
+
readonly near: number;
|
|
2001
|
+
readonly far: number;
|
|
2002
|
+
readonly projection: 'perspective' | 'orthographic';
|
|
2003
|
+
readonly orthoLeft: number;
|
|
2004
|
+
readonly orthoRight: number;
|
|
2005
|
+
readonly orthoBottom: number;
|
|
2006
|
+
readonly orthoTop: number;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
/**
|
|
2010
|
+
* bug-20260710-editor-cross-world-shadow: the raw directional-shadow config a
|
|
2011
|
+
* DirectionalLight carries, threaded onto {@link ExtractedLights} so the merge
|
|
2012
|
+
* layer can recompute CSM matrices against the surfaced camera. Distinct from
|
|
2013
|
+
* the *computed* outputs (lightViewProj / splitPlanes) also on ExtractedLights.
|
|
2014
|
+
*/
|
|
2015
|
+
export interface DirectionalCsmConfig {
|
|
2016
|
+
readonly cascadeCount: number;
|
|
2017
|
+
readonly splitLambda: number;
|
|
2018
|
+
readonly cascadeBlend: number;
|
|
2019
|
+
readonly mapSize: number;
|
|
2020
|
+
readonly shadowDistance: number;
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
/**
|
|
2024
|
+
* bug-20260710-editor-cross-world-shadow: computed directional-CSM output —
|
|
2025
|
+
* the per-cascade light-view-projection matrices plus the derived cascade
|
|
2026
|
+
* metadata. Returned by {@link computeDirectionalCsm}; `undefined` when the
|
|
2027
|
+
* config or camera is missing (caller leaves the fields undefined).
|
|
2028
|
+
*/
|
|
2029
|
+
export interface DirectionalCsmResult {
|
|
2030
|
+
readonly lightViewProj: Float32Array[];
|
|
2031
|
+
readonly splitPlanes: Float32Array;
|
|
2032
|
+
readonly cascadeCount: number;
|
|
2033
|
+
readonly cascadeBlend: number;
|
|
2034
|
+
readonly shadowMapSize: number;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
/**
|
|
2038
|
+
* bug-20260710-editor-cross-world-shadow: pure directional-CSM matrix builder,
|
|
2039
|
+
* extracted verbatim from the former inline block in {@link extractFrame} (the
|
|
2040
|
+
* PSSM split + per-cascade frustum-slice AABB fit + orthographic light
|
|
2041
|
+
* projection). It is now a free function so BOTH the per-world extract and the
|
|
2042
|
+
* cross-world merge layer can call it — the merge layer is the only place that
|
|
2043
|
+
* pairs the first-hit directional light of one world with the surfaced camera
|
|
2044
|
+
* of another (editor super-composite). Returns `null` when there is no camera
|
|
2045
|
+
* to fit against (a cameraless world's directional light yields no matrices —
|
|
2046
|
+
* the caller leaves lightViewProj undefined rather than emitting zero matrices
|
|
2047
|
+
* that the WGSL reader would sample as NaN → "fully lit").
|
|
2048
|
+
*
|
|
2049
|
+
* Byte-identical to the prior inline computation for the single-world case
|
|
2050
|
+
* (same PSSM lambda, same toward-light Z reach RC-2 fix, same clip-space
|
|
2051
|
+
* matrix / tile-placement-in-shader split).
|
|
2052
|
+
*/
|
|
2053
|
+
export function computeDirectionalCsm(
|
|
2054
|
+
direction: Vec3,
|
|
2055
|
+
config: DirectionalCsmConfig,
|
|
2056
|
+
cameraData: CsmCameraData | undefined,
|
|
2057
|
+
): DirectionalCsmResult | null {
|
|
2058
|
+
const cascadeCount = Math.round(config.cascadeCount);
|
|
2059
|
+
// Coverage range: near from the active camera near (no separate near knob);
|
|
2060
|
+
// far is the component's shadowDistance. Fallback near (0.1) only when no
|
|
2061
|
+
// camera exists — but matrices are gated on cameraData below anyway.
|
|
2062
|
+
const sNear = cameraData?.near ?? 0.1;
|
|
2063
|
+
const sFar = config.shadowDistance;
|
|
2064
|
+
|
|
2065
|
+
// PSSM split planes: [camera near, shadowDistance], not the camera far.
|
|
2066
|
+
const splits = pssmSplit(sNear, sFar, cascadeCount, config.splitLambda);
|
|
2067
|
+
const paddedSplits = new Float32Array(4);
|
|
2068
|
+
for (let i = 0; i < splits.length; i++) paddedSplits[i] = splits[i] ?? 0;
|
|
2069
|
+
|
|
2070
|
+
// Light view matrix: camera at origin, looking along the light direction.
|
|
2071
|
+
const lightDirN = vec3.normalize(vec3.create(), direction);
|
|
2072
|
+
const lightTarget = vec3.create(lightDirN[0] ?? 0, lightDirN[1] ?? 0, lightDirN[2] ?? 0);
|
|
2073
|
+
const lightView = mat4.create();
|
|
2074
|
+
mat4.lookAt(lightView, vec3.create(0, 0, 0), lightTarget, vec3.create(0, 1, 0));
|
|
2075
|
+
|
|
2076
|
+
// Camera view-projection for frustum-corner computation. Without a camera the
|
|
2077
|
+
// frustum cannot be fit — bail so the caller emits no matrices (undefined),
|
|
2078
|
+
// NOT zero matrices (which read as NaN in the shadow shader → no shadow).
|
|
2079
|
+
if (cameraData === undefined) return null;
|
|
2080
|
+
const camProj = mat4.create();
|
|
2081
|
+
if (cameraData.projection === 'orthographic') {
|
|
2082
|
+
mat4.orthographic(
|
|
2083
|
+
camProj,
|
|
2084
|
+
cameraData.orthoLeft,
|
|
2085
|
+
cameraData.orthoRight,
|
|
2086
|
+
cameraData.orthoBottom,
|
|
2087
|
+
cameraData.orthoTop,
|
|
2088
|
+
cameraData.near,
|
|
2089
|
+
cameraData.far,
|
|
2090
|
+
);
|
|
2091
|
+
} else {
|
|
2092
|
+
mat4.perspective(camProj, cameraData.fov, cameraData.aspect, cameraData.near, cameraData.far);
|
|
2093
|
+
}
|
|
2094
|
+
const camView = mat4.create();
|
|
2095
|
+
mat4.invert(camView, cameraData.world);
|
|
2096
|
+
const cameraVP = mat4.create();
|
|
2097
|
+
mat4.multiply(cameraVP, camProj, camView);
|
|
2098
|
+
|
|
2099
|
+
const resultLightViewProjs: Float32Array[] = [];
|
|
2100
|
+
|
|
2101
|
+
// bug-20260619 RC-2 (AC-05): toward-light Z reach. Extend the near (toward-
|
|
2102
|
+
// light) bound of EVERY cascade to the toward-light extreme of the WHOLE
|
|
2103
|
+
// shadow frustum (sNear..sFar) so casters between the light and a slice are
|
|
2104
|
+
// admitted; X/Y stays per-cascade tight. Larger light-space z == closer to
|
|
2105
|
+
// the light (lookAt forward = eye-target), so the full-frustum max-z is the
|
|
2106
|
+
// toward-light reach used as -maxZ (the ortho near plane) per cascade.
|
|
2107
|
+
let lightSpaceMaxZFull = -Infinity;
|
|
2108
|
+
const fullCorners = computeFrustumCorners(
|
|
2109
|
+
cameraVP,
|
|
2110
|
+
cameraData.near,
|
|
2111
|
+
cameraData.far,
|
|
2112
|
+
sNear,
|
|
2113
|
+
sFar,
|
|
2114
|
+
cameraData.projection,
|
|
2115
|
+
);
|
|
2116
|
+
for (const ws of fullCorners) {
|
|
2117
|
+
const ls = vec3.create();
|
|
2118
|
+
mat4.transformVec3(ls, lightView, ws);
|
|
2119
|
+
if ((ls[2] ?? 0) > lightSpaceMaxZFull) lightSpaceMaxZFull = ls[2] ?? 0;
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
// Pre-allocate 4-cascade array; fill in the effective cascades.
|
|
2123
|
+
for (let cIdx = 0; cIdx < 4; cIdx++) {
|
|
2124
|
+
if (cIdx >= cascadeCount) {
|
|
2125
|
+
resultLightViewProjs.push(new Float32Array(16));
|
|
2126
|
+
continue;
|
|
2127
|
+
}
|
|
2128
|
+
const cascadeNear = cIdx === 0 ? sNear : (splits[cIdx - 1] ?? sFar);
|
|
2129
|
+
const cascadeFar = splits[cIdx] ?? sFar;
|
|
2130
|
+
const corners = computeFrustumCorners(
|
|
2131
|
+
cameraVP,
|
|
2132
|
+
cameraData.near,
|
|
2133
|
+
cameraData.far,
|
|
2134
|
+
cascadeNear,
|
|
2135
|
+
cascadeFar,
|
|
2136
|
+
cameraData.projection,
|
|
2137
|
+
);
|
|
2138
|
+
const lightMVP = mat4.clone(lightView);
|
|
2139
|
+
let minX = Infinity;
|
|
2140
|
+
let maxX = -Infinity;
|
|
2141
|
+
let minY = Infinity;
|
|
2142
|
+
let maxY = -Infinity;
|
|
2143
|
+
let minZ = Infinity;
|
|
2144
|
+
let maxZ = -Infinity;
|
|
2145
|
+
for (const ws of corners) {
|
|
2146
|
+
const ls = vec3.create();
|
|
2147
|
+
mat4.transformVec3(ls, lightMVP, ws);
|
|
2148
|
+
if ((ls[0] ?? 0) < minX) minX = ls[0] ?? 0;
|
|
2149
|
+
if ((ls[0] ?? 0) > maxX) maxX = ls[0] ?? 0;
|
|
2150
|
+
if ((ls[1] ?? 0) < minY) minY = ls[1] ?? 0;
|
|
2151
|
+
if ((ls[1] ?? 0) > maxY) maxY = ls[1] ?? 0;
|
|
2152
|
+
if ((ls[2] ?? 0) < minZ) minZ = ls[2] ?? 0;
|
|
2153
|
+
if ((ls[2] ?? 0) > maxZ) maxZ = ls[2] ?? 0;
|
|
2154
|
+
}
|
|
2155
|
+
// Orthographic projection from light-space AABB. RC-2: whole-frustum
|
|
2156
|
+
// toward-light extreme for the near bound; per-cascade far (minZ) + X/Y.
|
|
2157
|
+
const nearZ = Math.max(maxZ, lightSpaceMaxZFull);
|
|
2158
|
+
const orthoProj = mat4.create();
|
|
2159
|
+
mat4.orthographic(orthoProj, minX, maxX, minY, maxY, -nearZ, -minZ);
|
|
2160
|
+
// lightViewProj = orthoProj * lightView — pure clip-space [-1,1]. Atlas
|
|
2161
|
+
// tile placement is handled by the per-cascade viewport + fragment-side UV
|
|
2162
|
+
// math (evalDirectional), so shadow_caster.gl_Position stays clip-space.
|
|
2163
|
+
void cIdx;
|
|
2164
|
+
resultLightViewProjs.push(new Float32Array(mat4.multiply(mat4.create(), orthoProj, lightView)));
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
return {
|
|
2168
|
+
lightViewProj: resultLightViewProjs,
|
|
2169
|
+
splitPlanes: paddedSplits,
|
|
2170
|
+
cascadeCount,
|
|
2171
|
+
cascadeBlend: config.cascadeBlend,
|
|
2172
|
+
shadowMapSize: config.mapSize,
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2347
2176
|
/**
|
|
2348
2177
|
* feat-20260709-editor-world-partition M1 / w4 (AC-08, plan-strategy §2 D-3):
|
|
2349
2178
|
* the owner index that previously served BOTH cameras and singleton render
|
|
@@ -2360,42 +2189,29 @@ export interface ExtractFramesOwner {
|
|
|
2360
2189
|
}
|
|
2361
2190
|
|
|
2362
2191
|
export interface PreparedExtractContext {
|
|
2363
|
-
|
|
2364
|
-
readonly worldId?: number | undefined;
|
|
2365
|
-
readonly assets: RenderAssetPort | null | undefined;
|
|
2192
|
+
readonly assets: AssetRegistry | null | undefined;
|
|
2366
2193
|
readonly pipelineState: ExtractPipelineSurface | null | undefined;
|
|
2367
|
-
readonly gpuStore: import('./gpu-resource-store').GpuResourceStore | undefined;
|
|
2368
2194
|
readonly materialSnapshotCache: MaterialSnapshotCache | undefined;
|
|
2369
2195
|
readonly cull: 'self' | 'none' | 'external';
|
|
2370
2196
|
readonly cullCameras: readonly CameraSnapshot[] | undefined;
|
|
2371
2197
|
readonly hierarchy: SceneHierarchySnapshot;
|
|
2372
|
-
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[];
|
|
2198
|
+
readonly visibility: VisibilitySnapshot;
|
|
2383
2199
|
}
|
|
2384
2200
|
|
|
2385
2201
|
export function extractCameraSnapshots(world: World): CameraSnapshot[] {
|
|
2202
|
+
const worldInternal = createWorldInternalView(world);
|
|
2386
2203
|
const cameras: CameraSnapshot[] = [];
|
|
2387
2204
|
const cameraEntities: number[] = [];
|
|
2388
|
-
const cameraQuery = world.query({ read: [Camera, Transform] }).unwrap();
|
|
2205
|
+
const cameraQuery = world.query({ read: [Camera], with: [Transform] }).unwrap();
|
|
2389
2206
|
for (const row of cameraQuery) {
|
|
2390
2207
|
const cam = row.get(Camera);
|
|
2391
2208
|
const entity = row.entity;
|
|
2392
|
-
const
|
|
2393
|
-
if (
|
|
2394
|
-
const worldMat = new Float32Array(
|
|
2209
|
+
const view = worldInternal._getArrayView(entity, Transform, 'world');
|
|
2210
|
+
if (view === undefined) continue;
|
|
2211
|
+
const worldMat = new Float32Array(view);
|
|
2395
2212
|
cameras.push({
|
|
2396
|
-
worldId: 0,
|
|
2397
2213
|
entityKey: entity as number,
|
|
2398
|
-
position: mat4.getTranslation(vec3.create(), worldMat
|
|
2214
|
+
position: mat4.getTranslation(vec3.create(), worldMat),
|
|
2399
2215
|
world: worldMat,
|
|
2400
2216
|
fov: cam.fov,
|
|
2401
2217
|
aspect: cam.aspect,
|
|
@@ -2410,7 +2226,6 @@ export function extractCameraSnapshots(world: World): CameraSnapshot[] {
|
|
|
2410
2226
|
exposure: cam.exposure,
|
|
2411
2227
|
whitePoint: cam.whitePoint,
|
|
2412
2228
|
antialias: antialiasFromF32(cam.antialias),
|
|
2413
|
-
historyVersion: cam.historyVersion,
|
|
2414
2229
|
bloom: bloomEnabledFromF32(cam.bloom),
|
|
2415
2230
|
bloomThreshold: cam.bloomThreshold,
|
|
2416
2231
|
bloomIntensity: cam.bloomIntensity,
|
|
@@ -2440,114 +2255,35 @@ function tonemapParams(camera: CameraSnapshot): Uint8Array {
|
|
|
2440
2255
|
return new Uint8Array(bytes);
|
|
2441
2256
|
}
|
|
2442
2257
|
|
|
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
2258
|
export function prepareExtractContext(
|
|
2511
2259
|
world: World,
|
|
2512
2260
|
options: {
|
|
2513
|
-
readonly
|
|
2514
|
-
readonly assets?: RenderAssetPort | null;
|
|
2261
|
+
readonly assets?: AssetRegistry | null;
|
|
2515
2262
|
readonly pipelineState?: ExtractPipelineSurface | null;
|
|
2516
|
-
readonly gpuStore?: import('./gpu-resource-store').GpuResourceStore;
|
|
2517
2263
|
readonly materialSnapshotCache?: MaterialSnapshotCache;
|
|
2518
2264
|
readonly cull?: 'self' | 'none' | 'external';
|
|
2519
2265
|
readonly cullCameras?: readonly CameraSnapshot[];
|
|
2520
|
-
readonly paramSchemaProjectionCensus?: ParamSchemaProjectionCensus;
|
|
2521
2266
|
} = {},
|
|
2522
2267
|
): PreparedExtractContext {
|
|
2523
2268
|
const hierarchy = projectHierarchy(world);
|
|
2524
2269
|
return {
|
|
2525
|
-
worldId: options.worldId ?? 0,
|
|
2526
2270
|
assets: options.assets,
|
|
2527
2271
|
pipelineState: options.pipelineState,
|
|
2528
|
-
gpuStore: options.gpuStore,
|
|
2529
2272
|
materialSnapshotCache: options.materialSnapshotCache,
|
|
2530
2273
|
cull: options.cull ?? 'self',
|
|
2531
2274
|
cullCameras: options.cullCameras,
|
|
2532
2275
|
hierarchy,
|
|
2533
2276
|
visibility: resolveVisibility(world, hierarchy),
|
|
2534
|
-
...(options.paramSchemaProjectionCensus === undefined
|
|
2535
|
-
? {}
|
|
2536
|
-
: { paramSchemaProjectionCensus: options.paramSchemaProjectionCensus }),
|
|
2537
2277
|
};
|
|
2538
2278
|
}
|
|
2539
2279
|
|
|
2540
2280
|
export function extractFrames(
|
|
2541
2281
|
worlds: readonly World[],
|
|
2542
2282
|
owner: number | ExtractFramesOwner,
|
|
2543
|
-
assets?:
|
|
2283
|
+
assets?: AssetRegistry | null,
|
|
2544
2284
|
pipelineState?: ExtractPipelineSurface | null,
|
|
2545
|
-
gpuStore?: import('./gpu-resource-store').GpuResourceStore,
|
|
2546
2285
|
materialSnapshotCachesByWorld?: MaterialSnapshotCachesByWorld,
|
|
2547
|
-
options: {
|
|
2548
|
-
readonly cull?: 'normal' | 'none';
|
|
2549
|
-
readonly paramSchemaProjectionCensus?: ParamSchemaProjectionCensus;
|
|
2550
|
-
} = {},
|
|
2286
|
+
options: { readonly cull?: 'normal' | 'none' } = {},
|
|
2551
2287
|
): ExtractedFrame {
|
|
2552
2288
|
// w4: normalize the owner argument. A bare number is the legacy single-owner
|
|
2553
2289
|
// form (cameraOwner === resourceOwner); an object carries the two split
|
|
@@ -2559,7 +2295,7 @@ export function extractFrames(
|
|
|
2559
2295
|
// ── D-2: frame-level side effects live here ────────────────────────────
|
|
2560
2296
|
//
|
|
2561
2297
|
// resetForFrame is called exactly once per frame, at the extractFrames
|
|
2562
|
-
// entry. The skinPaletteAllocator cursor is reset before
|
|
2298
|
+
// entry. The skinPaletteAllocator cursor is reset before each per-world
|
|
2563
2299
|
// extract runs, so sequential per-world allocation yields non-overlapping
|
|
2564
2300
|
// palette slices (AC-08).
|
|
2565
2301
|
const skinPaletteAllocator = pipelineState?.skinPaletteAllocator ?? null;
|
|
@@ -2567,55 +2303,7 @@ export function extractFrames(
|
|
|
2567
2303
|
skinPaletteAllocator.resetForFrame();
|
|
2568
2304
|
}
|
|
2569
2305
|
|
|
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
2306
|
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
2307
|
// ── D-2: per-world extract with error isolation ────────────────────────
|
|
2620
2308
|
//
|
|
2621
2309
|
// Each world runs extractFrame over the final state published by
|
|
@@ -2644,10 +2332,8 @@ export function extractFrames(
|
|
|
2644
2332
|
const isCameraOwner = wi === cameraOwner;
|
|
2645
2333
|
const cameraOwnerFrame = framesByWorld.get(cameraOwner);
|
|
2646
2334
|
const prepared = prepareExtractContext(world, {
|
|
2647
|
-
worldId: wi,
|
|
2648
2335
|
...(assets !== undefined ? { assets } : {}),
|
|
2649
2336
|
...(pipelineState !== undefined ? { pipelineState } : {}),
|
|
2650
|
-
...(gpuStore !== undefined ? { gpuStore } : {}),
|
|
2651
2337
|
...(materialSnapshotCachesByWorld === undefined
|
|
2652
2338
|
? {}
|
|
2653
2339
|
: {
|
|
@@ -2661,9 +2347,6 @@ export function extractFrames(
|
|
|
2661
2347
|
}),
|
|
2662
2348
|
cull: options.cull === 'none' ? 'none' : isCameraOwner ? 'self' : 'external',
|
|
2663
2349
|
...(cameraOwnerFrame === undefined ? {} : { cullCameras: cameraOwnerFrame.cameras }),
|
|
2664
|
-
...(options.paramSchemaProjectionCensus === undefined
|
|
2665
|
-
? {}
|
|
2666
|
-
: { paramSchemaProjectionCensus: options.paramSchemaProjectionCensus }),
|
|
2667
2350
|
});
|
|
2668
2351
|
const frame = extractFrame(world, prepared);
|
|
2669
2352
|
|
|
@@ -2671,7 +2354,10 @@ export function extractFrames(
|
|
|
2671
2354
|
} catch (err) {
|
|
2672
2355
|
// Per-world failure: route to world's own error handler, skip contribution.
|
|
2673
2356
|
try {
|
|
2674
|
-
|
|
2357
|
+
createWorldInternalView(world)._routeError(err, {
|
|
2358
|
+
severity: Severity.Error,
|
|
2359
|
+
systemName: `RenderSystem.extractFrames(world[${wi}])`,
|
|
2360
|
+
});
|
|
2675
2361
|
} catch {
|
|
2676
2362
|
// If _routeError itself throws, the world already failed — skip silently.
|
|
2677
2363
|
}
|
|
@@ -2690,9 +2376,7 @@ export function extractFrames(
|
|
|
2690
2376
|
|
|
2691
2377
|
// AC-04: renderables — concat by worlds[] order, stamp worldId.
|
|
2692
2378
|
const renderables: RenderableSnapshot[] = [];
|
|
2693
|
-
const candidateRenderables: RenderableSnapshot[] = [];
|
|
2694
2379
|
const dispatchEntries: DispatchEntry[] = [];
|
|
2695
|
-
const candidateDispatchEntries: DispatchEntry[] = [];
|
|
2696
2380
|
const visibilitySnapshots: VisibilitySnapshot[] = [];
|
|
2697
2381
|
const featureVisibilitySnapshots: RenderFeatureWorldVisibilitySnapshot[] = [];
|
|
2698
2382
|
const hiddenEntityReports: RenderFeatureHiddenEntityReport[] = [];
|
|
@@ -2703,7 +2387,6 @@ export function extractFrames(
|
|
|
2703
2387
|
if (f === undefined || wId === undefined) continue;
|
|
2704
2388
|
|
|
2705
2389
|
const base = renderables.length;
|
|
2706
|
-
const candidateBase = candidateRenderables.length;
|
|
2707
2390
|
const visibilitySnapshot = f.visibilitySnapshots[0];
|
|
2708
2391
|
const world = worlds[wId];
|
|
2709
2392
|
if (visibilitySnapshot !== undefined) {
|
|
@@ -2716,18 +2399,7 @@ export function extractFrames(
|
|
|
2716
2399
|
renderables.push({
|
|
2717
2400
|
...r,
|
|
2718
2401
|
worldId: wId,
|
|
2719
|
-
...(r.
|
|
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
|
-
: {}),
|
|
2402
|
+
...(r.pointsLines === undefined ? {} : { pointsLines: { ...r.pointsLines, worldId: wId } }),
|
|
2731
2403
|
});
|
|
2732
2404
|
}
|
|
2733
2405
|
|
|
@@ -2735,22 +2407,10 @@ export function extractFrames(
|
|
|
2735
2407
|
for (const d of f.dispatch) {
|
|
2736
2408
|
dispatchEntries.push({ ...d, renderableIndex: (d.renderableIndex ?? 0) + base });
|
|
2737
2409
|
}
|
|
2738
|
-
for (const d of f.candidateDispatch ?? f.dispatch) {
|
|
2739
|
-
candidateDispatchEntries.push({
|
|
2740
|
-
...d,
|
|
2741
|
-
renderableIndex: (d.renderableIndex ?? 0) + candidateBase,
|
|
2742
|
-
});
|
|
2743
|
-
}
|
|
2744
2410
|
}
|
|
2745
2411
|
|
|
2746
2412
|
// Stable sort dispatch by queue value.
|
|
2747
2413
|
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
2414
|
|
|
2755
2415
|
// AC-04: lights — point[]/spot[] concat; directional first-hit in
|
|
2756
2416
|
// succeededFrames order (which preserves worlds[] order for successful
|
|
@@ -2767,7 +2427,6 @@ export function extractFrames(
|
|
|
2767
2427
|
let depthBias: number | undefined;
|
|
2768
2428
|
let normalBias: number | undefined;
|
|
2769
2429
|
let pcfKernelSize: number | undefined;
|
|
2770
|
-
let directionalCsm: DirectionalCsmResult | undefined;
|
|
2771
2430
|
const pointShadow: PointShadowSnapshot[] = [];
|
|
2772
2431
|
// bug-20260710-editor-cross-world-shadow: carry the raw CSM config +
|
|
2773
2432
|
// direction of the first-hit directional so the merge layer can recompute
|
|
@@ -2807,10 +2466,7 @@ export function extractFrames(
|
|
|
2807
2466
|
if (succeededIndices[fi] === cameraOwner) cameraOwnerFrame = succeededFrames[fi];
|
|
2808
2467
|
if (succeededIndices[fi] === resourceOwner) resourceOwnerFrame = succeededFrames[fi];
|
|
2809
2468
|
}
|
|
2810
|
-
const cameras =
|
|
2811
|
-
cameraOwnerFrame !== undefined
|
|
2812
|
-
? cameraOwnerFrame.cameras.map((camera) => ({ ...camera, worldId: cameraOwner }))
|
|
2813
|
-
: [];
|
|
2469
|
+
const cameras = cameraOwnerFrame !== undefined ? [...cameraOwnerFrame.cameras] : [];
|
|
2814
2470
|
|
|
2815
2471
|
// bug-20260710-editor-cross-world-shadow: RECOMPUTE directional CSM matrices
|
|
2816
2472
|
// against the surfaced camera. In a single-world app the per-world extract
|
|
@@ -2842,10 +2498,8 @@ export function extractFrames(
|
|
|
2842
2498
|
directionalCsmDirection,
|
|
2843
2499
|
directionalCsmConfig,
|
|
2844
2500
|
mergeCameraData,
|
|
2845
|
-
renderCandidateCsmCandidates(candidateDomain),
|
|
2846
2501
|
);
|
|
2847
2502
|
if (csm !== null) {
|
|
2848
|
-
directionalCsm = csm;
|
|
2849
2503
|
lightViewProj = csm.lightViewProj;
|
|
2850
2504
|
splitPlanes = csm.splitPlanes;
|
|
2851
2505
|
cascadeCount = csm.cascadeCount;
|
|
@@ -2872,35 +2526,30 @@ export function extractFrames(
|
|
|
2872
2526
|
pointShadow,
|
|
2873
2527
|
directionalCsmConfig,
|
|
2874
2528
|
directionalCsmDirection,
|
|
2875
|
-
...(directionalCsm !== undefined ? { directionalCsm, cascades: directionalCsm.cascades } : {}),
|
|
2876
2529
|
};
|
|
2877
|
-
const
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
worldId: resourceOwner,
|
|
2882
|
-
skylightCount: 0,
|
|
2883
|
-
skyboxCount: 0,
|
|
2884
|
-
} satisfies ExtractedEnvironment);
|
|
2530
|
+
const skylight = resourceOwnerFrame?.skylight;
|
|
2531
|
+
const skylightCount = resourceOwnerFrame?.skylightCount ?? 0;
|
|
2532
|
+
const skybox = resourceOwnerFrame?.skybox;
|
|
2533
|
+
const skyboxCount = resourceOwnerFrame?.skyboxCount ?? 0;
|
|
2885
2534
|
// User-authored PostProcessParams entities are singleton scene resources, so
|
|
2886
2535
|
// they come from the resourceOwner world (holistic snapshot selection).
|
|
2887
2536
|
const postProcessParams = new Map(resourceOwnerFrame?.postProcessParams);
|
|
2888
2537
|
// feat-20260709-editor-world-partition ENGINE-fix-round2 (defect 1): the
|
|
2889
|
-
// engine built-in
|
|
2538
|
+
// engine built-in Standard tonemap param is NOT a scene resource — the
|
|
2890
2539
|
// per-world extractFrame bridges it from that world's own `cameras[0]`
|
|
2891
2540
|
// (Camera.exposure / whitePoint / tonemap is the SSOT). It therefore lives on
|
|
2892
2541
|
// the CAMERA-owner frame, not the resource-owner frame. In the split-owner
|
|
2893
2542
|
// editor topology the resourceOwner world has no Camera, so its frame carries
|
|
2894
|
-
// no
|
|
2543
|
+
// no Standard tonemap entry; taking postProcessParams from resourceOwner
|
|
2895
2544
|
// alone drops it, the tonemap pass's params UBO stays zero-filled
|
|
2896
2545
|
// (exposure=0 => tonemapped output is uniformly black), and the whole frame
|
|
2897
2546
|
// reads black even though geometry drew into the HDR target. Overlay the
|
|
2898
2547
|
// camera-owner frame's tonemap param (its SSOT source) so the surfaced
|
|
2899
2548
|
// camera's exposure/whitePoint/mode reach the tonemap pass. When
|
|
2900
2549
|
// cameraOwner === resourceOwner this is a no-op (identical entry). The 'na'
|
|
2901
|
-
//
|
|
2902
|
-
// bottom of extractFrame (SSOT: same
|
|
2903
|
-
const TONEMAP_PARAM_KEY =
|
|
2550
|
+
// Standard tonemap identity mirrors the engine provider key set at the
|
|
2551
|
+
// bottom of extractFrame (SSOT: same identity, same 16B layout).
|
|
2552
|
+
const TONEMAP_PARAM_KEY = STANDARD_TONEMAP_FEATURE_ID;
|
|
2904
2553
|
const cameraTonemapParam = cameraOwnerFrame?.postProcessParams.get(TONEMAP_PARAM_KEY);
|
|
2905
2554
|
if (cameraTonemapParam !== undefined) {
|
|
2906
2555
|
postProcessParams.set(TONEMAP_PARAM_KEY, cameraTonemapParam);
|
|
@@ -2920,21 +2569,16 @@ export function extractFrames(
|
|
|
2920
2569
|
const visibilityStats = {
|
|
2921
2570
|
explicitlyHidden: succeededFrames.reduce((s, f) => s + f.visibilityStats.explicitlyHidden, 0),
|
|
2922
2571
|
};
|
|
2923
|
-
const resourceWorld = worlds[resourceOwner];
|
|
2924
|
-
const fog = selectFog(resourceWorld, resourceOwner);
|
|
2925
|
-
if (!fog.ok) routeFogSelectionError(resourceWorld, fog.error);
|
|
2926
2572
|
|
|
2927
2573
|
return {
|
|
2928
2574
|
cameras,
|
|
2929
2575
|
lights,
|
|
2930
2576
|
renderables,
|
|
2931
2577
|
dispatch: dispatchEntries,
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
fog: fog.ok ? fog.value.snapshot : undefined,
|
|
2937
|
-
environment,
|
|
2578
|
+
skylight,
|
|
2579
|
+
skylightCount,
|
|
2580
|
+
skybox,
|
|
2581
|
+
skyboxCount,
|
|
2938
2582
|
frustumStats,
|
|
2939
2583
|
visibilityStats,
|
|
2940
2584
|
postProcessParams,
|
|
@@ -2982,6 +2626,36 @@ function hasFiniteOrderedLocalAabb(aabb: Float32Array | undefined): aabb is Floa
|
|
|
2982
2626
|
return minX <= maxX && minY <= maxY && minZ <= maxZ;
|
|
2983
2627
|
}
|
|
2984
2628
|
|
|
2629
|
+
function morphSnapshotFor(
|
|
2630
|
+
mesh: MeshAsset,
|
|
2631
|
+
weights: ArrayLike<number> | undefined,
|
|
2632
|
+
): MorphSnapshot | undefined {
|
|
2633
|
+
const targets = mesh.morphTargets;
|
|
2634
|
+
if (targets === undefined || targets.length === 0 || weights === undefined) return undefined;
|
|
2635
|
+
if (weights.length !== targets.length) return undefined;
|
|
2636
|
+
const firstPositions = targets[0]?.position;
|
|
2637
|
+
if (
|
|
2638
|
+
firstPositions === undefined ||
|
|
2639
|
+
firstPositions.length === 0 ||
|
|
2640
|
+
firstPositions.length % 3 !== 0
|
|
2641
|
+
) {
|
|
2642
|
+
return undefined;
|
|
2643
|
+
}
|
|
2644
|
+
const vertexCount = firstPositions.length / 3;
|
|
2645
|
+
if (mesh.vertices.length % vertexCount !== 0) return undefined;
|
|
2646
|
+
for (let targetIndex = 0; targetIndex < targets.length; targetIndex += 1) {
|
|
2647
|
+
const target = targets[targetIndex];
|
|
2648
|
+
if (target?.position?.length !== firstPositions.length) return undefined;
|
|
2649
|
+
}
|
|
2650
|
+
const copied = new Float32Array(weights.length);
|
|
2651
|
+
for (let index = 0; index < weights.length; index += 1) {
|
|
2652
|
+
const weight = weights[index] ?? Number.NaN;
|
|
2653
|
+
if (!Number.isFinite(weight)) return undefined;
|
|
2654
|
+
copied[index] = weight;
|
|
2655
|
+
}
|
|
2656
|
+
return { weights: copied, targetCount: targets.length };
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2985
2659
|
export function extractFrame(world: World, context: PreparedExtractContext): ExtractedFrame {
|
|
2986
2660
|
// feat-20260708-composited-multi-world-rendering M2 / D-2: resetForFrame
|
|
2987
2661
|
// has been lifted to extractFrames (the frame-level entry point).
|
|
@@ -2990,16 +2664,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
2990
2664
|
const {
|
|
2991
2665
|
assets,
|
|
2992
2666
|
pipelineState,
|
|
2993
|
-
gpuStore,
|
|
2994
2667
|
materialSnapshotCache: persistentMaterialSnapshotCache,
|
|
2995
2668
|
cull: cullMode,
|
|
2996
2669
|
} = context;
|
|
2997
|
-
const reportRenderError = (
|
|
2998
|
-
error: unknown,
|
|
2999
|
-
errorContext?: { readonly systemName: string },
|
|
3000
|
-
): void => {
|
|
3001
|
-
routeWorldError(world, error, errorContext);
|
|
3002
|
-
};
|
|
3003
2670
|
const visibility = context.visibility.hasAnyHiddenIntent ? context.visibility : undefined;
|
|
3004
2671
|
const skinPaletteAllocator = pipelineState?.skinPaletteAllocator ?? null;
|
|
3005
2672
|
|
|
@@ -3011,11 +2678,13 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3011
2678
|
//
|
|
3012
2679
|
// Each segment routes through a World-owned Query with explicit read and
|
|
3013
2680
|
// optional roles. The packed entity handle for `readWorldMat4Copy` /
|
|
3014
|
-
//
|
|
2681
|
+
// `_getArrayView` reads comes from `row.entity` -- the
|
|
3015
2682
|
// archetype-graph back-door (`graph.archetypes` / `arch.components.some`) is gone.
|
|
3016
2683
|
// Plan-decisions K-2 sniffing scheme B (archetype-edge sniff once via
|
|
3017
|
-
// `row.get(X) !== undefined`); K-3 invariant preserved (
|
|
2684
|
+
// `row.get(X) !== undefined`); K-3 invariant preserved (`_getArrayView`
|
|
3018
2685
|
// calls survive untouched, only the entity source changes).
|
|
2686
|
+
const worldInternal = createWorldInternalView(world);
|
|
2687
|
+
|
|
3019
2688
|
const cameras = extractCameraSnapshots(world);
|
|
3020
2689
|
|
|
3021
2690
|
// Three-query union (M2 / w16 / AC-03): directional has no Transform
|
|
@@ -3042,14 +2711,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3042
2711
|
| undefined;
|
|
3043
2712
|
for (const row of directionalLightQuery) {
|
|
3044
2713
|
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
2714
|
directionalCount += 1;
|
|
3054
2715
|
const intensity = l.intensity;
|
|
3055
2716
|
const snapshot: DirectionalLightSnapshot = {
|
|
@@ -3092,24 +2753,21 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3092
2753
|
.unwrap();
|
|
3093
2754
|
for (const row of pointLightQuery) {
|
|
3094
2755
|
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
2756
|
// K-2 scheme B: archetype-edge sniff -- `bundle.Transform` key is absent
|
|
3100
2757
|
// when the archetype does not carry the Transform column.
|
|
3101
|
-
const
|
|
2758
|
+
const hasTransform = row.get(Transform) !== undefined;
|
|
3102
2759
|
const intensity = p.intensity;
|
|
3103
2760
|
const range = p.range;
|
|
3104
2761
|
const entityId = row.entity;
|
|
3105
2762
|
// Position = world-space translation extracted from Transform.world.
|
|
3106
2763
|
// A point light archetype without a Transform column sits at the origin.
|
|
3107
2764
|
let worldMat: Float32Array | undefined;
|
|
3108
|
-
if (
|
|
2765
|
+
if (hasTransform) {
|
|
2766
|
+
const view = worldInternal._getArrayView(entityId, Transform, 'world');
|
|
2767
|
+
if (view !== undefined) worldMat = new Float32Array(view);
|
|
2768
|
+
}
|
|
3109
2769
|
const position =
|
|
3110
|
-
worldMat !== undefined
|
|
3111
|
-
? mat4.getTranslation(vec3.create(), worldMat as unknown as mat4.Mat4Like)
|
|
3112
|
-
: vec3.create(0, 0, 0);
|
|
2770
|
+
worldMat !== undefined ? mat4.getTranslation(vec3.create(), worldMat) : vec3.create(0, 0, 0);
|
|
3113
2771
|
pointSnapshots.push({
|
|
3114
2772
|
kind: 'point',
|
|
3115
2773
|
position,
|
|
@@ -3137,21 +2795,19 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3137
2795
|
let spotTileNext = 0;
|
|
3138
2796
|
for (const row of spotLightQuery) {
|
|
3139
2797
|
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);
|
|
2798
|
+
const hasTransform = row.get(Transform) !== undefined;
|
|
3145
2799
|
const intensity = s.intensity;
|
|
3146
2800
|
const range = s.range;
|
|
3147
2801
|
const innerConeDeg = s.innerConeDeg;
|
|
3148
2802
|
const outerConeDeg = s.outerConeDeg;
|
|
3149
2803
|
let worldMat: Float32Array | undefined;
|
|
3150
|
-
if (
|
|
2804
|
+
if (hasTransform) {
|
|
2805
|
+
const entity = row.entity;
|
|
2806
|
+
const view = worldInternal._getArrayView(entity, Transform, 'world');
|
|
2807
|
+
if (view !== undefined) worldMat = new Float32Array(view);
|
|
2808
|
+
}
|
|
3151
2809
|
const position =
|
|
3152
|
-
worldMat !== undefined
|
|
3153
|
-
? mat4.getTranslation(vec3.create(), worldMat as unknown as mat4.Mat4Like)
|
|
3154
|
-
: vec3.create(0, 0, 0);
|
|
2810
|
+
worldMat !== undefined ? mat4.getTranslation(vec3.create(), worldMat) : vec3.create(0, 0, 0);
|
|
3155
2811
|
const dir = vec3.create(s.direction[0] ?? 0, s.direction[1] ?? -1, s.direction[2] ?? 0);
|
|
3156
2812
|
|
|
3157
2813
|
// Extract is the single direction-normalization owner for direct-light
|
|
@@ -3161,6 +2817,19 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3161
2817
|
);
|
|
3162
2818
|
const EPSILON = 1e-6;
|
|
3163
2819
|
const hasValidDirection = dirLen > EPSILON;
|
|
2820
|
+
if (!hasValidDirection) {
|
|
2821
|
+
worldInternal._routeError(
|
|
2822
|
+
new SpawnLightInvalidBoundsError('SpotLight', 'direction', [
|
|
2823
|
+
dir[0] ?? 0,
|
|
2824
|
+
dir[1] ?? 0,
|
|
2825
|
+
dir[2] ?? 0,
|
|
2826
|
+
]),
|
|
2827
|
+
{
|
|
2828
|
+
severity: Severity.Error,
|
|
2829
|
+
systemName: 'RenderSystem.extract (spot-direction)',
|
|
2830
|
+
},
|
|
2831
|
+
);
|
|
2832
|
+
}
|
|
3164
2833
|
const dirN = vec3.create(
|
|
3165
2834
|
hasValidDirection ? (dir[0] ?? 0) / dirLen : (dir[0] ?? 0),
|
|
3166
2835
|
hasValidDirection ? (dir[1] ?? 0) / dirLen : (dir[1] ?? 0),
|
|
@@ -3203,7 +2872,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3203
2872
|
|
|
3204
2873
|
spotSnapshots.push({
|
|
3205
2874
|
kind: 'spot',
|
|
3206
|
-
entity: row.entity,
|
|
3207
2875
|
// D-6: position reflects world transform; direction stays sourced
|
|
3208
2876
|
// from SpotLight.direction (NOT rotated by the parent).
|
|
3209
2877
|
position,
|
|
@@ -3276,7 +2944,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3276
2944
|
cascadeBlend: sf.cascadeBlend,
|
|
3277
2945
|
mapSize: sf.mapSize,
|
|
3278
2946
|
shadowDistance: sf.shadowDistance,
|
|
3279
|
-
pcfKernelSize: sf.pcfKernelSize,
|
|
3280
2947
|
};
|
|
3281
2948
|
directionalCsmDirection = dirSnapshot.direction;
|
|
3282
2949
|
const csm = computeDirectionalCsm(dirSnapshot.direction, directionalCsmConfig, cameraData);
|
|
@@ -3327,14 +2994,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3327
2994
|
for (const row of pointShadowQuery) {
|
|
3328
2995
|
const t = row.get(Transform);
|
|
3329
2996
|
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
2997
|
// Read world-space position from Transform.world (mat4 column-major;
|
|
3339
2998
|
// translation lives at indices 12..14, mirroring CameraSnapshot.world
|
|
3340
2999
|
// semantics in this file).
|
|
@@ -3400,7 +3059,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3400
3059
|
}
|
|
3401
3060
|
}
|
|
3402
3061
|
|
|
3403
|
-
|
|
3062
|
+
const lights: ExtractedLights = {
|
|
3404
3063
|
directional,
|
|
3405
3064
|
directionalCount,
|
|
3406
3065
|
point: pointSnapshots,
|
|
@@ -3485,56 +3144,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3485
3144
|
}
|
|
3486
3145
|
}
|
|
3487
3146
|
|
|
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
3147
|
// feat-20260528-frustum-culling M3 / w10: precompute per-camera frustum
|
|
3539
3148
|
// planes so entities can be tested against all cameras in the inner loop.
|
|
3540
3149
|
// Cameras with degenerate projection parameters (e.g. zero fov, zero aspect)
|
|
@@ -3580,7 +3189,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3580
3189
|
// carried in the world basis columns; the cull frustum uses the same view
|
|
3581
3190
|
// the record stage derives, so cull stays same-source with render (AC-05).
|
|
3582
3191
|
const view = mat4.create();
|
|
3583
|
-
mat4.invert(view, cam.world
|
|
3192
|
+
mat4.invert(view, cam.world);
|
|
3584
3193
|
const vp = mat4.create();
|
|
3585
3194
|
mat4.multiply(vp, proj, view);
|
|
3586
3195
|
const f = frustum.create();
|
|
@@ -3591,7 +3200,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3591
3200
|
|
|
3592
3201
|
const renderables: RenderableSnapshot[] = [];
|
|
3593
3202
|
// feat-20260528-frustum-culling M3 / w11: frustum culling counters.
|
|
3594
|
-
const candidateRenderables: RenderableSnapshot[] = [];
|
|
3595
3203
|
let frustumCulled = 0;
|
|
3596
3204
|
let frustumTotal = 0;
|
|
3597
3205
|
const explicitlyHidden = new Set<EntityHandle>();
|
|
@@ -3614,7 +3222,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3614
3222
|
// (`row.get(X) !== undefined` edge sniff) replaces the prior
|
|
3615
3223
|
// `arch.components.some` row-internal back-door. K-3 invariant: the
|
|
3616
3224
|
// variable-length array reads (`MeshRenderer.materials`,
|
|
3617
|
-
// `Instances.transforms`) still flow through
|
|
3225
|
+
// `Instances.transforms`) still flow through `_getArrayView` /
|
|
3618
3226
|
// `world.get(e, Instances)` -- only the `entity` source switches to
|
|
3619
3227
|
// `row.entity`.
|
|
3620
3228
|
//
|
|
@@ -3637,46 +3245,53 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3637
3245
|
MeshFilter,
|
|
3638
3246
|
Instances,
|
|
3639
3247
|
Skin,
|
|
3640
|
-
MorphWeights,
|
|
3641
3248
|
Layer,
|
|
3249
|
+
MorphWeights,
|
|
3642
3250
|
SpriteRegionOverride,
|
|
3643
3251
|
SpriteInstances,
|
|
3252
|
+
Points,
|
|
3253
|
+
Lines,
|
|
3254
|
+
SortKey,
|
|
3644
3255
|
],
|
|
3645
3256
|
})
|
|
3646
3257
|
.unwrap();
|
|
3647
|
-
// QueryRow deliberately exposes component data, not
|
|
3648
|
-
//
|
|
3649
|
-
//
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3258
|
+
// QueryRow deliberately exposes component data, not an archetype version.
|
|
3259
|
+
// The optimized ECS owner keeps table storage private, so instance-buffer
|
|
3260
|
+
// invalidation uses a content fingerprint of the managed arrays instead.
|
|
3261
|
+
const resolveArchVersion = (entity: EntityHandle): number => {
|
|
3262
|
+
const values = [
|
|
3263
|
+
worldInternal._getArrayView(entity, Instances, 'transforms'),
|
|
3264
|
+
worldInternal._getArrayView(entity, SpriteInstances, 'transforms'),
|
|
3265
|
+
worldInternal._getArrayView(entity, SpriteInstances, 'regions'),
|
|
3266
|
+
];
|
|
3267
|
+
let hash = 2166136261;
|
|
3268
|
+
for (const value of values) {
|
|
3269
|
+
hash = Math.imul(hash ^ (value?.length ?? 0), 16777619) >>> 0;
|
|
3270
|
+
if (value === undefined) continue;
|
|
3271
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
3272
|
+
hash = Math.imul(hash ^ Math.fround(value[index] ?? 0), 16777619) >>> 0;
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
return hash;
|
|
3276
|
+
};
|
|
3277
|
+
// Pending entries are created with the current renderables.length, which is
|
|
3278
|
+
// exactly the slot this entity receives if it survives culling. No other
|
|
3279
|
+
// renderable can be pushed between staging and this entity's push, so publish
|
|
3280
|
+
// the original fresh entries instead of cloning every submesh descriptor.
|
|
3281
|
+
// A culled entity still discards its private pending array unchanged.
|
|
3659
3282
|
const flushPendingDispatch = (pending: readonly DispatchEntry[]): void => {
|
|
3660
|
-
for (const entry of pending)
|
|
3283
|
+
for (const entry of pending) dispatch.push(entry);
|
|
3661
3284
|
};
|
|
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) {
|
|
3285
|
+
for (const row of meshRendererQuery) {
|
|
3670
3286
|
// K-2 archetype-edge sniff (scheme B): a missing optional component
|
|
3671
3287
|
// surfaces as an absent bundle key, not a row-internal optional chain.
|
|
3672
3288
|
// Presence-only checks use QueryRow.has so large array-bearing components
|
|
3673
3289
|
// are not materialised merely to answer a boolean question.
|
|
3674
|
-
const
|
|
3675
|
-
const hasTransform = transformData !== undefined;
|
|
3290
|
+
const hasTransform = row.has(Transform);
|
|
3676
3291
|
const meshFilter = row.get(MeshFilter);
|
|
3677
3292
|
const hasInstances = row.has(Instances);
|
|
3678
3293
|
const skin = row.get(Skin);
|
|
3679
|
-
const
|
|
3294
|
+
const morphWeightsView = worldInternal._getArrayView(row.entity, MorphWeights, 'weights');
|
|
3680
3295
|
const hasMeshFilter = meshFilter !== undefined;
|
|
3681
3296
|
const hasSkin = skin !== undefined;
|
|
3682
3297
|
// feat-20260625-sprite-instances-and-tilemap-terrain-static-batch M3 / w10:
|
|
@@ -3689,6 +3304,11 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3689
3304
|
// - 'sprite-instances-count-mismatch'
|
|
3690
3305
|
// (transforms.length / 16 !== regions.length / 4) — stride pair desync.
|
|
3691
3306
|
const hasSpriteInstances = row.has(SpriteInstances);
|
|
3307
|
+
const points = row.get(Points);
|
|
3308
|
+
const lines = row.get(Lines);
|
|
3309
|
+
const sortKey = row.get(SortKey)?.value;
|
|
3310
|
+
const pointsLinesComponent =
|
|
3311
|
+
points !== undefined ? 'Points' : lines !== undefined ? 'Lines' : undefined;
|
|
3692
3312
|
const isRenderable = hasTransform && hasMeshFilter;
|
|
3693
3313
|
|
|
3694
3314
|
// feat-20260601 D-3: the resolved world transform is read per-entity from
|
|
@@ -3705,8 +3325,8 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3705
3325
|
// feat-20260608-tilemap-object-layer-rendering M3 / m3-t5: tilemap-spawned
|
|
3706
3326
|
// per-cell render entities (the ones `tilemap-chunk-extract-system`
|
|
3707
3327
|
// pushes via `spawnDerivedRenderEntities`) reach this loop via the same
|
|
3708
|
-
// archetype edge that carries
|
|
3709
|
-
//
|
|
3328
|
+
// archetype edge that carries a sprite entity -- they all wear
|
|
3329
|
+
// `MeshFilter.assetHandle === HANDLE_QUAD` + a `forgeax::sprite`-shaded
|
|
3710
3330
|
// material asset + the sprite-bucket `values.region` rectangle.
|
|
3711
3331
|
// For the per-entity Y-sort path (requirements §AC-12 / §AC-13):
|
|
3712
3332
|
//
|
|
@@ -3721,7 +3341,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3721
3341
|
// bucket types therefore feed one `transparentSortEntries` argsort
|
|
3722
3342
|
// step + share the `argsortInPlace` radix LSD primitive (plan-strategy
|
|
3723
3343
|
// §D-1 / §D-3). The detection lives on the material side -- detect a
|
|
3724
|
-
// tilemap-spawned entity by
|
|
3344
|
+
// tilemap-spawned entity by `MeshFilter.assetHandle === HANDLE_QUAD`
|
|
3725
3345
|
// plus the `forgeax::sprite` shader id on `MeshRenderer.material`'s
|
|
3726
3346
|
// first pass + non-empty `values.region`; no new public ECS
|
|
3727
3347
|
// marker component lands (charter F1 minimum surface).
|
|
@@ -3739,10 +3359,10 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3739
3359
|
// sprite reduces the anchor budget to 0.5 rather than the asset's 1.0.
|
|
3740
3360
|
//
|
|
3741
3361
|
// SpriteRegionOverride.region is a fixed stride-4 value. Per-row reads
|
|
3742
|
-
// route through
|
|
3362
|
+
// route through `_getArrayView` for the zero-copy row window
|
|
3743
3363
|
// row-window slice (consistent with the variable-length array column
|
|
3744
|
-
// reads -- K-3 carve-out keeps
|
|
3745
|
-
// record for
|
|
3364
|
+
// reads -- K-3 carve-out keeps `_getArrayView` as the row-accessor of
|
|
3365
|
+
// record for each non-scalar column).
|
|
3746
3366
|
const hasSpriteRegionOverride = row.has(SpriteRegionOverride);
|
|
3747
3367
|
// feat-20260523-skin-skeleton-animation M2 / T-21: Skin component
|
|
3748
3368
|
// column views for coexistence check + joint despawn fail-fast.
|
|
@@ -3754,11 +3374,11 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3754
3374
|
if (hasInstances || hasSpriteInstances) {
|
|
3755
3375
|
// All rows in this callback share one archetype. Only these two
|
|
3756
3376
|
// instance-bearing paths consume the version in their cache key.
|
|
3757
|
-
archVersion =
|
|
3377
|
+
archVersion = resolveArchVersion(row.entity);
|
|
3758
3378
|
}
|
|
3759
3379
|
|
|
3760
3380
|
{
|
|
3761
|
-
// feat-20260608 M2 / w11: read materials array via
|
|
3381
|
+
// feat-20260608 M2 / w11: read materials array via _getArrayView
|
|
3762
3382
|
const entity = row.entity;
|
|
3763
3383
|
if (isRenderable && visibility?.effective(entity) === 'hidden') {
|
|
3764
3384
|
explicitlyHidden.add(entity);
|
|
@@ -3777,9 +3397,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3777
3397
|
// the deferred-shading smoke (PR #598 CI). Pure ordering fix; cull
|
|
3778
3398
|
// logic and MeshRenderer contract unchanged.
|
|
3779
3399
|
const pendingDispatch: DispatchEntry[] = [];
|
|
3780
|
-
const
|
|
3781
|
-
|
|
3782
|
-
|
|
3400
|
+
const materialsView = worldInternal._getArrayView(entity, MeshRenderer, 'materials') as
|
|
3401
|
+
| Uint32Array
|
|
3402
|
+
| undefined;
|
|
3783
3403
|
const materialCount = materialsView?.length ?? 0;
|
|
3784
3404
|
let materialHandles = Array.from(materialsView ?? []);
|
|
3785
3405
|
let materialBindingSources: MeshMaterialBindingSource[] = materialHandles.map(
|
|
@@ -3798,7 +3418,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3798
3418
|
assets !== null
|
|
3799
3419
|
) {
|
|
3800
3420
|
const meshHandle = toShared<'MeshAsset'>(fAssetHandleVal);
|
|
3801
|
-
const meshRes =
|
|
3421
|
+
const meshRes = resolveAssetHandle<Asset>(world, meshHandle);
|
|
3802
3422
|
if (meshRes.ok && meshRes.value.kind === 'mesh') {
|
|
3803
3423
|
const meshAsset = meshRes.value as MeshAsset;
|
|
3804
3424
|
gpuDrivenSubmeshes = meshAsset.submeshes;
|
|
@@ -3813,7 +3433,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3813
3433
|
const guid = (meshRes.value as { guid?: string }).guid ?? '<no-guid>';
|
|
3814
3434
|
const resolvedBindings = resolveMeshMaterialBindings(meshAsset, materialsView ?? [], {
|
|
3815
3435
|
isValidOverride(handle) {
|
|
3816
|
-
const resolved =
|
|
3436
|
+
const resolved = resolveAssetHandle(world, toShared<'MaterialAsset'>(handle));
|
|
3817
3437
|
return resolved.ok && resolved.value.kind === 'material';
|
|
3818
3438
|
},
|
|
3819
3439
|
resolveMeshDefault(defaultGuid) {
|
|
@@ -3826,7 +3446,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3826
3446
|
});
|
|
3827
3447
|
if (!resolvedBindings.ok) {
|
|
3828
3448
|
if (resolvedBindings.code === 'mesh-material-slots-missing') {
|
|
3829
|
-
|
|
3449
|
+
worldInternal._routeError(
|
|
3830
3450
|
new AssetError({
|
|
3831
3451
|
code: 'load-failed',
|
|
3832
3452
|
expected: 'every MeshAsset producer supplies materialSlots[]',
|
|
@@ -3837,19 +3457,20 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3837
3457
|
subAssetGuid: '<material-slots-missing>',
|
|
3838
3458
|
sourceField: { fieldName: 'materialSlots' },
|
|
3839
3459
|
},
|
|
3840
|
-
})
|
|
3460
|
+
}),
|
|
3841
3461
|
{
|
|
3462
|
+
severity: Severity.Error,
|
|
3842
3463
|
systemName: 'RenderSystem.extract (mesh-material-slots-missing)',
|
|
3843
3464
|
},
|
|
3844
3465
|
);
|
|
3845
3466
|
continue;
|
|
3846
3467
|
}
|
|
3847
3468
|
const materialGuid = AssetGuid.format(resolvedBindings.defaultMaterial);
|
|
3848
|
-
|
|
3469
|
+
worldInternal._routeError(
|
|
3849
3470
|
new AssetError({
|
|
3850
3471
|
code: 'load-failed',
|
|
3851
3472
|
expected: `MeshAsset materialSlots[${resolvedBindings.slotIndex}] default ${materialGuid} is ready and a MaterialAsset`,
|
|
3852
|
-
hint: `
|
|
3473
|
+
hint: `loadByGuid(meshGuid) must recursively load the declared default material; mesh=${guid}, slot=${resolvedBindings.slotIndex}, material=${materialGuid}`,
|
|
3853
3474
|
detail: {
|
|
3854
3475
|
referencedByGuid: guid,
|
|
3855
3476
|
referencedByKind: 'mesh',
|
|
@@ -3859,8 +3480,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3859
3480
|
arrayIndex: resolvedBindings.slotIndex,
|
|
3860
3481
|
},
|
|
3861
3482
|
},
|
|
3862
|
-
})
|
|
3483
|
+
}),
|
|
3863
3484
|
{
|
|
3485
|
+
severity: Severity.Error,
|
|
3864
3486
|
systemName: 'RenderSystem.extract (mesh-default-not-ready)',
|
|
3865
3487
|
},
|
|
3866
3488
|
);
|
|
@@ -3884,7 +3506,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3884
3506
|
},
|
|
3885
3507
|
}));
|
|
3886
3508
|
for (const diagnostic of resolvedBindings.diagnostics) {
|
|
3887
|
-
|
|
3509
|
+
worldInternal._routeError(
|
|
3888
3510
|
new AssetError({
|
|
3889
3511
|
code: diagnostic.code,
|
|
3890
3512
|
expected:
|
|
@@ -3904,8 +3526,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3904
3526
|
slotIndex: diagnostic.slotIndex,
|
|
3905
3527
|
handle: diagnostic.handle ?? 0,
|
|
3906
3528
|
},
|
|
3907
|
-
})
|
|
3529
|
+
}),
|
|
3908
3530
|
{
|
|
3531
|
+
severity: Severity.Warning,
|
|
3909
3532
|
systemName: `RenderSystem.extract (${diagnostic.code})`,
|
|
3910
3533
|
},
|
|
3911
3534
|
);
|
|
@@ -3941,7 +3564,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3941
3564
|
cachedMaterial.passes,
|
|
3942
3565
|
entity,
|
|
3943
3566
|
handleRaw,
|
|
3944
|
-
|
|
3567
|
+
renderables.length,
|
|
3945
3568
|
layerVal,
|
|
3946
3569
|
materialSnap.paramSnapshot,
|
|
3947
3570
|
);
|
|
@@ -3968,22 +3591,23 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3968
3591
|
stablePersistentCached.passes,
|
|
3969
3592
|
entity,
|
|
3970
3593
|
handleRaw,
|
|
3971
|
-
|
|
3594
|
+
renderables.length,
|
|
3972
3595
|
layerVal,
|
|
3973
3596
|
materialSnap.paramSnapshot,
|
|
3974
3597
|
);
|
|
3975
3598
|
}
|
|
3976
3599
|
} else {
|
|
3977
|
-
const res =
|
|
3600
|
+
const res = resolveAssetHandle(world, tagged);
|
|
3978
3601
|
if (!res.ok) {
|
|
3979
3602
|
if (isRenderable) {
|
|
3980
3603
|
const rhiErr = new RhiError({
|
|
3981
3604
|
code: 'asset-not-registered',
|
|
3982
|
-
expected: 'MeshRenderer.material in
|
|
3605
|
+
expected: 'MeshRenderer.material in AssetRegistry',
|
|
3983
3606
|
hint: 'catalog the material via assetRegistry.catalog(guid, asset) + world.allocSharedRef before spawn, or remove the material field to fall back to default',
|
|
3984
3607
|
detail: { assetHandle: handleRaw },
|
|
3985
3608
|
});
|
|
3986
|
-
|
|
3609
|
+
worldInternal._routeError(rhiErr, {
|
|
3610
|
+
severity: Severity.Error,
|
|
3987
3611
|
systemName: 'RenderSystem.extract (material asset-not-registered)',
|
|
3988
3612
|
});
|
|
3989
3613
|
}
|
|
@@ -3993,12 +3617,11 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
3993
3617
|
if (asset.kind !== 'material') {
|
|
3994
3618
|
materialSnap = defaultMaterialSnapshot(handleRaw);
|
|
3995
3619
|
} else {
|
|
3996
|
-
const materialGuid = assets.guidOf(asset);
|
|
3997
3620
|
// feat-20260529 M3 / w11: material parent chain inheritance via
|
|
3998
3621
|
// read-through _materialWalk accessor (plan-strategy D-6).
|
|
3999
3622
|
// The old direct asset.passes / asset.values read never
|
|
4000
3623
|
// walked the parent chain, causing broken-inheritance (root cause).
|
|
4001
|
-
const resolvedResult =
|
|
3624
|
+
const resolvedResult = walkMaterialPassesOverSharedRefs(world, tagged, assets);
|
|
4002
3625
|
if (!resolvedResult.ok) {
|
|
4003
3626
|
// AC-09 / S-7 / q8=A: passes-empty or cycle must fire structured
|
|
4004
3627
|
// error through _routeError (same routing as asset-not-registered
|
|
@@ -4011,26 +3634,29 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4011
3634
|
case 'material-value-unknown':
|
|
4012
3635
|
case 'material-value-type-mismatch':
|
|
4013
3636
|
case 'material-contract-program-mismatch':
|
|
4014
|
-
|
|
3637
|
+
worldInternal._routeError(err, {
|
|
3638
|
+
severity: Severity.Error,
|
|
4015
3639
|
systemName: `RenderSystem.extract (${err.code})`,
|
|
4016
3640
|
});
|
|
4017
3641
|
break;
|
|
4018
3642
|
case 'material-circular-inheritance':
|
|
4019
|
-
|
|
3643
|
+
worldInternal._routeError(err, {
|
|
3644
|
+
severity: Severity.Error,
|
|
4020
3645
|
systemName: 'RenderSystem.extract (material-circular-inheritance)',
|
|
4021
3646
|
});
|
|
4022
3647
|
break;
|
|
4023
3648
|
default:
|
|
4024
3649
|
// Exhaustive guard: unhandled error codes from _materialWalk
|
|
4025
3650
|
// surface an internal assertion to avoid silent continuation.
|
|
4026
|
-
|
|
3651
|
+
worldInternal._routeError(err, {
|
|
3652
|
+
severity: Severity.Error,
|
|
4027
3653
|
systemName: `RenderSystem.extract (_materialWalk: ${err.code})`,
|
|
4028
3654
|
});
|
|
4029
3655
|
}
|
|
4030
3656
|
continue;
|
|
4031
3657
|
}
|
|
4032
3658
|
const resolved = resolvedResult.value;
|
|
4033
|
-
const allPasses = resolved.passes
|
|
3659
|
+
const allPasses = resolved.passes;
|
|
4034
3660
|
const firstPassShader =
|
|
4035
3661
|
allPasses.length > 0
|
|
4036
3662
|
? runtimeMaterialShaderId(allPasses[0]?.program.module, allPasses[0]?.name)
|
|
@@ -4064,50 +3690,10 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4064
3690
|
}
|
|
4065
3691
|
}
|
|
4066
3692
|
|
|
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
|
-
}
|
|
3693
|
+
const materialParamSchema = materialParametersToParamSchema(
|
|
3694
|
+
resolved.parameters ?? [],
|
|
3695
|
+
firstPassShader,
|
|
3696
|
+
);
|
|
4111
3697
|
// feat-20260611-fox-skinning-vertex-attribute-chain M4 / w17 (D-5):
|
|
4112
3698
|
// bidirectional Skin <-> pbr-skin material fail-fast at extract.
|
|
4113
3699
|
// Skin component without a forgeax::pbr-skin first-pass material
|
|
@@ -4126,20 +3712,15 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4126
3712
|
skinSkeletonView !== 0;
|
|
4127
3713
|
const isPbrSkinMaterial = firstPassShader === 'forgeax::pbr-skin';
|
|
4128
3714
|
if (hasSkinSkel && !isPbrSkinMaterial) {
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
) as unknown as Error,
|
|
4134
|
-
{
|
|
4135
|
-
systemName: 'RenderSystem.extract (skin-material-mismatch)',
|
|
4136
|
-
},
|
|
4137
|
-
);
|
|
3715
|
+
worldInternal._routeError(new SkinMaterialMismatchError(entity, firstPassShader), {
|
|
3716
|
+
severity: Severity.Error,
|
|
3717
|
+
systemName: 'RenderSystem.extract (skin-material-mismatch)',
|
|
3718
|
+
});
|
|
4138
3719
|
continue;
|
|
4139
3720
|
}
|
|
4140
3721
|
if (isPbrSkinMaterial && fAssetHandleVal !== undefined && fAssetHandleVal !== 0) {
|
|
4141
3722
|
const meshHandleForSkinCheck = toShared<'MeshAsset'>(fAssetHandleVal);
|
|
4142
|
-
const meshResForSkinCheck =
|
|
3723
|
+
const meshResForSkinCheck = resolveAssetHandle<MeshAsset>(
|
|
4143
3724
|
world,
|
|
4144
3725
|
meshHandleForSkinCheck,
|
|
4145
3726
|
);
|
|
@@ -4150,15 +3731,10 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4150
3731
|
if (!hasSkinIdx || !hasSkinWt) {
|
|
4151
3732
|
const missing: 'skinIndex' | 'skinWeight' | 'both' =
|
|
4152
3733
|
!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
|
-
);
|
|
3734
|
+
worldInternal._routeError(new MaterialSkinAttrMissingError(entity, missing), {
|
|
3735
|
+
severity: Severity.Error,
|
|
3736
|
+
systemName: 'RenderSystem.extract (material-skin-attr-missing)',
|
|
3737
|
+
});
|
|
4162
3738
|
continue;
|
|
4163
3739
|
}
|
|
4164
3740
|
}
|
|
@@ -4183,32 +3759,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4183
3759
|
const isSprite =
|
|
4184
3760
|
firstPassShader === 'forgeax::sprite' || firstPassShader === 'forgeax::sprite-lit';
|
|
4185
3761
|
|
|
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
3762
|
// feat-20260613-material-paramschema-driven-binding M4 / w23
|
|
4213
3763
|
// (D-5 graceful): paramSchema-driven texture-field validation.
|
|
4214
3764
|
// For each handle-shaped paramValue (typeof === 'number'),
|
|
@@ -4232,7 +3782,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4232
3782
|
if (assets === null || assets === undefined) return undefined;
|
|
4233
3783
|
// M4: intern so the GUID mints one stable handle per World
|
|
4234
3784
|
// instead of a fresh slot every frame (GPU residency relies on
|
|
4235
|
-
// a stable handleSlot).
|
|
3785
|
+
// a stable handleSlot). onLastRelease -> gpuStore.evictTexture.
|
|
4236
3786
|
const interned = internSharedRefFromGuid(
|
|
4237
3787
|
world,
|
|
4238
3788
|
assets,
|
|
@@ -4242,34 +3792,34 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4242
3792
|
if (interned === undefined) return undefined;
|
|
4243
3793
|
handle = interned;
|
|
4244
3794
|
} else if (typeof textureRef === 'number') {
|
|
4245
|
-
handle =
|
|
3795
|
+
handle = toShared<'TextureAsset'>(textureRef);
|
|
4246
3796
|
} else {
|
|
4247
3797
|
return undefined;
|
|
4248
3798
|
}
|
|
4249
3799
|
if (assets === null || assets === undefined) return handle;
|
|
4250
|
-
const declaredFields =
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
3800
|
+
const declaredFields = materialTextureFields(
|
|
3801
|
+
firstPassShader,
|
|
3802
|
+
materialParamSchema.length > 0
|
|
3803
|
+
? derive(materialParamSchema).textureFieldNames
|
|
3804
|
+
: firstPassShader !== undefined
|
|
3805
|
+
? assets.materialShaderTextureFieldNames(firstPassShader)
|
|
3806
|
+
: undefined,
|
|
3807
|
+
);
|
|
4257
3808
|
// Shader not registered (R-4 cross-worktree path) -> trust the
|
|
4258
|
-
// raw handle and let the record stage / GPU layer surface
|
|
3809
|
+
// raw handle and let the record stage / GPU layer surface each
|
|
4259
3810
|
// mismatch via MISSING_TEXTURE_HANDLE.
|
|
4260
3811
|
if (declaredFields === undefined) return handle;
|
|
4261
3812
|
// Field is not declared as a texture by the shader -> the
|
|
4262
3813
|
// loader's "try every int" fallback misclassified a scalar;
|
|
4263
3814
|
// drop the slot so the record stage uses the default white.
|
|
4264
|
-
|
|
4265
|
-
declaredFields
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
if (!declared) {
|
|
3815
|
+
if (
|
|
3816
|
+
!declaredFields.has(fieldName) &&
|
|
3817
|
+
!isEngineInjectedTextureField(firstPassShader, fieldName)
|
|
3818
|
+
) {
|
|
4269
3819
|
return undefined;
|
|
4270
3820
|
}
|
|
4271
3821
|
// Field declared as texture: verify the handle's asset kind.
|
|
4272
|
-
const assetRes =
|
|
3822
|
+
const assetRes = resolveAssetHandle(world, handle);
|
|
4273
3823
|
if (!assetRes.ok) return undefined;
|
|
4274
3824
|
const kind = (assetRes.value as { kind?: string }).kind;
|
|
4275
3825
|
if (kind !== 'texture') return undefined;
|
|
@@ -4283,7 +3833,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4283
3833
|
brand: B,
|
|
4284
3834
|
): Handle<B, 'shared'> | undefined => {
|
|
4285
3835
|
const value = materialTextureRef(raw);
|
|
4286
|
-
if (typeof value === 'number') return
|
|
3836
|
+
if (typeof value === 'number') return toShared<B>(value);
|
|
4287
3837
|
const guid = assetReferenceText(value);
|
|
4288
3838
|
if (guid !== undefined) {
|
|
4289
3839
|
if (assets === null || assets === undefined) return undefined;
|
|
@@ -4299,15 +3849,17 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4299
3849
|
// carried, replacing the hardcoded 3-field list. validateTextureHandle
|
|
4300
3850
|
// already drops fields a shader doesn't declare as a texture.
|
|
4301
3851
|
const userRegionFields =
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
(
|
|
3852
|
+
materialTextureFields(
|
|
3853
|
+
firstPassShader,
|
|
3854
|
+
materialParamSchema.length > 0
|
|
3855
|
+
? derive(materialParamSchema).textureFieldNames
|
|
3856
|
+
: firstPassShader !== undefined && assets !== null && assets !== undefined
|
|
4306
3857
|
? assets.materialShaderTextureFieldNames(firstPassShader)
|
|
4307
|
-
: undefined
|
|
3858
|
+
: undefined,
|
|
3859
|
+
) ?? BUILTIN_USER_REGION_TEXTURE_FIELDS;
|
|
4308
3860
|
const textureHandles = new Map<string, Handle<'TextureAsset', 'shared'>>();
|
|
4309
3861
|
const videoTextureFields = new Map<string, Handle<'VideoAsset', 'shared'>>();
|
|
4310
|
-
for (const field of userRegionFields
|
|
3862
|
+
for (const field of userRegionFields) {
|
|
4311
3863
|
// D-5: a video-kind paramValue routes to the transient path
|
|
4312
3864
|
// (videoTextureFields), NOT validateTextureHandle (which drops
|
|
4313
3865
|
// kind!=='texture', the R-7 silent-fail path). Static fields fall
|
|
@@ -4329,20 +3881,15 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4329
3881
|
const samplerHandles = collectMaterialTextureSamplers(pv, (value) =>
|
|
4330
3882
|
resolveParamHandle(materialTextureRef(value), 'SamplerAsset'),
|
|
4331
3883
|
);
|
|
4332
|
-
const emissiveTextureHandle =
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
pv,
|
|
4336
|
-
materialParamSchema,
|
|
4337
|
-
derivedMaterialInterface,
|
|
3884
|
+
const emissiveTextureHandle = validateTextureHandle(
|
|
3885
|
+
'emissiveTexture',
|
|
3886
|
+
pv.emissiveTexture,
|
|
4338
3887
|
);
|
|
4339
|
-
const
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
}
|
|
4345
|
-
}
|
|
3888
|
+
const occlusionTextureHandle = validateTextureHandle(
|
|
3889
|
+
'occlusionTexture',
|
|
3890
|
+
pv.occlusionTexture,
|
|
3891
|
+
);
|
|
3892
|
+
const textureCoordinates = collectMaterialTextureCoordinates(pv);
|
|
4346
3893
|
const emissivePv = pv.emissive as readonly number[] | undefined;
|
|
4347
3894
|
// feat-20260625 M2 / w6: first-pass transparency flag folds into
|
|
4348
3895
|
// MaterialSnapshot.transparent so the record stage can drive the
|
|
@@ -4369,10 +3916,11 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4369
3916
|
// SpriteRegionOverride: per-entity per-frame region displacement.
|
|
4370
3917
|
let overrideRegion: readonly [number, number, number, number] | undefined;
|
|
4371
3918
|
if (hasSpriteRegionOverride) {
|
|
4372
|
-
const
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
3919
|
+
const overrideView = worldInternal._getArrayView(
|
|
3920
|
+
entity,
|
|
3921
|
+
SpriteRegionOverride,
|
|
3922
|
+
'region',
|
|
3923
|
+
) as Float32Array | undefined;
|
|
4376
3924
|
if (overrideView !== undefined && overrideView.length >= 4) {
|
|
4377
3925
|
overrideRegion = [
|
|
4378
3926
|
overrideView[0] ?? 0,
|
|
@@ -4402,36 +3950,18 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4402
3950
|
regionY += regionW;
|
|
4403
3951
|
regionW = -regionW;
|
|
4404
3952
|
}
|
|
4405
|
-
paramSnap.region = [regionX, regionY, regionZ, regionW]
|
|
3953
|
+
paramSnap.region = [regionX, regionY, regionZ, regionW];
|
|
4406
3954
|
// Guard: slicesAndMode must be present and zero for non-9-slice
|
|
4407
3955
|
// sprites so the record-stage UBO writer (applyParamSnapshotToUbo)
|
|
4408
|
-
// writes [0,0,0,0]
|
|
4409
|
-
//
|
|
4410
|
-
//
|
|
4411
|
-
// geometry
|
|
3956
|
+
// writes [0,0,0,0] at offset 48 instead of leaving the
|
|
3957
|
+
// buildPbrMaterialUboPayload PBR baseline (e.g. occlusionStrength=1
|
|
3958
|
+
// at that slot). A non-zero slicesAndMode trips `useSlices=true`
|
|
3959
|
+
// in sprite.wgsl, which degenerates HANDLE_QUAD geometry → invisible.
|
|
4412
3960
|
if (!('slicesAndMode' in paramSnap)) {
|
|
4413
3961
|
(paramSnap as Record<string, unknown>).slicesAndMode = [0, 0, 0, 0];
|
|
4414
3962
|
}
|
|
4415
3963
|
}
|
|
4416
3964
|
|
|
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
3965
|
// Generic materialShaderId snapshot --- sprite included now flows
|
|
4436
3966
|
// through this single branch (plan-strategy D-3 / AC-01 / AC-02 /
|
|
4437
3967
|
// AC-07). The sprite block above only writes paramSnap.region (D-8
|
|
@@ -4452,18 +3982,10 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4452
3982
|
}),
|
|
4453
3983
|
normalScale: normalScalePv,
|
|
4454
3984
|
materialShaderId: firstPassShader,
|
|
4455
|
-
...(materialGuid === undefined ? {} : { materialGuid }),
|
|
4456
3985
|
materialHandle: handleRaw,
|
|
4457
|
-
shadowCaster: hasShadowCasterPass(allPasses),
|
|
4458
3986
|
renderState: pipelineRenderState(allPasses[0]?.renderState),
|
|
4459
3987
|
paramSnapshot: paramSnap,
|
|
4460
3988
|
...(materialParamSchema.length > 0 && { materialParamSchema }),
|
|
4461
|
-
...(derivedMaterialSlot?.ok === true && {
|
|
4462
|
-
derivedMaterialSlot: derivedMaterialSlot.value,
|
|
4463
|
-
}),
|
|
4464
|
-
...(derivedMaterialSlot?.ok === false && {
|
|
4465
|
-
derivedMaterialError: derivedMaterialSlot.error,
|
|
4466
|
-
}),
|
|
4467
3989
|
...(textureCoordinates.size > 0 && { textureCoordinates }),
|
|
4468
3990
|
...(samplerHandles.size > 0 && { samplerHandles }),
|
|
4469
3991
|
...(textureHandles.size > 0 && { textureHandles }),
|
|
@@ -4516,7 +4038,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4516
4038
|
allPasses,
|
|
4517
4039
|
entity,
|
|
4518
4040
|
handleRaw,
|
|
4519
|
-
|
|
4041
|
+
renderables.length,
|
|
4520
4042
|
layerVal,
|
|
4521
4043
|
paramSnap,
|
|
4522
4044
|
);
|
|
@@ -4550,7 +4072,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4550
4072
|
// handleRaw===0 guard alone preserves the exact same dispatch shape.
|
|
4551
4073
|
if (isRenderable && handleRaw === 0) {
|
|
4552
4074
|
const shadowCasterTags: Record<string, string> = { LightMode: 'ShadowCaster' };
|
|
4553
|
-
const nextRenderableIndex =
|
|
4075
|
+
const nextRenderableIndex = renderables.length;
|
|
4554
4076
|
// M2.5: stage into pendingDispatch; flushed at the renderable push
|
|
4555
4077
|
// site below only when the entity survives cull.
|
|
4556
4078
|
pendingDispatch.push({
|
|
@@ -4558,7 +4080,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4558
4080
|
materialHandle: 0,
|
|
4559
4081
|
renderableIndex: nextRenderableIndex,
|
|
4560
4082
|
passIndex: 0,
|
|
4561
|
-
queue:
|
|
4083
|
+
queue: 2000,
|
|
4562
4084
|
layer: layerVal,
|
|
4563
4085
|
tags: shadowCasterTags,
|
|
4564
4086
|
renderState: undefined,
|
|
@@ -4570,22 +4092,16 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4570
4092
|
});
|
|
4571
4093
|
// Also add a Forward pass entry so the entity renders in the
|
|
4572
4094
|
// 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
|
-
});
|
|
4095
|
+
appendMaterialDispatchEntries(
|
|
4096
|
+
pendingDispatch,
|
|
4097
|
+
[DEFAULT_FORWARD_PASS],
|
|
4098
|
+
entity,
|
|
4099
|
+
0,
|
|
4100
|
+
nextRenderableIndex,
|
|
4101
|
+
layerVal,
|
|
4102
|
+
{},
|
|
4103
|
+
1,
|
|
4104
|
+
);
|
|
4589
4105
|
}
|
|
4590
4106
|
|
|
4591
4107
|
if (isRenderable) {
|
|
@@ -4605,30 +4121,21 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4605
4121
|
) {
|
|
4606
4122
|
// Skin + Instances coexistence is forbidden (D-10).
|
|
4607
4123
|
if (hasInstances) {
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
{
|
|
4613
|
-
systemName: 'RenderSystem.extract (skin-instances-coexist)',
|
|
4614
|
-
},
|
|
4615
|
-
);
|
|
4124
|
+
worldInternal._routeError(new SkinInstancesCoexistForbiddenError(entity), {
|
|
4125
|
+
severity: Severity.Error,
|
|
4126
|
+
systemName: 'RenderSystem.extract (skin-instances-coexist)',
|
|
4127
|
+
});
|
|
4616
4128
|
|
|
4617
4129
|
continue;
|
|
4618
4130
|
}
|
|
4619
4131
|
// (a) Resolve skeleton asset; on failure -> skeleton-resolve-failed.
|
|
4620
4132
|
const skeletonHandle = toShared<'SkeletonAsset'>(skeletonHandleRaw);
|
|
4621
|
-
const skeletonRes =
|
|
4133
|
+
const skeletonRes = resolveAssetHandle<SkeletonAsset>(world, skeletonHandle);
|
|
4622
4134
|
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
|
-
);
|
|
4135
|
+
worldInternal._routeError(new SkeletonResolveFailedError(entity, skeletonHandleRaw), {
|
|
4136
|
+
severity: Severity.Error,
|
|
4137
|
+
systemName: 'RenderSystem.extract (skeleton-resolve-failed)',
|
|
4138
|
+
});
|
|
4632
4139
|
continue;
|
|
4633
4140
|
}
|
|
4634
4141
|
const skeleton = skeletonRes.value;
|
|
@@ -4638,20 +4145,17 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4638
4145
|
if (skinJoints === undefined) continue;
|
|
4639
4146
|
const jointsLength = skinJoints.length;
|
|
4640
4147
|
if (jointsLength !== skeleton.jointCount) {
|
|
4641
|
-
|
|
4642
|
-
new JointCountMismatchError(
|
|
4643
|
-
entity as unknown as number,
|
|
4644
|
-
skeleton.jointCount,
|
|
4645
|
-
jointsLength,
|
|
4646
|
-
) as unknown as Error,
|
|
4148
|
+
worldInternal._routeError(
|
|
4149
|
+
new JointCountMismatchError(entity, skeleton.jointCount, jointsLength),
|
|
4647
4150
|
{
|
|
4151
|
+
severity: Severity.Error,
|
|
4648
4152
|
systemName: 'RenderSystem.extract (joint-count-mismatch)',
|
|
4649
4153
|
},
|
|
4650
4154
|
);
|
|
4651
4155
|
continue;
|
|
4652
4156
|
}
|
|
4653
4157
|
// (c) Resolve only each joint's Transform.world column. The
|
|
4654
|
-
// transient view is consumed before
|
|
4158
|
+
// transient view is consumed before a structural mutation and
|
|
4655
4159
|
// therefore preserves the same dangling-joint behavior without
|
|
4656
4160
|
// constructing every other Transform field.
|
|
4657
4161
|
// Build mat4 list eagerly so write happens once per entity (no
|
|
@@ -4660,27 +4164,24 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4660
4164
|
let jointDangling = -1;
|
|
4661
4165
|
for (let jIdx = 0; jIdx < skeleton.jointCount; jIdx++) {
|
|
4662
4166
|
const jointEntityRaw = skinJoints[jIdx] ?? 0;
|
|
4663
|
-
const jointEntity = jointEntityRaw as
|
|
4664
|
-
const
|
|
4665
|
-
if (
|
|
4167
|
+
const jointEntity = jointEntityRaw as EntityHandle;
|
|
4168
|
+
const jointWorld = worldInternal._getArrayView(jointEntity, Transform, 'world');
|
|
4169
|
+
if (jointWorld === undefined) {
|
|
4666
4170
|
jointDangling = jIdx;
|
|
4667
4171
|
break;
|
|
4668
4172
|
}
|
|
4669
|
-
// The
|
|
4670
|
-
//
|
|
4671
|
-
//
|
|
4672
|
-
|
|
4173
|
+
// The allocator consumes the canonical Mat4 brand. Copy the
|
|
4174
|
+
// column view into that owner-created value so the ECS storage
|
|
4175
|
+
// view never crosses the math brand boundary by assertion.
|
|
4176
|
+
const jointWorldMat = mat4.create();
|
|
4177
|
+
jointWorldMat.set(jointWorld);
|
|
4178
|
+
jointWorlds[jIdx] = jointWorldMat;
|
|
4673
4179
|
}
|
|
4674
4180
|
if (jointDangling >= 0) {
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
) as unknown as Error,
|
|
4680
|
-
{
|
|
4681
|
-
systemName: 'RenderSystem.extract (joint-entity-dangling)',
|
|
4682
|
-
},
|
|
4683
|
-
);
|
|
4181
|
+
worldInternal._routeError(new JointEntityDanglingError(entity, jointDangling), {
|
|
4182
|
+
severity: Severity.Error,
|
|
4183
|
+
systemName: 'RenderSystem.extract (joint-entity-dangling)',
|
|
4184
|
+
});
|
|
4684
4185
|
continue;
|
|
4685
4186
|
}
|
|
4686
4187
|
// (d) Slice the IBM flat Float32Array into per-joint Float32Arrays.
|
|
@@ -4699,7 +4200,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4699
4200
|
skinPaletteAllocator.writeJointPalette(slice, ibms, jointWorlds);
|
|
4700
4201
|
skinSlice = {
|
|
4701
4202
|
jointCount: slice.jointCount,
|
|
4702
|
-
jointMatrices: new Float32Array(slice.jointMatrices),
|
|
4703
4203
|
byteOffset: slice.byteOffset,
|
|
4704
4204
|
buffer: slice.buffer,
|
|
4705
4205
|
};
|
|
@@ -4712,14 +4212,11 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4712
4212
|
// record stage copies these 16 floats into the mesh SSBO with zero
|
|
4713
4213
|
// per-snapshot `mat4.compose` (AC-07). A stale slot (generation gone)
|
|
4714
4214
|
// skips the renderable, mirroring the Instances dangling-row sweep.
|
|
4715
|
-
// tweak-20260611 M1 / K-3:
|
|
4215
|
+
// tweak-20260611 M1 / K-3: `_getArrayView` call survives untouched;
|
|
4716
4216
|
// 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;
|
|
4217
|
+
const worldView = worldInternal._getArrayView(entity, Transform, 'world');
|
|
4218
|
+
if (worldView === undefined) continue;
|
|
4219
|
+
const worldMat = new Float32Array(worldView);
|
|
4723
4220
|
const transformSnap: TransformSnapshot = { world: worldMat };
|
|
4724
4221
|
// feat-20260608 M5 amend / w11-a: per-submesh `materials[]` array
|
|
4725
4222
|
// aligned 1-1 with `MeshAsset.submeshes[]`. materials[0] === the
|
|
@@ -4741,14 +4238,30 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4741
4238
|
subHandle,
|
|
4742
4239
|
world,
|
|
4743
4240
|
assets,
|
|
4744
|
-
gpuStore,
|
|
4745
4241
|
materialSnapshotCache,
|
|
4746
4242
|
persistentMaterialSnapshotCache,
|
|
4747
|
-
context.paramSchemaProjectionCensus,
|
|
4748
4243
|
),
|
|
4749
4244
|
);
|
|
4750
4245
|
}
|
|
4751
4246
|
}
|
|
4247
|
+
if (isRenderable && assets !== undefined && assets !== null) {
|
|
4248
|
+
for (let mi = 1; mi < materialHandles.length; mi++) {
|
|
4249
|
+
const subHandle = materialHandles[mi] ?? 0;
|
|
4250
|
+
if (subHandle === handleRaw) continue;
|
|
4251
|
+
const subEntry =
|
|
4252
|
+
materialSnapshotCache.get(subHandle) ??
|
|
4253
|
+
readPersistentMaterialSnapshot(persistentMaterialSnapshotCache, subHandle, assets);
|
|
4254
|
+
appendMaterialDispatchEntries(
|
|
4255
|
+
pendingDispatch,
|
|
4256
|
+
subEntry?.passes ?? (subHandle === 0 ? [DEFAULT_FORWARD_PASS] : []),
|
|
4257
|
+
entity,
|
|
4258
|
+
subHandle,
|
|
4259
|
+
renderables.length,
|
|
4260
|
+
layerVal,
|
|
4261
|
+
materialsArr[mi]?.paramSnapshot,
|
|
4262
|
+
);
|
|
4263
|
+
}
|
|
4264
|
+
}
|
|
4752
4265
|
|
|
4753
4266
|
// feat-20260625-sprite-instances-and-tilemap-terrain-static-batch M3 /
|
|
4754
4267
|
// w10: SpriteInstances validation + snapshot materialisation.
|
|
@@ -4758,11 +4271,10 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4758
4271
|
if (hasSpriteInstances) {
|
|
4759
4272
|
// (1) mutually exclusive with Instances (peers — pick one).
|
|
4760
4273
|
if (hasInstances) {
|
|
4761
|
-
|
|
4762
|
-
new SpriteInstancesMutuallyExclusiveWithInstancesError(
|
|
4763
|
-
entity as unknown as number,
|
|
4764
|
-
) as unknown as Error,
|
|
4274
|
+
worldInternal._routeError(
|
|
4275
|
+
new SpriteInstancesMutuallyExclusiveWithInstancesError(entity),
|
|
4765
4276
|
{
|
|
4277
|
+
severity: Severity.Error,
|
|
4766
4278
|
systemName: 'RenderSystem.extract (sprite-instances-mutually-exclusive)',
|
|
4767
4279
|
},
|
|
4768
4280
|
);
|
|
@@ -4781,12 +4293,13 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4781
4293
|
materialSnap.materialShaderId !== 'forgeax::sprite' &&
|
|
4782
4294
|
materialSnap.materialShaderId !== 'forgeax::sprite-lit'
|
|
4783
4295
|
) {
|
|
4784
|
-
|
|
4296
|
+
worldInternal._routeError(
|
|
4785
4297
|
new SpriteInstancesRequiresSpriteShaderError(
|
|
4786
|
-
entity
|
|
4298
|
+
entity,
|
|
4787
4299
|
materialSnap.materialShaderId ?? 'undefined',
|
|
4788
|
-
)
|
|
4300
|
+
),
|
|
4789
4301
|
{
|
|
4302
|
+
severity: Severity.Error,
|
|
4790
4303
|
systemName: 'RenderSystem.extract (sprite-instances-requires-sprite-shader)',
|
|
4791
4304
|
},
|
|
4792
4305
|
);
|
|
@@ -4795,9 +4308,8 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4795
4308
|
// (3) count mismatch — transforms.length / 16 === regions.length / 4
|
|
4796
4309
|
// (transforms.length=0 + regions.length=0 is the zero-instance lawful
|
|
4797
4310
|
// 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;
|
|
4311
|
+
const transforms = worldInternal._getArrayView(entity, SpriteInstances, 'transforms');
|
|
4312
|
+
const regions = worldInternal._getArrayView(entity, SpriteInstances, 'regions');
|
|
4801
4313
|
if (transforms !== undefined && regions !== undefined) {
|
|
4802
4314
|
const transformsLength = transforms.length;
|
|
4803
4315
|
const regionsLength = regions.length;
|
|
@@ -4808,12 +4320,10 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4808
4320
|
const tCount = transformsLength / 16;
|
|
4809
4321
|
const rCount = regionsLength / 4;
|
|
4810
4322
|
if (transformsLength % 16 !== 0 || regionsLength % 4 !== 0 || tCount !== rCount) {
|
|
4811
|
-
|
|
4812
|
-
new SpriteInstancesCountMismatchError(
|
|
4813
|
-
transformsLength,
|
|
4814
|
-
regionsLength,
|
|
4815
|
-
) as unknown as Error,
|
|
4323
|
+
worldInternal._routeError(
|
|
4324
|
+
new SpriteInstancesCountMismatchError(transformsLength, regionsLength),
|
|
4816
4325
|
{
|
|
4326
|
+
severity: Severity.Error,
|
|
4817
4327
|
systemName: 'RenderSystem.extract (sprite-instances-count-mismatch)',
|
|
4818
4328
|
},
|
|
4819
4329
|
);
|
|
@@ -4828,35 +4338,71 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4828
4338
|
transforms: transformsCopy,
|
|
4829
4339
|
regions: regionsCopy,
|
|
4830
4340
|
instanceCount: tCount,
|
|
4831
|
-
cacheKey: entity
|
|
4341
|
+
cacheKey: entity,
|
|
4832
4342
|
archVersion,
|
|
4833
4343
|
};
|
|
4834
4344
|
}
|
|
4835
4345
|
}
|
|
4836
4346
|
|
|
4837
4347
|
let localAabb: Float32Array | undefined;
|
|
4348
|
+
let morph: MorphSnapshot | undefined;
|
|
4838
4349
|
const assetHandleRaw = Math.round(fAssetHandle ?? 0);
|
|
4839
4350
|
if (assetHandleRaw !== 0) {
|
|
4840
|
-
const meshRes =
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4351
|
+
const meshRes = resolveAssetHandle(world, toShared<'MeshAsset'>(assetHandleRaw));
|
|
4352
|
+
if (meshRes.ok && meshRes.value.kind === 'mesh') {
|
|
4353
|
+
const meshAsset = meshRes.value as MeshAsset;
|
|
4354
|
+
morph = morphSnapshotFor(meshAsset, morphWeightsView);
|
|
4355
|
+
const meshAabb = meshAsset.aabb;
|
|
4356
|
+
// Morph targets can expand the authored bounds. Keep the
|
|
4357
|
+
// specialized lane conservative until a target-aware bounds
|
|
4358
|
+
// projection is available; a false-positive draw is preferable
|
|
4359
|
+
// to culling a valid deformed vertex.
|
|
4360
|
+
if (morph === undefined && hasFiniteOrderedLocalAabb(meshAabb)) {
|
|
4361
|
+
localAabb = new Float32Array(meshAabb);
|
|
4362
|
+
}
|
|
4363
|
+
}
|
|
4846
4364
|
}
|
|
4847
4365
|
|
|
4366
|
+
const pointsLinesStyle: PointsLinesStyle | undefined =
|
|
4367
|
+
points !== undefined
|
|
4368
|
+
? (() => {
|
|
4369
|
+
const shape = pointShapeFromU32(points.shape);
|
|
4370
|
+
return shape === undefined
|
|
4371
|
+
? undefined
|
|
4372
|
+
: { kind: 'points' as const, sizePx: points.sizePx, shape };
|
|
4373
|
+
})()
|
|
4374
|
+
: lines === undefined
|
|
4375
|
+
? undefined
|
|
4376
|
+
: { kind: 'lines' as const, widthPx: lines.widthPx };
|
|
4377
|
+
const cullingLocalAabb = expandPointsLinesBounds(localAabb ?? [], pointsLinesStyle);
|
|
4378
|
+
const pointsLines =
|
|
4379
|
+
pointsLinesComponent === undefined
|
|
4380
|
+
? undefined
|
|
4381
|
+
: ({
|
|
4382
|
+
worldId: 0,
|
|
4383
|
+
entityKey: entity,
|
|
4384
|
+
component: pointsLinesComponent,
|
|
4385
|
+
meshHandle: assetHandleRaw,
|
|
4386
|
+
meshGeneration: assets?.catalogEpoch ?? 0,
|
|
4387
|
+
materialHandle: handleRaw,
|
|
4388
|
+
materialGeneration: assets?.catalogEpoch ?? 0,
|
|
4389
|
+
style: pointsLinesStyle,
|
|
4390
|
+
layer: layerVal,
|
|
4391
|
+
sortKey,
|
|
4392
|
+
visible: true,
|
|
4393
|
+
sourceBounds: cullingLocalAabb,
|
|
4394
|
+
viewport: { width: 0, height: 0, dpr: 1 },
|
|
4395
|
+
projection: identityProjection(),
|
|
4396
|
+
} satisfies PointsLinesRetainedSnapshot);
|
|
4397
|
+
|
|
4848
4398
|
let nonIndexedFirst = 0;
|
|
4849
|
-
const gpuDrivenDraws = gpuDrivenSubmeshes.flatMap((submesh
|
|
4399
|
+
const gpuDrivenDraws = gpuDrivenSubmeshes.flatMap((submesh) => {
|
|
4850
4400
|
const drawMaterial = materialsArr[submesh.materialSlot] ?? materialSnap;
|
|
4851
|
-
const forwardPass = gpuDrivenForwardPass(drawMaterial, materialSnapshotCache);
|
|
4852
4401
|
const first = gpuDrivenIndexed ? submesh.indexOffset : nonIndexedFirst;
|
|
4853
4402
|
nonIndexedFirst += submesh.vertexCount;
|
|
4854
|
-
if (drawMaterial.transparent === true
|
|
4403
|
+
if (drawMaterial.transparent === true) return [];
|
|
4855
4404
|
return [
|
|
4856
4405
|
{
|
|
4857
|
-
submeshIndex,
|
|
4858
|
-
passIndex: forwardPass.passIndex,
|
|
4859
|
-
queue: forwardPass.queue,
|
|
4860
4406
|
kind: gpuDrivenIndexed ? ('indexed' as const) : ('non-indexed' as const),
|
|
4861
4407
|
first,
|
|
4862
4408
|
count: gpuDrivenIndexed ? submesh.indexCount : submesh.vertexCount,
|
|
@@ -4871,53 +4417,20 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4871
4417
|
const baseRenderable: RenderableSnapshot = {
|
|
4872
4418
|
assetHandle: Math.round(fAssetHandle ?? 0),
|
|
4873
4419
|
transform: transformSnap,
|
|
4874
|
-
...(
|
|
4875
|
-
...(candidateInstanceTransforms !== undefined
|
|
4876
|
-
? { candidateInstanceTransforms: new Float32Array(candidateInstanceTransforms) }
|
|
4877
|
-
: {}),
|
|
4878
|
-
...(localAabb !== undefined ? { localAabb } : {}),
|
|
4420
|
+
...(localAabb !== undefined ? { localAabb: cullingLocalAabb } : {}),
|
|
4879
4421
|
material: materialSnap,
|
|
4880
4422
|
materials: materialsArr,
|
|
4881
4423
|
materialBindingSources,
|
|
4882
4424
|
materialBindingDiagnostics,
|
|
4883
|
-
...(gpuDrivenDraws.length
|
|
4425
|
+
...(morph === undefined && gpuDrivenDraws.length > 0 ? { gpuDrivenDraws } : {}),
|
|
4884
4426
|
worldId: 0,
|
|
4885
|
-
entityKey: entity
|
|
4886
|
-
candidateIndex,
|
|
4427
|
+
entityKey: entity,
|
|
4887
4428
|
...(skinSlice !== undefined ? { skin: skinSlice } : {}),
|
|
4888
|
-
...(
|
|
4429
|
+
...(morph !== undefined ? { morph } : {}),
|
|
4889
4430
|
...(spriteInstancesSnap !== undefined ? { spriteInstances: spriteInstancesSnap } : {}),
|
|
4431
|
+
...(pointsLines !== undefined ? { pointsLines } : {}),
|
|
4890
4432
|
};
|
|
4891
4433
|
|
|
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
4434
|
// feat-20260528-frustum-culling M3 / w10: frustum culling check.
|
|
4922
4435
|
// Skip the entity if a valid AABB exists AND ALL cameras' frusta
|
|
4923
4436
|
// reject the world-space AABB. Missing or malformed AABBs are
|
|
@@ -4929,13 +4442,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4929
4442
|
// directly (no compose) -- same source the record stage feeds
|
|
4930
4443
|
// the mesh SSBO, so cull stays same-source with render (AC-05).
|
|
4931
4444
|
const worldAabb = box3.create();
|
|
4932
|
-
box3.transformBox3(
|
|
4933
|
-
worldAabb,
|
|
4934
|
-
localAabb,
|
|
4935
|
-
transformSnap.world as unknown as Parameters<typeof box3.transformBox3>[2],
|
|
4936
|
-
);
|
|
4445
|
+
box3.transformBox3(worldAabb, cullingLocalAabb, transformSnap.world);
|
|
4937
4446
|
|
|
4938
|
-
// Test against all cameras. Entity is visible if
|
|
4447
|
+
// Test against all cameras. Entity is visible if one camera
|
|
4939
4448
|
// frustum intersects the world-space AABB (or planes are empty
|
|
4940
4449
|
// from degenerate projection).
|
|
4941
4450
|
frustumTotal += 1;
|
|
@@ -4953,13 +4462,42 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4953
4462
|
}
|
|
4954
4463
|
if (!visible) {
|
|
4955
4464
|
frustumCulled += 1;
|
|
4956
|
-
for (const entry of pendingDispatch) candidateDispatch.push(entry);
|
|
4957
4465
|
continue;
|
|
4958
4466
|
}
|
|
4959
4467
|
}
|
|
4960
4468
|
|
|
4961
|
-
|
|
4962
|
-
|
|
4469
|
+
if (hasInstances) {
|
|
4470
|
+
const transforms = worldInternal._getArrayView(entity, Instances, 'transforms');
|
|
4471
|
+
if (transforms === undefined) {
|
|
4472
|
+
flushPendingDispatch(pendingDispatch);
|
|
4473
|
+
renderables.push(baseRenderable);
|
|
4474
|
+
} else {
|
|
4475
|
+
const actualLength = transforms.length;
|
|
4476
|
+
if (actualLength % 16 !== 0) {
|
|
4477
|
+
worldInternal._routeError(new InstanceTransformsStrideMismatchError(actualLength), {
|
|
4478
|
+
severity: Severity.Error,
|
|
4479
|
+
systemName: 'RenderSystem.extract (Instances stride)',
|
|
4480
|
+
});
|
|
4481
|
+
|
|
4482
|
+
continue;
|
|
4483
|
+
}
|
|
4484
|
+
const snapshotCopy = new Float32Array(transforms);
|
|
4485
|
+
const instanceCount = Math.max(1, Math.floor(actualLength / 16));
|
|
4486
|
+
flushPendingDispatch(pendingDispatch);
|
|
4487
|
+
renderables.push({
|
|
4488
|
+
...baseRenderable,
|
|
4489
|
+
instances: {
|
|
4490
|
+
transforms: snapshotCopy,
|
|
4491
|
+
instanceCount,
|
|
4492
|
+
cacheKey: entity,
|
|
4493
|
+
archVersion,
|
|
4494
|
+
},
|
|
4495
|
+
});
|
|
4496
|
+
}
|
|
4497
|
+
} else {
|
|
4498
|
+
flushPendingDispatch(pendingDispatch);
|
|
4499
|
+
renderables.push(baseRenderable);
|
|
4500
|
+
}
|
|
4963
4501
|
}
|
|
4964
4502
|
|
|
4965
4503
|
// feat-20260520-2d-sprite-layer-mvp M-3 / w22 + w25: finalise the
|
|
@@ -4973,53 +4511,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
4973
4511
|
}
|
|
4974
4512
|
}
|
|
4975
4513
|
|
|
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
|
-
};
|
|
4514
|
+
// M3 / w26: sort dispatch entries by queue (ascending, stable sort)
|
|
4515
|
+
// per plan-strategy D-3.
|
|
4516
|
+
dispatch = sortDispatchByQueue(dispatch);
|
|
5023
4517
|
|
|
5024
4518
|
// D-1: collect PostProcessParams entities into Map<shaderId, Uint8Array>.
|
|
5025
4519
|
// Last-one-wins when multiple entities bear the same shader id (mirrors
|
|
@@ -5037,17 +4531,17 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
5037
4531
|
// provider. The engine bridges the active camera's `Camera.exposure /
|
|
5038
4532
|
// whitePoint / tonemap` onto the SAME unified params channel custom
|
|
5039
4533
|
// post-processes use — `Camera.exposure` stays the AI-user-facing SSOT (D-5),
|
|
5040
|
-
// the engine itself acts as the provider for the
|
|
4534
|
+
// the engine itself acts as the provider for the Standard tonemap shader
|
|
5041
4535
|
// id. The 16B layout is byte-identical to the prior recordTonemapPass packing
|
|
5042
4536
|
// (render-system-record.ts pre-w14): Float32 [exposure, whitePoint, _, pad]
|
|
5043
4537
|
// with the mode u32 occupying the third 4-byte slot via tonemapToU32 (SSOT in
|
|
5044
4538
|
// camera.ts). Run AFTER the user-entity collection above so the engine's
|
|
5045
4539
|
// built-in provider is authoritative for its own reserved key (a user entity
|
|
5046
|
-
// can never shadow
|
|
4540
|
+
// can never shadow the Standard tonemap identity. The single active camera mirrors
|
|
5047
4541
|
// recordFrame's `activeCameras[0]` selection.
|
|
5048
4542
|
const tonemapCamera = cameras[0];
|
|
5049
4543
|
if (tonemapCamera !== undefined) {
|
|
5050
|
-
postProcessParams.set(
|
|
4544
|
+
postProcessParams.set(STANDARD_TONEMAP_FEATURE_ID, tonemapParams(tonemapCamera));
|
|
5051
4545
|
}
|
|
5052
4546
|
|
|
5053
4547
|
return {
|
|
@@ -5055,11 +4549,10 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
5055
4549
|
lights,
|
|
5056
4550
|
renderables,
|
|
5057
4551
|
dispatch,
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
environment,
|
|
4552
|
+
skylight,
|
|
4553
|
+
skylightCount,
|
|
4554
|
+
skybox,
|
|
4555
|
+
skyboxCount,
|
|
5063
4556
|
frustumStats: { culled: frustumCulled, total: frustumTotal },
|
|
5064
4557
|
visibilityStats: { explicitlyHidden: explicitlyHidden.size },
|
|
5065
4558
|
postProcessParams,
|
|
@@ -5069,135 +4562,24 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
|
|
|
5069
4562
|
};
|
|
5070
4563
|
}
|
|
5071
4564
|
|
|
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
4565
|
export function defaultMaterialSnapshot(materialHandle = 0): MaterialSnapshot {
|
|
5182
4566
|
// Mid-grey unlit fallback (D-Q7 case B + extract-stage missing-spec),
|
|
5183
4567
|
// matching the pre-w6 visual outcome where the record stage's force-cast
|
|
5184
4568
|
// read of `firstMaterial.baseColorTexture` returned undefined and the
|
|
5185
4569
|
// 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
4570
|
return {
|
|
5195
4571
|
baseColor: vec3.create(0.5, 0.5, 0.5),
|
|
5196
4572
|
metallic: 0,
|
|
5197
4573
|
roughness: 1,
|
|
5198
4574
|
materialHandle,
|
|
5199
|
-
shadowCaster: true,
|
|
5200
|
-
materialParamSchema,
|
|
5201
|
-
derivedMaterialSlot,
|
|
5202
4575
|
};
|
|
5203
4576
|
}
|
|
4577
|
+
|
|
4578
|
+
function identityProjection(): Float32Array {
|
|
4579
|
+
const projection = new Float32Array(16);
|
|
4580
|
+
projection[0] = 1;
|
|
4581
|
+
projection[5] = 1;
|
|
4582
|
+
projection[10] = 1;
|
|
4583
|
+
projection[15] = 1;
|
|
4584
|
+
return projection;
|
|
4585
|
+
}
|