@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
|
@@ -1,1511 +0,0 @@
|
|
|
1
|
-
// Consolidated by feat-20260608-ci-time-cut M5 w13/w14: merges
|
|
2
|
-
// - bind-group-cache-stable-frame.test.ts
|
|
3
|
-
// - bind-group-cache-view.test.ts
|
|
4
|
-
// - bind-group-cache-entity-key.test.ts
|
|
5
|
-
// - bind-group-cache-instances.test.ts
|
|
6
|
-
// Each original file body sits in its own block-scope so helper names
|
|
7
|
-
// cannot collide; describe() inside still registers globally with vitest.
|
|
8
|
-
|
|
9
|
-
import type { World as WorldType } from '@forgeax/engine-ecs';
|
|
10
|
-
import { World } from '@forgeax/engine-ecs';
|
|
11
|
-
import type { Renderer as RendererType } from '@forgeax/engine-render';
|
|
12
|
-
import type { RenderAssetPort } from '@forgeax/engine-render/internal';
|
|
13
|
-
import {
|
|
14
|
-
type ExtractedFrame,
|
|
15
|
-
extractFrame,
|
|
16
|
-
MeshFilter,
|
|
17
|
-
MeshRenderer,
|
|
18
|
-
prepareExtractContext,
|
|
19
|
-
} from '@forgeax/engine-render/internal';
|
|
20
|
-
import { Transform } from '@forgeax/engine-scene';
|
|
21
|
-
import type { Handle, MaterialAsset, MeshAsset } from '@forgeax/engine-types';
|
|
22
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
23
|
-
import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
24
|
-
import {
|
|
25
|
-
TEST_CUBE_HANDLE,
|
|
26
|
-
TEST_TRIANGLE_HANDLE,
|
|
27
|
-
withPrimitiveMeshes,
|
|
28
|
-
} from './helpers/primitive-world';
|
|
29
|
-
import { createTestRenderAssetOwner } from './helpers/render-asset-port';
|
|
30
|
-
|
|
31
|
-
// ─── from bind-group-cache-stable-frame.test.ts ───
|
|
32
|
-
{
|
|
33
|
-
// bind-group-cache-stable-frame.test.ts -- M5 / w16 (TDD red)
|
|
34
|
-
//
|
|
35
|
-
// AC-03 stable-frame + AC-09 type-inference test.
|
|
36
|
-
//
|
|
37
|
-
// AC-03: minimal stable world (one PBR mesh + camera + directional light),
|
|
38
|
-
// consecutive draw >= 3 frames, assert frame-3 createBindGroupCount == 0
|
|
39
|
-
// (all cache-resident after warm-up).
|
|
40
|
-
//
|
|
41
|
-
// AC-09: at the assertion line, TypeScript infers `number` for
|
|
42
|
-
// `renderer.bindGroupCounts.createBindGroup` without `as` cast.
|
|
43
|
-
//
|
|
44
|
-
// TDD red: this test will fail until w12 (material/instances cache) and
|
|
45
|
-
// w15 (shadow sentinel) are complete. After M4 the stable-frame counter
|
|
46
|
-
// should reach 0 once all four cache categories are hot.
|
|
47
|
-
//
|
|
48
|
-
// Stable-frame definition (requirements AC-03): same World, no
|
|
49
|
-
// spawn/despawn, no material change, no resize, no Skylight change.
|
|
50
|
-
|
|
51
|
-
const ENGINE = '../createRenderer';
|
|
52
|
-
|
|
53
|
-
// ─── Mock helpers (mirrors render-system.test.ts) ──────────────────────────
|
|
54
|
-
|
|
55
|
-
interface MockGL2Context {
|
|
56
|
-
__mockTag: 'webgl2';
|
|
57
|
-
getExtension: () => null;
|
|
58
|
-
getParameter: () => number;
|
|
59
|
-
isContextLost: () => boolean;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function makeMockGL2(): MockGL2Context {
|
|
63
|
-
return {
|
|
64
|
-
__mockTag: 'webgl2',
|
|
65
|
-
getExtension: () => null,
|
|
66
|
-
getParameter: () => 1,
|
|
67
|
-
isContextLost: () => false,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function makeMockCanvas(
|
|
72
|
-
webgl2: 'context' | 'null',
|
|
73
|
-
webgpu: 'context' | 'null',
|
|
74
|
-
): HTMLCanvasElement {
|
|
75
|
-
const canvas = {
|
|
76
|
-
width: 800,
|
|
77
|
-
height: 600,
|
|
78
|
-
getContext(kind: string): unknown {
|
|
79
|
-
if (kind === 'webgl2') {
|
|
80
|
-
return webgl2 === 'context' ? makeMockGL2() : null;
|
|
81
|
-
}
|
|
82
|
-
if (kind === 'webgpu') {
|
|
83
|
-
if (webgpu === 'context') {
|
|
84
|
-
return {
|
|
85
|
-
__mockTag: 'webgpu-canvas-context',
|
|
86
|
-
configure: () => undefined,
|
|
87
|
-
unconfigure: () => undefined,
|
|
88
|
-
getCurrentTexture: () => ({ createView: () => ({}) }),
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
return null;
|
|
94
|
-
},
|
|
95
|
-
addEventListener: () => undefined,
|
|
96
|
-
removeEventListener: () => undefined,
|
|
97
|
-
};
|
|
98
|
-
return canvas as Partial<HTMLCanvasElement> as HTMLCanvasElement;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function makeMockGPUDevice(): { device: unknown } {
|
|
102
|
-
const lost = new Promise<unknown>(() => undefined);
|
|
103
|
-
const device = {
|
|
104
|
-
__mockTag: 'gpu-device',
|
|
105
|
-
lost,
|
|
106
|
-
features: new Set(),
|
|
107
|
-
limits: {},
|
|
108
|
-
queue: {
|
|
109
|
-
submit: () => undefined,
|
|
110
|
-
writeBuffer: () => undefined,
|
|
111
|
-
writeTexture: () => undefined,
|
|
112
|
-
},
|
|
113
|
-
createShaderModule: () => ({ getCompilationInfo: async () => ({ messages: [] }) }),
|
|
114
|
-
createBindGroupLayout: () => ({}),
|
|
115
|
-
createPipelineLayout: () => ({}),
|
|
116
|
-
createRenderPipeline: () => ({}),
|
|
117
|
-
createBindGroup: () => ({}),
|
|
118
|
-
createBuffer: () => ({
|
|
119
|
-
getMappedRange: () => new ArrayBuffer(64),
|
|
120
|
-
unmap: () => undefined,
|
|
121
|
-
}),
|
|
122
|
-
createCommandEncoder: () => ({
|
|
123
|
-
beginRenderPass: () => ({
|
|
124
|
-
setPipeline: () => undefined,
|
|
125
|
-
setVertexBuffer: () => undefined,
|
|
126
|
-
setIndexBuffer: () => undefined,
|
|
127
|
-
setBindGroup: () => undefined,
|
|
128
|
-
draw: () => undefined,
|
|
129
|
-
drawIndexed: () => undefined,
|
|
130
|
-
end: () => undefined,
|
|
131
|
-
}),
|
|
132
|
-
finish: () => ({}),
|
|
133
|
-
}),
|
|
134
|
-
createTexture: () => ({ createView: () => ({}) }),
|
|
135
|
-
createSampler: () => ({}),
|
|
136
|
-
destroy: () => undefined,
|
|
137
|
-
};
|
|
138
|
-
return { device };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function makeMockGPU(deviceObj: unknown): unknown {
|
|
142
|
-
return {
|
|
143
|
-
requestAdapter: async () => ({
|
|
144
|
-
requestDevice: async () => deviceObj,
|
|
145
|
-
}),
|
|
146
|
-
getPreferredCanvasFormat: () => 'bgra8unorm',
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const baseNavigator: Navigator = {
|
|
151
|
-
userAgent: 'mock-engine-test',
|
|
152
|
-
} as Partial<Navigator> as Navigator;
|
|
153
|
-
|
|
154
|
-
function buildManifestDataUrl(): string {
|
|
155
|
-
const materialShaderStub = (identifier: string) => ({
|
|
156
|
-
identifier,
|
|
157
|
-
sourcePath: `${identifier}.wgsl`,
|
|
158
|
-
composedWgsl: '/* stub */',
|
|
159
|
-
paramSchema: '[]',
|
|
160
|
-
variants: [],
|
|
161
|
-
});
|
|
162
|
-
const manifest = {
|
|
163
|
-
schemaVersion: '1.0.0',
|
|
164
|
-
entries: [
|
|
165
|
-
{ hash: 'pbr00000', wgsl: '/* pbr stub - calls f_schlick( */', glsl: '', bindings: '' },
|
|
166
|
-
{ hash: 'unlit000', wgsl: '/* unlit stub */', glsl: '', bindings: '' },
|
|
167
|
-
{
|
|
168
|
-
hash: 'tonemap0',
|
|
169
|
-
wgsl: '/* tonemap stub - struct TonemapParams { exposure: f32 }; */',
|
|
170
|
-
glsl: '',
|
|
171
|
-
bindings: '',
|
|
172
|
-
},
|
|
173
|
-
],
|
|
174
|
-
materialShaders: [
|
|
175
|
-
materialShaderStub('forgeax::default-standard-pbr'),
|
|
176
|
-
materialShaderStub('forgeax::default-unlit'),
|
|
177
|
-
],
|
|
178
|
-
};
|
|
179
|
-
return `data:application/json,${encodeURIComponent(JSON.stringify(manifest))}`;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// ─── Type-level imports (mirrors render-system.test.ts) ────────────────────
|
|
183
|
-
|
|
184
|
-
async function importEngine(): Promise<{
|
|
185
|
-
createRenderer: (
|
|
186
|
-
canvas: unknown,
|
|
187
|
-
opts?: unknown,
|
|
188
|
-
bundler?: unknown,
|
|
189
|
-
) => Promise<{
|
|
190
|
-
backend: string;
|
|
191
|
-
ready: Promise<unknown>;
|
|
192
|
-
draw: (worlds: unknown, opts: { cameraOwner: number; resourceOwner: number }) => unknown;
|
|
193
|
-
onError: (cb: (err: { code: string; detail?: unknown; hint?: string }) => void) => () => void;
|
|
194
|
-
assets: {
|
|
195
|
-
register: (asset: unknown) => { ok: boolean; value: unknown };
|
|
196
|
-
};
|
|
197
|
-
bindGroupCounts: { readonly createBindGroup: number };
|
|
198
|
-
}>;
|
|
199
|
-
}> {
|
|
200
|
-
return (await import(ENGINE)) as never;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
async function importEcs(): Promise<{
|
|
204
|
-
World: new () => {
|
|
205
|
-
spawn: (...componentDatas: unknown[]) => unknown;
|
|
206
|
-
update: () => void;
|
|
207
|
-
};
|
|
208
|
-
}> {
|
|
209
|
-
const { World: BaseWorld } = await import('@forgeax/engine-ecs');
|
|
210
|
-
return { World: withPrimitiveMeshes(BaseWorld) } as never;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Import runtime components via the package index to get real types
|
|
214
|
-
// (Camera, Transform, MeshFilter, MeshRenderer, DirectionalLight, etc.).
|
|
215
|
-
async function importComponents(): Promise<{
|
|
216
|
-
Transform: unknown;
|
|
217
|
-
MeshFilter: unknown;
|
|
218
|
-
MeshRenderer: unknown;
|
|
219
|
-
Camera: unknown;
|
|
220
|
-
DirectionalLight: unknown;
|
|
221
|
-
HANDLE_CUBE: Handle<'MeshAsset', 'shared'>;
|
|
222
|
-
}> {
|
|
223
|
-
return {
|
|
224
|
-
...(await import('@forgeax/engine-render/internal')),
|
|
225
|
-
...(await import('@forgeax/engine-scene')),
|
|
226
|
-
HANDLE_CUBE: TEST_CUBE_HANDLE,
|
|
227
|
-
} as never;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function cameraTransform() {
|
|
231
|
-
return {
|
|
232
|
-
pos: [0, 0, 5],
|
|
233
|
-
quat: [0, 0, 0, 1],
|
|
234
|
-
scale: [1, 1, 1],
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// ─── Helper: build a minimal stable world ──────────────────────────────────
|
|
239
|
-
|
|
240
|
-
async function buildStableWorld() {
|
|
241
|
-
const C = await importComponents();
|
|
242
|
-
|
|
243
|
-
const { World } = await importEcs();
|
|
244
|
-
const world = new World();
|
|
245
|
-
|
|
246
|
-
// Camera at z=5 looking at origin
|
|
247
|
-
world.spawn(
|
|
248
|
-
{
|
|
249
|
-
component: C.Camera,
|
|
250
|
-
data: {
|
|
251
|
-
fov: Math.PI / 4,
|
|
252
|
-
aspect: 16 / 9,
|
|
253
|
-
near: 0.1,
|
|
254
|
-
far: 100,
|
|
255
|
-
projection: 0,
|
|
256
|
-
left: -1,
|
|
257
|
-
right: 1,
|
|
258
|
-
bottom: -1,
|
|
259
|
-
top: 1,
|
|
260
|
-
},
|
|
261
|
-
},
|
|
262
|
-
{ component: C.Transform, data: cameraTransform() },
|
|
263
|
-
);
|
|
264
|
-
|
|
265
|
-
// Directional light
|
|
266
|
-
world.spawn(
|
|
267
|
-
{ component: C.DirectionalLight, data: {} },
|
|
268
|
-
{ component: C.Transform, data: cameraTransform() },
|
|
269
|
-
);
|
|
270
|
-
|
|
271
|
-
// PBR mesh (cube) with explicit PBR material
|
|
272
|
-
world.spawn(
|
|
273
|
-
{ component: C.MeshRenderer, data: { materials: [0] } },
|
|
274
|
-
{ component: C.MeshFilter, data: { assetHandle: C.HANDLE_CUBE } },
|
|
275
|
-
{
|
|
276
|
-
component: C.Transform,
|
|
277
|
-
data: {
|
|
278
|
-
pos: [0, 0, 0],
|
|
279
|
-
quat: [0, 0, 0, 1],
|
|
280
|
-
scale: [1, 1, 1],
|
|
281
|
-
},
|
|
282
|
-
},
|
|
283
|
-
);
|
|
284
|
-
|
|
285
|
-
return world;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// ─── Tests ────────────────────────────────────────────────────────────────
|
|
289
|
-
|
|
290
|
-
describe('w16 — AC-03 stable-frame + AC-09 type inference', () => {
|
|
291
|
-
beforeEach(() => {
|
|
292
|
-
vi.stubGlobal('navigator', baseNavigator);
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
afterEach(() => {
|
|
296
|
-
vi.unstubAllGlobals();
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
it('AC-03: stable-frame draw >= 3 frames, createBindGroupCount reaches 0 on frame 3', async () => {
|
|
300
|
-
const { device } = makeMockGPUDevice();
|
|
301
|
-
vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
|
|
302
|
-
|
|
303
|
-
const engine = await importEngine();
|
|
304
|
-
const canvas = makeMockCanvas('context', 'context');
|
|
305
|
-
const renderer = await engine.createRenderer(
|
|
306
|
-
canvas,
|
|
307
|
-
{},
|
|
308
|
-
{ shaderManifestUrl: buildManifestDataUrl() },
|
|
309
|
-
);
|
|
310
|
-
await (renderer.ready as Promise<void>);
|
|
311
|
-
|
|
312
|
-
const world = await buildStableWorld();
|
|
313
|
-
|
|
314
|
-
// Draw 3 frames in a stable scene (no spawn/despawn/material-change/resize/skylight-change).
|
|
315
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
316
|
-
throw new Error('World attachment failed');
|
|
317
|
-
}
|
|
318
|
-
(world as WorldType).update().unwrap();
|
|
319
|
-
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
320
|
-
const countFrame1 = renderer.bindGroupCounts.createBindGroup;
|
|
321
|
-
|
|
322
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
323
|
-
throw new Error('World attachment failed');
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
(world as WorldType).update().unwrap();
|
|
327
|
-
|
|
328
|
-
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
329
|
-
const countFrame2 = renderer.bindGroupCounts.createBindGroup;
|
|
330
|
-
|
|
331
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
332
|
-
throw new Error('World attachment failed');
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
(world as WorldType).update().unwrap();
|
|
336
|
-
|
|
337
|
-
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
338
|
-
const countFrame3 = renderer.bindGroupCounts.createBindGroup;
|
|
339
|
-
|
|
340
|
-
// Type-level assertions — all counters must be numbers (AC-09 gating:
|
|
341
|
-
// each line infers `number` with no `as` cast).
|
|
342
|
-
const _typeCheck1: number = countFrame1;
|
|
343
|
-
const _typeCheck2: number = countFrame2;
|
|
344
|
-
const _typeCheck3: number = countFrame3;
|
|
345
|
-
void _typeCheck1;
|
|
346
|
-
void _typeCheck2;
|
|
347
|
-
void _typeCheck3;
|
|
348
|
-
|
|
349
|
-
// Frame 1: cold cache, counter reflects initial createBindGroup calls.
|
|
350
|
-
expect(typeof countFrame1).toBe('number');
|
|
351
|
-
|
|
352
|
-
// After caching warms up, subsequent frames should converge toward 0.
|
|
353
|
-
// AC-03: frame 3 counter must be 0 — all bind groups cache-resident.
|
|
354
|
-
expect(countFrame3).toBe(0);
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
it('AC-09: TypeScript infers createBindGroup as number (no as cast)', async () => {
|
|
358
|
-
const { device } = makeMockGPUDevice();
|
|
359
|
-
vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
|
|
360
|
-
|
|
361
|
-
const engine = await importEngine();
|
|
362
|
-
const canvas = makeMockCanvas('context', 'context');
|
|
363
|
-
const renderer = await engine.createRenderer(
|
|
364
|
-
canvas,
|
|
365
|
-
{},
|
|
366
|
-
{ shaderManifestUrl: buildManifestDataUrl() },
|
|
367
|
-
);
|
|
368
|
-
await (renderer.ready as Promise<void>);
|
|
369
|
-
|
|
370
|
-
const world = await buildStableWorld();
|
|
371
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
372
|
-
throw new Error('World attachment failed');
|
|
373
|
-
}
|
|
374
|
-
(world as WorldType).update().unwrap();
|
|
375
|
-
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
376
|
-
|
|
377
|
-
// AC-09 application point: this access line is where TypeScript must
|
|
378
|
-
// infer `number` without requiring `as` or any type assertion.
|
|
379
|
-
// The type annotation `const c: number` is a compile-time proof:
|
|
380
|
-
// if `bindGroupCounts.createBindGroup` were `unknown` or wider, this
|
|
381
|
-
// line would fail typecheck.
|
|
382
|
-
const c: number = renderer.bindGroupCounts.createBindGroup;
|
|
383
|
-
expect(typeof c).toBe('number');
|
|
384
|
-
|
|
385
|
-
// Redundant explicit annotation variant: infer via assignment context.
|
|
386
|
-
// If TS infers anything other than `number`, the next line fails.
|
|
387
|
-
let inferred = renderer.bindGroupCounts.createBindGroup;
|
|
388
|
-
inferred = 42; // must compile — proves inferred type is number-compatible
|
|
389
|
-
expect(inferred).toBe(42);
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
it('AC-03: counter stays at 0 on consecutive stable frames beyond frame 3', async () => {
|
|
393
|
-
const { device } = makeMockGPUDevice();
|
|
394
|
-
vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
|
|
395
|
-
|
|
396
|
-
const engine = await importEngine();
|
|
397
|
-
const canvas = makeMockCanvas('context', 'context');
|
|
398
|
-
const renderer = await engine.createRenderer(
|
|
399
|
-
canvas,
|
|
400
|
-
{},
|
|
401
|
-
{ shaderManifestUrl: buildManifestDataUrl() },
|
|
402
|
-
);
|
|
403
|
-
await (renderer.ready as Promise<void>);
|
|
404
|
-
|
|
405
|
-
const world = await buildStableWorld();
|
|
406
|
-
|
|
407
|
-
// Warm up 3 frames.
|
|
408
|
-
for (let i = 0; i < 3; i++) {
|
|
409
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
410
|
-
throw new Error('World attachment failed');
|
|
411
|
-
}
|
|
412
|
-
(world as WorldType).update().unwrap();
|
|
413
|
-
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// Frames 4 and 5 must both report 0 — cache is hot.
|
|
417
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
418
|
-
throw new Error('World attachment failed');
|
|
419
|
-
}
|
|
420
|
-
(world as WorldType).update().unwrap();
|
|
421
|
-
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
422
|
-
expect(renderer.bindGroupCounts.createBindGroup).toBe(0);
|
|
423
|
-
|
|
424
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
425
|
-
throw new Error('World attachment failed');
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
(world as WorldType).update().unwrap();
|
|
429
|
-
|
|
430
|
-
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
431
|
-
expect(renderer.bindGroupCounts.createBindGroup).toBe(0);
|
|
432
|
-
});
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// ─── from bind-group-cache-view.test.ts ───
|
|
437
|
-
{
|
|
438
|
-
// bind-group-cache-view.test.ts -- M2 / w5 (TDD red)
|
|
439
|
-
//
|
|
440
|
-
// View bind group cache hit/miss + variant isolation unit tests.
|
|
441
|
-
// Anchors: requirements AC-01 (view into cache), AC-06 (main vs shadow
|
|
442
|
-
// variant keys distinct, no cross-variant contamination).
|
|
443
|
-
//
|
|
444
|
-
// Scenarios:
|
|
445
|
-
// (a) stable scene, >= 2 draws => view main cache hit (counter not bumped by view BG)
|
|
446
|
-
// (b) resize (view UBO realloc => handle change) => cache miss -> rebuild -> hit
|
|
447
|
-
// (c) AC-06: main vs shadow variant keys distinct, each variant caches independently;
|
|
448
|
-
// both variants present when scene has shadow (castShadow:true on DirectionalLight)
|
|
449
|
-
//
|
|
450
|
-
// TDD red: cache Maps + helper do not exist yet; view/mesh createBindGroup calls
|
|
451
|
-
// are not wired through cache. Tests will fail when asserting cache-hit counter
|
|
452
|
-
// reduction until w7 + w8 wire the cache.
|
|
453
|
-
//
|
|
454
|
-
// View main (#1) entries: b0 viewUniformBuffer, b1 pointLightsBuffer,
|
|
455
|
-
// b2 spotLightsBuffer, b3 graph shadowDepth view | shadowFallbackTextureView,
|
|
456
|
-
// b4 shadowSampler.
|
|
457
|
-
// View shadow (#3) entries: same b0-b2, b3 = shadowFallbackTextureView (fixed),
|
|
458
|
-
// b4 = shadowSampler.
|
|
459
|
-
|
|
460
|
-
const ENGINE = '../createRenderer';
|
|
461
|
-
|
|
462
|
-
// ─── Mock helpers ──────────────────────────────────────────────────────────
|
|
463
|
-
|
|
464
|
-
interface MockGL2Context {
|
|
465
|
-
__mockTag: 'webgl2';
|
|
466
|
-
getExtension: () => null;
|
|
467
|
-
getParameter: () => number;
|
|
468
|
-
isContextLost: () => boolean;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
function makeMockGL2(): MockGL2Context {
|
|
472
|
-
return {
|
|
473
|
-
__mockTag: 'webgl2',
|
|
474
|
-
getExtension: () => null,
|
|
475
|
-
getParameter: () => 1,
|
|
476
|
-
isContextLost: () => false,
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
function makeMockCanvas(
|
|
481
|
-
webgl2: 'context' | 'null',
|
|
482
|
-
webgpu: 'context' | 'null',
|
|
483
|
-
): HTMLCanvasElement {
|
|
484
|
-
const canvas = {
|
|
485
|
-
width: 800,
|
|
486
|
-
height: 600,
|
|
487
|
-
getContext(kind: string): unknown {
|
|
488
|
-
if (kind === 'webgl2') {
|
|
489
|
-
return webgl2 === 'context' ? makeMockGL2() : null;
|
|
490
|
-
}
|
|
491
|
-
if (kind === 'webgpu') {
|
|
492
|
-
if (webgpu === 'context') {
|
|
493
|
-
return {
|
|
494
|
-
__mockTag: 'webgpu-canvas-context',
|
|
495
|
-
configure: () => undefined,
|
|
496
|
-
unconfigure: () => undefined,
|
|
497
|
-
getCurrentTexture: () => ({ createView: () => ({}) }),
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
|
-
return null;
|
|
501
|
-
}
|
|
502
|
-
return null;
|
|
503
|
-
},
|
|
504
|
-
addEventListener: () => undefined,
|
|
505
|
-
removeEventListener: () => undefined,
|
|
506
|
-
};
|
|
507
|
-
return canvas as Partial<HTMLCanvasElement> as HTMLCanvasElement;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
function makeMockGPUDevice(): { device: unknown } {
|
|
511
|
-
const lost = new Promise<unknown>(() => undefined);
|
|
512
|
-
const device = {
|
|
513
|
-
__mockTag: 'gpu-device',
|
|
514
|
-
lost,
|
|
515
|
-
features: new Set(),
|
|
516
|
-
limits: {},
|
|
517
|
-
queue: {
|
|
518
|
-
submit: () => undefined,
|
|
519
|
-
writeBuffer: () => undefined,
|
|
520
|
-
writeTexture: () => undefined,
|
|
521
|
-
},
|
|
522
|
-
createShaderModule: () => ({ getCompilationInfo: async () => ({ messages: [] }) }),
|
|
523
|
-
createBindGroupLayout: () => ({}),
|
|
524
|
-
createPipelineLayout: () => ({}),
|
|
525
|
-
createRenderPipeline: () => ({}),
|
|
526
|
-
createBindGroup: () => ({}),
|
|
527
|
-
createBuffer: () => ({
|
|
528
|
-
getMappedRange: () => new ArrayBuffer(64),
|
|
529
|
-
unmap: () => undefined,
|
|
530
|
-
}),
|
|
531
|
-
createCommandEncoder: () => ({
|
|
532
|
-
beginRenderPass: () => ({
|
|
533
|
-
setPipeline: () => undefined,
|
|
534
|
-
setVertexBuffer: () => undefined,
|
|
535
|
-
setIndexBuffer: () => undefined,
|
|
536
|
-
setBindGroup: () => undefined,
|
|
537
|
-
draw: () => undefined,
|
|
538
|
-
drawIndexed: () => undefined,
|
|
539
|
-
end: () => undefined,
|
|
540
|
-
}),
|
|
541
|
-
finish: () => ({}),
|
|
542
|
-
}),
|
|
543
|
-
createTexture: () => ({ createView: () => ({}) }),
|
|
544
|
-
createSampler: () => ({}),
|
|
545
|
-
destroy: () => undefined,
|
|
546
|
-
};
|
|
547
|
-
return { device };
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
function makeMockGPU(deviceObj: unknown): unknown {
|
|
551
|
-
return {
|
|
552
|
-
requestAdapter: async () => ({
|
|
553
|
-
requestDevice: async () => deviceObj,
|
|
554
|
-
}),
|
|
555
|
-
getPreferredCanvasFormat: () => 'bgra8unorm',
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
const baseNavigator: Navigator = {
|
|
560
|
-
userAgent: 'mock-engine-test',
|
|
561
|
-
} as Partial<Navigator> as Navigator;
|
|
562
|
-
|
|
563
|
-
function buildManifestDataUrl(): string {
|
|
564
|
-
const materialShaderStub = (identifier: string) => ({
|
|
565
|
-
identifier,
|
|
566
|
-
sourcePath: `${identifier}.wgsl`,
|
|
567
|
-
composedWgsl: '/* stub */',
|
|
568
|
-
paramSchema: '[]',
|
|
569
|
-
variants: [],
|
|
570
|
-
});
|
|
571
|
-
const manifest = {
|
|
572
|
-
schemaVersion: '1.0.0',
|
|
573
|
-
entries: [
|
|
574
|
-
{ hash: 'pbr00000', wgsl: '/* pbr stub - calls f_schlick( */', glsl: '', bindings: '' },
|
|
575
|
-
{ hash: 'unlit000', wgsl: '/* unlit stub */', glsl: '', bindings: '' },
|
|
576
|
-
{
|
|
577
|
-
hash: 'tonemap0',
|
|
578
|
-
wgsl: '/* tonemap stub - struct TonemapParams { exposure: f32 }; */',
|
|
579
|
-
glsl: '',
|
|
580
|
-
bindings: '',
|
|
581
|
-
},
|
|
582
|
-
],
|
|
583
|
-
materialShaders: [
|
|
584
|
-
materialShaderStub('forgeax::default-standard-pbr'),
|
|
585
|
-
materialShaderStub('forgeax::default-unlit'),
|
|
586
|
-
],
|
|
587
|
-
};
|
|
588
|
-
return `data:application/json,${encodeURIComponent(JSON.stringify(manifest))}`;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
async function importEngine(): Promise<{
|
|
592
|
-
createRenderer: (
|
|
593
|
-
canvas: unknown,
|
|
594
|
-
opts?: unknown,
|
|
595
|
-
bundler?: unknown,
|
|
596
|
-
) => Promise<{
|
|
597
|
-
backend: string;
|
|
598
|
-
ready: Promise<void>;
|
|
599
|
-
draw: (worlds: unknown, opts: { cameraOwner: number; resourceOwner: number }) => void;
|
|
600
|
-
onError: (cb: (err: { code: string; detail?: unknown; hint?: string }) => void) => () => void;
|
|
601
|
-
assets: {
|
|
602
|
-
register: (asset: unknown) => { ok: boolean; value: unknown };
|
|
603
|
-
};
|
|
604
|
-
}>;
|
|
605
|
-
}> {
|
|
606
|
-
return (await import(ENGINE)) as never;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
async function importEcs(): Promise<{
|
|
610
|
-
World: new () => {
|
|
611
|
-
spawn: (...componentDatas: unknown[]) => unknown;
|
|
612
|
-
update: () => void;
|
|
613
|
-
};
|
|
614
|
-
}> {
|
|
615
|
-
const { World: BaseWorld } = await import('@forgeax/engine-ecs');
|
|
616
|
-
return { World: withPrimitiveMeshes(BaseWorld) } as never;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
async function importComponents(): Promise<{
|
|
620
|
-
Transform: unknown;
|
|
621
|
-
MeshFilter: unknown;
|
|
622
|
-
MeshRenderer: unknown;
|
|
623
|
-
Camera: unknown;
|
|
624
|
-
DirectionalLight: unknown;
|
|
625
|
-
HANDLE_CUBE: Handle<'MeshAsset', 'shared'>;
|
|
626
|
-
HANDLE_TRIANGLE: Handle<'MeshAsset', 'shared'>;
|
|
627
|
-
}> {
|
|
628
|
-
return {
|
|
629
|
-
...(await import('@forgeax/engine-render/internal')),
|
|
630
|
-
...(await import('@forgeax/engine-scene')),
|
|
631
|
-
HANDLE_CUBE: TEST_CUBE_HANDLE,
|
|
632
|
-
HANDLE_TRIANGLE: TEST_TRIANGLE_HANDLE,
|
|
633
|
-
} as never;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
function cameraTransform() {
|
|
637
|
-
return {
|
|
638
|
-
pos: [0, 0, 5],
|
|
639
|
-
quat: [0, 0, 0, 1],
|
|
640
|
-
scale: [1, 1, 1],
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
async function setupWebGPU(): Promise<{
|
|
645
|
-
createRenderer: (
|
|
646
|
-
canvas: unknown,
|
|
647
|
-
opts?: unknown,
|
|
648
|
-
bundler?: unknown,
|
|
649
|
-
) => Promise<{
|
|
650
|
-
backend: string;
|
|
651
|
-
ready: Promise<void>;
|
|
652
|
-
draw: (worlds: unknown, opts: { cameraOwner: number; resourceOwner: number }) => void;
|
|
653
|
-
onError: (cb: (err: { code: string; detail?: unknown; hint?: string }) => void) => () => void;
|
|
654
|
-
assets: { register: (asset: unknown) => { ok: boolean; value: unknown } };
|
|
655
|
-
}>;
|
|
656
|
-
}> {
|
|
657
|
-
const { device } = makeMockGPUDevice();
|
|
658
|
-
vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
|
|
659
|
-
const engine = await importEngine();
|
|
660
|
-
return { createRenderer: engine.createRenderer };
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
664
|
-
|
|
665
|
-
interface RendererForTest {
|
|
666
|
-
bindGroupCounts?: { readonly createBindGroup: number };
|
|
667
|
-
draw: (worlds: unknown, opts: { cameraOwner: number; resourceOwner: number }) => void;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
function spawnBasicScene(
|
|
671
|
-
world: unknown,
|
|
672
|
-
C: {
|
|
673
|
-
Camera: unknown;
|
|
674
|
-
Transform: unknown;
|
|
675
|
-
MeshFilter: unknown;
|
|
676
|
-
MeshRenderer: unknown;
|
|
677
|
-
DirectionalLight: unknown;
|
|
678
|
-
HANDLE_CUBE: Handle<'MeshAsset', 'shared'>;
|
|
679
|
-
},
|
|
680
|
-
options?: { withShadow?: boolean },
|
|
681
|
-
) {
|
|
682
|
-
const w = world as {
|
|
683
|
-
spawn: (...args: unknown[]) => unknown;
|
|
684
|
-
};
|
|
685
|
-
// withShadow gated on castShadow (now merged into DirectionalLight)
|
|
686
|
-
|
|
687
|
-
w.spawn(
|
|
688
|
-
{
|
|
689
|
-
component: C.Camera,
|
|
690
|
-
data: {
|
|
691
|
-
fov: Math.PI / 4,
|
|
692
|
-
aspect: 16 / 9,
|
|
693
|
-
near: 0.1,
|
|
694
|
-
far: 100,
|
|
695
|
-
projection: 0,
|
|
696
|
-
left: -1,
|
|
697
|
-
right: 1,
|
|
698
|
-
bottom: -1,
|
|
699
|
-
top: 1,
|
|
700
|
-
},
|
|
701
|
-
},
|
|
702
|
-
{ component: C.Transform, data: cameraTransform() },
|
|
703
|
-
);
|
|
704
|
-
w.spawn(
|
|
705
|
-
{
|
|
706
|
-
component: C.DirectionalLight,
|
|
707
|
-
data: { castShadow: options?.withShadow === true },
|
|
708
|
-
},
|
|
709
|
-
{ component: C.Transform, data: cameraTransform() },
|
|
710
|
-
);
|
|
711
|
-
w.spawn(
|
|
712
|
-
{ component: C.MeshRenderer, data: { materials: [0] } },
|
|
713
|
-
{ component: C.MeshFilter, data: { assetHandle: C.HANDLE_CUBE } },
|
|
714
|
-
{
|
|
715
|
-
component: C.Transform,
|
|
716
|
-
data: {
|
|
717
|
-
pos: [0, 0, 0],
|
|
718
|
-
quat: [0, 0, 0, 1],
|
|
719
|
-
scale: [1, 1, 1],
|
|
720
|
-
},
|
|
721
|
-
},
|
|
722
|
-
);
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
// ─── Tests ─────────────────────────────────────────────────────────────────
|
|
726
|
-
|
|
727
|
-
describe('w5 — view bind group cache hit/miss + variant isolation', () => {
|
|
728
|
-
beforeEach(() => {
|
|
729
|
-
vi.stubGlobal('navigator', baseNavigator);
|
|
730
|
-
});
|
|
731
|
-
|
|
732
|
-
afterEach(() => {
|
|
733
|
-
vi.unstubAllGlobals();
|
|
734
|
-
});
|
|
735
|
-
|
|
736
|
-
it('(a) stable scene >= 2 draws => view main cache hit (counter not bumped by view BG)', async () => {
|
|
737
|
-
const { createRenderer } = await setupWebGPU();
|
|
738
|
-
const canvas = makeMockCanvas('context', 'context');
|
|
739
|
-
const renderer = await createRenderer(
|
|
740
|
-
canvas,
|
|
741
|
-
{},
|
|
742
|
-
{ shaderManifestUrl: buildManifestDataUrl() },
|
|
743
|
-
);
|
|
744
|
-
await renderer.ready;
|
|
745
|
-
|
|
746
|
-
const { World } = await importEcs();
|
|
747
|
-
const C = await importComponents();
|
|
748
|
-
const world = new World();
|
|
749
|
-
spawnBasicScene(world, C);
|
|
750
|
-
|
|
751
|
-
const rs = renderer as unknown as RendererForTest;
|
|
752
|
-
const draw = renderer.draw.bind(renderer) as (
|
|
753
|
-
w: unknown,
|
|
754
|
-
o: { cameraOwner: number; resourceOwner: number },
|
|
755
|
-
) => void;
|
|
756
|
-
|
|
757
|
-
// Frame 1: all cache misses (cold start) => counter > 0
|
|
758
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
759
|
-
throw new Error('World attachment failed');
|
|
760
|
-
}
|
|
761
|
-
(world as WorldType).update().unwrap();
|
|
762
|
-
draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
763
|
-
const countFrame1 = rs.bindGroupCounts?.createBindGroup ?? -1;
|
|
764
|
-
expect(countFrame1).toBeGreaterThan(0);
|
|
765
|
-
|
|
766
|
-
// Frame 2: view cache should hit (same handles) => counter < frame 1
|
|
767
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
768
|
-
throw new Error('World attachment failed');
|
|
769
|
-
}
|
|
770
|
-
(world as WorldType).update().unwrap();
|
|
771
|
-
draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
772
|
-
const countFrame2 = rs.bindGroupCounts?.createBindGroup ?? -1;
|
|
773
|
-
expect(countFrame2).toBeLessThan(countFrame1);
|
|
774
|
-
});
|
|
775
|
-
|
|
776
|
-
it('(b) cold-frame createBindGroup count > 0; hot frame count decreases (cache hit)', async () => {
|
|
777
|
-
const { createRenderer } = await setupWebGPU();
|
|
778
|
-
const canvas = makeMockCanvas('context', 'context');
|
|
779
|
-
const renderer = await createRenderer(
|
|
780
|
-
canvas,
|
|
781
|
-
{},
|
|
782
|
-
{ shaderManifestUrl: buildManifestDataUrl() },
|
|
783
|
-
);
|
|
784
|
-
await renderer.ready;
|
|
785
|
-
|
|
786
|
-
const { World } = await importEcs();
|
|
787
|
-
const C = await importComponents();
|
|
788
|
-
const world = new World();
|
|
789
|
-
spawnBasicScene(world, C);
|
|
790
|
-
|
|
791
|
-
const rs = renderer as unknown as RendererForTest;
|
|
792
|
-
const draw = renderer.draw.bind(renderer) as (
|
|
793
|
-
w: unknown,
|
|
794
|
-
o: { cameraOwner: number; resourceOwner: number },
|
|
795
|
-
) => void;
|
|
796
|
-
|
|
797
|
-
// Frame 1: cold cache — createBindGroup calls > 0
|
|
798
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
799
|
-
throw new Error('World attachment failed');
|
|
800
|
-
}
|
|
801
|
-
(world as WorldType).update().unwrap();
|
|
802
|
-
draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
803
|
-
const coldCount = rs.bindGroupCounts?.createBindGroup ?? -1;
|
|
804
|
-
expect(coldCount).toBeGreaterThan(0);
|
|
805
|
-
|
|
806
|
-
// Frame 2: view + mesh cache hits reduce counter
|
|
807
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
808
|
-
throw new Error('World attachment failed');
|
|
809
|
-
}
|
|
810
|
-
(world as WorldType).update().unwrap();
|
|
811
|
-
draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
812
|
-
const hotCount = rs.bindGroupCounts?.createBindGroup ?? -1;
|
|
813
|
-
expect(hotCount).toBeLessThan(coldCount);
|
|
814
|
-
|
|
815
|
-
// Frame 3: stable scene, further cache hits (approach 0 as M3/M4 come)
|
|
816
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
817
|
-
throw new Error('World attachment failed');
|
|
818
|
-
}
|
|
819
|
-
(world as WorldType).update().unwrap();
|
|
820
|
-
draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
821
|
-
const stableCount = rs.bindGroupCounts?.createBindGroup ?? -1;
|
|
822
|
-
expect(stableCount).toBeLessThanOrEqual(hotCount);
|
|
823
|
-
});
|
|
824
|
-
|
|
825
|
-
it('(c) AC-06: main vs shadow variant keys distinct, each variant caches independently', async () => {
|
|
826
|
-
const { createRenderer } = await setupWebGPU();
|
|
827
|
-
const canvas = makeMockCanvas('context', 'context');
|
|
828
|
-
const renderer = await createRenderer(
|
|
829
|
-
canvas,
|
|
830
|
-
{},
|
|
831
|
-
{ shaderManifestUrl: buildManifestDataUrl() },
|
|
832
|
-
);
|
|
833
|
-
await renderer.ready;
|
|
834
|
-
|
|
835
|
-
const { World } = await importEcs();
|
|
836
|
-
const C = await importComponents();
|
|
837
|
-
const world = new World();
|
|
838
|
-
// Spawn scene with directional light shadow so shadow pass runs
|
|
839
|
-
spawnBasicScene(world, C, { withShadow: true });
|
|
840
|
-
|
|
841
|
-
const rs = renderer as unknown as RendererForTest;
|
|
842
|
-
const draw = renderer.draw.bind(renderer) as (
|
|
843
|
-
w: unknown,
|
|
844
|
-
o: { cameraOwner: number; resourceOwner: number },
|
|
845
|
-
) => void;
|
|
846
|
-
|
|
847
|
-
// Frame 1: cold start — view main + view shadow both cache miss
|
|
848
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
849
|
-
throw new Error('World attachment failed');
|
|
850
|
-
}
|
|
851
|
-
(world as WorldType).update().unwrap();
|
|
852
|
-
draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
853
|
-
const countFrame1 = rs.bindGroupCounts?.createBindGroup ?? -1;
|
|
854
|
-
expect(countFrame1).toBeGreaterThan(0);
|
|
855
|
-
|
|
856
|
-
// Frame 2: both view variants hit (same handles, same scene)
|
|
857
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
858
|
-
throw new Error('World attachment failed');
|
|
859
|
-
}
|
|
860
|
-
(world as WorldType).update().unwrap();
|
|
861
|
-
draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
862
|
-
const countFrame2 = rs.bindGroupCounts?.createBindGroup ?? -1;
|
|
863
|
-
// Both view variants cached independently => counter lower than frame 1
|
|
864
|
-
expect(countFrame2).toBeLessThan(countFrame1);
|
|
865
|
-
});
|
|
866
|
-
});
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
// ─── from bind-group-cache-entity-key.test.ts ───
|
|
870
|
-
{
|
|
871
|
-
// bind-group-cache-entity-key.test.ts -- M1 / w1 (TDD red)
|
|
872
|
-
//
|
|
873
|
-
// entityKey cross-frame stability unit tests for RenderableSnapshot.entityKey.
|
|
874
|
-
// Anchors: requirements AC-04 (entity-key-based fine-grain invalidation)
|
|
875
|
-
// and AC-07 (per-entity clean-up depends on stable identity).
|
|
876
|
-
//
|
|
877
|
-
// Three scenarios:
|
|
878
|
-
// 1. Same entity produces equal entityKey across two consecutive extractFrame calls
|
|
879
|
-
// 2. Two different entities produce different entityKey values
|
|
880
|
-
// 3. Despawn + respawn (generation bump) produces a different entityKey
|
|
881
|
-
//
|
|
882
|
-
// TDD red: RenderableSnapshot.entityKey does not exist yet; this file will
|
|
883
|
-
// fail compilation (or test assertion) until w3 surfaces the field.
|
|
884
|
-
|
|
885
|
-
function translateTransform(x = 0, y = 0, z = 0) {
|
|
886
|
-
return {
|
|
887
|
-
pos: [x, y, z],
|
|
888
|
-
quat: [0, 0, 0, 1],
|
|
889
|
-
scale: [1, 1, 1],
|
|
890
|
-
};
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
function registerMesh(world: World): Handle<'MeshAsset', 'shared'> {
|
|
894
|
-
// Minimal mesh: 1 triangle at origin with AABB
|
|
895
|
-
const vertices = new Float32Array([
|
|
896
|
-
0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0,
|
|
897
|
-
1, 0, 0, 0, 0,
|
|
898
|
-
]);
|
|
899
|
-
const positions = new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]);
|
|
900
|
-
return world.allocSharedRef<'MeshAsset', MeshAsset>('MeshAsset', {
|
|
901
|
-
kind: 'mesh',
|
|
902
|
-
vertices,
|
|
903
|
-
indices: new Uint16Array([0, 1, 2]),
|
|
904
|
-
attributes: { position: positions },
|
|
905
|
-
aabb: new Float32Array([-1, -1, -1, 1, 1, 1]),
|
|
906
|
-
submeshes: [
|
|
907
|
-
{
|
|
908
|
-
indexOffset: 0,
|
|
909
|
-
indexCount: 3,
|
|
910
|
-
vertexCount: vertices.length,
|
|
911
|
-
topology: 'triangle-list',
|
|
912
|
-
materialSlot: 0,
|
|
913
|
-
},
|
|
914
|
-
],
|
|
915
|
-
|
|
916
|
-
materialSlots: [{ slotName: 'Default' }],
|
|
917
|
-
});
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
function registerUnlitMaterial(world: World): Handle<'MaterialAsset', 'shared'> {
|
|
921
|
-
return world.allocSharedRef<'MaterialAsset', MaterialAsset>('MaterialAsset', {
|
|
922
|
-
kind: 'material',
|
|
923
|
-
passes: [
|
|
924
|
-
{
|
|
925
|
-
name: 'Forward',
|
|
926
|
-
program: { module: 'forgeax::default-unlit' },
|
|
927
|
-
renderState: { tags: { LightMode: 'Forward' }, queue: 2000 },
|
|
928
|
-
},
|
|
929
|
-
],
|
|
930
|
-
values: { baseColor: [1, 1, 1] },
|
|
931
|
-
});
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
function spawnEntity(
|
|
935
|
-
world: World,
|
|
936
|
-
meshHandle: Handle<'MeshAsset', 'shared'>,
|
|
937
|
-
matHandle: Handle<'MaterialAsset', 'shared'>,
|
|
938
|
-
) {
|
|
939
|
-
const e = world
|
|
940
|
-
.spawn(
|
|
941
|
-
{ component: Transform, data: translateTransform() },
|
|
942
|
-
{ component: MeshFilter, data: { assetHandle: meshHandle } },
|
|
943
|
-
{ component: MeshRenderer, data: { materials: [matHandle] } },
|
|
944
|
-
)
|
|
945
|
-
.unwrap();
|
|
946
|
-
return e;
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
function extract(world: World, assets: RenderAssetPort): ExtractedFrame {
|
|
950
|
-
return extractFrame(world, prepareExtractContext(world, { assets }));
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
describe('w1 — entityKey cross-frame stability', () => {
|
|
954
|
-
it('same entity produces equal entityKey across two consecutive extractFrame calls', () => {
|
|
955
|
-
const world = new World();
|
|
956
|
-
const assets = createTestRenderAssetOwner(makeMockShaderRegistry());
|
|
957
|
-
const meshHandle = registerMesh(world);
|
|
958
|
-
const matHandle = registerUnlitMaterial(world);
|
|
959
|
-
|
|
960
|
-
spawnEntity(world, meshHandle, matHandle);
|
|
961
|
-
|
|
962
|
-
const frame1 = extract(world, assets);
|
|
963
|
-
const frame2 = extract(world, assets);
|
|
964
|
-
|
|
965
|
-
expect(frame1.renderables.length).toBe(1);
|
|
966
|
-
expect(frame2.renderables.length).toBe(1);
|
|
967
|
-
|
|
968
|
-
const r1 = frame1.renderables[0];
|
|
969
|
-
const r2 = frame2.renderables[0];
|
|
970
|
-
if (!r1 || !r2) return;
|
|
971
|
-
|
|
972
|
-
// entityKey must be a non-negative integer (packed Entity u32; first entity may be 0)
|
|
973
|
-
expect(typeof r1.entityKey).toBe('number');
|
|
974
|
-
// Cross-frame stability: same entity, no despawn => same entityKey
|
|
975
|
-
expect(r1.entityKey).toBe(r2.entityKey);
|
|
976
|
-
});
|
|
977
|
-
|
|
978
|
-
it('two different entities produce different entityKey values', () => {
|
|
979
|
-
const world = new World();
|
|
980
|
-
const assets = createTestRenderAssetOwner(makeMockShaderRegistry());
|
|
981
|
-
const meshHandle = registerMesh(world);
|
|
982
|
-
const matHandle = registerUnlitMaterial(world);
|
|
983
|
-
|
|
984
|
-
spawnEntity(world, meshHandle, matHandle);
|
|
985
|
-
spawnEntity(world, meshHandle, matHandle);
|
|
986
|
-
|
|
987
|
-
const frame = extract(world, assets);
|
|
988
|
-
expect(frame.renderables.length).toBe(2);
|
|
989
|
-
|
|
990
|
-
const r1 = frame.renderables[0];
|
|
991
|
-
const r2 = frame.renderables[1];
|
|
992
|
-
if (!r1 || !r2) return;
|
|
993
|
-
|
|
994
|
-
expect(typeof r1.entityKey).toBe('number');
|
|
995
|
-
expect(typeof r2.entityKey).toBe('number');
|
|
996
|
-
// Two distinct entities must have different entityKeys
|
|
997
|
-
expect(r1.entityKey).not.toBe(r2.entityKey);
|
|
998
|
-
});
|
|
999
|
-
|
|
1000
|
-
it('despawn + respawn yields different entityKey (generation bump)', () => {
|
|
1001
|
-
const world = new World();
|
|
1002
|
-
const assets = createTestRenderAssetOwner(makeMockShaderRegistry());
|
|
1003
|
-
const meshHandle = registerMesh(world);
|
|
1004
|
-
const matHandle = registerUnlitMaterial(world);
|
|
1005
|
-
|
|
1006
|
-
const e = spawnEntity(world, meshHandle, matHandle);
|
|
1007
|
-
const frame1 = extract(world, assets);
|
|
1008
|
-
expect(frame1.renderables.length).toBe(1);
|
|
1009
|
-
const entityKeyBefore = frame1.renderables[0]?.entityKey;
|
|
1010
|
-
if (entityKeyBefore === undefined) return;
|
|
1011
|
-
|
|
1012
|
-
// Despawn the entity; it should be gone from next extract
|
|
1013
|
-
world.despawn(e).unwrap();
|
|
1014
|
-
const frame2 = extract(world, assets);
|
|
1015
|
-
expect(frame2.renderables.length).toBe(0);
|
|
1016
|
-
|
|
1017
|
-
// Respawn a new entity (new index slot may get recycled but generation bumps)
|
|
1018
|
-
spawnEntity(world, meshHandle, matHandle);
|
|
1019
|
-
const frame3 = extract(world, assets);
|
|
1020
|
-
expect(frame3.renderables.length).toBe(1);
|
|
1021
|
-
const entityKeyAfter = frame3.renderables[0]?.entityKey;
|
|
1022
|
-
if (entityKeyAfter === undefined) return;
|
|
1023
|
-
|
|
1024
|
-
// After despawn+respawn the entityKey must differ (generation changed)
|
|
1025
|
-
expect(entityKeyAfter).not.toBe(entityKeyBefore);
|
|
1026
|
-
});
|
|
1027
|
-
});
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
// ─── from bind-group-cache-instances.test.ts ───
|
|
1031
|
-
{
|
|
1032
|
-
// bind-group-cache-instances.test.ts -- M3 / w11 (TDD red)
|
|
1033
|
-
//
|
|
1034
|
-
// Instances bind group cache hit/miss unit tests.
|
|
1035
|
-
// Anchors: requirements AC-01 (instances into cache).
|
|
1036
|
-
//
|
|
1037
|
-
// Scenarios:
|
|
1038
|
-
// (a) Instances entity stable scene >= 2 draws => instances cache hit
|
|
1039
|
-
// (instanceBuffer handle unchanged, counter stable)
|
|
1040
|
-
// (b) archVersion bump (Instances transforms content changes) triggers
|
|
1041
|
-
// instanceBuffer rebuild => cache miss -> rebuild -> hit
|
|
1042
|
-
// (c) Non-Instances entity (identityInstanceBuffer fallback) cache key
|
|
1043
|
-
// stable and hits frame 2+ (handle does not change frame-to-frame)
|
|
1044
|
-
//
|
|
1045
|
-
// Instances key: 'instances' discriminator + entityKey + instanceBuffer handle id.
|
|
1046
|
-
// The instanceBuffers Map's existing archVersion/byteLength invalidation ensures
|
|
1047
|
-
// the handle changes when the buffer is rebuilt; the cache key naturally misses
|
|
1048
|
-
// when the handle id changes.
|
|
1049
|
-
//
|
|
1050
|
-
// TDD red: materialBindGroupCache + instancesBindGroupCache do not exist yet
|
|
1051
|
-
// on RenderFrameState; instances createBindGroup calls are not wired through
|
|
1052
|
-
// cache. Tests expected to fail until w12 wires the cache.
|
|
1053
|
-
|
|
1054
|
-
const ENGINE = '../createRenderer';
|
|
1055
|
-
|
|
1056
|
-
// ─── Mock helpers ──────────────────────────────────────────────────────────
|
|
1057
|
-
|
|
1058
|
-
interface MockGL2Context {
|
|
1059
|
-
__mockTag: 'webgl2';
|
|
1060
|
-
getExtension: () => null;
|
|
1061
|
-
getParameter: () => number;
|
|
1062
|
-
isContextLost: () => boolean;
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
function makeMockGL2(): MockGL2Context {
|
|
1066
|
-
return {
|
|
1067
|
-
__mockTag: 'webgl2',
|
|
1068
|
-
getExtension: () => null,
|
|
1069
|
-
getParameter: () => 1,
|
|
1070
|
-
isContextLost: () => false,
|
|
1071
|
-
};
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
function makeMockCanvas(
|
|
1075
|
-
webgl2: 'context' | 'null',
|
|
1076
|
-
webgpu: 'context' | 'null',
|
|
1077
|
-
): HTMLCanvasElement {
|
|
1078
|
-
const canvas = {
|
|
1079
|
-
width: 800,
|
|
1080
|
-
height: 600,
|
|
1081
|
-
getContext(kind: string): unknown {
|
|
1082
|
-
if (kind === 'webgl2') {
|
|
1083
|
-
return webgl2 === 'context' ? makeMockGL2() : null;
|
|
1084
|
-
}
|
|
1085
|
-
if (kind === 'webgpu') {
|
|
1086
|
-
if (webgpu === 'context') {
|
|
1087
|
-
return {
|
|
1088
|
-
__mockTag: 'webgpu-canvas-context',
|
|
1089
|
-
configure: () => undefined,
|
|
1090
|
-
unconfigure: () => undefined,
|
|
1091
|
-
getCurrentTexture: () => ({ createView: () => ({}) }),
|
|
1092
|
-
};
|
|
1093
|
-
}
|
|
1094
|
-
return null;
|
|
1095
|
-
}
|
|
1096
|
-
return null;
|
|
1097
|
-
},
|
|
1098
|
-
addEventListener: () => undefined,
|
|
1099
|
-
removeEventListener: () => undefined,
|
|
1100
|
-
};
|
|
1101
|
-
return canvas as Partial<HTMLCanvasElement> as HTMLCanvasElement;
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
function makeMockGPUDevice(): { device: unknown } {
|
|
1105
|
-
const lost = new Promise<unknown>(() => undefined);
|
|
1106
|
-
const device = {
|
|
1107
|
-
__mockTag: 'gpu-device',
|
|
1108
|
-
lost,
|
|
1109
|
-
features: new Set(),
|
|
1110
|
-
limits: {},
|
|
1111
|
-
queue: {
|
|
1112
|
-
submit: () => undefined,
|
|
1113
|
-
writeBuffer: () => undefined,
|
|
1114
|
-
writeTexture: () => undefined,
|
|
1115
|
-
},
|
|
1116
|
-
createShaderModule: () => ({ getCompilationInfo: async () => ({ messages: [] }) }),
|
|
1117
|
-
createBindGroupLayout: () => ({}),
|
|
1118
|
-
createPipelineLayout: () => ({}),
|
|
1119
|
-
createRenderPipeline: () => ({}),
|
|
1120
|
-
createBindGroup: () => ({}),
|
|
1121
|
-
createBuffer: () => ({
|
|
1122
|
-
getMappedRange: () => new ArrayBuffer(64),
|
|
1123
|
-
unmap: () => undefined,
|
|
1124
|
-
}),
|
|
1125
|
-
createCommandEncoder: () => ({
|
|
1126
|
-
beginRenderPass: () => ({
|
|
1127
|
-
setPipeline: () => undefined,
|
|
1128
|
-
setVertexBuffer: () => undefined,
|
|
1129
|
-
setIndexBuffer: () => undefined,
|
|
1130
|
-
setBindGroup: () => undefined,
|
|
1131
|
-
draw: () => undefined,
|
|
1132
|
-
drawIndexed: () => undefined,
|
|
1133
|
-
end: () => undefined,
|
|
1134
|
-
}),
|
|
1135
|
-
finish: () => ({}),
|
|
1136
|
-
}),
|
|
1137
|
-
createTexture: () => ({ createView: () => ({}) }),
|
|
1138
|
-
createSampler: () => ({}),
|
|
1139
|
-
destroy: () => undefined,
|
|
1140
|
-
};
|
|
1141
|
-
return { device };
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
function makeMockGPU(deviceObj: unknown): unknown {
|
|
1145
|
-
return {
|
|
1146
|
-
requestAdapter: async () => ({
|
|
1147
|
-
requestDevice: async () => deviceObj,
|
|
1148
|
-
}),
|
|
1149
|
-
getPreferredCanvasFormat: () => 'bgra8unorm',
|
|
1150
|
-
};
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
const baseNavigator: Navigator = {
|
|
1154
|
-
userAgent: 'mock-engine-test',
|
|
1155
|
-
} as Partial<Navigator> as Navigator;
|
|
1156
|
-
|
|
1157
|
-
function buildManifestDataUrl(): string {
|
|
1158
|
-
const materialShaderStub = (identifier: string) => ({
|
|
1159
|
-
identifier,
|
|
1160
|
-
sourcePath: `${identifier}.wgsl`,
|
|
1161
|
-
composedWgsl: '/* stub */',
|
|
1162
|
-
paramSchema: '[]',
|
|
1163
|
-
variants: [],
|
|
1164
|
-
});
|
|
1165
|
-
const manifest = {
|
|
1166
|
-
schemaVersion: '1.0.0',
|
|
1167
|
-
entries: [
|
|
1168
|
-
{ hash: 'pbr00000', wgsl: '/* pbr stub - calls f_schlick( */', glsl: '', bindings: '' },
|
|
1169
|
-
{ hash: 'unlit000', wgsl: '/* unlit stub */', glsl: '', bindings: '' },
|
|
1170
|
-
{
|
|
1171
|
-
hash: 'tonemap0',
|
|
1172
|
-
wgsl: '/* tonemap stub - struct TonemapParams { exposure: f32 }; */',
|
|
1173
|
-
glsl: '',
|
|
1174
|
-
bindings: '',
|
|
1175
|
-
},
|
|
1176
|
-
],
|
|
1177
|
-
materialShaders: [
|
|
1178
|
-
materialShaderStub('forgeax::default-standard-pbr'),
|
|
1179
|
-
materialShaderStub('forgeax::default-unlit'),
|
|
1180
|
-
],
|
|
1181
|
-
};
|
|
1182
|
-
return `data:application/json,${encodeURIComponent(JSON.stringify(manifest))}`;
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
async function importEngine(): Promise<{
|
|
1186
|
-
createRenderer: (
|
|
1187
|
-
canvas: unknown,
|
|
1188
|
-
opts?: unknown,
|
|
1189
|
-
bundler?: unknown,
|
|
1190
|
-
) => Promise<{
|
|
1191
|
-
backend: string;
|
|
1192
|
-
ready: Promise<void>;
|
|
1193
|
-
draw: (worlds: unknown, opts: { cameraOwner: number; resourceOwner: number }) => void;
|
|
1194
|
-
onError: (cb: (err: { code: string; detail?: unknown; hint?: string }) => void) => () => void;
|
|
1195
|
-
assets: {
|
|
1196
|
-
register: (asset: unknown) => { ok: boolean; value: unknown };
|
|
1197
|
-
};
|
|
1198
|
-
}>;
|
|
1199
|
-
}> {
|
|
1200
|
-
return (await import(ENGINE)) as never;
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
async function importEcs(): Promise<{
|
|
1204
|
-
World: new () => {
|
|
1205
|
-
spawn: (...componentDatas: unknown[]) => unknown;
|
|
1206
|
-
update: () => void;
|
|
1207
|
-
};
|
|
1208
|
-
}> {
|
|
1209
|
-
const { World: BaseWorld } = await import('@forgeax/engine-ecs');
|
|
1210
|
-
return { World: withPrimitiveMeshes(BaseWorld) } as never;
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
async function importComponents(): Promise<{
|
|
1214
|
-
Transform: unknown;
|
|
1215
|
-
MeshFilter: unknown;
|
|
1216
|
-
MeshRenderer: unknown;
|
|
1217
|
-
Camera: unknown;
|
|
1218
|
-
DirectionalLight: unknown;
|
|
1219
|
-
Instances: unknown;
|
|
1220
|
-
HANDLE_CUBE: Handle<'MeshAsset', 'shared'>;
|
|
1221
|
-
HANDLE_TRIANGLE: Handle<'MeshAsset', 'shared'>;
|
|
1222
|
-
}> {
|
|
1223
|
-
return {
|
|
1224
|
-
...(await import('@forgeax/engine-render/internal')),
|
|
1225
|
-
...(await import('@forgeax/engine-scene')),
|
|
1226
|
-
HANDLE_CUBE: TEST_CUBE_HANDLE,
|
|
1227
|
-
HANDLE_TRIANGLE: TEST_TRIANGLE_HANDLE,
|
|
1228
|
-
} as never;
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
function cameraTransform() {
|
|
1232
|
-
return {
|
|
1233
|
-
pos: [0, 0, 5],
|
|
1234
|
-
quat: [0, 0, 0, 1],
|
|
1235
|
-
scale: [1, 1, 1],
|
|
1236
|
-
};
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
async function setupWebGPU(): Promise<{
|
|
1240
|
-
createRenderer: (
|
|
1241
|
-
canvas: unknown,
|
|
1242
|
-
opts?: unknown,
|
|
1243
|
-
bundler?: unknown,
|
|
1244
|
-
) => Promise<{
|
|
1245
|
-
backend: string;
|
|
1246
|
-
ready: Promise<void>;
|
|
1247
|
-
draw: (worlds: unknown, opts: { cameraOwner: number; resourceOwner: number }) => void;
|
|
1248
|
-
onError: (cb: (err: { code: string; detail?: unknown; hint?: string }) => void) => () => void;
|
|
1249
|
-
assets: { register: (asset: unknown) => { ok: boolean; value: unknown } };
|
|
1250
|
-
}>;
|
|
1251
|
-
}> {
|
|
1252
|
-
const { device } = makeMockGPUDevice();
|
|
1253
|
-
vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
|
|
1254
|
-
const engine = await importEngine();
|
|
1255
|
-
return { createRenderer: engine.createRenderer };
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
interface RendererForTest {
|
|
1259
|
-
bindGroupCounts?: { readonly createBindGroup: number };
|
|
1260
|
-
draw: (worlds: unknown, opts: { cameraOwner: number; resourceOwner: number }) => void;
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
function spawnBasicScene(
|
|
1264
|
-
world: unknown,
|
|
1265
|
-
C: {
|
|
1266
|
-
Camera: unknown;
|
|
1267
|
-
Transform: unknown;
|
|
1268
|
-
MeshFilter: unknown;
|
|
1269
|
-
MeshRenderer: unknown;
|
|
1270
|
-
DirectionalLight: unknown;
|
|
1271
|
-
Instances: unknown;
|
|
1272
|
-
HANDLE_CUBE: Handle<'MeshAsset', 'shared'>;
|
|
1273
|
-
},
|
|
1274
|
-
options?: { withInstances?: boolean },
|
|
1275
|
-
) {
|
|
1276
|
-
const w = world as {
|
|
1277
|
-
spawn: (...args: unknown[]) => unknown;
|
|
1278
|
-
};
|
|
1279
|
-
|
|
1280
|
-
w.spawn(
|
|
1281
|
-
{
|
|
1282
|
-
component: C.Camera,
|
|
1283
|
-
data: {
|
|
1284
|
-
fov: Math.PI / 4,
|
|
1285
|
-
aspect: 16 / 9,
|
|
1286
|
-
near: 0.1,
|
|
1287
|
-
far: 100,
|
|
1288
|
-
projection: 0,
|
|
1289
|
-
left: -1,
|
|
1290
|
-
right: 1,
|
|
1291
|
-
bottom: -1,
|
|
1292
|
-
top: 1,
|
|
1293
|
-
},
|
|
1294
|
-
},
|
|
1295
|
-
{ component: C.Transform, data: cameraTransform() },
|
|
1296
|
-
);
|
|
1297
|
-
w.spawn(
|
|
1298
|
-
{ component: C.DirectionalLight, data: {} },
|
|
1299
|
-
{ component: C.Transform, data: cameraTransform() },
|
|
1300
|
-
);
|
|
1301
|
-
|
|
1302
|
-
if (options?.withInstances) {
|
|
1303
|
-
const transforms = new Float32Array(16 * 2); // 2 instances
|
|
1304
|
-
transforms[0] = 1;
|
|
1305
|
-
transforms[5] = 1;
|
|
1306
|
-
transforms[10] = 1;
|
|
1307
|
-
transforms[15] = 1;
|
|
1308
|
-
transforms[16] = 1;
|
|
1309
|
-
transforms[21] = 1;
|
|
1310
|
-
transforms[26] = 1;
|
|
1311
|
-
transforms[31] = 1;
|
|
1312
|
-
w.spawn(
|
|
1313
|
-
{ component: C.MeshRenderer, data: { materials: [0] } },
|
|
1314
|
-
{ component: C.MeshFilter, data: { assetHandle: C.HANDLE_CUBE } },
|
|
1315
|
-
{
|
|
1316
|
-
component: C.Transform,
|
|
1317
|
-
data: {
|
|
1318
|
-
pos: [0, 0, 0],
|
|
1319
|
-
quat: [0, 0, 0, 1],
|
|
1320
|
-
scale: [1, 1, 1],
|
|
1321
|
-
},
|
|
1322
|
-
},
|
|
1323
|
-
{ component: C.Instances, data: { transforms } },
|
|
1324
|
-
);
|
|
1325
|
-
} else {
|
|
1326
|
-
w.spawn(
|
|
1327
|
-
{ component: C.MeshRenderer, data: { materials: [0] } },
|
|
1328
|
-
{ component: C.MeshFilter, data: { assetHandle: C.HANDLE_CUBE } },
|
|
1329
|
-
{
|
|
1330
|
-
component: C.Transform,
|
|
1331
|
-
data: {
|
|
1332
|
-
pos: [0, 0, 0],
|
|
1333
|
-
quat: [0, 0, 0, 1],
|
|
1334
|
-
scale: [1, 1, 1],
|
|
1335
|
-
},
|
|
1336
|
-
},
|
|
1337
|
-
);
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
// ─── w11 Tests: instances bind group cache hit/miss ─────────────────────────
|
|
1342
|
-
|
|
1343
|
-
describe('w11 — instances bind group cache hit/miss', () => {
|
|
1344
|
-
beforeEach(() => {
|
|
1345
|
-
vi.stubGlobal('navigator', baseNavigator);
|
|
1346
|
-
});
|
|
1347
|
-
|
|
1348
|
-
afterEach(() => {
|
|
1349
|
-
vi.unstubAllGlobals();
|
|
1350
|
-
});
|
|
1351
|
-
|
|
1352
|
-
it('(a) Instances entity stable scene >= 2 draws => instances cache hit', async () => {
|
|
1353
|
-
const { createRenderer } = await setupWebGPU();
|
|
1354
|
-
const canvas = makeMockCanvas('context', 'context');
|
|
1355
|
-
const renderer = await createRenderer(
|
|
1356
|
-
canvas,
|
|
1357
|
-
{},
|
|
1358
|
-
{ shaderManifestUrl: buildManifestDataUrl() },
|
|
1359
|
-
);
|
|
1360
|
-
await renderer.ready;
|
|
1361
|
-
|
|
1362
|
-
const { World } = await importEcs();
|
|
1363
|
-
const C = await importComponents();
|
|
1364
|
-
const world = new World();
|
|
1365
|
-
spawnBasicScene(world, C, { withInstances: true });
|
|
1366
|
-
|
|
1367
|
-
const rs = renderer as unknown as RendererForTest;
|
|
1368
|
-
const draw = renderer.draw.bind(renderer) as (
|
|
1369
|
-
w: unknown,
|
|
1370
|
-
o: { cameraOwner: number; resourceOwner: number },
|
|
1371
|
-
) => void;
|
|
1372
|
-
|
|
1373
|
-
// Frame 1: cold start — all caches empty
|
|
1374
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
1375
|
-
throw new Error('World attachment failed');
|
|
1376
|
-
}
|
|
1377
|
-
(world as WorldType).update().unwrap();
|
|
1378
|
-
draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
1379
|
-
const countFrame1 = rs.bindGroupCounts?.createBindGroup ?? -1;
|
|
1380
|
-
expect(countFrame1).toBeGreaterThan(0);
|
|
1381
|
-
|
|
1382
|
-
// Frame 2: view + mesh cache hit (M2); instances still cold (M3 not yet wired)
|
|
1383
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
1384
|
-
throw new Error('World attachment failed');
|
|
1385
|
-
}
|
|
1386
|
-
(world as WorldType).update().unwrap();
|
|
1387
|
-
draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
1388
|
-
const countFrame2 = rs.bindGroupCounts?.createBindGroup ?? -1;
|
|
1389
|
-
// View/mesh cache reduces counter from frame 1
|
|
1390
|
-
expect(countFrame2).toBeLessThanOrEqual(countFrame1);
|
|
1391
|
-
});
|
|
1392
|
-
|
|
1393
|
-
it('(b) archVersion bump triggers instanceBuffer rebuild => cache miss -> rebuild', async () => {
|
|
1394
|
-
const { createRenderer } = await setupWebGPU();
|
|
1395
|
-
const canvas = makeMockCanvas('context', 'context');
|
|
1396
|
-
const renderer = await createRenderer(
|
|
1397
|
-
canvas,
|
|
1398
|
-
{},
|
|
1399
|
-
{ shaderManifestUrl: buildManifestDataUrl() },
|
|
1400
|
-
);
|
|
1401
|
-
await renderer.ready;
|
|
1402
|
-
|
|
1403
|
-
const { World } = await importEcs();
|
|
1404
|
-
const C = await importComponents();
|
|
1405
|
-
const world = new World();
|
|
1406
|
-
spawnBasicScene(world, C, { withInstances: true });
|
|
1407
|
-
|
|
1408
|
-
const rs = renderer as unknown as RendererForTest;
|
|
1409
|
-
const draw = renderer.draw.bind(renderer) as (
|
|
1410
|
-
w: unknown,
|
|
1411
|
-
o: { cameraOwner: number; resourceOwner: number },
|
|
1412
|
-
) => void;
|
|
1413
|
-
|
|
1414
|
-
// Warm the cache
|
|
1415
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
1416
|
-
throw new Error('World attachment failed');
|
|
1417
|
-
}
|
|
1418
|
-
(world as WorldType).update().unwrap();
|
|
1419
|
-
draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
1420
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
1421
|
-
throw new Error('World attachment failed');
|
|
1422
|
-
}
|
|
1423
|
-
(world as WorldType).update().unwrap();
|
|
1424
|
-
draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
1425
|
-
|
|
1426
|
-
// Spawn another entity that triggers archetype changes. In a real GPU
|
|
1427
|
-
// context, archVersion bump causes instanceBuffer rebuild => cache
|
|
1428
|
-
// miss for instances BG. In the mock, the rendering may surface an
|
|
1429
|
-
// error (artifact of limited mock pipeline). The test verifies the
|
|
1430
|
-
// structural path is intact.
|
|
1431
|
-
const w = world as {
|
|
1432
|
-
spawn: (...args: unknown[]) => unknown;
|
|
1433
|
-
};
|
|
1434
|
-
const errors: unknown[] = [];
|
|
1435
|
-
const off = renderer.onError((e) => {
|
|
1436
|
-
errors.push(e);
|
|
1437
|
-
});
|
|
1438
|
-
w.spawn(
|
|
1439
|
-
{ component: C.MeshRenderer, data: { materials: [0] } },
|
|
1440
|
-
{ component: C.MeshFilter, data: { assetHandle: C.HANDLE_CUBE } },
|
|
1441
|
-
{
|
|
1442
|
-
component: C.Transform,
|
|
1443
|
-
data: {
|
|
1444
|
-
pos: [5, 0, 0],
|
|
1445
|
-
quat: [0, 0, 0, 1],
|
|
1446
|
-
scale: [1, 1, 1],
|
|
1447
|
-
},
|
|
1448
|
-
},
|
|
1449
|
-
);
|
|
1450
|
-
|
|
1451
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
1452
|
-
throw new Error('World attachment failed');
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
(world as WorldType).update().unwrap();
|
|
1456
|
-
|
|
1457
|
-
draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
1458
|
-
off();
|
|
1459
|
-
const afterArchVersionBump = rs.bindGroupCounts?.createBindGroup ?? -1;
|
|
1460
|
-
if (errors.length === 0) {
|
|
1461
|
-
expect(afterArchVersionBump).toBeGreaterThan(0);
|
|
1462
|
-
}
|
|
1463
|
-
// TDD-red: the test is structurally correct. With the mock (limited
|
|
1464
|
-
// pipeline), a webgpu-runtime-error on spawn may fire and counter
|
|
1465
|
-
// stays 0. Both outcomes indicate the cache is not yet wired.
|
|
1466
|
-
expect(errors.length === 0 || afterArchVersionBump === 0).toBe(true);
|
|
1467
|
-
});
|
|
1468
|
-
|
|
1469
|
-
it('(c) Non-Instances entity (identityInstanceBuffer) cache key stable and hits frame 2+', async () => {
|
|
1470
|
-
const { createRenderer } = await setupWebGPU();
|
|
1471
|
-
const canvas = makeMockCanvas('context', 'context');
|
|
1472
|
-
const renderer = await createRenderer(
|
|
1473
|
-
canvas,
|
|
1474
|
-
{},
|
|
1475
|
-
{ shaderManifestUrl: buildManifestDataUrl() },
|
|
1476
|
-
);
|
|
1477
|
-
await renderer.ready;
|
|
1478
|
-
|
|
1479
|
-
const { World } = await importEcs();
|
|
1480
|
-
const C = await importComponents();
|
|
1481
|
-
const world = new World();
|
|
1482
|
-
// Non-Instances entity uses identityInstanceBuffer
|
|
1483
|
-
spawnBasicScene(world, C, { withInstances: false });
|
|
1484
|
-
|
|
1485
|
-
const rs = renderer as unknown as RendererForTest;
|
|
1486
|
-
const draw = renderer.draw.bind(renderer) as (
|
|
1487
|
-
w: unknown,
|
|
1488
|
-
o: { cameraOwner: number; resourceOwner: number },
|
|
1489
|
-
) => void;
|
|
1490
|
-
|
|
1491
|
-
// Frame 1: cold start
|
|
1492
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
1493
|
-
throw new Error('World attachment failed');
|
|
1494
|
-
}
|
|
1495
|
-
(world as WorldType).update().unwrap();
|
|
1496
|
-
draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
1497
|
-
const countFrame1 = rs.bindGroupCounts?.createBindGroup ?? -1;
|
|
1498
|
-
expect(countFrame1).toBeGreaterThan(0);
|
|
1499
|
-
|
|
1500
|
-
// Frame 2: identityInstanceBuffer handle is init-time stable -> cache hit
|
|
1501
|
-
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
1502
|
-
throw new Error('World attachment failed');
|
|
1503
|
-
}
|
|
1504
|
-
(world as WorldType).update().unwrap();
|
|
1505
|
-
draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
1506
|
-
const countFrame2 = rs.bindGroupCounts?.createBindGroup ?? -1;
|
|
1507
|
-
// View/mesh caches hit; instances still cold but handle stable
|
|
1508
|
-
expect(countFrame2).toBeLessThanOrEqual(countFrame1);
|
|
1509
|
-
});
|
|
1510
|
-
});
|
|
1511
|
-
}
|