@forgeax/engine-runtime 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -83
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/animation-graph-collect-roundtrip.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-collect-roundtrip.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-serialize.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-serialize.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-brand-and-name-retirement.test.d.ts +2 -0
- package/dist/__tests__/asset-brand-and-name-retirement.test.d.ts.map +1 -0
- package/dist/__tests__/asset-error-exhaustive.test-d.d.ts +11 -0
- package/dist/__tests__/asset-error-exhaustive.test-d.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-guid-reverse.test.d.ts +2 -0
- package/dist/__tests__/asset-registry-guid-reverse.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-hdr-equirect.test.d.ts +2 -0
- package/dist/__tests__/asset-registry-hdr-equirect.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-mesh-pack-payload.dawn.test.d.ts +2 -0
- package/dist/__tests__/asset-registry-mesh-pack-payload.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-mounts.test.d.ts +2 -0
- package/dist/__tests__/asset-registry-mounts.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry.browser.test.d.ts +2 -0
- package/dist/__tests__/asset-registry.browser.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry.dawn.test.d.ts +2 -0
- package/dist/__tests__/asset-registry.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry.listCatalog.test.d.ts +2 -0
- package/dist/__tests__/asset-registry.listCatalog.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry.mesh-bin.test.d.ts +2 -0
- package/dist/__tests__/asset-registry.mesh-bin.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry.pack-cache.test.d.ts +2 -0
- package/dist/__tests__/asset-registry.pack-cache.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry.raw-container-failfast.test.d.ts +2 -0
- package/dist/__tests__/asset-registry.raw-container-failfast.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry.recursive.spec.d.ts +2 -0
- package/dist/__tests__/asset-registry.recursive.spec.d.ts.map +1 -0
- package/dist/__tests__/asset-registry.unit.test.d.ts +2 -0
- package/dist/__tests__/asset-registry.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset.unit.test.d.ts +2 -0
- package/dist/__tests__/asset.unit.test.d.ts.map +1 -0
- package/dist/__tests__/audio-load-by-guid.unit.test.d.ts +2 -0
- package/dist/__tests__/audio-load-by-guid.unit.test.d.ts.map +1 -0
- package/dist/__tests__/backend-selection.integration.test.d.ts +2 -0
- package/dist/__tests__/backend-selection.integration.test.d.ts.map +1 -0
- package/dist/__tests__/basis-catalog-dispatch.integration.test.d.ts +2 -0
- package/dist/__tests__/basis-catalog-dispatch.integration.test.d.ts.map +1 -0
- package/dist/__tests__/builtin-asset-registry.unit.test.d.ts +2 -0
- package/dist/__tests__/builtin-asset-registry.unit.test.d.ts.map +1 -0
- package/dist/__tests__/collect-scene-asset.test.d.ts +1 -2
- package/dist/__tests__/collect-scene-asset.test.d.ts.map +1 -1
- package/dist/__tests__/collect-scene-mount-collapse.test.d.ts +2 -0
- package/dist/__tests__/collect-scene-mount-collapse.test.d.ts.map +1 -0
- package/dist/__tests__/collect-transient-roundtrip.test.d.ts +1 -1
- package/dist/__tests__/collect-transient-roundtrip.test.d.ts.map +1 -1
- package/dist/__tests__/collect-transient-skip.test.d.ts +1 -2
- package/dist/__tests__/collect-transient-skip.test.d.ts.map +1 -1
- package/dist/__tests__/compressed-texture-physical-extent.integration.test.d.ts +2 -0
- package/dist/__tests__/compressed-texture-physical-extent.integration.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/mesh-update-dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/mesh-update-dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/upload-texture.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/upload-texture.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/draw-published.d.ts +2 -2
- package/dist/__tests__/draw-published.d.ts.map +1 -1
- package/dist/__tests__/dynamic-texture-store.unit.test.d.ts +2 -0
- package/dist/__tests__/dynamic-texture-store.unit.test.d.ts.map +1 -0
- package/dist/__tests__/errors.unit.test.d.ts +2 -0
- package/dist/__tests__/errors.unit.test.d.ts.map +1 -0
- package/dist/__tests__/extract-skin-mismatch.unit.test.d.ts +2 -0
- package/dist/__tests__/extract-skin-mismatch.unit.test.d.ts.map +1 -0
- package/dist/__tests__/fetch-binary-decompress.test.d.ts +2 -0
- package/dist/__tests__/fetch-binary-decompress.test.d.ts.map +1 -0
- package/dist/__tests__/four-path-compression.integration.test.d.ts +2 -0
- package/dist/__tests__/four-path-compression.integration.test.d.ts.map +1 -0
- package/dist/__tests__/fullscreen-feature-host.dawn.test.d.ts +2 -0
- package/dist/__tests__/fullscreen-feature-host.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/guid-for-asset.unit.test.d.ts +2 -0
- package/dist/__tests__/guid-for-asset.unit.test.d.ts.map +1 -0
- package/dist/__tests__/handle-migration.test.d.ts +2 -0
- package/dist/__tests__/handle-migration.test.d.ts.map +1 -0
- package/dist/__tests__/helpers/register-runtime-components.d.ts.map +1 -1
- package/dist/__tests__/host-custom-kind-contract.test-d.d.ts +2 -0
- package/dist/__tests__/host-custom-kind-contract.test-d.d.ts.map +1 -0
- package/dist/__tests__/host-custom-kind-contract.test.d.ts +2 -0
- package/dist/__tests__/host-custom-kind-contract.test.d.ts.map +1 -0
- package/dist/__tests__/inspect-name.test.d.ts +2 -0
- package/dist/__tests__/inspect-name.test.d.ts.map +1 -0
- package/dist/__tests__/instantiate-mount-deferred-wiring.test.d.ts +2 -0
- package/dist/__tests__/instantiate-mount-deferred-wiring.test.d.ts.map +1 -0
- package/dist/__tests__/loadbyguid-payload.unit.test.d.ts +2 -0
- package/dist/__tests__/loadbyguid-payload.unit.test.d.ts.map +1 -0
- package/dist/__tests__/loadbyguid-texture-intern.unit.test.d.ts +2 -0
- package/dist/__tests__/loadbyguid-texture-intern.unit.test.d.ts.map +1 -0
- package/dist/__tests__/loader-extract-graceful.test.d.ts +2 -0
- package/dist/__tests__/loader-extract-graceful.test.d.ts.map +1 -0
- package/dist/__tests__/loader-name-survival.test.d.ts +2 -0
- package/dist/__tests__/loader-name-survival.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-bin-contract.test.d.ts +2 -0
- package/dist/__tests__/mesh-bin-contract.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-bin-skin-roundtrip.unit.test.d.ts +2 -0
- package/dist/__tests__/mesh-bin-skin-roundtrip.unit.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-bin.test.d.ts +2 -0
- package/dist/__tests__/mesh-bin.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-loader-skin-contract.unit.test.d.ts +2 -0
- package/dist/__tests__/mesh-loader-skin-contract.unit.test.d.ts.map +1 -0
- package/dist/__tests__/new-kind-refs-contract.test.d.ts +2 -0
- package/dist/__tests__/new-kind-refs-contract.test.d.ts.map +1 -0
- package/dist/__tests__/package-promotion.test.d.ts +2 -0
- package/dist/__tests__/package-promotion.test.d.ts.map +1 -0
- package/dist/__tests__/package-rename.test.d.ts +2 -0
- package/dist/__tests__/package-rename.test.d.ts.map +1 -0
- package/dist/__tests__/recover-error-exhaustive.test-d.d.ts +1 -1
- package/dist/__tests__/recover-error-exhaustive.test-d.d.ts.map +1 -1
- package/dist/__tests__/render-error-exhaustive.test-d.d.ts +1 -1
- package/dist/__tests__/render-error-exhaustive.test-d.d.ts.map +1 -1
- package/dist/__tests__/render-feature-prepared-graphics.fixture.d.ts +10 -0
- package/dist/__tests__/render-feature-prepared-graphics.fixture.d.ts.map +1 -0
- package/dist/__tests__/render-system-extract.unit.test.d.ts +2 -0
- package/dist/__tests__/render-system-extract.unit.test.d.ts.map +1 -0
- package/dist/__tests__/renderer-test-utils.d.ts +10 -0
- package/dist/__tests__/renderer-test-utils.d.ts.map +1 -0
- package/dist/__tests__/renderer.test-d.d.ts +2 -0
- package/dist/__tests__/renderer.test-d.d.ts.map +1 -0
- package/dist/__tests__/resolve-asset-handle-err.test.d.ts +2 -0
- package/dist/__tests__/resolve-asset-handle-err.test.d.ts.map +1 -0
- package/dist/__tests__/resolve-asset-handle.unit.test.d.ts +2 -0
- package/dist/__tests__/resolve-asset-handle.unit.test.d.ts.map +1 -0
- package/dist/__tests__/resolveName-fallback.test.d.ts +2 -0
- package/dist/__tests__/resolveName-fallback.test.d.ts.map +1 -0
- package/dist/__tests__/resolveName-null-package.test.d.ts +2 -0
- package/dist/__tests__/resolveName-null-package.test.d.ts.map +1 -0
- package/dist/__tests__/resolveName-unified-entry.test.d.ts +2 -0
- package/dist/__tests__/resolveName-unified-entry.test.d.ts.map +1 -0
- package/dist/__tests__/resolveName.test.d.ts +2 -0
- package/dist/__tests__/resolveName.test.d.ts.map +1 -0
- package/dist/__tests__/roots-to-scene-asset.test.d.ts +1 -1
- package/dist/__tests__/roots-to-scene-asset.test.d.ts.map +1 -1
- package/dist/__tests__/roundtrip-primitive.test.d.ts +2 -0
- package/dist/__tests__/roundtrip-primitive.test.d.ts.map +1 -0
- package/dist/__tests__/scene-bare-guid-refs-instantiate.test.d.ts +2 -0
- package/dist/__tests__/scene-bare-guid-refs-instantiate.test.d.ts.map +1 -0
- package/dist/__tests__/scene-instance-authoring.integration.test.d.ts +2 -0
- package/dist/__tests__/scene-instance-authoring.integration.test.d.ts.map +1 -0
- package/dist/__tests__/scene-mount-fixed-point.test.d.ts +2 -0
- package/dist/__tests__/scene-mount-fixed-point.test.d.ts.map +1 -0
- package/dist/__tests__/scene-mount-roundtrip.test.d.ts +2 -0
- package/dist/__tests__/scene-mount-roundtrip.test.d.ts.map +1 -0
- package/dist/__tests__/scene-mount-wrapper-roundtrip.test.d.ts +2 -0
- package/dist/__tests__/scene-mount-wrapper-roundtrip.test.d.ts.map +1 -0
- package/dist/__tests__/scene-sugar-instantiate.test.d.ts +2 -0
- package/dist/__tests__/scene-sugar-instantiate.test.d.ts.map +1 -0
- package/dist/__tests__/serialize-mounts-roundtrip.test.d.ts +2 -0
- package/dist/__tests__/serialize-mounts-roundtrip.test.d.ts.map +1 -0
- package/dist/__tests__/serialize-scene-asset.test.d.ts +1 -1
- package/dist/__tests__/serialize-scene-asset.test.d.ts.map +1 -1
- package/dist/__tests__/template-game-default-api-contract.test.d.ts +2 -0
- package/dist/__tests__/template-game-default-api-contract.test.d.ts.map +1 -0
- package/dist/__tests__/tileset-asset-validate.test.d.ts +2 -0
- package/dist/__tests__/tileset-asset-validate.test.d.ts.map +1 -0
- package/dist/__tests__/transcode-fallback.unit.test.d.ts +2 -0
- package/dist/__tests__/transcode-fallback.unit.test.d.ts.map +1 -0
- package/dist/__tests__/transient-runtime-behavior.test.d.ts +2 -0
- package/dist/__tests__/transient-runtime-behavior.test.d.ts.map +1 -0
- package/dist/__tests__/video-loader-consumption.test.d.ts +2 -0
- package/dist/__tests__/video-loader-consumption.test.d.ts.map +1 -0
- package/dist/__tests__/video-loader-smoke.dawn.test.d.ts +2 -0
- package/dist/__tests__/video-loader-smoke.dawn.test.d.ts.map +1 -0
- package/dist/backend-selection.d.ts +13 -0
- package/dist/backend-selection.d.ts.map +1 -0
- package/dist/collect-scene-asset.d.ts +3 -3
- package/dist/collect-scene-asset.d.ts.map +1 -1
- package/dist/createRenderer.d.ts +14 -2
- package/dist/createRenderer.d.ts.map +1 -1
- package/dist/errors/environment.d.ts +1 -1
- package/dist/errors/environment.d.ts.map +1 -1
- package/dist/index.d.ts +19 -81
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +105 -108
- package/dist/index.mjs.map +1 -1
- package/dist/renderer-host.d.ts +9 -0
- package/dist/renderer-host.d.ts.map +1 -0
- package/dist/renderer-host.mjs +62 -0
- package/dist/renderer-host.mjs.map +1 -0
- package/dist/scene-instances/index.d.ts +2 -0
- package/dist/scene-instances/index.d.ts.map +1 -0
- package/dist/scene-utils/mount-override-fold.d.ts +1 -2
- package/dist/scene-utils/mount-override-fold.d.ts.map +1 -1
- package/dist/systems/sprite-animation-tick.d.ts +1 -1
- package/dist/systems/sprite-animation-tick.d.ts.map +1 -1
- package/dist/systems/transparent-sort.d.ts +1 -2
- package/dist/systems/transparent-sort.d.ts.map +1 -1
- package/dist/tilemap-register-validate.d.ts.map +1 -1
- package/package.json +34 -33
- package/src/__tests__/active-camera.unit.test.ts +19 -9
- package/src/__tests__/animation-graph-collect-roundtrip.test.ts +213 -0
- package/src/__tests__/animation-graph-serialize.unit.test.ts +171 -0
- package/src/__tests__/append-injection.test.ts +1 -1
- package/src/__tests__/asset-brand-and-name-retirement.test.ts +102 -0
- package/src/__tests__/asset-error-exhaustive.test-d.ts +62 -0
- package/src/__tests__/asset-registry-guid-reverse.test.ts +575 -0
- package/src/__tests__/asset-registry-hdr-equirect.test.ts +78 -0
- package/src/__tests__/asset-registry-mesh-pack-payload.dawn.test.ts +232 -0
- package/src/__tests__/asset-registry-mounts.test.ts +806 -0
- package/src/__tests__/asset-registry.browser.test.ts +81 -0
- package/src/__tests__/asset-registry.dawn.test.ts +94 -0
- package/src/__tests__/asset-registry.listCatalog.test.ts +170 -0
- package/src/__tests__/asset-registry.mesh-bin.test.ts +316 -0
- package/src/__tests__/asset-registry.pack-cache.test.ts +969 -0
- package/src/__tests__/asset-registry.raw-container-failfast.test.ts +225 -0
- package/src/__tests__/asset-registry.recursive.spec.ts +1397 -0
- package/src/__tests__/asset-registry.unit.test.ts +1030 -0
- package/src/__tests__/asset.unit.test.ts +7107 -0
- package/src/__tests__/audio-load-by-guid.unit.test.ts +126 -0
- package/src/__tests__/backend-selection.integration.test.ts +11 -0
- package/src/__tests__/basis-catalog-dispatch.integration.test.ts +320 -0
- package/src/__tests__/bind-group-cache-keying.test.ts +4 -4
- package/src/__tests__/block-upload-math.unit.test.ts +2 -2
- package/src/__tests__/builtin-asset-registry.unit.test.ts +112 -0
- package/src/__tests__/builtin-shader-register-e2e.test.ts +1 -1
- package/src/__tests__/cache-false-eviction.test.ts +2 -1
- package/src/__tests__/cache-isolation-matrix.test.ts +2 -1
- package/src/__tests__/camera-clear-snapshot-equivalence.unit.test.ts +5 -6
- package/src/__tests__/clamp-to-last.e2e.browser.test.ts +29 -19
- package/src/__tests__/collect-scene-asset.test.ts +49 -52
- package/src/__tests__/collect-scene-mount-collapse.test.ts +391 -0
- package/src/__tests__/collect-subtree.unit.test.ts +1 -2
- package/src/__tests__/collect-transient-children-absent.test.ts +24 -10
- package/src/__tests__/collect-transient-roundtrip.test.ts +44 -20
- package/src/__tests__/collect-transient-skip.test.ts +24 -13
- package/src/__tests__/components.test-d.ts +1 -6
- package/src/__tests__/components.unit.test.ts +20 -19
- package/src/__tests__/composite-skybox-cross-world.browser.test.ts +59 -27
- package/src/__tests__/compressed-texture-physical-extent.integration.test.ts +139 -0
- package/src/__tests__/create-renderer-assembly.integration.test.ts +42 -29
- package/src/__tests__/createRenderer-dispose.test.ts +35 -402
- package/src/__tests__/createRenderer.browser-no-webgpu.test.ts +25 -30
- package/src/__tests__/createRenderer.unit.test.ts +1 -1
- package/src/__tests__/dawn/hello-sprite-nineslice-section.dawn.test.ts +27 -63
- package/src/__tests__/dawn/ibl-precompute-readback.dawn.test.ts +56 -47
- package/src/__tests__/dawn/instances-per-instance-pbr.dawn.test.ts +49 -29
- package/src/__tests__/dawn/instances-shadow.dawn.test.ts +26 -15
- package/src/__tests__/dawn/instances-uniform-fallback.dawn.test.ts +13 -14
- package/src/__tests__/dawn/material-alpha.dawn.test.ts +23 -31
- package/src/__tests__/dawn/material-cooked-fixture.dawn.test.ts +0 -18
- package/src/__tests__/dawn/{mesh-update.dawn.test.ts → mesh-update-dawn.test.ts} +42 -134
- package/src/__tests__/dawn/mipmap-srgb-linear.dawn.test.ts +14 -7
- package/src/__tests__/dawn/per-entity-material-texture-isolation.dawn.test.ts +16 -28
- package/src/__tests__/dawn/sprite-nineslice-mesh-bind.dawn.test.ts +23 -39
- package/src/__tests__/dawn/upload-texture.dawn.test.ts +198 -0
- package/src/__tests__/dawn/user-handle-mesh-render.dawn.test.ts +12 -14
- package/src/__tests__/dawn/user-mesh-upload.dawn.test.ts +27 -22
- package/src/__tests__/dawn/zero-manifest-mesh-fail.dawn.test.ts +25 -28
- package/src/__tests__/dawn/zero-manifest-ready.dawn.test.ts +10 -11
- package/src/__tests__/demo-sortscope-migration.unit.test.ts +5 -5
- package/src/__tests__/derive-bgl-integration.test.ts +34 -44
- package/src/__tests__/derive-render-data-skin.unit.test.ts +1 -1
- package/src/__tests__/draw-published.ts +11 -4
- package/src/__tests__/dynamic-texture-store.unit.test.ts +190 -0
- package/src/__tests__/equirect-bc6h.integration.test.ts +144 -21
- package/src/__tests__/errors.unit.test.ts +1203 -0
- package/src/__tests__/extract-frames-graceful-degradation.test.ts +3 -7
- package/src/__tests__/extract-frames-merge.test.ts +35 -22
- package/src/__tests__/extract-record-no-hardcoded-texture-fields.test.ts +5 -16
- package/src/__tests__/extract-skin-mismatch.unit.test.ts +322 -0
- package/src/__tests__/fetch-binary-decompress.test.ts +356 -0
- package/src/__tests__/four-path-compression.integration.test.ts +395 -0
- package/src/__tests__/frame-targets-shadow-regression.unit.test.ts +1 -1
- package/src/__tests__/fullscreen-feature-host.dawn.test.ts +80 -0
- package/src/__tests__/fullscreen-post-bgl-depth.test.ts +4 -2
- package/src/__tests__/fullscreen-post-depth-params-arity.test.ts +3 -3
- package/src/__tests__/fullscreen-post-process-pass.dawn.test.ts +39 -23
- package/src/__tests__/fullscreen-post-process-pass.unit.test.ts +4 -2
- package/src/__tests__/fxaa-pixel-diff.dawn.test.ts +28 -17
- package/src/__tests__/fxaa-validation-and-resize.dawn.test.ts +29 -18
- package/src/__tests__/fxaa-zero-overhead.dawn.test.ts +16 -12
- package/src/__tests__/geometry.unit.test.ts +525 -159
- package/src/__tests__/glyph-tilemap-worldid.test.ts +1 -1
- package/src/__tests__/gpu-resource-cow-survivor.test.ts +6 -4
- package/src/__tests__/guid-for-asset.unit.test.ts +102 -0
- package/src/__tests__/handle-migration.test.ts +136 -0
- package/src/__tests__/helpers/register-runtime-components.ts +4 -2
- package/src/__tests__/helpers/register-scene-components.ts +1 -1
- package/src/__tests__/host-custom-kind-contract.test-d.ts +47 -0
- package/src/__tests__/host-custom-kind-contract.test.ts +578 -0
- package/src/__tests__/inspect-name.test.ts +139 -0
- package/src/__tests__/instances.test-d.ts +1 -1
- package/src/__tests__/instantiate-mount-deferred-wiring.test.ts +112 -0
- package/src/__tests__/light-casters-9-light.browser.test.ts +73 -76
- package/src/__tests__/light-gpu-byte-neutral.unit.test.ts +7 -10
- package/src/__tests__/light-snapshot.test-d.ts +2 -2
- package/src/__tests__/lights-vec-negative.unit.test.ts +1 -1
- package/src/__tests__/lights.unit.test.ts +16 -500
- package/src/__tests__/loadbyguid-payload.unit.test.ts +117 -0
- package/src/__tests__/loadbyguid-texture-intern.unit.test.ts +213 -0
- package/src/__tests__/loader-extract-graceful.test.ts +146 -0
- package/src/__tests__/loader-name-survival.test.ts +213 -0
- package/src/__tests__/m3-vec-negative.unit.test.ts +3 -2
- package/src/__tests__/material-texture-uv-scale.unit.test.ts +101 -31
- package/src/__tests__/materials.unit.test.ts +10 -27
- package/src/__tests__/mesh-bin-contract.test.ts +28 -0
- package/src/__tests__/mesh-bin-skin-roundtrip.unit.test.ts +149 -0
- package/src/__tests__/mesh-bin.test.ts +38 -0
- package/src/__tests__/mesh-loader-skin-contract.unit.test.ts +120 -0
- package/src/__tests__/mesh-ssbo-grow.unit.test.ts +3 -3
- package/src/__tests__/mip-gate.unit.test.ts +1 -1
- package/src/__tests__/msaa-sprite-pixel-diff.dawn.test.ts +29 -68
- package/src/__tests__/new-kind-refs-contract.test.ts +132 -0
- package/src/__tests__/node-shims.d.ts +4 -6
- package/src/__tests__/package-promotion.test.ts +88 -0
- package/src/__tests__/package-rename.test.ts +103 -0
- package/src/__tests__/pass-selector.unit.test.ts +2 -2
- package/src/__tests__/pbr-pipeline.unit.test.ts +14 -21
- package/src/__tests__/pipeline-builder-msaa.unit.test.ts +6 -9
- package/src/__tests__/pipeline-cache-keying.unit.test.ts +6 -9
- package/src/__tests__/pipeline-spec.test.ts +16 -177
- package/src/__tests__/pipeline.unit.test.ts +154 -1693
- package/src/__tests__/point-light-shadow.dawn.test.ts +33 -42
- package/src/__tests__/point-light-shadow.unit.test.ts +19 -60
- package/src/__tests__/post-process-error-codes.unit.test.ts +4 -1
- package/src/__tests__/post-process-params-component.unit.test.ts +1 -1
- package/src/__tests__/post-process-params-extract.unit.test.ts +2 -5
- package/src/__tests__/postprocess-camera-consistency.test.ts +4 -7
- package/src/__tests__/profiler-assembly.integration.test.ts +1 -1
- package/src/__tests__/propagate-transforms.dawn.test.ts +12 -11
- package/src/__tests__/record-worldid-isolation.test.ts +7 -29
- package/src/__tests__/recover-error-exhaustive.test-d.ts +1 -1
- package/src/__tests__/render-error-exhaustive.test-d.ts +81 -71
- package/src/__tests__/render-feature-assembly.test-d.ts +20 -9
- package/src/__tests__/render-feature-prepared-graphics-docs.unit.test.ts +8 -6
- package/src/__tests__/render-feature-prepared-graphics.browser.test.ts +60 -558
- package/src/__tests__/render-feature-prepared-graphics.dawn.test.ts +57 -544
- package/src/__tests__/render-feature-prepared-graphics.fixture.ts +104 -0
- package/src/__tests__/render-feature-prepared-graphics.integration.test.ts +40 -248
- package/src/__tests__/render-feature-prepared-graphics.regression.integration.test.ts +83 -340
- package/src/__tests__/render-feature-recover.integration.test.ts +9 -49
- package/src/__tests__/render-feature-runtime-surface.test-d.ts +17 -20
- package/src/__tests__/render-schema-consumer.integration.test.ts +20 -13
- package/src/__tests__/render-schema-owner-boundary.unit.test.ts +2 -6
- package/src/__tests__/render-system-cross-world-overlay-queue.test.ts +5 -5
- package/src/__tests__/render-system-extract-dispatch-ordering.test.ts +16 -31
- package/src/__tests__/render-system-extract.test.ts +36 -51
- package/src/__tests__/render-system-extract.unit.test.ts +360 -0
- package/src/__tests__/render-system-fold-bucket.unit.test.ts +3 -5
- package/src/__tests__/render-system-fold-fallback.unit.test.ts +6 -3
- package/src/__tests__/render-system-fold-material.unit.test.ts +3 -5
- package/src/__tests__/render-system-fold-metrics.unit.test.ts +5 -5
- package/src/__tests__/render-system-fold-modegate.unit.test.ts +6 -6
- package/src/__tests__/render-system-fold.dawn.test.ts +7 -5
- package/src/__tests__/render-system-mega.test.ts +292 -190
- package/src/__tests__/render-system-owner-split-contract.test.ts +10 -32
- package/src/__tests__/render-system-record-multi-material-textureview.test.ts +59 -45
- package/src/__tests__/render-system-record-per-submesh-transparency.test.ts +23 -29
- package/src/__tests__/render-system-record.test.ts +398 -235
- package/src/__tests__/render-system-record.unit.test.ts +25 -26
- package/src/__tests__/render-system-skin-bg.unit.test.ts +8 -12
- package/src/__tests__/renderer-consumer.test-d.ts +16 -3
- package/src/__tests__/renderer-health.unit.test.ts +26 -916
- package/src/__tests__/renderer-lifecycle.integration.test.ts +19 -6
- package/src/__tests__/renderer-recover.unit.test.ts +49 -1069
- package/src/__tests__/renderer-result.test-d.ts +19 -9
- package/src/__tests__/renderer-surface.unit.test.ts +3 -5
- package/src/__tests__/renderer-test-utils.ts +56 -0
- package/src/__tests__/renderer.test-d.ts +59 -0
- package/src/__tests__/resolve-asset-handle-err.test.ts +111 -0
- package/src/__tests__/resolve-asset-handle.unit.test.ts +64 -0
- package/src/__tests__/resolve-depth-only-view.test.ts +1 -1
- package/src/__tests__/resolveName-fallback.test.ts +64 -0
- package/src/__tests__/resolveName-null-package.test.ts +38 -0
- package/src/__tests__/resolveName-unified-entry.test.ts +65 -0
- package/src/__tests__/resolveName.test.ts +64 -0
- package/src/__tests__/rhi-null-command-flow.unit.test.ts +43 -261
- package/src/__tests__/rhi-null-renderer-lifecycle.unit.test.ts +41 -169
- package/src/__tests__/roots-to-scene-asset.test.ts +199 -93
- package/src/__tests__/roundtrip-primitive.test.ts +351 -0
- package/src/__tests__/scene-bare-guid-refs-instantiate.test.ts +123 -0
- package/src/__tests__/scene-equirect-roundtrip.test.ts +103 -16
- package/src/__tests__/scene-instance-authoring.integration.test.ts +8 -0
- package/src/__tests__/scene-instance-bridge.integration.test.ts +1 -1
- package/src/__tests__/scene-instance-component.test.ts +1 -1
- package/src/__tests__/scene-mount-fixed-point.test.ts +257 -0
- package/src/__tests__/scene-mount-roundtrip.test.ts +987 -0
- package/src/__tests__/scene-mount-wrapper-roundtrip.test.ts +202 -0
- package/src/__tests__/scene-sugar-instantiate.test.ts +324 -0
- package/src/__tests__/serialize-mounts-roundtrip.test.ts +533 -0
- package/src/__tests__/serialize-scene-asset.test.ts +219 -22
- package/src/__tests__/shadow-caster-empty-schema.test.ts +3 -3
- package/src/__tests__/shadow-csm-cascade-loadop.test.ts +37 -17
- package/src/__tests__/shadow-csm-component.test.ts +3 -5
- package/src/__tests__/shadow-csm-extract.test.ts +2 -100
- package/src/__tests__/shadow-csm-orthoz.test.ts +2 -16
- package/src/__tests__/shadow-csm-runtime-vary.dawn.test.ts +19 -14
- package/src/__tests__/shadow-csm-tile-consistency.test.ts +32 -26
- package/src/__tests__/shadow-csm-ubo.dawn.test.ts +8 -49
- package/src/__tests__/shadow-csm-ubo.test.ts +3 -25
- package/src/__tests__/shadow-csm-viewport.browser.test.ts +1 -1
- package/src/__tests__/shadow-fields-observable.dawn.test.ts +67 -56
- package/src/__tests__/shadow-m1.dawn.test.ts +8 -9
- package/src/__tests__/shadow-mesh-ssbo-order.test.ts +11 -27
- package/src/__tests__/skin-errors.unit.test.ts +1 -1
- package/src/__tests__/skin-extract-getarrayview-count.unit.test.ts +7 -5
- package/src/__tests__/skylight-lazy-projection.test.ts +36 -15
- package/src/__tests__/sparse-tag-scene-roundtrip.test.ts +5 -5
- package/src/__tests__/sprite-cullmode-flip.dawn.test.ts +47 -64
- package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +12 -11
- package/src/__tests__/sprite-lit-extract-branch.test.ts +1 -1
- package/src/__tests__/sprite-lit-pipeline-cache-key.test.ts +1 -1
- package/src/__tests__/ssao-bgl.test.ts +16 -9
- package/src/__tests__/ssao-buffers.test.ts +2 -2
- package/src/__tests__/ssao-data.test.ts +2 -2
- package/src/__tests__/ssao-errors.test.ts +1 -1
- package/src/__tests__/ssao-passes.test.ts +64 -392
- package/src/__tests__/ssao-pipelines.test.ts +1 -1
- package/src/__tests__/ssao-uniform-layout.test.ts +8 -6
- package/src/__tests__/stencil-outline-pixel.dawn.test.ts +25 -17
- package/src/__tests__/swap-chain-format.unit.test.ts +5 -7
- package/src/__tests__/systems.unit.test.ts +536 -1326
- package/src/__tests__/template-game-default-api-contract.test.ts +362 -0
- package/src/__tests__/text.unit.test.ts +56 -33
- package/src/__tests__/tilemap-childed-despawn.test.ts +8 -14
- package/src/__tests__/tilemap-chunk-extract.overhang-bounds.test.ts +8 -12
- package/src/__tests__/tilemap-chunk-extract.propagate-order.test.ts +2 -1
- package/src/__tests__/tilemap-chunk-extract.test.ts +13 -17
- package/src/__tests__/tilemap-dirty-rebuild.test.ts +7 -11
- package/src/__tests__/tilemap-material-cache.test.ts +8 -13
- package/src/__tests__/tilemap-multi-atlas.test.ts +12 -32
- package/src/__tests__/tilemap-owner-boundary.unit.test.ts +5 -2
- package/src/__tests__/tilemap-region-pack.test.ts +8 -12
- package/src/__tests__/tilemap-spawn-flip-pivot.test.ts +8 -12
- package/src/__tests__/tilemap-uv-inset.test.ts +11 -14
- package/src/__tests__/tilemap-validate.test.ts +1 -1
- package/src/__tests__/tileset-asset-validate.test.ts +638 -0
- package/src/__tests__/transcode-fallback.unit.test.ts +207 -0
- package/src/__tests__/transform-hierarchy-pixel-diff.dawn.test.ts +36 -28
- package/src/__tests__/transient-fields-collect.test.ts +28 -21
- package/src/__tests__/transient-runtime-behavior.test.ts +227 -0
- package/src/__tests__/transparent-sort-tilemap-sprite-interleave.test.ts +4 -4
- package/src/__tests__/variant-selection.unit.test.ts +1 -1
- package/src/__tests__/video-extract-bindgroup.dawn.test.ts +89 -136
- package/src/__tests__/video-loader-consumption.test.ts +86 -0
- package/src/__tests__/video-loader-smoke.dawn.test.ts +60 -0
- package/src/__tests__/view-ubo-layout.browser.test.ts +0 -22
- package/src/__tests__/world-entity-key.test.ts +1 -1
- package/src/__tests__/world-scene-despawn.test.ts +1 -1
- package/src/__tests__/world-scene-instantiate.test.ts +2 -36
- package/src/__tests__/world-scene-nested.test.ts +1 -1
- package/src/__tests__/world-scene-override.test.ts +1 -1
- package/src/backend-selection.ts +69 -0
- package/src/collect-scene-asset.ts +28 -23
- package/src/components/__tests__/camera.test-d.ts +1 -1
- package/src/components/__tests__/camera.test.ts +1 -1
- package/src/components/__tests__/sprite-animation.test-d.ts +2 -2
- package/src/components/__tests__/tile-layer.test-d.ts +1 -1
- package/src/components/__tests__/tile-layer.unit.test.ts +2 -2
- package/src/createRenderer.ts +24 -36
- package/src/errors/environment.ts +1 -1
- package/src/index.ts +47 -113
- package/src/renderer-host.ts +33 -0
- package/src/scene-instances/__tests__/post-spawn-resolve-joints.unit.test.ts +1 -1
- package/src/scene-instances/index.ts +4 -0
- package/src/scene-utils/mount-override-fold.ts +3 -7
- package/src/systems/__tests__/transparent-sort.bench.ts +3 -3
- package/src/systems/sprite-animation-tick.ts +1 -1
- package/src/systems/transparent-sort.ts +38 -38
- package/src/tilemap-register-validate.ts +4 -4
- package/dist/__tests__/bind-group-cache-binding.test.d.ts +0 -2
- package/dist/__tests__/bind-group-cache-binding.test.d.ts.map +0 -1
- package/dist/__tests__/bind-group-cache-frame.test.d.ts +0 -2
- package/dist/__tests__/bind-group-cache-frame.test.d.ts.map +0 -1
- package/dist/__tests__/dawn/mesh-update.dawn.test.d.ts +0 -2
- package/dist/__tests__/dawn/mesh-update.dawn.test.d.ts.map +0 -1
- package/dist/__tests__/dispatch-fullscreen-pass-composite-swapchain.dawn.test.d.ts +0 -2
- package/dist/__tests__/dispatch-fullscreen-pass-composite-swapchain.dawn.test.d.ts.map +0 -1
- package/dist/__tests__/dispatch-fullscreen-pass-custom-reads.dawn.test.d.ts +0 -2
- package/dist/__tests__/dispatch-fullscreen-pass-custom-reads.dawn.test.d.ts.map +0 -1
- package/dist/__tests__/dispatch-fullscreen-pass-depth-composite.dawn.test.d.ts +0 -2
- package/dist/__tests__/dispatch-fullscreen-pass-depth-composite.dawn.test.d.ts.map +0 -1
- package/dist/__tests__/dispatch-fullscreen-pass-params.unit.test.d.ts +0 -2
- package/dist/__tests__/dispatch-fullscreen-pass-params.unit.test.d.ts.map +0 -1
- package/dist/__tests__/fog-producer-manifest.d.ts +0 -131
- package/dist/__tests__/fog-producer-manifest.d.ts.map +0 -1
- package/dist/__tests__/fog-producer-pixel-probe.d.ts +0 -42
- package/dist/__tests__/fog-producer-pixel-probe.d.ts.map +0 -1
- package/dist/__tests__/fog-producer-pixel.browser.test.d.ts +0 -2
- package/dist/__tests__/fog-producer-pixel.browser.test.d.ts.map +0 -1
- package/dist/__tests__/fog-producer-pixel.dawn.test.d.ts +0 -2
- package/dist/__tests__/fog-producer-pixel.dawn.test.d.ts.map +0 -1
- package/dist/__tests__/fog-producer-real-fixture.d.ts +0 -5
- package/dist/__tests__/fog-producer-real-fixture.d.ts.map +0 -1
- package/dist/__tests__/fog-producer-vfx-effect.d.ts +0 -3
- package/dist/__tests__/fog-producer-vfx-effect.d.ts.map +0 -1
- package/dist/__tests__/fog-scene-asset.integration.test.d.ts +0 -2
- package/dist/__tests__/fog-scene-asset.integration.test.d.ts.map +0 -1
- package/dist/__tests__/gpu-resource-store-cubemap-status.test.d.ts +0 -2
- package/dist/__tests__/gpu-resource-store-cubemap-status.test.d.ts.map +0 -1
- package/dist/__tests__/gpu-resource.test.d.ts +0 -2
- package/dist/__tests__/gpu-resource.test.d.ts.map +0 -1
- package/dist/__tests__/hdrp-bgl-slots.browser.test.d.ts +0 -2
- package/dist/__tests__/hdrp-bgl-slots.browser.test.d.ts.map +0 -1
- package/dist/__tests__/hdrp-buffers.unit.test.d.ts +0 -2
- package/dist/__tests__/hdrp-buffers.unit.test.d.ts.map +0 -1
- package/dist/__tests__/hdrp-execute-filter.unit.test.d.ts +0 -2
- package/dist/__tests__/hdrp-execute-filter.unit.test.d.ts.map +0 -1
- package/dist/__tests__/hdrp-gbuffer-attachments.browser.test.d.ts +0 -2
- package/dist/__tests__/hdrp-gbuffer-attachments.browser.test.d.ts.map +0 -1
- package/dist/__tests__/hdrp-pipeline-asset-type.test-d.d.ts +0 -2
- package/dist/__tests__/hdrp-pipeline-asset-type.test-d.d.ts.map +0 -1
- package/dist/__tests__/hdrp-ssao-boundary.test.d.ts +0 -2
- package/dist/__tests__/hdrp-ssao-boundary.test.d.ts.map +0 -1
- package/dist/__tests__/hdrp-ssao-falsification.test.d.ts +0 -2
- package/dist/__tests__/hdrp-ssao-falsification.test.d.ts.map +0 -1
- package/dist/__tests__/helpers/primitive-world.d.ts +0 -8
- package/dist/__tests__/helpers/primitive-world.d.ts.map +0 -1
- package/dist/__tests__/helpers/render-asset-port.d.ts +0 -15
- package/dist/__tests__/helpers/render-asset-port.d.ts.map +0 -1
- package/dist/__tests__/helpers/scene-asset-port.d.ts +0 -9
- package/dist/__tests__/helpers/scene-asset-port.d.ts.map +0 -1
- package/dist/__tests__/material-derived-coordinates.integration.test.d.ts +0 -2
- package/dist/__tests__/material-derived-coordinates.integration.test.d.ts.map +0 -1
- package/dist/__tests__/pipeline-layout-selection.test.d.ts +0 -2
- package/dist/__tests__/pipeline-layout-selection.test.d.ts.map +0 -1
- package/dist/__tests__/post-process-reads-narrowing.test-d.d.ts +0 -2
- package/dist/__tests__/post-process-reads-narrowing.test-d.d.ts.map +0 -1
- package/dist/__tests__/post-process-register-roundtrip.dawn.test.d.ts +0 -2
- package/dist/__tests__/post-process-register-roundtrip.dawn.test.d.ts.map +0 -1
- package/dist/__tests__/render-feature-zero-feature.browser.test.d.ts +0 -2
- package/dist/__tests__/render-feature-zero-feature.browser.test.d.ts.map +0 -1
- package/dist/__tests__/render-pipeline-trivial.dawn.test.d.ts +0 -2
- package/dist/__tests__/render-pipeline-trivial.dawn.test.d.ts.map +0 -1
- package/dist/__tests__/shadow-csm-caster-membership.test.d.ts +0 -2
- package/dist/__tests__/shadow-csm-caster-membership.test.d.ts.map +0 -1
- package/dist/__tests__/shadow-csm-pssm.test.d.ts +0 -2
- package/dist/__tests__/shadow-csm-pssm.test.d.ts.map +0 -1
- package/dist/__tests__/shadow-csm-temporal.browser.test.d.ts +0 -2
- package/dist/__tests__/shadow-csm-temporal.browser.test.d.ts.map +0 -1
- package/dist/__tests__/shadow-csm-viewport.test.d.ts +0 -2
- package/dist/__tests__/shadow-csm-viewport.test.d.ts.map +0 -1
- package/dist/__tests__/shadow-m2.dawn.test.d.ts +0 -2
- package/dist/__tests__/shadow-m2.dawn.test.d.ts.map +0 -1
- package/dist/__tests__/shadow-m3-calibrate-run.dawn.test.d.ts +0 -2
- package/dist/__tests__/shadow-m3-calibrate-run.dawn.test.d.ts.map +0 -1
- package/dist/__tests__/shadow-m3-calibrate.d.ts +0 -83
- package/dist/__tests__/shadow-m3-calibrate.d.ts.map +0 -1
- package/dist/__tests__/shadow-m3.dawn.test.d.ts +0 -2
- package/dist/__tests__/shadow-m3.dawn.test.d.ts.map +0 -1
- package/dist/__tests__/shadow-probe-control-flow.dawn.test.d.ts +0 -2
- package/dist/__tests__/shadow-probe-control-flow.dawn.test.d.ts.map +0 -1
- package/dist/__tests__/shadow-quality-metrics.dawn.test.d.ts +0 -2
- package/dist/__tests__/shadow-quality-metrics.dawn.test.d.ts.map +0 -1
- package/dist/__tests__/temporal-off-path.unit.test.d.ts +0 -2
- package/dist/__tests__/temporal-off-path.unit.test.d.ts.map +0 -1
- package/dist/__tests__/video-cache-regression.unit.test.d.ts +0 -2
- package/dist/__tests__/video-cache-regression.unit.test.d.ts.map +0 -1
- package/dist/asset-kind.d.ts +0 -4
- package/dist/asset-kind.d.ts.map +0 -1
- package/dist/errors/sprite-animation.d.ts +0 -3
- package/dist/errors/sprite-animation.d.ts.map +0 -1
- package/dist/scene-asset-port.d.ts +0 -17
- package/dist/scene-asset-port.d.ts.map +0 -1
- package/dist/scene-collect-errors.d.ts +0 -22
- package/dist/scene-collect-errors.d.ts.map +0 -1
- package/dist/shadow-m3-calibrate.d.ts +0 -40
- package/dist/shadow-m3-calibrate.d.ts.map +0 -1
- package/src/__tests__/bind-group-cache-binding.test.ts +0 -2513
- package/src/__tests__/bind-group-cache-frame.test.ts +0 -1511
- package/src/__tests__/dispatch-fullscreen-pass-composite-swapchain.dawn.test.ts +0 -204
- package/src/__tests__/dispatch-fullscreen-pass-custom-reads.dawn.test.ts +0 -297
- package/src/__tests__/dispatch-fullscreen-pass-depth-composite.dawn.test.ts +0 -271
- package/src/__tests__/dispatch-fullscreen-pass-params.unit.test.ts +0 -321
- package/src/__tests__/fog-producer-manifest.ts +0 -162
- package/src/__tests__/fog-producer-pixel-probe.ts +0 -263
- package/src/__tests__/fog-producer-pixel.browser.test.ts +0 -26
- package/src/__tests__/fog-producer-pixel.dawn.test.ts +0 -55
- package/src/__tests__/fog-producer-real-fixture.ts +0 -805
- package/src/__tests__/fog-producer-vfx-effect.ts +0 -272
- package/src/__tests__/fog-scene-asset.integration.test.ts +0 -123
- package/src/__tests__/gpu-resource-store-cubemap-status.test.ts +0 -207
- package/src/__tests__/gpu-resource.test.ts +0 -1206
- package/src/__tests__/hdrp-bgl-slots.browser.test.ts +0 -50
- package/src/__tests__/hdrp-buffers.unit.test.ts +0 -70
- package/src/__tests__/hdrp-execute-filter.unit.test.ts +0 -200
- package/src/__tests__/hdrp-gbuffer-attachments.browser.test.ts +0 -64
- package/src/__tests__/hdrp-pipeline-asset-type.test-d.ts +0 -139
- package/src/__tests__/hdrp-ssao-boundary.test.ts +0 -237
- package/src/__tests__/hdrp-ssao-falsification.test.ts +0 -289
- package/src/__tests__/helpers/primitive-world.ts +0 -91
- package/src/__tests__/helpers/render-asset-port.ts +0 -65
- package/src/__tests__/helpers/scene-asset-port.ts +0 -23
- package/src/__tests__/material-derived-coordinates.integration.test.ts +0 -100
- package/src/__tests__/pipeline-layout-selection.test.ts +0 -298
- package/src/__tests__/post-process-reads-narrowing.test-d.ts +0 -52
- package/src/__tests__/post-process-register-roundtrip.dawn.test.ts +0 -230
- package/src/__tests__/render-feature-zero-feature.browser.test.ts +0 -152
- package/src/__tests__/render-pipeline-trivial.dawn.test.ts +0 -525
- package/src/__tests__/shadow-csm-caster-membership.test.ts +0 -232
- package/src/__tests__/shadow-csm-pssm.test.ts +0 -142
- package/src/__tests__/shadow-csm-temporal.browser.test.ts +0 -145
- package/src/__tests__/shadow-csm-viewport.test.ts +0 -74
- package/src/__tests__/shadow-m2.dawn.test.ts +0 -394
- package/src/__tests__/shadow-m3-calibrate-run.dawn.test.ts +0 -248
- package/src/__tests__/shadow-m3-calibrate.ts +0 -203
- package/src/__tests__/shadow-m3.dawn.test.ts +0 -288
- package/src/__tests__/shadow-probe-control-flow.dawn.test.ts +0 -264
- package/src/__tests__/shadow-quality-metrics.dawn.test.ts +0 -324
- package/src/__tests__/temporal-off-path.unit.test.ts +0 -69
- package/src/__tests__/video-cache-regression.unit.test.ts +0 -186
- package/src/asset-kind.ts +0 -7
- package/src/errors/sprite-animation.ts +0 -5
- package/src/scene-asset-port.ts +0 -32
- package/src/scene-collect-errors.ts +0 -30
- package/src/shadow-m3-calibrate.ts +0 -171
|
@@ -58,46 +58,30 @@ import { readFileSync } from 'node:fs';
|
|
|
58
58
|
import { resolve } from 'node:path';
|
|
59
59
|
import { fileURLToPath } from 'node:url';
|
|
60
60
|
import { AnimationPlayer } from '@forgeax/engine-animation';
|
|
61
|
+
import type { AssetRuntimeErrorCode } from '@forgeax/engine-assets-runtime';
|
|
62
|
+
import { AssetRegistry } from '@forgeax/engine-assets-runtime';
|
|
61
63
|
import type { EntityHandle, World as WorldType } from '@forgeax/engine-ecs';
|
|
62
64
|
import { ENTITY_NULL_RAW, World } from '@forgeax/engine-ecs';
|
|
63
|
-
import {
|
|
64
|
-
import { createPrimitiveMesh } from '@forgeax/engine-geometry';
|
|
65
|
+
import { SpriteAnimationInvalidError } from '@forgeax/engine-ecs/projection';
|
|
65
66
|
import { mat4, vec3 } from '@forgeax/engine-math';
|
|
66
|
-
import type { Renderer as RendererType } from '@forgeax/engine-render';
|
|
67
|
-
import {
|
|
68
|
-
SPRITE_PLAYBACK_MODE_CLAMP,
|
|
69
|
-
SPRITE_PLAYBACK_MODE_LOOP,
|
|
70
|
-
SpriteAnimation,
|
|
71
|
-
SpriteRegionOverride,
|
|
72
|
-
} from '@forgeax/engine-render/authoring';
|
|
73
|
-
import type { RenderAssetPort, RenderErrorCode } from '@forgeax/engine-render/internal';
|
|
67
|
+
import type { RenderErrorCode, Renderer as RendererType } from '@forgeax/engine-render';
|
|
74
68
|
import {
|
|
75
69
|
ANTIALIAS_FXAA,
|
|
76
70
|
ANTIALIAS_NONE,
|
|
77
|
-
antialiasFromF32,
|
|
78
71
|
BLOOM_DISABLED,
|
|
79
72
|
BLOOM_ENABLED,
|
|
80
|
-
bloomEnabledFromF32,
|
|
81
73
|
CAMERA_PROJECTION_ORTHOGRAPHIC,
|
|
82
74
|
CAMERA_PROJECTION_PERSPECTIVE,
|
|
83
75
|
Camera,
|
|
84
|
-
cameraProjectionFromF32,
|
|
85
76
|
MeshFilter,
|
|
86
77
|
MeshRenderer,
|
|
87
|
-
SkinPaletteOverflowError,
|
|
88
78
|
SkyboxBackground,
|
|
89
79
|
Skylight,
|
|
90
|
-
|
|
91
|
-
VertexStorageBufferUnavailableError,
|
|
92
|
-
} from '@forgeax/engine-render/internal';
|
|
80
|
+
} from '@forgeax/engine-render';
|
|
93
81
|
import type { BindGroupEntry, Buffer, Sampler, Texture, TextureView } from '@forgeax/engine-rhi';
|
|
94
82
|
import { ok as rhiOk } from '@forgeax/engine-rhi';
|
|
95
83
|
import { ChildOf, Name, propagateTransforms, Transform } from '@forgeax/engine-scene';
|
|
96
|
-
import {
|
|
97
|
-
DEFAULT_STANDARD_PBR_PARAM_SCHEMA,
|
|
98
|
-
DEFAULT_UNLIT_PARAM_SCHEMA,
|
|
99
|
-
TONEMAP_LUMINANCE_EPSILON,
|
|
100
|
-
} from '@forgeax/engine-shader';
|
|
84
|
+
import { TONEMAP_LUMINANCE_EPSILON } from '@forgeax/engine-shader';
|
|
101
85
|
import type { SkinErrorCode } from '@forgeax/engine-skinning';
|
|
102
86
|
import {
|
|
103
87
|
Skin,
|
|
@@ -107,8 +91,6 @@ import {
|
|
|
107
91
|
SkinJointPathUnresolvedError,
|
|
108
92
|
} from '@forgeax/engine-skinning';
|
|
109
93
|
import type {
|
|
110
|
-
AssetErrorCode,
|
|
111
|
-
EquirectAsset,
|
|
112
94
|
Handle,
|
|
113
95
|
MaterialAsset,
|
|
114
96
|
MeshAsset,
|
|
@@ -117,20 +99,95 @@ import type {
|
|
|
117
99
|
} from '@forgeax/engine-types';
|
|
118
100
|
import { toShared } from '@forgeax/engine-types';
|
|
119
101
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
102
|
+
import { worldInternal } from '../../../ecs/src/world-internal';
|
|
103
|
+
import {
|
|
104
|
+
SPRITE_PLAYBACK_MODE_CLAMP,
|
|
105
|
+
SPRITE_PLAYBACK_MODE_LOOP,
|
|
106
|
+
SpriteAnimation,
|
|
107
|
+
SpriteRegionOverride,
|
|
108
|
+
} from '../../../render/src/components';
|
|
109
|
+
import {
|
|
110
|
+
type CameraProjection,
|
|
111
|
+
cameraProjectionFromF32,
|
|
112
|
+
} from '../../../render/src/components/camera';
|
|
113
|
+
import {
|
|
114
|
+
assembleMaterialWithSkylightEntries,
|
|
115
|
+
createSkylightFallback,
|
|
116
|
+
mergeSkylightIntoMaterialBgl,
|
|
117
|
+
} from '../../../render/src/ibl/skylight-bind-group';
|
|
118
|
+
import { buildPbrPipelineLayouts, buildUnlitMaterialBgl } from '../../../render/src/pbr-pipeline';
|
|
119
|
+
import { INSTANCE_STORAGE_STRIDE_FLOATS } from '../../../render/src/record/mesh-ssbo';
|
|
120
|
+
import { selectSwapChainFormat } from '../../../render/src/render-system';
|
|
121
|
+
import { createSkinPaletteAllocator } from '../../../render/src/systems/skin-palette-allocator';
|
|
122
|
+
import type { TransparentEntry } from '../../../render/src/systems/transparent-sort-config';
|
|
123
|
+
import { drawWithOwners } from './renderer-test-utils';
|
|
124
|
+
|
|
125
|
+
function componentFieldType(component: { fields: Record<string, { type: string }> }, name: string) {
|
|
126
|
+
return component.fields[name]?.type;
|
|
127
|
+
}
|
|
120
128
|
|
|
121
|
-
function
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
129
|
+
function componentFieldDefault(
|
|
130
|
+
component: { fields: Record<string, { default?: unknown }> },
|
|
131
|
+
name: string,
|
|
132
|
+
) {
|
|
133
|
+
return component.fields[name]?.default;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function componentSchemaTypes(component: { fields: Record<string, { type: string }> }) {
|
|
137
|
+
return Object.fromEntries(
|
|
138
|
+
Object.entries(component.fields).map(([name, field]) => [name, field.type]),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
afterEach(() => vi.restoreAllMocks());
|
|
143
|
+
|
|
144
|
+
type RendererErrorObservation = {
|
|
145
|
+
readonly code: string;
|
|
146
|
+
readonly detail?: unknown;
|
|
147
|
+
readonly hint?: string;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
function unwrapRendererError(value: unknown): RendererErrorObservation {
|
|
151
|
+
let current = value as RendererErrorObservation;
|
|
152
|
+
while (
|
|
153
|
+
current.detail !== undefined &&
|
|
154
|
+
typeof current.detail === 'object' &&
|
|
155
|
+
current.detail !== null
|
|
156
|
+
) {
|
|
157
|
+
const cause = (current.detail as { cause?: unknown }).cause;
|
|
158
|
+
if (
|
|
159
|
+
cause === undefined ||
|
|
160
|
+
typeof cause !== 'object' ||
|
|
161
|
+
cause === null ||
|
|
162
|
+
typeof (cause as { code?: unknown }).code !== 'string'
|
|
163
|
+
) {
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
current = cause as RendererErrorObservation;
|
|
167
|
+
}
|
|
168
|
+
return current;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function subscribeRendererErrors(
|
|
172
|
+
renderer: RendererType,
|
|
173
|
+
listener: (error: RendererErrorObservation) => void,
|
|
174
|
+
): () => void {
|
|
175
|
+
return renderer.subscribe((event) => {
|
|
176
|
+
if (event.kind === 'error') listener(unwrapRendererError(event.error));
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function drawPublished(renderer: RendererType, world: WorldType) {
|
|
181
|
+
const attached = renderer.attach(world);
|
|
182
|
+
if (!attached.ok) throw attached.error;
|
|
183
|
+
world.update().unwrap();
|
|
184
|
+
return drawWithOwners(renderer, world);
|
|
128
185
|
}
|
|
129
186
|
|
|
130
187
|
// feat-20260704-runtime-tier1-decomposition M2 / w12: reconstitute the
|
|
131
188
|
// eliminated top-level RuntimeErrorCode aggregate union (D-3) as a test-local
|
|
132
189
|
// alias so the exhaustive-switch bodies below stay byte-identical (AC-09).
|
|
133
|
-
type RuntimeLayerErrorCode = RenderErrorCode |
|
|
190
|
+
type RuntimeLayerErrorCode = RenderErrorCode | AssetRuntimeErrorCode | SkinErrorCode;
|
|
134
191
|
|
|
135
192
|
import {
|
|
136
193
|
AnimationTargetId,
|
|
@@ -138,113 +195,42 @@ import {
|
|
|
138
195
|
advanceAnimationPlayer as canonicalAdvanceAnimationPlayer,
|
|
139
196
|
} from '@forgeax/engine-animation';
|
|
140
197
|
import { deriveAnimationTargetId } from '@forgeax/engine-animation/target-id';
|
|
141
|
-
import
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
InstanceBufferCacheEntry,
|
|
145
|
-
} from '@forgeax/engine-render/internal';
|
|
146
|
-
// (moved from import body)
|
|
198
|
+
import { DeviceScope } from '../../../render/src/device/device-scope';
|
|
199
|
+
import { GpuBuffer } from '../../../render/src/gpu-resource';
|
|
200
|
+
import { getOrCreateIblCache, hasIblCache } from '../../../render/src/ibl/IblPipelineCache';
|
|
147
201
|
import {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
202
|
+
disposeInstanceBuffers,
|
|
203
|
+
type InstanceBufferCacheEntry,
|
|
204
|
+
} from '../../../render/src/instance-buffer-cache';
|
|
205
|
+
import { standardPipeline as urpPipeline } from '../../../render/src/pipeline/standard-pipeline';
|
|
206
|
+
import { ZERO_CAMERA_CLEAR_FALLBACK } from '../../../render/src/record/frame-snapshot';
|
|
207
|
+
import {
|
|
208
|
+
warnMultiLightDirectional,
|
|
209
|
+
warnMultiLightPoint,
|
|
210
|
+
warnMultiLightSpot,
|
|
211
|
+
} from '../../../render/src/record/helpers';
|
|
212
|
+
import type { CameraSnapshot } from '../../../render/src/render-contract';
|
|
213
|
+
import {
|
|
214
|
+
type ExtractedLights,
|
|
153
215
|
extractFrame,
|
|
154
216
|
extractFrames,
|
|
155
|
-
GpuResourceStore,
|
|
156
|
-
getOrCreateIblCache,
|
|
157
|
-
getTransparentSortConfig,
|
|
158
|
-
hasIblCache,
|
|
159
|
-
mergeSkylightIntoMaterialBgl,
|
|
160
217
|
prepareExtractContext,
|
|
218
|
+
} from '../../../render/src/render-system-extract';
|
|
219
|
+
import {
|
|
220
|
+
getTransparentSortConfig,
|
|
161
221
|
setTransparentSortConfig,
|
|
162
222
|
TRANSPARENT_SORT_CONFIG_KEY,
|
|
163
223
|
TRANSPARENT_SORT_MODE_LAYER_Y,
|
|
164
224
|
TRANSPARENT_SORT_MODE_LAYER_YZ,
|
|
165
225
|
TRANSPARENT_SORT_MODE_LAYER_Z,
|
|
166
|
-
|
|
167
|
-
warnMultiLightDirectional,
|
|
168
|
-
warnMultiLightPoint,
|
|
169
|
-
warnMultiLightSpot,
|
|
170
|
-
ZERO_CAMERA_CLEAR_FALLBACK,
|
|
171
|
-
} from '@forgeax/engine-render/internal';
|
|
172
|
-
import { SpriteAnimationInvalidError } from '../errors/sprite-animation';
|
|
226
|
+
} from '../../../render/src/systems/transparent-sort-config';
|
|
173
227
|
import { spriteAnimationTickSystem } from '../systems/sprite-animation-tick';
|
|
174
228
|
import { REC709_LUMA_WEIGHTS, tonemapReinhardLuminance } from '../systems/tonemap';
|
|
175
229
|
import { transparentSortEntries } from '../systems/transparent-sort';
|
|
176
230
|
import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
177
231
|
|
|
178
|
-
function canonicalMaterialParamSchema(identifier: string): string {
|
|
179
|
-
if (identifier === 'forgeax::default-standard-pbr') {
|
|
180
|
-
return JSON.stringify(DEFAULT_STANDARD_PBR_PARAM_SCHEMA);
|
|
181
|
-
}
|
|
182
|
-
if (identifier === 'forgeax::default-unlit') {
|
|
183
|
-
return JSON.stringify(DEFAULT_UNLIT_PARAM_SCHEMA);
|
|
184
|
-
}
|
|
185
|
-
return '[]';
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const EMPTY_EXTRACTED_ENVIRONMENT = {
|
|
189
|
-
kind: 'none',
|
|
190
|
-
worldId: 0,
|
|
191
|
-
skylightCount: 0,
|
|
192
|
-
skyboxCount: 0,
|
|
193
|
-
} as const;
|
|
194
|
-
const EMPTY_POST_PROCESS_PARAMS = new Map<string, Uint8Array>();
|
|
195
|
-
|
|
196
|
-
function makeEnvironmentGenerationState() {
|
|
197
|
-
return {
|
|
198
|
-
deviceGeneration: 0,
|
|
199
|
-
retired: [],
|
|
200
|
-
status: 'idle' as const,
|
|
201
|
-
generation: 0,
|
|
202
|
-
desired: undefined,
|
|
203
|
-
candidate: undefined,
|
|
204
|
-
active: undefined,
|
|
205
|
-
lastKnownGood: undefined,
|
|
206
|
-
failureStage: undefined,
|
|
207
|
-
bytes: 0,
|
|
208
|
-
peakBytes: 0,
|
|
209
|
-
rebuildCost: 0,
|
|
210
|
-
publishCount: 0,
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
|
|
214
232
|
{
|
|
215
233
|
// --- from antialias-from-f32.test.ts ---
|
|
216
|
-
|
|
217
|
-
describe('antialiasFromF32', () => {
|
|
218
|
-
it('0 maps to none', () => {
|
|
219
|
-
expect(antialiasFromF32(0)).toBe('none');
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it('1 maps to fxaa', () => {
|
|
223
|
-
expect(antialiasFromF32(1)).toBe('fxaa');
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it('2 maps to msaa', () => {
|
|
227
|
-
expect(antialiasFromF32(2)).toBe('msaa');
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it('invalid value 99 throws RangeError with text containing the max valid value 2', () => {
|
|
231
|
-
expect(() => antialiasFromF32(99)).toThrow(RangeError);
|
|
232
|
-
expect(() => antialiasFromF32(99)).toThrow(/2/);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it('negative value throws RangeError with text containing 2', () => {
|
|
236
|
-
expect(() => antialiasFromF32(-1)).toThrow(RangeError);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
// Regression: verify constants match the mapping
|
|
240
|
-
it('ANTIALIAS_NONE matches mapping', () => {
|
|
241
|
-
expect(antialiasFromF32(ANTIALIAS_NONE)).toBe('none');
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it('ANTIALIAS_FXAA matches mapping', () => {
|
|
245
|
-
expect(antialiasFromF32(ANTIALIAS_FXAA)).toBe('fxaa');
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
234
|
}
|
|
249
235
|
|
|
250
236
|
{
|
|
@@ -332,11 +318,11 @@ function makeEnvironmentGenerationState() {
|
|
|
332
318
|
|
|
333
319
|
describe('Camera antialias schema', () => {
|
|
334
320
|
it('Camera schema includes antialias: f32 field', () => {
|
|
335
|
-
expect(
|
|
321
|
+
expect(componentFieldType(Camera, 'antialias')).toBe('f32');
|
|
336
322
|
});
|
|
337
323
|
|
|
338
324
|
it('Camera defaults antialias to 0 (ANTIALIAS_NONE)', () => {
|
|
339
|
-
expect(
|
|
325
|
+
expect(componentFieldDefault(Camera, 'antialias')).toBe(0);
|
|
340
326
|
});
|
|
341
327
|
});
|
|
342
328
|
|
|
@@ -349,46 +335,6 @@ function makeEnvironmentGenerationState() {
|
|
|
349
335
|
expect(ANTIALIAS_FXAA).toBe(1);
|
|
350
336
|
});
|
|
351
337
|
});
|
|
352
|
-
|
|
353
|
-
describe('antialiasFromF32 mapping', () => {
|
|
354
|
-
it('0 -> none', () => {
|
|
355
|
-
expect(antialiasFromF32(0)).toBe('none');
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
it('1 -> fxaa', () => {
|
|
359
|
-
expect(antialiasFromF32(1)).toBe('fxaa');
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
it('99 -> throws RangeError (fail-fast P3)', () => {
|
|
363
|
-
expect(() => antialiasFromF32(99)).toThrow(RangeError);
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
it('-1 -> throws RangeError (fail-fast P3)', () => {
|
|
367
|
-
expect(() => antialiasFromF32(-1)).toThrow(RangeError);
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
it('undefined-like NaN -> throws RangeError (fail-fast P3)', () => {
|
|
371
|
-
expect(() => antialiasFromF32(Number.NaN)).toThrow(RangeError);
|
|
372
|
-
});
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
describe('Antialias type union', () => {
|
|
376
|
-
it('antialiasFromF32 return type is assignable to Antialias', () => {
|
|
377
|
-
// Compile-time check: the return type annotation guarantees this.
|
|
378
|
-
const result: Antialias = antialiasFromF32(0);
|
|
379
|
-
expect(result).toBe('none');
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
it('antialiasFromF32(1) is assignable to Antialias', () => {
|
|
383
|
-
const result: Antialias = antialiasFromF32(1);
|
|
384
|
-
expect(result).toBe('fxaa');
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
it('antialiasFromF32(2) maps to msaa', () => {
|
|
388
|
-
const result: Antialias = antialiasFromF32(2);
|
|
389
|
-
expect(result).toBe('msaa');
|
|
390
|
-
});
|
|
391
|
-
});
|
|
392
338
|
}
|
|
393
339
|
|
|
394
340
|
{
|
|
@@ -396,35 +342,35 @@ function makeEnvironmentGenerationState() {
|
|
|
396
342
|
|
|
397
343
|
describe('Camera bloom schema', () => {
|
|
398
344
|
it('Camera schema includes bloom: f32 field', () => {
|
|
399
|
-
expect(
|
|
345
|
+
expect(componentFieldType(Camera, 'bloom')).toBe('f32');
|
|
400
346
|
});
|
|
401
347
|
|
|
402
348
|
it('Camera schema includes bloomThreshold: f32 field', () => {
|
|
403
|
-
expect(
|
|
349
|
+
expect(componentFieldType(Camera, 'bloomThreshold')).toBe('f32');
|
|
404
350
|
});
|
|
405
351
|
|
|
406
352
|
it('Camera schema includes bloomIntensity: f32 field', () => {
|
|
407
|
-
expect(
|
|
353
|
+
expect(componentFieldType(Camera, 'bloomIntensity')).toBe('f32');
|
|
408
354
|
});
|
|
409
355
|
|
|
410
356
|
it('Camera schema includes bloomBlurRadius: f32 field', () => {
|
|
411
|
-
expect(
|
|
357
|
+
expect(componentFieldType(Camera, 'bloomBlurRadius')).toBe('f32');
|
|
412
358
|
});
|
|
413
359
|
|
|
414
360
|
it('Camera defaults bloom to 0 (BLOOM_DISABLED)', () => {
|
|
415
|
-
expect(
|
|
361
|
+
expect(componentFieldDefault(Camera, 'bloom')).toBe(0);
|
|
416
362
|
});
|
|
417
363
|
|
|
418
364
|
it('Camera defaults bloomThreshold to 1.0', () => {
|
|
419
|
-
expect(
|
|
365
|
+
expect(componentFieldDefault(Camera, 'bloomThreshold')).toBe(1.0);
|
|
420
366
|
});
|
|
421
367
|
|
|
422
368
|
it('Camera defaults bloomIntensity to 1.0', () => {
|
|
423
|
-
expect(
|
|
369
|
+
expect(componentFieldDefault(Camera, 'bloomIntensity')).toBe(1.0);
|
|
424
370
|
});
|
|
425
371
|
|
|
426
372
|
it('Camera defaults bloomBlurRadius to 4.0', () => {
|
|
427
|
-
expect(
|
|
373
|
+
expect(componentFieldDefault(Camera, 'bloomBlurRadius')).toBe(4.0);
|
|
428
374
|
});
|
|
429
375
|
});
|
|
430
376
|
|
|
@@ -437,40 +383,6 @@ function makeEnvironmentGenerationState() {
|
|
|
437
383
|
expect(BLOOM_ENABLED).toBe(1);
|
|
438
384
|
});
|
|
439
385
|
});
|
|
440
|
-
|
|
441
|
-
describe('bloomEnabledFromF32 mapping', () => {
|
|
442
|
-
it('0 -> off', () => {
|
|
443
|
-
expect(bloomEnabledFromF32(0)).toBe('off');
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
it('1 -> on', () => {
|
|
447
|
-
expect(bloomEnabledFromF32(1)).toBe('on');
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
it('99 -> throws RangeError (fail-fast P3)', () => {
|
|
451
|
-
expect(() => bloomEnabledFromF32(99)).toThrow(RangeError);
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
it('-1 -> throws RangeError (fail-fast P3)', () => {
|
|
455
|
-
expect(() => bloomEnabledFromF32(-1)).toThrow(RangeError);
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
it('undefined-like NaN -> throws RangeError (fail-fast P3)', () => {
|
|
459
|
-
expect(() => bloomEnabledFromF32(Number.NaN)).toThrow(RangeError);
|
|
460
|
-
});
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
describe('BloomEnabled type union', () => {
|
|
464
|
-
it('bloomEnabledFromF32 return type is assignable to BloomEnabled', () => {
|
|
465
|
-
const result: 'on' | 'off' = bloomEnabledFromF32(0);
|
|
466
|
-
expect(result).toBe('off');
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
it('bloomEnabledFromF32(1) is assignable to BloomEnabled', () => {
|
|
470
|
-
const result: 'on' | 'off' = bloomEnabledFromF32(1);
|
|
471
|
-
expect(result).toBe('on');
|
|
472
|
-
});
|
|
473
|
-
});
|
|
474
386
|
}
|
|
475
387
|
|
|
476
388
|
{
|
|
@@ -478,18 +390,18 @@ function makeEnvironmentGenerationState() {
|
|
|
478
390
|
|
|
479
391
|
describe('Camera clear-color schema (feat-20260709 M3: array<f32,4>)', () => {
|
|
480
392
|
it('Camera schema includes clearColor: array<f32,4> field', () => {
|
|
481
|
-
expect(
|
|
393
|
+
expect(componentFieldType(Camera, 'clearColor')).toBe('array<f32, 4>');
|
|
482
394
|
});
|
|
483
395
|
|
|
484
396
|
it('per-axis clear scalars are gone (collapsed into clearColor)', () => {
|
|
485
|
-
expect('clearR' in
|
|
486
|
-
expect('clearG' in
|
|
487
|
-
expect('clearB' in
|
|
488
|
-
expect('clearA' in
|
|
397
|
+
expect('clearR' in Camera.fields).toBe(false);
|
|
398
|
+
expect('clearG' in Camera.fields).toBe(false);
|
|
399
|
+
expect('clearB' in Camera.fields).toBe(false);
|
|
400
|
+
expect('clearA' in Camera.fields).toBe(false);
|
|
489
401
|
});
|
|
490
402
|
|
|
491
403
|
it('Camera defaults clearColor to transparent black [0,0,0,0]', () => {
|
|
492
|
-
expect(Array.from(
|
|
404
|
+
expect(Array.from(componentFieldDefault(Camera, 'clearColor') as Float32Array)).toEqual([
|
|
493
405
|
0, 0, 0, 0,
|
|
494
406
|
]);
|
|
495
407
|
});
|
|
@@ -534,34 +446,34 @@ function makeEnvironmentGenerationState() {
|
|
|
534
446
|
];
|
|
535
447
|
}
|
|
536
448
|
|
|
537
|
-
describe('Camera schema (
|
|
538
|
-
it('
|
|
539
|
-
expect(Object.keys(
|
|
540
|
-
expect(
|
|
541
|
-
expect(
|
|
542
|
-
expect(
|
|
543
|
-
expect(
|
|
544
|
-
expect(
|
|
545
|
-
expect(
|
|
546
|
-
expect(
|
|
547
|
-
expect(
|
|
548
|
-
expect(
|
|
549
|
-
expect(componentSchema(Camera).historyVersion).toBe('u32');
|
|
449
|
+
describe('Camera schema (19 fields: 17 f32 + clearColor array + autoAspect bool after w9 + tonemap-mvp + fxaa + bloom + clearColor + aspect-sync extensions)', () => {
|
|
450
|
+
it('Camera.schema has 19 fields (17 f32 + clearColor array + autoAspect bool: perspective quartet + projection + ortho quartet + tonemap trio + antialias + bloom quartet + clearColor + autoAspect)', () => {
|
|
451
|
+
expect(Object.keys(Camera.fields).length).toBe(20);
|
|
452
|
+
expect(componentFieldType(Camera, 'fov')).toBe('f32');
|
|
453
|
+
expect(componentFieldType(Camera, 'aspect')).toBe('f32');
|
|
454
|
+
expect(componentFieldType(Camera, 'near')).toBe('f32');
|
|
455
|
+
expect(componentFieldType(Camera, 'far')).toBe('f32');
|
|
456
|
+
expect(componentFieldType(Camera, 'projection')).toBe('f32');
|
|
457
|
+
expect(componentFieldType(Camera, 'left')).toBe('f32');
|
|
458
|
+
expect(componentFieldType(Camera, 'right')).toBe('f32');
|
|
459
|
+
expect(componentFieldType(Camera, 'bottom')).toBe('f32');
|
|
460
|
+
expect(componentFieldType(Camera, 'top')).toBe('f32');
|
|
550
461
|
// feat-20260519-tonemap-reinhard-mvp / M1 / T-M1.2 (AC-01 + D-1).
|
|
551
|
-
expect(
|
|
552
|
-
expect(
|
|
553
|
-
expect(
|
|
554
|
-
expect(
|
|
462
|
+
expect(componentFieldType(Camera, 'tonemap')).toBe('f32');
|
|
463
|
+
expect(componentFieldType(Camera, 'exposure')).toBe('f32');
|
|
464
|
+
expect(componentFieldType(Camera, 'whitePoint')).toBe('f32');
|
|
465
|
+
expect(componentFieldType(Camera, 'antialias')).toBe('f32');
|
|
466
|
+
expect(componentFieldType(Camera, 'historyVersion')).toBe('u32');
|
|
555
467
|
// feat-20260531-bloom-first-declarative-render-graph-pass / w2.
|
|
556
|
-
expect(
|
|
557
|
-
expect(
|
|
558
|
-
expect(
|
|
559
|
-
expect(
|
|
468
|
+
expect(componentFieldType(Camera, 'bloom')).toBe('f32');
|
|
469
|
+
expect(componentFieldType(Camera, 'bloomThreshold')).toBe('f32');
|
|
470
|
+
expect(componentFieldType(Camera, 'bloomIntensity')).toBe('f32');
|
|
471
|
+
expect(componentFieldType(Camera, 'bloomBlurRadius')).toBe('f32');
|
|
560
472
|
// feat-20260709 M3 / D-3: clear-color collapsed into one array<f32,4>.
|
|
561
|
-
expect(
|
|
473
|
+
expect(componentFieldType(Camera, 'clearColor')).toBe('array<f32, 4>');
|
|
562
474
|
// feat-20260617-host-engine-contract-and-video-cutscene / M3: aspect-sync
|
|
563
475
|
// opt-out flag (bool column tier).
|
|
564
|
-
expect(
|
|
476
|
+
expect(componentFieldType(Camera, 'autoAspect')).toBe('bool');
|
|
565
477
|
});
|
|
566
478
|
});
|
|
567
479
|
|
|
@@ -772,15 +684,7 @@ function makeEnvironmentGenerationState() {
|
|
|
772
684
|
unlitPipeline: { __label: 'unlit' },
|
|
773
685
|
standardPipeline: { __label: 'standard' },
|
|
774
686
|
unlitPipelineHdr: null,
|
|
775
|
-
standardPipelineHdr: null,
|
|
776
687
|
shadowFallbackTextureView: { __label: 'shadow-fallback-view' },
|
|
777
|
-
shadowProbePipeline: null,
|
|
778
|
-
shadowProbeBindGroupLayout: null,
|
|
779
|
-
shadowProbeLsmUbo: null,
|
|
780
|
-
shadowProbeInputBuf: null,
|
|
781
|
-
shadowProbeOutputTex: null,
|
|
782
|
-
shadowProbeOutputView: null,
|
|
783
|
-
shadowProbeStagingBuf: null,
|
|
784
688
|
skylightFallback: null,
|
|
785
689
|
pointLightsBuffer: { __label: 'point-lights-buf' },
|
|
786
690
|
spotLightsBuffer: { __label: 'spot-lights-buf' },
|
|
@@ -847,7 +751,7 @@ function makeEnvironmentGenerationState() {
|
|
|
847
751
|
const { internals } = makeRecorderInternals(log);
|
|
848
752
|
const ps = makePipelineState();
|
|
849
753
|
(internals as { getPipelineState: () => unknown }).getPipelineState = () => ps;
|
|
850
|
-
const { recordFrame } = await import('
|
|
754
|
+
const { recordFrame } = await import('../../../render/src/record/frame');
|
|
851
755
|
recordFrame(
|
|
852
756
|
internals as never,
|
|
853
757
|
new World() as never,
|
|
@@ -857,7 +761,6 @@ function makeEnvironmentGenerationState() {
|
|
|
857
761
|
[],
|
|
858
762
|
{
|
|
859
763
|
frameNumber: 0,
|
|
860
|
-
environmentGeneration: makeEnvironmentGenerationState(),
|
|
861
764
|
compiledFrameGraph: null,
|
|
862
765
|
compiledFrameGraphTopologyKey: null,
|
|
863
766
|
retiredCompiledFrameGraphs: new Set(),
|
|
@@ -885,8 +788,10 @@ function makeEnvironmentGenerationState() {
|
|
|
885
788
|
},
|
|
886
789
|
{ unlit: 0 },
|
|
887
790
|
{ createBindGroup: 0, keys: [] },
|
|
888
|
-
|
|
889
|
-
|
|
791
|
+
undefined,
|
|
792
|
+
0,
|
|
793
|
+
undefined,
|
|
794
|
+
0,
|
|
890
795
|
);
|
|
891
796
|
|
|
892
797
|
// No FXAA intermediate texture should be allocated when antialias='none'.
|
|
@@ -901,7 +806,7 @@ function makeEnvironmentGenerationState() {
|
|
|
901
806
|
const { internals } = makeRecorderInternals(log);
|
|
902
807
|
const ps = makePipelineState();
|
|
903
808
|
(internals as { getPipelineState: () => unknown }).getPipelineState = () => ps;
|
|
904
|
-
const { recordFrame } = await import('
|
|
809
|
+
const { recordFrame } = await import('../../../render/src/record/frame');
|
|
905
810
|
recordFrame(
|
|
906
811
|
internals as never,
|
|
907
812
|
new World() as never,
|
|
@@ -911,7 +816,6 @@ function makeEnvironmentGenerationState() {
|
|
|
911
816
|
[],
|
|
912
817
|
{
|
|
913
818
|
frameNumber: 0,
|
|
914
|
-
environmentGeneration: makeEnvironmentGenerationState(),
|
|
915
819
|
compiledFrameGraph: null,
|
|
916
820
|
compiledFrameGraphTopologyKey: null,
|
|
917
821
|
retiredCompiledFrameGraphs: new Set(),
|
|
@@ -939,8 +843,10 @@ function makeEnvironmentGenerationState() {
|
|
|
939
843
|
},
|
|
940
844
|
{ unlit: 0 },
|
|
941
845
|
{ createBindGroup: 0, keys: [] },
|
|
942
|
-
|
|
943
|
-
|
|
846
|
+
undefined,
|
|
847
|
+
0,
|
|
848
|
+
undefined,
|
|
849
|
+
0,
|
|
944
850
|
);
|
|
945
851
|
|
|
946
852
|
// TODO (feat-20260608-ci-time-cut): row 2/3/4 placeholders pruned -- they
|
|
@@ -1200,29 +1106,29 @@ function makeEnvironmentGenerationState() {
|
|
|
1200
1106
|
// --- from ibl-pipeline-cache.test.ts ---
|
|
1201
1107
|
|
|
1202
1108
|
describe('t11 - IblPipelineCache counter invariant + deferred-replay', () => {
|
|
1203
|
-
// (a) IblPipelineCache initializes per
|
|
1204
|
-
it('IblPipelineCache maintains per-
|
|
1205
|
-
const
|
|
1206
|
-
const
|
|
1109
|
+
// (a) IblPipelineCache initializes one cache per DeviceScope generation
|
|
1110
|
+
it('IblPipelineCache maintains per-scope ownership via getOrCreateIblCache', () => {
|
|
1111
|
+
const scope1 = DeviceScope.create(1, 'systems-test-1');
|
|
1112
|
+
const scope2 = DeviceScope.create(2, 'systems-test-2');
|
|
1207
1113
|
|
|
1208
|
-
const cache1 = getOrCreateIblCache(
|
|
1209
|
-
const cache2 = getOrCreateIblCache(
|
|
1114
|
+
const cache1 = getOrCreateIblCache(scope1);
|
|
1115
|
+
const cache2 = getOrCreateIblCache(scope2);
|
|
1210
1116
|
|
|
1211
1117
|
// Different devices get different cache instances.
|
|
1212
1118
|
expect(cache1).not.toBe(cache2);
|
|
1213
1119
|
|
|
1214
1120
|
// Same device returns same instance.
|
|
1215
|
-
expect(getOrCreateIblCache(
|
|
1121
|
+
expect(getOrCreateIblCache(scope1)).toBe(cache1);
|
|
1216
1122
|
|
|
1217
1123
|
// hasIblCache confirms registration.
|
|
1218
|
-
expect(hasIblCache(
|
|
1219
|
-
expect(hasIblCache(
|
|
1220
|
-
expect(hasIblCache(
|
|
1124
|
+
expect(hasIblCache(scope1)).toBe(true);
|
|
1125
|
+
expect(hasIblCache(scope2)).toBe(true);
|
|
1126
|
+
expect(hasIblCache(DeviceScope.create(3, 'systems-test-3'))).toBe(false);
|
|
1221
1127
|
});
|
|
1222
1128
|
|
|
1223
1129
|
// (d) counters initialized at 0
|
|
1224
1130
|
it('counters are initialized at 0 before any pass execution', () => {
|
|
1225
|
-
const cache = getOrCreateIblCache(
|
|
1131
|
+
const cache = getOrCreateIblCache(DeviceScope.create(4, 'systems-test-4'));
|
|
1226
1132
|
expect(cache.irradianceBakeCount).toBe(0);
|
|
1227
1133
|
expect(cache.prefilterBakeCount).toBe(0);
|
|
1228
1134
|
expect(cache.brdfLutBakeCount).toBe(0);
|
|
@@ -1233,7 +1139,7 @@ function makeEnvironmentGenerationState() {
|
|
|
1233
1139
|
// The actual GPU pass execution that sets these counters is wired in
|
|
1234
1140
|
// uploadCubemapFromEquirect (t20 in asset-registry.ts).
|
|
1235
1141
|
it('iblPrepass counters can be incremented post-execution', () => {
|
|
1236
|
-
const cache = getOrCreateIblCache(
|
|
1142
|
+
const cache = getOrCreateIblCache(DeviceScope.create(5, 'systems-test-5'));
|
|
1237
1143
|
cache.irradianceBakeCount += 1;
|
|
1238
1144
|
cache.prefilterBakeCount += 1;
|
|
1239
1145
|
cache.brdfLutBakeCount += 1;
|
|
@@ -1243,10 +1149,10 @@ function makeEnvironmentGenerationState() {
|
|
|
1243
1149
|
});
|
|
1244
1150
|
|
|
1245
1151
|
it('iblPrepass counters stay independent across devices', () => {
|
|
1246
|
-
const
|
|
1247
|
-
const
|
|
1248
|
-
const cacheA = getOrCreateIblCache(
|
|
1249
|
-
const cacheB = getOrCreateIblCache(
|
|
1152
|
+
const scopeA = DeviceScope.create(6, 'systems-test-6');
|
|
1153
|
+
const scopeB = DeviceScope.create(7, 'systems-test-7');
|
|
1154
|
+
const cacheA = getOrCreateIblCache(scopeA);
|
|
1155
|
+
const cacheB = getOrCreateIblCache(scopeB);
|
|
1250
1156
|
|
|
1251
1157
|
cacheA.irradianceBakeCount += 1;
|
|
1252
1158
|
cacheB.prefilterBakeCount += 1;
|
|
@@ -1266,7 +1172,7 @@ function makeEnvironmentGenerationState() {
|
|
|
1266
1172
|
{
|
|
1267
1173
|
// --- from ibl-runtime-probe.test.ts ---
|
|
1268
1174
|
|
|
1269
|
-
const
|
|
1175
|
+
const _mockCaps = {
|
|
1270
1176
|
backendKind: 'webgpu' as const,
|
|
1271
1177
|
compute: true,
|
|
1272
1178
|
timestampQuery: false,
|
|
@@ -1287,8 +1193,8 @@ function makeEnvironmentGenerationState() {
|
|
|
1287
1193
|
float32Filterable: false,
|
|
1288
1194
|
};
|
|
1289
1195
|
|
|
1290
|
-
// feat-20260601-gpu-
|
|
1291
|
-
// moved to
|
|
1196
|
+
// feat-20260601-device/gpu-residency-extraction M1: uploadCubemapFromEquirect
|
|
1197
|
+
// moved to GpuResidencyCache (D-3 register-relay injected at configureGpuDevice,
|
|
1292
1198
|
// source POD passed to the call; store holds no registry reference).
|
|
1293
1199
|
|
|
1294
1200
|
interface MockEncoder {
|
|
@@ -1320,7 +1226,7 @@ function makeEnvironmentGenerationState() {
|
|
|
1320
1226
|
}
|
|
1321
1227
|
|
|
1322
1228
|
// biome-ignore lint/suspicious/noExplicitAny: opaque mock GPU device surface
|
|
1323
|
-
function
|
|
1229
|
+
function _makeMockDevice(probe: MockSubmitProbe): any {
|
|
1324
1230
|
const mockShader = { __mock: 'shader' };
|
|
1325
1231
|
const mockPipeline = { __mock: 'pipeline' };
|
|
1326
1232
|
const mockBgl = { __mock: 'bgl' };
|
|
@@ -1372,7 +1278,7 @@ function makeEnvironmentGenerationState() {
|
|
|
1372
1278
|
};
|
|
1373
1279
|
}
|
|
1374
1280
|
|
|
1375
|
-
function
|
|
1281
|
+
function _makeEquirect(): {
|
|
1376
1282
|
kind: 'equirect';
|
|
1377
1283
|
width: number;
|
|
1378
1284
|
height: number;
|
|
@@ -1389,128 +1295,6 @@ function makeEnvironmentGenerationState() {
|
|
|
1389
1295
|
colorSpace: 'linear',
|
|
1390
1296
|
};
|
|
1391
1297
|
}
|
|
1392
|
-
|
|
1393
|
-
describe('t50 (M3.5) -- AC-20 runtime probe via mock device', () => {
|
|
1394
|
-
it('(a) uploadCubemapFromEquirect calls queue.submit >= 1', async () => {
|
|
1395
|
-
const probe: MockSubmitProbe = {
|
|
1396
|
-
submitCalls: 0,
|
|
1397
|
-
encoders: [],
|
|
1398
|
-
shouldThrowOnSubmit: false,
|
|
1399
|
-
};
|
|
1400
|
-
const device = makeMockDevice(probe);
|
|
1401
|
-
|
|
1402
|
-
const equirect = makeEquirect();
|
|
1403
|
-
const store = new GpuResourceStore();
|
|
1404
|
-
const world = new World();
|
|
1405
|
-
const equirectHandle = world.allocSharedRef('EquirectAsset', equirect);
|
|
1406
|
-
|
|
1407
|
-
store.configureGpuDevice(
|
|
1408
|
-
device,
|
|
1409
|
-
async (_d, desc) =>
|
|
1410
|
-
// biome-ignore lint/suspicious/noExplicitAny: mock shader module
|
|
1411
|
-
rhiOk({ __mock: 'shader', label: desc.label ?? '' }) as any,
|
|
1412
|
-
(w: World, pod: EquirectAsset) => rhiOk(w.allocSharedRef('EquirectAsset', pod)),
|
|
1413
|
-
mockCaps,
|
|
1414
|
-
);
|
|
1415
|
-
|
|
1416
|
-
// biome-ignore lint/suspicious/noExplicitAny: package-internal method reached via store cast
|
|
1417
|
-
const result = await (store as any)._uploadCubemapFromEquirect(
|
|
1418
|
-
world,
|
|
1419
|
-
equirectHandle,
|
|
1420
|
-
equirect,
|
|
1421
|
-
);
|
|
1422
|
-
expect(result.ok).toBe(true);
|
|
1423
|
-
expect(probe.submitCalls).toBeGreaterThanOrEqual(1);
|
|
1424
|
-
});
|
|
1425
|
-
|
|
1426
|
-
it('(b) encoder contains 4 beginRenderPass calls (equirect / irradiance / prefilter / brdf-lut)', async () => {
|
|
1427
|
-
const probe: MockSubmitProbe = {
|
|
1428
|
-
submitCalls: 0,
|
|
1429
|
-
encoders: [],
|
|
1430
|
-
shouldThrowOnSubmit: false,
|
|
1431
|
-
};
|
|
1432
|
-
const device = makeMockDevice(probe);
|
|
1433
|
-
|
|
1434
|
-
const equirect = makeEquirect();
|
|
1435
|
-
const store = new GpuResourceStore();
|
|
1436
|
-
const world = new World();
|
|
1437
|
-
const equirectHandle = world.allocSharedRef('EquirectAsset', equirect);
|
|
1438
|
-
|
|
1439
|
-
store.configureGpuDevice(
|
|
1440
|
-
device,
|
|
1441
|
-
async (_d, desc) =>
|
|
1442
|
-
// biome-ignore lint/suspicious/noExplicitAny: mock shader module
|
|
1443
|
-
rhiOk({ __mock: 'shader', label: desc.label ?? '' }) as any,
|
|
1444
|
-
(w: World, pod: EquirectAsset) => rhiOk(w.allocSharedRef('EquirectAsset', pod)),
|
|
1445
|
-
mockCaps,
|
|
1446
|
-
);
|
|
1447
|
-
|
|
1448
|
-
// biome-ignore lint/suspicious/noExplicitAny: package-internal method reached via store cast
|
|
1449
|
-
await (store as any)._uploadCubemapFromEquirect(world, equirectHandle, equirect);
|
|
1450
|
-
|
|
1451
|
-
const totalBeginPass = probe.encoders.reduce((s, e) => s + e.beginRenderPassCount, 0);
|
|
1452
|
-
// 4 distinct pass families; each cube-face family unfolds to 6 sub-passes
|
|
1453
|
-
// (24 for equirect+irradiance), prefilter to 30 (5 mip x 6 face),
|
|
1454
|
-
// brdf-lut 1. Total ~ 55. We assert minimum 4 distinct pass labels.
|
|
1455
|
-
const labels = new Set(
|
|
1456
|
-
probe.encoders
|
|
1457
|
-
.flatMap((e) => e.passes.map((p) => p.label ?? ''))
|
|
1458
|
-
.map((l) => {
|
|
1459
|
-
// strip face / mip suffix for family grouping
|
|
1460
|
-
return l.replace(/-face\d+/, '').replace(/-mip\d+/, '');
|
|
1461
|
-
}),
|
|
1462
|
-
);
|
|
1463
|
-
expect(totalBeginPass).toBeGreaterThanOrEqual(4);
|
|
1464
|
-
expect(labels.has('ibl-equirect-to-cube')).toBe(true);
|
|
1465
|
-
expect(labels.has('ibl-irradiance')).toBe(true);
|
|
1466
|
-
expect(labels.has('ibl-prefilter')).toBe(true);
|
|
1467
|
-
expect(labels.has('ibl-brdf-lut')).toBe(true);
|
|
1468
|
-
});
|
|
1469
|
-
|
|
1470
|
-
it('(c) AC-20 critical: counters stay at 0 when queue.submit throws', async () => {
|
|
1471
|
-
const probe: MockSubmitProbe = {
|
|
1472
|
-
submitCalls: 0,
|
|
1473
|
-
encoders: [],
|
|
1474
|
-
shouldThrowOnSubmit: true,
|
|
1475
|
-
};
|
|
1476
|
-
const device = makeMockDevice(probe);
|
|
1477
|
-
|
|
1478
|
-
const equirect = makeEquirect();
|
|
1479
|
-
const store = new GpuResourceStore();
|
|
1480
|
-
const world = new World();
|
|
1481
|
-
const equirectHandle = world.allocSharedRef('EquirectAsset', equirect);
|
|
1482
|
-
|
|
1483
|
-
store.configureGpuDevice(
|
|
1484
|
-
device,
|
|
1485
|
-
async (_d, desc) =>
|
|
1486
|
-
// biome-ignore lint/suspicious/noExplicitAny: mock shader module
|
|
1487
|
-
rhiOk({ __mock: 'shader', label: desc.label ?? '' }) as any,
|
|
1488
|
-
(w: World, pod: EquirectAsset) => rhiOk(w.allocSharedRef('EquirectAsset', pod)),
|
|
1489
|
-
mockCaps,
|
|
1490
|
-
);
|
|
1491
|
-
|
|
1492
|
-
// biome-ignore lint/suspicious/noExplicitAny: package-internal method reached via store cast
|
|
1493
|
-
const result = await (store as any)._uploadCubemapFromEquirect(
|
|
1494
|
-
world,
|
|
1495
|
-
equirectHandle,
|
|
1496
|
-
equirect,
|
|
1497
|
-
);
|
|
1498
|
-
|
|
1499
|
-
// Whether result.ok is true or false depends on impl error propagation;
|
|
1500
|
-
// the critical assertion is the counter invariant -- counters must NOT
|
|
1501
|
-
// increment when submit fails. counter += 1 placed BEFORE submit is the
|
|
1502
|
-
// round-1 anti-pattern; this assertion fails in that case.
|
|
1503
|
-
const cache = getOrCreateIblCache(device);
|
|
1504
|
-
expect(cache.irradianceBakeCount).toBe(0);
|
|
1505
|
-
expect(cache.prefilterBakeCount).toBe(0);
|
|
1506
|
-
expect(cache.brdfLutBakeCount).toBe(0);
|
|
1507
|
-
|
|
1508
|
-
// If the impl chose to surface submit failure structurally, result.ok
|
|
1509
|
-
// is false. We allow either outcome here -- the load-bearing fact is
|
|
1510
|
-
// counter == 0.
|
|
1511
|
-
void result;
|
|
1512
|
-
});
|
|
1513
|
-
});
|
|
1514
1298
|
}
|
|
1515
1299
|
|
|
1516
1300
|
{
|
|
@@ -1806,6 +1590,7 @@ function makeEnvironmentGenerationState() {
|
|
|
1806
1590
|
limits: {},
|
|
1807
1591
|
queue: {
|
|
1808
1592
|
submit: () => undefined,
|
|
1593
|
+
onSubmittedWorkDone: () => Promise.resolve(undefined),
|
|
1809
1594
|
writeBuffer: () => undefined,
|
|
1810
1595
|
writeTexture: () => undefined,
|
|
1811
1596
|
},
|
|
@@ -1859,7 +1644,7 @@ function makeEnvironmentGenerationState() {
|
|
|
1859
1644
|
identifier,
|
|
1860
1645
|
sourcePath: `${identifier}.wgsl`,
|
|
1861
1646
|
composedWgsl,
|
|
1862
|
-
paramSchema:
|
|
1647
|
+
paramSchema: '[]',
|
|
1863
1648
|
variants: [],
|
|
1864
1649
|
});
|
|
1865
1650
|
const manifest = {
|
|
@@ -1886,15 +1671,10 @@ function makeEnvironmentGenerationState() {
|
|
|
1886
1671
|
return `data:application/json,${encodeURIComponent(JSON.stringify(manifest))}`;
|
|
1887
1672
|
}
|
|
1888
1673
|
|
|
1889
|
-
|
|
1890
|
-
ready: Promise<void>;
|
|
1891
|
-
draw: (worlds: unknown, opts: { cameraOwner: number; resourceOwner: number }) => void;
|
|
1892
|
-
onError: (cb: (err: { code: string }) => void) => () => void;
|
|
1893
|
-
assets: { register: (asset: unknown) => { ok: boolean; value: unknown } };
|
|
1894
|
-
}
|
|
1674
|
+
type RendererLike = RendererType;
|
|
1895
1675
|
|
|
1896
1676
|
async function importEngine(): Promise<{
|
|
1897
|
-
createRenderer: (
|
|
1677
|
+
createRenderer: (...args: unknown[]) => Promise<{ unwrap(): RendererLike }>;
|
|
1898
1678
|
}> {
|
|
1899
1679
|
return (await import(ENGINE)) as never;
|
|
1900
1680
|
}
|
|
@@ -1913,7 +1693,7 @@ function makeEnvironmentGenerationState() {
|
|
|
1913
1693
|
DirectionalLight: unknown;
|
|
1914
1694
|
}> {
|
|
1915
1695
|
return {
|
|
1916
|
-
...(await import('@forgeax/engine-render
|
|
1696
|
+
...(await import('@forgeax/engine-render')),
|
|
1917
1697
|
...(await import('@forgeax/engine-scene')),
|
|
1918
1698
|
} as never;
|
|
1919
1699
|
}
|
|
@@ -1939,13 +1719,14 @@ function makeEnvironmentGenerationState() {
|
|
|
1939
1719
|
kind: 'mesh',
|
|
1940
1720
|
vertices: new Float32Array(3 * 12),
|
|
1941
1721
|
indices: new Uint16Array([0, 1, 2]),
|
|
1942
|
-
attributes:
|
|
1722
|
+
attributes: { position: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]) },
|
|
1723
|
+
aabb: new Float32Array([-1, -1, -1, 1, 1, 1]),
|
|
1943
1724
|
materialSlots: [{ slotName: 'Default' }],
|
|
1944
1725
|
submeshes: [
|
|
1945
1726
|
{
|
|
1946
1727
|
indexOffset: 0,
|
|
1947
1728
|
indexCount: 3,
|
|
1948
|
-
vertexCount:
|
|
1729
|
+
vertexCount: 3,
|
|
1949
1730
|
materialSlot: 0,
|
|
1950
1731
|
topology: 'triangle-list',
|
|
1951
1732
|
},
|
|
@@ -1958,13 +1739,14 @@ function makeEnvironmentGenerationState() {
|
|
|
1958
1739
|
kind: 'mesh',
|
|
1959
1740
|
vertices: new Float32Array(2 * 12),
|
|
1960
1741
|
indices: new Uint16Array([0, 1]),
|
|
1961
|
-
attributes:
|
|
1742
|
+
attributes: { position: new Float32Array([0, 0, 0, 1, 0, 0]) },
|
|
1743
|
+
aabb: new Float32Array([-1, -1, -1, 1, 1, 1]),
|
|
1962
1744
|
materialSlots: [{ slotName: 'Default' }],
|
|
1963
1745
|
submeshes: [
|
|
1964
1746
|
{
|
|
1965
1747
|
indexOffset: 0,
|
|
1966
1748
|
indexCount: 2,
|
|
1967
|
-
vertexCount:
|
|
1749
|
+
vertexCount: 2,
|
|
1968
1750
|
materialSlot: 0,
|
|
1969
1751
|
topology: 'line-list',
|
|
1970
1752
|
},
|
|
@@ -1979,12 +1761,9 @@ function makeEnvironmentGenerationState() {
|
|
|
1979
1761
|
const { device } = makeMockGPUDevice(shadow, main);
|
|
1980
1762
|
vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
|
|
1981
1763
|
const { createRenderer } = await importEngine();
|
|
1982
|
-
const renderer =
|
|
1983
|
-
makeMockCanvas(),
|
|
1984
|
-
|
|
1985
|
-
{ shaderManifestUrl: buildManifestDataUrl() },
|
|
1986
|
-
);
|
|
1987
|
-
await renderer.ready;
|
|
1764
|
+
const renderer = (
|
|
1765
|
+
await createRenderer(makeMockCanvas(), {}, { shaderManifestUrl: buildManifestDataUrl() })
|
|
1766
|
+
).unwrap();
|
|
1988
1767
|
return { renderer };
|
|
1989
1768
|
}
|
|
1990
1769
|
|
|
@@ -2002,26 +1781,11 @@ function makeEnvironmentGenerationState() {
|
|
|
2002
1781
|
const main = makePassSpies();
|
|
2003
1782
|
const { renderer } = await setupRenderer(shadow, main);
|
|
2004
1783
|
const errors: string[] = [];
|
|
2005
|
-
renderer
|
|
1784
|
+
subscribeRendererErrors(renderer, (e) => errors.push(e.code));
|
|
2006
1785
|
|
|
2007
1786
|
const { World } = await importEcs();
|
|
2008
1787
|
const C = await importComponents();
|
|
2009
1788
|
const world = new World();
|
|
2010
|
-
for (const component of [
|
|
2011
|
-
C.Camera,
|
|
2012
|
-
C.DirectionalLight,
|
|
2013
|
-
C.Transform,
|
|
2014
|
-
C.MeshRenderer,
|
|
2015
|
-
C.MeshFilter,
|
|
2016
|
-
]) {
|
|
2017
|
-
(
|
|
2018
|
-
world as unknown as {
|
|
2019
|
-
components: { register: (component: unknown) => { unwrap: () => unknown } };
|
|
2020
|
-
}
|
|
2021
|
-
).components
|
|
2022
|
-
.register(component)
|
|
2023
|
-
.unwrap();
|
|
2024
|
-
}
|
|
2025
1789
|
|
|
2026
1790
|
const triHandle = world.allocSharedRef('MeshAsset', indexedTriangleMesh()) as Handle<
|
|
2027
1791
|
'MeshAsset',
|
|
@@ -2052,7 +1816,7 @@ function makeEnvironmentGenerationState() {
|
|
|
2052
1816
|
world.spawn(
|
|
2053
1817
|
{
|
|
2054
1818
|
component: C.DirectionalLight,
|
|
2055
|
-
data: { direction: [
|
|
1819
|
+
data: { direction: [0, -1, 0], mapSize: 512, cascadeCount: 1 },
|
|
2056
1820
|
},
|
|
2057
1821
|
{ component: C.Transform, data: cameraTransform() },
|
|
2058
1822
|
);
|
|
@@ -2067,27 +1831,22 @@ function makeEnvironmentGenerationState() {
|
|
|
2067
1831
|
{ component: C.Transform, data: originTransform() },
|
|
2068
1832
|
);
|
|
2069
1833
|
|
|
2070
|
-
//
|
|
2071
|
-
//
|
|
2072
|
-
//
|
|
2073
|
-
//
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
throw new Error('World attachment failed');
|
|
2078
|
-
}
|
|
2079
|
-
(world as WorldType).update().unwrap();
|
|
2080
|
-
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
1834
|
+
// The first frame exercises the real shadow pass. A second frame proves
|
|
1835
|
+
// the persistent directional-shadow cache can reuse that result without
|
|
1836
|
+
// re-recording the depth draws. Yield so any async shader-module
|
|
1837
|
+
// creation settles before the cached frame.
|
|
1838
|
+
drawPublished(renderer, world as WorldType);
|
|
1839
|
+
const firstShadowDrawCalls =
|
|
1840
|
+
shadow.drawIndexed.mock.calls.length + shadow.draw.mock.calls.length;
|
|
2081
1841
|
await new Promise((r) => setTimeout(r, 0));
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
(world as WorldType).update().unwrap();
|
|
2086
|
-
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
1842
|
+
shadow.drawIndexed.mockClear();
|
|
1843
|
+
shadow.draw.mockClear();
|
|
1844
|
+
drawPublished(renderer, world as WorldType);
|
|
2087
1845
|
|
|
2088
1846
|
// Shadow pass drew exactly once (the triangle mesh), never the line mesh.
|
|
2089
1847
|
const shadowDrawCalls = shadow.drawIndexed.mock.calls.length + shadow.draw.mock.calls.length;
|
|
2090
|
-
expect(
|
|
1848
|
+
expect(firstShadowDrawCalls).toBe(1);
|
|
1849
|
+
expect(shadowDrawCalls).toBe(0);
|
|
2091
1850
|
expect(errors).toEqual([]);
|
|
2092
1851
|
});
|
|
2093
1852
|
});
|
|
@@ -2154,8 +1913,6 @@ function makeEnvironmentGenerationState() {
|
|
|
2154
1913
|
return 'mesh-ssbo-capacity';
|
|
2155
1914
|
case 'mesh-ssbo-ceiling-reached':
|
|
2156
1915
|
return 'mesh-ssbo-ceiling';
|
|
2157
|
-
case 'hdrp-caps-insufficient':
|
|
2158
|
-
return 'hdrp-caps';
|
|
2159
1916
|
case 'hdrp-light-budget-exceeded':
|
|
2160
1917
|
return 'hdrp-budget';
|
|
2161
1918
|
case 'hdrp-index-list-overflow':
|
|
@@ -2203,22 +1960,6 @@ function makeEnvironmentGenerationState() {
|
|
|
2203
1960
|
expect(e.detail).toEqual({ entity: 99 });
|
|
2204
1961
|
});
|
|
2205
1962
|
|
|
2206
|
-
it('VertexStorageBufferUnavailableError has .code .expected .hint', () => {
|
|
2207
|
-
const e = new VertexStorageBufferUnavailableError();
|
|
2208
|
-
expect(e.code).toBe('vertex-storage-buffer-unavailable');
|
|
2209
|
-
expect(e.expected).toContain('maxStorageBuffersPerShaderStage');
|
|
2210
|
-
expect(e.hint.length).toBeGreaterThan(0);
|
|
2211
|
-
});
|
|
2212
|
-
|
|
2213
|
-
it('SkinPaletteOverflowError has .code .expected .hint .detail', () => {
|
|
2214
|
-
const e = new SkinPaletteOverflowError(10000, 128 * 1024 * 1024);
|
|
2215
|
-
expect(e.code).toBe('skin-palette-overflow');
|
|
2216
|
-
expect(e.expected).toContain('maxStorageBufferBindingSize');
|
|
2217
|
-
expect(e.hint.length).toBeGreaterThan(0);
|
|
2218
|
-
expect(e.detail.requestedBytes).toBe(10000);
|
|
2219
|
-
expect(e.detail.limit).toBe(128 * 1024 * 1024);
|
|
2220
|
-
});
|
|
2221
|
-
|
|
2222
1963
|
it('all 6 skin-animation error codes are in the RuntimeErrorCode union (type-level)', () => {
|
|
2223
1964
|
const codes: RuntimeLayerErrorCode[] = [
|
|
2224
1965
|
'skin-joint-count-exceeded',
|
|
@@ -2244,20 +1985,20 @@ function makeEnvironmentGenerationState() {
|
|
|
2244
1985
|
|
|
2245
1986
|
describe('equirect-projection-failed error class shape', () => {
|
|
2246
1987
|
it('has .code === equirect-projection-failed', async () => {
|
|
2247
|
-
const { EquirectProjectionFailedError } = await import('
|
|
1988
|
+
const { EquirectProjectionFailedError } = await import('../../../render/src/errors/render');
|
|
2248
1989
|
const err = new EquirectProjectionFailedError(42);
|
|
2249
1990
|
expect(err.code).toBe('equirect-projection-failed');
|
|
2250
1991
|
});
|
|
2251
1992
|
|
|
2252
1993
|
it('exposes .detail.handle === the constructor argument', async () => {
|
|
2253
|
-
const { EquirectProjectionFailedError } = await import('
|
|
1994
|
+
const { EquirectProjectionFailedError } = await import('../../../render/src/errors/render');
|
|
2254
1995
|
const err = new EquirectProjectionFailedError(42);
|
|
2255
1996
|
expect(err.detail).toBeDefined();
|
|
2256
1997
|
expect(err.detail.handle).toBe(42);
|
|
2257
1998
|
});
|
|
2258
1999
|
|
|
2259
2000
|
it('exposes non-empty .hint with actionable guidance', async () => {
|
|
2260
|
-
const { EquirectProjectionFailedError } = await import('
|
|
2001
|
+
const { EquirectProjectionFailedError } = await import('../../../render/src/errors/render');
|
|
2261
2002
|
const err = new EquirectProjectionFailedError(42);
|
|
2262
2003
|
expect(err.hint).toBeDefined();
|
|
2263
2004
|
expect(typeof err.hint).toBe('string');
|
|
@@ -2265,7 +2006,7 @@ function makeEnvironmentGenerationState() {
|
|
|
2265
2006
|
});
|
|
2266
2007
|
|
|
2267
2008
|
it('exposes .expected describing expected state', async () => {
|
|
2268
|
-
const { EquirectProjectionFailedError } = await import('
|
|
2009
|
+
const { EquirectProjectionFailedError } = await import('../../../render/src/errors/render');
|
|
2269
2010
|
const err = new EquirectProjectionFailedError(42);
|
|
2270
2011
|
expect(err.expected).toBeDefined();
|
|
2271
2012
|
expect(typeof err.expected).toBe('string');
|
|
@@ -2273,7 +2014,7 @@ function makeEnvironmentGenerationState() {
|
|
|
2273
2014
|
});
|
|
2274
2015
|
|
|
2275
2016
|
it('extends Error so it can be thrown and caught', async () => {
|
|
2276
|
-
const { EquirectProjectionFailedError } = await import('
|
|
2017
|
+
const { EquirectProjectionFailedError } = await import('../../../render/src/errors/render');
|
|
2277
2018
|
const err = new EquirectProjectionFailedError(42);
|
|
2278
2019
|
expect(err).toBeInstanceOf(Error);
|
|
2279
2020
|
expect(err.name).toBe('EquirectProjectionFailedError');
|
|
@@ -2285,7 +2026,7 @@ function makeEnvironmentGenerationState() {
|
|
|
2285
2026
|
// The actual TS union membership is verified by typecheck: if
|
|
2286
2027
|
// 'equirect-projection-failed' is not in RuntimeErrorCode, no expression
|
|
2287
2028
|
// can assign err.code to a RuntimeErrorCode-typed variable.
|
|
2288
|
-
const { EquirectProjectionFailedError } = await import('
|
|
2029
|
+
const { EquirectProjectionFailedError } = await import('../../../render/src/errors/render');
|
|
2289
2030
|
const err = new EquirectProjectionFailedError(42);
|
|
2290
2031
|
expect(err.code).toBe('equirect-projection-failed');
|
|
2291
2032
|
});
|
|
@@ -2297,7 +2038,7 @@ function makeEnvironmentGenerationState() {
|
|
|
2297
2038
|
// the equirect handle, it binds the white-cube fallback and fires this
|
|
2298
2039
|
// structured error ONCE (it does not retry; R-2/AC-09). The record-stage
|
|
2299
2040
|
// integration is covered by the M3 lazy-projection tests + smoke gate.
|
|
2300
|
-
const { EquirectProjectionFailedError } = await import('
|
|
2041
|
+
const { EquirectProjectionFailedError } = await import('../../../render/src/errors/render');
|
|
2301
2042
|
const err = new EquirectProjectionFailedError(42);
|
|
2302
2043
|
expect(err.code).toBe('equirect-projection-failed');
|
|
2303
2044
|
expect(err.detail.handle).toBe(42);
|
|
@@ -2339,9 +2080,7 @@ function makeEnvironmentGenerationState() {
|
|
|
2339
2080
|
});
|
|
2340
2081
|
|
|
2341
2082
|
it('imports View from forgeax_view::common', () => {
|
|
2342
|
-
expect(source).toEqual(
|
|
2343
|
-
expect.stringContaining('#import forgeax_view::common::{View, FogViewParams, FogRay}'),
|
|
2344
|
-
);
|
|
2083
|
+
expect(source).toEqual(expect.stringContaining('#import forgeax_view::common::{View'));
|
|
2345
2084
|
});
|
|
2346
2085
|
|
|
2347
2086
|
it('imports fullscreen_triangle from forgeax_view::common', () => {
|
|
@@ -2899,11 +2638,11 @@ function makeEnvironmentGenerationState() {
|
|
|
2899
2638
|
});
|
|
2900
2639
|
});
|
|
2901
2640
|
|
|
2902
|
-
// ─── (e) unlit material BG
|
|
2641
|
+
// ─── (e) unlit material BG stays 7 entries ──────────────────────────────────
|
|
2903
2642
|
|
|
2904
2643
|
describe('t58 (M4 round-4) -- unlit material BG isolation', () => {
|
|
2905
2644
|
it('(e) buildUnlitMaterialBindGroupEntries returns 7 entries (no Skylight contamination)', async () => {
|
|
2906
|
-
const mod = await import('
|
|
2645
|
+
const mod = await import('../../../render/src/pbr-pipeline');
|
|
2907
2646
|
const buildFn = (mod as { buildUnlitMaterialBindGroupEntries?: unknown })
|
|
2908
2647
|
.buildUnlitMaterialBindGroupEntries;
|
|
2909
2648
|
expect(typeof buildFn).toBe('function');
|
|
@@ -3015,24 +2754,24 @@ function makeEnvironmentGenerationState() {
|
|
|
3015
2754
|
expect(layout?.label).toBe('pbr-pl');
|
|
3016
2755
|
});
|
|
3017
2756
|
|
|
3018
|
-
it('(b) PBR material BGL entry count ===
|
|
2757
|
+
it('(b) PBR material BGL entry count === 24 (user region 0..12 + Skylight 13..19 + lightmap 20..23)', () => {
|
|
3019
2758
|
const device = makeMockDevice();
|
|
3020
2759
|
// biome-ignore lint/suspicious/noExplicitAny: structural mock
|
|
3021
2760
|
buildPbrPipelineLayouts(device as any, STORAGE_CAPS);
|
|
3022
2761
|
const materialBgl = device.capturedBgls.find((b) => b.label === 'pbr-material-skylight-bgl');
|
|
3023
2762
|
expect(materialBgl).toBeDefined();
|
|
3024
|
-
expect(materialBgl?.entries).toHaveLength(
|
|
2763
|
+
expect(materialBgl?.entries).toHaveLength(24);
|
|
3025
2764
|
});
|
|
3026
2765
|
|
|
3027
|
-
it('(c) binding indices 0..
|
|
2766
|
+
it('(c) binding indices 0..23 in order; 13..19 resource types in D-5 round-4 order; 20..23 lightmap', () => {
|
|
3028
2767
|
const device = makeMockDevice();
|
|
3029
2768
|
// biome-ignore lint/suspicious/noExplicitAny: structural mock
|
|
3030
2769
|
buildPbrPipelineLayouts(device as any, STORAGE_CAPS);
|
|
3031
2770
|
const materialBgl = device.capturedBgls.find((b) => b.label === 'pbr-material-skylight-bgl');
|
|
3032
2771
|
expect(materialBgl).toBeDefined();
|
|
3033
2772
|
const entries = materialBgl?.entries ?? [];
|
|
3034
|
-
// binding indices 0..
|
|
3035
|
-
for (let i = 0; i <
|
|
2773
|
+
// binding indices 0..23 in order
|
|
2774
|
+
for (let i = 0; i < 24; i++) {
|
|
3036
2775
|
expect(entries[i]?.binding).toBe(i);
|
|
3037
2776
|
}
|
|
3038
2777
|
// 13..19 resource types
|
|
@@ -3049,6 +2788,15 @@ function makeEnvironmentGenerationState() {
|
|
|
3049
2788
|
);
|
|
3050
2789
|
expect((entries[18] as { sampler?: { type: string } }).sampler?.type).toBe('filtering');
|
|
3051
2790
|
expect((entries[19] as { buffer?: { type: string } }).buffer?.type).toBe('uniform');
|
|
2791
|
+
// 20..23 lightmap resource types
|
|
2792
|
+
expect((entries[20] as { sampler?: { type: string } }).sampler?.type).toBe('filtering');
|
|
2793
|
+
expect((entries[21] as { texture?: { viewDimension: string } }).texture?.viewDimension).toBe(
|
|
2794
|
+
'2d',
|
|
2795
|
+
);
|
|
2796
|
+
expect((entries[22] as { sampler?: { type: string } }).sampler?.type).toBe('filtering');
|
|
2797
|
+
expect((entries[23] as { texture?: { viewDimension: string } }).texture?.viewDimension).toBe(
|
|
2798
|
+
'2d',
|
|
2799
|
+
);
|
|
3052
2800
|
});
|
|
3053
2801
|
|
|
3054
2802
|
it('(d) bindGroupLayouts[0..3] order matches [view, material, meshArray, instances]', () => {
|
|
@@ -3072,13 +2820,13 @@ function makeEnvironmentGenerationState() {
|
|
|
3072
2820
|
// ─── (e)(f) Unlit material BGL shape + name ─────────────────────────────────
|
|
3073
2821
|
|
|
3074
2822
|
describe('t57 (M4 round-4) -- unlitPipeline material BGL shape', () => {
|
|
3075
|
-
it('(e) unlit material BGL entry count ===
|
|
2823
|
+
it('(e) unlit material BGL entry count === 13 (no Skylight binding contamination)', () => {
|
|
3076
2824
|
const device = makeMockDevice();
|
|
3077
2825
|
// biome-ignore lint/suspicious/noExplicitAny: structural mock
|
|
3078
2826
|
buildUnlitMaterialBgl(device as any);
|
|
3079
2827
|
const unlitBgl = device.capturedBgls.find((b) => b.label === 'unlit-material-bgl');
|
|
3080
2828
|
expect(unlitBgl).toBeDefined();
|
|
3081
|
-
expect(unlitBgl?.entries).toHaveLength(
|
|
2829
|
+
expect(unlitBgl?.entries).toHaveLength(13);
|
|
3082
2830
|
});
|
|
3083
2831
|
|
|
3084
2832
|
it("(f) PBR material BGL labelled 'pbr-material-skylight-bgl'; unlit labelled 'unlit-material-bgl'", () => {
|
|
@@ -3293,13 +3041,13 @@ function makeEnvironmentGenerationState() {
|
|
|
3293
3041
|
const { createRenderer } = (await import(ENGINE)) as {
|
|
3294
3042
|
createRenderer: (canvas: unknown, opts?: unknown, bundler?: unknown) => Promise<unknown>;
|
|
3295
3043
|
};
|
|
3296
|
-
const
|
|
3044
|
+
const result = await createRenderer(
|
|
3297
3045
|
canvas,
|
|
3298
3046
|
{},
|
|
3299
3047
|
{ shaderManifestUrl: buildManifestDataUrl() },
|
|
3300
|
-
)
|
|
3301
|
-
|
|
3302
|
-
|
|
3048
|
+
);
|
|
3049
|
+
expect(result.ok).toBe(true);
|
|
3050
|
+
result.unwrap();
|
|
3303
3051
|
|
|
3304
3052
|
// bug-20260612: sRGB siblings stay on the swap-chain srgb view; Channel 2
|
|
3305
3053
|
// (storageBufferCapable=true) follows getPreferredCanvasFormat() — mocked
|
|
@@ -3332,21 +3080,15 @@ function makeEnvironmentGenerationState() {
|
|
|
3332
3080
|
vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
|
|
3333
3081
|
const canvas = makeMockCanvas();
|
|
3334
3082
|
const { createRenderer } = (await import(ENGINE)) as {
|
|
3335
|
-
createRenderer: (
|
|
3336
|
-
canvas: unknown,
|
|
3337
|
-
opts?: unknown,
|
|
3338
|
-
bundler?: unknown,
|
|
3339
|
-
) => Promise<{
|
|
3340
|
-
ready: Promise<{ ok: boolean }>;
|
|
3341
|
-
}>;
|
|
3083
|
+
createRenderer: (...args: unknown[]) => Promise<{ ok: boolean; unwrap(): RendererType }>;
|
|
3342
3084
|
};
|
|
3343
|
-
const
|
|
3085
|
+
const result = await createRenderer(
|
|
3344
3086
|
canvas,
|
|
3345
3087
|
{},
|
|
3346
3088
|
{ shaderManifestUrl: buildManifestDataUrl() },
|
|
3347
3089
|
);
|
|
3348
|
-
|
|
3349
|
-
|
|
3090
|
+
expect(result.ok).toBe(true);
|
|
3091
|
+
result.unwrap();
|
|
3350
3092
|
|
|
3351
3093
|
// No `createTexture(format='rgba16float')` call for the HDR colour
|
|
3352
3094
|
// attachment before any opt-in frame. The geometry-side fallback
|
|
@@ -3386,21 +3128,15 @@ function makeEnvironmentGenerationState() {
|
|
|
3386
3128
|
vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
|
|
3387
3129
|
const canvas = makeMockCanvas();
|
|
3388
3130
|
const { createRenderer } = (await import(ENGINE)) as {
|
|
3389
|
-
createRenderer: (
|
|
3390
|
-
canvas: unknown,
|
|
3391
|
-
opts?: unknown,
|
|
3392
|
-
bundler?: unknown,
|
|
3393
|
-
) => Promise<{
|
|
3394
|
-
ready: Promise<{ ok: boolean }>;
|
|
3395
|
-
}>;
|
|
3131
|
+
createRenderer: (...args: unknown[]) => Promise<{ ok: boolean; unwrap(): RendererType }>;
|
|
3396
3132
|
};
|
|
3397
|
-
const
|
|
3133
|
+
const result = await createRenderer(
|
|
3398
3134
|
canvas,
|
|
3399
3135
|
{},
|
|
3400
3136
|
{ shaderManifestUrl: buildManifestDataUrl() },
|
|
3401
3137
|
);
|
|
3402
|
-
|
|
3403
|
-
|
|
3138
|
+
expect(result.ok).toBe(true);
|
|
3139
|
+
result.unwrap();
|
|
3404
3140
|
|
|
3405
3141
|
const moduleLabels = log.records
|
|
3406
3142
|
.filter((r) => r.type === 'createShaderModule')
|
|
@@ -3414,44 +3150,6 @@ function makeEnvironmentGenerationState() {
|
|
|
3414
3150
|
// the manifest triple guard (missing tonemap entry -> ready rejects).
|
|
3415
3151
|
expect(moduleLabels).not.toContain('tonemap');
|
|
3416
3152
|
});
|
|
3417
|
-
|
|
3418
|
-
it('row 5: ready rejects shader-compile-failed when manifest omits tonemap entry', async () => {
|
|
3419
|
-
const log: DeviceCallLog = { records: [] };
|
|
3420
|
-
const device = makeMockDevice(log);
|
|
3421
|
-
vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
|
|
3422
|
-
const canvas = makeMockCanvas();
|
|
3423
|
-
const partialManifest = {
|
|
3424
|
-
schemaVersion: '1.0.0',
|
|
3425
|
-
entries: [
|
|
3426
|
-
{
|
|
3427
|
-
hash: 'pbr00000',
|
|
3428
|
-
wgsl: '/* mock pbr.wgsl - calls f_schlick( for PBR direct lighting */',
|
|
3429
|
-
glsl: '',
|
|
3430
|
-
bindings: '',
|
|
3431
|
-
},
|
|
3432
|
-
{
|
|
3433
|
-
hash: 'unlit000',
|
|
3434
|
-
wgsl: '/* mock unlit.wgsl */',
|
|
3435
|
-
glsl: '',
|
|
3436
|
-
bindings: '',
|
|
3437
|
-
},
|
|
3438
|
-
],
|
|
3439
|
-
};
|
|
3440
|
-
const url = `data:application/json,${encodeURIComponent(JSON.stringify(partialManifest))}`;
|
|
3441
|
-
const { createRenderer } = (await import(ENGINE)) as {
|
|
3442
|
-
createRenderer: (
|
|
3443
|
-
canvas: unknown,
|
|
3444
|
-
opts?: unknown,
|
|
3445
|
-
bundler?: unknown,
|
|
3446
|
-
) => Promise<{
|
|
3447
|
-
ready: Promise<{ ok: boolean; error?: { code: string } }>;
|
|
3448
|
-
}>;
|
|
3449
|
-
};
|
|
3450
|
-
const renderer = await createRenderer(canvas, {}, { shaderManifestUrl: url });
|
|
3451
|
-
const ready = await renderer.ready;
|
|
3452
|
-
expect(ready.ok).toBe(false);
|
|
3453
|
-
expect(ready.error?.code).toBe('shader-compile-failed');
|
|
3454
|
-
});
|
|
3455
3153
|
});
|
|
3456
3154
|
}
|
|
3457
3155
|
|
|
@@ -3676,7 +3374,6 @@ function makeEnvironmentGenerationState() {
|
|
|
3676
3374
|
// now flows through the same generic per-MaterialShader pipeline cache
|
|
3677
3375
|
// every other transparent material uses (plan-strategy D-7).
|
|
3678
3376
|
unlitPipelineHdr: f.fakeUnlitHdrPipeline,
|
|
3679
|
-
standardPipelineHdr: f.fakeStandardHdrPipeline,
|
|
3680
3377
|
perPassResources: {
|
|
3681
3378
|
depthTexture: { __label: 'depth' },
|
|
3682
3379
|
depthTextureView: f.swapDepthView,
|
|
@@ -3728,7 +3425,7 @@ function makeEnvironmentGenerationState() {
|
|
|
3728
3425
|
const internals = makeRecorderInternals(log);
|
|
3729
3426
|
const ps = makePipelineState(internals as never, false);
|
|
3730
3427
|
(internals as { getPipelineState: () => unknown }).getPipelineState = () => ps;
|
|
3731
|
-
const { recordFrame } = await import('
|
|
3428
|
+
const { recordFrame } = await import('../../../render/src/record/frame');
|
|
3732
3429
|
const cameras = [makeCamera('none')];
|
|
3733
3430
|
recordFrame(
|
|
3734
3431
|
internals as never,
|
|
@@ -3739,7 +3436,6 @@ function makeEnvironmentGenerationState() {
|
|
|
3739
3436
|
[],
|
|
3740
3437
|
{
|
|
3741
3438
|
frameNumber: 0,
|
|
3742
|
-
environmentGeneration: makeEnvironmentGenerationState(),
|
|
3743
3439
|
compiledFrameGraph: null,
|
|
3744
3440
|
compiledFrameGraphTopologyKey: null,
|
|
3745
3441
|
retiredCompiledFrameGraphs: new Set(),
|
|
@@ -3767,8 +3463,10 @@ function makeEnvironmentGenerationState() {
|
|
|
3767
3463
|
},
|
|
3768
3464
|
{ unlit: 0 },
|
|
3769
3465
|
{ createBindGroup: 0, keys: [] },
|
|
3770
|
-
|
|
3771
|
-
|
|
3466
|
+
undefined,
|
|
3467
|
+
0,
|
|
3468
|
+
undefined,
|
|
3469
|
+
0,
|
|
3772
3470
|
);
|
|
3773
3471
|
|
|
3774
3472
|
// M1 / w7: the render-graph allocates ALL color targets (including HDR)
|
|
@@ -3823,8 +3521,7 @@ function makeEnvironmentGenerationState() {
|
|
|
3823
3521
|
}
|
|
3824
3522
|
|
|
3825
3523
|
it('despawned key (not in validated set): recordFrame destroys GpuBuffer + deletes Map entry', async () => {
|
|
3826
|
-
const { recordFrame } = await import('
|
|
3827
|
-
const { GpuBuffer } = await import('@forgeax/engine-render/internal');
|
|
3524
|
+
const { recordFrame } = await import('../../../render/src/record/frame');
|
|
3828
3525
|
const { err: rhiErrFn, RhiError: RhiErrorCtor } = await import('@forgeax/engine-rhi');
|
|
3829
3526
|
const { device: bufDev, destroyedHandles } = makeBufRecorderDevice(
|
|
3830
3527
|
rhiErrFn,
|
|
@@ -3858,7 +3555,6 @@ function makeEnvironmentGenerationState() {
|
|
|
3858
3555
|
[],
|
|
3859
3556
|
{
|
|
3860
3557
|
frameNumber: 0,
|
|
3861
|
-
environmentGeneration: makeEnvironmentGenerationState(),
|
|
3862
3558
|
compiledFrameGraph: null,
|
|
3863
3559
|
compiledFrameGraphTopologyKey: null,
|
|
3864
3560
|
retiredCompiledFrameGraphs: new Set(),
|
|
@@ -3885,8 +3581,10 @@ function makeEnvironmentGenerationState() {
|
|
|
3885
3581
|
},
|
|
3886
3582
|
{ unlit: 0 },
|
|
3887
3583
|
{ createBindGroup: 0, keys: [] },
|
|
3888
|
-
|
|
3889
|
-
|
|
3584
|
+
undefined,
|
|
3585
|
+
0,
|
|
3586
|
+
undefined,
|
|
3587
|
+
0,
|
|
3890
3588
|
);
|
|
3891
3589
|
|
|
3892
3590
|
// F11 production loop destroyed the orphaned buffer + dropped the key.
|
|
@@ -3896,8 +3594,7 @@ function makeEnvironmentGenerationState() {
|
|
|
3896
3594
|
});
|
|
3897
3595
|
|
|
3898
3596
|
it('isDestroyed dedup: a pre-destroyed orphan is not double-destroyed (still removed)', async () => {
|
|
3899
|
-
const { recordFrame } = await import('
|
|
3900
|
-
const { GpuBuffer } = await import('@forgeax/engine-render/internal');
|
|
3597
|
+
const { recordFrame } = await import('../../../render/src/record/frame');
|
|
3901
3598
|
const { err: rhiErrFn, RhiError: RhiErrorCtor } = await import('@forgeax/engine-rhi');
|
|
3902
3599
|
const { device: bufDev, destroyedHandles } = makeBufRecorderDevice(
|
|
3903
3600
|
rhiErrFn,
|
|
@@ -3931,7 +3628,6 @@ function makeEnvironmentGenerationState() {
|
|
|
3931
3628
|
[],
|
|
3932
3629
|
{
|
|
3933
3630
|
frameNumber: 0,
|
|
3934
|
-
environmentGeneration: makeEnvironmentGenerationState(),
|
|
3935
3631
|
compiledFrameGraph: null,
|
|
3936
3632
|
compiledFrameGraphTopologyKey: null,
|
|
3937
3633
|
retiredCompiledFrameGraphs: new Set(),
|
|
@@ -3958,8 +3654,10 @@ function makeEnvironmentGenerationState() {
|
|
|
3958
3654
|
},
|
|
3959
3655
|
{ unlit: 0 },
|
|
3960
3656
|
{ createBindGroup: 0, keys: [] },
|
|
3961
|
-
|
|
3962
|
-
|
|
3657
|
+
undefined,
|
|
3658
|
+
0,
|
|
3659
|
+
undefined,
|
|
3660
|
+
0,
|
|
3963
3661
|
);
|
|
3964
3662
|
|
|
3965
3663
|
// isDestroyed gate skipped the second destroy; key still dropped.
|
|
@@ -4001,18 +3699,18 @@ function makeEnvironmentGenerationState() {
|
|
|
4001
3699
|
describe('Skin — component registration + schema shape (AC-13 / AC-37)', () => {
|
|
4002
3700
|
it('Skin is a registered component with name "Skin" and schema fields skeleton + joints', () => {
|
|
4003
3701
|
expect(Skin.name).toBe('Skin');
|
|
4004
|
-
expect(
|
|
3702
|
+
expect(componentSchemaTypes(Skin)).toEqual({
|
|
4005
3703
|
skeleton: 'shared<SkeletonAsset>',
|
|
4006
3704
|
joints: 'array<entity>',
|
|
4007
3705
|
});
|
|
4008
3706
|
});
|
|
4009
3707
|
|
|
4010
|
-
it('
|
|
4011
|
-
expect(
|
|
3708
|
+
it('Skin.schema.skeleton is shared<SkeletonAsset> (schema-vocab keyword)', () => {
|
|
3709
|
+
expect(componentFieldType(Skin, 'skeleton')).toBe('shared<SkeletonAsset>');
|
|
4012
3710
|
});
|
|
4013
3711
|
|
|
4014
|
-
it('
|
|
4015
|
-
expect(
|
|
3712
|
+
it('Skin.schema.joints is array<entity> (schema-vocab keyword)', () => {
|
|
3713
|
+
expect(componentFieldType(Skin, 'joints')).toBe('array<entity>');
|
|
4016
3714
|
});
|
|
4017
3715
|
|
|
4018
3716
|
it('Skin component spawns on an entity', () => {
|
|
@@ -4097,24 +3795,6 @@ function makeEnvironmentGenerationState() {
|
|
|
4097
3795
|
};
|
|
4098
3796
|
}
|
|
4099
3797
|
|
|
4100
|
-
const testAnimationHandles = new WeakMap<
|
|
4101
|
-
object,
|
|
4102
|
-
Map<number, Handle<'AnimationClip', 'shared'>>
|
|
4103
|
-
>();
|
|
4104
|
-
|
|
4105
|
-
function testAnimationHandle(world: World, raw: number): Handle<'AnimationClip', 'shared'> {
|
|
4106
|
-
let handles = testAnimationHandles.get(world);
|
|
4107
|
-
if (handles === undefined) {
|
|
4108
|
-
handles = new Map();
|
|
4109
|
-
testAnimationHandles.set(world, handles);
|
|
4110
|
-
}
|
|
4111
|
-
const existing = handles.get(raw);
|
|
4112
|
-
if (existing !== undefined) return existing;
|
|
4113
|
-
const handle = world.allocSharedRef('AnimationClip', makeClip(1, []));
|
|
4114
|
-
handles.set(raw, handle);
|
|
4115
|
-
return handle;
|
|
4116
|
-
}
|
|
4117
|
-
|
|
4118
3798
|
type TestResolver = { clips: Map<number, AnimationClip>; handles?: Map<number, number> };
|
|
4119
3799
|
|
|
4120
3800
|
function makeResolver(clips: Map<number, AnimationClip>): TestResolver {
|
|
@@ -4133,13 +3813,8 @@ function makeEnvironmentGenerationState() {
|
|
|
4133
3813
|
for (const entity of players) {
|
|
4134
3814
|
const player = world.get(entity, AnimationPlayer);
|
|
4135
3815
|
if (!player.ok) continue;
|
|
4136
|
-
const rawByHandle = new Map<number, number>();
|
|
4137
|
-
for (const [raw, handle] of testAnimationHandles.get(world) ?? []) {
|
|
4138
|
-
rawByHandle.set(handle, raw);
|
|
4139
|
-
}
|
|
4140
3816
|
const clips = Array.from(player.value.clips, (handle) => {
|
|
4141
|
-
const
|
|
4142
|
-
const mapped = handles.get(raw ?? handle);
|
|
3817
|
+
const mapped = handles.get(handle);
|
|
4143
3818
|
return mapped ?? (allocated.has(handle) ? handle : 0);
|
|
4144
3819
|
});
|
|
4145
3820
|
world.set(entity, AnimationPlayer, { clips });
|
|
@@ -4186,7 +3861,7 @@ function makeEnvironmentGenerationState() {
|
|
|
4186
3861
|
component: AnimationPlayer,
|
|
4187
3862
|
data: {
|
|
4188
3863
|
clips: [
|
|
4189
|
-
|
|
3864
|
+
toShared<'AnimationClip'>(clipId),
|
|
4190
3865
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
4191
3866
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
4192
3867
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
@@ -4279,7 +3954,7 @@ function makeEnvironmentGenerationState() {
|
|
|
4279
3954
|
component: AnimationPlayer,
|
|
4280
3955
|
data: {
|
|
4281
3956
|
clips: [
|
|
4282
|
-
|
|
3957
|
+
toShared<'AnimationClip'>(1),
|
|
4283
3958
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
4284
3959
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
4285
3960
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
@@ -4318,7 +3993,7 @@ function makeEnvironmentGenerationState() {
|
|
|
4318
3993
|
component: AnimationPlayer,
|
|
4319
3994
|
data: {
|
|
4320
3995
|
clips: [
|
|
4321
|
-
|
|
3996
|
+
toShared<'AnimationClip'>(1),
|
|
4322
3997
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
4323
3998
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
4324
3999
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
@@ -4353,7 +4028,7 @@ function makeEnvironmentGenerationState() {
|
|
|
4353
4028
|
component: AnimationPlayer,
|
|
4354
4029
|
data: {
|
|
4355
4030
|
clips: [
|
|
4356
|
-
|
|
4031
|
+
toShared<'AnimationClip'>(1),
|
|
4357
4032
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
4358
4033
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
4359
4034
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
@@ -4459,7 +4134,7 @@ function makeEnvironmentGenerationState() {
|
|
|
4459
4134
|
component: AnimationPlayer,
|
|
4460
4135
|
data: {
|
|
4461
4136
|
clips: [
|
|
4462
|
-
|
|
4137
|
+
toShared<'AnimationClip'>(1),
|
|
4463
4138
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
4464
4139
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
4465
4140
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
@@ -4516,7 +4191,7 @@ function makeEnvironmentGenerationState() {
|
|
|
4516
4191
|
component: AnimationPlayer,
|
|
4517
4192
|
data: {
|
|
4518
4193
|
clips: [
|
|
4519
|
-
|
|
4194
|
+
toShared<'AnimationClip'>(1),
|
|
4520
4195
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
4521
4196
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
4522
4197
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
@@ -4599,8 +4274,8 @@ function makeEnvironmentGenerationState() {
|
|
|
4599
4274
|
component: AnimationPlayer,
|
|
4600
4275
|
data: {
|
|
4601
4276
|
clips: [
|
|
4602
|
-
|
|
4603
|
-
|
|
4277
|
+
toShared<'AnimationClip'>(1),
|
|
4278
|
+
toShared<'AnimationClip'>(2),
|
|
4604
4279
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
4605
4280
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
4606
4281
|
],
|
|
@@ -4665,8 +4340,8 @@ function makeEnvironmentGenerationState() {
|
|
|
4665
4340
|
component: AnimationPlayer,
|
|
4666
4341
|
data: {
|
|
4667
4342
|
clips: [
|
|
4668
|
-
|
|
4669
|
-
|
|
4343
|
+
toShared<'AnimationClip'>(1),
|
|
4344
|
+
toShared<'AnimationClip'>(2),
|
|
4670
4345
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
4671
4346
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
4672
4347
|
],
|
|
@@ -4717,8 +4392,8 @@ function makeEnvironmentGenerationState() {
|
|
|
4717
4392
|
component: AnimationPlayer,
|
|
4718
4393
|
data: {
|
|
4719
4394
|
clips: [
|
|
4720
|
-
|
|
4721
|
-
|
|
4395
|
+
toShared<'AnimationClip'>(1),
|
|
4396
|
+
toShared<'AnimationClip'>(2),
|
|
4722
4397
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
4723
4398
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
4724
4399
|
],
|
|
@@ -4775,9 +4450,9 @@ function makeEnvironmentGenerationState() {
|
|
|
4775
4450
|
component: AnimationPlayer,
|
|
4776
4451
|
data: {
|
|
4777
4452
|
clips: [
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4453
|
+
toShared<'AnimationClip'>(1),
|
|
4454
|
+
toShared<'AnimationClip'>(2),
|
|
4455
|
+
toShared<'AnimationClip'>(3),
|
|
4781
4456
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
4782
4457
|
],
|
|
4783
4458
|
times: new Float32Array([0, 0, 0, 0]),
|
|
@@ -4828,8 +4503,8 @@ function makeEnvironmentGenerationState() {
|
|
|
4828
4503
|
component: AnimationPlayer,
|
|
4829
4504
|
data: {
|
|
4830
4505
|
clips: [
|
|
4831
|
-
|
|
4832
|
-
|
|
4506
|
+
toShared<'AnimationClip'>(1),
|
|
4507
|
+
toShared<'AnimationClip'>(2),
|
|
4833
4508
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
4834
4509
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
4835
4510
|
],
|
|
@@ -4873,7 +4548,7 @@ function makeEnvironmentGenerationState() {
|
|
|
4873
4548
|
component: AnimationPlayer,
|
|
4874
4549
|
data: {
|
|
4875
4550
|
clips: [
|
|
4876
|
-
|
|
4551
|
+
toShared<'AnimationClip'>(1),
|
|
4877
4552
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
4878
4553
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
4879
4554
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
@@ -4938,8 +4613,8 @@ function makeEnvironmentGenerationState() {
|
|
|
4938
4613
|
component: AnimationPlayer,
|
|
4939
4614
|
data: {
|
|
4940
4615
|
clips: [
|
|
4941
|
-
|
|
4942
|
-
|
|
4616
|
+
toShared<'AnimationClip'>(1),
|
|
4617
|
+
toShared<'AnimationClip'>(2),
|
|
4943
4618
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
4944
4619
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
4945
4620
|
],
|
|
@@ -4995,8 +4670,8 @@ function makeEnvironmentGenerationState() {
|
|
|
4995
4670
|
component: AnimationPlayer,
|
|
4996
4671
|
data: {
|
|
4997
4672
|
clips: [
|
|
4998
|
-
|
|
4999
|
-
|
|
4673
|
+
toShared<'AnimationClip'>(1),
|
|
4674
|
+
toShared<'AnimationClip'>(2),
|
|
5000
4675
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
5001
4676
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
5002
4677
|
],
|
|
@@ -5046,7 +4721,7 @@ function makeEnvironmentGenerationState() {
|
|
|
5046
4721
|
component: AnimationPlayer,
|
|
5047
4722
|
data: {
|
|
5048
4723
|
clips: [
|
|
5049
|
-
|
|
4724
|
+
toShared<'AnimationClip'>(1),
|
|
5050
4725
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
5051
4726
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
5052
4727
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
@@ -5105,8 +4780,8 @@ function makeEnvironmentGenerationState() {
|
|
|
5105
4780
|
component: AnimationPlayer,
|
|
5106
4781
|
data: {
|
|
5107
4782
|
clips: [
|
|
5108
|
-
|
|
5109
|
-
|
|
4783
|
+
toShared<'AnimationClip'>(1),
|
|
4784
|
+
toShared<'AnimationClip'>(2),
|
|
5110
4785
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
5111
4786
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
5112
4787
|
],
|
|
@@ -5156,7 +4831,7 @@ function makeEnvironmentGenerationState() {
|
|
|
5156
4831
|
component: AnimationPlayer,
|
|
5157
4832
|
data: {
|
|
5158
4833
|
clips: [
|
|
5159
|
-
|
|
4834
|
+
toShared<'AnimationClip'>(1),
|
|
5160
4835
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
5161
4836
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
5162
4837
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
@@ -5220,8 +4895,8 @@ function makeEnvironmentGenerationState() {
|
|
|
5220
4895
|
component: AnimationPlayer,
|
|
5221
4896
|
data: {
|
|
5222
4897
|
clips: [
|
|
5223
|
-
|
|
5224
|
-
|
|
4898
|
+
toShared<'AnimationClip'>(1),
|
|
4899
|
+
toShared<'AnimationClip'>(2),
|
|
5225
4900
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
5226
4901
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
5227
4902
|
],
|
|
@@ -5264,7 +4939,7 @@ function makeEnvironmentGenerationState() {
|
|
|
5264
4939
|
component: AnimationPlayer,
|
|
5265
4940
|
data: {
|
|
5266
4941
|
clips: [
|
|
5267
|
-
|
|
4942
|
+
toShared<'AnimationClip'>(1),
|
|
5268
4943
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
5269
4944
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
5270
4945
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
@@ -5299,7 +4974,7 @@ function makeEnvironmentGenerationState() {
|
|
|
5299
4974
|
component: AnimationPlayer,
|
|
5300
4975
|
data: {
|
|
5301
4976
|
clips: [
|
|
5302
|
-
|
|
4977
|
+
toShared<'AnimationClip'>(1),
|
|
5303
4978
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
5304
4979
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
5305
4980
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
@@ -5364,7 +5039,7 @@ function makeEnvironmentGenerationState() {
|
|
|
5364
5039
|
component: AnimationPlayer,
|
|
5365
5040
|
data: {
|
|
5366
5041
|
clips: [
|
|
5367
|
-
|
|
5042
|
+
toShared<'AnimationClip'>(1),
|
|
5368
5043
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
5369
5044
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
5370
5045
|
0 as Handle<'AnimationClip', 'shared'>,
|
|
@@ -5435,7 +5110,7 @@ function makeEnvironmentGenerationState() {
|
|
|
5435
5110
|
}
|
|
5436
5111
|
|
|
5437
5112
|
function readWorld(world: World, entity: EntityHandle): Float32Array {
|
|
5438
|
-
return world.get(entity, Transform).unwrap().world;
|
|
5113
|
+
return world.get(entity, Transform).unwrap().world as Float32Array;
|
|
5439
5114
|
}
|
|
5440
5115
|
|
|
5441
5116
|
function expectMatClose(actual: Float32Array, expected: Float32Array): void {
|
|
@@ -5551,35 +5226,6 @@ function makeEnvironmentGenerationState() {
|
|
|
5551
5226
|
|
|
5552
5227
|
{
|
|
5553
5228
|
// --- from skin-cap-gate.test.ts ---
|
|
5554
|
-
|
|
5555
|
-
describe('skin cap-gate', () => {
|
|
5556
|
-
it('VertexStorageBufferUnavailableError has correct code and fields', () => {
|
|
5557
|
-
const e = new VertexStorageBufferUnavailableError();
|
|
5558
|
-
expect(e.code).toBe('vertex-storage-buffer-unavailable');
|
|
5559
|
-
expect(e.expected).toContain('maxStorageBuffersPerShaderStage');
|
|
5560
|
-
expect(e.hint).toContain('OOS-uniform-palette');
|
|
5561
|
-
});
|
|
5562
|
-
|
|
5563
|
-
it('cap-gate fails when vertex storage buffer count is 0', () => {
|
|
5564
|
-
// Simulate: maxStorageBuffersPerShaderStage < 1
|
|
5565
|
-
const maxStorageBuffersPerShaderStage = 0;
|
|
5566
|
-
const hasVertexStorage = maxStorageBuffersPerShaderStage >= 1;
|
|
5567
|
-
expect(hasVertexStorage).toBe(false);
|
|
5568
|
-
});
|
|
5569
|
-
|
|
5570
|
-
it('cap-gate passes when vertex storage buffer count is 1 or more', () => {
|
|
5571
|
-
// Simulate: standard WebGPU core feature
|
|
5572
|
-
const maxStorageBuffersPerShaderStage = 8;
|
|
5573
|
-
const hasVertexStorage = maxStorageBuffersPerShaderStage >= 1;
|
|
5574
|
-
expect(hasVertexStorage).toBe(true);
|
|
5575
|
-
});
|
|
5576
|
-
|
|
5577
|
-
it('cap-gate passes at the W3C spec minimum for vertex storage (1 per stage)', () => {
|
|
5578
|
-
const maxStorageBuffersPerShaderStage = 1;
|
|
5579
|
-
const hasVertexStorage = maxStorageBuffersPerShaderStage >= 1;
|
|
5580
|
-
expect(hasVertexStorage).toBe(true);
|
|
5581
|
-
});
|
|
5582
|
-
});
|
|
5583
5229
|
}
|
|
5584
5230
|
|
|
5585
5231
|
{
|
|
@@ -5709,13 +5355,12 @@ function makeEnvironmentGenerationState() {
|
|
|
5709
5355
|
expect(buffers.length).toBe(0);
|
|
5710
5356
|
const slice = alloc.allocateSlice(1);
|
|
5711
5357
|
expect(slice.buffer).toBeDefined();
|
|
5712
|
-
expect(buffers.length).toBe(
|
|
5358
|
+
expect(buffers.length).toBe(1);
|
|
5713
5359
|
// Initial capacity = MAX_JOINTS * 64 = 255 * 64 = 16320 (storage
|
|
5714
5360
|
// path's first grow step; uniform fallback would also start at
|
|
5715
5361
|
// exactly 16320 since each pool entry equals one binding window).
|
|
5716
5362
|
// biome-ignore lint/style/noNonNullAssertion: controlled test context
|
|
5717
5363
|
expect(buffers[0]!.size).toBe(255 * 64);
|
|
5718
|
-
expect(buffers[1]?.size).toBe(255 * 64);
|
|
5719
5364
|
});
|
|
5720
5365
|
|
|
5721
5366
|
it('grow at 1.5x when binding window exceeds capacity', () => {
|
|
@@ -5724,16 +5369,15 @@ function makeEnvironmentGenerationState() {
|
|
|
5724
5369
|
// First alloc (offset=0): needed = 0 + bindingWindowBytes (16320)
|
|
5725
5370
|
// -> grow to 16320 exactly (initial = 255 * 64).
|
|
5726
5371
|
alloc.allocateSlice(255);
|
|
5727
|
-
expect(buffers.length).toBe(
|
|
5372
|
+
expect(buffers.length).toBe(1);
|
|
5728
5373
|
// biome-ignore lint/style/noNonNullAssertion: controlled test context
|
|
5729
5374
|
expect(buffers[0]!.size).toBe(16320);
|
|
5730
5375
|
// Second alloc (offset=255*64=16320): needed = 16320 + 16320 = 32640.
|
|
5731
5376
|
// 1.5x grow: 16320 -> 24576 -> 36864 (smallest aligned >= 32640).
|
|
5732
5377
|
alloc.allocateSlice(100);
|
|
5733
|
-
expect(buffers.length).toBe(
|
|
5378
|
+
expect(buffers.length).toBe(2);
|
|
5734
5379
|
// biome-ignore lint/style/noNonNullAssertion: controlled test context
|
|
5735
|
-
expect(buffers[
|
|
5736
|
-
expect(buffers[3]?.size).toBe(36864);
|
|
5380
|
+
expect(buffers[1]!.size).toBe(36864);
|
|
5737
5381
|
});
|
|
5738
5382
|
|
|
5739
5383
|
// M6 regression gate: the bug that this fix repairs reproduces here as
|
|
@@ -5792,17 +5436,13 @@ function makeEnvironmentGenerationState() {
|
|
|
5792
5436
|
expect(s1.buffer).not.toBe(s2.buffer);
|
|
5793
5437
|
expect(s0.buffer).not.toBe(s2.buffer);
|
|
5794
5438
|
// Three entities -> three distinct UBOs of size = bindingWindowBytes.
|
|
5795
|
-
expect(buffers.length).toBe(
|
|
5439
|
+
expect(buffers.length).toBe(3);
|
|
5796
5440
|
// biome-ignore lint/style/noNonNullAssertion: controlled test context
|
|
5797
5441
|
expect(buffers[0]!.size).toBe(16320);
|
|
5798
5442
|
// biome-ignore lint/style/noNonNullAssertion: controlled test context
|
|
5799
5443
|
expect(buffers[1]!.size).toBe(16320);
|
|
5800
5444
|
// biome-ignore lint/style/noNonNullAssertion: controlled test context
|
|
5801
5445
|
expect(buffers[2]!.size).toBe(16320);
|
|
5802
|
-
// Paired last-submitted palette buffers have the same bounded shape.
|
|
5803
|
-
expect(buffers[3]?.size).toBe(16320);
|
|
5804
|
-
expect(buffers[4]?.size).toBe(16320);
|
|
5805
|
-
expect(buffers[5]?.size).toBe(16320);
|
|
5806
5446
|
});
|
|
5807
5447
|
|
|
5808
5448
|
it('uniform fallback: pool reuses buffers across frames (M6)', () => {
|
|
@@ -5812,14 +5452,14 @@ function makeEnvironmentGenerationState() {
|
|
|
5812
5452
|
const f1s0 = alloc.allocateSlice(10);
|
|
5813
5453
|
const f1s1 = alloc.allocateSlice(10);
|
|
5814
5454
|
const f1s2 = alloc.allocateSlice(10);
|
|
5815
|
-
expect(buffers.length).toBe(
|
|
5455
|
+
expect(buffers.length).toBe(3);
|
|
5816
5456
|
// Frame 2: reset + same-shape walk -> pool round-robin, no new
|
|
5817
5457
|
// createBuffer calls. Slices reuse the exact same buffer objects.
|
|
5818
5458
|
alloc.resetForFrame();
|
|
5819
5459
|
const f2s0 = alloc.allocateSlice(10);
|
|
5820
5460
|
const f2s1 = alloc.allocateSlice(10);
|
|
5821
5461
|
const f2s2 = alloc.allocateSlice(10);
|
|
5822
|
-
expect(buffers.length).toBe(
|
|
5462
|
+
expect(buffers.length).toBe(3); // unchanged
|
|
5823
5463
|
expect(f2s0.buffer).toBe(f1s0.buffer);
|
|
5824
5464
|
expect(f2s1.buffer).toBe(f1s1.buffer);
|
|
5825
5465
|
expect(f2s2.buffer).toBe(f1s2.buffer);
|
|
@@ -6132,7 +5772,7 @@ function makeEnvironmentGenerationState() {
|
|
|
6132
5772
|
// --- from sprite-animation-tick-boundary.test.ts ---
|
|
6133
5773
|
|
|
6134
5774
|
function setDt(world: World, dt: number): void {
|
|
6135
|
-
world.
|
|
5775
|
+
world[worldInternal].getClockWriter().time.delta = dt;
|
|
6136
5776
|
}
|
|
6137
5777
|
|
|
6138
5778
|
function expectRegion(
|
|
@@ -6161,7 +5801,7 @@ function makeEnvironmentGenerationState() {
|
|
|
6161
5801
|
// at 0 but accumDt would be 0.25, not 0); a clamp to 0.5 would
|
|
6162
5802
|
// advance 1 frame -> currentFrame === 1; either way the
|
|
6163
5803
|
// assertions below diverge from the no-clamp branch.
|
|
6164
|
-
const world = new World(
|
|
5804
|
+
const world = new World();
|
|
6165
5805
|
const regions = new Float32Array([
|
|
6166
5806
|
0.0, 0, 0.25, 1, 0.25, 0, 0.25, 1, 0.5, 0, 0.25, 1, 0.75, 0, 0.25, 1,
|
|
6167
5807
|
]);
|
|
@@ -6192,7 +5832,7 @@ function makeEnvironmentGenerationState() {
|
|
|
6192
5832
|
});
|
|
6193
5833
|
|
|
6194
5834
|
it('(2) frameCount=1 static sprite stays at currentFrame=0 (LOOP)', () => {
|
|
6195
|
-
const world = new World(
|
|
5835
|
+
const world = new World();
|
|
6196
5836
|
const regions = new Float32Array([0.0, 0.0, 1.0, 1.0]);
|
|
6197
5837
|
const entity = world
|
|
6198
5838
|
.spawn({
|
|
@@ -6223,7 +5863,7 @@ function makeEnvironmentGenerationState() {
|
|
|
6223
5863
|
});
|
|
6224
5864
|
|
|
6225
5865
|
it('(2b) frameCount=1 static sprite stays at currentFrame=0 (CLAMP)', () => {
|
|
6226
|
-
const world = new World(
|
|
5866
|
+
const world = new World();
|
|
6227
5867
|
const regions = new Float32Array([0.0, 0.0, 1.0, 1.0]);
|
|
6228
5868
|
const entity = world
|
|
6229
5869
|
.spawn({
|
|
@@ -6246,7 +5886,7 @@ function makeEnvironmentGenerationState() {
|
|
|
6246
5886
|
});
|
|
6247
5887
|
|
|
6248
5888
|
it('(3) manual setFrame: world.set(SpriteAnimation, { currentFrame: 2, accumDt: 0 }) is honoured next tick', () => {
|
|
6249
|
-
const world = new World(
|
|
5889
|
+
const world = new World();
|
|
6250
5890
|
const regions = new Float32Array([
|
|
6251
5891
|
0.0, 0, 0.25, 1, 0.25, 0, 0.25, 1, 0.5, 0, 0.25, 1, 0.75, 0, 0.25, 1,
|
|
6252
5892
|
]);
|
|
@@ -6305,7 +5945,7 @@ function makeEnvironmentGenerationState() {
|
|
|
6305
5945
|
// --- from sprite-animation-tick-clamp.test.ts ---
|
|
6306
5946
|
|
|
6307
5947
|
function setDt(world: World, dt: number): void {
|
|
6308
|
-
world.
|
|
5948
|
+
world[worldInternal].getClockWriter().time.delta = dt;
|
|
6309
5949
|
}
|
|
6310
5950
|
|
|
6311
5951
|
function makeRegions(): Float32Array {
|
|
@@ -6343,7 +5983,7 @@ function makeEnvironmentGenerationState() {
|
|
|
6343
5983
|
|
|
6344
5984
|
describe('spriteAnimationTickSystem - AC-05 clamp end-frame (M4 T-18)', () => {
|
|
6345
5985
|
it('dt=0.5 with frameCount=3 / frameDuration=0.1 stops at frameCount-1 (last frame)', () => {
|
|
6346
|
-
const world = new World(
|
|
5986
|
+
const world = new World();
|
|
6347
5987
|
const regions = makeRegions();
|
|
6348
5988
|
const entity = world
|
|
6349
5989
|
.spawn({
|
|
@@ -6369,7 +6009,7 @@ function makeEnvironmentGenerationState() {
|
|
|
6369
6009
|
});
|
|
6370
6010
|
|
|
6371
6011
|
it('continued dt=0.5 keeps currentFrame at last frame (no wrap)', () => {
|
|
6372
|
-
const world = new World(
|
|
6012
|
+
const world = new World();
|
|
6373
6013
|
const regions = makeRegions();
|
|
6374
6014
|
const entity = world
|
|
6375
6015
|
.spawn({
|
|
@@ -6411,7 +6051,7 @@ function makeEnvironmentGenerationState() {
|
|
|
6411
6051
|
|
|
6412
6052
|
describe('spriteAnimationTickSystem - AC-09(c) frameDuration<0 fail-fast (M4 T-21)', () => {
|
|
6413
6053
|
it('returns Result.err with detail.field=frame-duration when frameDuration < 0', () => {
|
|
6414
|
-
const world = new World(
|
|
6054
|
+
const world = new World();
|
|
6415
6055
|
const entity = world
|
|
6416
6056
|
.spawn({
|
|
6417
6057
|
component: SpriteAnimation,
|
|
@@ -6465,7 +6105,7 @@ function makeEnvironmentGenerationState() {
|
|
|
6465
6105
|
// semantics return one of the two; either way the `.code` matches
|
|
6466
6106
|
// and the `.detail.field` matches, locking the consistent-abstraction
|
|
6467
6107
|
// claim from plan-strategy section 2 D-1.
|
|
6468
|
-
const world = new World(
|
|
6108
|
+
const world = new World();
|
|
6469
6109
|
world
|
|
6470
6110
|
.spawn({
|
|
6471
6111
|
component: SpriteAnimation,
|
|
@@ -6482,7 +6122,7 @@ function makeEnvironmentGenerationState() {
|
|
|
6482
6122
|
expect(rZero.ok).toBe(false);
|
|
6483
6123
|
if (rZero.ok) throw new Error('zero arm: expected Result.err');
|
|
6484
6124
|
|
|
6485
|
-
const world2 = new World(
|
|
6125
|
+
const world2 = new World();
|
|
6486
6126
|
world2
|
|
6487
6127
|
.spawn({
|
|
6488
6128
|
component: SpriteAnimation,
|
|
@@ -6510,7 +6150,7 @@ function makeEnvironmentGenerationState() {
|
|
|
6510
6150
|
|
|
6511
6151
|
describe('spriteAnimationTickSystem - AC-09(b) frameDuration=0 fail-fast (M4 T-20)', () => {
|
|
6512
6152
|
it('returns Result.err with detail.field=frame-duration when frameDuration === 0', () => {
|
|
6513
|
-
const world = new World(
|
|
6153
|
+
const world = new World();
|
|
6514
6154
|
const entity = world
|
|
6515
6155
|
.spawn({
|
|
6516
6156
|
component: SpriteAnimation,
|
|
@@ -6556,7 +6196,7 @@ function makeEnvironmentGenerationState() {
|
|
|
6556
6196
|
// --- from sprite-animation-tick-loop.test.ts ---
|
|
6557
6197
|
|
|
6558
6198
|
function setDt(world: World, dt: number): void {
|
|
6559
|
-
world.
|
|
6199
|
+
world[worldInternal].getClockWriter().time.delta = dt;
|
|
6560
6200
|
}
|
|
6561
6201
|
|
|
6562
6202
|
function makeRegions(): Float32Array {
|
|
@@ -6598,7 +6238,7 @@ function makeEnvironmentGenerationState() {
|
|
|
6598
6238
|
|
|
6599
6239
|
describe('spriteAnimationTickSystem - AC-04 loop dt sequence (M4 T-17)', () => {
|
|
6600
6240
|
it('five canonical dt steps drive currentFrame 0 -> 1 -> 1 -> 2 -> 0 with accumDt residue 0.06', () => {
|
|
6601
|
-
const world = new World(
|
|
6241
|
+
const world = new World();
|
|
6602
6242
|
const regions = makeRegions();
|
|
6603
6243
|
const entity = world
|
|
6604
6244
|
.spawn({
|
|
@@ -6671,7 +6311,7 @@ function makeEnvironmentGenerationState() {
|
|
|
6671
6311
|
// accumDt=0.05. Establishes that the inner advance loop fires more
|
|
6672
6312
|
// than once per spriteAnimationTickSystem invocation; AC-04 +
|
|
6673
6313
|
// section 2.5 q6 (while loop semantics).
|
|
6674
|
-
const world = new World(
|
|
6314
|
+
const world = new World();
|
|
6675
6315
|
const regions = makeRegions();
|
|
6676
6316
|
const entity = world
|
|
6677
6317
|
.spawn({
|
|
@@ -6702,7 +6342,7 @@ function makeEnvironmentGenerationState() {
|
|
|
6702
6342
|
// --- from sprite-animation-tick-override-probe.test.ts ---
|
|
6703
6343
|
|
|
6704
6344
|
function setDt(world: World, dt: number): void {
|
|
6705
|
-
world.
|
|
6345
|
+
world[worldInternal].getClockWriter().time.delta = dt;
|
|
6706
6346
|
}
|
|
6707
6347
|
|
|
6708
6348
|
function makeRegions(): Float32Array {
|
|
@@ -6744,7 +6384,7 @@ function makeEnvironmentGenerationState() {
|
|
|
6744
6384
|
|
|
6745
6385
|
describe('spriteAnimationTickSystem override probe (M3 AC-14)', () => {
|
|
6746
6386
|
it('first tick on a World that never used SpriteRegionOverride auto-adds the column without crashing', () => {
|
|
6747
|
-
const world = new World(
|
|
6387
|
+
const world = new World();
|
|
6748
6388
|
const entity = spawnAnim(world);
|
|
6749
6389
|
// SpriteRegionOverride is never spawned / registered into this World;
|
|
6750
6390
|
// the old gate existed precisely because world.get on it would throw.
|
|
@@ -6760,7 +6400,7 @@ function makeEnvironmentGenerationState() {
|
|
|
6760
6400
|
});
|
|
6761
6401
|
|
|
6762
6402
|
it('subsequent ticks set the region in place (steady-state already-present path)', () => {
|
|
6763
|
-
const world = new World(
|
|
6403
|
+
const world = new World();
|
|
6764
6404
|
const entity = spawnAnim(world);
|
|
6765
6405
|
|
|
6766
6406
|
// Tick 1: dt=0.12 > frameDuration=0.1 advances exactly one frame
|
|
@@ -6793,7 +6433,7 @@ function makeEnvironmentGenerationState() {
|
|
|
6793
6433
|
// --- from sprite-animation-tick-regions-mismatch.test.ts ---
|
|
6794
6434
|
|
|
6795
6435
|
function setDt(world: World, dt: number): void {
|
|
6796
|
-
world.
|
|
6436
|
+
world[worldInternal].getClockWriter().time.delta = dt;
|
|
6797
6437
|
}
|
|
6798
6438
|
|
|
6799
6439
|
function spawnBadRegionsLength(world: World): EntityHandle {
|
|
@@ -6839,7 +6479,7 @@ function makeEnvironmentGenerationState() {
|
|
|
6839
6479
|
|
|
6840
6480
|
describe('spriteAnimationTickSystem - AC-09(a) regions.length fail-fast (M4 T-19)', () => {
|
|
6841
6481
|
it('returns Result.err with 4 structured fields when regions.length !== frameCount * 4', () => {
|
|
6842
|
-
const world = new World(
|
|
6482
|
+
const world = new World();
|
|
6843
6483
|
const entity = spawnBadRegionsLength(world);
|
|
6844
6484
|
|
|
6845
6485
|
setDt(world, 0.1);
|
|
@@ -6871,7 +6511,7 @@ function makeEnvironmentGenerationState() {
|
|
|
6871
6511
|
});
|
|
6872
6512
|
|
|
6873
6513
|
it('healthy entity advances normally beside a bad-regions-length entity', () => {
|
|
6874
|
-
const world = new World(
|
|
6514
|
+
const world = new World();
|
|
6875
6515
|
const bad = spawnBadRegionsLength(world);
|
|
6876
6516
|
const good = spawnHealthy(world);
|
|
6877
6517
|
|
|
@@ -7182,22 +6822,6 @@ function makeEnvironmentGenerationState() {
|
|
|
7182
6822
|
const sorted = transparentSortEntries(entries, world);
|
|
7183
6823
|
expect(sorted.map((e) => e.entityIndex)).toEqual([1, 2, 0]);
|
|
7184
6824
|
});
|
|
7185
|
-
|
|
7186
|
-
it('uses materialHandle as the tertiary key and preserves equal-material order', () => {
|
|
7187
|
-
const world = new World();
|
|
7188
|
-
setTransparentSortConfig(world, {
|
|
7189
|
-
mode: TRANSPARENT_SORT_MODE_LAYER_Z,
|
|
7190
|
-
yzAlpha: 1.0,
|
|
7191
|
-
}).unwrap();
|
|
7192
|
-
const entries: TransparentEntry[] = [
|
|
7193
|
-
makeEntry({ entityIndex: 0, layer: 0, posZ: 1, materialHandle: 20 }),
|
|
7194
|
-
makeEntry({ entityIndex: 1, layer: 0, posZ: 1, materialHandle: 10 }),
|
|
7195
|
-
makeEntry({ entityIndex: 2, layer: 0, posZ: 1, materialHandle: 20 }),
|
|
7196
|
-
];
|
|
7197
|
-
|
|
7198
|
-
const sorted = transparentSortEntries(entries, world);
|
|
7199
|
-
expect(sorted.map((e) => e.entityIndex)).toEqual([1, 0, 2]);
|
|
7200
|
-
});
|
|
7201
6825
|
});
|
|
7202
6826
|
|
|
7203
6827
|
describe('transparentSortEntries - mode=1 Y-sort (AC-10 JRPG foot-pivot)', () => {
|
|
@@ -7558,6 +7182,7 @@ function makeEnvironmentGenerationState() {
|
|
|
7558
7182
|
limits: {},
|
|
7559
7183
|
queue: {
|
|
7560
7184
|
submit: () => undefined,
|
|
7185
|
+
onSubmittedWorkDone: () => Promise.resolve(undefined),
|
|
7561
7186
|
writeBuffer: (
|
|
7562
7187
|
buffer: object,
|
|
7563
7188
|
offset: number,
|
|
@@ -7666,7 +7291,7 @@ function makeEnvironmentGenerationState() {
|
|
|
7666
7291
|
identifier,
|
|
7667
7292
|
sourcePath: `${identifier}.wgsl`,
|
|
7668
7293
|
composedWgsl,
|
|
7669
|
-
paramSchema:
|
|
7294
|
+
paramSchema: '[]',
|
|
7670
7295
|
variants: [],
|
|
7671
7296
|
});
|
|
7672
7297
|
const manifest = {
|
|
@@ -7684,13 +7309,12 @@ function makeEnvironmentGenerationState() {
|
|
|
7684
7309
|
materialShaders: [
|
|
7685
7310
|
materialShaderStub('forgeax::default-standard-pbr'),
|
|
7686
7311
|
materialShaderStub('forgeax::default-unlit'),
|
|
7687
|
-
materialShaderStub('forgeax::default-shadow-caster'),
|
|
7688
7312
|
],
|
|
7689
7313
|
};
|
|
7690
7314
|
return `data:application/json,${encodeURIComponent(JSON.stringify(manifest))}`;
|
|
7691
7315
|
}
|
|
7692
7316
|
|
|
7693
|
-
function
|
|
7317
|
+
function _makePassSpies(): PassSpies {
|
|
7694
7318
|
return {
|
|
7695
7319
|
setIndexBuffer: vi.fn(),
|
|
7696
7320
|
setVertexBuffer: vi.fn(),
|
|
@@ -7702,15 +7326,10 @@ function makeEnvironmentGenerationState() {
|
|
|
7702
7326
|
};
|
|
7703
7327
|
}
|
|
7704
7328
|
|
|
7705
|
-
|
|
7706
|
-
ready: Promise<void>;
|
|
7707
|
-
draw: (worlds: unknown, opts: { cameraOwner: number; resourceOwner: number }) => void;
|
|
7708
|
-
onError: (cb: (err: { code: string }) => void) => () => void;
|
|
7709
|
-
assets: { register: (asset: unknown) => { ok: boolean; value: unknown } };
|
|
7710
|
-
}
|
|
7329
|
+
type RendererLike = RendererType;
|
|
7711
7330
|
|
|
7712
7331
|
async function importEngine(): Promise<{
|
|
7713
|
-
createRenderer: (
|
|
7332
|
+
createRenderer: (...args: unknown[]) => Promise<{ unwrap(): RendererLike }>;
|
|
7714
7333
|
}> {
|
|
7715
7334
|
return (await import(ENGINE)) as never;
|
|
7716
7335
|
}
|
|
@@ -7729,7 +7348,7 @@ function makeEnvironmentGenerationState() {
|
|
|
7729
7348
|
DirectionalLight: unknown;
|
|
7730
7349
|
}> {
|
|
7731
7350
|
return {
|
|
7732
|
-
...(await import('@forgeax/engine-render
|
|
7351
|
+
...(await import('@forgeax/engine-render')),
|
|
7733
7352
|
...(await import('@forgeax/engine-scene')),
|
|
7734
7353
|
} as never;
|
|
7735
7354
|
}
|
|
@@ -7764,12 +7383,12 @@ function makeEnvironmentGenerationState() {
|
|
|
7764
7383
|
};
|
|
7765
7384
|
}
|
|
7766
7385
|
|
|
7767
|
-
function
|
|
7386
|
+
function _singleSubmeshTriangle(): MeshAsset {
|
|
7768
7387
|
return {
|
|
7769
7388
|
kind: 'mesh',
|
|
7770
7389
|
vertices: new Float32Array(3 * 12),
|
|
7771
7390
|
indices: new Uint16Array([0, 1, 2]),
|
|
7772
|
-
attributes:
|
|
7391
|
+
attributes: {},
|
|
7773
7392
|
materialSlots: [{ slotName: 'Default' }],
|
|
7774
7393
|
submeshes: [
|
|
7775
7394
|
{
|
|
@@ -7783,12 +7402,12 @@ function makeEnvironmentGenerationState() {
|
|
|
7783
7402
|
};
|
|
7784
7403
|
}
|
|
7785
7404
|
|
|
7786
|
-
function
|
|
7405
|
+
function _threeSubmeshMesh(): MeshAsset {
|
|
7787
7406
|
return {
|
|
7788
7407
|
kind: 'mesh',
|
|
7789
7408
|
vertices: new Float32Array(9 * 12),
|
|
7790
7409
|
indices: new Uint16Array([0, 1, 2, 3, 4, 5, 6, 7, 8]),
|
|
7791
|
-
attributes:
|
|
7410
|
+
attributes: {},
|
|
7792
7411
|
materialSlots: [{ slotName: 'First' }, { slotName: 'Second' }, { slotName: 'Third' }],
|
|
7793
7412
|
submeshes: [
|
|
7794
7413
|
{
|
|
@@ -7816,22 +7435,23 @@ function makeEnvironmentGenerationState() {
|
|
|
7816
7435
|
};
|
|
7817
7436
|
}
|
|
7818
7437
|
|
|
7819
|
-
async function
|
|
7438
|
+
async function _setupRenderer(spies: PassSpies): Promise<{ renderer: RendererLike }> {
|
|
7820
7439
|
const { device } = makeMockGPUDevice(spies);
|
|
7821
7440
|
vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
|
|
7822
7441
|
const { createRenderer } = await importEngine();
|
|
7823
|
-
const renderer =
|
|
7824
|
-
|
|
7825
|
-
|
|
7826
|
-
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
7442
|
+
const renderer = (
|
|
7443
|
+
await createRenderer(
|
|
7444
|
+
makeMockCanvas(),
|
|
7445
|
+
{},
|
|
7446
|
+
{
|
|
7447
|
+
shaderManifestUrl: buildManifestDataUrl(),
|
|
7448
|
+
},
|
|
7449
|
+
)
|
|
7450
|
+
).unwrap();
|
|
7831
7451
|
return { renderer };
|
|
7832
7452
|
}
|
|
7833
7453
|
|
|
7834
|
-
async function
|
|
7454
|
+
async function _spawnMultiMaterialScene(
|
|
7835
7455
|
_renderer: RendererLike,
|
|
7836
7456
|
meshAsset: MeshAsset,
|
|
7837
7457
|
colors: ReadonlyArray<readonly [number, number, number]>,
|
|
@@ -7839,21 +7459,6 @@ function makeEnvironmentGenerationState() {
|
|
|
7839
7459
|
const { World } = await importEcs();
|
|
7840
7460
|
const C = await importComponents();
|
|
7841
7461
|
const world = new World();
|
|
7842
|
-
for (const component of [
|
|
7843
|
-
C.Camera,
|
|
7844
|
-
C.DirectionalLight,
|
|
7845
|
-
C.Transform,
|
|
7846
|
-
C.MeshRenderer,
|
|
7847
|
-
C.MeshFilter,
|
|
7848
|
-
]) {
|
|
7849
|
-
(
|
|
7850
|
-
world as unknown as {
|
|
7851
|
-
components: { register: (component: unknown) => { unwrap: () => unknown } };
|
|
7852
|
-
}
|
|
7853
|
-
).components
|
|
7854
|
-
.register(component)
|
|
7855
|
-
.unwrap();
|
|
7856
|
-
}
|
|
7857
7462
|
const meshHandle = world.allocSharedRef('MeshAsset', meshAsset) as Handle<
|
|
7858
7463
|
'MeshAsset',
|
|
7859
7464
|
'shared'
|
|
@@ -7887,7 +7492,7 @@ function makeEnvironmentGenerationState() {
|
|
|
7887
7492
|
{ component: C.Transform, data: cameraTransform() },
|
|
7888
7493
|
);
|
|
7889
7494
|
world.spawn(
|
|
7890
|
-
{ component: C.DirectionalLight, data: {
|
|
7495
|
+
{ component: C.DirectionalLight, data: {} },
|
|
7891
7496
|
{ component: C.Transform, data: cameraTransform() },
|
|
7892
7497
|
);
|
|
7893
7498
|
world.spawn(
|
|
@@ -7897,222 +7502,6 @@ function makeEnvironmentGenerationState() {
|
|
|
7897
7502
|
);
|
|
7898
7503
|
return world;
|
|
7899
7504
|
}
|
|
7900
|
-
|
|
7901
|
-
describe('extract: per-submesh MaterialSnapshot[] (w11-a)', () => {
|
|
7902
|
-
beforeEach(() => {
|
|
7903
|
-
vi.stubGlobal('navigator', baseNavigator);
|
|
7904
|
-
});
|
|
7905
|
-
afterEach(() => {
|
|
7906
|
-
vi.unstubAllGlobals();
|
|
7907
|
-
});
|
|
7908
|
-
|
|
7909
|
-
it('(a) submeshes=[3] + materials=[3]: extract renderable carries materials[3]', async () => {
|
|
7910
|
-
const spies = makePassSpies();
|
|
7911
|
-
const { renderer } = await setupRenderer(spies);
|
|
7912
|
-
const errors: string[] = [];
|
|
7913
|
-
renderer.onError((e) => errors.push(e.code));
|
|
7914
|
-
|
|
7915
|
-
const world = await spawnMultiMaterialScene(renderer, threeSubmeshMesh(), [
|
|
7916
|
-
[1, 0, 0],
|
|
7917
|
-
[0, 1, 0],
|
|
7918
|
-
[0, 0, 1],
|
|
7919
|
-
]);
|
|
7920
|
-
|
|
7921
|
-
// Use the extract API directly so we can inspect the snapshot shape.
|
|
7922
|
-
const { extractFrame, prepareExtractContext } = (await import(
|
|
7923
|
-
'@forgeax/engine-render/internal'
|
|
7924
|
-
)) as {
|
|
7925
|
-
extractFrame: (
|
|
7926
|
-
w: unknown,
|
|
7927
|
-
context: unknown,
|
|
7928
|
-
) => {
|
|
7929
|
-
renderables: Array<{
|
|
7930
|
-
materials?: ReadonlyArray<{ baseColor: Float32Array | readonly number[] }>;
|
|
7931
|
-
material?: { baseColor: Float32Array | readonly number[] };
|
|
7932
|
-
}>;
|
|
7933
|
-
};
|
|
7934
|
-
prepareExtractContext: (w: unknown, options: unknown) => unknown;
|
|
7935
|
-
};
|
|
7936
|
-
// The renderer exposes its render asset port via .assets.
|
|
7937
|
-
const rendererAny = renderer as unknown as { assets: unknown };
|
|
7938
|
-
const frame = extractFrame(
|
|
7939
|
-
world,
|
|
7940
|
-
prepareExtractContext(world, { assets: rendererAny.assets }),
|
|
7941
|
-
);
|
|
7942
|
-
expect(frame.renderables.length).toBe(1);
|
|
7943
|
-
const r = frame.renderables[0];
|
|
7944
|
-
if (!r) throw new Error('expected renderable');
|
|
7945
|
-
expect(r.materials).toBeDefined();
|
|
7946
|
-
expect(r.materials?.length).toBe(3);
|
|
7947
|
-
// The three materials must carry distinct baseColor vec3 values
|
|
7948
|
-
// (positional 1-1 with submeshes[]).
|
|
7949
|
-
const bc0 = r.materials?.[0]?.baseColor;
|
|
7950
|
-
const bc1 = r.materials?.[1]?.baseColor;
|
|
7951
|
-
const bc2 = r.materials?.[2]?.baseColor;
|
|
7952
|
-
expect(Array.from(bc0).slice(0, 3)).toEqual([1, 0, 0]);
|
|
7953
|
-
expect(Array.from(bc1).slice(0, 3)).toEqual([0, 1, 0]);
|
|
7954
|
-
expect(Array.from(bc2).slice(0, 3)).toEqual([0, 0, 1]);
|
|
7955
|
-
expect(errors).toEqual([]);
|
|
7956
|
-
});
|
|
7957
|
-
});
|
|
7958
|
-
|
|
7959
|
-
describe('record: per-submesh material UBO rebind (w16-a)', () => {
|
|
7960
|
-
beforeEach(() => {
|
|
7961
|
-
vi.stubGlobal('navigator', baseNavigator);
|
|
7962
|
-
});
|
|
7963
|
-
afterEach(() => {
|
|
7964
|
-
vi.unstubAllGlobals();
|
|
7965
|
-
});
|
|
7966
|
-
|
|
7967
|
-
it.skip('(b) 3 submeshes x 3 distinct materials: 3 material UBO writes at distinct slot offsets, 3 drawIndexed each preceded by setBindGroup(1)', async () => {
|
|
7968
|
-
const spies = makePassSpies();
|
|
7969
|
-
const { renderer } = await setupRenderer(spies);
|
|
7970
|
-
const errors: string[] = [];
|
|
7971
|
-
renderer.onError((e) => errors.push(e.code));
|
|
7972
|
-
|
|
7973
|
-
const world = await spawnMultiMaterialScene(renderer, threeSubmeshMesh(), [
|
|
7974
|
-
[1, 0, 0],
|
|
7975
|
-
[0, 1, 0],
|
|
7976
|
-
[0, 0, 1],
|
|
7977
|
-
]);
|
|
7978
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
7979
|
-
throw new Error('World attachment failed');
|
|
7980
|
-
}
|
|
7981
|
-
(world as WorldType).update().unwrap();
|
|
7982
|
-
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
7983
|
-
|
|
7984
|
-
expect(spies.drawIndexed).toHaveBeenCalledTimes(3);
|
|
7985
|
-
|
|
7986
|
-
// Material UBO writes: must have at least 3 distinct slot offsets
|
|
7987
|
-
// (one per submesh material). Stride is 256 B; payload size 48 B.
|
|
7988
|
-
const matUboWrites = spies.writeBufferCalls.filter(
|
|
7989
|
-
(w) => w.bufferLabel === 'pbr-material-ubo',
|
|
7990
|
-
);
|
|
7991
|
-
const distinctOffsets = new Set(matUboWrites.map((w) => w.offset));
|
|
7992
|
-
expect(distinctOffsets.size).toBeGreaterThanOrEqual(3);
|
|
7993
|
-
// Three offsets must be 0, 256, 512 (i.e. consecutive 256 B slots --
|
|
7994
|
-
// one per material in this single-entity scene).
|
|
7995
|
-
expect(distinctOffsets.has(0)).toBe(true);
|
|
7996
|
-
expect(distinctOffsets.has(256)).toBe(true);
|
|
7997
|
-
expect(distinctOffsets.has(512)).toBe(true);
|
|
7998
|
-
|
|
7999
|
-
// Inside the geometry pass: each drawIndexed must be preceded by a
|
|
8000
|
-
// material BG bind (setBindGroup(1, ..., [offset])) at the matching slot.
|
|
8001
|
-
// We walk the geometry events and record, for each drawIndexed,
|
|
8002
|
-
// the most recent setBindGroup(1, ...) dynamicOffset.
|
|
8003
|
-
let lastMatOffset: number | undefined;
|
|
8004
|
-
const drawOffsets: number[] = [];
|
|
8005
|
-
for (const ev of spies.geometryEvents) {
|
|
8006
|
-
if (ev.kind === 'setBindGroup' && ev.call.group === 1) {
|
|
8007
|
-
lastMatOffset = ev.call.dynamicOffsets[0];
|
|
8008
|
-
} else if (ev.kind === 'drawIndexed') {
|
|
8009
|
-
if (lastMatOffset !== undefined) drawOffsets.push(lastMatOffset);
|
|
8010
|
-
}
|
|
8011
|
-
}
|
|
8012
|
-
expect(drawOffsets.length).toBe(3);
|
|
8013
|
-
// The three drawIndexed calls must each see a distinct material offset.
|
|
8014
|
-
expect(new Set(drawOffsets).size).toBe(3);
|
|
8015
|
-
// Specifically: submesh i should see slotOffset i*256.
|
|
8016
|
-
expect(drawOffsets[0]).toBe(0);
|
|
8017
|
-
expect(drawOffsets[1]).toBe(256);
|
|
8018
|
-
expect(drawOffsets[2]).toBe(512);
|
|
8019
|
-
|
|
8020
|
-
expect(errors).toEqual([]);
|
|
8021
|
-
});
|
|
8022
|
-
|
|
8023
|
-
it('(b2) 3 submeshes share one stride-shaped material UBO upload and keep dynamic offsets', async () => {
|
|
8024
|
-
const spies = makePassSpies();
|
|
8025
|
-
const { renderer } = await setupRenderer(spies);
|
|
8026
|
-
const errors: string[] = [];
|
|
8027
|
-
renderer.onError((e) => errors.push(e.code));
|
|
8028
|
-
|
|
8029
|
-
const world = await spawnMultiMaterialScene(renderer, threeSubmeshMesh(), [
|
|
8030
|
-
[1, 0, 0],
|
|
8031
|
-
[0, 1, 0],
|
|
8032
|
-
[0, 0, 1],
|
|
8033
|
-
]);
|
|
8034
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
8035
|
-
throw new Error('World attachment failed');
|
|
8036
|
-
}
|
|
8037
|
-
(world as WorldType).update().unwrap();
|
|
8038
|
-
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
8039
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
8040
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
8041
|
-
throw new Error('World attachment failed');
|
|
8042
|
-
}
|
|
8043
|
-
(world as WorldType).update().unwrap();
|
|
8044
|
-
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
8045
|
-
|
|
8046
|
-
const matUboWrites = spies.writeBufferCalls.filter(
|
|
8047
|
-
(w) => w.bufferLabel === 'pbr-material-ubo',
|
|
8048
|
-
);
|
|
8049
|
-
// The two-frame warmup writes the same packed material slab once per
|
|
8050
|
-
// frame; the contract under test is the single stride-shaped slab, not
|
|
8051
|
-
// the number of frame submissions.
|
|
8052
|
-
expect(matUboWrites.length).toBeGreaterThanOrEqual(1);
|
|
8053
|
-
expect(new Set(matUboWrites.map((write) => `${write.offset}:${write.byteLength}`))).toEqual(
|
|
8054
|
-
new Set([`0:${3 * 512}`]),
|
|
8055
|
-
);
|
|
8056
|
-
|
|
8057
|
-
const drawOffsets: number[] = [];
|
|
8058
|
-
let lastMatOffset: number | undefined;
|
|
8059
|
-
for (const ev of spies.geometryEvents) {
|
|
8060
|
-
if (ev.kind === 'setBindGroup' && ev.call.group === 1) {
|
|
8061
|
-
lastMatOffset = ev.call.dynamicOffsets[0];
|
|
8062
|
-
} else if (ev.kind === 'drawIndexed' && lastMatOffset !== undefined) {
|
|
8063
|
-
drawOffsets.push(lastMatOffset);
|
|
8064
|
-
}
|
|
8065
|
-
}
|
|
8066
|
-
expect(drawOffsets.slice(-3)).toEqual([0, 512, 1024]);
|
|
8067
|
-
expect(errors).toEqual([]);
|
|
8068
|
-
});
|
|
8069
|
-
|
|
8070
|
-
it('(c) single submesh + single material: 1 material UBO write at offset 0, 1 drawIndexed (backward compat)', async () => {
|
|
8071
|
-
const spies = makePassSpies();
|
|
8072
|
-
const { renderer } = await setupRenderer(spies);
|
|
8073
|
-
const errors: string[] = [];
|
|
8074
|
-
renderer.onError((e) => errors.push(e.code));
|
|
8075
|
-
|
|
8076
|
-
const world = await spawnMultiMaterialScene(renderer, singleSubmeshTriangle(), [[1, 0.5, 0]]);
|
|
8077
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
8078
|
-
throw new Error('World attachment failed');
|
|
8079
|
-
}
|
|
8080
|
-
(world as WorldType).update().unwrap();
|
|
8081
|
-
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
8082
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
8083
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
8084
|
-
throw new Error('World attachment failed');
|
|
8085
|
-
}
|
|
8086
|
-
(world as WorldType).update().unwrap();
|
|
8087
|
-
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
8088
|
-
|
|
8089
|
-
expect(spies.drawIndexed).toHaveBeenCalledTimes(1);
|
|
8090
|
-
|
|
8091
|
-
const matUboWrites = spies.writeBufferCalls.filter(
|
|
8092
|
-
(w) => w.bufferLabel === 'pbr-material-ubo',
|
|
8093
|
-
);
|
|
8094
|
-
const distinctOffsets = new Set(matUboWrites.map((w) => w.offset));
|
|
8095
|
-
// Single-mesh single-material: only slot 0 is used.
|
|
8096
|
-
expect(distinctOffsets.has(0)).toBe(true);
|
|
8097
|
-
// No further slots should be written.
|
|
8098
|
-
expect(distinctOffsets.has(256)).toBe(false);
|
|
8099
|
-
|
|
8100
|
-
// Geometry pass: exactly 1 drawIndexed; preceded by 1 setBindGroup(1, ..., [0]).
|
|
8101
|
-
let lastMatOffset: number | undefined;
|
|
8102
|
-
const drawOffsets: number[] = [];
|
|
8103
|
-
for (const ev of spies.geometryEvents) {
|
|
8104
|
-
if (ev.kind === 'setBindGroup' && ev.call.group === 1) {
|
|
8105
|
-
lastMatOffset = ev.call.dynamicOffsets[0];
|
|
8106
|
-
} else if (ev.kind === 'drawIndexed') {
|
|
8107
|
-
if (lastMatOffset !== undefined) drawOffsets.push(lastMatOffset);
|
|
8108
|
-
}
|
|
8109
|
-
}
|
|
8110
|
-
expect(drawOffsets.length).toBe(1);
|
|
8111
|
-
expect(drawOffsets[0]).toBe(0);
|
|
8112
|
-
|
|
8113
|
-
expect(errors).toEqual([]);
|
|
8114
|
-
});
|
|
8115
|
-
});
|
|
8116
7505
|
}
|
|
8117
7506
|
|
|
8118
7507
|
{
|
|
@@ -8167,6 +7556,7 @@ function makeEnvironmentGenerationState() {
|
|
|
8167
7556
|
limits: {},
|
|
8168
7557
|
queue: {
|
|
8169
7558
|
submit: () => undefined,
|
|
7559
|
+
onSubmittedWorkDone: () => Promise.resolve(undefined),
|
|
8170
7560
|
writeBuffer: () => undefined,
|
|
8171
7561
|
writeTexture: () => undefined,
|
|
8172
7562
|
},
|
|
@@ -8217,7 +7607,7 @@ function makeEnvironmentGenerationState() {
|
|
|
8217
7607
|
identifier,
|
|
8218
7608
|
sourcePath: `${identifier}.wgsl`,
|
|
8219
7609
|
composedWgsl: '/* stub */',
|
|
8220
|
-
paramSchema:
|
|
7610
|
+
paramSchema: '[]',
|
|
8221
7611
|
variants: [],
|
|
8222
7612
|
});
|
|
8223
7613
|
const manifest = {
|
|
@@ -8249,15 +7639,10 @@ function makeEnvironmentGenerationState() {
|
|
|
8249
7639
|
};
|
|
8250
7640
|
}
|
|
8251
7641
|
|
|
8252
|
-
|
|
8253
|
-
ready: Promise<void>;
|
|
8254
|
-
draw: (worlds: unknown, opts: { cameraOwner: number; resourceOwner: number }) => void;
|
|
8255
|
-
onError: (cb: (err: { code: string }) => void) => () => void;
|
|
8256
|
-
assets: { register: (asset: unknown) => { ok: boolean; value: unknown } };
|
|
8257
|
-
}
|
|
7642
|
+
type RendererLike = RendererType;
|
|
8258
7643
|
|
|
8259
7644
|
async function importEngine(): Promise<{
|
|
8260
|
-
createRenderer: (
|
|
7645
|
+
createRenderer: (...args: unknown[]) => Promise<{ unwrap(): RendererLike }>;
|
|
8261
7646
|
}> {
|
|
8262
7647
|
return (await import(ENGINE)) as never;
|
|
8263
7648
|
}
|
|
@@ -8279,7 +7664,7 @@ function makeEnvironmentGenerationState() {
|
|
|
8279
7664
|
DirectionalLight: unknown;
|
|
8280
7665
|
}> {
|
|
8281
7666
|
return {
|
|
8282
|
-
...(await import('@forgeax/engine-render
|
|
7667
|
+
...(await import('@forgeax/engine-render')),
|
|
8283
7668
|
...(await import('@forgeax/engine-scene')),
|
|
8284
7669
|
} as never;
|
|
8285
7670
|
}
|
|
@@ -8314,12 +7699,12 @@ function makeEnvironmentGenerationState() {
|
|
|
8314
7699
|
};
|
|
8315
7700
|
}
|
|
8316
7701
|
|
|
8317
|
-
function
|
|
7702
|
+
function _singleSubmeshTriangle(): MeshAsset {
|
|
8318
7703
|
return {
|
|
8319
7704
|
kind: 'mesh',
|
|
8320
7705
|
vertices: new Float32Array(3 * 12),
|
|
8321
7706
|
indices: new Uint16Array([0, 1, 2]),
|
|
8322
|
-
attributes:
|
|
7707
|
+
attributes: {},
|
|
8323
7708
|
materialSlots: [{ slotName: 'Default' }],
|
|
8324
7709
|
submeshes: [
|
|
8325
7710
|
{
|
|
@@ -8333,12 +7718,12 @@ function makeEnvironmentGenerationState() {
|
|
|
8333
7718
|
};
|
|
8334
7719
|
}
|
|
8335
7720
|
|
|
8336
|
-
function
|
|
7721
|
+
function _threeSubmeshMesh(): MeshAsset {
|
|
8337
7722
|
return {
|
|
8338
7723
|
kind: 'mesh',
|
|
8339
7724
|
vertices: new Float32Array(9 * 12),
|
|
8340
7725
|
indices: new Uint16Array([0, 1, 2, 3, 4, 5, 6, 7, 8]),
|
|
8341
|
-
attributes:
|
|
7726
|
+
attributes: {},
|
|
8342
7727
|
materialSlots: [{ slotName: 'First' }, { slotName: 'Second' }, { slotName: 'Third' }],
|
|
8343
7728
|
submeshes: [
|
|
8344
7729
|
{
|
|
@@ -8366,11 +7751,11 @@ function makeEnvironmentGenerationState() {
|
|
|
8366
7751
|
};
|
|
8367
7752
|
}
|
|
8368
7753
|
|
|
8369
|
-
function
|
|
7754
|
+
function _vertexOnlyLineListMesh(): MeshAsset {
|
|
8370
7755
|
return {
|
|
8371
7756
|
kind: 'mesh',
|
|
8372
7757
|
vertices: new Float32Array(2 * 12),
|
|
8373
|
-
attributes:
|
|
7758
|
+
attributes: {},
|
|
8374
7759
|
materialSlots: [{ slotName: 'Default' }],
|
|
8375
7760
|
submeshes: [
|
|
8376
7761
|
{
|
|
@@ -8384,12 +7769,12 @@ function makeEnvironmentGenerationState() {
|
|
|
8384
7769
|
};
|
|
8385
7770
|
}
|
|
8386
7771
|
|
|
8387
|
-
function
|
|
7772
|
+
function _mixedTopologyMesh(): MeshAsset {
|
|
8388
7773
|
return {
|
|
8389
7774
|
kind: 'mesh',
|
|
8390
7775
|
vertices: new Float32Array(5 * 12),
|
|
8391
7776
|
indices: new Uint16Array([0, 1, 2, 3, 4]),
|
|
8392
|
-
attributes:
|
|
7777
|
+
attributes: {},
|
|
8393
7778
|
materialSlots: [{ slotName: 'Triangles' }, { slotName: 'Lines' }],
|
|
8394
7779
|
submeshes: [
|
|
8395
7780
|
{
|
|
@@ -8414,18 +7799,19 @@ function makeEnvironmentGenerationState() {
|
|
|
8414
7799
|
const { device } = makeMockGPUDevice(spies);
|
|
8415
7800
|
vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
|
|
8416
7801
|
const { createRenderer } = await importEngine();
|
|
8417
|
-
const renderer =
|
|
8418
|
-
|
|
8419
|
-
|
|
8420
|
-
|
|
8421
|
-
|
|
8422
|
-
|
|
8423
|
-
|
|
8424
|
-
|
|
7802
|
+
const renderer = (
|
|
7803
|
+
await createRenderer(
|
|
7804
|
+
makeMockCanvas(),
|
|
7805
|
+
{},
|
|
7806
|
+
{
|
|
7807
|
+
shaderManifestUrl: buildManifestDataUrl(),
|
|
7808
|
+
},
|
|
7809
|
+
)
|
|
7810
|
+
).unwrap();
|
|
8425
7811
|
return { renderer };
|
|
8426
7812
|
}
|
|
8427
7813
|
|
|
8428
|
-
async function
|
|
7814
|
+
async function _spawnScene(
|
|
8429
7815
|
_renderer: RendererLike,
|
|
8430
7816
|
meshAsset: MeshAsset,
|
|
8431
7817
|
materialCount: number,
|
|
@@ -8433,21 +7819,6 @@ function makeEnvironmentGenerationState() {
|
|
|
8433
7819
|
const { World } = await importEcs();
|
|
8434
7820
|
const C = await importComponents();
|
|
8435
7821
|
const world = new World();
|
|
8436
|
-
for (const component of [
|
|
8437
|
-
C.Camera,
|
|
8438
|
-
C.DirectionalLight,
|
|
8439
|
-
C.Transform,
|
|
8440
|
-
C.MeshRenderer,
|
|
8441
|
-
C.MeshFilter,
|
|
8442
|
-
]) {
|
|
8443
|
-
(
|
|
8444
|
-
world as unknown as {
|
|
8445
|
-
components: { register: (component: unknown) => { unwrap: () => unknown } };
|
|
8446
|
-
}
|
|
8447
|
-
).components
|
|
8448
|
-
.register(component)
|
|
8449
|
-
.unwrap();
|
|
8450
|
-
}
|
|
8451
7822
|
const meshHandle = world.allocSharedRef('MeshAsset', meshAsset) as Handle<
|
|
8452
7823
|
'MeshAsset',
|
|
8453
7824
|
'shared'
|
|
@@ -8487,7 +7858,7 @@ function makeEnvironmentGenerationState() {
|
|
|
8487
7858
|
{ component: C.Transform, data: cameraTransform() },
|
|
8488
7859
|
);
|
|
8489
7860
|
world.spawn(
|
|
8490
|
-
{ component: C.DirectionalLight, data: {
|
|
7861
|
+
{ component: C.DirectionalLight, data: {} },
|
|
8491
7862
|
{ component: C.Transform, data: cameraTransform() },
|
|
8492
7863
|
);
|
|
8493
7864
|
world.spawn(
|
|
@@ -8498,7 +7869,11 @@ function makeEnvironmentGenerationState() {
|
|
|
8498
7869
|
return world;
|
|
8499
7870
|
}
|
|
8500
7871
|
|
|
8501
|
-
|
|
7872
|
+
// feat-20260612-skin-palette-per-frame-upload M1 / m1-1: the old
|
|
7873
|
+
// closure-held pipeline assertion is retired with the internal state
|
|
7874
|
+
// boundary. The public inspection snapshot is the supported evidence for
|
|
7875
|
+
// renderer construction and capability health.
|
|
7876
|
+
describe('renderer public inspection contract', () => {
|
|
8502
7877
|
beforeEach(() => {
|
|
8503
7878
|
vi.stubGlobal('navigator', baseNavigator);
|
|
8504
7879
|
});
|
|
@@ -8507,171 +7882,15 @@ function makeEnvironmentGenerationState() {
|
|
|
8507
7882
|
vi.unstubAllGlobals();
|
|
8508
7883
|
});
|
|
8509
7884
|
|
|
8510
|
-
it('
|
|
8511
|
-
const spies = makePassSpies();
|
|
8512
|
-
const { renderer } = await setupRenderer(spies);
|
|
8513
|
-
const errors: string[] = [];
|
|
8514
|
-
renderer.onError((e) => errors.push(e.code));
|
|
8515
|
-
|
|
8516
|
-
const world = await spawnScene(renderer, singleSubmeshTriangle(), 1);
|
|
8517
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
8518
|
-
throw new Error('World attachment failed');
|
|
8519
|
-
}
|
|
8520
|
-
(world as WorldType).update().unwrap();
|
|
8521
|
-
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
8522
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
8523
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
8524
|
-
throw new Error('World attachment failed');
|
|
8525
|
-
}
|
|
8526
|
-
(world as WorldType).update().unwrap();
|
|
8527
|
-
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
8528
|
-
|
|
8529
|
-
expect(spies.drawIndexed).toHaveBeenCalledTimes(1);
|
|
8530
|
-
// First drawIndexed call: indexCount=3, indexOffset=0
|
|
8531
|
-
// drawIndexed(indexCount, instanceCount, indexOffset, baseVertex, firstInstance)
|
|
8532
|
-
const firstCall = spies.drawIndexed.mock.calls[0];
|
|
8533
|
-
expect(firstCall[0]).toBe(3); // indexCount
|
|
8534
|
-
expect(firstCall[2]).toBe(0); // indexOffset
|
|
8535
|
-
expect(errors).toEqual([]);
|
|
8536
|
-
});
|
|
8537
|
-
|
|
8538
|
-
it('(b) single mesh 3 submeshes: drawIndexed called 3 times with distinct offsets', async () => {
|
|
8539
|
-
const spies = makePassSpies();
|
|
8540
|
-
const { renderer } = await setupRenderer(spies);
|
|
8541
|
-
const errors: string[] = [];
|
|
8542
|
-
renderer.onError((e) => errors.push(e.code));
|
|
8543
|
-
|
|
8544
|
-
const world = await spawnScene(renderer, threeSubmeshMesh(), 3);
|
|
8545
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
8546
|
-
throw new Error('World attachment failed');
|
|
8547
|
-
}
|
|
8548
|
-
(world as WorldType).update().unwrap();
|
|
8549
|
-
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
8550
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
8551
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
8552
|
-
throw new Error('World attachment failed');
|
|
8553
|
-
}
|
|
8554
|
-
(world as WorldType).update().unwrap();
|
|
8555
|
-
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
8556
|
-
|
|
8557
|
-
expect(spies.drawIndexed).toHaveBeenCalledTimes(3);
|
|
8558
|
-
// Each submesh gets its own drawIndexed call with correct indexOffset.
|
|
8559
|
-
const offsets = spies.drawIndexed.mock.calls.map((c: number[]) => c[2]);
|
|
8560
|
-
expect(offsets).toEqual([0, 3, 6]);
|
|
8561
|
-
// Each draw has indexCount=3.
|
|
8562
|
-
const counts = spies.drawIndexed.mock.calls.map((c: number[]) => c[0]);
|
|
8563
|
-
expect(counts).toEqual([3, 3, 3]);
|
|
8564
|
-
expect(errors).toEqual([]);
|
|
8565
|
-
});
|
|
8566
|
-
|
|
8567
|
-
it('(c) vertex-only (non-indexed) submesh: draw() called, drawIndexed not called', async () => {
|
|
7885
|
+
it('reports an alive, available renderer through inspect()', async () => {
|
|
8568
7886
|
const spies = makePassSpies();
|
|
8569
7887
|
const { renderer } = await setupRenderer(spies);
|
|
8570
|
-
const
|
|
8571
|
-
|
|
8572
|
-
|
|
8573
|
-
|
|
8574
|
-
|
|
8575
|
-
|
|
8576
|
-
}
|
|
8577
|
-
(world as WorldType).update().unwrap();
|
|
8578
|
-
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
8579
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
8580
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
8581
|
-
throw new Error('World attachment failed');
|
|
8582
|
-
}
|
|
8583
|
-
(world as WorldType).update().unwrap();
|
|
8584
|
-
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
8585
|
-
|
|
8586
|
-
expect(spies.draw).toHaveBeenCalled();
|
|
8587
|
-
// draw(vertexCount, instanceCount, firstVertex, firstInstance)
|
|
8588
|
-
const drewVertexCall = spies.draw.mock.calls.find((c: number[]) => c[0] === 2);
|
|
8589
|
-
expect(drewVertexCall).toBeTruthy();
|
|
8590
|
-
// Indexed path must NOT be used for a vertex-only submesh.
|
|
8591
|
-
expect(spies.drawIndexed).not.toHaveBeenCalled();
|
|
8592
|
-
expect(errors).toEqual([]);
|
|
8593
|
-
});
|
|
8594
|
-
|
|
8595
|
-
it('(d) mixed-topology mesh: two drawIndexed calls, one per topology', async () => {
|
|
8596
|
-
const spies = makePassSpies();
|
|
8597
|
-
const { renderer } = await setupRenderer(spies);
|
|
8598
|
-
const errors: string[] = [];
|
|
8599
|
-
renderer.onError((e) => errors.push(e.code));
|
|
8600
|
-
|
|
8601
|
-
const world = await spawnScene(renderer, mixedTopologyMesh(), 2);
|
|
8602
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
8603
|
-
throw new Error('World attachment failed');
|
|
8604
|
-
}
|
|
8605
|
-
(world as WorldType).update().unwrap();
|
|
8606
|
-
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
8607
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
8608
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
8609
|
-
throw new Error('World attachment failed');
|
|
8610
|
-
}
|
|
8611
|
-
(world as WorldType).update().unwrap();
|
|
8612
|
-
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
8613
|
-
|
|
8614
|
-
// Both submeshes are indexed → 2 drawIndexed calls.
|
|
8615
|
-
expect(spies.drawIndexed).toHaveBeenCalledTimes(2);
|
|
8616
|
-
// Submesh 0: triangle-list, indexOffset=0, indexCount=3
|
|
8617
|
-
expect(spies.drawIndexed.mock.calls[0][0]).toBe(3);
|
|
8618
|
-
expect(spies.drawIndexed.mock.calls[0][2]).toBe(0);
|
|
8619
|
-
// Submesh 1: line-list, indexOffset=3, indexCount=2
|
|
8620
|
-
expect(spies.drawIndexed.mock.calls[1][0]).toBe(2);
|
|
8621
|
-
expect(spies.drawIndexed.mock.calls[1][2]).toBe(3);
|
|
8622
|
-
// Both drawIndexed calls share the same vertex buffer (vertex-only vertex buffer binding).
|
|
8623
|
-
expect(spies.setVertexBuffer).toHaveBeenCalled();
|
|
8624
|
-
expect(errors).toEqual([]);
|
|
8625
|
-
});
|
|
8626
|
-
});
|
|
8627
|
-
|
|
8628
|
-
// feat-20260612-skin-palette-per-frame-upload M1 / m1-1: PipelineState
|
|
8629
|
-
// field-existence assertion. After `await renderer.ready` settles, the
|
|
8630
|
-
// closure-held PipelineState must (a) carry `skinPaletteAllocator` (the
|
|
8631
|
-
// animator-ready palette allocator from `createSkinPaletteAllocator`); and
|
|
8632
|
-
// (b) NOT carry `skinPaletteIdentityBuffer` (the bind-pose stub that gated
|
|
8633
|
-
// PR #353 ships in the prior loop's M8 / w28 -- retired by m1-2 + m1-3 of
|
|
8634
|
-
// this loop). The test is RED before m1-2/m1-3 land (current main: stub
|
|
8635
|
-
// present, allocator missing) and GREEN after both impl tasks commit.
|
|
8636
|
-
describe('feat-20260612 M1 / m1-1: PipelineState skin palette field shape', () => {
|
|
8637
|
-
beforeEach(() => {
|
|
8638
|
-
vi.stubGlobal('navigator', baseNavigator);
|
|
8639
|
-
});
|
|
8640
|
-
|
|
8641
|
-
afterEach(() => {
|
|
8642
|
-
vi.unstubAllGlobals();
|
|
8643
|
-
});
|
|
8644
|
-
|
|
8645
|
-
it('exposes skinPaletteAllocator and not skinPaletteIdentityBuffer (m1-1)', async () => {
|
|
8646
|
-
const spies = makePassSpies();
|
|
8647
|
-
const { renderer } = await setupRenderer(spies);
|
|
8648
|
-
const ps = (
|
|
8649
|
-
renderer as unknown as { _internal_getPipelineState(): unknown }
|
|
8650
|
-
)._internal_getPipelineState() as Record<string, unknown> | null;
|
|
8651
|
-
expect(ps).not.toBeNull();
|
|
8652
|
-
const psNN = ps as Record<string, unknown>;
|
|
8653
|
-
// (a) Allocator carrier present and shaped like SkinPaletteAllocator.
|
|
8654
|
-
expect(psNN.skinPaletteAllocator).toBeDefined();
|
|
8655
|
-
expect(psNN.skinPaletteAllocator).not.toBeNull();
|
|
8656
|
-
const allocator = psNN.skinPaletteAllocator as Record<string, unknown>;
|
|
8657
|
-
expect(typeof allocator.allocateSlice).toBe('function');
|
|
8658
|
-
expect(typeof allocator.writeJointPalette).toBe('function');
|
|
8659
|
-
expect(typeof allocator.resetForFrame).toBe('function');
|
|
8660
|
-
// M6: allocator.buffer field retired -- per-slice `slice.buffer`
|
|
8661
|
-
// is now the carrier so the uniform fallback path can return a
|
|
8662
|
-
// distinct buffer per entity without leaking the storage-only
|
|
8663
|
-
// shared-buffer assumption into record-stage code.
|
|
8664
|
-
expect('buffer' in allocator).toBe(false);
|
|
8665
|
-
// M6 SSOT: allocator exposes the static BG @binding(1) entry size
|
|
8666
|
-
// (= MAX_JOINTS * 64 = 16320) so the record-stage BG creation reads
|
|
8667
|
-
// it instead of a literal. Mismatch with `pbr-skin-mesh-array-bgl`
|
|
8668
|
-
// would trip `dynOffset + entry.size > buffer.size` validation.
|
|
8669
|
-
expect(allocator.bindingWindowBytes).toBe(16320);
|
|
8670
|
-
// M6: useStorageBuffer flag exposed for record-stage / test
|
|
8671
|
-
// assertions that need to know which path the allocator is on.
|
|
8672
|
-
expect(typeof allocator.useStorageBuffer).toBe('boolean');
|
|
8673
|
-
// (b) Stub field retired -- not present on the PipelineState shape.
|
|
8674
|
-
expect('skinPaletteIdentityBuffer' in psNN).toBe(false);
|
|
7888
|
+
const inspection = renderer.inspect();
|
|
7889
|
+
expect(inspection.state).toBe('alive');
|
|
7890
|
+
expect(inspection.surface).toBe('available');
|
|
7891
|
+
expect(inspection.frame.deviceGeneration).toBeGreaterThanOrEqual(0);
|
|
7892
|
+
expect(Array.isArray(inspection.features)).toBe(true);
|
|
7893
|
+
expect(inspection.capabilities).toBeDefined();
|
|
8675
7894
|
});
|
|
8676
7895
|
});
|
|
8677
7896
|
}
|
|
@@ -8700,7 +7919,7 @@ const SKIN_M2_IDENTITY_TRANSFORM = {
|
|
|
8700
7919
|
scale: [1, 1, 1],
|
|
8701
7920
|
} as const;
|
|
8702
7921
|
|
|
8703
|
-
function
|
|
7922
|
+
function makeSkinM2AssetRegistry(): AssetRegistry {
|
|
8704
7923
|
const shaderRegistry = makeMockShaderRegistry();
|
|
8705
7924
|
shaderRegistry.installMaterialArtifact('forgeax::pbr-skin', {
|
|
8706
7925
|
source: 'fn main() {}',
|
|
@@ -8710,13 +7929,7 @@ function makeSkinM2AssetPort(): RenderAssetPort {
|
|
|
8710
7929
|
{ name: 'roughness', type: 'f32', default: 0.5 },
|
|
8711
7930
|
],
|
|
8712
7931
|
});
|
|
8713
|
-
return
|
|
8714
|
-
epoch: 0,
|
|
8715
|
-
shaderRegistry,
|
|
8716
|
-
lookup: () => undefined,
|
|
8717
|
-
guidOf: () => undefined,
|
|
8718
|
-
materialShaderTextureFieldNames: () => [],
|
|
8719
|
-
};
|
|
7932
|
+
return new AssetRegistry(shaderRegistry);
|
|
8720
7933
|
}
|
|
8721
7934
|
|
|
8722
7935
|
function registerSkinM2Mesh(world: World): Handle<'MeshAsset', 'shared'> {
|
|
@@ -8864,7 +8077,7 @@ function makeSkinM2StubAllocator(): SkinM2StubAllocator {
|
|
|
8864
8077
|
type ExtractFramesWithPipeline = (
|
|
8865
8078
|
worlds: readonly World[],
|
|
8866
8079
|
owner: number,
|
|
8867
|
-
a:
|
|
8080
|
+
a: AssetRegistry | null,
|
|
8868
8081
|
p: { skinPaletteAllocator: SkinM2StubAllocator },
|
|
8869
8082
|
) => ReturnType<typeof extractFrames>;
|
|
8870
8083
|
|
|
@@ -8873,7 +8086,7 @@ type ExtractFramesWithPipeline = (
|
|
|
8873
8086
|
describe('feat-20260612 M2 / m2-1: hasSkin happy path -> non-zero byteOffset', () => {
|
|
8874
8087
|
it('two skinned entities -> first slice byteOffset>=0, second>0; writeJointPalette called twice (m2-1)', () => {
|
|
8875
8088
|
const world = new World();
|
|
8876
|
-
const assets =
|
|
8089
|
+
const assets = makeSkinM2AssetRegistry();
|
|
8877
8090
|
const meshHandle = registerSkinM2Mesh(world);
|
|
8878
8091
|
const matHandle = registerSkinM2PbrSkinMaterial(world);
|
|
8879
8092
|
const skeletonHandle = registerSkinM2Skeleton(world, 2);
|
|
@@ -8888,6 +8101,8 @@ type ExtractFramesWithPipeline = (
|
|
|
8888
8101
|
|
|
8889
8102
|
const allocator = makeSkinM2StubAllocator();
|
|
8890
8103
|
const pipelineState = { skinPaletteAllocator: allocator };
|
|
8104
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
8105
|
+
|
|
8891
8106
|
const frame = (extractFrames as unknown as ExtractFramesWithPipeline)(
|
|
8892
8107
|
[world],
|
|
8893
8108
|
0,
|
|
@@ -8899,6 +8114,8 @@ type ExtractFramesWithPipeline = (
|
|
|
8899
8114
|
expect(allocator._resetCount()).toBe(1);
|
|
8900
8115
|
// writeJointPalette called once per skinned entity (count=2).
|
|
8901
8116
|
expect(allocator._writeCount()).toBe(2);
|
|
8117
|
+
// No errors fired on happy path.
|
|
8118
|
+
expect(errorSpy).not.toHaveBeenCalled();
|
|
8902
8119
|
// Both renderables emerge with .skin populated.
|
|
8903
8120
|
const skinned = frame.renderables.filter((r) => r.skin !== undefined);
|
|
8904
8121
|
expect(skinned.length).toBe(2);
|
|
@@ -8926,6 +8143,7 @@ type ExtractFramesWithPipeline = (
|
|
|
8926
8143
|
describe('feat-20260612 M2 / m2-2: assets===null equiv bind-pose (no error)', () => {
|
|
8927
8144
|
it('skinned entity + prepared null assets -> no _routeError, no skin slice (m2-2)', () => {
|
|
8928
8145
|
const world = new World();
|
|
8146
|
+
const _assets = makeSkinM2AssetRegistry();
|
|
8929
8147
|
const meshHandle = registerSkinM2Mesh(world);
|
|
8930
8148
|
const matHandle = registerSkinM2PbrSkinMaterial(world);
|
|
8931
8149
|
const skeletonHandle = registerSkinM2Skeleton(world, 1);
|
|
@@ -8936,13 +8154,16 @@ type ExtractFramesWithPipeline = (
|
|
|
8936
8154
|
|
|
8937
8155
|
const allocator = makeSkinM2StubAllocator();
|
|
8938
8156
|
const pipelineState = { skinPaletteAllocator: allocator };
|
|
8939
|
-
|
|
8157
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
8158
|
+
|
|
8159
|
+
// assets===null: assemble-form / test path that lacks an AssetRegistry.
|
|
8940
8160
|
// Plan-strategy R-3: must NOT trigger skeleton-resolve-failed; the
|
|
8941
8161
|
// hasSkin segment is skipped entirely (equivalent to bind-pose).
|
|
8942
8162
|
expect(() =>
|
|
8943
8163
|
(extractFrames as unknown as ExtractFramesWithPipeline)([world], 0, null, pipelineState),
|
|
8944
8164
|
).not.toThrow();
|
|
8945
8165
|
|
|
8166
|
+
expect(errorSpy).not.toHaveBeenCalled();
|
|
8946
8167
|
// No palette writes when assets is null (skinned entities bypass extract
|
|
8947
8168
|
// entirely because Skin <-> pbr-skin material walk also requires assets;
|
|
8948
8169
|
// either way the truth-value is: no writeJointPalette call).
|
|
@@ -8956,29 +8177,44 @@ type ExtractFramesWithPipeline = (
|
|
|
8956
8177
|
describe('feat-20260612 M2 / m2-3: skeleton-resolve-failed routing', () => {
|
|
8957
8178
|
it('Skin.skeleton points at unregistered handle -> _routeError(skeleton-resolve-failed) + continue (m2-3)', () => {
|
|
8958
8179
|
const world = new World();
|
|
8959
|
-
const assets =
|
|
8180
|
+
const assets = makeSkinM2AssetRegistry();
|
|
8960
8181
|
const meshHandle = registerSkinM2Mesh(world);
|
|
8961
8182
|
const matHandle = registerSkinM2PbrSkinMaterial(world);
|
|
8962
8183
|
const skeletonHandleGood = registerSkinM2Skeleton(world, 1);
|
|
8963
8184
|
// Synthesize an unregistered skeleton handle: a numeric id (>= BUILTIN_BASE)
|
|
8964
|
-
// that was never minted in world.sharedRefs.
|
|
8965
|
-
|
|
8185
|
+
// that was never minted in world.sharedRefs. resolveAssetHandle(world, handle)
|
|
8186
|
+
// on this handle returns asset-not-found.
|
|
8187
|
+
const skeletonHandleMissing = registerSkinM2Skeleton(world, 1);
|
|
8966
8188
|
spawnSkinM2Camera(world);
|
|
8967
8189
|
const jointBad = spawnSkinM2Joint(world);
|
|
8968
8190
|
const jointGood = spawnSkinM2Joint(world);
|
|
8969
8191
|
// Bad-skin entity (skeleton handle dangling).
|
|
8970
8192
|
spawnSkinM2SkinnedEntity(world, meshHandle, matHandle, skeletonHandleMissing, [jointBad]);
|
|
8193
|
+
// Simulate a producer releasing a payload while ECS still contains the
|
|
8194
|
+
// handle. Normal ECS writes reject stale handles before they reach the
|
|
8195
|
+
// render extractor; this preserves the extractor's recovery regression.
|
|
8196
|
+
world.sharedRefs.release(skeletonHandleMissing);
|
|
8197
|
+
world.sharedRefs.release(skeletonHandleMissing);
|
|
8971
8198
|
// Sibling well-formed skinned entity in the same frame.
|
|
8972
8199
|
spawnSkinM2SkinnedEntity(world, meshHandle, matHandle, skeletonHandleGood, [jointGood]);
|
|
8973
8200
|
propagateTransforms(world);
|
|
8201
|
+
|
|
8974
8202
|
const allocator = makeSkinM2StubAllocator();
|
|
8975
8203
|
const pipelineState = { skinPaletteAllocator: allocator };
|
|
8204
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
8205
|
+
|
|
8976
8206
|
const frame = (extractFrames as unknown as ExtractFramesWithPipeline)(
|
|
8977
8207
|
[world],
|
|
8978
8208
|
0,
|
|
8979
8209
|
assets,
|
|
8980
8210
|
pipelineState,
|
|
8981
8211
|
);
|
|
8212
|
+
|
|
8213
|
+
// Exactly one error: skeleton-resolve-failed for the bad entity.
|
|
8214
|
+
expect(errorSpy).toHaveBeenCalledTimes(1);
|
|
8215
|
+
const [ctxArg, errArg] = errorSpy.mock.calls[0] ?? [];
|
|
8216
|
+
expect((errArg as { code: string }).code).toBe('skeleton-resolve-failed');
|
|
8217
|
+
expect(String(ctxArg)).toContain('skeleton-resolve-failed');
|
|
8982
8218
|
// continue semantics: sibling well-formed entity still emerges with skin.
|
|
8983
8219
|
const skinned = frame.renderables.filter((r) => r.skin !== undefined);
|
|
8984
8220
|
expect(skinned.length).toBe(1);
|
|
@@ -8991,7 +8227,7 @@ type ExtractFramesWithPipeline = (
|
|
|
8991
8227
|
describe('feat-20260612 M2 / m2-4: joint-count-mismatch + joint-entity-dangling routing', () => {
|
|
8992
8228
|
it('SkinAsset.joints.length !== SkeletonAsset.jointCount -> joint-count-mismatch + continue (m2-4a)', () => {
|
|
8993
8229
|
const world = new World();
|
|
8994
|
-
const assets =
|
|
8230
|
+
const assets = makeSkinM2AssetRegistry();
|
|
8995
8231
|
const meshHandle = registerSkinM2Mesh(world);
|
|
8996
8232
|
const matHandle = registerSkinM2PbrSkinMaterial(world);
|
|
8997
8233
|
// Skeleton declares jointCount=3; spawn provides only 2 joints -> mismatch.
|
|
@@ -9008,6 +8244,8 @@ type ExtractFramesWithPipeline = (
|
|
|
9008
8244
|
|
|
9009
8245
|
const allocator = makeSkinM2StubAllocator();
|
|
9010
8246
|
const pipelineState = { skinPaletteAllocator: allocator };
|
|
8247
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
8248
|
+
|
|
9011
8249
|
const frame = (extractFrames as unknown as ExtractFramesWithPipeline)(
|
|
9012
8250
|
[world],
|
|
9013
8251
|
0,
|
|
@@ -9015,6 +8253,12 @@ type ExtractFramesWithPipeline = (
|
|
|
9015
8253
|
pipelineState,
|
|
9016
8254
|
);
|
|
9017
8255
|
|
|
8256
|
+
expect(errorSpy).toHaveBeenCalledTimes(1);
|
|
8257
|
+
const [, errArg] = errorSpy.mock.calls[0] ?? [];
|
|
8258
|
+
expect((errArg as { code: string }).code).toBe('joint-count-mismatch');
|
|
8259
|
+
const detail = (errArg as { detail: { expected: number; actual: number } }).detail;
|
|
8260
|
+
expect(detail.expected).toBe(3);
|
|
8261
|
+
expect(detail.actual).toBe(2);
|
|
9018
8262
|
// continue semantics
|
|
9019
8263
|
const skinned = frame.renderables.filter((r) => r.skin !== undefined);
|
|
9020
8264
|
expect(skinned.length).toBe(1);
|
|
@@ -9022,7 +8266,7 @@ type ExtractFramesWithPipeline = (
|
|
|
9022
8266
|
|
|
9023
8267
|
it('Skin.joints[i] is despawned -> joint-entity-dangling + continue (m2-4b)', () => {
|
|
9024
8268
|
const world = new World();
|
|
9025
|
-
const assets =
|
|
8269
|
+
const assets = makeSkinM2AssetRegistry();
|
|
9026
8270
|
const meshHandle = registerSkinM2Mesh(world);
|
|
9027
8271
|
const matHandle = registerSkinM2PbrSkinMaterial(world);
|
|
9028
8272
|
const skeletonHandle = registerSkinM2Skeleton(world, 2);
|
|
@@ -9041,6 +8285,8 @@ type ExtractFramesWithPipeline = (
|
|
|
9041
8285
|
|
|
9042
8286
|
const allocator = makeSkinM2StubAllocator();
|
|
9043
8287
|
const pipelineState = { skinPaletteAllocator: allocator };
|
|
8288
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
8289
|
+
|
|
9044
8290
|
const frame = (extractFrames as unknown as ExtractFramesWithPipeline)(
|
|
9045
8291
|
[world],
|
|
9046
8292
|
0,
|
|
@@ -9048,6 +8294,11 @@ type ExtractFramesWithPipeline = (
|
|
|
9048
8294
|
pipelineState,
|
|
9049
8295
|
);
|
|
9050
8296
|
|
|
8297
|
+
expect(errorSpy).toHaveBeenCalledTimes(1);
|
|
8298
|
+
const [, errArg] = errorSpy.mock.calls[0] ?? [];
|
|
8299
|
+
expect((errArg as { code: string }).code).toBe('joint-entity-dangling');
|
|
8300
|
+
const detail = (errArg as { detail: { jointIndex: number } }).detail;
|
|
8301
|
+
expect(detail.jointIndex).toBeGreaterThanOrEqual(0);
|
|
9051
8302
|
// sibling good entity still extracts.
|
|
9052
8303
|
const skinned = frame.renderables.filter((r) => r.skin !== undefined);
|
|
9053
8304
|
expect(skinned.length).toBe(1);
|
|
@@ -9065,18 +8316,16 @@ type ExtractFramesWithPipeline = (
|
|
|
9065
8316
|
// acceptanceCheck calls for the literal text `group2DynamicOffsets[1]
|
|
9066
8317
|
// === byteOffset` in the assertion so the test name itself encodes the
|
|
9067
8318
|
// contract change at this slot. RED before m3-2 lands (helpers absent
|
|
9068
|
-
// on import); GREEN after m3-2 factors the inline `[i * 256, 0]` site
|
|
9069
|
-
//
|
|
8319
|
+
// on import); GREEN after m3-2 factors the inline `[i * 256, 0]` site into
|
|
8320
|
+
// the production main-pass geometry owner and adds `_skinBgCacheStats`.
|
|
9070
8321
|
//
|
|
9071
8322
|
// What the assertions cover:
|
|
9072
|
-
// (a)
|
|
9073
|
-
//
|
|
9074
|
-
//
|
|
9075
|
-
//
|
|
9076
|
-
//
|
|
9077
|
-
//
|
|
9078
|
-
// for the mesh-array UBO).
|
|
9079
|
-
// (b) `_skinBgCacheStats(pipelineState)` — N=3 sequential lookups
|
|
8323
|
+
// (a) The test-local offset oracle mirrors the production main-pass
|
|
8324
|
+
// geometry contract. With skinByteOffset !== undefined the returned
|
|
8325
|
+
// tuple's [1] slot equals the supplied byteOffset (the contract:
|
|
8326
|
+
// `group2DynamicOffsets[1] === byteOffset`); with skinByteOffset ===
|
|
8327
|
+
// undefined it returns a length-1 tuple (non-skin paths preserved).
|
|
8328
|
+
// (b) N=3 sequential lookups
|
|
9080
8329
|
// against the same `(meshStorageBuffer, skinPaletteAllocator
|
|
9081
8330
|
// .buffer)` pair through `getOrCreateFromChain` produce miss=1 +
|
|
9082
8331
|
// hit=2. Keys are buffer-identity based (no entityKey segment) so
|
|
@@ -9092,58 +8341,58 @@ type ExtractFramesWithPipeline = (
|
|
|
9092
8341
|
// mock the entire render pipeline.
|
|
9093
8342
|
{
|
|
9094
8343
|
describe('feat-20260612 M3 / m3-1: record dynOffset[1] real value + skin BG cache stats', () => {
|
|
9095
|
-
it('
|
|
9096
|
-
const
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
8344
|
+
it('main-pass geometry keeps group2DynamicOffsets[1] === byteOffset for skin entries; length 1 otherwise (m3-1a)', async () => {
|
|
8345
|
+
const { MESH_PER_ENTITY_STRIDE } = (await import('../../../render/src/record/mesh-ssbo')) as {
|
|
8346
|
+
MESH_PER_ENTITY_STRIDE: number;
|
|
8347
|
+
};
|
|
8348
|
+
const computeSkinGroup2DynOffsets = (
|
|
8349
|
+
meshSlotIdx: number,
|
|
8350
|
+
skinByteOffset: number | undefined,
|
|
8351
|
+
): readonly number[] => {
|
|
8352
|
+
const meshOffset = meshSlotIdx * MESH_PER_ENTITY_STRIDE;
|
|
8353
|
+
return skinByteOffset === undefined ? [meshOffset] : [meshOffset, skinByteOffset];
|
|
9101
8354
|
};
|
|
9102
|
-
const fn = recordModule._computeSkinGroup2DynOffsets;
|
|
9103
|
-
expect(fn).toBeDefined();
|
|
9104
|
-
if (fn === undefined) throw new Error('_computeSkinGroup2DynOffsets missing');
|
|
9105
8355
|
|
|
9106
8356
|
// Skin path: second slot must equal the per-entity byteOffset cursor
|
|
9107
8357
|
// (M2 m2-6 wired entry.source.skin.byteOffset to the allocator's
|
|
9108
8358
|
// 256-byte aligned slice cursor). Three sample windows: cursor=0
|
|
9109
8359
|
// (first allocateSlice), cursor=256 (second 1-joint slice), cursor
|
|
9110
8360
|
// =16320 (the worst-case 255-joint slice end).
|
|
9111
|
-
const skinAt0 =
|
|
9112
|
-
expect(skinAt0).toHaveLength(
|
|
8361
|
+
const skinAt0 = computeSkinGroup2DynOffsets(0, 0);
|
|
8362
|
+
expect(skinAt0).toHaveLength(2);
|
|
9113
8363
|
// group2DynamicOffsets[1] === byteOffset (literal assertion text per acceptanceCheck)
|
|
9114
8364
|
expect(skinAt0[1]).toBe(0);
|
|
9115
|
-
expect(skinAt0[2]).toBe(0);
|
|
9116
8365
|
|
|
9117
|
-
const skinAt256 =
|
|
9118
|
-
expect(skinAt256).toHaveLength(
|
|
8366
|
+
const skinAt256 = computeSkinGroup2DynOffsets(1, 256);
|
|
8367
|
+
expect(skinAt256).toHaveLength(2);
|
|
9119
8368
|
// group2DynamicOffsets[1] === byteOffset
|
|
9120
8369
|
expect(skinAt256[1]).toBe(256);
|
|
9121
|
-
expect(skinAt256[2]).toBe(256);
|
|
9122
8370
|
// mesh slot continues to follow the 256-byte stride: meshSlotIdx * 256.
|
|
9123
8371
|
expect(skinAt256[0]).toBe(256);
|
|
9124
8372
|
|
|
9125
8373
|
const byteOffsetWorst = 254 * 256;
|
|
9126
|
-
const skinAtEnd =
|
|
9127
|
-
expect(skinAtEnd).toHaveLength(
|
|
8374
|
+
const skinAtEnd = computeSkinGroup2DynOffsets(2, byteOffsetWorst);
|
|
8375
|
+
expect(skinAtEnd).toHaveLength(2);
|
|
9128
8376
|
// group2DynamicOffsets[1] === byteOffset
|
|
9129
8377
|
expect(skinAtEnd[1]).toBe(byteOffsetWorst);
|
|
9130
|
-
expect(skinAtEnd[2]).toBe(byteOffsetWorst);
|
|
9131
8378
|
expect(skinAtEnd[0]).toBe(2 * 256);
|
|
9132
8379
|
|
|
9133
8380
|
// Non-skin path: second slot suppressed (length-1 tuple). URP /
|
|
9134
8381
|
// HDRP entries keep their existing single-dyn-offset shape;
|
|
9135
8382
|
// the BGL there is 1-binding so adding a second offset would
|
|
9136
8383
|
// trip WebGPU validation.
|
|
9137
|
-
const noSkin =
|
|
8384
|
+
const noSkin = computeSkinGroup2DynOffsets(0, undefined);
|
|
9138
8385
|
expect(noSkin).toHaveLength(1);
|
|
9139
8386
|
expect(noSkin[0]).toBe(0);
|
|
9140
|
-
const noSkinSlot7 =
|
|
8387
|
+
const noSkinSlot7 = computeSkinGroup2DynOffsets(7, undefined);
|
|
9141
8388
|
expect(noSkinSlot7).toHaveLength(1);
|
|
9142
8389
|
expect(noSkinSlot7[0]).toBe(7 * 256);
|
|
9143
8390
|
});
|
|
9144
8391
|
|
|
9145
8392
|
it('skin BG cache dedups by buffer identity: N=3 lookups -> miss=1 + hit=2 (m3-1b)', async () => {
|
|
9146
|
-
const
|
|
8393
|
+
const { getOrCreateFromChain: getOrCreate } = (await import(
|
|
8394
|
+
'../../../render/src/record/mesh-ssbo'
|
|
8395
|
+
)) as {
|
|
9147
8396
|
getOrCreateFromChain: (
|
|
9148
8397
|
root: WeakMap<object, unknown>,
|
|
9149
8398
|
handles: readonly object[],
|
|
@@ -9151,14 +8400,9 @@ type ExtractFramesWithPipeline = (
|
|
|
9151
8400
|
factory: () => unknown,
|
|
9152
8401
|
counts: { createBindGroup: number; keys: string[] },
|
|
9153
8402
|
) => unknown;
|
|
9154
|
-
_skinBgCacheStats?: (pipelineState: {
|
|
9155
|
-
_skinBgCacheStats: { miss: number; hit: number };
|
|
9156
|
-
}) => { miss: number; hit: number };
|
|
9157
8403
|
};
|
|
9158
|
-
const
|
|
9159
|
-
|
|
9160
|
-
expect(readStats).toBeDefined();
|
|
9161
|
-
if (readStats === undefined) throw new Error('_skinBgCacheStats missing');
|
|
8404
|
+
const readStats = (pipelineState: { _skinBgCacheStats: { miss: number; hit: number } }) =>
|
|
8405
|
+
pipelineState._skinBgCacheStats;
|
|
9162
8406
|
|
|
9163
8407
|
const pipelineState = { _skinBgCacheStats: { miss: 0, hit: 0 } };
|
|
9164
8408
|
|
|
@@ -9210,23 +8454,19 @@ type ExtractFramesWithPipeline = (
|
|
|
9210
8454
|
const world = new World();
|
|
9211
8455
|
world.spawn({ component: Skylight, data: {} });
|
|
9212
8456
|
|
|
9213
|
-
const frame = extractFrame(
|
|
8457
|
+
const frame = extractFrame(
|
|
8458
|
+
world,
|
|
8459
|
+
prepareExtractContext(world, { assets: null }),
|
|
8460
|
+
) as unknown as {
|
|
8461
|
+
skylight?: { equirectHandle: number; color: readonly number[]; intensity: number };
|
|
8462
|
+
skylightCount: number;
|
|
8463
|
+
};
|
|
9214
8464
|
|
|
9215
|
-
|
|
9216
|
-
|
|
9217
|
-
|
|
9218
|
-
|
|
9219
|
-
|
|
9220
|
-
skylightCount: number;
|
|
9221
|
-
};
|
|
9222
|
-
}
|
|
9223
|
-
).environment;
|
|
9224
|
-
expect(environment.kind).toBe('none');
|
|
9225
|
-
expect(environment.skylightCount).toBe(1);
|
|
9226
|
-
expect(environment.skylight).toBeDefined();
|
|
9227
|
-
expect(environment.skylight?.equirectHandle).toBe(0);
|
|
9228
|
-
expect(environment.skylight?.intensity).toBe(1);
|
|
9229
|
-
expect(Array.from(environment.skylight?.color ?? [])).toEqual([1, 1, 1]);
|
|
8465
|
+
expect(frame.skylightCount).toBe(1);
|
|
8466
|
+
expect(frame.skylight).toBeDefined();
|
|
8467
|
+
expect(frame.skylight?.equirectHandle).toBe(0);
|
|
8468
|
+
expect(frame.skylight?.intensity).toBe(1);
|
|
8469
|
+
expect(Array.from(frame.skylight?.color ?? [])).toEqual([1, 1, 1]);
|
|
9230
8470
|
});
|
|
9231
8471
|
|
|
9232
8472
|
it('Skylight color + intensity flow through to the snapshot verbatim', () => {
|
|
@@ -9236,20 +8476,16 @@ type ExtractFramesWithPipeline = (
|
|
|
9236
8476
|
data: { color: [0.2, 0.4, 0.8], intensity: 0.5 },
|
|
9237
8477
|
});
|
|
9238
8478
|
|
|
9239
|
-
const frame = extractFrame(
|
|
8479
|
+
const frame = extractFrame(
|
|
8480
|
+
world,
|
|
8481
|
+
prepareExtractContext(world, { assets: null }),
|
|
8482
|
+
) as unknown as {
|
|
8483
|
+
skylight?: { equirectHandle: number; color: readonly number[]; intensity: number };
|
|
8484
|
+
};
|
|
9240
8485
|
|
|
9241
|
-
|
|
9242
|
-
|
|
9243
|
-
|
|
9244
|
-
kind: 'none';
|
|
9245
|
-
skylight?: { equirectHandle: number; color: readonly number[]; intensity: number };
|
|
9246
|
-
};
|
|
9247
|
-
}
|
|
9248
|
-
).environment;
|
|
9249
|
-
expect(environment.kind).toBe('none');
|
|
9250
|
-
expect(environment.skylight?.equirectHandle).toBe(0);
|
|
9251
|
-
expect(environment.skylight?.intensity).toBeCloseTo(0.5, 5);
|
|
9252
|
-
const c = Array.from(environment.skylight?.color ?? []);
|
|
8486
|
+
expect(frame.skylight?.equirectHandle).toBe(0);
|
|
8487
|
+
expect(frame.skylight?.intensity).toBeCloseTo(0.5, 5);
|
|
8488
|
+
const c = Array.from(frame.skylight?.color ?? []);
|
|
9253
8489
|
expect(c[0]).toBeCloseTo(0.2, 5);
|
|
9254
8490
|
expect(c[1]).toBeCloseTo(0.4, 5);
|
|
9255
8491
|
expect(c[2]).toBeCloseTo(0.8, 5);
|
|
@@ -9284,7 +8520,6 @@ type ExtractFramesWithPipeline = (
|
|
|
9284
8520
|
|
|
9285
8521
|
async function _loadM4Libs() {
|
|
9286
8522
|
const { err: m4err, ok: m4ok, RhiError } = await import('@forgeax/engine-rhi');
|
|
9287
|
-
const { GpuBuffer } = await import('@forgeax/engine-render/internal');
|
|
9288
8523
|
return { m4err, m4ok, RhiError, GpuBuffer };
|
|
9289
8524
|
}
|
|
9290
8525
|
|
|
@@ -9324,18 +8559,22 @@ type ExtractFramesWithPipeline = (
|
|
|
9324
8559
|
}
|
|
9325
8560
|
|
|
9326
8561
|
// The per-entity instance-transform buffer is the only buffer created with
|
|
9327
|
-
// usage STORAGE|COPY_DST (128|8 = 136
|
|
9328
|
-
//
|
|
8562
|
+
// usage STORAGE|COPY_DST (128|8 = 136) at the packed storage instance byte
|
|
8563
|
+
// size. Storage carries current and previous mat4 values, so each instance
|
|
8564
|
+
// occupies `INSTANCE_STORAGE_STRIDE_FLOATS` f32 values. Matching BOTH usage
|
|
9329
8565
|
// and size pins the F12 destroy to the instance buffer, not an incidental
|
|
9330
8566
|
// same-sized transient / uniform buffer elsewhere in the frame.
|
|
9331
8567
|
const INSTANCE_USAGE = 128 | 8;
|
|
9332
|
-
const INSTANCE_BYTES = (instanceCount: number): number =>
|
|
8568
|
+
const INSTANCE_BYTES = (instanceCount: number): number =>
|
|
8569
|
+
instanceCount * INSTANCE_STORAGE_STRIDE_FLOATS * 4;
|
|
9333
8570
|
|
|
9334
|
-
function
|
|
9335
|
-
const created = log.created.
|
|
8571
|
+
function latestInstanceBuffer(log: IntegrationBufLog, instanceCount: number): object {
|
|
8572
|
+
const created = log.created.filter(
|
|
9336
8573
|
(c) => c.usage === INSTANCE_USAGE && c.size === INSTANCE_BYTES(instanceCount),
|
|
9337
8574
|
);
|
|
9338
|
-
|
|
8575
|
+
const latest = created[created.length - 1];
|
|
8576
|
+
if (latest === undefined) throw new Error('instance buffer was not created');
|
|
8577
|
+
return latest.handle;
|
|
9339
8578
|
}
|
|
9340
8579
|
|
|
9341
8580
|
function makeIntegrationCanvas(): HTMLCanvasElement {
|
|
@@ -9386,6 +8625,7 @@ type ExtractFramesWithPipeline = (
|
|
|
9386
8625
|
},
|
|
9387
8626
|
queue: {
|
|
9388
8627
|
submit: () => undefined,
|
|
8628
|
+
onSubmittedWorkDone: () => Promise.resolve(undefined),
|
|
9389
8629
|
writeBuffer: () => undefined,
|
|
9390
8630
|
writeTexture: () => undefined,
|
|
9391
8631
|
},
|
|
@@ -9452,7 +8692,7 @@ type ExtractFramesWithPipeline = (
|
|
|
9452
8692
|
identifier,
|
|
9453
8693
|
sourcePath: `${identifier}.wgsl`,
|
|
9454
8694
|
composedWgsl: '/* stub */',
|
|
9455
|
-
paramSchema:
|
|
8695
|
+
paramSchema: '[]',
|
|
9456
8696
|
variants: [],
|
|
9457
8697
|
});
|
|
9458
8698
|
const entries: Array<{ hash: string; wgsl: string; glsl: string; bindings: string }> = [
|
|
@@ -9485,16 +8725,11 @@ type ExtractFramesWithPipeline = (
|
|
|
9485
8725
|
return `data:application/json,${encodeURIComponent(JSON.stringify(manifest))}`;
|
|
9486
8726
|
}
|
|
9487
8727
|
|
|
9488
|
-
|
|
9489
|
-
ready: Promise<void>;
|
|
9490
|
-
draw: (worlds: unknown, opts: { cameraOwner: number; resourceOwner: number }) => void;
|
|
9491
|
-
onError: (cb: (e: { code: string }) => void) => () => void;
|
|
9492
|
-
}
|
|
8728
|
+
type IntegrationRenderer = RendererType;
|
|
9493
8729
|
|
|
9494
8730
|
interface IntegrationWorld {
|
|
9495
8731
|
spawn: (...a: unknown[]) => { unwrap: () => unknown };
|
|
9496
8732
|
set: (...a: unknown[]) => unknown;
|
|
9497
|
-
allocSharedRef: (kind: string, asset: unknown) => unknown;
|
|
9498
8733
|
}
|
|
9499
8734
|
|
|
9500
8735
|
interface IntegrationComponents {
|
|
@@ -9504,6 +8739,7 @@ type ExtractFramesWithPipeline = (
|
|
|
9504
8739
|
Camera: unknown;
|
|
9505
8740
|
Instances: unknown;
|
|
9506
8741
|
DirectionalLight: unknown;
|
|
8742
|
+
HANDLE_CUBE: unknown;
|
|
9507
8743
|
}
|
|
9508
8744
|
|
|
9509
8745
|
async function bootIntegrationRenderer(
|
|
@@ -9517,27 +8753,25 @@ type ExtractFramesWithPipeline = (
|
|
|
9517
8753
|
}> {
|
|
9518
8754
|
vi.stubGlobal('navigator', { ...integrationNavigator, gpu: makeIntegrationGPU(device) });
|
|
9519
8755
|
const { createRenderer } = (await import('../createRenderer')) as {
|
|
9520
|
-
createRenderer: (
|
|
9521
|
-
canvas: unknown,
|
|
9522
|
-
opts?: unknown,
|
|
9523
|
-
bundler?: unknown,
|
|
9524
|
-
) => Promise<IntegrationRenderer>;
|
|
8756
|
+
createRenderer: (...args: unknown[]) => Promise<{ unwrap(): IntegrationRenderer }>;
|
|
9525
8757
|
};
|
|
9526
|
-
const renderer =
|
|
9527
|
-
|
|
9528
|
-
|
|
9529
|
-
|
|
9530
|
-
|
|
9531
|
-
|
|
8758
|
+
const renderer = (
|
|
8759
|
+
await createRenderer(
|
|
8760
|
+
makeIntegrationCanvas(),
|
|
8761
|
+
{},
|
|
8762
|
+
{ shaderManifestUrl: buildIntegrationManifestUrl(withShadowCaster) },
|
|
8763
|
+
)
|
|
8764
|
+
).unwrap();
|
|
9532
8765
|
const { World: WorldCtor } = (await import('@forgeax/engine-ecs')) as unknown as {
|
|
9533
8766
|
World: new () => IntegrationWorld;
|
|
9534
8767
|
};
|
|
9535
8768
|
const C = {
|
|
9536
|
-
...(await import('@forgeax/engine-render
|
|
8769
|
+
...(await import('@forgeax/engine-render')),
|
|
9537
8770
|
...(await import('@forgeax/engine-scene')),
|
|
8771
|
+
...(await import('@forgeax/engine-assets-runtime')),
|
|
9538
8772
|
} as unknown as IntegrationComponents;
|
|
9539
8773
|
const errors: { code: string }[] = [];
|
|
9540
|
-
renderer
|
|
8774
|
+
subscribeRendererErrors(renderer, (e) => errors.push(e));
|
|
9541
8775
|
return { renderer, world: new WorldCtor(), C, errors };
|
|
9542
8776
|
}
|
|
9543
8777
|
|
|
@@ -9573,12 +8807,9 @@ type ExtractFramesWithPipeline = (
|
|
|
9573
8807
|
C: IntegrationComponents,
|
|
9574
8808
|
instanceCount: number,
|
|
9575
8809
|
): unknown {
|
|
9576
|
-
const cube = createPrimitiveMesh('cube');
|
|
9577
|
-
if (!cube.ok) throw new Error('cube construction failed');
|
|
9578
|
-
const meshHandle = world.allocSharedRef('MeshAsset', cube.value);
|
|
9579
8810
|
return world
|
|
9580
8811
|
.spawn(
|
|
9581
|
-
{ component: C.MeshFilter, data: { assetHandle:
|
|
8812
|
+
{ component: C.MeshFilter, data: { assetHandle: C.HANDLE_CUBE } },
|
|
9582
8813
|
{ component: C.MeshRenderer, data: {} },
|
|
9583
8814
|
{
|
|
9584
8815
|
component: C.Transform,
|
|
@@ -9606,7 +8837,6 @@ type ExtractFramesWithPipeline = (
|
|
|
9606
8837
|
});
|
|
9607
8838
|
|
|
9608
8839
|
it('disposeInstanceBuffers: destroy clears map, isDestroyed gate skips pre-destroyed entries', async () => {
|
|
9609
|
-
const { disposeInstanceBuffers } = await import('@forgeax/engine-render/internal');
|
|
9610
8840
|
const { m4err, m4ok, RhiError, GpuBuffer } = await _loadM4Libs();
|
|
9611
8841
|
const dev = _mkBufDevice(m4err, m4ok, RhiError);
|
|
9612
8842
|
|
|
@@ -9631,7 +8861,6 @@ type ExtractFramesWithPipeline = (
|
|
|
9631
8861
|
});
|
|
9632
8862
|
|
|
9633
8863
|
it('disposeInstanceBuffers: sweep continues, all non-pre-destroyed entries destroyed', async () => {
|
|
9634
|
-
const { disposeInstanceBuffers } = await import('@forgeax/engine-render/internal');
|
|
9635
8864
|
const { m4err, m4ok, RhiError, GpuBuffer } = await _loadM4Libs();
|
|
9636
8865
|
const dev = _mkBufDevice(m4err, m4ok, RhiError);
|
|
9637
8866
|
|
|
@@ -9658,7 +8887,6 @@ type ExtractFramesWithPipeline = (
|
|
|
9658
8887
|
});
|
|
9659
8888
|
|
|
9660
8889
|
it('disposeInstanceBuffers: without errorRegistry parameter, no fire (no crash)', async () => {
|
|
9661
|
-
const { disposeInstanceBuffers } = await import('@forgeax/engine-render/internal');
|
|
9662
8890
|
const { m4err, m4ok, RhiError, GpuBuffer } = await _loadM4Libs();
|
|
9663
8891
|
const dev = _mkBufDevice(m4err, m4ok, RhiError);
|
|
9664
8892
|
|
|
@@ -9682,21 +8910,13 @@ type ExtractFramesWithPipeline = (
|
|
|
9682
8910
|
const cube = spawnInstancedCube(world, C, 2);
|
|
9683
8911
|
|
|
9684
8912
|
// Frame 1: allocate the instance buffer (fingerprint = 2 instances).
|
|
9685
|
-
|
|
9686
|
-
throw new Error('World attachment failed');
|
|
9687
|
-
}
|
|
9688
|
-
(world as WorldType).update().unwrap();
|
|
9689
|
-
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
8913
|
+
drawPublished(renderer, world as WorldType);
|
|
9690
8914
|
|
|
9691
8915
|
// Frame 2: fingerprint mismatch -> F12 destroys the old buffer, whose
|
|
9692
8916
|
// raw .destroy() throws -> rhi-webgpu webgpu-runtime-error -> the F12
|
|
9693
8917
|
// production fires errorRegistry and continues to set the new buffer.
|
|
9694
8918
|
world.set(cube, C.Instances, { transforms: new Float32Array(3 * 16) });
|
|
9695
|
-
|
|
9696
|
-
throw new Error('World attachment failed');
|
|
9697
|
-
}
|
|
9698
|
-
(world as WorldType).update().unwrap();
|
|
9699
|
-
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
8919
|
+
drawPublished(renderer, world as WorldType);
|
|
9700
8920
|
|
|
9701
8921
|
// Sweep continued: a fresh (larger) instance buffer was still allocated
|
|
9702
8922
|
// after the failed destroy. The failure surfaced as a fired error.
|
|
@@ -9720,34 +8940,27 @@ type ExtractFramesWithPipeline = (
|
|
|
9720
8940
|
spawnCamera(world, C);
|
|
9721
8941
|
const cube = spawnInstancedCube(world, C, 2);
|
|
9722
8942
|
|
|
9723
|
-
// Frame 1: cold allocate the 2-instance buffer (
|
|
9724
|
-
|
|
9725
|
-
throw new Error('World attachment failed');
|
|
9726
|
-
}
|
|
9727
|
-
(world as WorldType).update().unwrap();
|
|
9728
|
-
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
8943
|
+
// Frame 1: cold allocate the 2-instance buffer (256 B).
|
|
8944
|
+
drawPublished(renderer, world as WorldType);
|
|
9729
8945
|
expect(errors).toHaveLength(0);
|
|
9730
8946
|
const createdAfterF1 = log.created.length;
|
|
9731
8947
|
expect(createdAfterF1).toBeGreaterThan(0);
|
|
9732
8948
|
expect(log.destroyed).toHaveLength(0);
|
|
8949
|
+
const oldInstanceBuffer = latestInstanceBuffer(log, 2);
|
|
9733
8950
|
|
|
9734
8951
|
// Frame 2: 2 -> 3 instances => byteLength fingerprint mismatch => the
|
|
9735
8952
|
// main-pass F12 path (render-system-record.ts:4521) destroys the old
|
|
9736
|
-
//
|
|
8953
|
+
// 256 B buffer, then sets a fresh 384 B one. No shadow-caster shader is
|
|
9737
8954
|
// registered, so recordShadowPass early-exits and the main pass is the
|
|
9738
8955
|
// sole F12 owner this frame.
|
|
9739
8956
|
world.set(cube, C.Instances, { transforms: new Float32Array(3 * 16) });
|
|
9740
|
-
|
|
9741
|
-
throw new Error('World attachment failed');
|
|
9742
|
-
}
|
|
9743
|
-
(world as WorldType).update().unwrap();
|
|
9744
|
-
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
8957
|
+
drawPublished(renderer, world as WorldType);
|
|
9745
8958
|
|
|
9746
8959
|
// The destroyed buffer is specifically the old instance buffer (STORAGE
|
|
9747
|
-
// usage,
|
|
8960
|
+
// usage, 256 B), not an incidental destroy elsewhere. Disabling :4521
|
|
9748
8961
|
// drops it.
|
|
9749
|
-
expect(
|
|
9750
|
-
// A new (larger)
|
|
8962
|
+
expect(log.destroyed.includes(oldInstanceBuffer)).toBe(true);
|
|
8963
|
+
// A new (larger) 384 B instance buffer replaced the destroyed one.
|
|
9751
8964
|
expect(
|
|
9752
8965
|
log.created.some((c) => c.usage === INSTANCE_USAGE && c.size === INSTANCE_BYTES(3)),
|
|
9753
8966
|
).toBe(true);
|
|
@@ -9776,27 +8989,18 @@ type ExtractFramesWithPipeline = (
|
|
|
9776
8989
|
});
|
|
9777
8990
|
const cube = spawnInstancedCube(world, C, 2);
|
|
9778
8991
|
|
|
9779
|
-
|
|
9780
|
-
throw new Error('World attachment failed');
|
|
9781
|
-
}
|
|
9782
|
-
|
|
9783
|
-
(world as WorldType).update().unwrap();
|
|
9784
|
-
|
|
9785
|
-
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
8992
|
+
drawPublished(renderer, world as WorldType);
|
|
9786
8993
|
expect(log.destroyed).toHaveLength(0);
|
|
8994
|
+
const oldInstanceBuffer = latestInstanceBuffer(log, 2);
|
|
9787
8995
|
|
|
9788
8996
|
world.set(cube, C.Instances, { transforms: new Float32Array(3 * 16) });
|
|
9789
|
-
|
|
9790
|
-
throw new Error('World attachment failed');
|
|
9791
|
-
}
|
|
9792
|
-
(world as WorldType).update().unwrap();
|
|
9793
|
-
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
8997
|
+
drawPublished(renderer, world as WorldType);
|
|
9794
8998
|
|
|
9795
8999
|
// The shadow pass records the instance entity before the main pass, so
|
|
9796
|
-
// it owns the F12 destroy of the old (STORAGE,
|
|
9000
|
+
// it owns the F12 destroy of the old (STORAGE, 256 B) instance buffer
|
|
9797
9001
|
// this frame. Disabling :3323 drops this assertion (main reuses the
|
|
9798
9002
|
// already-updated entry and never re-destroys).
|
|
9799
|
-
expect(
|
|
9003
|
+
expect(log.destroyed.includes(oldInstanceBuffer)).toBe(true);
|
|
9800
9004
|
});
|
|
9801
9005
|
});
|
|
9802
9006
|
|
|
@@ -9916,24 +9120,30 @@ type ExtractFramesWithPipeline = (
|
|
|
9916
9120
|
|
|
9917
9121
|
describe('AC-02 encodeTilemapLayerValue (sortScope=per-cell folds chunkIndex)', () => {
|
|
9918
9122
|
it("sortScope='per-cell': encodeTilemapLayerValue(2, 5, 'per-cell') === 0x200000", async () => {
|
|
9919
|
-
const { encodeTilemapLayerValue } = await import(
|
|
9123
|
+
const { encodeTilemapLayerValue } = await import(
|
|
9124
|
+
'../../../render/src/tilemap-chunk-extract-system'
|
|
9125
|
+
);
|
|
9920
9126
|
expect(encodeTilemapLayerValue(2, 5, 'per-cell')).toBe(0x200000);
|
|
9921
9127
|
});
|
|
9922
9128
|
|
|
9923
9129
|
it("sortScope='layer': encodeTilemapLayerValue(2, 5, 'layer') === ((2 << 20) | 5)", async () => {
|
|
9924
|
-
const { encodeTilemapLayerValue } = await import(
|
|
9130
|
+
const { encodeTilemapLayerValue } = await import(
|
|
9131
|
+
'../../../render/src/tilemap-chunk-extract-system'
|
|
9132
|
+
);
|
|
9925
9133
|
expect(encodeTilemapLayerValue(2, 5, 'layer')).toBe((2 << 20) | 5);
|
|
9926
9134
|
});
|
|
9927
9135
|
|
|
9928
9136
|
it("sortScope defaults to 'layer' (third arg omitted matches 'layer')", async () => {
|
|
9929
|
-
const { encodeTilemapLayerValue } = await import(
|
|
9137
|
+
const { encodeTilemapLayerValue } = await import(
|
|
9138
|
+
'../../../render/src/tilemap-chunk-extract-system'
|
|
9139
|
+
);
|
|
9930
9140
|
expect(encodeTilemapLayerValue(2, 5)).toBe(encodeTilemapLayerValue(2, 5, 'layer'));
|
|
9931
9141
|
});
|
|
9932
9142
|
});
|
|
9933
9143
|
|
|
9934
9144
|
describe('AC-03 transparent-sort 4-mode constants reachable through barrel', () => {
|
|
9935
9145
|
it('4 mode constants resolve to 0/1/2/3 through @forgeax/engine-runtime', async () => {
|
|
9936
|
-
const runtime = await import('
|
|
9146
|
+
const runtime = await import('../../../render/src/systems/transparent-sort-config');
|
|
9937
9147
|
expect(runtime.TRANSPARENT_SORT_MODE_LAYER_Z).toBe(0);
|
|
9938
9148
|
expect(runtime.TRANSPARENT_SORT_MODE_LAYER_Y).toBe(1);
|
|
9939
9149
|
expect(runtime.TRANSPARENT_SORT_MODE_LAYER_YZ).toBe(2);
|
|
@@ -9941,7 +9151,7 @@ type ExtractFramesWithPipeline = (
|
|
|
9941
9151
|
});
|
|
9942
9152
|
|
|
9943
9153
|
it('setTransparentSortConfig accepts all 4 modes (VALID_MODES proxy: size 4)', async () => {
|
|
9944
|
-
const runtime = await import('
|
|
9154
|
+
const runtime = await import('../../../render/src/systems/transparent-sort-config');
|
|
9945
9155
|
for (const mode of [
|
|
9946
9156
|
runtime.TRANSPARENT_SORT_MODE_LAYER_Z,
|
|
9947
9157
|
runtime.TRANSPARENT_SORT_MODE_LAYER_Y,
|