@forgeax/engine-runtime 0.1.4 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -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 +7 -4
- 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 +20 -82
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +107 -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 +608 -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 +24 -9
- 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 +1227 -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 +31 -511
- 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 +21 -25
- 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 +117 -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 +65 -559
- 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 +61 -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 +56 -260
- 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 +15 -14
- 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 +68 -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 +35 -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 +51 -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
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// - packages/runtime/src/__tests__/createRenderer.test.ts
|
|
11
11
|
// - packages/runtime/src/__tests__/dispatch-sort.test.ts
|
|
12
12
|
// - packages/runtime/src/__tests__/engine-metrics.test.ts
|
|
13
|
-
// - packages/runtime/src/__tests__/gpu-
|
|
13
|
+
// - packages/runtime/src/__tests__/device/gpu-residency-caps-guard.test.ts
|
|
14
14
|
// - packages/runtime/src/__tests__/pass-selector.test.ts
|
|
15
15
|
// - packages/runtime/src/__tests__/pipeline-builder.test.ts
|
|
16
16
|
// - packages/runtime/src/__tests__/pipeline-cache-key-topology.test.ts
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
// - packages/runtime/src/__tests__/renderer-ready.test.ts
|
|
28
28
|
// - packages/runtime/src/__tests__/renderstate-pipeline-cache.test.ts
|
|
29
29
|
// - packages/runtime/src/__tests__/storage-buffer-caps.test.ts
|
|
30
|
-
// - packages/runtime/src/__tests__/gpu-
|
|
30
|
+
// - packages/runtime/src/__tests__/device/gpu-residency.test.ts
|
|
31
31
|
// - packages/runtime/src/__tests__/render-data.test.ts
|
|
32
32
|
// - packages/runtime/src/__tests__/hdrp-bgl-slots.test.ts
|
|
33
33
|
// - packages/runtime/src/__tests__/hdrp-caps-gate.test.ts
|
|
@@ -46,58 +46,22 @@
|
|
|
46
46
|
import { existsSync, readFileSync } from 'node:fs';
|
|
47
47
|
import { dirname, resolve } from 'node:path';
|
|
48
48
|
import { fileURLToPath } from 'node:url';
|
|
49
|
+
import { HANDLE_CUBE, HANDLE_TRIANGLE, resolveAssetHandle } from '@forgeax/engine-assets-runtime';
|
|
49
50
|
import type { World as WorldType } from '@forgeax/engine-ecs';
|
|
50
51
|
import { defineComponent, World } from '@forgeax/engine-ecs';
|
|
51
52
|
import {
|
|
53
|
+
buildMeshAttributeMapForUvSets,
|
|
52
54
|
createBoxGeometry,
|
|
53
55
|
createConeGeometry,
|
|
54
56
|
createCylinderGeometry,
|
|
55
57
|
createPlaneGeometry,
|
|
56
|
-
createPrimitiveMesh,
|
|
57
58
|
createSphereGeometry,
|
|
58
59
|
createTorusGeometry,
|
|
59
60
|
PROCEDURAL_FLOATS_PER_VERTEX,
|
|
60
61
|
} from '@forgeax/engine-geometry';
|
|
61
|
-
import {
|
|
62
|
-
createInputSnapshot,
|
|
63
|
-
INPUT_SNAPSHOT_RESOURCE_KEY,
|
|
64
|
-
type InputSnapshot,
|
|
65
|
-
} from '@forgeax/engine-input';
|
|
66
62
|
import { type Mat4, mat4, type Vec3, vec3 } from '@forgeax/engine-math';
|
|
67
63
|
import type { Renderer as RendererType } from '@forgeax/engine-render';
|
|
68
|
-
import {
|
|
69
|
-
assertStorageBufferCap,
|
|
70
|
-
bin,
|
|
71
|
-
buildPbrPipelineLayouts,
|
|
72
|
-
buildPbrViewBglEntries,
|
|
73
|
-
buildPipelineForMaterialShader,
|
|
74
|
-
Camera,
|
|
75
|
-
type ClusterBinError,
|
|
76
|
-
cacheKeyOf,
|
|
77
|
-
calculateSphereClusterBounds,
|
|
78
|
-
clusterSpaceObjectAabb,
|
|
79
|
-
createEngineMetrics,
|
|
80
|
-
createHdrpBindGroupLayoutDescriptor,
|
|
81
|
-
deriveCullingRadius,
|
|
82
|
-
deriveRenderDataCubemap,
|
|
83
|
-
deriveRenderDataMesh,
|
|
84
|
-
deriveRenderDataTexture,
|
|
85
|
-
extractFrame,
|
|
86
|
-
GpuResourceStore,
|
|
87
|
-
HdrpInstallError,
|
|
88
|
-
MeshFilter,
|
|
89
|
-
MeshRenderer,
|
|
90
|
-
matchPass,
|
|
91
|
-
ndcPositionToCluster,
|
|
92
|
-
type PbrCaps,
|
|
93
|
-
type PipelineBuilderContext,
|
|
94
|
-
type PipelineBuilderShaderModuleFactory,
|
|
95
|
-
type PipelineSpec,
|
|
96
|
-
prepareExtractContext,
|
|
97
|
-
sortDispatchByQueue,
|
|
98
|
-
validateClusterGrid,
|
|
99
|
-
viewZToZSlice,
|
|
100
|
-
} from '@forgeax/engine-render/internal';
|
|
64
|
+
import { Camera, MeshFilter, MeshRenderer } from '@forgeax/engine-render';
|
|
101
65
|
import {
|
|
102
66
|
err,
|
|
103
67
|
ok,
|
|
@@ -117,8 +81,6 @@ import {
|
|
|
117
81
|
} from '@forgeax/engine-rhi-null';
|
|
118
82
|
import { ChildOf, Transform } from '@forgeax/engine-scene';
|
|
119
83
|
import {
|
|
120
|
-
DEFAULT_STANDARD_PBR_PARAM_SCHEMA,
|
|
121
|
-
DEFAULT_UNLIT_PARAM_SCHEMA,
|
|
122
84
|
findVariantByKey,
|
|
123
85
|
type MaterialShaderEntry,
|
|
124
86
|
type MaterialShaderManifestEntry,
|
|
@@ -133,26 +95,49 @@ import {
|
|
|
133
95
|
type PassSelector,
|
|
134
96
|
type PrimitiveTopology,
|
|
135
97
|
type RenderPipelineAsset,
|
|
136
|
-
type StencilFaceState,
|
|
137
98
|
type TextureAsset,
|
|
138
99
|
toShared,
|
|
139
100
|
} from '@forgeax/engine-types';
|
|
140
101
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
141
102
|
import {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
103
|
+
bin,
|
|
104
|
+
type ClusterBinError,
|
|
105
|
+
calculateSphereClusterBounds,
|
|
106
|
+
clusterSpaceObjectAabb,
|
|
107
|
+
deriveCullingRadius,
|
|
108
|
+
ndcPositionToCluster,
|
|
109
|
+
viewZToZSlice,
|
|
110
|
+
} from '../../../render/src/cluster-binner';
|
|
111
|
+
import { GpuResidencyCache } from '../../../render/src/device/gpu-residency';
|
|
112
|
+
import { createEngineMetrics } from '../../../render/src/engine-metrics';
|
|
113
|
+
import { assertStorageBufferCap } from '../../../render/src/light-buffer-layout';
|
|
114
|
+
import {
|
|
115
|
+
buildPbrPipelineLayouts,
|
|
116
|
+
buildPbrViewBglEntries,
|
|
117
|
+
type PbrCaps,
|
|
118
|
+
} from '../../../render/src/pbr-pipeline';
|
|
119
|
+
import {
|
|
120
|
+
HdrpInstallError,
|
|
121
|
+
validateClusterGrid,
|
|
122
|
+
} from '../../../render/src/pipeline/standard-pipeline';
|
|
123
|
+
import {
|
|
124
|
+
buildPipelineForMaterialShader,
|
|
125
|
+
type PipelineBuilderContext,
|
|
126
|
+
type PipelineBuilderShaderModuleFactory,
|
|
127
|
+
} from '../../../render/src/pipeline-builder';
|
|
128
|
+
import {
|
|
129
|
+
cacheKeyOf,
|
|
130
|
+
createHdrpBindGroupLayoutDescriptor,
|
|
131
|
+
type PipelineSpec,
|
|
132
|
+
} from '../../../render/src/pipeline-spec';
|
|
133
|
+
import {
|
|
134
|
+
deriveRenderDataCubemap,
|
|
135
|
+
deriveRenderDataMesh,
|
|
136
|
+
deriveRenderDataTexture,
|
|
137
|
+
} from '../../../render/src/render-data';
|
|
138
|
+
import { extractFrame, prepareExtractContext } from '../../../render/src/render-system-extract';
|
|
139
|
+
import { matchPass } from '../../../render/src/systems/pass-selector';
|
|
140
|
+
import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
156
141
|
|
|
157
142
|
function makeExplicitNullRhi(spies: {
|
|
158
143
|
setIndexBuffer: ReturnType<typeof vi.fn>;
|
|
@@ -236,8 +221,8 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
236
221
|
|
|
237
222
|
{
|
|
238
223
|
// --- from create-renderer-fallback.test.ts ---
|
|
239
|
-
const
|
|
240
|
-
const
|
|
224
|
+
const _ENGINE = new URL('../../../render/src/assembly/factory.ts', import.meta.url).href;
|
|
225
|
+
const _ERRORS = new URL('../../../render/src/assembly/backend-contract.ts', import.meta.url).href;
|
|
241
226
|
|
|
242
227
|
// ─── RhiError shape ──────────────────────────────────────────────────────────
|
|
243
228
|
|
|
@@ -250,7 +235,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
250
235
|
|
|
251
236
|
// ─── Canvas mock ─────────────────────────────────────────────────────────────
|
|
252
237
|
|
|
253
|
-
function
|
|
238
|
+
function _makeMockCanvas(opts: { webgpu?: 'context' | 'null' } = {}): HTMLCanvasElement {
|
|
254
239
|
const canvas = {
|
|
255
240
|
width: 800,
|
|
256
241
|
height: 600,
|
|
@@ -315,7 +300,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
315
300
|
return { device };
|
|
316
301
|
}
|
|
317
302
|
|
|
318
|
-
function
|
|
303
|
+
function _makeMockChannel3Module(adapterCase: 'ok' | 'null'): {
|
|
319
304
|
rhi: unknown;
|
|
320
305
|
ensureReady: () => Promise<void>;
|
|
321
306
|
} {
|
|
@@ -376,94 +361,6 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
376
361
|
});
|
|
377
362
|
|
|
378
363
|
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
379
|
-
|
|
380
|
-
describe('createRenderer — Channel 3 integration (navigator.gpu completely absent)', () => {
|
|
381
|
-
// ── AC-01: navigator.gpu absent, Channel 3 ok → createRenderer resolves ──
|
|
382
|
-
|
|
383
|
-
it('AC-01: createRenderer resolves with Renderer when navigator.gpu is absent and Channel 3 works', async () => {
|
|
384
|
-
// navigator.gpu completely absent (no gpu property)
|
|
385
|
-
vi.stubGlobal('navigator', { ...baseNavigator });
|
|
386
|
-
// Mock Channel 3 (rhi-wgpu) with a working backend (ok adapter)
|
|
387
|
-
vi.resetModules();
|
|
388
|
-
vi.doMock('@forgeax/engine-rhi-wgpu', () => makeMockChannel3Module('ok'));
|
|
389
|
-
|
|
390
|
-
const canvas = makeMockCanvas({ webgpu: 'context' });
|
|
391
|
-
const { createRenderer } = (await import(ENGINE)) as {
|
|
392
|
-
createRenderer: (
|
|
393
|
-
canvas: unknown,
|
|
394
|
-
opts?: { shaderManifestUrl?: string | undefined },
|
|
395
|
-
bundler?: unknown,
|
|
396
|
-
) => Promise<{ backend: string; draw: unknown }>;
|
|
397
|
-
};
|
|
398
|
-
|
|
399
|
-
const renderer = await createRenderer(canvas, {}, { shaderManifestUrl: undefined });
|
|
400
|
-
|
|
401
|
-
// AC-01 assertion: renderer resolves successfully
|
|
402
|
-
expect(renderer.backend).toBe('webgpu');
|
|
403
|
-
// AC-01 assertion: draw is a function
|
|
404
|
-
expect(typeof renderer.draw).toBe('function');
|
|
405
|
-
// AC-01 assertion: device is populated
|
|
406
|
-
const rendererAny = renderer as unknown as Record<string, unknown>;
|
|
407
|
-
expect(rendererAny.device).toBeDefined();
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
// ── AC-03: Both channels fail → structured compound error ─────────────────
|
|
411
|
-
|
|
412
|
-
it('AC-03: throws EngineEnvironmentError with structured webgpuError and wgpuError when both channels fail', async () => {
|
|
413
|
-
// Channel 2: navigator.gpu completely absent (falls to Channel 3 directly
|
|
414
|
-
// via loadBackendPack). Mock Channel 3 to fail.
|
|
415
|
-
vi.stubGlobal('navigator', { ...baseNavigator });
|
|
416
|
-
// Mock Channel 3 with a failing adapter
|
|
417
|
-
vi.resetModules();
|
|
418
|
-
vi.doMock('@forgeax/engine-rhi-wgpu', () => makeMockChannel3Module('null'));
|
|
419
|
-
|
|
420
|
-
const canvas = makeMockCanvas({ webgpu: 'context' });
|
|
421
|
-
const { createRenderer } = (await import(ENGINE)) as {
|
|
422
|
-
createRenderer: (canvas: unknown) => Promise<unknown>;
|
|
423
|
-
};
|
|
424
|
-
const { EngineEnvironmentError } = (await import(ERRORS)) as {
|
|
425
|
-
EngineEnvironmentError: new (...args: unknown[]) => Error;
|
|
426
|
-
};
|
|
427
|
-
|
|
428
|
-
try {
|
|
429
|
-
await createRenderer(canvas);
|
|
430
|
-
expect.fail('should have thrown EngineEnvironmentError');
|
|
431
|
-
} catch (err: unknown) {
|
|
432
|
-
// AC-03 assertion: error is an EngineEnvironmentError
|
|
433
|
-
expect(err).toBeInstanceOf(EngineEnvironmentError);
|
|
434
|
-
|
|
435
|
-
const e = err as {
|
|
436
|
-
detail?: {
|
|
437
|
-
webgpuError?: { code?: string; expected?: string; hint?: string };
|
|
438
|
-
wgpuError?: { code?: string; expected?: string; hint?: string };
|
|
439
|
-
};
|
|
440
|
-
};
|
|
441
|
-
|
|
442
|
-
// AC-03 assertion: detail exists
|
|
443
|
-
expect(e.detail).toBeDefined();
|
|
444
|
-
|
|
445
|
-
// AC-03 assertion: at least one of webgpuError or wgpuError is present
|
|
446
|
-
const hasWebgpu = e.detail?.webgpuError !== undefined;
|
|
447
|
-
const hasWgpu = e.detail?.wgpuError !== undefined;
|
|
448
|
-
expect(hasWebgpu || hasWgpu).toBe(true);
|
|
449
|
-
|
|
450
|
-
// AC-03 assertion: error codes are kebab-case strings (RhiErrorCode union members)
|
|
451
|
-
if (e.detail?.wgpuError !== undefined) {
|
|
452
|
-
expect(typeof e.detail.wgpuError.code).toBe('string');
|
|
453
|
-
expect(e.detail.wgpuError.code).toMatch(/^[a-z][a-z-]+$/);
|
|
454
|
-
expect(typeof e.detail.wgpuError.expected).toBe('string');
|
|
455
|
-
expect(typeof e.detail.wgpuError.hint).toBe('string');
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
if (e.detail?.webgpuError !== undefined) {
|
|
459
|
-
expect(typeof e.detail.webgpuError.code).toBe('string');
|
|
460
|
-
expect(e.detail.webgpuError.code).toMatch(/^[a-z][a-z-]+$/);
|
|
461
|
-
expect(typeof e.detail.webgpuError.expected).toBe('string');
|
|
462
|
-
expect(typeof e.detail.webgpuError.hint).toBe('string');
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
});
|
|
466
|
-
});
|
|
467
364
|
}
|
|
468
365
|
|
|
469
366
|
{
|
|
@@ -969,7 +866,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
969
866
|
{
|
|
970
867
|
// --- from create-renderer-fallback-shader-manifest.test.ts ---
|
|
971
868
|
const CREATE_RENDERER_SRC = fileURLToPath(
|
|
972
|
-
new URL('../../../render/src/
|
|
869
|
+
new URL('../../../render/src/assembly/factory.ts', import.meta.url),
|
|
973
870
|
);
|
|
974
871
|
|
|
975
872
|
describe('createRenderer fallback shaderManifestUrl literal (AC-08, D-2 q5-A)', () => {
|
|
@@ -1125,8 +1022,8 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
1125
1022
|
|
|
1126
1023
|
{
|
|
1127
1024
|
// --- from createRenderer.test.ts ---
|
|
1128
|
-
const
|
|
1129
|
-
const
|
|
1025
|
+
const _ENGINE = new URL('../../../render/src/assembly/factory.ts', import.meta.url).href;
|
|
1026
|
+
const _ERRORS = new URL('../../../render/src/assembly/backend-contract.ts', import.meta.url).href;
|
|
1130
1027
|
|
|
1131
1028
|
// Default mock: Channel 3 (rhi-wgpu) dynamic import fails so existing
|
|
1132
1029
|
// "throws" tests continue to see EngineEnvironmentError. Overridden
|
|
@@ -1167,7 +1064,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
1167
1064
|
|
|
1168
1065
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
1169
1066
|
|
|
1170
|
-
function
|
|
1067
|
+
function _makeMockCanvas(opts: { webgpu?: 'context' | 'null' }): HTMLCanvasElement {
|
|
1171
1068
|
const canvas = {
|
|
1172
1069
|
width: 800,
|
|
1173
1070
|
height: 600,
|
|
@@ -1230,7 +1127,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
1230
1127
|
return { device };
|
|
1231
1128
|
}
|
|
1232
1129
|
|
|
1233
|
-
function
|
|
1130
|
+
function _makeMockGPUWithAdapter(adapterCase: 'ok' | 'null'): unknown {
|
|
1234
1131
|
const { device } = makeMockGPUDevice();
|
|
1235
1132
|
return {
|
|
1236
1133
|
requestAdapter: async () => {
|
|
@@ -1249,7 +1146,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
1249
1146
|
* Build a mock Channel 3 (rhi-wgpu) backend for fallback tests (bug-20260526).
|
|
1250
1147
|
* @param adapterCase 'ok' = adapter+device succeed; 'null' = adapter is null (Channel 3 also fails).
|
|
1251
1148
|
*/
|
|
1252
|
-
function
|
|
1149
|
+
function _makeMockChannel3Module(adapterCase: 'ok' | 'null'): {
|
|
1253
1150
|
rhi: unknown;
|
|
1254
1151
|
ensureReady: () => Promise<void>;
|
|
1255
1152
|
} {
|
|
@@ -1301,271 +1198,6 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
1301
1198
|
});
|
|
1302
1199
|
|
|
1303
1200
|
// ─── Tests ──────────────────────────────────────────────────────────────────
|
|
1304
|
-
|
|
1305
|
-
describe('createRenderer — post-WebGL2-stub-deletion contract', () => {
|
|
1306
|
-
it('throws EngineEnvironmentError when navigator.gpu is absent (channel 3 wasm load fails)', async () => {
|
|
1307
|
-
// navigator.gpu absent + no rhi-wgpu mock → loadBackendPack channel 3 fails
|
|
1308
|
-
vi.stubGlobal('navigator', { ...baseNavigator });
|
|
1309
|
-
const canvas = makeMockCanvas({ webgpu: 'null' });
|
|
1310
|
-
const { createRenderer } = (await import(ENGINE)) as {
|
|
1311
|
-
createRenderer: (canvas: unknown) => Promise<unknown>;
|
|
1312
|
-
};
|
|
1313
|
-
const { EngineEnvironmentError } = (await import(ERRORS)) as {
|
|
1314
|
-
EngineEnvironmentError: new (...args: unknown[]) => Error;
|
|
1315
|
-
};
|
|
1316
|
-
|
|
1317
|
-
await expect(createRenderer(canvas)).rejects.toBeInstanceOf(EngineEnvironmentError);
|
|
1318
|
-
});
|
|
1319
|
-
|
|
1320
|
-
it('thrown EngineEnvironmentError carries detail.webgpuError when channel 3 fails', async () => {
|
|
1321
|
-
vi.stubGlobal('navigator', { ...baseNavigator });
|
|
1322
|
-
const canvas = makeMockCanvas({ webgpu: 'null' });
|
|
1323
|
-
const { createRenderer } = (await import(ENGINE)) as {
|
|
1324
|
-
createRenderer: (canvas: unknown) => Promise<unknown>;
|
|
1325
|
-
};
|
|
1326
|
-
|
|
1327
|
-
try {
|
|
1328
|
-
await createRenderer(canvas);
|
|
1329
|
-
expect.fail('should have thrown');
|
|
1330
|
-
} catch (err: unknown) {
|
|
1331
|
-
const e = err as { detail?: { webgpuError?: { code?: string } } };
|
|
1332
|
-
expect(e.detail).toBeDefined();
|
|
1333
|
-
expect(e.detail?.webgpuError).toBeDefined();
|
|
1334
|
-
expect(typeof e.detail?.webgpuError?.code).toBe('string');
|
|
1335
|
-
}
|
|
1336
|
-
});
|
|
1337
|
-
|
|
1338
|
-
it('returns a webgpu Renderer when navigator.gpu yields an adapter+device', async () => {
|
|
1339
|
-
vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPUWithAdapter('ok') });
|
|
1340
|
-
const canvas = makeMockCanvas({ webgpu: 'context' });
|
|
1341
|
-
const { createRenderer } = (await import(ENGINE)) as {
|
|
1342
|
-
createRenderer: (
|
|
1343
|
-
canvas: unknown,
|
|
1344
|
-
opts?: { shaderManifestUrl?: string | undefined },
|
|
1345
|
-
bundler?: unknown,
|
|
1346
|
-
) => Promise<{ backend: string }>;
|
|
1347
|
-
};
|
|
1348
|
-
|
|
1349
|
-
const renderer = await createRenderer(canvas, {}, { shaderManifestUrl: undefined });
|
|
1350
|
-
expect(renderer.backend).toBe('webgpu');
|
|
1351
|
-
});
|
|
1352
|
-
|
|
1353
|
-
it('returned Renderer exposes the documented surface (draw / dispose / onLost / backend / device)', async () => {
|
|
1354
|
-
vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPUWithAdapter('ok') });
|
|
1355
|
-
const canvas = makeMockCanvas({ webgpu: 'context' });
|
|
1356
|
-
const { createRenderer } = (await import(ENGINE)) as {
|
|
1357
|
-
createRenderer: (
|
|
1358
|
-
canvas: unknown,
|
|
1359
|
-
opts?: { shaderManifestUrl?: string | undefined },
|
|
1360
|
-
bundler?: unknown,
|
|
1361
|
-
) => Promise<Record<string, unknown>>;
|
|
1362
|
-
};
|
|
1363
|
-
|
|
1364
|
-
const renderer = await createRenderer(canvas, {}, { shaderManifestUrl: undefined });
|
|
1365
|
-
expect(typeof (renderer as { draw: unknown }).draw).toBe('function');
|
|
1366
|
-
expect(typeof (renderer as { dispose: unknown }).dispose).toBe('function');
|
|
1367
|
-
expect(typeof (renderer as { onLost: unknown }).onLost).toBe('function');
|
|
1368
|
-
expect((renderer as { backend: unknown }).backend).toBe('webgpu');
|
|
1369
|
-
expect((renderer as { device: unknown }).device).toBeDefined();
|
|
1370
|
-
});
|
|
1371
|
-
|
|
1372
|
-
it('throws EngineEnvironmentError when adapter request returns null (rhi-err path)', async () => {
|
|
1373
|
-
vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPUWithAdapter('null') });
|
|
1374
|
-
const canvas = makeMockCanvas({ webgpu: 'context' });
|
|
1375
|
-
const { createRenderer } = (await import(ENGINE)) as {
|
|
1376
|
-
createRenderer: (canvas: unknown) => Promise<unknown>;
|
|
1377
|
-
};
|
|
1378
|
-
const { EngineEnvironmentError } = (await import(ERRORS)) as {
|
|
1379
|
-
EngineEnvironmentError: new (...args: unknown[]) => Error;
|
|
1380
|
-
};
|
|
1381
|
-
|
|
1382
|
-
await expect(createRenderer(canvas)).rejects.toBeInstanceOf(EngineEnvironmentError);
|
|
1383
|
-
});
|
|
1384
|
-
|
|
1385
|
-
// ── bug-20260526: Channel 2 -> Channel 3 fallback ───────────────────────
|
|
1386
|
-
|
|
1387
|
-
it('AC-01: falls back to Channel 3 when Channel 2 adapter is null (navigator.gpu present, adapter=null, Channel 3 ok)', async () => {
|
|
1388
|
-
// Channel 2: navigator.gpu present but adapter is null.
|
|
1389
|
-
vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPUWithAdapter('null') });
|
|
1390
|
-
// Channel 3: mock rhi-wgpu with a working adapter+device.
|
|
1391
|
-
vi.doMock('@forgeax/engine-rhi-wgpu', () => makeMockChannel3Module('ok'));
|
|
1392
|
-
|
|
1393
|
-
const canvas = makeMockCanvas({ webgpu: 'context' });
|
|
1394
|
-
const { createRenderer } = (await import(ENGINE)) as {
|
|
1395
|
-
createRenderer: (
|
|
1396
|
-
canvas: unknown,
|
|
1397
|
-
opts?: { shaderManifestUrl?: string | undefined },
|
|
1398
|
-
bundler?: unknown,
|
|
1399
|
-
) => Promise<{ backend: string }>;
|
|
1400
|
-
};
|
|
1401
|
-
|
|
1402
|
-
const renderer = await createRenderer(canvas, {}, { shaderManifestUrl: undefined });
|
|
1403
|
-
expect(renderer.backend).toBe('webgpu');
|
|
1404
|
-
});
|
|
1405
|
-
|
|
1406
|
-
it('AC-02: throws EngineEnvironmentError with dual error detail when both Channel 2 and Channel 3 fail', async () => {
|
|
1407
|
-
// Channel 2: navigator.gpu present but adapter is null.
|
|
1408
|
-
vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPUWithAdapter('null') });
|
|
1409
|
-
// Channel 3: mock rhi-wgpu where adapter is also null (both fail).
|
|
1410
|
-
vi.doMock('@forgeax/engine-rhi-wgpu', () => makeMockChannel3Module('null'));
|
|
1411
|
-
|
|
1412
|
-
const canvas = makeMockCanvas({ webgpu: 'context' });
|
|
1413
|
-
const { createRenderer } = (await import(ENGINE)) as {
|
|
1414
|
-
createRenderer: (canvas: unknown) => Promise<unknown>;
|
|
1415
|
-
};
|
|
1416
|
-
|
|
1417
|
-
try {
|
|
1418
|
-
await createRenderer(canvas);
|
|
1419
|
-
expect.fail('should have thrown');
|
|
1420
|
-
} catch (err: unknown) {
|
|
1421
|
-
const e = err as {
|
|
1422
|
-
detail?: { webgpuError?: { code?: string }; wgpuError?: { code?: string } };
|
|
1423
|
-
};
|
|
1424
|
-
expect(e.detail).toBeDefined();
|
|
1425
|
-
expect(e.detail?.webgpuError).toBeDefined();
|
|
1426
|
-
expect(typeof e.detail?.webgpuError?.code).toBe('string');
|
|
1427
|
-
expect(e.detail?.wgpuError).toBeDefined();
|
|
1428
|
-
expect(typeof e.detail?.wgpuError?.code).toBe('string');
|
|
1429
|
-
}
|
|
1430
|
-
});
|
|
1431
|
-
|
|
1432
|
-
// ── w20: loadRhiPack helper extraction tests ───────────────────────────
|
|
1433
|
-
|
|
1434
|
-
describe('loadRhiPack — pack field extraction', () => {
|
|
1435
|
-
it('(a) module with only rhi returns pack with rhi only', async () => {
|
|
1436
|
-
const { loadRhiPack } = (await import(ENGINE)) as {
|
|
1437
|
-
loadRhiPack: (mod: Record<string, unknown>) => Record<string, unknown>;
|
|
1438
|
-
};
|
|
1439
|
-
const rhi = { createAdapter: () => undefined };
|
|
1440
|
-
const mod: Record<string, unknown> = { rhi };
|
|
1441
|
-
const pack = loadRhiPack(mod);
|
|
1442
|
-
expect(pack.rhi).toBe(rhi);
|
|
1443
|
-
expect('createShaderModule' in (pack as Record<string, unknown>)).toBe(false);
|
|
1444
|
-
expect('translateErrorEventToRhiError' in (pack as Record<string, unknown>)).toBe(false);
|
|
1445
|
-
expect('_internal_getRawDevice' in (pack as Record<string, unknown>)).toBe(false);
|
|
1446
|
-
});
|
|
1447
|
-
|
|
1448
|
-
it('(b) module with rhi + createShaderModule returns pack with both', async () => {
|
|
1449
|
-
const { loadRhiPack } = (await import(ENGINE)) as {
|
|
1450
|
-
loadRhiPack: (mod: Record<string, unknown>) => Record<string, unknown>;
|
|
1451
|
-
};
|
|
1452
|
-
const rhi = { createAdapter: () => undefined };
|
|
1453
|
-
const csm = () => Promise.resolve();
|
|
1454
|
-
const mod: Record<string, unknown> = { rhi, createShaderModule: csm };
|
|
1455
|
-
const pack = loadRhiPack(mod) as Record<string, unknown>;
|
|
1456
|
-
expect(pack.rhi).toBe(rhi);
|
|
1457
|
-
expect(pack.createShaderModule).toBe(csm);
|
|
1458
|
-
expect(pack.translateErrorEventToRhiError).toBeUndefined();
|
|
1459
|
-
expect(pack._internal_getRawDevice).toBeUndefined();
|
|
1460
|
-
});
|
|
1461
|
-
|
|
1462
|
-
it('(c) module with rhi + all three optional fields returns pack with all four', async () => {
|
|
1463
|
-
const { loadRhiPack } = (await import(ENGINE)) as {
|
|
1464
|
-
loadRhiPack: (mod: Record<string, unknown>) => Record<string, unknown>;
|
|
1465
|
-
};
|
|
1466
|
-
const rhi = { createAdapter: () => undefined };
|
|
1467
|
-
const csm = () => Promise.resolve();
|
|
1468
|
-
const tx = () => ({ ok: false, error: {} });
|
|
1469
|
-
const rd = () => undefined;
|
|
1470
|
-
const mod: Record<string, unknown> = {
|
|
1471
|
-
rhi,
|
|
1472
|
-
createShaderModule: csm,
|
|
1473
|
-
translateErrorEventToRhiError: tx,
|
|
1474
|
-
_internal_getRawDevice: rd,
|
|
1475
|
-
};
|
|
1476
|
-
const pack = loadRhiPack(mod) as Record<string, unknown>;
|
|
1477
|
-
expect(pack.rhi).toBe(rhi);
|
|
1478
|
-
expect(pack.createShaderModule).toBe(csm);
|
|
1479
|
-
expect(pack.translateErrorEventToRhiError).toBe(tx);
|
|
1480
|
-
expect(pack._internal_getRawDevice).toBe(rd);
|
|
1481
|
-
});
|
|
1482
|
-
|
|
1483
|
-
it('(d) module with rhi + irrelevant noise field ignores noise', async () => {
|
|
1484
|
-
const { loadRhiPack } = (await import(ENGINE)) as {
|
|
1485
|
-
loadRhiPack: (mod: Record<string, unknown>) => Record<string, unknown>;
|
|
1486
|
-
};
|
|
1487
|
-
const rhi = { createAdapter: () => undefined };
|
|
1488
|
-
const mod: Record<string, unknown> = {
|
|
1489
|
-
rhi,
|
|
1490
|
-
noise: 'irrelevant-value',
|
|
1491
|
-
alsoNoise: 42,
|
|
1492
|
-
};
|
|
1493
|
-
const pack = loadRhiPack(mod) as Record<string, unknown>;
|
|
1494
|
-
expect(pack.rhi).toBe(rhi);
|
|
1495
|
-
expect((pack as Record<string, unknown>).noise).toBeUndefined();
|
|
1496
|
-
expect((pack as Record<string, unknown>).alsoNoise).toBeUndefined();
|
|
1497
|
-
expect('createShaderModule' in pack).toBe(false);
|
|
1498
|
-
});
|
|
1499
|
-
|
|
1500
|
-
it('(e) non-function values for optional fields: null/nullish pass through via in operator', async () => {
|
|
1501
|
-
const { loadRhiPack } = (await import(ENGINE)) as {
|
|
1502
|
-
loadRhiPack: (mod: Record<string, unknown>) => Record<string, unknown>;
|
|
1503
|
-
};
|
|
1504
|
-
const rhi = { createAdapter: () => undefined };
|
|
1505
|
-
const mod: Record<string, unknown> = {
|
|
1506
|
-
rhi,
|
|
1507
|
-
createShaderModule: null,
|
|
1508
|
-
translateErrorEventToRhiError: null,
|
|
1509
|
-
_internal_getRawDevice: null,
|
|
1510
|
-
};
|
|
1511
|
-
const pack = loadRhiPack(mod) as Record<string, unknown>;
|
|
1512
|
-
expect(pack.rhi).toBe(rhi);
|
|
1513
|
-
// 'x' in mod returns true for explicit null/undefined; helper preserves them.
|
|
1514
|
-
expect(pack.createShaderModule).toBe(null);
|
|
1515
|
-
expect(pack.translateErrorEventToRhiError).toBe(null);
|
|
1516
|
-
expect(pack._internal_getRawDevice).toBe(null);
|
|
1517
|
-
});
|
|
1518
|
-
});
|
|
1519
|
-
});
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
{
|
|
1523
|
-
// --- from dispatch-sort.test.ts ---
|
|
1524
|
-
interface SortEntry {
|
|
1525
|
-
id: string;
|
|
1526
|
-
queue: number;
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
describe('sortDispatchByQueue', () => {
|
|
1530
|
-
it('different queue values sorted in ascending order', () => {
|
|
1531
|
-
const entries: SortEntry[] = [
|
|
1532
|
-
{ id: 'overlay', queue: 4000 },
|
|
1533
|
-
{ id: 'background', queue: 1000 },
|
|
1534
|
-
{ id: 'geometry', queue: 2000 },
|
|
1535
|
-
{ id: 'transparent', queue: 3000 },
|
|
1536
|
-
];
|
|
1537
|
-
const sorted = sortDispatchByQueue(entries);
|
|
1538
|
-
expect(sorted.map((e) => e.id)).toEqual(['background', 'geometry', 'transparent', 'overlay']);
|
|
1539
|
-
});
|
|
1540
|
-
|
|
1541
|
-
it('same queue values preserve insertion order (stable sort)', () => {
|
|
1542
|
-
const entries: SortEntry[] = [
|
|
1543
|
-
{ id: 'first', queue: 2000 },
|
|
1544
|
-
{ id: 'second', queue: 2000 },
|
|
1545
|
-
{ id: 'third', queue: 2000 },
|
|
1546
|
-
];
|
|
1547
|
-
const sorted = sortDispatchByQueue(entries);
|
|
1548
|
-
expect(sorted.map((e) => e.id)).toEqual(['first', 'second', 'third']);
|
|
1549
|
-
});
|
|
1550
|
-
|
|
1551
|
-
it('empty array returns empty array', () => {
|
|
1552
|
-
const sorted = sortDispatchByQueue([]);
|
|
1553
|
-
expect(sorted).toEqual([]);
|
|
1554
|
-
});
|
|
1555
|
-
|
|
1556
|
-
it('mixed queue values with duplicates preserve stable order within each queue', () => {
|
|
1557
|
-
const entries: SortEntry[] = [
|
|
1558
|
-
{ id: 'bg', queue: 1000 },
|
|
1559
|
-
{ id: 'geo-1', queue: 2000 },
|
|
1560
|
-
{ id: 'at-1', queue: 2450 },
|
|
1561
|
-
{ id: 'geo-2', queue: 2000 },
|
|
1562
|
-
{ id: 'at-2', queue: 2450 },
|
|
1563
|
-
{ id: 'trans', queue: 3000 },
|
|
1564
|
-
];
|
|
1565
|
-
const sorted = sortDispatchByQueue(entries);
|
|
1566
|
-
expect(sorted.map((e) => e.id)).toEqual(['bg', 'geo-1', 'geo-2', 'at-1', 'at-2', 'trans']);
|
|
1567
|
-
});
|
|
1568
|
-
});
|
|
1569
1201
|
}
|
|
1570
1202
|
|
|
1571
1203
|
{
|
|
@@ -1623,7 +1255,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
1623
1255
|
}
|
|
1624
1256
|
|
|
1625
1257
|
{
|
|
1626
|
-
// --- from gpu-
|
|
1258
|
+
// --- from device/gpu-residency-caps-guard.test.ts ---
|
|
1627
1259
|
const okShim = <T>(v: T) => ({ ok: true as const, value: v });
|
|
1628
1260
|
|
|
1629
1261
|
const capsTrue: RhiCaps = {
|
|
@@ -1647,14 +1279,14 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
1647
1279
|
float32Filterable: false,
|
|
1648
1280
|
};
|
|
1649
1281
|
|
|
1650
|
-
const
|
|
1282
|
+
const _capsCubemapDisabled: RhiCaps = {
|
|
1651
1283
|
...capsTrue,
|
|
1652
1284
|
rgba16floatRenderable: false,
|
|
1653
1285
|
};
|
|
1654
1286
|
|
|
1655
1287
|
// Minimal mock device covering the cubemap upload surface.
|
|
1656
1288
|
// biome-ignore lint/suspicious/noExplicitAny: opaque mock device surface
|
|
1657
|
-
function
|
|
1289
|
+
function _makeMockDevice(submitProbe?: { count?: number; formats?: string[] }): any {
|
|
1658
1290
|
const mockOpaque = { __mock: 'opaque' };
|
|
1659
1291
|
const makePass = () => ({
|
|
1660
1292
|
setPipeline: () => {},
|
|
@@ -1695,17 +1327,17 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
1695
1327
|
}
|
|
1696
1328
|
|
|
1697
1329
|
// biome-ignore lint/suspicious/noExplicitAny: shader factory shim
|
|
1698
|
-
const
|
|
1330
|
+
const _shaderFactory = async (_d: any, desc: { code: string; label?: string }) =>
|
|
1699
1331
|
rhiOk({ __mock: 'shader', label: desc.label ?? '' }) as never;
|
|
1700
1332
|
|
|
1701
|
-
function
|
|
1333
|
+
function _makeRegisterCube(): (
|
|
1702
1334
|
pod: EquirectAsset,
|
|
1703
1335
|
) => Result<Handle<'EquirectAsset', 'shared'>, AssetError> {
|
|
1704
1336
|
let next = 2000;
|
|
1705
1337
|
return () => rhiOk(toShared<'EquirectAsset'>(next++));
|
|
1706
1338
|
}
|
|
1707
1339
|
|
|
1708
|
-
function
|
|
1340
|
+
function _makeEquirectSource(): EquirectAsset {
|
|
1709
1341
|
return {
|
|
1710
1342
|
kind: 'equirect',
|
|
1711
1343
|
width: 4,
|
|
@@ -1715,144 +1347,6 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
1715
1347
|
colorSpace: 'linear',
|
|
1716
1348
|
};
|
|
1717
1349
|
}
|
|
1718
|
-
|
|
1719
|
-
describe('equirect-to-cubemap projection caps guard (M2)', () => {
|
|
1720
|
-
it('1. caps=false selects a renderable rgba8 precompute output', async () => {
|
|
1721
|
-
const formatProbe: { formats: string[] } = { formats: [] };
|
|
1722
|
-
const store = new GpuResourceStore();
|
|
1723
|
-
store.configureGpuDevice(
|
|
1724
|
-
makeMockDevice(formatProbe),
|
|
1725
|
-
shaderFactory,
|
|
1726
|
-
makeRegisterCube(),
|
|
1727
|
-
capsCubemapDisabled,
|
|
1728
|
-
);
|
|
1729
|
-
|
|
1730
|
-
const srcHandle = toShared<'EquirectAsset'>(4096);
|
|
1731
|
-
// biome-ignore lint/suspicious/noExplicitAny: package-internal projection reached via store cast
|
|
1732
|
-
const result = await (store as any)._uploadCubemapFromEquirect(
|
|
1733
|
-
new World(),
|
|
1734
|
-
srcHandle,
|
|
1735
|
-
makeEquirectSource(),
|
|
1736
|
-
);
|
|
1737
|
-
|
|
1738
|
-
expect(result.ok).toBe(true);
|
|
1739
|
-
expect(formatProbe.formats).toContain('rgba8unorm');
|
|
1740
|
-
});
|
|
1741
|
-
|
|
1742
|
-
it('2. caps=true goes through the normal path (derives cubemap data)', async () => {
|
|
1743
|
-
const store = new GpuResourceStore();
|
|
1744
|
-
store.configureGpuDevice(makeMockDevice(), shaderFactory, makeRegisterCube(), capsTrue);
|
|
1745
|
-
|
|
1746
|
-
const srcHandle = toShared<'EquirectAsset'>(4096);
|
|
1747
|
-
// biome-ignore lint/suspicious/noExplicitAny: package-internal projection reached via store cast
|
|
1748
|
-
const result = await (store as any)._uploadCubemapFromEquirect(
|
|
1749
|
-
new World(),
|
|
1750
|
-
srcHandle,
|
|
1751
|
-
makeEquirectSource(),
|
|
1752
|
-
);
|
|
1753
|
-
|
|
1754
|
-
// With caps=true the path should reach the IBL precompute and succeed.
|
|
1755
|
-
expect(result.ok).toBe(true);
|
|
1756
|
-
});
|
|
1757
|
-
|
|
1758
|
-
it('3. idempotent map wins over caps guard: previously uploaded cube returns ok even when caps is later false', async () => {
|
|
1759
|
-
const store = new GpuResourceStore();
|
|
1760
|
-
store.configureGpuDevice(makeMockDevice(), shaderFactory, makeRegisterCube(), capsTrue);
|
|
1761
|
-
|
|
1762
|
-
const srcHandle = toShared<'EquirectAsset'>(4096);
|
|
1763
|
-
// biome-ignore lint/suspicious/noExplicitAny: package-internal projection reached via store cast
|
|
1764
|
-
const first = await (store as any)._uploadCubemapFromEquirect(
|
|
1765
|
-
new World(),
|
|
1766
|
-
srcHandle,
|
|
1767
|
-
makeEquirectSource(),
|
|
1768
|
-
);
|
|
1769
|
-
expect(first.ok).toBe(true);
|
|
1770
|
-
|
|
1771
|
-
// Simulate caps becoming false (not a real use case; the store's caps
|
|
1772
|
-
// field is injected once at configureGpuDevice -- but the idempotent map
|
|
1773
|
-
// check comes first, so we test the ordering directly by constructing a
|
|
1774
|
-
// second store that starts with caps=false but WITH the idempotent map
|
|
1775
|
-
// pre-populated -- and this is impossible via the public API surface.
|
|
1776
|
-
// Reality: idempotent map means the same store, same caps. The guard
|
|
1777
|
-
// ordering means: if caps IS false from the start, the idempotent map
|
|
1778
|
-
// is empty so the cap guard fires before any IBL work. The ordering
|
|
1779
|
-
// invariant is already proven by test 1.)
|
|
1780
|
-
//
|
|
1781
|
-
// Instead: verify that with caps=true, a second call hits the idempotent
|
|
1782
|
-
// map and returns the same handle without extra IBL submits.
|
|
1783
|
-
const submitProbe = { count: 0 };
|
|
1784
|
-
const store2 = new GpuResourceStore();
|
|
1785
|
-
store2.configureGpuDevice(
|
|
1786
|
-
makeMockDevice(submitProbe),
|
|
1787
|
-
shaderFactory,
|
|
1788
|
-
makeRegisterCube(),
|
|
1789
|
-
capsTrue,
|
|
1790
|
-
);
|
|
1791
|
-
|
|
1792
|
-
// biome-ignore lint/suspicious/noExplicitAny: package-internal projection reached via store cast
|
|
1793
|
-
const first2 = await (store2 as any)._uploadCubemapFromEquirect(
|
|
1794
|
-
new World(),
|
|
1795
|
-
srcHandle,
|
|
1796
|
-
makeEquirectSource(),
|
|
1797
|
-
);
|
|
1798
|
-
const submitsAfterFirst = submitProbe.count;
|
|
1799
|
-
expect(submitsAfterFirst).toBeGreaterThanOrEqual(1);
|
|
1800
|
-
|
|
1801
|
-
// biome-ignore lint/suspicious/noExplicitAny: package-internal projection reached via store cast
|
|
1802
|
-
const second2 = await (store2 as any)._uploadCubemapFromEquirect(
|
|
1803
|
-
new World(),
|
|
1804
|
-
srcHandle,
|
|
1805
|
-
makeEquirectSource(),
|
|
1806
|
-
);
|
|
1807
|
-
// Second call hits the idempotent map, no new submits.
|
|
1808
|
-
expect(submitProbe.count).toBe(submitsAfterFirst);
|
|
1809
|
-
|
|
1810
|
-
expect(first2.ok && second2.ok).toBe(true);
|
|
1811
|
-
if (first2.ok && second2.ok) {
|
|
1812
|
-
expect(JSON.stringify(first2.value)).toBe(JSON.stringify(second2.value));
|
|
1813
|
-
}
|
|
1814
|
-
});
|
|
1815
|
-
|
|
1816
|
-
it('4. registerCube guard fires before caps guard: missing registerCube returns asset-not-found', async () => {
|
|
1817
|
-
// configureGpuDevice is never called, so both registerCube AND caps are undefined.
|
|
1818
|
-
// The registerCube guard at line ~555 fires before the caps guard at ~565.
|
|
1819
|
-
// But we need registerCube undefined while caps is set.
|
|
1820
|
-
// The public API (configureGpuDevice) wires both together, so to test
|
|
1821
|
-
// ordering we rely on the source-code order: registerCube check is at
|
|
1822
|
-
// a lower line number than the caps guard. The ordering is a code-structure
|
|
1823
|
-
// invariant, not a runtime one we can test with a single store instance
|
|
1824
|
-
// where both are wired by the same configureGpuDevice call.
|
|
1825
|
-
//
|
|
1826
|
-
// Instead: test that when configureGpuDevice is called without registerCube
|
|
1827
|
-
// (impossible via the current public signature — registerCube is required),
|
|
1828
|
-
// the registerCube guard would fire first.
|
|
1829
|
-
//
|
|
1830
|
-
// Reality check: the current configureGpuDevice signature makes registerCube
|
|
1831
|
-
// a required parameter, so this ordering is purely structural.
|
|
1832
|
-
// We verify that when the store is unconfigured (no device, no registerCube),
|
|
1833
|
-
// uploadCubemapFromEquirect returns asset-not-found (registerCube guard
|
|
1834
|
-
// at line ~555) and NOT feature-not-enabled (caps guard at ~565).
|
|
1835
|
-
const store = new GpuResourceStore();
|
|
1836
|
-
// Store is never configured — both registerCube and caps are undefined.
|
|
1837
|
-
const srcHandle = toShared<'EquirectAsset'>(4096);
|
|
1838
|
-
// biome-ignore lint/suspicious/noExplicitAny: package-internal projection reached via store cast
|
|
1839
|
-
const result = await (store as any)._uploadCubemapFromEquirect(
|
|
1840
|
-
new World(),
|
|
1841
|
-
srcHandle,
|
|
1842
|
-
makeEquirectSource(),
|
|
1843
|
-
);
|
|
1844
|
-
|
|
1845
|
-
expect(result.ok).toBe(false);
|
|
1846
|
-
if (!result.ok) {
|
|
1847
|
-
// The registerCube guard is at a lower line number than the caps guard
|
|
1848
|
-
// in the source, so it fires first. The caps guard only fires when
|
|
1849
|
-
// `this.caps !== undefined` (precondition), which is true only after
|
|
1850
|
-
// configureGpuDevice is called. When the store is unconfigured,
|
|
1851
|
-
// both are undefined and registerCube guard wins.
|
|
1852
|
-
expect(result.error.code).toBe('asset-not-found');
|
|
1853
|
-
}
|
|
1854
|
-
});
|
|
1855
|
-
});
|
|
1856
1350
|
}
|
|
1857
1351
|
|
|
1858
1352
|
{
|
|
@@ -2171,48 +1665,6 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
2171
1665
|
|
|
2172
1666
|
// ─── w1: mask + frontFace pipeline descriptor pass-through (TDD red phase) ─
|
|
2173
1667
|
|
|
2174
|
-
it('(j) renderState with stencilReadMask=0x00 + stencilWriteMask=0x00 lands in GPUDepthStencilState top-level (not StencilFaceState)', () => {
|
|
2175
|
-
const mocks = makeMocks();
|
|
2176
|
-
const ctx = makeMockContext(mocks);
|
|
2177
|
-
const entry = makeMockEntry();
|
|
2178
|
-
// Future fields not yet on MaterialRenderState; cast for red-phase test.
|
|
2179
|
-
const renderState = {
|
|
2180
|
-
stencilReadMask: 0x00,
|
|
2181
|
-
stencilWriteMask: 0x00,
|
|
2182
|
-
} as MaterialRenderState;
|
|
2183
|
-
|
|
2184
|
-
const result = buildPipelineForMaterialShader(
|
|
2185
|
-
'my-game::stencil-mask-red',
|
|
2186
|
-
entry,
|
|
2187
|
-
ctx,
|
|
2188
|
-
renderState,
|
|
2189
|
-
);
|
|
2190
|
-
|
|
2191
|
-
expect(result.ok).toBe(true);
|
|
2192
|
-
const desc = mocks.createRenderPipeline.mock.calls[0]?.[0] as {
|
|
2193
|
-
depthStencil: {
|
|
2194
|
-
stencilReadMask?: number;
|
|
2195
|
-
stencilWriteMask?: number;
|
|
2196
|
-
stencilFront?: unknown;
|
|
2197
|
-
stencilBack?: unknown;
|
|
2198
|
-
};
|
|
2199
|
-
};
|
|
2200
|
-
// Mask fields MUST be at GPUDepthStencilState top level.
|
|
2201
|
-
expect(desc.depthStencil.stencilReadMask).toBe(0x00);
|
|
2202
|
-
expect(desc.depthStencil.stencilWriteMask).toBe(0x00);
|
|
2203
|
-
// Mask fields MUST NOT be inside StencilFaceState (AC-01 constraint).
|
|
2204
|
-
if (desc.depthStencil.stencilFront !== undefined) {
|
|
2205
|
-
const front = desc.depthStencil.stencilFront as Record<string, unknown>;
|
|
2206
|
-
expect(front.stencilReadMask).toBeUndefined();
|
|
2207
|
-
expect(front.stencilWriteMask).toBeUndefined();
|
|
2208
|
-
}
|
|
2209
|
-
if (desc.depthStencil.stencilBack !== undefined) {
|
|
2210
|
-
const back = desc.depthStencil.stencilBack as Record<string, unknown>;
|
|
2211
|
-
expect(back.stencilReadMask).toBeUndefined();
|
|
2212
|
-
expect(back.stencilWriteMask).toBeUndefined();
|
|
2213
|
-
}
|
|
2214
|
-
});
|
|
2215
|
-
|
|
2216
1668
|
it('(k) renderState with frontFace="cw" lands in GPUPrimitiveState.frontFace', () => {
|
|
2217
1669
|
const mocks = makeMocks();
|
|
2218
1670
|
const ctx = makeMockContext(mocks);
|
|
@@ -2349,12 +1801,12 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
2349
1801
|
|
|
2350
1802
|
expect(
|
|
2351
1803
|
key.startsWith(
|
|
2352
|
-
'forgeax::default-pbr:forward:~:
|
|
1804
|
+
'forgeax::default-pbr:forward:~:bgra8unorm-srgb:depth24plus-stencil8:1:triangle-list',
|
|
2353
1805
|
),
|
|
2354
1806
|
).toBe(true);
|
|
2355
1807
|
// vl:<digest> segment present (vertex layout hash); may have trailing : from
|
|
2356
1808
|
// renderStateHash('') at end of join
|
|
2357
|
-
expect(key).toMatch(/:vl:
|
|
1809
|
+
expect(key).toMatch(/:vl:[^:]+/);
|
|
2358
1810
|
});
|
|
2359
1811
|
|
|
2360
1812
|
it('(c) AC-06: same tuple -> identical key (idempotent, cache hit)', () => {
|
|
@@ -2753,70 +2205,6 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
2753
2205
|
});
|
|
2754
2206
|
}
|
|
2755
2207
|
|
|
2756
|
-
{
|
|
2757
|
-
// --- from pipeline-rename-grep-gate.test.ts ---
|
|
2758
|
-
const EXPECTED_BASELINE_HITS = [
|
|
2759
|
-
// Source files to be renamed (9 files)
|
|
2760
|
-
'packages/runtime/src/urp-pipeline.ts',
|
|
2761
|
-
'packages/runtime/src/index.ts',
|
|
2762
|
-
'packages/runtime/src/render-system-record.ts',
|
|
2763
|
-
'packages/runtime/src/render-pipeline.ts',
|
|
2764
|
-
'packages/runtime/src/__tests__/pipeline-errors.test.ts',
|
|
2765
|
-
'packages/runtime/src/systems/__tests__/graph-skybox.test.ts',
|
|
2766
|
-
'packages/runtime/README.md',
|
|
2767
|
-
'packages/types/src/index.ts',
|
|
2768
|
-
// Historical spec — allowed in AC-25 allow-list after w2 adds a migration footnote
|
|
2769
|
-
'.forgeax-harness/docs/specs/2026-06-01-customizable-render-pipeline-design.md',
|
|
2770
|
-
];
|
|
2771
|
-
|
|
2772
|
-
describe('pipeline rename grep gate baseline (AC-25 red-phase)', () => {
|
|
2773
|
-
it('documents the 10-file baseline hit set per research Finding 5', () => {
|
|
2774
|
-
// This test acts as a human-readable contract: the files listed above
|
|
2775
|
-
// are the known targets for the w2 rename. The actual grep is run via
|
|
2776
|
-
// command-line and checked in w5 (grep gate enumeration) + finalize.
|
|
2777
|
-
//
|
|
2778
|
-
// The 9 source files (packages/ and types/) must all be renamed.
|
|
2779
|
-
// The 1 historical spec (.forgeax-harness/docs/specs/) stays with a migration footnote.
|
|
2780
|
-
expect(EXPECTED_BASELINE_HITS.length).toBe(9);
|
|
2781
|
-
|
|
2782
|
-
// Verify the structural groupings are correct
|
|
2783
|
-
const sourceFiles = EXPECTED_BASELINE_HITS.filter(
|
|
2784
|
-
(f) => f.startsWith('packages/') || f.startsWith('apps/'),
|
|
2785
|
-
);
|
|
2786
|
-
const docFiles = EXPECTED_BASELINE_HITS.filter((f) => f.startsWith('.forgeax-harness/docs/'));
|
|
2787
|
-
|
|
2788
|
-
// 8 source files to be fully renamed
|
|
2789
|
-
expect(sourceFiles.length).toBe(8);
|
|
2790
|
-
// 1 historical spec to receive migration footnote
|
|
2791
|
-
expect(docFiles.length).toBe(1);
|
|
2792
|
-
|
|
2793
|
-
// The one doc file is the customizable-render-pipeline design spec
|
|
2794
|
-
expect(docFiles).toContain(
|
|
2795
|
-
'.forgeax-harness/docs/specs/2026-06-01-customizable-render-pipeline-design.md',
|
|
2796
|
-
);
|
|
2797
|
-
});
|
|
2798
|
-
|
|
2799
|
-
it('baseline: urp-pipeline.ts is the rename SSOT site', () => {
|
|
2800
|
-
// The primary rename site is the constants file — it defines the
|
|
2801
|
-
// URP_PIPELINE_ID that all other files reference.
|
|
2802
|
-
expect(EXPECTED_BASELINE_HITS).toContain('packages/runtime/src/urp-pipeline.ts');
|
|
2803
|
-
});
|
|
2804
|
-
|
|
2805
|
-
it('baseline: pipeline-errors test fixture references the old pipelineId', () => {
|
|
2806
|
-
// The test fixture in pipeline-errors.test.ts uses the old string literal
|
|
2807
|
-
// in its test assertions — must be updated for new 'forgeax::urp' literal.
|
|
2808
|
-
expect(EXPECTED_BASELINE_HITS).toContain(
|
|
2809
|
-
'packages/runtime/src/__tests__/pipeline-errors.test.ts',
|
|
2810
|
-
);
|
|
2811
|
-
});
|
|
2812
|
-
|
|
2813
|
-
it('baseline: no apps/* files hit — rename is engine-only for M1', () => {
|
|
2814
|
-
const appFiles = EXPECTED_BASELINE_HITS.filter((f) => f.startsWith('apps/'));
|
|
2815
|
-
expect(appFiles.length).toBe(0);
|
|
2816
|
-
});
|
|
2817
|
-
});
|
|
2818
|
-
}
|
|
2819
|
-
|
|
2820
2208
|
{
|
|
2821
2209
|
// --- from pipeline-vertex-stride-branch.test.ts ---
|
|
2822
2210
|
describe('vertex stride is uniformly 12 floats per vertex (bug-20260519)', () => {
|
|
@@ -2826,10 +2214,8 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
2826
2214
|
|
|
2827
2215
|
it('BUILTIN_CUBE and BUILTIN_TRIANGLE expose 12 floats per vertex (boundary)', () => {
|
|
2828
2216
|
const world = new World();
|
|
2829
|
-
const cube =
|
|
2830
|
-
const tri =
|
|
2831
|
-
internPrimitiveMesh(world, 'triangle'),
|
|
2832
|
-
);
|
|
2217
|
+
const cube = resolveAssetHandle<MeshAsset>(world, HANDLE_CUBE);
|
|
2218
|
+
const tri = resolveAssetHandle<MeshAsset>(world, HANDLE_TRIANGLE);
|
|
2833
2219
|
expect(cube.ok).toBe(true);
|
|
2834
2220
|
expect(tri.ok).toBe(true);
|
|
2835
2221
|
if (!cube.ok || !tri.ok) return;
|
|
@@ -2863,8 +2249,8 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
2863
2249
|
if (!proceduralRes.ok) return;
|
|
2864
2250
|
const handle = world.allocSharedRef('MeshAsset', proceduralRes.value);
|
|
2865
2251
|
|
|
2866
|
-
const cube =
|
|
2867
|
-
const proc =
|
|
2252
|
+
const cube = resolveAssetHandle<MeshAsset>(world, HANDLE_CUBE);
|
|
2253
|
+
const proc = resolveAssetHandle<MeshAsset>(world, handle);
|
|
2868
2254
|
expect(cube.ok).toBe(true);
|
|
2869
2255
|
expect(proc.ok).toBe(true);
|
|
2870
2256
|
if (!cube.ok || !proc.ok) return;
|
|
@@ -2971,7 +2357,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
2971
2357
|
|
|
2972
2358
|
{
|
|
2973
2359
|
// --- from record-all-topology.test.ts ---
|
|
2974
|
-
const ENGINE = new URL('../../../render/src/
|
|
2360
|
+
const ENGINE = new URL('../../../render/src/assembly/factory.ts', import.meta.url).href;
|
|
2975
2361
|
|
|
2976
2362
|
interface PassSpies {
|
|
2977
2363
|
setIndexBuffer: ReturnType<typeof vi.fn>;
|
|
@@ -3020,8 +2406,8 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
3020
2406
|
identifier,
|
|
3021
2407
|
sourcePath: `${identifier}.wgsl`,
|
|
3022
2408
|
composedWgsl: '/* stub */',
|
|
3023
|
-
paramSchema:
|
|
3024
|
-
variants:
|
|
2409
|
+
paramSchema: '[]',
|
|
2410
|
+
variants: [],
|
|
3025
2411
|
});
|
|
3026
2412
|
const manifest = {
|
|
3027
2413
|
schemaVersion: '1.0.0',
|
|
@@ -3103,15 +2489,6 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
3103
2489
|
// Vertex-only topologies omit indices (point-list / line-list); strip and
|
|
3104
2490
|
// triangle-list topologies carry indices (line-strip / triangle-list /
|
|
3105
2491
|
// triangle-strip). 3 vertices x 12 floats covers every case.
|
|
3106
|
-
function canonicalAttributes(vertexCount: number): MeshAsset['attributes'] {
|
|
3107
|
-
return {
|
|
3108
|
-
position: new Float32Array(vertexCount * 3),
|
|
3109
|
-
normal: new Float32Array(vertexCount * 3),
|
|
3110
|
-
uv: new Float32Array(vertexCount * 2),
|
|
3111
|
-
tangent: new Float32Array(vertexCount * 4),
|
|
3112
|
-
};
|
|
3113
|
-
}
|
|
3114
|
-
|
|
3115
2492
|
function meshForTopology(topology: PrimitiveTopology): MeshAsset {
|
|
3116
2493
|
const vertices = new Float32Array(3 * 12);
|
|
3117
2494
|
const indexed =
|
|
@@ -3121,7 +2498,8 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
3121
2498
|
kind: 'mesh',
|
|
3122
2499
|
vertices,
|
|
3123
2500
|
indices: new Uint16Array([0, 1, 2]),
|
|
3124
|
-
attributes:
|
|
2501
|
+
attributes: buildMeshAttributeMapForUvSets(1),
|
|
2502
|
+
aabb: new Float32Array([-1, -1, -1, 1, 1, 1]),
|
|
3125
2503
|
materialSlots: [{ slotName: 'Default' }],
|
|
3126
2504
|
submeshes: [
|
|
3127
2505
|
{
|
|
@@ -3137,7 +2515,8 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
3137
2515
|
return {
|
|
3138
2516
|
kind: 'mesh',
|
|
3139
2517
|
vertices,
|
|
3140
|
-
attributes:
|
|
2518
|
+
attributes: buildMeshAttributeMapForUvSets(1),
|
|
2519
|
+
aabb: new Float32Array([-1, -1, -1, 1, 1, 1]),
|
|
3141
2520
|
materialSlots: [{ slotName: 'Default' }],
|
|
3142
2521
|
submeshes: [
|
|
3143
2522
|
{
|
|
@@ -3167,15 +2546,6 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
3167
2546
|
const C = await importComponents();
|
|
3168
2547
|
const { registerPropagateTransforms } = await import('@forgeax/engine-scene');
|
|
3169
2548
|
const world = new World();
|
|
3170
|
-
for (const component of [
|
|
3171
|
-
C.Camera,
|
|
3172
|
-
C.DirectionalLight,
|
|
3173
|
-
C.Transform,
|
|
3174
|
-
C.MeshRenderer,
|
|
3175
|
-
C.MeshFilter,
|
|
3176
|
-
]) {
|
|
3177
|
-
world.components.register(component as never).unwrap();
|
|
3178
|
-
}
|
|
3179
2549
|
registerPropagateTransforms(world);
|
|
3180
2550
|
const meshHandle = world.allocSharedRef('MeshAsset', meshAsset) as Handle<
|
|
3181
2551
|
'MeshAsset',
|
|
@@ -3200,7 +2570,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
3200
2570
|
{ component: C.Transform, data: cameraTransform() },
|
|
3201
2571
|
);
|
|
3202
2572
|
world.spawn(
|
|
3203
|
-
{ component: C.DirectionalLight, data: {
|
|
2573
|
+
{ component: C.DirectionalLight, data: {} },
|
|
3204
2574
|
{ component: C.Transform, data: cameraTransform() },
|
|
3205
2575
|
);
|
|
3206
2576
|
world
|
|
@@ -3239,11 +2609,17 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
3239
2609
|
renderer.onError((e) => errors.push(e.code));
|
|
3240
2610
|
|
|
3241
2611
|
const world = await spawnScene(renderer, meshForTopology(topology));
|
|
3242
|
-
if (!(renderer as unknown as RendererType).
|
|
2612
|
+
if (!(renderer as unknown as RendererType).attach(world as WorldType).ok) {
|
|
3243
2613
|
throw new Error('World attachment failed');
|
|
3244
2614
|
}
|
|
3245
2615
|
(world as WorldType).update().unwrap();
|
|
3246
2616
|
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
2617
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
2618
|
+
spies.draw.mockClear();
|
|
2619
|
+
spies.drawIndexed.mockClear();
|
|
2620
|
+
spies.setIndexBuffer.mockClear();
|
|
2621
|
+
spies.setVertexBuffer.mockClear();
|
|
2622
|
+
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
3247
2623
|
|
|
3248
2624
|
// Some dispatch verb must have fired for this topology's mesh.
|
|
3249
2625
|
const dispatched = spies.draw.mock.calls.length + spies.drawIndexed.mock.calls.length;
|
|
@@ -3271,7 +2647,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
3271
2647
|
|
|
3272
2648
|
{
|
|
3273
2649
|
// --- from record-draw-branch.test.ts ---
|
|
3274
|
-
const ENGINE = new URL('../../../render/src/
|
|
2650
|
+
const ENGINE = new URL('../../../render/src/assembly/factory.ts', import.meta.url).href;
|
|
3275
2651
|
|
|
3276
2652
|
interface PassSpies {
|
|
3277
2653
|
setIndexBuffer: ReturnType<typeof vi.fn>;
|
|
@@ -3320,8 +2696,8 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
3320
2696
|
identifier,
|
|
3321
2697
|
sourcePath: `${identifier}.wgsl`,
|
|
3322
2698
|
composedWgsl: '/* stub */',
|
|
3323
|
-
paramSchema:
|
|
3324
|
-
variants:
|
|
2699
|
+
paramSchema: '[]',
|
|
2700
|
+
variants: [],
|
|
3325
2701
|
});
|
|
3326
2702
|
const manifest = {
|
|
3327
2703
|
schemaVersion: '1.0.0',
|
|
@@ -3400,28 +2776,20 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
3400
2776
|
};
|
|
3401
2777
|
}
|
|
3402
2778
|
|
|
3403
|
-
function canonicalAttributes(vertexCount: number): MeshAsset['attributes'] {
|
|
3404
|
-
return {
|
|
3405
|
-
position: new Float32Array(vertexCount * 3),
|
|
3406
|
-
normal: new Float32Array(vertexCount * 3),
|
|
3407
|
-
uv: new Float32Array(vertexCount * 2),
|
|
3408
|
-
tangent: new Float32Array(vertexCount * 4),
|
|
3409
|
-
};
|
|
3410
|
-
}
|
|
3411
|
-
|
|
3412
2779
|
function indexedTriangleMesh(): MeshAsset {
|
|
3413
2780
|
// 3 vertices x 12 floats = 1 triangle, indexed.
|
|
3414
2781
|
return {
|
|
3415
2782
|
kind: 'mesh',
|
|
3416
2783
|
vertices: new Float32Array(3 * 12),
|
|
3417
2784
|
indices: new Uint16Array([0, 1, 2]),
|
|
3418
|
-
attributes:
|
|
2785
|
+
attributes: buildMeshAttributeMapForUvSets(1),
|
|
2786
|
+
aabb: new Float32Array([-1, -1, -1, 1, 1, 1]),
|
|
3419
2787
|
materialSlots: [{ slotName: 'Default' }],
|
|
3420
2788
|
submeshes: [
|
|
3421
2789
|
{
|
|
3422
2790
|
indexOffset: 0,
|
|
3423
2791
|
indexCount: 3,
|
|
3424
|
-
vertexCount:
|
|
2792
|
+
vertexCount: 3,
|
|
3425
2793
|
materialSlot: 0,
|
|
3426
2794
|
topology: 'triangle-list',
|
|
3427
2795
|
},
|
|
@@ -3434,7 +2802,8 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
3434
2802
|
return {
|
|
3435
2803
|
kind: 'mesh',
|
|
3436
2804
|
vertices: new Float32Array(2 * 12),
|
|
3437
|
-
attributes:
|
|
2805
|
+
attributes: buildMeshAttributeMapForUvSets(1),
|
|
2806
|
+
aabb: new Float32Array([-1, -1, -1, 1, 1, 1]),
|
|
3438
2807
|
materialSlots: [{ slotName: 'Default' }],
|
|
3439
2808
|
submeshes: [
|
|
3440
2809
|
{
|
|
@@ -3464,15 +2833,6 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
3464
2833
|
const C = await importComponents();
|
|
3465
2834
|
const { registerPropagateTransforms } = await import('@forgeax/engine-scene');
|
|
3466
2835
|
const world = new World();
|
|
3467
|
-
for (const component of [
|
|
3468
|
-
C.Camera,
|
|
3469
|
-
C.DirectionalLight,
|
|
3470
|
-
C.Transform,
|
|
3471
|
-
C.MeshRenderer,
|
|
3472
|
-
C.MeshFilter,
|
|
3473
|
-
]) {
|
|
3474
|
-
world.components.register(component as never).unwrap();
|
|
3475
|
-
}
|
|
3476
2836
|
registerPropagateTransforms(world);
|
|
3477
2837
|
const meshHandle = world.allocSharedRef('MeshAsset', meshAsset) as Handle<
|
|
3478
2838
|
'MeshAsset',
|
|
@@ -3497,7 +2857,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
3497
2857
|
{ component: C.Transform, data: cameraTransform() },
|
|
3498
2858
|
);
|
|
3499
2859
|
world.spawn(
|
|
3500
|
-
{ component: C.DirectionalLight, data: {
|
|
2860
|
+
{ component: C.DirectionalLight, data: {} },
|
|
3501
2861
|
{ component: C.Transform, data: cameraTransform() },
|
|
3502
2862
|
);
|
|
3503
2863
|
world
|
|
@@ -3527,11 +2887,17 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
3527
2887
|
renderer.onError((e) => errors.push(e.code));
|
|
3528
2888
|
|
|
3529
2889
|
const world = await spawnScene(renderer, vertexOnlyLineMesh());
|
|
3530
|
-
if (!(renderer as unknown as RendererType).
|
|
2890
|
+
if (!(renderer as unknown as RendererType).attach(world as WorldType).ok) {
|
|
3531
2891
|
throw new Error('World attachment failed');
|
|
3532
2892
|
}
|
|
3533
2893
|
(world as WorldType).update().unwrap();
|
|
3534
2894
|
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
2895
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
2896
|
+
spies.draw.mockClear();
|
|
2897
|
+
spies.drawIndexed.mockClear();
|
|
2898
|
+
spies.setIndexBuffer.mockClear();
|
|
2899
|
+
spies.setVertexBuffer.mockClear();
|
|
2900
|
+
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
3535
2901
|
|
|
3536
2902
|
// The vertex-only mesh draws via the non-indexed path.
|
|
3537
2903
|
expect(spies.draw).toHaveBeenCalled();
|
|
@@ -3553,11 +2919,17 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
3553
2919
|
renderer.onError((e) => errors.push(e.code));
|
|
3554
2920
|
|
|
3555
2921
|
const world = await spawnScene(renderer, indexedTriangleMesh());
|
|
3556
|
-
if (!(renderer as unknown as RendererType).
|
|
2922
|
+
if (!(renderer as unknown as RendererType).attach(world as WorldType).ok) {
|
|
3557
2923
|
throw new Error('World attachment failed');
|
|
3558
2924
|
}
|
|
3559
2925
|
(world as WorldType).update().unwrap();
|
|
3560
2926
|
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
2927
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
2928
|
+
spies.draw.mockClear();
|
|
2929
|
+
spies.drawIndexed.mockClear();
|
|
2930
|
+
spies.setIndexBuffer.mockClear();
|
|
2931
|
+
spies.setVertexBuffer.mockClear();
|
|
2932
|
+
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
3561
2933
|
|
|
3562
2934
|
expect(spies.setIndexBuffer).toHaveBeenCalled();
|
|
3563
2935
|
expect(spies.drawIndexed).toHaveBeenCalled();
|
|
@@ -3656,9 +3028,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
3656
3028
|
// `~` (was empty `::`).
|
|
3657
3029
|
const withIdx = cacheKeyOf(mkSpec(ID, false, undefined, 'triangle-list', 'uint32'));
|
|
3658
3030
|
expect(
|
|
3659
|
-
withIdx.startsWith(
|
|
3660
|
-
`${ID}:forward:~:fs_main:bgra8unorm-srgb:depth24plus-stencil8:1:triangle-list`,
|
|
3661
|
-
),
|
|
3031
|
+
withIdx.startsWith(`${ID}:forward:~:bgra8unorm-srgb:depth24plus-stencil8:1:triangle-list`),
|
|
3662
3032
|
).toBe(true);
|
|
3663
3033
|
expect(withIdx.includes(':uint32')).toBe(false);
|
|
3664
3034
|
});
|
|
@@ -3706,7 +3076,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
3706
3076
|
component: Transform,
|
|
3707
3077
|
data: { pos: [1, 0, 0], quat: [0, 0, 0, 1], scale: [1, 1, 1] },
|
|
3708
3078
|
},
|
|
3709
|
-
{ component: MeshFilter, data: { assetHandle:
|
|
3079
|
+
{ component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
|
|
3710
3080
|
{ component: MeshRenderer, data: { materials: [MATERIAL_SENTINEL] } },
|
|
3711
3081
|
)
|
|
3712
3082
|
.unwrap();
|
|
@@ -3734,7 +3104,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
3734
3104
|
component: Transform,
|
|
3735
3105
|
data: { pos: [1, 0, 0], quat: [0, 0, 0, 1], scale: [1, 1, 1] },
|
|
3736
3106
|
},
|
|
3737
|
-
{ component: MeshFilter, data: { assetHandle:
|
|
3107
|
+
{ component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
|
|
3738
3108
|
{ component: MeshRenderer, data: { materials: [MATERIAL_SENTINEL] } },
|
|
3739
3109
|
)
|
|
3740
3110
|
.unwrap();
|
|
@@ -3771,7 +3141,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
3771
3141
|
component: Transform,
|
|
3772
3142
|
data: { pos: [1, 0, 0], quat: [0, 0, 0, 1], scale: [1, 1, 1] },
|
|
3773
3143
|
},
|
|
3774
|
-
{ component: MeshFilter, data: { assetHandle:
|
|
3144
|
+
{ component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
|
|
3775
3145
|
{ component: MeshRenderer, data: { materials: [MATERIAL_SENTINEL] } },
|
|
3776
3146
|
{ component: ChildOf, data: { parent } },
|
|
3777
3147
|
)
|
|
@@ -3808,7 +3178,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
3808
3178
|
component: Transform,
|
|
3809
3179
|
data: { pos: [n, 0, 0], quat: [0, 0, 0, 1], scale: [1, 1, 1] },
|
|
3810
3180
|
},
|
|
3811
|
-
{ component: MeshFilter, data: { assetHandle:
|
|
3181
|
+
{ component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
|
|
3812
3182
|
{ component: MeshRenderer, data: { materials: [MATERIAL_SENTINEL] } },
|
|
3813
3183
|
{ component: ChildOf, data: { parent } },
|
|
3814
3184
|
)
|
|
@@ -3840,7 +3210,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
3840
3210
|
component: Transform,
|
|
3841
3211
|
data: { pos: [0, 0, 0], quat: [0, 0, 0, 1], scale: [1, 1, 1] },
|
|
3842
3212
|
},
|
|
3843
|
-
{ component: MeshFilter, data: { assetHandle:
|
|
3213
|
+
{ component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
|
|
3844
3214
|
{ component: MeshRenderer, data: { materials: [MATERIAL_SENTINEL] } },
|
|
3845
3215
|
)
|
|
3846
3216
|
.unwrap();
|
|
@@ -3871,7 +3241,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
3871
3241
|
component: Transform,
|
|
3872
3242
|
data: { pos: [0, 0, 0], quat: [0, 0, 0, 1], scale: [1, 1, 1] },
|
|
3873
3243
|
},
|
|
3874
|
-
{ component: MeshFilter, data: { assetHandle:
|
|
3244
|
+
{ component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
|
|
3875
3245
|
{ component: MeshRenderer, data: { materials: [MATERIAL_SENTINEL] } },
|
|
3876
3246
|
)
|
|
3877
3247
|
.unwrap();
|
|
@@ -3882,7 +3252,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
3882
3252
|
component: Transform,
|
|
3883
3253
|
data: { pos: [2, 0, 0], quat: [0, 0, 0, 1], scale: [1, 1, 1] },
|
|
3884
3254
|
},
|
|
3885
|
-
{ component: MeshFilter, data: { assetHandle:
|
|
3255
|
+
{ component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
|
|
3886
3256
|
{ component: MeshRenderer, data: { materials: [MATERIAL_SENTINEL] } },
|
|
3887
3257
|
)
|
|
3888
3258
|
.unwrap();
|
|
@@ -3896,9 +3266,8 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
3896
3266
|
|
|
3897
3267
|
{
|
|
3898
3268
|
// --- from renderer-draw-world.test.ts ---
|
|
3899
|
-
const
|
|
3900
|
-
const RENDERER = new URL('../../../render/src/
|
|
3901
|
-
.href;
|
|
3269
|
+
const _ENGINE = new URL('../../../render/src/assembly/factory.ts', import.meta.url).href;
|
|
3270
|
+
const RENDERER = new URL('../../../render/src/assembly/factory.ts', import.meta.url).href;
|
|
3902
3271
|
|
|
3903
3272
|
// ─── Mock helpers ───────────────────────────────────────────────────────────
|
|
3904
3273
|
|
|
@@ -3923,7 +3292,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
3923
3292
|
webgpu?: 'context' | 'null';
|
|
3924
3293
|
}
|
|
3925
3294
|
|
|
3926
|
-
function
|
|
3295
|
+
function _makeMockCanvas(opts: CanvasOptions): HTMLCanvasElement {
|
|
3927
3296
|
const canvas = {
|
|
3928
3297
|
width: 800,
|
|
3929
3298
|
height: 600,
|
|
@@ -3950,7 +3319,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
3950
3319
|
return canvas;
|
|
3951
3320
|
}
|
|
3952
3321
|
|
|
3953
|
-
function
|
|
3322
|
+
function _makeMockGPUDevice(): { device: unknown } {
|
|
3954
3323
|
const lost = new Promise<unknown>(() => undefined);
|
|
3955
3324
|
const device = {
|
|
3956
3325
|
__mockTag: 'gpu-device',
|
|
@@ -3996,7 +3365,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
3996
3365
|
return { device };
|
|
3997
3366
|
}
|
|
3998
3367
|
|
|
3999
|
-
function
|
|
3368
|
+
function _makeMockGPU(deviceObj: unknown): unknown {
|
|
4000
3369
|
return {
|
|
4001
3370
|
requestAdapter: async () => ({
|
|
4002
3371
|
requestDevice: async () => deviceObj,
|
|
@@ -4007,7 +3376,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
4007
3376
|
|
|
4008
3377
|
const baseNavigator = { userAgent: 'mock-engine-test' } as unknown as Navigator;
|
|
4009
3378
|
|
|
4010
|
-
function
|
|
3379
|
+
function _buildManifestDataUrl(): string {
|
|
4011
3380
|
// feat-20260518-pbr-direct-lighting-mvp M5 / w22.9: createRenderer's
|
|
4012
3381
|
// post-fallback path requires both pbr (`f_schlick(` marker) + unlit
|
|
4013
3382
|
// entries; seed two minimal stubs (mock device's createShaderModule does
|
|
@@ -4064,8 +3433,8 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
4064
3433
|
'..',
|
|
4065
3434
|
'render',
|
|
4066
3435
|
'src',
|
|
4067
|
-
'
|
|
4068
|
-
'
|
|
3436
|
+
'assembly',
|
|
3437
|
+
'factory.ts',
|
|
4069
3438
|
);
|
|
4070
3439
|
const text = fs.readFileSync(rendererSrc, 'utf8');
|
|
4071
3440
|
expect(text).not.toMatch(/interface RendererDrawTarget/);
|
|
@@ -4075,101 +3444,6 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
4075
3444
|
const mod = (await import(RENDERER)) as Record<string, unknown>;
|
|
4076
3445
|
expect(mod.RendererDrawTarget).toBeUndefined();
|
|
4077
3446
|
});
|
|
4078
|
-
|
|
4079
|
-
it('Renderer.draw signature accepts a World instance, not a kind marker', async () => {
|
|
4080
|
-
const { device } = makeMockGPUDevice();
|
|
4081
|
-
vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
|
|
4082
|
-
const canvas = makeMockCanvas({ webgl2: 'context', webgpu: 'context' });
|
|
4083
|
-
const { createRenderer } = (await import(ENGINE)) as {
|
|
4084
|
-
createRenderer: (
|
|
4085
|
-
canvas: unknown,
|
|
4086
|
-
opts?: unknown,
|
|
4087
|
-
bundler?: unknown,
|
|
4088
|
-
) => Promise<{
|
|
4089
|
-
draw: (worlds: unknown, opts: { cameraOwner: number; resourceOwner: number }) => void;
|
|
4090
|
-
ready: Promise<void>;
|
|
4091
|
-
}>;
|
|
4092
|
-
};
|
|
4093
|
-
const { World } = (await import('@forgeax/engine-ecs')) as { World: new () => unknown };
|
|
4094
|
-
const renderer = await createRenderer(
|
|
4095
|
-
canvas,
|
|
4096
|
-
{},
|
|
4097
|
-
{ shaderManifestUrl: buildManifestDataUrl() },
|
|
4098
|
-
);
|
|
4099
|
-
await renderer.ready;
|
|
4100
|
-
const world = new World();
|
|
4101
|
-
const typedRenderer = renderer as unknown as RendererType;
|
|
4102
|
-
const typedWorld = world as WorldType;
|
|
4103
|
-
const attachment = typedRenderer.attachWorld(typedWorld);
|
|
4104
|
-
if (!attachment.ok) throw attachment.error;
|
|
4105
|
-
typedWorld.update().unwrap();
|
|
4106
|
-
expect(typedRenderer.draw([typedWorld], { cameraOwner: 0, resourceOwner: 0 }).ok).toBe(true);
|
|
4107
|
-
// Source-level guarantee: createRenderer.ts must use World as the draw
|
|
4108
|
-
// parameter type after the K-4 rewrite (D-S2: RenderSystem walks the
|
|
4109
|
-
// World query graph; charter proposition 5 single ECS-driven entry).
|
|
4110
|
-
const fsId = 'node:fs';
|
|
4111
|
-
const pathId = 'node:path';
|
|
4112
|
-
const urlId = 'node:url';
|
|
4113
|
-
const fs = (await import(/* @vite-ignore */ fsId)) as {
|
|
4114
|
-
readFileSync: (p: string, enc: string) => string;
|
|
4115
|
-
};
|
|
4116
|
-
const path = (await import(/* @vite-ignore */ pathId)) as {
|
|
4117
|
-
resolve: (...parts: string[]) => string;
|
|
4118
|
-
dirname: (p: string) => string;
|
|
4119
|
-
};
|
|
4120
|
-
const url = (await import(/* @vite-ignore */ urlId)) as {
|
|
4121
|
-
fileURLToPath: (u: string) => string;
|
|
4122
|
-
};
|
|
4123
|
-
const here = url.fileURLToPath(import.meta.url);
|
|
4124
|
-
const createRendererSrc = path.resolve(
|
|
4125
|
-
path.dirname(here),
|
|
4126
|
-
'..',
|
|
4127
|
-
'..',
|
|
4128
|
-
'..',
|
|
4129
|
-
'render',
|
|
4130
|
-
'src',
|
|
4131
|
-
'renderer',
|
|
4132
|
-
'renderer-factory.ts',
|
|
4133
|
-
);
|
|
4134
|
-
const text = fs.readFileSync(createRendererSrc, 'utf8');
|
|
4135
|
-
expect(text).toMatch(/draw\s*\(\s*\w+\s*:\s*readonly World\[\]/);
|
|
4136
|
-
expect(text).not.toMatch(/draw\s*\(\s*\w+\s*:\s*RendererDrawTarget\b/);
|
|
4137
|
-
});
|
|
4138
|
-
|
|
4139
|
-
it('fires onError with rhi-not-available when draw(world) is called before ready settles', async () => {
|
|
4140
|
-
const { device } = makeMockGPUDevice();
|
|
4141
|
-
vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
|
|
4142
|
-
const canvas = makeMockCanvas({ webgl2: 'context', webgpu: 'context' });
|
|
4143
|
-
const { createRenderer } = (await import(ENGINE)) as {
|
|
4144
|
-
createRenderer: (
|
|
4145
|
-
canvas: unknown,
|
|
4146
|
-
opts?: unknown,
|
|
4147
|
-
bundler?: unknown,
|
|
4148
|
-
) => Promise<{
|
|
4149
|
-
draw: (worlds: unknown, opts: { cameraOwner: number; resourceOwner: number }) => void;
|
|
4150
|
-
ready: Promise<void>;
|
|
4151
|
-
onError: (cb: (err: { code: string }) => void) => () => void;
|
|
4152
|
-
}>;
|
|
4153
|
-
};
|
|
4154
|
-
const { World } = (await import('@forgeax/engine-ecs')) as { World: new () => unknown };
|
|
4155
|
-
|
|
4156
|
-
const renderer = await createRenderer(
|
|
4157
|
-
canvas,
|
|
4158
|
-
{},
|
|
4159
|
-
{ shaderManifestUrl: buildManifestDataUrl() },
|
|
4160
|
-
);
|
|
4161
|
-
const world = new World();
|
|
4162
|
-
const errors: { code: string }[] = [];
|
|
4163
|
-
renderer.onError((e) => errors.push(e));
|
|
4164
|
-
|
|
4165
|
-
// Call draw before awaiting ready - must fire onError + skip frame.
|
|
4166
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
4167
|
-
throw new Error('World attachment failed');
|
|
4168
|
-
}
|
|
4169
|
-
(world as WorldType).update().unwrap();
|
|
4170
|
-
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
4171
|
-
expect(errors.some((e) => e.code === 'rhi-not-available')).toBe(true);
|
|
4172
|
-
});
|
|
4173
3447
|
});
|
|
4174
3448
|
|
|
4175
3449
|
// bug-20260519 AC-03: when a world *does* carry a `MeshRenderer` entity but
|
|
@@ -4185,132 +3459,11 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
4185
3459
|
// `baseColor` but zero passes — the extract stage
|
|
4186
3460
|
// walks the parent chain (no parent), finds zero passes, and fires
|
|
4187
3461
|
// `material-no-effective-pass` with a recovery hint for adding a pass.
|
|
4188
|
-
describe('Renderer.draw(world) bug-20260519 zero-manifest mesh path (AC-03)', () => {
|
|
4189
|
-
it('MeshRenderer + no shaderManifestUrl -> render-time fires material-no-effective-pass', async () => {
|
|
4190
|
-
const { device } = makeMockGPUDevice();
|
|
4191
|
-
vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
|
|
4192
|
-
const canvas = makeMockCanvas({ webgl2: 'context', webgpu: 'context' });
|
|
4193
|
-
const { createRenderer } = (await import(ENGINE)) as {
|
|
4194
|
-
createRenderer: (
|
|
4195
|
-
canvas: unknown,
|
|
4196
|
-
opts?: unknown,
|
|
4197
|
-
bundler?: unknown,
|
|
4198
|
-
) => Promise<{
|
|
4199
|
-
draw: (worlds: unknown, opts: { cameraOwner: number; resourceOwner: number }) => void;
|
|
4200
|
-
ready: Promise<unknown>;
|
|
4201
|
-
onError: (cb: (err: { code: string; hint?: string }) => void) => () => void;
|
|
4202
|
-
assets: {
|
|
4203
|
-
register: (asset: unknown) => { unwrap: () => unknown };
|
|
4204
|
-
};
|
|
4205
|
-
}>;
|
|
4206
|
-
};
|
|
4207
|
-
const { World } = (await import('@forgeax/engine-ecs')) as { World: new () => unknown };
|
|
4208
|
-
const { registerPropagateTransforms } = await import('@forgeax/engine-scene');
|
|
4209
|
-
const components = {
|
|
4210
|
-
...(await import(new URL('../../../render/src/index.ts', import.meta.url).href)),
|
|
4211
|
-
...(await import('@forgeax/engine-scene')),
|
|
4212
|
-
} as {
|
|
4213
|
-
Transform: unknown;
|
|
4214
|
-
Camera: unknown;
|
|
4215
|
-
MeshFilter: unknown;
|
|
4216
|
-
MeshRenderer: unknown;
|
|
4217
|
-
};
|
|
4218
|
-
// Zero-manifest path: explicit `shaderManifestUrl: undefined` opts into
|
|
4219
|
-
// zero-entry mode. After feat-20260529 D-3, the material's zero-passes
|
|
4220
|
-
// surface is caught at extract (not record):
|
|
4221
|
-
// - `await renderer.ready` resolves Result.ok (Step 2 guard skips
|
|
4222
|
-
// dual createShaderModule because `registry.entries().length === 0`)
|
|
4223
|
-
// - on `renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 })` the per-entity loop in
|
|
4224
|
-
// `render-system-extract.ts` walks the material parent chain (none),
|
|
4225
|
-
// finds zero passes, and fires
|
|
4226
|
-
// `material-no-effective-pass` (MaterialError).
|
|
4227
|
-
const renderer = await createRenderer(canvas, {}, { shaderManifestUrl: undefined });
|
|
4228
|
-
const ready = (await renderer.ready) as { ok: boolean };
|
|
4229
|
-
expect(ready.ok).toBe(true);
|
|
4230
|
-
|
|
4231
|
-
// Spawn a Camera (so `cameras.length === 1`) plus a single MeshRenderer
|
|
4232
|
-
// entity (so `validated.length > 0` reaches the pipeline-pick branch).
|
|
4233
|
-
const world = new World() as {
|
|
4234
|
-
spawn: (...components: unknown[]) => { unwrap: () => unknown };
|
|
4235
|
-
allocSharedRef: (target: string, payload: unknown) => number;
|
|
4236
|
-
update: (delta: number) => { ok: boolean };
|
|
4237
|
-
};
|
|
4238
|
-
registerPropagateTransforms(world as never);
|
|
4239
|
-
|
|
4240
|
-
// Mint a material with zero passes (no parent chain) as a user-tier column
|
|
4241
|
-
// handle. The extract stage's material walk returns Err with
|
|
4242
|
-
// `material-no-effective-pass`.
|
|
4243
|
-
const matHandle = world.allocSharedRef('MaterialAsset', {
|
|
4244
|
-
kind: 'material',
|
|
4245
|
-
baseColor: [1, 0, 0, 1],
|
|
4246
|
-
});
|
|
4247
|
-
world
|
|
4248
|
-
.spawn(
|
|
4249
|
-
{
|
|
4250
|
-
component: components.Transform,
|
|
4251
|
-
data: {
|
|
4252
|
-
pos: [0, 0, 5],
|
|
4253
|
-
quat: [0, 0, 0, 1],
|
|
4254
|
-
scale: [1, 1, 1],
|
|
4255
|
-
},
|
|
4256
|
-
},
|
|
4257
|
-
{
|
|
4258
|
-
component: components.Camera,
|
|
4259
|
-
data: {
|
|
4260
|
-
fov: Math.PI / 4,
|
|
4261
|
-
aspect: 1,
|
|
4262
|
-
near: 0.1,
|
|
4263
|
-
far: 100,
|
|
4264
|
-
projection: 0,
|
|
4265
|
-
left: -1,
|
|
4266
|
-
right: 1,
|
|
4267
|
-
bottom: -1,
|
|
4268
|
-
top: 1,
|
|
4269
|
-
},
|
|
4270
|
-
},
|
|
4271
|
-
)
|
|
4272
|
-
.unwrap();
|
|
4273
|
-
world
|
|
4274
|
-
.spawn(
|
|
4275
|
-
{
|
|
4276
|
-
component: components.Transform,
|
|
4277
|
-
data: {
|
|
4278
|
-
pos: [0, 0, 0],
|
|
4279
|
-
quat: [0, 0, 0, 1],
|
|
4280
|
-
scale: [1, 1, 1],
|
|
4281
|
-
},
|
|
4282
|
-
},
|
|
4283
|
-
{
|
|
4284
|
-
component: components.MeshFilter,
|
|
4285
|
-
data: {
|
|
4286
|
-
assetHandle: world.allocSharedRef(
|
|
4287
|
-
'MeshAsset',
|
|
4288
|
-
createPrimitiveMesh('triangle').unwrap(),
|
|
4289
|
-
),
|
|
4290
|
-
},
|
|
4291
|
-
},
|
|
4292
|
-
{ component: components.MeshRenderer, data: { materials: [matHandle] } },
|
|
4293
|
-
)
|
|
4294
|
-
.unwrap();
|
|
4295
|
-
|
|
4296
|
-
expect(world.update(1 / 60).ok).toBe(true);
|
|
4297
|
-
|
|
4298
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
4299
|
-
throw new Error('World attachment failed');
|
|
4300
|
-
}
|
|
4301
|
-
|
|
4302
|
-
(world as WorldType).update().unwrap();
|
|
4303
|
-
|
|
4304
|
-
// The host-owned draw boundary isolates the invalid material and keeps
|
|
4305
|
-
// the call non-throwing; ECS no longer exposes a user error callback.
|
|
4306
|
-
expect(() => renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 })).not.toThrow();
|
|
4307
|
-
});
|
|
4308
|
-
});
|
|
4309
3462
|
}
|
|
4310
3463
|
|
|
4311
3464
|
{
|
|
4312
3465
|
// --- from renderer-input-snapshot.test.ts ---
|
|
4313
|
-
const
|
|
3466
|
+
const _ENGINE = new URL('../../../render/src/assembly/factory.ts', import.meta.url).href;
|
|
4314
3467
|
|
|
4315
3468
|
interface MockGL2Context {
|
|
4316
3469
|
__mockTag: 'webgl2';
|
|
@@ -4328,7 +3481,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
4328
3481
|
};
|
|
4329
3482
|
}
|
|
4330
3483
|
|
|
4331
|
-
function
|
|
3484
|
+
function _makeMockCanvas(): HTMLCanvasElement {
|
|
4332
3485
|
const listeners = new Map<string, Set<(e: unknown) => void>>();
|
|
4333
3486
|
const canvas = {
|
|
4334
3487
|
width: 800,
|
|
@@ -4359,7 +3512,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
4359
3512
|
return canvas;
|
|
4360
3513
|
}
|
|
4361
3514
|
|
|
4362
|
-
function
|
|
3515
|
+
function _makeMockGPU(): unknown {
|
|
4363
3516
|
return {
|
|
4364
3517
|
requestAdapter: async () => ({
|
|
4365
3518
|
requestDevice: async () => ({
|
|
@@ -4391,88 +3544,11 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
4391
3544
|
getPreferredCanvasFormat: () => 'bgra8unorm',
|
|
4392
3545
|
};
|
|
4393
3546
|
}
|
|
4394
|
-
|
|
4395
|
-
describe('renderer.input.snapshot(world) (V-2 first-class shim, D-2 + AC-09)', () => {
|
|
4396
|
-
beforeEach(() => {
|
|
4397
|
-
vi.stubGlobal('navigator', { gpu: makeMockGPU() });
|
|
4398
|
-
});
|
|
4399
|
-
afterEach(() => {
|
|
4400
|
-
vi.unstubAllGlobals();
|
|
4401
|
-
});
|
|
4402
|
-
|
|
4403
|
-
it('exposes input.snapshot as a function on the Renderer surface', async () => {
|
|
4404
|
-
const canvas = makeMockCanvas();
|
|
4405
|
-
const { createRenderer } = (await import(ENGINE)) as {
|
|
4406
|
-
createRenderer: (
|
|
4407
|
-
canvas: unknown,
|
|
4408
|
-
opts?: { shaderManifestUrl?: string | undefined },
|
|
4409
|
-
bundler?: unknown,
|
|
4410
|
-
) => Promise<{ input: { snapshot: unknown } }>;
|
|
4411
|
-
};
|
|
4412
|
-
const renderer = await createRenderer(canvas, {}, { shaderManifestUrl: undefined });
|
|
4413
|
-
expect(renderer.input).toBeDefined();
|
|
4414
|
-
expect(typeof renderer.input.snapshot).toBe('function');
|
|
4415
|
-
});
|
|
4416
|
-
|
|
4417
|
-
it('returns the InputSnapshot Resource attached to the supplied World', async () => {
|
|
4418
|
-
const canvas = makeMockCanvas();
|
|
4419
|
-
const { createRenderer } = (await import(ENGINE)) as {
|
|
4420
|
-
createRenderer: (
|
|
4421
|
-
canvas: unknown,
|
|
4422
|
-
opts?: { shaderManifestUrl?: string | undefined },
|
|
4423
|
-
bundler?: unknown,
|
|
4424
|
-
) => Promise<{ input: { snapshot: (world: World) => InputSnapshot | undefined } }>;
|
|
4425
|
-
};
|
|
4426
|
-
const renderer = await createRenderer(canvas, {}, { shaderManifestUrl: undefined });
|
|
4427
|
-
const world = new World();
|
|
4428
|
-
const seeded = createInputSnapshot();
|
|
4429
|
-
world.insertResource(INPUT_SNAPSHOT_RESOURCE_KEY, seeded);
|
|
4430
|
-
const got: InputSnapshot | undefined = renderer.input.snapshot(world);
|
|
4431
|
-
// Identity check: curried reader is a thin facade over getResource.
|
|
4432
|
-
expect(got).toBe(seeded);
|
|
4433
|
-
});
|
|
4434
|
-
|
|
4435
|
-
it('returns undefined when the World has no InputSnapshot resource (P3 empty)', async () => {
|
|
4436
|
-
const canvas = makeMockCanvas();
|
|
4437
|
-
const { createRenderer } = (await import(ENGINE)) as {
|
|
4438
|
-
createRenderer: (
|
|
4439
|
-
canvas: unknown,
|
|
4440
|
-
opts?: { shaderManifestUrl?: string | undefined },
|
|
4441
|
-
bundler?: unknown,
|
|
4442
|
-
) => Promise<{ input: { snapshot: (world: World) => InputSnapshot | undefined } }>;
|
|
4443
|
-
};
|
|
4444
|
-
const renderer = await createRenderer(canvas, {}, { shaderManifestUrl: undefined });
|
|
4445
|
-
const world = new World();
|
|
4446
|
-
expect(renderer.input.snapshot(world)).toBeUndefined();
|
|
4447
|
-
});
|
|
4448
|
-
|
|
4449
|
-
it('does not persist World between calls (P5 producer/consumer split)', async () => {
|
|
4450
|
-
const canvas = makeMockCanvas();
|
|
4451
|
-
const { createRenderer } = (await import(ENGINE)) as {
|
|
4452
|
-
createRenderer: (
|
|
4453
|
-
canvas: unknown,
|
|
4454
|
-
opts?: { shaderManifestUrl?: string | undefined },
|
|
4455
|
-
bundler?: unknown,
|
|
4456
|
-
) => Promise<{ input: { snapshot: (world: World) => InputSnapshot | undefined } }>;
|
|
4457
|
-
};
|
|
4458
|
-
const renderer = await createRenderer(canvas, {}, { shaderManifestUrl: undefined });
|
|
4459
|
-
// Two distinct worlds, two distinct snapshots; the curried reader picks
|
|
4460
|
-
// the World argument verbatim instead of holding an internal reference.
|
|
4461
|
-
const wA = new World();
|
|
4462
|
-
const wB = new World();
|
|
4463
|
-
const sA = createInputSnapshot();
|
|
4464
|
-
const sB = createInputSnapshot();
|
|
4465
|
-
wA.insertResource(INPUT_SNAPSHOT_RESOURCE_KEY, sA);
|
|
4466
|
-
wB.insertResource(INPUT_SNAPSHOT_RESOURCE_KEY, sB);
|
|
4467
|
-
expect(renderer.input.snapshot(wA)).toBe(sA);
|
|
4468
|
-
expect(renderer.input.snapshot(wB)).toBe(sB);
|
|
4469
|
-
});
|
|
4470
|
-
});
|
|
4471
3547
|
}
|
|
4472
3548
|
|
|
4473
3549
|
{
|
|
4474
3550
|
// --- from renderer-read-pixels.test.ts ---
|
|
4475
|
-
const ENGINE = new URL('../../../render/src/
|
|
3551
|
+
const ENGINE = new URL('../../../render/src/assembly/factory.ts', import.meta.url).href;
|
|
4476
3552
|
|
|
4477
3553
|
interface CanvasOptions {
|
|
4478
3554
|
webgl2: 'context' | 'null';
|
|
@@ -4482,7 +3558,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
4482
3558
|
const CANVAS_W = 8;
|
|
4483
3559
|
const CANVAS_H = 8;
|
|
4484
3560
|
|
|
4485
|
-
function
|
|
3561
|
+
function _makeMockCanvas(opts: CanvasOptions): HTMLCanvasElement {
|
|
4486
3562
|
const canvas = {
|
|
4487
3563
|
width: CANVAS_W,
|
|
4488
3564
|
height: CANVAS_H,
|
|
@@ -4567,7 +3643,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
4567
3643
|
|
|
4568
3644
|
const baseNavigator = { userAgent: 'mock-engine-test' } as unknown as Navigator;
|
|
4569
3645
|
|
|
4570
|
-
function
|
|
3646
|
+
function _buildManifestDataUrl(): string {
|
|
4571
3647
|
// feat-20260518-pbr-direct-lighting-mvp M5 / w22.9: createRenderer's
|
|
4572
3648
|
// post-fallback path requires both pbr (`f_schlick(` marker) + unlit
|
|
4573
3649
|
// entries; seed two minimal stubs (mock device's createShaderModule does
|
|
@@ -4603,7 +3679,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
4603
3679
|
getContext: (kind: string, opts?: unknown) => Ctx2dStub | null;
|
|
4604
3680
|
}
|
|
4605
3681
|
|
|
4606
|
-
function
|
|
3682
|
+
function _installBitmapStubs(opts: {
|
|
4607
3683
|
failBitmap?: false | Error;
|
|
4608
3684
|
ctx2dReturn?: Ctx2dStub | null;
|
|
4609
3685
|
}): {
|
|
@@ -4660,7 +3736,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
4660
3736
|
return (await import(ENGINE)) as never;
|
|
4661
3737
|
}
|
|
4662
3738
|
|
|
4663
|
-
async function
|
|
3739
|
+
async function _setupWebGPU(): Promise<{
|
|
4664
3740
|
createRenderer: (
|
|
4665
3741
|
canvas: unknown,
|
|
4666
3742
|
opts?: unknown,
|
|
@@ -4671,86 +3747,11 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
4671
3747
|
vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
|
|
4672
3748
|
return importEngine();
|
|
4673
3749
|
}
|
|
4674
|
-
|
|
4675
|
-
describe('Renderer.readPixels()', () => {
|
|
4676
|
-
it('WebGPU happy path returns Result.ok(Uint8Array) of length canvas.width * canvas.height * 4', async () => {
|
|
4677
|
-
const { createRenderer } = await setupWebGPU();
|
|
4678
|
-
installBitmapStubs({
|
|
4679
|
-
ctx2dReturn: {
|
|
4680
|
-
drawImage: () => undefined,
|
|
4681
|
-
getImageData: (_x, _y, w, h) => ({
|
|
4682
|
-
data: new Uint8ClampedArray(w * h * 4).fill(128),
|
|
4683
|
-
}),
|
|
4684
|
-
},
|
|
4685
|
-
});
|
|
4686
|
-
const canvas = makeMockCanvas({ webgl2: 'context', webgpu: 'context' });
|
|
4687
|
-
const renderer = await createRenderer(
|
|
4688
|
-
canvas,
|
|
4689
|
-
{},
|
|
4690
|
-
{ shaderManifestUrl: buildManifestDataUrl() },
|
|
4691
|
-
);
|
|
4692
|
-
await renderer.ready;
|
|
4693
|
-
|
|
4694
|
-
const result = await renderer.readPixels();
|
|
4695
|
-
expect(result.ok).toBe(true);
|
|
4696
|
-
if (!result.ok) throw new Error('unreachable');
|
|
4697
|
-
expect(result.value).toBeInstanceOf(Uint8Array);
|
|
4698
|
-
expect(result.value.length).toBe(CANVAS_W * CANVAS_H * 4);
|
|
4699
|
-
// Sample value is 128 (the fill we placed in the mock).
|
|
4700
|
-
expect(result.value[0]).toBe(128);
|
|
4701
|
-
});
|
|
4702
|
-
|
|
4703
|
-
it('OffscreenCanvas 2D ctx unavailable returns webgpu-runtime-error with detail.error message', async () => {
|
|
4704
|
-
const { createRenderer } = await setupWebGPU();
|
|
4705
|
-
installBitmapStubs({ ctx2dReturn: null });
|
|
4706
|
-
const canvas = makeMockCanvas({ webgl2: 'context', webgpu: 'context' });
|
|
4707
|
-
const renderer = await createRenderer(
|
|
4708
|
-
canvas,
|
|
4709
|
-
{},
|
|
4710
|
-
{ shaderManifestUrl: buildManifestDataUrl() },
|
|
4711
|
-
);
|
|
4712
|
-
await renderer.ready;
|
|
4713
|
-
|
|
4714
|
-
const result = await renderer.readPixels();
|
|
4715
|
-
expect(result.ok).toBe(false);
|
|
4716
|
-
if (result.ok) throw new Error('unreachable');
|
|
4717
|
-
expect(result.error.code).toBe('webgpu-runtime-error');
|
|
4718
|
-
const detailErr = result.error.detail?.error;
|
|
4719
|
-
expect(detailErr).toBeDefined();
|
|
4720
|
-
// detail.error is widened to RhiError | { code: string; message: string; name?: string; };
|
|
4721
|
-
// Both branches carry .message (RhiError has it via Error base class).
|
|
4722
|
-
if (detailErr && typeof detailErr === 'object') {
|
|
4723
|
-
expect((detailErr as { message: string }).message).toContain('null');
|
|
4724
|
-
}
|
|
4725
|
-
});
|
|
4726
|
-
|
|
4727
|
-
it('createImageBitmap throw surfaces as webgpu-runtime-error with thrown message in detail.error', async () => {
|
|
4728
|
-
const { createRenderer } = await setupWebGPU();
|
|
4729
|
-
installBitmapStubs({ failBitmap: new Error('mock: createImageBitmap rejected by canvas') });
|
|
4730
|
-
const canvas = makeMockCanvas({ webgl2: 'context', webgpu: 'context' });
|
|
4731
|
-
const renderer = await createRenderer(
|
|
4732
|
-
canvas,
|
|
4733
|
-
{},
|
|
4734
|
-
{ shaderManifestUrl: buildManifestDataUrl() },
|
|
4735
|
-
);
|
|
4736
|
-
await renderer.ready;
|
|
4737
|
-
|
|
4738
|
-
const result = await renderer.readPixels();
|
|
4739
|
-
expect(result.ok).toBe(false);
|
|
4740
|
-
if (result.ok) throw new Error('unreachable');
|
|
4741
|
-
expect(result.error.code).toBe('webgpu-runtime-error');
|
|
4742
|
-
const detailErr = result.error.detail?.error;
|
|
4743
|
-
expect(detailErr).toBeDefined();
|
|
4744
|
-
if (detailErr && typeof detailErr === 'object') {
|
|
4745
|
-
expect((detailErr as { message: string }).message).toContain('createImageBitmap rejected');
|
|
4746
|
-
}
|
|
4747
|
-
});
|
|
4748
|
-
});
|
|
4749
3750
|
}
|
|
4750
3751
|
|
|
4751
3752
|
{
|
|
4752
3753
|
// --- from renderer-ready.test.ts ---
|
|
4753
|
-
const
|
|
3754
|
+
const _ENGINE = new URL('../../../render/src/assembly/factory.ts', import.meta.url).href;
|
|
4754
3755
|
|
|
4755
3756
|
// ─── Mock helpers (mirrors createRenderer.test.ts shape) ────────────────────
|
|
4756
3757
|
|
|
@@ -4775,7 +3776,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
4775
3776
|
webgpu?: 'context' | 'null';
|
|
4776
3777
|
}
|
|
4777
3778
|
|
|
4778
|
-
function
|
|
3779
|
+
function _makeMockCanvas(opts: CanvasOptions): HTMLCanvasElement {
|
|
4779
3780
|
const canvas = {
|
|
4780
3781
|
width: 800,
|
|
4781
3782
|
height: 600,
|
|
@@ -4815,7 +3816,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
4815
3816
|
failWriteBuffer?: boolean;
|
|
4816
3817
|
}
|
|
4817
3818
|
|
|
4818
|
-
function
|
|
3819
|
+
function _makeMockGPUDevice(
|
|
4819
3820
|
log: DeviceCallLog,
|
|
4820
3821
|
overrides: DeviceOverrides = {},
|
|
4821
3822
|
): {
|
|
@@ -4909,7 +3910,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
4909
3910
|
return { device };
|
|
4910
3911
|
}
|
|
4911
3912
|
|
|
4912
|
-
function
|
|
3913
|
+
function _makeMockGPU(deviceObj: unknown): unknown {
|
|
4913
3914
|
return {
|
|
4914
3915
|
requestAdapter: async () => ({
|
|
4915
3916
|
requestDevice: async () => deviceObj,
|
|
@@ -4928,7 +3929,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
4928
3929
|
// createShaderModule does not parse WGSL syntax, the runtime test only
|
|
4929
3930
|
// requires that two distinct entries with the right content markers
|
|
4930
3931
|
// exist.
|
|
4931
|
-
function
|
|
3932
|
+
function _buildManifestDataUrl(): string {
|
|
4932
3933
|
const manifest = {
|
|
4933
3934
|
schemaVersion: '1.0.0',
|
|
4934
3935
|
entries: [
|
|
@@ -4965,150 +3966,6 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
4965
3966
|
|
|
4966
3967
|
// ─── Tests ──────────────────────────────────────────────────────────────────
|
|
4967
3968
|
|
|
4968
|
-
describe('Renderer.ready - WebGPU path three-step serial (D-S3)', () => {
|
|
4969
|
-
it('exposes a readonly `ready: Promise<void>` property', async () => {
|
|
4970
|
-
const log: DeviceCallLog = { order: [] };
|
|
4971
|
-
const { device } = makeMockGPUDevice(log);
|
|
4972
|
-
vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
|
|
4973
|
-
const canvas = makeMockCanvas({ webgl2: 'context', webgpu: 'context' });
|
|
4974
|
-
const { createRenderer } = (await import(ENGINE)) as {
|
|
4975
|
-
createRenderer: (
|
|
4976
|
-
canvas: unknown,
|
|
4977
|
-
opts?: unknown,
|
|
4978
|
-
bundler?: unknown,
|
|
4979
|
-
) => Promise<{ ready: Promise<void> }>;
|
|
4980
|
-
};
|
|
4981
|
-
|
|
4982
|
-
const renderer = await createRenderer(
|
|
4983
|
-
canvas,
|
|
4984
|
-
{},
|
|
4985
|
-
{ shaderManifestUrl: buildManifestDataUrl() },
|
|
4986
|
-
);
|
|
4987
|
-
expect(renderer.ready).toBeInstanceOf(Promise);
|
|
4988
|
-
});
|
|
4989
|
-
|
|
4990
|
-
it('resolves only after manifest load -> pipeline compile -> asset upload (in order)', async () => {
|
|
4991
|
-
const log: DeviceCallLog = { order: [] };
|
|
4992
|
-
const { device } = makeMockGPUDevice(log);
|
|
4993
|
-
vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
|
|
4994
|
-
const canvas = makeMockCanvas({ webgl2: 'context', webgpu: 'context' });
|
|
4995
|
-
const { createRenderer } = (await import(ENGINE)) as {
|
|
4996
|
-
createRenderer: (
|
|
4997
|
-
canvas: unknown,
|
|
4998
|
-
opts?: unknown,
|
|
4999
|
-
bundler?: unknown,
|
|
5000
|
-
) => Promise<{ ready: Promise<void> }>;
|
|
5001
|
-
};
|
|
5002
|
-
|
|
5003
|
-
const renderer = await createRenderer(
|
|
5004
|
-
canvas,
|
|
5005
|
-
{},
|
|
5006
|
-
{ shaderManifestUrl: buildManifestDataUrl() },
|
|
5007
|
-
);
|
|
5008
|
-
await renderer.ready;
|
|
5009
|
-
|
|
5010
|
-
// Order assertion: createShaderModule (manifest-driven pipeline step 1)
|
|
5011
|
-
// must happen before createBuffer (asset upload step 3). Specifically,
|
|
5012
|
-
// the very first create* call belongs to step 2 pipeline (after manifest
|
|
5013
|
-
// load completes async), and createBuffer runs after every pipeline call.
|
|
5014
|
-
const firstShader = log.order.indexOf('createShaderModule');
|
|
5015
|
-
const firstBuffer = log.order.indexOf('createBuffer');
|
|
5016
|
-
const firstWrite = log.order.indexOf('queue.writeBuffer');
|
|
5017
|
-
expect(firstShader).toBeGreaterThanOrEqual(0);
|
|
5018
|
-
expect(firstBuffer).toBeGreaterThan(firstShader);
|
|
5019
|
-
expect(firstWrite).toBeGreaterThan(firstBuffer);
|
|
5020
|
-
|
|
5021
|
-
// Three BindGroupLayouts must be created (view / material / mesh-array)
|
|
5022
|
-
// before createPipelineLayout aggregates them. (bug-20260519 merge:
|
|
5023
|
-
// feat-20260519-tonemap-reinhard-mvp added a second createPipelineLayout
|
|
5024
|
-
// call for the post-process tonemap pipeline whose own BGL is built
|
|
5025
|
-
// *after* the geometry pipeline-layout has already been aggregated, so
|
|
5026
|
-
// `lastIndexOf('createBindGroupLayout')` may now sit between the two
|
|
5027
|
-
// PLs. Constrain to the geometry chain only — slice the prefix up to
|
|
5028
|
-
// the first PL and assert the count of BGLs in that prefix is >= 3.)
|
|
5029
|
-
const bglCount = log.order.filter((c) => c === 'createBindGroupLayout').length;
|
|
5030
|
-
expect(bglCount).toBeGreaterThanOrEqual(3);
|
|
5031
|
-
const firstPipelineLayout = log.order.indexOf('createPipelineLayout');
|
|
5032
|
-
const bglCountBeforeFirstPL = log.order
|
|
5033
|
-
.slice(0, firstPipelineLayout)
|
|
5034
|
-
.filter((c) => c === 'createBindGroupLayout').length;
|
|
5035
|
-
expect(bglCountBeforeFirstPL).toBeGreaterThanOrEqual(3);
|
|
5036
|
-
});
|
|
5037
|
-
|
|
5038
|
-
it('settles with err shader-compile-failed when shader module creation fails (w24)', async () => {
|
|
5039
|
-
const log: DeviceCallLog = { order: [] };
|
|
5040
|
-
const { device } = makeMockGPUDevice(log, { failShaderModule: true });
|
|
5041
|
-
vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
|
|
5042
|
-
const canvas = makeMockCanvas({ webgl2: 'context', webgpu: 'context' });
|
|
5043
|
-
const { createRenderer } = (await import(ENGINE)) as {
|
|
5044
|
-
createRenderer: (
|
|
5045
|
-
canvas: unknown,
|
|
5046
|
-
opts?: unknown,
|
|
5047
|
-
bundler?: unknown,
|
|
5048
|
-
) => Promise<{ ready: Promise<unknown> }>;
|
|
5049
|
-
};
|
|
5050
|
-
|
|
5051
|
-
const renderer = await createRenderer(
|
|
5052
|
-
canvas,
|
|
5053
|
-
{},
|
|
5054
|
-
{ shaderManifestUrl: buildManifestDataUrl() },
|
|
5055
|
-
);
|
|
5056
|
-
// w24 — Renderer.ready now resolves Result<void, RhiError>; AI users
|
|
5057
|
-
// branch on `.ok` rather than try/catch the await.
|
|
5058
|
-
const ready = (await renderer.ready) as { ok: boolean; error?: { code: string } };
|
|
5059
|
-
expect(ready.ok).toBe(false);
|
|
5060
|
-
expect(ready.error?.code).toMatch(
|
|
5061
|
-
/shader-compile-failed|manifest-malformed|shader-not-found/,
|
|
5062
|
-
);
|
|
5063
|
-
});
|
|
5064
|
-
|
|
5065
|
-
it('settles with err when render pipeline creation fails (w24)', async () => {
|
|
5066
|
-
const log: DeviceCallLog = { order: [] };
|
|
5067
|
-
const { device } = makeMockGPUDevice(log, { failRenderPipeline: true });
|
|
5068
|
-
vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
|
|
5069
|
-
const canvas = makeMockCanvas({ webgl2: 'context', webgpu: 'context' });
|
|
5070
|
-
const { createRenderer } = (await import(ENGINE)) as {
|
|
5071
|
-
createRenderer: (
|
|
5072
|
-
canvas: unknown,
|
|
5073
|
-
opts?: unknown,
|
|
5074
|
-
bundler?: unknown,
|
|
5075
|
-
) => Promise<{ ready: Promise<unknown> }>;
|
|
5076
|
-
};
|
|
5077
|
-
|
|
5078
|
-
const renderer = await createRenderer(
|
|
5079
|
-
canvas,
|
|
5080
|
-
{},
|
|
5081
|
-
{ shaderManifestUrl: buildManifestDataUrl() },
|
|
5082
|
-
);
|
|
5083
|
-
const ready = (await renderer.ready) as { ok: boolean; error?: { code: string } };
|
|
5084
|
-
expect(ready.ok).toBe(false);
|
|
5085
|
-
expect(ready.error?.code).toBeDefined();
|
|
5086
|
-
});
|
|
5087
|
-
|
|
5088
|
-
it('settles with err when asset upload (createBuffer) fails (w24)', async () => {
|
|
5089
|
-
const log: DeviceCallLog = { order: [] };
|
|
5090
|
-
const { device } = makeMockGPUDevice(log, { failBuffer: true });
|
|
5091
|
-
vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
|
|
5092
|
-
const canvas = makeMockCanvas({ webgl2: 'context', webgpu: 'context' });
|
|
5093
|
-
const { createRenderer } = (await import(ENGINE)) as {
|
|
5094
|
-
createRenderer: (
|
|
5095
|
-
canvas: unknown,
|
|
5096
|
-
opts?: unknown,
|
|
5097
|
-
bundler?: unknown,
|
|
5098
|
-
) => Promise<{ ready: Promise<unknown> }>;
|
|
5099
|
-
};
|
|
5100
|
-
|
|
5101
|
-
const renderer = await createRenderer(
|
|
5102
|
-
canvas,
|
|
5103
|
-
{},
|
|
5104
|
-
{ shaderManifestUrl: buildManifestDataUrl() },
|
|
5105
|
-
);
|
|
5106
|
-
const ready = (await renderer.ready) as { ok: boolean; error?: { code: string } };
|
|
5107
|
-
expect(ready.ok).toBe(false);
|
|
5108
|
-
expect(ready.error?.code).toBeDefined();
|
|
5109
|
-
});
|
|
5110
|
-
});
|
|
5111
|
-
|
|
5112
3969
|
// bug-20260519 AC-02: Camera-only world / clear-pass-only path must NOT
|
|
5113
3970
|
// force shader-compile when the caller does not pass `shaderManifestUrl`.
|
|
5114
3971
|
// The fix lands in `createRenderer.ts` Step 2 guard + `RendererOptions`
|
|
@@ -5118,35 +3975,6 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
5118
3975
|
// minimal LO 1.1 hello-window equivalent (Engine.create + clearColor +
|
|
5119
3976
|
// no shader manifest) reach `Result.ok` without configuring vite-plugin-
|
|
5120
3977
|
// shader. Plan-strategy D-1 + D-2 + D-3.
|
|
5121
|
-
describe('Renderer.ready - bug-20260519 zero-manifest path (AC-02)', () => {
|
|
5122
|
-
it('Camera-only world + no shaderManifestUrl -> ready ok + 0 createShaderModule', async () => {
|
|
5123
|
-
const log: DeviceCallLog = { order: [] };
|
|
5124
|
-
const { device } = makeMockGPUDevice(log);
|
|
5125
|
-
vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
|
|
5126
|
-
const canvas = makeMockCanvas({ webgl2: 'context', webgpu: 'context' });
|
|
5127
|
-
const { createRenderer } = (await import(ENGINE)) as {
|
|
5128
|
-
createRenderer: (
|
|
5129
|
-
canvas: unknown,
|
|
5130
|
-
opts?: unknown,
|
|
5131
|
-
bundler?: unknown,
|
|
5132
|
-
) => Promise<{ ready: Promise<unknown> }>;
|
|
5133
|
-
};
|
|
5134
|
-
|
|
5135
|
-
// Explicit `shaderManifestUrl: undefined` opts into zero-entry mode
|
|
5136
|
-
// (D-7: the AC-02 case is the "no manifest configured" path).
|
|
5137
|
-
// ShaderRegistry yields zero entries -> Step 2 guard skips
|
|
5138
|
-
// createShaderModule entirely.
|
|
5139
|
-
const renderer = await createRenderer(canvas, {}, { shaderManifestUrl: undefined });
|
|
5140
|
-
const ready = (await renderer.ready) as { ok: boolean; error?: unknown };
|
|
5141
|
-
|
|
5142
|
-
expect(ready.ok).toBe(true);
|
|
5143
|
-
// Spy log: zero createShaderModule invocations -- the dual-compile
|
|
5144
|
-
// (PBR + unlit) block in `buildReadyWebGPU` Step 2 must be gated
|
|
5145
|
-
// behind `registry.entries().length > 0` so the zero-entry manifest
|
|
5146
|
-
// skips it entirely (charter P3 + D-3 nullable PipelineState).
|
|
5147
|
-
expect(log.order).not.toContain('createShaderModule');
|
|
5148
|
-
});
|
|
5149
|
-
});
|
|
5150
3978
|
}
|
|
5151
3979
|
|
|
5152
3980
|
{
|
|
@@ -5411,33 +4239,6 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
5411
4239
|
expect(fragment.targets[0]?.blend).toEqual(blend);
|
|
5412
4240
|
});
|
|
5413
4241
|
|
|
5414
|
-
it('(d) AC-08: stencil reaches createRenderPipeline depthStencil descriptor', () => {
|
|
5415
|
-
const mocks = makeMocks();
|
|
5416
|
-
const ctx = makeMockContext(mocks);
|
|
5417
|
-
const entry = makeMockEntry();
|
|
5418
|
-
|
|
5419
|
-
const stencilFace: StencilFaceState = {
|
|
5420
|
-
compare: 'always',
|
|
5421
|
-
failOp: 'keep',
|
|
5422
|
-
depthFailOp: 'keep',
|
|
5423
|
-
passOp: 'replace',
|
|
5424
|
-
};
|
|
5425
|
-
const rs: MaterialRenderState = { stencil: stencilFace };
|
|
5426
|
-
buildPipelineForMaterialShader('test::stencil', entry, ctx, rs);
|
|
5427
|
-
|
|
5428
|
-
const desc = (mocks.createRenderPipeline.mock.calls as unknown[][])[0]?.[0] as Record<
|
|
5429
|
-
string,
|
|
5430
|
-
unknown
|
|
5431
|
-
>;
|
|
5432
|
-
|
|
5433
|
-
const depthStencil = desc.depthStencil as {
|
|
5434
|
-
stencilFront?: typeof stencilFace;
|
|
5435
|
-
stencilBack?: typeof stencilFace;
|
|
5436
|
-
};
|
|
5437
|
-
expect(depthStencil.stencilFront).toEqual(stencilFace);
|
|
5438
|
-
expect(depthStencil.stencilBack).toEqual(stencilFace);
|
|
5439
|
-
});
|
|
5440
|
-
|
|
5441
4242
|
it('(d) AC-08: undefined renderState falls back to engine defaults', () => {
|
|
5442
4243
|
const mocks = makeMocks();
|
|
5443
4244
|
const ctx = makeMockContext(mocks);
|
|
@@ -5547,7 +4348,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
5547
4348
|
}
|
|
5548
4349
|
|
|
5549
4350
|
{
|
|
5550
|
-
// --- from gpu-
|
|
4351
|
+
// --- from device/gpu-residency.test.ts (__tests__/) ---
|
|
5551
4352
|
const okShim = <T>(v: T) => ({ ok: true as const, value: v });
|
|
5552
4353
|
|
|
5553
4354
|
interface DeviceProbe {
|
|
@@ -5642,17 +4443,11 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
5642
4443
|
}
|
|
5643
4444
|
|
|
5644
4445
|
function meshPod(verts = 4): MeshAsset {
|
|
5645
|
-
const attributes = {
|
|
5646
|
-
position: new Float32Array(verts * 3),
|
|
5647
|
-
normal: new Float32Array(verts * 3),
|
|
5648
|
-
uv: new Float32Array(verts * 2),
|
|
5649
|
-
tangent: new Float32Array(verts * 4),
|
|
5650
|
-
};
|
|
5651
4446
|
return {
|
|
5652
4447
|
kind: 'mesh',
|
|
5653
4448
|
vertices: new Float32Array(verts * 12),
|
|
5654
4449
|
indices: new Uint16Array([0, 1, 2, 0, 2, 3]),
|
|
5655
|
-
attributes,
|
|
4450
|
+
attributes: buildMeshAttributeMapForUvSets(1),
|
|
5656
4451
|
aabb: new Float32Array(6),
|
|
5657
4452
|
submeshes: [
|
|
5658
4453
|
{
|
|
@@ -5677,13 +4472,13 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
5677
4472
|
};
|
|
5678
4473
|
}
|
|
5679
4474
|
|
|
5680
|
-
function configured(probe: DeviceProbe):
|
|
5681
|
-
const store = new
|
|
4475
|
+
function configured(probe: DeviceProbe): GpuResidencyCache {
|
|
4476
|
+
const store = new GpuResidencyCache();
|
|
5682
4477
|
store.configureGpuDevice(makeMockDevice(probe), shaderFactory, makeRegisterCube(), mockCaps);
|
|
5683
4478
|
return store;
|
|
5684
4479
|
}
|
|
5685
4480
|
|
|
5686
|
-
describe('
|
|
4481
|
+
describe('GpuResidencyCache residency', () => {
|
|
5687
4482
|
it('(1)+(2) mesh ensureResident miss builds buffers, hit is O(1) (same buffers)', () => {
|
|
5688
4483
|
const probe = freshProbe();
|
|
5689
4484
|
const store = configured(probe);
|
|
@@ -5704,100 +4499,8 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
5704
4499
|
}
|
|
5705
4500
|
});
|
|
5706
4501
|
|
|
5707
|
-
it('(3) texture ensureResident miss (mipmap=false) builds texture + view synchronously', () => {
|
|
5708
|
-
const probe = freshProbe();
|
|
5709
|
-
const store = configured(probe);
|
|
5710
|
-
const handle = toShared<'TextureAsset'>(2048);
|
|
5711
|
-
const pod = texturePod(false);
|
|
5712
|
-
|
|
5713
|
-
const res = store.ensureResident(handle, pod);
|
|
5714
|
-
expect(res.ok).toBe(true);
|
|
5715
|
-
expect(probe.textures).toBe(1);
|
|
5716
|
-
expect(store.getTextureGpuView(handle)).toBeDefined();
|
|
5717
|
-
|
|
5718
|
-
// Hit: no second texture allocation.
|
|
5719
|
-
const again = store.ensureResident(handle, pod);
|
|
5720
|
-
expect(again.ok).toBe(true);
|
|
5721
|
-
expect(probe.textures).toBe(1);
|
|
5722
|
-
});
|
|
5723
|
-
|
|
5724
|
-
it('(4) accessors return cached entry; miss returns undefined', () => {
|
|
5725
|
-
const probe = freshProbe();
|
|
5726
|
-
const store = configured(probe);
|
|
5727
|
-
const meshHandle = toShared<'MeshAsset'>(1024);
|
|
5728
|
-
const texHandle = toShared<'TextureAsset'>(2048);
|
|
5729
|
-
|
|
5730
|
-
// Miss before residency.
|
|
5731
|
-
expect(store.getMeshGpuHandles(meshHandle)).toBeUndefined();
|
|
5732
|
-
expect(store.getTextureGpuView(texHandle)).toBeUndefined();
|
|
5733
|
-
expect(store.getCubemapGpuView(toShared<'EquirectAsset'>(9))).toBeUndefined();
|
|
5734
|
-
expect(store.getCubemapGpuTexture(toShared<'EquirectAsset'>(9))).toBeUndefined();
|
|
5735
|
-
expect(store.getCubemapFaceViews(toShared<'EquirectAsset'>(9))).toBeUndefined();
|
|
5736
|
-
|
|
5737
|
-
store.ensureResident(meshHandle, meshPod());
|
|
5738
|
-
store.ensureResident(texHandle, texturePod(false));
|
|
5739
|
-
expect(store.getMeshGpuHandles(meshHandle)).toBeDefined();
|
|
5740
|
-
expect(store.getTextureGpuView(texHandle)).toBeDefined();
|
|
5741
|
-
});
|
|
5742
|
-
|
|
5743
|
-
it('(5) cubemapIdempotentMap: same source handle returns the same cube handle', async () => {
|
|
5744
|
-
const probe = freshProbe();
|
|
5745
|
-
const store = configured(probe);
|
|
5746
|
-
const srcHandle = toShared<'EquirectAsset'>(2048);
|
|
5747
|
-
const srcPod: EquirectAsset = {
|
|
5748
|
-
kind: 'equirect',
|
|
5749
|
-
width: 8,
|
|
5750
|
-
height: 4,
|
|
5751
|
-
format: 'rgba16float',
|
|
5752
|
-
data: new Uint8Array(8 * 4 * 8),
|
|
5753
|
-
colorSpace: 'linear',
|
|
5754
|
-
};
|
|
5755
|
-
|
|
5756
|
-
// biome-ignore lint/suspicious/noExplicitAny: package-internal projection reached via store cast
|
|
5757
|
-
const r1 = await (store as any)._uploadCubemapFromEquirect(new World(), srcHandle, srcPod);
|
|
5758
|
-
// biome-ignore lint/suspicious/noExplicitAny: package-internal projection reached via store cast
|
|
5759
|
-
const r2 = await (store as any)._uploadCubemapFromEquirect(new World(), srcHandle, srcPod);
|
|
5760
|
-
expect(r1.ok && r2.ok).toBe(true);
|
|
5761
|
-
if (r1.ok && r2.ok) {
|
|
5762
|
-
// Second call is the idempotent cache hit -> identical cube handle.
|
|
5763
|
-
expect(JSON.stringify(r1.value)).toBe(JSON.stringify(r2.value));
|
|
5764
|
-
// getCubemapGpuTexture resolves the cube handle (D-3 single-call contract).
|
|
5765
|
-
expect(store.getCubemapGpuTexture(r1.value)).toBeDefined();
|
|
5766
|
-
}
|
|
5767
|
-
});
|
|
5768
|
-
|
|
5769
|
-
it('(6) prewarmMipmapPipeline then mipmap=true texture ensureResident builds synchronously', async () => {
|
|
5770
|
-
const probe = freshProbe();
|
|
5771
|
-
const device = makeMockDevice(probe);
|
|
5772
|
-
const store = new GpuResourceStore();
|
|
5773
|
-
store.configureGpuDevice(device, shaderFactory, makeRegisterCube(), mockCaps);
|
|
5774
|
-
// Prewarm so the sync blit reads the cached pipeline (no lazy await).
|
|
5775
|
-
const prewarm = await store.prewarmMipmapPipeline(device, ['rgba8unorm-srgb']);
|
|
5776
|
-
expect(prewarm.ok).toBe(true);
|
|
5777
|
-
|
|
5778
|
-
const handle = toShared<'TextureAsset'>(2048);
|
|
5779
|
-
const pod = texturePod(true, 'rgba8unorm-srgb');
|
|
5780
|
-
const res = store.ensureResident(handle, pod);
|
|
5781
|
-
expect(res.ok).toBe(true);
|
|
5782
|
-
// The synchronous mipmap blit submitted a command buffer.
|
|
5783
|
-
expect(probe.submits).toBeGreaterThanOrEqual(1);
|
|
5784
|
-
});
|
|
5785
|
-
|
|
5786
|
-
it('(7a) un-prewarmed mipmap format on the sync path returns structured RhiError (no lazy await)', () => {
|
|
5787
|
-
const probe = freshProbe();
|
|
5788
|
-
const store = configured(probe); // device wired, but NO prewarm
|
|
5789
|
-
const handle = toShared<'TextureAsset'>(2048);
|
|
5790
|
-
const pod = texturePod(true, 'rgba8unorm-srgb'); // mipmap=true, never prewarmed
|
|
5791
|
-
|
|
5792
|
-
const res = store.ensureResident(handle, pod);
|
|
5793
|
-
expect(res.ok).toBe(false);
|
|
5794
|
-
if (!res.ok) {
|
|
5795
|
-
expect(res.error.code).toBe('rhi-not-available');
|
|
5796
|
-
}
|
|
5797
|
-
});
|
|
5798
|
-
|
|
5799
4502
|
it('(7b) no-device ensureResident returns a structured error (OOS-3 legacy degradation made explicit)', () => {
|
|
5800
|
-
const store = new
|
|
4503
|
+
const store = new GpuResidencyCache(); // configureGpuDevice never called
|
|
5801
4504
|
const meshRes = store.ensureResident(toShared<'MeshAsset'>(1024), meshPod());
|
|
5802
4505
|
expect(meshRes.ok).toBe(false);
|
|
5803
4506
|
const texRes = store.ensureResident(toShared<'TextureAsset'>(2048), texturePod(false));
|
|
@@ -5816,21 +4519,12 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
5816
4519
|
const COPY_DST = 0x2;
|
|
5817
4520
|
const RENDER_ATTACHMENT = 0x10;
|
|
5818
4521
|
|
|
5819
|
-
function canonicalAttributes(vertexCount: number): MeshAsset['attributes'] {
|
|
5820
|
-
return {
|
|
5821
|
-
position: new Float32Array(vertexCount * 3),
|
|
5822
|
-
normal: new Float32Array(vertexCount * 3),
|
|
5823
|
-
uv: new Float32Array(vertexCount * 2),
|
|
5824
|
-
tangent: new Float32Array(vertexCount * 4),
|
|
5825
|
-
};
|
|
5826
|
-
}
|
|
5827
|
-
|
|
5828
4522
|
function meshPod(overrides: Partial<MeshAsset> = {}): MeshAsset {
|
|
5829
4523
|
return {
|
|
5830
4524
|
kind: 'mesh',
|
|
5831
4525
|
vertices: new Float32Array(4 * 12),
|
|
5832
4526
|
indices: new Uint16Array([0, 1, 2, 0, 2, 3]),
|
|
5833
|
-
attributes:
|
|
4527
|
+
attributes: buildMeshAttributeMapForUvSets(1),
|
|
5834
4528
|
aabb: new Float32Array(6),
|
|
5835
4529
|
...overrides,
|
|
5836
4530
|
submeshes: [
|
|
@@ -5890,6 +4584,12 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
5890
4584
|
expect(rd.indexCount).toBe(6);
|
|
5891
4585
|
expect(rd.indexFormat).toBe('uint16');
|
|
5892
4586
|
expect(rd.layoutProjection.arrayStride).toBe(48);
|
|
4587
|
+
expect(rd.layoutProjection.attributes.map((attribute) => attribute.key)).toEqual([
|
|
4588
|
+
'position',
|
|
4589
|
+
'normal',
|
|
4590
|
+
'uv',
|
|
4591
|
+
'tangent',
|
|
4592
|
+
]);
|
|
5893
4593
|
expect(rd.vertexUsage).toBe(GPU_BUFFER_USAGE_VERTEX | GPU_BUFFER_USAGE_COPY_DST);
|
|
5894
4594
|
expect(rd.indexUsage).toBe(GPU_BUFFER_USAGE_INDEX | GPU_BUFFER_USAGE_COPY_DST);
|
|
5895
4595
|
});
|
|
@@ -6179,121 +4879,17 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
6179
4879
|
}
|
|
6180
4880
|
|
|
6181
4881
|
{
|
|
6182
|
-
// --- from hdrp-caps-gate.test.ts ---
|
|
6183
|
-
describe('HdrpCapsInsufficientError class shape (AC-17/AC-18/AC-20)', () => {
|
|
6184
|
-
it('has .code === hdrp-caps-insufficient', async () => {
|
|
6185
|
-
const { HdrpCapsInsufficientError } = await import('@forgeax/engine-render/internal');
|
|
6186
|
-
const err = new HdrpCapsInsufficientError('maxStorageBuffersPerShaderStage', 2, 4);
|
|
6187
|
-
expect(err.code).toBe('hdrp-caps-insufficient');
|
|
6188
|
-
});
|
|
6189
|
-
|
|
6190
|
-
it('.detail carries { capName, actual, required }', async () => {
|
|
6191
|
-
const { HdrpCapsInsufficientError } = await import('@forgeax/engine-render/internal');
|
|
6192
|
-
const err = new HdrpCapsInsufficientError('maxStorageBuffersPerShaderStage', 2, 4);
|
|
6193
|
-
expect(err.detail).toBeDefined();
|
|
6194
|
-
expect(err.detail.capName).toBe('maxStorageBuffersPerShaderStage');
|
|
6195
|
-
expect(err.detail.actual).toBe(2);
|
|
6196
|
-
expect(err.detail.required).toBe(4);
|
|
6197
|
-
});
|
|
6198
|
-
|
|
6199
|
-
it('.hint contains fall-back-to-URP substring (AC-18)', async () => {
|
|
6200
|
-
const { HdrpCapsInsufficientError } = await import('@forgeax/engine-render/internal');
|
|
6201
|
-
const err = new HdrpCapsInsufficientError('maxStorageBuffersPerShaderStage', 2, 4);
|
|
6202
|
-
expect(err.hint).toBeDefined();
|
|
6203
|
-
expect(typeof err.hint).toBe('string');
|
|
6204
|
-
expect(err.hint.length).toBeGreaterThan(0);
|
|
6205
|
-
expect(err.hint).toMatch(/fall back to URP|do not call installPipeline/i);
|
|
6206
|
-
});
|
|
6207
|
-
|
|
6208
|
-
it('.expected describes the capacity requirement', async () => {
|
|
6209
|
-
const { HdrpCapsInsufficientError } = await import('@forgeax/engine-render/internal');
|
|
6210
|
-
const err = new HdrpCapsInsufficientError('maxStorageBuffersPerShaderStage', 2, 4);
|
|
6211
|
-
expect(err.expected).toBeDefined();
|
|
6212
|
-
expect(typeof err.expected).toBe('string');
|
|
6213
|
-
expect(err.expected.length).toBeGreaterThan(0);
|
|
6214
|
-
});
|
|
6215
|
-
|
|
6216
|
-
it('extends Error', async () => {
|
|
6217
|
-
const { HdrpCapsInsufficientError } = await import('@forgeax/engine-render/internal');
|
|
6218
|
-
const err = new HdrpCapsInsufficientError('maxStorageBuffersPerShaderStage', 2, 4);
|
|
6219
|
-
expect(err).toBeInstanceOf(Error);
|
|
6220
|
-
expect(err.name).toBe('HdrpCapsInsufficientError');
|
|
6221
|
-
});
|
|
6222
|
-
});
|
|
6223
|
-
|
|
6224
4882
|
// ── Light budget error class shape ─────────────────────────────────────────────
|
|
6225
4883
|
|
|
6226
|
-
describe('HdrpLightBudgetExceededError class shape (AC-07/AC-20)', () => {
|
|
6227
|
-
it('has .code === hdrp-light-budget-exceeded', async () => {
|
|
6228
|
-
const { HdrpLightBudgetExceededError } = await import('@forgeax/engine-render/internal');
|
|
6229
|
-
const err = new HdrpLightBudgetExceededError(257, 256);
|
|
6230
|
-
expect(err.code).toBe('hdrp-light-budget-exceeded');
|
|
6231
|
-
});
|
|
6232
|
-
|
|
6233
|
-
it('.detail carries { actual, budget }', async () => {
|
|
6234
|
-
const { HdrpLightBudgetExceededError } = await import('@forgeax/engine-render/internal');
|
|
6235
|
-
const err = new HdrpLightBudgetExceededError(257, 256);
|
|
6236
|
-
expect(err.detail).toBeDefined();
|
|
6237
|
-
expect(err.detail.actual).toBe(257);
|
|
6238
|
-
expect(err.detail.budget).toBe(256);
|
|
6239
|
-
});
|
|
6240
|
-
|
|
6241
|
-
it('.hint is a non-empty actionable string', async () => {
|
|
6242
|
-
const { HdrpLightBudgetExceededError } = await import('@forgeax/engine-render/internal');
|
|
6243
|
-
const err = new HdrpLightBudgetExceededError(257, 256);
|
|
6244
|
-
expect(err.hint).toBeDefined();
|
|
6245
|
-
expect(typeof err.hint).toBe('string');
|
|
6246
|
-
expect(err.hint.length).toBeGreaterThan(0);
|
|
6247
|
-
});
|
|
6248
|
-
|
|
6249
|
-
it('extends Error', async () => {
|
|
6250
|
-
const { HdrpLightBudgetExceededError } = await import('@forgeax/engine-render/internal');
|
|
6251
|
-
const err = new HdrpLightBudgetExceededError(257, 256);
|
|
6252
|
-
expect(err).toBeInstanceOf(Error);
|
|
6253
|
-
expect(err.name).toBe('HdrpLightBudgetExceededError');
|
|
6254
|
-
});
|
|
6255
|
-
});
|
|
6256
|
-
|
|
6257
4884
|
// ── Index list overflow error class shape ─────────────────────────────────────
|
|
6258
4885
|
|
|
6259
|
-
describe('HdrpIndexListOverflowError class shape (AC-24)', () => {
|
|
6260
|
-
it('has .code === hdrp-index-list-overflow', async () => {
|
|
6261
|
-
const { HdrpIndexListOverflowError } = await import('@forgeax/engine-render/internal');
|
|
6262
|
-
const err = new HdrpIndexListOverflowError(65537, 65536);
|
|
6263
|
-
expect(err.code).toBe('hdrp-index-list-overflow');
|
|
6264
|
-
});
|
|
6265
|
-
|
|
6266
|
-
it('.detail carries { actual, capacity }', async () => {
|
|
6267
|
-
const { HdrpIndexListOverflowError } = await import('@forgeax/engine-render/internal');
|
|
6268
|
-
const err = new HdrpIndexListOverflowError(65537, 65536);
|
|
6269
|
-
expect(err.detail).toBeDefined();
|
|
6270
|
-
expect(err.detail.actual).toBe(65537);
|
|
6271
|
-
expect(err.detail.capacity).toBe(65536);
|
|
6272
|
-
});
|
|
6273
|
-
|
|
6274
|
-
it('.hint is a non-empty actionable string', async () => {
|
|
6275
|
-
const { HdrpIndexListOverflowError } = await import('@forgeax/engine-render/internal');
|
|
6276
|
-
const err = new HdrpIndexListOverflowError(65537, 65536);
|
|
6277
|
-
expect(err.hint).toBeDefined();
|
|
6278
|
-
expect(typeof err.hint).toBe('string');
|
|
6279
|
-
expect(err.hint.length).toBeGreaterThan(0);
|
|
6280
|
-
});
|
|
6281
|
-
|
|
6282
|
-
it('extends Error', async () => {
|
|
6283
|
-
const { HdrpIndexListOverflowError } = await import('@forgeax/engine-render/internal');
|
|
6284
|
-
const err = new HdrpIndexListOverflowError(65537, 65536);
|
|
6285
|
-
expect(err).toBeInstanceOf(Error);
|
|
6286
|
-
expect(err.name).toBe('HdrpIndexListOverflowError');
|
|
6287
|
-
});
|
|
6288
|
-
});
|
|
6289
|
-
|
|
6290
4886
|
// ── Caps gate 5 scenarios (AC-17) ─────────────────────────────────────────────
|
|
6291
4887
|
// Note: the caps router lives in hdrp-pipeline.ts. These tests verify
|
|
6292
4888
|
// the installPipeline behaviour by mocking device.caps.
|
|
6293
4889
|
|
|
6294
4890
|
describe('HDRP caps gate 5 scenarios (AC-17)', () => {
|
|
6295
4891
|
it('caps=0 (uniform fallback) yields ok(false)', async () => {
|
|
6296
|
-
const { assertStorageBufferCap } = await import('
|
|
4892
|
+
const { assertStorageBufferCap } = await import('../../../render/src/light-buffer-layout');
|
|
6297
4893
|
const result = assertStorageBufferCap(0);
|
|
6298
4894
|
expect(result.ok).toBe(true);
|
|
6299
4895
|
if (result.ok) {
|
|
@@ -6302,19 +4898,19 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
6302
4898
|
});
|
|
6303
4899
|
|
|
6304
4900
|
it('caps=1 (insufficient, partial storage) yields err', async () => {
|
|
6305
|
-
const { assertStorageBufferCap } = await import('
|
|
4901
|
+
const { assertStorageBufferCap } = await import('../../../render/src/light-buffer-layout');
|
|
6306
4902
|
const result = assertStorageBufferCap(1);
|
|
6307
4903
|
expect(result.ok).toBe(false);
|
|
6308
4904
|
});
|
|
6309
4905
|
|
|
6310
4906
|
it('caps=3 (insufficient, below 4) yields err', async () => {
|
|
6311
|
-
const { assertStorageBufferCap } = await import('
|
|
4907
|
+
const { assertStorageBufferCap } = await import('../../../render/src/light-buffer-layout');
|
|
6312
4908
|
const result = assertStorageBufferCap(3);
|
|
6313
4909
|
expect(result.ok).toBe(false);
|
|
6314
4910
|
});
|
|
6315
4911
|
|
|
6316
4912
|
it('caps=4 (sufficient) yields ok(true)', async () => {
|
|
6317
|
-
const { assertStorageBufferCap } = await import('
|
|
4913
|
+
const { assertStorageBufferCap } = await import('../../../render/src/light-buffer-layout');
|
|
6318
4914
|
const result = assertStorageBufferCap(4);
|
|
6319
4915
|
expect(result.ok).toBe(true);
|
|
6320
4916
|
if (result.ok) {
|
|
@@ -6323,7 +4919,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
6323
4919
|
});
|
|
6324
4920
|
|
|
6325
4921
|
it('caps=8 (sufficient, above minimum) yields ok(true)', async () => {
|
|
6326
|
-
const { assertStorageBufferCap } = await import('
|
|
4922
|
+
const { assertStorageBufferCap } = await import('../../../render/src/light-buffer-layout');
|
|
6327
4923
|
const result = assertStorageBufferCap(8);
|
|
6328
4924
|
expect(result.ok).toBe(true);
|
|
6329
4925
|
if (result.ok) {
|
|
@@ -6333,32 +4929,6 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
6333
4929
|
});
|
|
6334
4930
|
|
|
6335
4931
|
// ── RuntimeErrorCode union member count (12 -> 15) ───────────────────────────
|
|
6336
|
-
|
|
6337
|
-
describe('RuntimeErrorCode union 12 -> 15 (D-4)', () => {
|
|
6338
|
-
it('HdrpCapsInsufficientError .code is recognized as RuntimeErrorCode literal', async () => {
|
|
6339
|
-
const { HdrpCapsInsufficientError } = await import('@forgeax/engine-render/internal');
|
|
6340
|
-
const err = new HdrpCapsInsufficientError('maxStorageBuffersPerShaderStage', 2, 4);
|
|
6341
|
-
const code: string = err.code;
|
|
6342
|
-
// If hdrp-caps-insufficient is not in the RuntimeErrorCode union,
|
|
6343
|
-
// assigning err.code to a typed RuntimeErrorCode variable would fail.
|
|
6344
|
-
// This is a runtime smoke: assert the literal is kebab-case and hdrp-prefixed.
|
|
6345
|
-
expect(code).toMatch(/^hdrp-/);
|
|
6346
|
-
});
|
|
6347
|
-
|
|
6348
|
-
it('HdrpLightBudgetExceededError .code is recognized as RuntimeErrorCode literal', async () => {
|
|
6349
|
-
const { HdrpLightBudgetExceededError } = await import('@forgeax/engine-render/internal');
|
|
6350
|
-
const err = new HdrpLightBudgetExceededError(257, 256);
|
|
6351
|
-
const code: string = err.code;
|
|
6352
|
-
expect(code).toMatch(/^hdrp-/);
|
|
6353
|
-
});
|
|
6354
|
-
|
|
6355
|
-
it('HdrpIndexListOverflowError .code is recognized as RuntimeErrorCode literal', async () => {
|
|
6356
|
-
const { HdrpIndexListOverflowError } = await import('@forgeax/engine-render/internal');
|
|
6357
|
-
const err = new HdrpIndexListOverflowError(65537, 65536);
|
|
6358
|
-
const code: string = err.code;
|
|
6359
|
-
expect(code).toMatch(/^hdrp-/);
|
|
6360
|
-
});
|
|
6361
|
-
});
|
|
6362
4932
|
}
|
|
6363
4933
|
|
|
6364
4934
|
{
|
|
@@ -6506,18 +5076,6 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
6506
5076
|
|
|
6507
5077
|
// ── hdrp-index-list-overflow RuntimeError class shape ────────────────────────
|
|
6508
5078
|
|
|
6509
|
-
describe('hdrp-index-list-overflow RuntimeError class shape (AC-24)', () => {
|
|
6510
|
-
it('HdrpIndexListOverflowError has the expected shape', async () => {
|
|
6511
|
-
const { HdrpIndexListOverflowError } = await import('@forgeax/engine-render/internal');
|
|
6512
|
-
const err = new HdrpIndexListOverflowError(70000, 65536);
|
|
6513
|
-
expect(err.code).toBe('hdrp-index-list-overflow');
|
|
6514
|
-
expect(err.detail.actual).toBe(70000);
|
|
6515
|
-
expect(err.detail.capacity).toBe(65536);
|
|
6516
|
-
expect(err.hint.length).toBeGreaterThan(0);
|
|
6517
|
-
expect(err.expected.length).toBeGreaterThan(0);
|
|
6518
|
-
});
|
|
6519
|
-
});
|
|
6520
|
-
|
|
6521
5079
|
// ── AC-03 guardrail: URP LIGHT_ARRAY_MAX_SLOTS=4 + 5 PointLights ─────────────
|
|
6522
5080
|
//
|
|
6523
5081
|
// This test verifies that the existing URP `render-system-multi-light` warn-once
|
|
@@ -6526,13 +5084,13 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
6526
5084
|
|
|
6527
5085
|
describe('URP light budget warn-once guardrail (AC-03)', () => {
|
|
6528
5086
|
it('LIGHT_ARRAY_MAX_SLOTS is still 4 (URP first-slice cap)', async () => {
|
|
6529
|
-
const { LIGHT_ARRAY_MAX_SLOTS } = await import('
|
|
5087
|
+
const { LIGHT_ARRAY_MAX_SLOTS } = await import('../../../render/src/light-buffer-layout');
|
|
6530
5088
|
expect(LIGHT_ARRAY_MAX_SLOTS).toBe(4);
|
|
6531
5089
|
});
|
|
6532
5090
|
|
|
6533
5091
|
it('POINT_LIGHT_STD430_BYTES and SPOT_LIGHT_STD430_BYTES still match URP specs', async () => {
|
|
6534
5092
|
const { POINT_LIGHT_STD430_BYTES, SPOT_LIGHT_STD430_BYTES } = await import(
|
|
6535
|
-
'
|
|
5093
|
+
'../../../render/src/light-buffer-layout'
|
|
6536
5094
|
);
|
|
6537
5095
|
expect(POINT_LIGHT_STD430_BYTES).toBe(32);
|
|
6538
5096
|
// feat-20260625-spot-light-shadow-mapping M2 / w8 (D-4): SpotLight grew
|
|
@@ -6544,39 +5102,6 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
6544
5102
|
|
|
6545
5103
|
{
|
|
6546
5104
|
// --- from hdrp-light-budget.test.ts ---
|
|
6547
|
-
describe('HdrpLightBudgetExceededError class shape (AC-07)', () => {
|
|
6548
|
-
it('has .code === hdrp-light-budget-exceeded', async () => {
|
|
6549
|
-
const { HdrpLightBudgetExceededError } = await import('@forgeax/engine-render/internal');
|
|
6550
|
-
const err = new HdrpLightBudgetExceededError(257, 256);
|
|
6551
|
-
expect(err.code).toBe('hdrp-light-budget-exceeded');
|
|
6552
|
-
});
|
|
6553
|
-
|
|
6554
|
-
it('.detail carries { actual, budget } with correct values', async () => {
|
|
6555
|
-
const { HdrpLightBudgetExceededError } = await import('@forgeax/engine-render/internal');
|
|
6556
|
-
const err = new HdrpLightBudgetExceededError(257, 256);
|
|
6557
|
-
expect(err.detail.actual).toBe(257);
|
|
6558
|
-
expect(err.detail.budget).toBe(256);
|
|
6559
|
-
});
|
|
6560
|
-
|
|
6561
|
-
it('.detail budget is 256 (the D-scope limit)', async () => {
|
|
6562
|
-
const { HdrpLightBudgetExceededError } = await import('@forgeax/engine-render/internal');
|
|
6563
|
-
const err = new HdrpLightBudgetExceededError(300, 256);
|
|
6564
|
-
expect(err.detail.budget).toBe(256);
|
|
6565
|
-
});
|
|
6566
|
-
|
|
6567
|
-
it('.expected describes the budget constraint', async () => {
|
|
6568
|
-
const { HdrpLightBudgetExceededError } = await import('@forgeax/engine-render/internal');
|
|
6569
|
-
const err = new HdrpLightBudgetExceededError(257, 256);
|
|
6570
|
-
expect(err.expected.length).toBeGreaterThan(0);
|
|
6571
|
-
expect(err.expected).toMatch(/256/);
|
|
6572
|
-
});
|
|
6573
|
-
|
|
6574
|
-
it('.hint is a non-empty actionable string', async () => {
|
|
6575
|
-
const { HdrpLightBudgetExceededError } = await import('@forgeax/engine-render/internal');
|
|
6576
|
-
const err = new HdrpLightBudgetExceededError(257, 256);
|
|
6577
|
-
expect(err.hint.length).toBeGreaterThan(0);
|
|
6578
|
-
});
|
|
6579
|
-
});
|
|
6580
5105
|
|
|
6581
5106
|
// ── Budget gate unit logic (the pure gate w23 will embed) ─────────────────────
|
|
6582
5107
|
|
|
@@ -6869,9 +5394,9 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
6869
5394
|
// --- from m7-hdrp-demo-shape.test.ts ---
|
|
6870
5395
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
6871
5396
|
const REPO_ROOT = resolve(HERE, '..', '..', '..', '..');
|
|
6872
|
-
const
|
|
5397
|
+
const _FIVE_METRIC_KINDS = ['bundle-size', 'fps', 'bench', 'gate', 'spike-report'] as const;
|
|
6873
5398
|
|
|
6874
|
-
function
|
|
5399
|
+
function _readPkg(rel: string): {
|
|
6875
5400
|
forgeax?: { metrics?: Record<string, unknown>; smokeInvocation?: string };
|
|
6876
5401
|
name?: string;
|
|
6877
5402
|
scripts?: Record<string, string>;
|
|
@@ -6887,70 +5412,6 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
|
|
|
6887
5412
|
return readFileSync(path, 'utf8');
|
|
6888
5413
|
}
|
|
6889
5414
|
|
|
6890
|
-
describe('M7 hello-hdrp-lighting demo shape (AC-21)', () => {
|
|
6891
|
-
const pkgRel = 'apps/hello/hdrp-lighting/package.json';
|
|
6892
|
-
const srcRel = 'apps/hello/hdrp-lighting/src/main.ts';
|
|
6893
|
-
|
|
6894
|
-
it('package.json exists with @forgeax/hello-hdrp-lighting name', () => {
|
|
6895
|
-
const pkg = readPkg(pkgRel);
|
|
6896
|
-
expect(pkg.name).toBe('@forgeax/hello-hdrp-lighting');
|
|
6897
|
-
});
|
|
6898
|
-
|
|
6899
|
-
it('declares smoke invocation literal aligned with ci.yml', () => {
|
|
6900
|
-
const pkg = readPkg(pkgRel);
|
|
6901
|
-
expect(pkg.forgeax?.smokeInvocation).toBe('pnpm --filter @forgeax/hello-hdrp-lighting smoke');
|
|
6902
|
-
});
|
|
6903
|
-
|
|
6904
|
-
it('declares all 5 forgeax.metrics kinds (plan-strategy §5.6)', () => {
|
|
6905
|
-
const pkg = readPkg(pkgRel);
|
|
6906
|
-
const metrics = pkg.forgeax?.metrics ?? {};
|
|
6907
|
-
for (const kind of FIVE_METRIC_KINDS) {
|
|
6908
|
-
expect(metrics, `metric kind '${kind}' missing`).toHaveProperty(kind);
|
|
6909
|
-
}
|
|
6910
|
-
});
|
|
6911
|
-
|
|
6912
|
-
it('exposes scripts.smoke = node scripts/smoke-dawn.mjs', () => {
|
|
6913
|
-
const pkg = readPkg(pkgRel);
|
|
6914
|
-
expect(pkg.scripts?.smoke).toBe('node scripts/smoke-dawn.mjs');
|
|
6915
|
-
});
|
|
6916
|
-
|
|
6917
|
-
it('src/main.ts wires HDRP install seam (charter P1: installPipeline + HDRP_PIPELINE_ID)', () => {
|
|
6918
|
-
const src = readSource(srcRel);
|
|
6919
|
-
// AC-06: installPipeline + HDRP_PIPELINE_ID + 256-light spawn signal
|
|
6920
|
-
expect(src).toMatch(/installPipeline\s*\(/);
|
|
6921
|
-
expect(src).toMatch(/HDRP_PIPELINE_ID|forgeax::hdrp/);
|
|
6922
|
-
expect(src).toMatch(/PointLight/);
|
|
6923
|
-
});
|
|
6924
|
-
});
|
|
6925
|
-
|
|
6926
|
-
describe('M7 parity-urp-vs-hdrp demo shape (AC-22)', () => {
|
|
6927
|
-
const pkgRel = 'apps/parity/urp-vs-hdrp/package.json';
|
|
6928
|
-
const srcRel = 'apps/parity/urp-vs-hdrp/src/main.ts';
|
|
6929
|
-
|
|
6930
|
-
it('package.json exists with @forgeax/parity-urp-vs-hdrp name', () => {
|
|
6931
|
-
const pkg = readPkg(pkgRel);
|
|
6932
|
-
expect(pkg.name).toBe('@forgeax/parity-urp-vs-hdrp');
|
|
6933
|
-
});
|
|
6934
|
-
|
|
6935
|
-
it('declares all 5 forgeax.metrics kinds (plan-strategy §5.6)', () => {
|
|
6936
|
-
const pkg = readPkg(pkgRel);
|
|
6937
|
-
const metrics = pkg.forgeax?.metrics ?? {};
|
|
6938
|
-
for (const kind of FIVE_METRIC_KINDS) {
|
|
6939
|
-
expect(metrics, `metric kind '${kind}' missing`).toHaveProperty(kind);
|
|
6940
|
-
}
|
|
6941
|
-
});
|
|
6942
|
-
|
|
6943
|
-
it('src/main.ts wires both URP and HDRP install seams (AC-22)', () => {
|
|
6944
|
-
const src = readSource(srcRel);
|
|
6945
|
-
expect(src).toMatch(/URP_PIPELINE_ID|forgeax::urp/);
|
|
6946
|
-
expect(src).toMatch(/HDRP_PIPELINE_ID|forgeax::hdrp/);
|
|
6947
|
-
// Side-by-side comparison: both __captureLeft and __captureRight hooks
|
|
6948
|
-
// mirror the existing parity/forgeax + parity/threejs idiom for the
|
|
6949
|
-
// bench's single-page dual-capture pipeline.
|
|
6950
|
-
expect(src).toMatch(/__captureLeft|__captureRight/);
|
|
6951
|
-
});
|
|
6952
|
-
});
|
|
6953
|
-
|
|
6954
5415
|
describe('M7 pixel-parity bench wires urp-vs-hdrp target', () => {
|
|
6955
5416
|
it('scripts/bench/pixel-parity.mjs has urp-vs-hdrp target marker', () => {
|
|
6956
5417
|
const src = readSource('scripts/bench/pixel-parity.mjs');
|