@forgeax/engine-runtime 0.1.2
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/LICENSE +202 -0
- package/README.md +183 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/active-camera.unit.test.d.ts +2 -0
- package/dist/__tests__/active-camera.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-add.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-add.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-asset-union.test-d.d.ts +2 -0
- package/dist/__tests__/animation-graph-asset-union.test-d.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-blend.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-blend.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-build.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-build.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-clip-missing.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-clip-missing.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-clip.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-clip.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-default-path.integration.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-default-path.integration.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-describe.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-describe.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-errors.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-errors.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-nesting.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-nesting.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-orthogonal-weight.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-orthogonal-weight.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-graph-slot-fill.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-graph-slot-fill.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-nslot.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-nslot.unit.test.d.ts.map +1 -0
- package/dist/__tests__/animation-slot-length-guard.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-slot-length-guard.unit.test.d.ts.map +1 -0
- package/dist/__tests__/append-injection.test.d.ts +2 -0
- package/dist/__tests__/append-injection.test.d.ts.map +1 -0
- package/dist/__tests__/asi-world-baseline-export.unit.test.d.ts +2 -0
- package/dist/__tests__/asi-world-baseline-export.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asi-world-ci-config.unit.test.d.ts +2 -0
- package/dist/__tests__/asi-world-ci-config.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asi-world-smoke.unit.test.d.ts +2 -0
- package/dist/__tests__/asi-world-smoke.unit.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-scene.test-d.d.ts +2 -0
- package/dist/__tests__/asset-registry-scene.test-d.d.ts.map +1 -0
- package/dist/__tests__/bind-group-cache-binding.test.d.ts +2 -0
- package/dist/__tests__/bind-group-cache-binding.test.d.ts.map +1 -0
- package/dist/__tests__/bind-group-cache-frame.test.d.ts +2 -0
- package/dist/__tests__/bind-group-cache-frame.test.d.ts.map +1 -0
- package/dist/__tests__/bind-group-cache-keying.test.d.ts +2 -0
- package/dist/__tests__/bind-group-cache-keying.test.d.ts.map +1 -0
- package/dist/__tests__/block-upload-math.unit.test.d.ts +2 -0
- package/dist/__tests__/block-upload-math.unit.test.d.ts.map +1 -0
- package/dist/__tests__/builtin-shader-register-e2e.test.d.ts +2 -0
- package/dist/__tests__/builtin-shader-register-e2e.test.d.ts.map +1 -0
- package/dist/__tests__/cache-false-eviction.test.d.ts +2 -0
- package/dist/__tests__/cache-false-eviction.test.d.ts.map +1 -0
- package/dist/__tests__/cache-isolation-matrix.test.d.ts +2 -0
- package/dist/__tests__/cache-isolation-matrix.test.d.ts.map +1 -0
- package/dist/__tests__/camera-clear-snapshot-equivalence.unit.test.d.ts +2 -0
- package/dist/__tests__/camera-clear-snapshot-equivalence.unit.test.d.ts.map +1 -0
- package/dist/__tests__/clamp-to-last.e2e.browser.test.d.ts +2 -0
- package/dist/__tests__/clamp-to-last.e2e.browser.test.d.ts.map +1 -0
- package/dist/__tests__/clamp-to-last.e2e.dawn.test.d.ts +2 -0
- package/dist/__tests__/clamp-to-last.e2e.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/collect-scene-asset.test.d.ts +3 -0
- package/dist/__tests__/collect-scene-asset.test.d.ts.map +1 -0
- package/dist/__tests__/collect-subtree.unit.test.d.ts +2 -0
- package/dist/__tests__/collect-subtree.unit.test.d.ts.map +1 -0
- package/dist/__tests__/collect-transient-children-absent.test.d.ts +2 -0
- package/dist/__tests__/collect-transient-children-absent.test.d.ts.map +1 -0
- package/dist/__tests__/collect-transient-roundtrip.test.d.ts +2 -0
- package/dist/__tests__/collect-transient-roundtrip.test.d.ts.map +1 -0
- package/dist/__tests__/collect-transient-skip.test.d.ts +3 -0
- package/dist/__tests__/collect-transient-skip.test.d.ts.map +1 -0
- package/dist/__tests__/components.test-d.d.ts +2 -0
- package/dist/__tests__/components.test-d.d.ts.map +1 -0
- package/dist/__tests__/components.unit.test.d.ts +2 -0
- package/dist/__tests__/components.unit.test.d.ts.map +1 -0
- package/dist/__tests__/composite-skybox-cross-world.browser.test.d.ts +2 -0
- package/dist/__tests__/composite-skybox-cross-world.browser.test.d.ts.map +1 -0
- package/dist/__tests__/create-renderer-assembly.integration.test.d.ts +2 -0
- package/dist/__tests__/create-renderer-assembly.integration.test.d.ts.map +1 -0
- package/dist/__tests__/createRenderer-dispose.test.d.ts +2 -0
- package/dist/__tests__/createRenderer-dispose.test.d.ts.map +1 -0
- package/dist/__tests__/createRenderer-requiredFeatures.unit.test.d.ts +2 -0
- package/dist/__tests__/createRenderer-requiredFeatures.unit.test.d.ts.map +1 -0
- package/dist/__tests__/createRenderer.browser-no-webgpu.test.d.ts +2 -0
- package/dist/__tests__/createRenderer.browser-no-webgpu.test.d.ts.map +1 -0
- package/dist/__tests__/createRenderer.unit.test.d.ts +2 -0
- package/dist/__tests__/createRenderer.unit.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/hello-sprite-nineslice-section.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/hello-sprite-nineslice-section.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/ibl-compose.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/ibl-compose.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/ibl-modules-compose.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/ibl-modules-compose.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/ibl-precompute-readback.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/ibl-precompute-readback.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/instances-per-instance-pbr.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/instances-per-instance-pbr.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/instances-shadow.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/instances-shadow.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/instances-uniform-fallback.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/instances-uniform-fallback.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/material-alpha.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/material-alpha.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/material-cooked-fixture.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/material-cooked-fixture.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/mesh-ssbo-normal-matrix.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/mesh-ssbo-normal-matrix.dawn.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/mipmap-srgb-linear.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/mipmap-srgb-linear.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/per-entity-material-texture-isolation.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/per-entity-material-texture-isolation.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/sprite-nineslice-mesh-bind.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/sprite-nineslice-mesh-bind.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/tonemap.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/tonemap.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/user-handle-mesh-render.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/user-handle-mesh-render.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/user-mesh-upload.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/user-mesh-upload.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/zero-manifest-mesh-fail.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/zero-manifest-mesh-fail.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dawn/zero-manifest-ready.dawn.test.d.ts +2 -0
- package/dist/__tests__/dawn/zero-manifest-ready.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/demo-sortscope-migration.unit.test.d.ts +2 -0
- package/dist/__tests__/demo-sortscope-migration.unit.test.d.ts.map +1 -0
- package/dist/__tests__/derive-bgl-integration.test.d.ts +2 -0
- package/dist/__tests__/derive-bgl-integration.test.d.ts.map +1 -0
- package/dist/__tests__/derive-render-data-skin.unit.test.d.ts +2 -0
- package/dist/__tests__/derive-render-data-skin.unit.test.d.ts.map +1 -0
- package/dist/__tests__/dispatch-fullscreen-pass-composite-swapchain.dawn.test.d.ts +2 -0
- package/dist/__tests__/dispatch-fullscreen-pass-composite-swapchain.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dispatch-fullscreen-pass-custom-reads.dawn.test.d.ts +2 -0
- package/dist/__tests__/dispatch-fullscreen-pass-custom-reads.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dispatch-fullscreen-pass-depth-composite.dawn.test.d.ts +2 -0
- package/dist/__tests__/dispatch-fullscreen-pass-depth-composite.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/dispatch-fullscreen-pass-params.unit.test.d.ts +2 -0
- package/dist/__tests__/dispatch-fullscreen-pass-params.unit.test.d.ts.map +1 -0
- package/dist/__tests__/draw-published.d.ts +5 -0
- package/dist/__tests__/draw-published.d.ts.map +1 -0
- package/dist/__tests__/environment-error-exhaustive.test-d.d.ts +8 -0
- package/dist/__tests__/environment-error-exhaustive.test-d.d.ts.map +1 -0
- package/dist/__tests__/equirect-bc6h.integration.test.d.ts +2 -0
- package/dist/__tests__/equirect-bc6h.integration.test.d.ts.map +1 -0
- package/dist/__tests__/error-exhaustive.test.d.ts +2 -0
- package/dist/__tests__/error-exhaustive.test.d.ts.map +1 -0
- package/dist/__tests__/extract-frames-graceful-degradation.test.d.ts +2 -0
- package/dist/__tests__/extract-frames-graceful-degradation.test.d.ts.map +1 -0
- package/dist/__tests__/extract-frames-merge.test.d.ts +2 -0
- package/dist/__tests__/extract-frames-merge.test.d.ts.map +1 -0
- package/dist/__tests__/extract-frames-reset.test.d.ts +2 -0
- package/dist/__tests__/extract-frames-reset.test.d.ts.map +1 -0
- package/dist/__tests__/extract-record-no-hardcoded-texture-fields.test.d.ts +2 -0
- package/dist/__tests__/extract-record-no-hardcoded-texture-fields.test.d.ts.map +1 -0
- package/dist/__tests__/fog-producer-manifest.d.ts +131 -0
- package/dist/__tests__/fog-producer-manifest.d.ts.map +1 -0
- package/dist/__tests__/fog-producer-pixel-probe.d.ts +42 -0
- package/dist/__tests__/fog-producer-pixel-probe.d.ts.map +1 -0
- package/dist/__tests__/fog-producer-pixel.browser.test.d.ts +2 -0
- package/dist/__tests__/fog-producer-pixel.browser.test.d.ts.map +1 -0
- package/dist/__tests__/fog-producer-pixel.dawn.test.d.ts +2 -0
- package/dist/__tests__/fog-producer-pixel.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/fog-producer-real-fixture.d.ts +5 -0
- package/dist/__tests__/fog-producer-real-fixture.d.ts.map +1 -0
- package/dist/__tests__/fog-producer-vfx-effect.d.ts +3 -0
- package/dist/__tests__/fog-producer-vfx-effect.d.ts.map +1 -0
- package/dist/__tests__/fog-scene-asset.integration.test.d.ts +2 -0
- package/dist/__tests__/fog-scene-asset.integration.test.d.ts.map +1 -0
- package/dist/__tests__/frame-targets-shadow-regression.unit.test.d.ts +2 -0
- package/dist/__tests__/frame-targets-shadow-regression.unit.test.d.ts.map +1 -0
- package/dist/__tests__/fullscreen-post-bgl-depth.test.d.ts +2 -0
- package/dist/__tests__/fullscreen-post-bgl-depth.test.d.ts.map +1 -0
- package/dist/__tests__/fullscreen-post-depth-params-arity.test.d.ts +2 -0
- package/dist/__tests__/fullscreen-post-depth-params-arity.test.d.ts.map +1 -0
- package/dist/__tests__/fullscreen-post-process-pass.dawn.test.d.ts +2 -0
- package/dist/__tests__/fullscreen-post-process-pass.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/fullscreen-post-process-pass.unit.test.d.ts +2 -0
- package/dist/__tests__/fullscreen-post-process-pass.unit.test.d.ts.map +1 -0
- package/dist/__tests__/fxaa-pixel-diff.dawn.test.d.ts +2 -0
- package/dist/__tests__/fxaa-pixel-diff.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/fxaa-validation-and-resize.dawn.test.d.ts +2 -0
- package/dist/__tests__/fxaa-validation-and-resize.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/fxaa-zero-overhead.dawn.test.d.ts +2 -0
- package/dist/__tests__/fxaa-zero-overhead.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/geometry.unit.test.d.ts +2 -0
- package/dist/__tests__/geometry.unit.test.d.ts.map +1 -0
- package/dist/__tests__/glyph-tilemap-worldid.test.d.ts +2 -0
- package/dist/__tests__/glyph-tilemap-worldid.test.d.ts.map +1 -0
- package/dist/__tests__/gpu-resource-cow-survivor.test.d.ts +2 -0
- package/dist/__tests__/gpu-resource-cow-survivor.test.d.ts.map +1 -0
- package/dist/__tests__/gpu-resource-store-cubemap-status.test.d.ts +2 -0
- package/dist/__tests__/gpu-resource-store-cubemap-status.test.d.ts.map +1 -0
- package/dist/__tests__/gpu-resource.test.d.ts +2 -0
- package/dist/__tests__/gpu-resource.test.d.ts.map +1 -0
- package/dist/__tests__/hdrp-bgl-slots.browser.test.d.ts +2 -0
- package/dist/__tests__/hdrp-bgl-slots.browser.test.d.ts.map +1 -0
- package/dist/__tests__/hdrp-buffers.unit.test.d.ts +2 -0
- package/dist/__tests__/hdrp-buffers.unit.test.d.ts.map +1 -0
- package/dist/__tests__/hdrp-execute-filter.unit.test.d.ts +2 -0
- package/dist/__tests__/hdrp-execute-filter.unit.test.d.ts.map +1 -0
- package/dist/__tests__/hdrp-gbuffer-attachments.browser.test.d.ts +2 -0
- package/dist/__tests__/hdrp-gbuffer-attachments.browser.test.d.ts.map +1 -0
- package/dist/__tests__/hdrp-pipeline-asset-type.test-d.d.ts +2 -0
- package/dist/__tests__/hdrp-pipeline-asset-type.test-d.d.ts.map +1 -0
- package/dist/__tests__/hdrp-ssao-boundary.test.d.ts +2 -0
- package/dist/__tests__/hdrp-ssao-boundary.test.d.ts.map +1 -0
- package/dist/__tests__/hdrp-ssao-falsification.test.d.ts +2 -0
- package/dist/__tests__/hdrp-ssao-falsification.test.d.ts.map +1 -0
- package/dist/__tests__/hello-tilemap-smoke-upgrade.unit.test.d.ts +2 -0
- package/dist/__tests__/hello-tilemap-smoke-upgrade.unit.test.d.ts.map +1 -0
- package/dist/__tests__/helpers/mock-shader-registry.d.ts +11 -0
- package/dist/__tests__/helpers/mock-shader-registry.d.ts.map +1 -0
- package/dist/__tests__/helpers/primitive-world.d.ts +8 -0
- package/dist/__tests__/helpers/primitive-world.d.ts.map +1 -0
- package/dist/__tests__/helpers/register-runtime-components.d.ts +4 -0
- package/dist/__tests__/helpers/register-runtime-components.d.ts.map +1 -0
- package/dist/__tests__/helpers/register-scene-components.d.ts +4 -0
- package/dist/__tests__/helpers/register-scene-components.d.ts.map +1 -0
- package/dist/__tests__/helpers/render-asset-port.d.ts +15 -0
- package/dist/__tests__/helpers/render-asset-port.d.ts.map +1 -0
- package/dist/__tests__/helpers/scene-asset-port.d.ts +9 -0
- package/dist/__tests__/helpers/scene-asset-port.d.ts.map +1 -0
- package/dist/__tests__/helpers/tilemap-assets.d.ts +6 -0
- package/dist/__tests__/helpers/tilemap-assets.d.ts.map +1 -0
- package/dist/__tests__/instances.test-d.d.ts +2 -0
- package/dist/__tests__/instances.test-d.d.ts.map +1 -0
- package/dist/__tests__/light-attenuation-formula.test.d.ts +2 -0
- package/dist/__tests__/light-attenuation-formula.test.d.ts.map +1 -0
- package/dist/__tests__/light-casters-9-light.browser.test.d.ts +2 -0
- package/dist/__tests__/light-casters-9-light.browser.test.d.ts.map +1 -0
- package/dist/__tests__/light-gpu-byte-neutral.unit.test.d.ts +2 -0
- package/dist/__tests__/light-gpu-byte-neutral.unit.test.d.ts.map +1 -0
- package/dist/__tests__/light-snapshot.test-d.d.ts +2 -0
- package/dist/__tests__/light-snapshot.test-d.d.ts.map +1 -0
- package/dist/__tests__/lights-vec-negative.unit.test.d.ts +2 -0
- package/dist/__tests__/lights-vec-negative.unit.test.d.ts.map +1 -0
- package/dist/__tests__/lights.unit.test.d.ts +2 -0
- package/dist/__tests__/lights.unit.test.d.ts.map +1 -0
- package/dist/__tests__/m3-vec-negative.unit.test.d.ts +2 -0
- package/dist/__tests__/m3-vec-negative.unit.test.d.ts.map +1 -0
- package/dist/__tests__/m5-browser-dawn-sweep.integration.test.d.ts +2 -0
- package/dist/__tests__/m5-browser-dawn-sweep.integration.test.d.ts.map +1 -0
- package/dist/__tests__/material-derived-coordinates.integration.test.d.ts +2 -0
- package/dist/__tests__/material-derived-coordinates.integration.test.d.ts.map +1 -0
- package/dist/__tests__/material-texture-uv-scale.unit.test.d.ts +2 -0
- package/dist/__tests__/material-texture-uv-scale.unit.test.d.ts.map +1 -0
- package/dist/__tests__/materials.unit.test.d.ts +2 -0
- package/dist/__tests__/materials.unit.test.d.ts.map +1 -0
- package/dist/__tests__/mesh-ssbo-grow.unit.test.d.ts +2 -0
- package/dist/__tests__/mesh-ssbo-grow.unit.test.d.ts.map +1 -0
- package/dist/__tests__/mip-gate.unit.test.d.ts +2 -0
- package/dist/__tests__/mip-gate.unit.test.d.ts.map +1 -0
- package/dist/__tests__/msaa-sprite-pixel-diff.dawn.test.d.ts +2 -0
- package/dist/__tests__/msaa-sprite-pixel-diff.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/multi-uv-8set.e2e.dawn.test.d.ts +2 -0
- package/dist/__tests__/multi-uv-8set.e2e.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/name.unit.test.d.ts +2 -0
- package/dist/__tests__/name.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pass-selector.unit.test.d.ts +2 -0
- package/dist/__tests__/pass-selector.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pbr-pipeline.unit.test.d.ts +2 -0
- package/dist/__tests__/pbr-pipeline.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline-builder-msaa.unit.test.d.ts +2 -0
- package/dist/__tests__/pipeline-builder-msaa.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline-cache-keying.unit.test.d.ts +2 -0
- package/dist/__tests__/pipeline-cache-keying.unit.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline-layout-selection.test.d.ts +2 -0
- package/dist/__tests__/pipeline-layout-selection.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline-spec.test.d.ts +2 -0
- package/dist/__tests__/pipeline-spec.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline.unit.test.d.ts +2 -0
- package/dist/__tests__/pipeline.unit.test.d.ts.map +1 -0
- package/dist/__tests__/point-light-shadow.browser.test.d.ts +2 -0
- package/dist/__tests__/point-light-shadow.browser.test.d.ts.map +1 -0
- package/dist/__tests__/point-light-shadow.dawn.test.d.ts +2 -0
- package/dist/__tests__/point-light-shadow.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/point-light-shadow.unit.test.d.ts +2 -0
- package/dist/__tests__/point-light-shadow.unit.test.d.ts.map +1 -0
- package/dist/__tests__/post-process-error-codes.unit.test.d.ts +2 -0
- package/dist/__tests__/post-process-error-codes.unit.test.d.ts.map +1 -0
- package/dist/__tests__/post-process-params-component.unit.test.d.ts +2 -0
- package/dist/__tests__/post-process-params-component.unit.test.d.ts.map +1 -0
- package/dist/__tests__/post-process-params-extract.unit.test.d.ts +2 -0
- package/dist/__tests__/post-process-params-extract.unit.test.d.ts.map +1 -0
- package/dist/__tests__/post-process-reads-narrowing.test-d.d.ts +2 -0
- package/dist/__tests__/post-process-reads-narrowing.test-d.d.ts.map +1 -0
- package/dist/__tests__/post-process-register-roundtrip.dawn.test.d.ts +2 -0
- package/dist/__tests__/post-process-register-roundtrip.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/postprocess-camera-consistency.test.d.ts +2 -0
- package/dist/__tests__/postprocess-camera-consistency.test.d.ts.map +1 -0
- package/dist/__tests__/profiler-assembly.integration.test.d.ts +2 -0
- package/dist/__tests__/profiler-assembly.integration.test.d.ts.map +1 -0
- package/dist/__tests__/propagate-transforms.dawn.test.d.ts +2 -0
- package/dist/__tests__/propagate-transforms.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/record-worldid-isolation.test.d.ts +2 -0
- package/dist/__tests__/record-worldid-isolation.test.d.ts.map +1 -0
- package/dist/__tests__/recover-error-exhaustive.test-d.d.ts +11 -0
- package/dist/__tests__/recover-error-exhaustive.test-d.d.ts.map +1 -0
- package/dist/__tests__/render-error-exhaustive.test-d.d.ts +11 -0
- package/dist/__tests__/render-error-exhaustive.test-d.d.ts.map +1 -0
- package/dist/__tests__/render-feature-assembly.test-d.d.ts +2 -0
- package/dist/__tests__/render-feature-assembly.test-d.d.ts.map +1 -0
- package/dist/__tests__/render-feature-prepared-graphics-docs.unit.test.d.ts +2 -0
- package/dist/__tests__/render-feature-prepared-graphics-docs.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-feature-prepared-graphics.browser.test.d.ts +2 -0
- package/dist/__tests__/render-feature-prepared-graphics.browser.test.d.ts.map +1 -0
- package/dist/__tests__/render-feature-prepared-graphics.dawn.test.d.ts +2 -0
- package/dist/__tests__/render-feature-prepared-graphics.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/render-feature-prepared-graphics.integration.test.d.ts +2 -0
- package/dist/__tests__/render-feature-prepared-graphics.integration.test.d.ts.map +1 -0
- package/dist/__tests__/render-feature-prepared-graphics.regression.integration.test.d.ts +2 -0
- package/dist/__tests__/render-feature-prepared-graphics.regression.integration.test.d.ts.map +1 -0
- package/dist/__tests__/render-feature-recover.integration.test.d.ts +2 -0
- package/dist/__tests__/render-feature-recover.integration.test.d.ts.map +1 -0
- package/dist/__tests__/render-feature-runtime-surface.test-d.d.ts +2 -0
- package/dist/__tests__/render-feature-runtime-surface.test-d.d.ts.map +1 -0
- package/dist/__tests__/render-feature-zero-feature.browser.test.d.ts +2 -0
- package/dist/__tests__/render-feature-zero-feature.browser.test.d.ts.map +1 -0
- package/dist/__tests__/render-pipeline-trivial.dawn.test.d.ts +2 -0
- package/dist/__tests__/render-pipeline-trivial.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/render-schema-consumer.integration.test.d.ts +2 -0
- package/dist/__tests__/render-schema-consumer.integration.test.d.ts.map +1 -0
- package/dist/__tests__/render-schema-owner-boundary.unit.test.d.ts +2 -0
- package/dist/__tests__/render-schema-owner-boundary.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-cross-world-overlay-queue.test.d.ts +2 -0
- package/dist/__tests__/render-system-cross-world-overlay-queue.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-extract-dispatch-ordering.test.d.ts +2 -0
- package/dist/__tests__/render-system-extract-dispatch-ordering.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-extract.test.d.ts +2 -0
- package/dist/__tests__/render-system-extract.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-fold-bucket.unit.test.d.ts +2 -0
- package/dist/__tests__/render-system-fold-bucket.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-fold-fallback.unit.test.d.ts +2 -0
- package/dist/__tests__/render-system-fold-fallback.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-fold-material.unit.test.d.ts +2 -0
- package/dist/__tests__/render-system-fold-material.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-fold-metrics.unit.test.d.ts +2 -0
- package/dist/__tests__/render-system-fold-metrics.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-fold-modegate.unit.test.d.ts +2 -0
- package/dist/__tests__/render-system-fold-modegate.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-fold-scene-instances.unit.test.d.ts +2 -0
- package/dist/__tests__/render-system-fold-scene-instances.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-fold.dawn.test.d.ts +2 -0
- package/dist/__tests__/render-system-fold.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-mega.test.d.ts +2 -0
- package/dist/__tests__/render-system-mega.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-owner-split-contract.test.d.ts +2 -0
- package/dist/__tests__/render-system-owner-split-contract.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-record-multi-material-textureview.test.d.ts +2 -0
- package/dist/__tests__/render-system-record-multi-material-textureview.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-record-per-submesh-transparency.test.d.ts +2 -0
- package/dist/__tests__/render-system-record-per-submesh-transparency.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-record.test.d.ts +2 -0
- package/dist/__tests__/render-system-record.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-record.unit.test.d.ts +2 -0
- package/dist/__tests__/render-system-record.unit.test.d.ts.map +1 -0
- package/dist/__tests__/render-system-skin-bg.unit.test.d.ts +2 -0
- package/dist/__tests__/render-system-skin-bg.unit.test.d.ts.map +1 -0
- package/dist/__tests__/renderer-consumer.test-d.d.ts +2 -0
- package/dist/__tests__/renderer-consumer.test-d.d.ts.map +1 -0
- package/dist/__tests__/renderer-health.unit.test.d.ts +2 -0
- package/dist/__tests__/renderer-health.unit.test.d.ts.map +1 -0
- package/dist/__tests__/renderer-lifecycle.integration.test.d.ts +2 -0
- package/dist/__tests__/renderer-lifecycle.integration.test.d.ts.map +1 -0
- package/dist/__tests__/renderer-recover.unit.test.d.ts +2 -0
- package/dist/__tests__/renderer-recover.unit.test.d.ts.map +1 -0
- package/dist/__tests__/renderer-result.test-d.d.ts +2 -0
- package/dist/__tests__/renderer-result.test-d.d.ts.map +1 -0
- package/dist/__tests__/renderer-surface.unit.test.d.ts +2 -0
- package/dist/__tests__/renderer-surface.unit.test.d.ts.map +1 -0
- package/dist/__tests__/resolve-depth-only-view.test.d.ts +2 -0
- package/dist/__tests__/resolve-depth-only-view.test.d.ts.map +1 -0
- package/dist/__tests__/rhi-null-command-flow.unit.test.d.ts +2 -0
- package/dist/__tests__/rhi-null-command-flow.unit.test.d.ts.map +1 -0
- package/dist/__tests__/rhi-null-noop-behavior.unit.test.d.ts +2 -0
- package/dist/__tests__/rhi-null-noop-behavior.unit.test.d.ts.map +1 -0
- package/dist/__tests__/rhi-null-renderer-lifecycle.unit.test.d.ts +2 -0
- package/dist/__tests__/rhi-null-renderer-lifecycle.unit.test.d.ts.map +1 -0
- package/dist/__tests__/roots-to-scene-asset.test.d.ts +2 -0
- package/dist/__tests__/roots-to-scene-asset.test.d.ts.map +1 -0
- package/dist/__tests__/runtime-authority-scan.unit.test.d.ts +2 -0
- package/dist/__tests__/runtime-authority-scan.unit.test.d.ts.map +1 -0
- package/dist/__tests__/runtime-domain-residue.unit.test.d.ts +2 -0
- package/dist/__tests__/runtime-domain-residue.unit.test.d.ts.map +1 -0
- package/dist/__tests__/scene-equirect-roundtrip.test.d.ts +2 -0
- package/dist/__tests__/scene-equirect-roundtrip.test.d.ts.map +1 -0
- package/dist/__tests__/scene-instance-bridge.integration.test.d.ts +2 -0
- package/dist/__tests__/scene-instance-bridge.integration.test.d.ts.map +1 -0
- package/dist/__tests__/scene-instance-component.test.d.ts +2 -0
- package/dist/__tests__/scene-instance-component.test.d.ts.map +1 -0
- package/dist/__tests__/scene-instantiate-cycle-runtime.test.d.ts +2 -0
- package/dist/__tests__/scene-instantiate-cycle-runtime.test.d.ts.map +1 -0
- package/dist/__tests__/serialize-scene-asset.test.d.ts +2 -0
- package/dist/__tests__/serialize-scene-asset.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-caster-empty-schema.test.d.ts +2 -0
- package/dist/__tests__/shadow-caster-empty-schema.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-atlas.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-atlas.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-blend.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-blend.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-cascade-loadop.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-cascade-loadop.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-caster-membership.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-caster-membership.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-component.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-component.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-extract.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-extract.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-orthoz.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-orthoz.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-pssm.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-pssm.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-runtime-vary.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-runtime-vary.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-shader.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-shader.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-temporal.browser.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-temporal.browser.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-tile-consistency.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-tile-consistency.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-ubo.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-ubo.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-ubo.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-ubo.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-viewport.browser.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-viewport.browser.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-csm-viewport.test.d.ts +2 -0
- package/dist/__tests__/shadow-csm-viewport.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-fields-observable.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-fields-observable.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-m1.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-m1.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-m2.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-m2.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-m3-calibrate-run.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-m3-calibrate-run.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-m3-calibrate.d.ts +83 -0
- package/dist/__tests__/shadow-m3-calibrate.d.ts.map +1 -0
- package/dist/__tests__/shadow-m3.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-m3.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-mesh-ssbo-order.test.d.ts +2 -0
- package/dist/__tests__/shadow-mesh-ssbo-order.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-probe-control-flow.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-probe-control-flow.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/shadow-quality-metrics.dawn.test.d.ts +2 -0
- package/dist/__tests__/shadow-quality-metrics.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/skin-error-exhaustive.test-d.d.ts +11 -0
- package/dist/__tests__/skin-error-exhaustive.test-d.d.ts.map +1 -0
- package/dist/__tests__/skin-errors.unit.test.d.ts +2 -0
- package/dist/__tests__/skin-errors.unit.test.d.ts.map +1 -0
- package/dist/__tests__/skin-extract-getarrayview-count.unit.test.d.ts +2 -0
- package/dist/__tests__/skin-extract-getarrayview-count.unit.test.d.ts.map +1 -0
- package/dist/__tests__/skin-parented-double-transform.test.d.ts +14 -0
- package/dist/__tests__/skin-parented-double-transform.test.d.ts.map +1 -0
- package/dist/__tests__/skylight-lazy-projection.test.d.ts +2 -0
- package/dist/__tests__/skylight-lazy-projection.test.d.ts.map +1 -0
- package/dist/__tests__/sparse-tag-scene-roundtrip.test.d.ts +2 -0
- package/dist/__tests__/sparse-tag-scene-roundtrip.test.d.ts.map +1 -0
- package/dist/__tests__/sprite-cullmode-flip.dawn.test.d.ts +2 -0
- package/dist/__tests__/sprite-cullmode-flip.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/sprite-lit-bgl-byte-identical.test.d.ts +2 -0
- package/dist/__tests__/sprite-lit-bgl-byte-identical.test.d.ts.map +1 -0
- package/dist/__tests__/sprite-lit-extract-branch.test.d.ts +2 -0
- package/dist/__tests__/sprite-lit-extract-branch.test.d.ts.map +1 -0
- package/dist/__tests__/sprite-lit-pipeline-cache-key.test.d.ts +2 -0
- package/dist/__tests__/sprite-lit-pipeline-cache-key.test.d.ts.map +1 -0
- package/dist/__tests__/sprite-param-values.test-d.d.ts +2 -0
- package/dist/__tests__/sprite-param-values.test-d.d.ts.map +1 -0
- package/dist/__tests__/ssao-bgl.test.d.ts +2 -0
- package/dist/__tests__/ssao-bgl.test.d.ts.map +1 -0
- package/dist/__tests__/ssao-buffers.test.d.ts +2 -0
- package/dist/__tests__/ssao-buffers.test.d.ts.map +1 -0
- package/dist/__tests__/ssao-data.test.d.ts +2 -0
- package/dist/__tests__/ssao-data.test.d.ts.map +1 -0
- package/dist/__tests__/ssao-errors.test.d.ts +2 -0
- package/dist/__tests__/ssao-errors.test.d.ts.map +1 -0
- package/dist/__tests__/ssao-manifest.test.d.ts +2 -0
- package/dist/__tests__/ssao-manifest.test.d.ts.map +1 -0
- package/dist/__tests__/ssao-passes.test.d.ts +2 -0
- package/dist/__tests__/ssao-passes.test.d.ts.map +1 -0
- package/dist/__tests__/ssao-pipelines.test.d.ts +2 -0
- package/dist/__tests__/ssao-pipelines.test.d.ts.map +1 -0
- package/dist/__tests__/ssao-uniform-layout.test.d.ts +2 -0
- package/dist/__tests__/ssao-uniform-layout.test.d.ts.map +1 -0
- package/dist/__tests__/static-rig.integration.test.d.ts +2 -0
- package/dist/__tests__/static-rig.integration.test.d.ts.map +1 -0
- package/dist/__tests__/stencil-outline-pixel.dawn.test.d.ts +2 -0
- package/dist/__tests__/stencil-outline-pixel.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/swap-chain-format.unit.test.d.ts +2 -0
- package/dist/__tests__/swap-chain-format.unit.test.d.ts.map +1 -0
- package/dist/__tests__/systems.unit.test.d.ts +2 -0
- package/dist/__tests__/systems.unit.test.d.ts.map +1 -0
- package/dist/__tests__/temporal-off-path.unit.test.d.ts +2 -0
- package/dist/__tests__/temporal-off-path.unit.test.d.ts.map +1 -0
- package/dist/__tests__/text.unit.test.d.ts +2 -0
- package/dist/__tests__/text.unit.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-childed-despawn.test.d.ts +2 -0
- package/dist/__tests__/tilemap-childed-despawn.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-chunk-extract.overhang-bounds.test.d.ts +2 -0
- package/dist/__tests__/tilemap-chunk-extract.overhang-bounds.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-chunk-extract.propagate-order.test.d.ts +2 -0
- package/dist/__tests__/tilemap-chunk-extract.propagate-order.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-chunk-extract.terrain-rewrite.unit.test.d.ts +2 -0
- package/dist/__tests__/tilemap-chunk-extract.terrain-rewrite.unit.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-chunk-extract.test.d.ts +2 -0
- package/dist/__tests__/tilemap-chunk-extract.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-chunk-extract.unit.test.d.ts +2 -0
- package/dist/__tests__/tilemap-chunk-extract.unit.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-chunk-y-sort-bench.unit.test.d.ts +2 -0
- package/dist/__tests__/tilemap-chunk-y-sort-bench.unit.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-dirty-rebuild.test.d.ts +2 -0
- package/dist/__tests__/tilemap-dirty-rebuild.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-material-cache.test.d.ts +2 -0
- package/dist/__tests__/tilemap-material-cache.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-multi-atlas.test.d.ts +2 -0
- package/dist/__tests__/tilemap-multi-atlas.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-owner-boundary.unit.test.d.ts +2 -0
- package/dist/__tests__/tilemap-owner-boundary.unit.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-region-pack.test.d.ts +2 -0
- package/dist/__tests__/tilemap-region-pack.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-spawn-flip-pivot.test.d.ts +2 -0
- package/dist/__tests__/tilemap-spawn-flip-pivot.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-uv-inset.test.d.ts +2 -0
- package/dist/__tests__/tilemap-uv-inset.test.d.ts.map +1 -0
- package/dist/__tests__/tilemap-validate.test.d.ts +2 -0
- package/dist/__tests__/tilemap-validate.test.d.ts.map +1 -0
- package/dist/__tests__/transform-hierarchy-pixel-diff.dawn.test.d.ts +2 -0
- package/dist/__tests__/transform-hierarchy-pixel-diff.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/transient-fields-collect.test.d.ts +2 -0
- package/dist/__tests__/transient-fields-collect.test.d.ts.map +1 -0
- package/dist/__tests__/transitional-authoring.integration.test.d.ts +2 -0
- package/dist/__tests__/transitional-authoring.integration.test.d.ts.map +1 -0
- package/dist/__tests__/transparent-sort-tilemap-sprite-interleave.test.d.ts +2 -0
- package/dist/__tests__/transparent-sort-tilemap-sprite-interleave.test.d.ts.map +1 -0
- package/dist/__tests__/variant-selection.unit.test.d.ts +2 -0
- package/dist/__tests__/variant-selection.unit.test.d.ts.map +1 -0
- package/dist/__tests__/vertex-attribute-layout.test.d.ts +2 -0
- package/dist/__tests__/vertex-attribute-layout.test.d.ts.map +1 -0
- package/dist/__tests__/video-cache-regression.unit.test.d.ts +2 -0
- package/dist/__tests__/video-cache-regression.unit.test.d.ts.map +1 -0
- package/dist/__tests__/video-extract-bindgroup.dawn.test.d.ts +2 -0
- package/dist/__tests__/video-extract-bindgroup.dawn.test.d.ts.map +1 -0
- package/dist/__tests__/view-ubo-layout.browser.test.d.ts +2 -0
- package/dist/__tests__/view-ubo-layout.browser.test.d.ts.map +1 -0
- package/dist/__tests__/world-entity-key.test.d.ts +2 -0
- package/dist/__tests__/world-entity-key.test.d.ts.map +1 -0
- package/dist/__tests__/world-scene-despawn.test.d.ts +2 -0
- package/dist/__tests__/world-scene-despawn.test.d.ts.map +1 -0
- package/dist/__tests__/world-scene-instantiate.test.d.ts +2 -0
- package/dist/__tests__/world-scene-instantiate.test.d.ts.map +1 -0
- package/dist/__tests__/world-scene-nested.test.d.ts +2 -0
- package/dist/__tests__/world-scene-nested.test.d.ts.map +1 -0
- package/dist/__tests__/world-scene-override.test.d.ts +2 -0
- package/dist/__tests__/world-scene-override.test.d.ts.map +1 -0
- package/dist/__tests__/zero-compression-zero-load.integration.test.d.ts +2 -0
- package/dist/__tests__/zero-compression-zero-load.integration.test.d.ts.map +1 -0
- package/dist/asset-contributions.d.ts +3 -0
- package/dist/asset-contributions.d.ts.map +1 -0
- package/dist/asset-kind.d.ts +4 -0
- package/dist/asset-kind.d.ts.map +1 -0
- package/dist/collect-scene-asset.d.ts +16 -0
- package/dist/collect-scene-asset.d.ts.map +1 -0
- package/dist/components/__tests__/camera.test-d.d.ts +2 -0
- package/dist/components/__tests__/camera.test-d.d.ts.map +1 -0
- package/dist/components/__tests__/camera.test.d.ts +2 -0
- package/dist/components/__tests__/camera.test.d.ts.map +1 -0
- package/dist/components/__tests__/child-of.test.d.ts +2 -0
- package/dist/components/__tests__/child-of.test.d.ts.map +1 -0
- package/dist/components/__tests__/sprite-animation.test-d.d.ts +2 -0
- package/dist/components/__tests__/sprite-animation.test-d.d.ts.map +1 -0
- package/dist/components/__tests__/sprite-instances.unit.test.d.ts +2 -0
- package/dist/components/__tests__/sprite-instances.unit.test.d.ts.map +1 -0
- package/dist/components/__tests__/sprite-region-override.test-d.d.ts +2 -0
- package/dist/components/__tests__/sprite-region-override.test-d.d.ts.map +1 -0
- package/dist/components/__tests__/tile-layer.test-d.d.ts +2 -0
- package/dist/components/__tests__/tile-layer.test-d.d.ts.map +1 -0
- package/dist/components/__tests__/tile-layer.unit.test.d.ts +2 -0
- package/dist/components/__tests__/tile-layer.unit.test.d.ts.map +1 -0
- package/dist/components/__tests__/tilemap.test-d.d.ts +2 -0
- package/dist/components/__tests__/tilemap.test-d.d.ts.map +1 -0
- package/dist/components/__tests__/transform-vec-schema.test.d.ts +2 -0
- package/dist/components/__tests__/transform-vec-schema.test.d.ts.map +1 -0
- package/dist/create-renderer-env-classify.d.ts +21 -0
- package/dist/create-renderer-env-classify.d.ts.map +1 -0
- package/dist/createRenderer.d.ts +3 -0
- package/dist/createRenderer.d.ts.map +1 -0
- package/dist/dev-import-transport.d.ts +24 -0
- package/dist/dev-import-transport.d.ts.map +1 -0
- package/dist/errors/environment.d.ts +2 -0
- package/dist/errors/environment.d.ts.map +1 -0
- package/dist/errors/sprite-animation.d.ts +3 -0
- package/dist/errors/sprite-animation.d.ts.map +1 -0
- package/dist/index.d.ts +280 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +814 -0
- package/dist/index.mjs.map +1 -0
- package/dist/scene-asset-port.d.ts +17 -0
- package/dist/scene-asset-port.d.ts.map +1 -0
- package/dist/scene-collect-errors.d.ts +22 -0
- package/dist/scene-collect-errors.d.ts.map +1 -0
- package/dist/scene-instances/__tests__/post-spawn-resolve-joints.unit.test.d.ts +2 -0
- package/dist/scene-instances/__tests__/post-spawn-resolve-joints.unit.test.d.ts.map +1 -0
- package/dist/scene-utils/mount-override-fold.d.ts +16 -0
- package/dist/scene-utils/mount-override-fold.d.ts.map +1 -0
- package/dist/shadow-m3-calibrate.d.ts +40 -0
- package/dist/shadow-m3-calibrate.d.ts.map +1 -0
- package/dist/sprite-param-values.d.ts +62 -0
- package/dist/sprite-param-values.d.ts.map +1 -0
- package/dist/systems/__tests__/_xorshift.d.ts +28 -0
- package/dist/systems/__tests__/_xorshift.d.ts.map +1 -0
- package/dist/systems/__tests__/argsort-in-place.test.d.ts +2 -0
- package/dist/systems/__tests__/argsort-in-place.test.d.ts.map +1 -0
- package/dist/systems/__tests__/transparent-sort.bench.d.ts +2 -0
- package/dist/systems/__tests__/transparent-sort.bench.d.ts.map +1 -0
- package/dist/systems/sprite-animation-tick.d.ts +50 -0
- package/dist/systems/sprite-animation-tick.d.ts.map +1 -0
- package/dist/systems/tonemap.d.ts +27 -0
- package/dist/systems/tonemap.d.ts.map +1 -0
- package/dist/systems/transparent-sort.d.ts +51 -0
- package/dist/systems/transparent-sort.d.ts.map +1 -0
- package/dist/tilemap-register-validate.d.ts +21 -0
- package/dist/tilemap-register-validate.d.ts.map +1 -0
- package/package.json +100 -0
- package/src/__tests__/active-camera.unit.test.ts +193 -0
- package/src/__tests__/animation-graph-add.unit.test.ts +67 -0
- package/src/__tests__/animation-graph-asset-union.test-d.ts +83 -0
- package/src/__tests__/animation-graph-blend.unit.test.ts +66 -0
- package/src/__tests__/animation-graph-build.unit.test.ts +100 -0
- package/src/__tests__/animation-graph-clip-missing.unit.test.ts +62 -0
- package/src/__tests__/animation-graph-clip.unit.test.ts +77 -0
- package/src/__tests__/animation-graph-default-path.integration.test.ts +65 -0
- package/src/__tests__/animation-graph-describe.unit.test.ts +68 -0
- package/src/__tests__/animation-graph-errors.unit.test.ts +75 -0
- package/src/__tests__/animation-graph-nesting.unit.test.ts +84 -0
- package/src/__tests__/animation-graph-orthogonal-weight.unit.test.ts +98 -0
- package/src/__tests__/animation-graph-slot-fill.unit.test.ts +95 -0
- package/src/__tests__/animation-nslot.unit.test.ts +63 -0
- package/src/__tests__/animation-slot-length-guard.unit.test.ts +81 -0
- package/src/__tests__/append-injection.test.ts +105 -0
- package/src/__tests__/asi-world-baseline-export.unit.test.ts +173 -0
- package/src/__tests__/asi-world-ci-config.unit.test.ts +186 -0
- package/src/__tests__/asi-world-smoke.unit.test.ts +192 -0
- package/src/__tests__/asset-registry-scene.test-d.ts +49 -0
- package/src/__tests__/bind-group-cache-binding.test.ts +2513 -0
- package/src/__tests__/bind-group-cache-frame.test.ts +1511 -0
- package/src/__tests__/bind-group-cache-keying.test.ts +295 -0
- package/src/__tests__/block-upload-math.unit.test.ts +192 -0
- package/src/__tests__/builtin-shader-register-e2e.test.ts +151 -0
- package/src/__tests__/cache-false-eviction.test.ts +126 -0
- package/src/__tests__/cache-isolation-matrix.test.ts +182 -0
- package/src/__tests__/camera-clear-snapshot-equivalence.unit.test.ts +76 -0
- package/src/__tests__/clamp-to-last.e2e.browser.test.ts +183 -0
- package/src/__tests__/clamp-to-last.e2e.dawn.test.ts +325 -0
- package/src/__tests__/collect-scene-asset.test.ts +876 -0
- package/src/__tests__/collect-subtree.unit.test.ts +76 -0
- package/src/__tests__/collect-transient-children-absent.test.ts +154 -0
- package/src/__tests__/collect-transient-roundtrip.test.ts +245 -0
- package/src/__tests__/collect-transient-skip.test.ts +165 -0
- package/src/__tests__/components.test-d.ts +236 -0
- package/src/__tests__/components.unit.test.ts +2665 -0
- package/src/__tests__/composite-skybox-cross-world.browser.test.ts +378 -0
- package/src/__tests__/create-renderer-assembly.integration.test.ts +42 -0
- package/src/__tests__/createRenderer-dispose.test.ts +416 -0
- package/src/__tests__/createRenderer-requiredFeatures.unit.test.ts +134 -0
- package/src/__tests__/createRenderer.browser-no-webgpu.test.ts +59 -0
- package/src/__tests__/createRenderer.unit.test.ts +231 -0
- package/src/__tests__/dawn/hello-sprite-nineslice-section.dawn.test.ts +393 -0
- package/src/__tests__/dawn/ibl-compose.dawn.test.ts +151 -0
- package/src/__tests__/dawn/ibl-modules-compose.dawn.test.ts +223 -0
- package/src/__tests__/dawn/ibl-precompute-readback.dawn.test.ts +241 -0
- package/src/__tests__/dawn/instances-per-instance-pbr.dawn.test.ts +611 -0
- package/src/__tests__/dawn/instances-shadow.dawn.test.ts +443 -0
- package/src/__tests__/dawn/instances-uniform-fallback.dawn.test.ts +313 -0
- package/src/__tests__/dawn/material-alpha.dawn.test.ts +304 -0
- package/src/__tests__/dawn/material-cooked-fixture.dawn.test.ts +64 -0
- package/src/__tests__/dawn/mesh-ssbo-normal-matrix.dawn.test.ts +106 -0
- package/src/__tests__/dawn/mesh-update.dawn.test.ts +342 -0
- package/src/__tests__/dawn/mipmap-srgb-linear.dawn.test.ts +117 -0
- package/src/__tests__/dawn/per-entity-material-texture-isolation.dawn.test.ts +567 -0
- package/src/__tests__/dawn/sprite-nineslice-mesh-bind.dawn.test.ts +251 -0
- package/src/__tests__/dawn/tonemap.dawn.test.ts +107 -0
- package/src/__tests__/dawn/user-handle-mesh-render.dawn.test.ts +291 -0
- package/src/__tests__/dawn/user-mesh-upload.dawn.test.ts +204 -0
- package/src/__tests__/dawn/zero-manifest-mesh-fail.dawn.test.ts +122 -0
- package/src/__tests__/dawn/zero-manifest-ready.dawn.test.ts +99 -0
- package/src/__tests__/demo-sortscope-migration.unit.test.ts +115 -0
- package/src/__tests__/derive-bgl-integration.test.ts +398 -0
- package/src/__tests__/derive-render-data-skin.unit.test.ts +90 -0
- package/src/__tests__/dispatch-fullscreen-pass-composite-swapchain.dawn.test.ts +204 -0
- package/src/__tests__/dispatch-fullscreen-pass-custom-reads.dawn.test.ts +297 -0
- package/src/__tests__/dispatch-fullscreen-pass-depth-composite.dawn.test.ts +271 -0
- package/src/__tests__/dispatch-fullscreen-pass-params.unit.test.ts +321 -0
- package/src/__tests__/draw-published.ts +10 -0
- package/src/__tests__/environment-error-exhaustive.test-d.ts +28 -0
- package/src/__tests__/equirect-bc6h.integration.test.ts +35 -0
- package/src/__tests__/error-exhaustive.test.ts +220 -0
- package/src/__tests__/extract-frames-graceful-degradation.test.ts +298 -0
- package/src/__tests__/extract-frames-merge.test.ts +643 -0
- package/src/__tests__/extract-frames-reset.test.ts +158 -0
- package/src/__tests__/extract-record-no-hardcoded-texture-fields.test.ts +92 -0
- package/src/__tests__/fog-producer-manifest.ts +162 -0
- package/src/__tests__/fog-producer-pixel-probe.ts +263 -0
- package/src/__tests__/fog-producer-pixel.browser.test.ts +26 -0
- package/src/__tests__/fog-producer-pixel.dawn.test.ts +55 -0
- package/src/__tests__/fog-producer-real-fixture.ts +805 -0
- package/src/__tests__/fog-producer-vfx-effect.ts +272 -0
- package/src/__tests__/fog-scene-asset.integration.test.ts +123 -0
- package/src/__tests__/frame-targets-shadow-regression.unit.test.ts +28 -0
- package/src/__tests__/fullscreen-post-bgl-depth.test.ts +131 -0
- package/src/__tests__/fullscreen-post-depth-params-arity.test.ts +84 -0
- package/src/__tests__/fullscreen-post-process-pass.dawn.test.ts +320 -0
- package/src/__tests__/fullscreen-post-process-pass.unit.test.ts +107 -0
- package/src/__tests__/fxaa-pixel-diff.dawn.test.ts +292 -0
- package/src/__tests__/fxaa-validation-and-resize.dawn.test.ts +358 -0
- package/src/__tests__/fxaa-zero-overhead.dawn.test.ts +254 -0
- package/src/__tests__/geometry.unit.test.ts +2265 -0
- package/src/__tests__/glyph-tilemap-worldid.test.ts +146 -0
- package/src/__tests__/gpu-resource-cow-survivor.test.ts +388 -0
- package/src/__tests__/gpu-resource-store-cubemap-status.test.ts +207 -0
- package/src/__tests__/gpu-resource.test.ts +1206 -0
- package/src/__tests__/hdrp-bgl-slots.browser.test.ts +50 -0
- package/src/__tests__/hdrp-buffers.unit.test.ts +70 -0
- package/src/__tests__/hdrp-execute-filter.unit.test.ts +200 -0
- package/src/__tests__/hdrp-gbuffer-attachments.browser.test.ts +64 -0
- package/src/__tests__/hdrp-pipeline-asset-type.test-d.ts +139 -0
- package/src/__tests__/hdrp-ssao-boundary.test.ts +237 -0
- package/src/__tests__/hdrp-ssao-falsification.test.ts +289 -0
- package/src/__tests__/hello-tilemap-smoke-upgrade.unit.test.ts +147 -0
- package/src/__tests__/helpers/mock-shader-registry.ts +139 -0
- package/src/__tests__/helpers/primitive-world.ts +91 -0
- package/src/__tests__/helpers/register-runtime-components.ts +69 -0
- package/src/__tests__/helpers/register-scene-components.ts +10 -0
- package/src/__tests__/helpers/render-asset-port.ts +65 -0
- package/src/__tests__/helpers/scene-asset-port.ts +23 -0
- package/src/__tests__/helpers/tilemap-assets.ts +29 -0
- package/src/__tests__/instances.test-d.ts +61 -0
- package/src/__tests__/light-attenuation-formula.test.ts +171 -0
- package/src/__tests__/light-casters-9-light.browser.test.ts +880 -0
- package/src/__tests__/light-gpu-byte-neutral.unit.test.ts +163 -0
- package/src/__tests__/light-snapshot.test-d.ts +84 -0
- package/src/__tests__/lights-vec-negative.unit.test.ts +52 -0
- package/src/__tests__/lights.unit.test.ts +2936 -0
- package/src/__tests__/m3-vec-negative.unit.test.ts +45 -0
- package/src/__tests__/m5-browser-dawn-sweep.integration.test.ts +8 -0
- package/src/__tests__/material-derived-coordinates.integration.test.ts +100 -0
- package/src/__tests__/material-texture-uv-scale.unit.test.ts +49 -0
- package/src/__tests__/materials.unit.test.ts +271 -0
- package/src/__tests__/mesh-ssbo-grow.unit.test.ts +244 -0
- package/src/__tests__/mip-gate.unit.test.ts +79 -0
- package/src/__tests__/msaa-sprite-pixel-diff.dawn.test.ts +406 -0
- package/src/__tests__/multi-uv-8set.e2e.dawn.test.ts +296 -0
- package/src/__tests__/name.unit.test.ts +123 -0
- package/src/__tests__/node-shims.d.ts +33 -0
- package/src/__tests__/pass-selector.unit.test.ts +102 -0
- package/src/__tests__/pbr-pipeline.unit.test.ts +204 -0
- package/src/__tests__/pipeline-builder-msaa.unit.test.ts +224 -0
- package/src/__tests__/pipeline-cache-keying.unit.test.ts +316 -0
- package/src/__tests__/pipeline-layout-selection.test.ts +298 -0
- package/src/__tests__/pipeline-spec.test.ts +1061 -0
- package/src/__tests__/pipeline.unit.test.ts +6974 -0
- package/src/__tests__/point-light-shadow.browser.test.ts +278 -0
- package/src/__tests__/point-light-shadow.dawn.test.ts +822 -0
- package/src/__tests__/point-light-shadow.unit.test.ts +716 -0
- package/src/__tests__/post-process-error-codes.unit.test.ts +158 -0
- package/src/__tests__/post-process-params-component.unit.test.ts +153 -0
- package/src/__tests__/post-process-params-extract.unit.test.ts +100 -0
- package/src/__tests__/post-process-reads-narrowing.test-d.ts +52 -0
- package/src/__tests__/post-process-register-roundtrip.dawn.test.ts +230 -0
- package/src/__tests__/postprocess-camera-consistency.test.ts +378 -0
- package/src/__tests__/profiler-assembly.integration.test.ts +74 -0
- package/src/__tests__/propagate-transforms.dawn.test.ts +253 -0
- package/src/__tests__/record-worldid-isolation.test.ts +247 -0
- package/src/__tests__/recover-error-exhaustive.test-d.ts +35 -0
- package/src/__tests__/render-error-exhaustive.test-d.ts +245 -0
- package/src/__tests__/render-feature-assembly.test-d.ts +28 -0
- package/src/__tests__/render-feature-prepared-graphics-docs.unit.test.ts +124 -0
- package/src/__tests__/render-feature-prepared-graphics.browser.test.ts +582 -0
- package/src/__tests__/render-feature-prepared-graphics.dawn.test.ts +602 -0
- package/src/__tests__/render-feature-prepared-graphics.integration.test.ts +264 -0
- package/src/__tests__/render-feature-prepared-graphics.regression.integration.test.ts +361 -0
- package/src/__tests__/render-feature-recover.integration.test.ts +142 -0
- package/src/__tests__/render-feature-runtime-surface.test-d.ts +45 -0
- package/src/__tests__/render-feature-zero-feature.browser.test.ts +152 -0
- package/src/__tests__/render-pipeline-trivial.dawn.test.ts +525 -0
- package/src/__tests__/render-schema-consumer.integration.test.ts +25 -0
- package/src/__tests__/render-schema-owner-boundary.unit.test.ts +26 -0
- package/src/__tests__/render-system-cross-world-overlay-queue.test.ts +172 -0
- package/src/__tests__/render-system-extract-dispatch-ordering.test.ts +322 -0
- package/src/__tests__/render-system-extract.test.ts +1399 -0
- package/src/__tests__/render-system-fold-bucket.unit.test.ts +288 -0
- package/src/__tests__/render-system-fold-fallback.unit.test.ts +148 -0
- package/src/__tests__/render-system-fold-material.unit.test.ts +145 -0
- package/src/__tests__/render-system-fold-metrics.unit.test.ts +170 -0
- package/src/__tests__/render-system-fold-modegate.unit.test.ts +166 -0
- package/src/__tests__/render-system-fold-scene-instances.unit.test.ts +51 -0
- package/src/__tests__/render-system-fold.dawn.test.ts +306 -0
- package/src/__tests__/render-system-mega.test.ts +1578 -0
- package/src/__tests__/render-system-owner-split-contract.test.ts +200 -0
- package/src/__tests__/render-system-record-multi-material-textureview.test.ts +684 -0
- package/src/__tests__/render-system-record-per-submesh-transparency.test.ts +464 -0
- package/src/__tests__/render-system-record.test.ts +1505 -0
- package/src/__tests__/render-system-record.unit.test.ts +270 -0
- package/src/__tests__/render-system-skin-bg.unit.test.ts +147 -0
- package/src/__tests__/renderer-consumer.test-d.ts +5 -0
- package/src/__tests__/renderer-health.unit.test.ts +935 -0
- package/src/__tests__/renderer-lifecycle.integration.test.ts +29 -0
- package/src/__tests__/renderer-recover.unit.test.ts +1075 -0
- package/src/__tests__/renderer-result.test-d.ts +26 -0
- package/src/__tests__/renderer-surface.unit.test.ts +416 -0
- package/src/__tests__/resolve-depth-only-view.test.ts +173 -0
- package/src/__tests__/rhi-null-command-flow.unit.test.ts +273 -0
- package/src/__tests__/rhi-null-noop-behavior.unit.test.ts +196 -0
- package/src/__tests__/rhi-null-renderer-lifecycle.unit.test.ts +182 -0
- package/src/__tests__/roots-to-scene-asset.test.ts +605 -0
- package/src/__tests__/runtime-authority-scan.unit.test.ts +19 -0
- package/src/__tests__/runtime-domain-residue.unit.test.ts +13 -0
- package/src/__tests__/scene-equirect-roundtrip.test.ts +33 -0
- package/src/__tests__/scene-instance-bridge.integration.test.ts +8 -0
- package/src/__tests__/scene-instance-component.test.ts +45 -0
- package/src/__tests__/scene-instantiate-cycle-runtime.test.ts +66 -0
- package/src/__tests__/serialize-scene-asset.test.ts +336 -0
- package/src/__tests__/shadow-caster-empty-schema.test.ts +83 -0
- package/src/__tests__/shadow-csm-atlas.test.ts +178 -0
- package/src/__tests__/shadow-csm-blend.dawn.test.ts +230 -0
- package/src/__tests__/shadow-csm-cascade-loadop.test.ts +334 -0
- package/src/__tests__/shadow-csm-caster-membership.test.ts +232 -0
- package/src/__tests__/shadow-csm-component.test.ts +286 -0
- package/src/__tests__/shadow-csm-extract.test.ts +334 -0
- package/src/__tests__/shadow-csm-orthoz.test.ts +151 -0
- package/src/__tests__/shadow-csm-pssm.test.ts +142 -0
- package/src/__tests__/shadow-csm-runtime-vary.dawn.test.ts +198 -0
- package/src/__tests__/shadow-csm-shader.dawn.test.ts +358 -0
- package/src/__tests__/shadow-csm-temporal.browser.test.ts +145 -0
- package/src/__tests__/shadow-csm-tile-consistency.test.ts +419 -0
- package/src/__tests__/shadow-csm-ubo.dawn.test.ts +293 -0
- package/src/__tests__/shadow-csm-ubo.test.ts +300 -0
- package/src/__tests__/shadow-csm-viewport.browser.test.ts +85 -0
- package/src/__tests__/shadow-csm-viewport.test.ts +74 -0
- package/src/__tests__/shadow-fields-observable.dawn.test.ts +615 -0
- package/src/__tests__/shadow-m1.dawn.test.ts +105 -0
- package/src/__tests__/shadow-m2.dawn.test.ts +394 -0
- package/src/__tests__/shadow-m3-calibrate-run.dawn.test.ts +248 -0
- package/src/__tests__/shadow-m3-calibrate.ts +203 -0
- package/src/__tests__/shadow-m3.dawn.test.ts +288 -0
- package/src/__tests__/shadow-mesh-ssbo-order.test.ts +64 -0
- package/src/__tests__/shadow-probe-control-flow.dawn.test.ts +264 -0
- package/src/__tests__/shadow-quality-metrics.dawn.test.ts +324 -0
- package/src/__tests__/skin-error-exhaustive.test-d.ts +74 -0
- package/src/__tests__/skin-errors.unit.test.ts +10 -0
- package/src/__tests__/skin-extract-getarrayview-count.unit.test.ts +23 -0
- package/src/__tests__/skin-parented-double-transform.test.ts +268 -0
- package/src/__tests__/skylight-lazy-projection.test.ts +359 -0
- package/src/__tests__/sparse-tag-scene-roundtrip.test.ts +43 -0
- package/src/__tests__/sprite-cullmode-flip.dawn.test.ts +470 -0
- package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +141 -0
- package/src/__tests__/sprite-lit-extract-branch.test.ts +117 -0
- package/src/__tests__/sprite-lit-pipeline-cache-key.test.ts +123 -0
- package/src/__tests__/sprite-param-values.test-d.ts +104 -0
- package/src/__tests__/ssao-bgl.test.ts +347 -0
- package/src/__tests__/ssao-buffers.test.ts +211 -0
- package/src/__tests__/ssao-data.test.ts +121 -0
- package/src/__tests__/ssao-errors.test.ts +162 -0
- package/src/__tests__/ssao-manifest.test.ts +109 -0
- package/src/__tests__/ssao-passes.test.ts +1229 -0
- package/src/__tests__/ssao-pipelines.test.ts +275 -0
- package/src/__tests__/ssao-uniform-layout.test.ts +178 -0
- package/src/__tests__/static-rig.integration.test.ts +9 -0
- package/src/__tests__/stencil-outline-pixel.dawn.test.ts +295 -0
- package/src/__tests__/swap-chain-format.unit.test.ts +117 -0
- package/src/__tests__/systems.unit.test.ts +9957 -0
- package/src/__tests__/temporal-off-path.unit.test.ts +69 -0
- package/src/__tests__/text.unit.test.ts +466 -0
- package/src/__tests__/tilemap-childed-despawn.test.ts +468 -0
- package/src/__tests__/tilemap-chunk-extract.overhang-bounds.test.ts +314 -0
- package/src/__tests__/tilemap-chunk-extract.propagate-order.test.ts +80 -0
- package/src/__tests__/tilemap-chunk-extract.terrain-rewrite.unit.test.ts +424 -0
- package/src/__tests__/tilemap-chunk-extract.test.ts +236 -0
- package/src/__tests__/tilemap-chunk-extract.unit.test.ts +383 -0
- package/src/__tests__/tilemap-chunk-y-sort-bench.unit.test.ts +109 -0
- package/src/__tests__/tilemap-dirty-rebuild.test.ts +300 -0
- package/src/__tests__/tilemap-material-cache.test.ts +137 -0
- package/src/__tests__/tilemap-multi-atlas.test.ts +237 -0
- package/src/__tests__/tilemap-owner-boundary.unit.test.ts +63 -0
- package/src/__tests__/tilemap-region-pack.test.ts +153 -0
- package/src/__tests__/tilemap-spawn-flip-pivot.test.ts +274 -0
- package/src/__tests__/tilemap-uv-inset.test.ts +204 -0
- package/src/__tests__/tilemap-validate.test.ts +201 -0
- package/src/__tests__/transform-hierarchy-pixel-diff.dawn.test.ts +274 -0
- package/src/__tests__/transient-fields-collect.test.ts +203 -0
- package/src/__tests__/transitional-authoring.integration.test.ts +7 -0
- package/src/__tests__/transparent-sort-tilemap-sprite-interleave.test.ts +227 -0
- package/src/__tests__/variant-selection.unit.test.ts +21 -0
- package/src/__tests__/vertex-attribute-layout.test.ts +324 -0
- package/src/__tests__/video-cache-regression.unit.test.ts +186 -0
- package/src/__tests__/video-extract-bindgroup.dawn.test.ts +450 -0
- package/src/__tests__/view-ubo-layout.browser.test.ts +183 -0
- package/src/__tests__/world-entity-key.test.ts +92 -0
- package/src/__tests__/world-scene-despawn.test.ts +132 -0
- package/src/__tests__/world-scene-instantiate.test.ts +228 -0
- package/src/__tests__/world-scene-nested.test.ts +230 -0
- package/src/__tests__/world-scene-override.test.ts +199 -0
- package/src/__tests__/zero-compression-zero-load.integration.test.ts +107 -0
- package/src/asset-contributions.ts +48 -0
- package/src/asset-kind.ts +7 -0
- package/src/collect-scene-asset.ts +689 -0
- package/src/components/__tests__/camera.test-d.ts +57 -0
- package/src/components/__tests__/camera.test.ts +95 -0
- package/src/components/__tests__/child-of.test.ts +35 -0
- package/src/components/__tests__/sprite-animation.test-d.ts +160 -0
- package/src/components/__tests__/sprite-instances.unit.test.ts +108 -0
- package/src/components/__tests__/sprite-region-override.test-d.ts +101 -0
- package/src/components/__tests__/tile-layer.test-d.ts +33 -0
- package/src/components/__tests__/tile-layer.unit.test.ts +125 -0
- package/src/components/__tests__/tilemap.test-d.ts +35 -0
- package/src/components/__tests__/transform-vec-schema.test.ts +121 -0
- package/src/create-renderer-env-classify.ts +110 -0
- package/src/createRenderer.ts +61 -0
- package/src/dev-import-transport.ts +97 -0
- package/src/errors/environment.ts +6 -0
- package/src/errors/sprite-animation.ts +5 -0
- package/src/index.ts +442 -0
- package/src/scene-asset-port.ts +32 -0
- package/src/scene-collect-errors.ts +30 -0
- package/src/scene-instances/__tests__/post-spawn-resolve-joints.unit.test.ts +183 -0
- package/src/scene-utils/mount-override-fold.ts +217 -0
- package/src/shadow-m3-calibrate.ts +171 -0
- package/src/sprite-param-values.ts +61 -0
- package/src/systems/__tests__/_xorshift.ts +83 -0
- package/src/systems/__tests__/argsort-in-place.test.ts +155 -0
- package/src/systems/__tests__/transparent-sort.bench.ts +218 -0
- package/src/systems/sprite-animation-tick.ts +302 -0
- package/src/systems/tonemap.ts +68 -0
- package/src/systems/transparent-sort.ts +434 -0
- package/src/tilemap-register-validate.ts +100 -0
|
@@ -0,0 +1,2265 @@
|
|
|
1
|
+
// @ts-nocheck — merged file: cross-source type narrowing failures from blocks originally outside src/ rootDir
|
|
2
|
+
// Consolidated by feat-20260609-test-pool-startup-reduction-merge-tiny-test-files
|
|
3
|
+
// biome-ignore-all lint/complexity/noUselessLoneBlockStatements: block-scope isolation between merged source files (consolidation paradigm)
|
|
4
|
+
//
|
|
5
|
+
// Source files (N=7):
|
|
6
|
+
// - packages/runtime/src/__tests__/builder-topology-stripIndexFormat.test.ts
|
|
7
|
+
// - packages/runtime/src/__tests__/frustum-culling.test.ts
|
|
8
|
+
// - packages/runtime/src/__tests__/instances.test.ts
|
|
9
|
+
// - packages/runtime/src/__tests__/mesh-gpu-handles-vertex-only.test.ts
|
|
10
|
+
// - packages/runtime/src/__tests__/mesh-ssbo-grow.test.ts
|
|
11
|
+
// - packages/runtime/src/__tests__/mesh-update-no-leak.test.ts
|
|
12
|
+
// - packages/runtime/src/__tests__/instances-with-submeshes.test.ts
|
|
13
|
+
//
|
|
14
|
+
// feat-20260704-runtime-tier1-decomposition (w6): the geometry-pure blocks
|
|
15
|
+
// (geometry-tangent / geometry-winding / geometry / vertex-attribute-layout)
|
|
16
|
+
// moved to packages/geometry/src/__tests__/geometry.unit.test.ts (leaf package
|
|
17
|
+
// must not import runtime -- D-2).
|
|
18
|
+
|
|
19
|
+
//
|
|
20
|
+
// Paradigm: each block-scoped describe('<source-filename>.test.ts', ...) preserves
|
|
21
|
+
// source as ancestorTitles[0]. Top-level imports merged + deduped.
|
|
22
|
+
|
|
23
|
+
import type { World as WorldType } from '@forgeax/engine-ecs';
|
|
24
|
+
import { World } from '@forgeax/engine-ecs';
|
|
25
|
+
import { componentSchema } from '@forgeax/engine-ecs/internal';
|
|
26
|
+
import { deriveVertexLayoutProjection } from '@forgeax/engine-geometry';
|
|
27
|
+
import type { Renderer as RendererType } from '@forgeax/engine-render';
|
|
28
|
+
import type { RenderAssetPort, RenderError, SkinError } from '@forgeax/engine-render/internal';
|
|
29
|
+
import {
|
|
30
|
+
Camera,
|
|
31
|
+
createMeshSsboGrowController,
|
|
32
|
+
Instances,
|
|
33
|
+
MeshFilter,
|
|
34
|
+
MeshRenderer,
|
|
35
|
+
} from '@forgeax/engine-render/internal';
|
|
36
|
+
import type { PipelineLayout, RenderPipeline, RhiDevice, ShaderModule } from '@forgeax/engine-rhi';
|
|
37
|
+
import { Transform } from '@forgeax/engine-scene';
|
|
38
|
+
import type { AssetErrorDetail, Handle, MeshAsset } from '@forgeax/engine-types';
|
|
39
|
+
import { ASSET_ERROR_HINTS, type AssetError, unwrapHandle } from '@forgeax/engine-types';
|
|
40
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
41
|
+
|
|
42
|
+
// feat-20260704-runtime-tier1-decomposition M2 / w12: reconstitute the
|
|
43
|
+
// eliminated top-level RuntimeError aggregate union (D-3) as a test-local alias
|
|
44
|
+
// so the errorRegistry.fire mock callback signatures below stay byte-identical
|
|
45
|
+
// (AC-09). Equal to RenderError | AssetError | SkinError.
|
|
46
|
+
type RuntimeLayerError = RenderError | AssetError | SkinError;
|
|
47
|
+
|
|
48
|
+
import type {
|
|
49
|
+
ExtractedFrame,
|
|
50
|
+
GpuBuffer,
|
|
51
|
+
MeshGpuHandles,
|
|
52
|
+
PipelineBuilderContext,
|
|
53
|
+
PipelineBuilderShaderModuleFactory,
|
|
54
|
+
} from '@forgeax/engine-render/internal';
|
|
55
|
+
import {
|
|
56
|
+
buildPipelineForMaterialShader,
|
|
57
|
+
ensureMeshSsboCapacity,
|
|
58
|
+
extractFrame,
|
|
59
|
+
GpuResourceStore,
|
|
60
|
+
isMeshSsboDevMode,
|
|
61
|
+
prepareExtractContext,
|
|
62
|
+
setMeshSsboDevModeProbeForTests,
|
|
63
|
+
} from '@forgeax/engine-render/internal';
|
|
64
|
+
import { propagateTransforms } from '@forgeax/engine-scene';
|
|
65
|
+
import {
|
|
66
|
+
DEFAULT_STANDARD_PBR_PARAM_SCHEMA,
|
|
67
|
+
DEFAULT_UNLIT_PARAM_SCHEMA,
|
|
68
|
+
} from '@forgeax/engine-shader';
|
|
69
|
+
import {
|
|
70
|
+
canonicalDefaultMaterialVariants,
|
|
71
|
+
makeMockShaderRegistry,
|
|
72
|
+
} from './helpers/mock-shader-registry';
|
|
73
|
+
import { registerRuntimeComponents } from './helpers/register-runtime-components';
|
|
74
|
+
|
|
75
|
+
function canonicalMaterialParamSchema(identifier: string): string {
|
|
76
|
+
if (identifier === 'forgeax::default-standard-pbr') {
|
|
77
|
+
return JSON.stringify(DEFAULT_STANDARD_PBR_PARAM_SCHEMA);
|
|
78
|
+
}
|
|
79
|
+
if (identifier === 'forgeax::default-unlit') {
|
|
80
|
+
return JSON.stringify(DEFAULT_UNLIT_PARAM_SCHEMA);
|
|
81
|
+
}
|
|
82
|
+
return '[]';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
{
|
|
86
|
+
// --- from builder-topology-stripIndexFormat.test.ts ---
|
|
87
|
+
// builder-topology-stripIndexFormat.test.ts
|
|
88
|
+
// feat-20260604-mesh-topology-debug-draw M3 / w6 (test, TDD red phase).
|
|
89
|
+
//
|
|
90
|
+
// Verifies that buildPipelineForMaterialShader (pipeline-builder.ts, the real
|
|
91
|
+
// exported entry point) bakes the supplied topology into the immutable
|
|
92
|
+
// GPURenderPipeline `primitive.topology`, and sets `primitive.stripIndexFormat`
|
|
93
|
+
// for strip topologies while leaving it undefined for non-strip topologies.
|
|
94
|
+
//
|
|
95
|
+
// Approach: drive the real `buildPipelineForMaterialShader` with vi.fn mocks
|
|
96
|
+
// (same harness as renderstate-pipeline-cache.test.ts) and inspect the
|
|
97
|
+
// descriptor passed to `device.createRenderPipeline`. This is the AC-08
|
|
98
|
+
// verification surface against the actual builder, not a reproduction.
|
|
99
|
+
//
|
|
100
|
+
// Tests:
|
|
101
|
+
// (a) AC-08: explicit topology reaches primitive.topology.
|
|
102
|
+
// (b) AC-03: omitted geometry -> primitive.topology defaults to
|
|
103
|
+
// 'triangle-list' (zero-regression).
|
|
104
|
+
// (c) AC-08: strip topology + indexFormat -> primitive.stripIndexFormat
|
|
105
|
+
// equals the mesh indexFormat.
|
|
106
|
+
// (d) AC-08: non-strip topology -> primitive.stripIndexFormat is undefined
|
|
107
|
+
// (WebGPU spec: stripIndexFormat is only valid for strip topologies).
|
|
108
|
+
//
|
|
109
|
+
// Anchors:
|
|
110
|
+
// - requirements AC-03 / AC-08
|
|
111
|
+
// - plan-strategy D-A3 (builder topology param) + D-c (stripIndexFormat point)
|
|
112
|
+
// - research Finding 3 (stripIndexFormat spec rule) + Finding 5 (:205-209 hardcode)
|
|
113
|
+
|
|
114
|
+
function makeMockEntry() {
|
|
115
|
+
return {
|
|
116
|
+
source: '@vertex fn vs_main() -> @builtin(position) vec4<f32> { return vec4<f32>(0.0); }',
|
|
117
|
+
paramSchema: [],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function makeMocks() {
|
|
122
|
+
const shaderModule = { __tag: 'mock-shader-module' } as unknown as ShaderModule;
|
|
123
|
+
const renderPipeline = { __tag: 'mock-render-pipeline' } as unknown as RenderPipeline;
|
|
124
|
+
const createShaderModule = vi.fn(() => ({
|
|
125
|
+
ok: true as const,
|
|
126
|
+
value: shaderModule,
|
|
127
|
+
unwrap: () => shaderModule,
|
|
128
|
+
unwrapOr: (_d: unknown) => shaderModule,
|
|
129
|
+
}));
|
|
130
|
+
const createRenderPipeline = vi.fn(() => ({
|
|
131
|
+
ok: true as const,
|
|
132
|
+
value: renderPipeline,
|
|
133
|
+
unwrap: () => renderPipeline,
|
|
134
|
+
unwrapOr: (_d: unknown) => renderPipeline,
|
|
135
|
+
}));
|
|
136
|
+
return {
|
|
137
|
+
factory: { createShaderModule } as unknown as PipelineBuilderShaderModuleFactory,
|
|
138
|
+
device: { createRenderPipeline } as unknown as Pick<RhiDevice, 'createRenderPipeline'>,
|
|
139
|
+
createShaderModule,
|
|
140
|
+
createRenderPipeline,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function makeMockContext(mocks: ReturnType<typeof makeMocks>): PipelineBuilderContext {
|
|
145
|
+
return {
|
|
146
|
+
device: mocks.device as unknown as RhiDevice,
|
|
147
|
+
shaderModuleFactory: mocks.factory,
|
|
148
|
+
pipelineLayout: { __tag: 'mock-pipeline-layout' } as unknown as PipelineLayout,
|
|
149
|
+
colorFormat: 'bgra8unorm-srgb',
|
|
150
|
+
depthFormat: 'depth24plus-stencil8',
|
|
151
|
+
vertexBuffers: [
|
|
152
|
+
{
|
|
153
|
+
arrayStride: 12 * 4,
|
|
154
|
+
attributes: [
|
|
155
|
+
{ shaderLocation: 0, offset: 0, format: 'float32x3' },
|
|
156
|
+
{ shaderLocation: 1, offset: 3 * 4, format: 'float32x3' },
|
|
157
|
+
{ shaderLocation: 2, offset: 6 * 4, format: 'float32x2' },
|
|
158
|
+
{ shaderLocation: 3, offset: 8 * 4, format: 'float32x4' },
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
] as unknown as readonly GPUVertexBufferLayout[],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function descOf(mocks: ReturnType<typeof makeMocks>): Record<string, unknown> {
|
|
166
|
+
return (mocks.createRenderPipeline.mock.calls as unknown[][])[0]?.[0] as Record<
|
|
167
|
+
string,
|
|
168
|
+
unknown
|
|
169
|
+
>;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
describe('buildPipelineForMaterialShader topology + stripIndexFormat (AC-03/08)', () => {
|
|
173
|
+
it('(a) AC-08: explicit topology reaches primitive.topology', () => {
|
|
174
|
+
const mocks = makeMocks();
|
|
175
|
+
const ctx = makeMockContext(mocks);
|
|
176
|
+
buildPipelineForMaterialShader('test::lines', makeMockEntry(), ctx, undefined, {
|
|
177
|
+
topology: 'line-list',
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const primitive = descOf(mocks).primitive as { topology: string };
|
|
181
|
+
expect(primitive.topology).toBe('line-list');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('(b) AC-03: omitted geometry -> primitive.topology defaults to triangle-list', () => {
|
|
185
|
+
const mocks = makeMocks();
|
|
186
|
+
const ctx = makeMockContext(mocks);
|
|
187
|
+
buildPipelineForMaterialShader('test::default-topo', makeMockEntry(), ctx);
|
|
188
|
+
|
|
189
|
+
const primitive = descOf(mocks).primitive as { topology: string };
|
|
190
|
+
expect(primitive.topology).toBe('triangle-list');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('(b) AC-03: empty geometry object -> primitive.topology defaults to triangle-list', () => {
|
|
194
|
+
const mocks = makeMocks();
|
|
195
|
+
const ctx = makeMockContext(mocks);
|
|
196
|
+
buildPipelineForMaterialShader('test::empty-geo', makeMockEntry(), ctx, undefined, {});
|
|
197
|
+
|
|
198
|
+
const primitive = descOf(mocks).primitive as { topology: string };
|
|
199
|
+
expect(primitive.topology).toBe('triangle-list');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('(c) AC-08: line-strip + uint16 -> primitive.stripIndexFormat is uint16', () => {
|
|
203
|
+
const mocks = makeMocks();
|
|
204
|
+
const ctx = makeMockContext(mocks);
|
|
205
|
+
buildPipelineForMaterialShader('test::line-strip', makeMockEntry(), ctx, undefined, {
|
|
206
|
+
topology: 'line-strip',
|
|
207
|
+
stripIndexFormat: 'uint16',
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const primitive = descOf(mocks).primitive as { topology: string; stripIndexFormat?: string };
|
|
211
|
+
expect(primitive.topology).toBe('line-strip');
|
|
212
|
+
expect(primitive.stripIndexFormat).toBe('uint16');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('(c) AC-08: triangle-strip + uint32 -> primitive.stripIndexFormat is uint32', () => {
|
|
216
|
+
const mocks = makeMocks();
|
|
217
|
+
const ctx = makeMockContext(mocks);
|
|
218
|
+
buildPipelineForMaterialShader('test::tri-strip', makeMockEntry(), ctx, undefined, {
|
|
219
|
+
topology: 'triangle-strip',
|
|
220
|
+
stripIndexFormat: 'uint32',
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const primitive = descOf(mocks).primitive as { topology: string; stripIndexFormat?: string };
|
|
224
|
+
expect(primitive.topology).toBe('triangle-strip');
|
|
225
|
+
expect(primitive.stripIndexFormat).toBe('uint32');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('(d) AC-08: triangle-list (non-strip) -> primitive.stripIndexFormat is undefined', () => {
|
|
229
|
+
const mocks = makeMocks();
|
|
230
|
+
const ctx = makeMockContext(mocks);
|
|
231
|
+
buildPipelineForMaterialShader('test::tri-list', makeMockEntry(), ctx, undefined, {
|
|
232
|
+
topology: 'triangle-list',
|
|
233
|
+
stripIndexFormat: 'uint16',
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const primitive = descOf(mocks).primitive as { stripIndexFormat?: string };
|
|
237
|
+
expect(primitive.stripIndexFormat).toBeUndefined();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('(d) AC-08: line-list (non-strip) -> primitive.stripIndexFormat is undefined', () => {
|
|
241
|
+
const mocks = makeMocks();
|
|
242
|
+
const ctx = makeMockContext(mocks);
|
|
243
|
+
buildPipelineForMaterialShader('test::line-list', makeMockEntry(), ctx, undefined, {
|
|
244
|
+
topology: 'line-list',
|
|
245
|
+
stripIndexFormat: 'uint32',
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const primitive = descOf(mocks).primitive as { stripIndexFormat?: string };
|
|
249
|
+
expect(primitive.stripIndexFormat).toBeUndefined();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('AC-08: topology + renderState compose (cullMode still honored)', () => {
|
|
253
|
+
const mocks = makeMocks();
|
|
254
|
+
const ctx = makeMockContext(mocks);
|
|
255
|
+
buildPipelineForMaterialShader(
|
|
256
|
+
'test::compose',
|
|
257
|
+
makeMockEntry(),
|
|
258
|
+
ctx,
|
|
259
|
+
{ cullMode: 'none' },
|
|
260
|
+
{ topology: 'line-list' },
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
const primitive = descOf(mocks).primitive as { topology: string; cullMode: string };
|
|
264
|
+
expect(primitive.topology).toBe('line-list');
|
|
265
|
+
expect(primitive.cullMode).toBe('none');
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
{
|
|
271
|
+
// --- from frustum-culling.test.ts ---
|
|
272
|
+
// frustum-culling.test.ts — feat-20260528-frustum-culling M3 / w9 (TDD red).
|
|
273
|
+
//
|
|
274
|
+
// Tests for frustum culling in the extract phase. Frustum culling skips
|
|
275
|
+
// RenderableSnapshot push for entities whose world-space AABB does not
|
|
276
|
+
// intersect any camera's view frustum.
|
|
277
|
+
//
|
|
278
|
+
// Coverage:
|
|
279
|
+
// (1) entity outside frustum → culled (not in renderables)
|
|
280
|
+
// (2) entity inside frustum → not culled (in renderables)
|
|
281
|
+
// (4) no AABB (undefined/inverted-infinity) → always visible
|
|
282
|
+
// (5) instanced mesh → union AABB granularity
|
|
283
|
+
// (6) multi-camera → each camera independently culls
|
|
284
|
+
// (7) culling runs unconditionally (entity behind camera is culled)
|
|
285
|
+
// (8) entity exactly on frustum boundary → not culled
|
|
286
|
+
// (9) entity wholly outside all cameras → culled
|
|
287
|
+
// (10) mixed scene: inside + outside entities produce correct split
|
|
288
|
+
//
|
|
289
|
+
// Anchors: requirements AC-13 (frustum culling during extract);
|
|
290
|
+
// plan-strategy D-5 (entity-level union AABB, no per-instance expansion);
|
|
291
|
+
// research §F-5 (Gribb/Hartmann frustum plane extraction).
|
|
292
|
+
|
|
293
|
+
// ── helpers ──────────────────────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
function translateTransform(
|
|
296
|
+
x: number,
|
|
297
|
+
y: number,
|
|
298
|
+
z: number,
|
|
299
|
+
): {
|
|
300
|
+
posX: number;
|
|
301
|
+
posY: number;
|
|
302
|
+
posZ: number;
|
|
303
|
+
quatX: number;
|
|
304
|
+
quatY: number;
|
|
305
|
+
quatZ: number;
|
|
306
|
+
quatW: number;
|
|
307
|
+
scaleX: number;
|
|
308
|
+
scaleY: number;
|
|
309
|
+
scaleZ: number;
|
|
310
|
+
} {
|
|
311
|
+
return {
|
|
312
|
+
pos: [x, y, z],
|
|
313
|
+
quat: [0, 0, 0, 1],
|
|
314
|
+
scale: [1, 1, 1],
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function perspectiveCameraData(
|
|
319
|
+
fov = Math.PI / 4,
|
|
320
|
+
aspect = 1,
|
|
321
|
+
near = 0.1,
|
|
322
|
+
far = 100,
|
|
323
|
+
): {
|
|
324
|
+
fov: number;
|
|
325
|
+
aspect: number;
|
|
326
|
+
near: number;
|
|
327
|
+
far: number;
|
|
328
|
+
projection: number;
|
|
329
|
+
left: number;
|
|
330
|
+
right: number;
|
|
331
|
+
bottom: number;
|
|
332
|
+
top: number;
|
|
333
|
+
} {
|
|
334
|
+
return { fov, aspect, near, far, projection: 0, left: -1, right: 1, bottom: -1, top: 1 };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function registerMesh(world: World, aabb: Float32Array): Handle<'MeshAsset', 'shared'> {
|
|
338
|
+
// Minimal mesh: 1 triangle at origin with AABB
|
|
339
|
+
const vertices = new Float32Array([
|
|
340
|
+
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,
|
|
341
|
+
1, 0, 0, 0, 0,
|
|
342
|
+
]);
|
|
343
|
+
return world.allocSharedRef('MeshAsset', {
|
|
344
|
+
kind: 'mesh',
|
|
345
|
+
vertices,
|
|
346
|
+
indices: new Uint16Array([0, 1, 2]),
|
|
347
|
+
attributes: { position: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]) },
|
|
348
|
+
aabb,
|
|
349
|
+
materialSlots: [{ slotName: 'Default' }],
|
|
350
|
+
submeshes: [
|
|
351
|
+
{
|
|
352
|
+
indexOffset: 0,
|
|
353
|
+
indexCount: 3,
|
|
354
|
+
vertexCount: vertices.length,
|
|
355
|
+
materialSlot: 0,
|
|
356
|
+
topology: 'triangle-list',
|
|
357
|
+
},
|
|
358
|
+
],
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function registerUnlitMaterial(world: World): Handle<'MaterialAsset', 'shared'> {
|
|
363
|
+
return world.allocSharedRef('MaterialAsset', {
|
|
364
|
+
kind: 'material',
|
|
365
|
+
passes: [
|
|
366
|
+
{
|
|
367
|
+
name: 'Forward',
|
|
368
|
+
program: { module: 'forgeax::default-unlit' },
|
|
369
|
+
renderState: { tags: { LightMode: 'Forward' }, queue: 2000 },
|
|
370
|
+
},
|
|
371
|
+
],
|
|
372
|
+
values: { baseColor: [1, 1, 1] },
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function makeRenderAssetPort(): RenderAssetPort {
|
|
377
|
+
const shaderRegistry = makeMockShaderRegistry();
|
|
378
|
+
return {
|
|
379
|
+
epoch: 0,
|
|
380
|
+
shaderRegistry,
|
|
381
|
+
lookup: () => undefined,
|
|
382
|
+
guidOf: () => undefined,
|
|
383
|
+
materialShaderTextureFieldNames: () => [],
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function makeWorldWithAssets(): {
|
|
388
|
+
world: World;
|
|
389
|
+
assets: RenderAssetPort;
|
|
390
|
+
meshHandle: Handle<'MeshAsset', 'shared'>;
|
|
391
|
+
matHandle: Handle<'MaterialAsset', 'shared'>;
|
|
392
|
+
} {
|
|
393
|
+
const world = new World();
|
|
394
|
+
const assets = makeRenderAssetPort();
|
|
395
|
+
|
|
396
|
+
// AABB: unit cube [-1,1] in each axis
|
|
397
|
+
const aabb = new Float32Array([-1, -1, -1, 1, 1, 1]);
|
|
398
|
+
const meshHandle = registerMesh(world, aabb);
|
|
399
|
+
const matHandle = registerUnlitMaterial(world);
|
|
400
|
+
return { world, assets, meshHandle, matHandle };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** Spawn a renderable entity at (x,y,z). */
|
|
404
|
+
function spawnEntity(
|
|
405
|
+
world: World,
|
|
406
|
+
meshHandle: Handle<'MeshAsset', 'shared'>,
|
|
407
|
+
matHandle: Handle<'MaterialAsset', 'shared'>,
|
|
408
|
+
x = 0,
|
|
409
|
+
y = 0,
|
|
410
|
+
z = 0,
|
|
411
|
+
): void {
|
|
412
|
+
world
|
|
413
|
+
.spawn(
|
|
414
|
+
{ component: Transform, data: translateTransform(x, y, z) },
|
|
415
|
+
{ component: MeshFilter, data: { assetHandle: meshHandle } },
|
|
416
|
+
{ component: MeshRenderer, data: { materials: [matHandle] } },
|
|
417
|
+
)
|
|
418
|
+
.unwrap();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** Add a camera at the given position looking along -Z. */
|
|
422
|
+
function spawnCameraAt(
|
|
423
|
+
world: World,
|
|
424
|
+
x: number,
|
|
425
|
+
y: number,
|
|
426
|
+
z: number,
|
|
427
|
+
fov = Math.PI / 4,
|
|
428
|
+
aspect = 1,
|
|
429
|
+
near = 0.1,
|
|
430
|
+
far = 100,
|
|
431
|
+
): void {
|
|
432
|
+
world
|
|
433
|
+
.spawn(
|
|
434
|
+
{
|
|
435
|
+
component: Transform,
|
|
436
|
+
data: {
|
|
437
|
+
pos: [x, y, z],
|
|
438
|
+
quat: [0, 0, 0, 1],
|
|
439
|
+
scale: [1, 1, 1],
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
{ component: Camera, data: perspectiveCameraData(fov, aspect, near, far) },
|
|
443
|
+
)
|
|
444
|
+
.unwrap();
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ── tests ────────────────────────────────────────────────────────────────
|
|
448
|
+
|
|
449
|
+
describe('Extract-stage frustum culling (M3 w9)', () => {
|
|
450
|
+
it('(1) entity far outside frustum (behind camera) is culled', () => {
|
|
451
|
+
const { world, assets, meshHandle, matHandle } = makeWorldWithAssets();
|
|
452
|
+
spawnCameraAt(world, 0, 0, 5); // camera at z=5 looking -Z
|
|
453
|
+
// entity behind camera (z=6, while camera is at z=5 looking -Z → negative-Z is behind)
|
|
454
|
+
spawnEntity(world, meshHandle, matHandle, 0, 0, 6);
|
|
455
|
+
|
|
456
|
+
propagateTransforms(world);
|
|
457
|
+
|
|
458
|
+
const frame: ExtractedFrame = extractFrame(world, prepareExtractContext(world, { assets }));
|
|
459
|
+
expect(frame.renderables).toHaveLength(0);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('(1b) entity far to the left of frustum is culled', () => {
|
|
463
|
+
const { world, assets, meshHandle, matHandle } = makeWorldWithAssets();
|
|
464
|
+
spawnCameraAt(world, 0, 0, 10); // camera at z=10 looking -Z, fov=90, near=0.1, far=100
|
|
465
|
+
// entity far to the left (x=-50, way outside frustum with aspect=1, fov=90 at z=0)
|
|
466
|
+
spawnEntity(world, meshHandle, matHandle, -50, 0, 0);
|
|
467
|
+
|
|
468
|
+
propagateTransforms(world);
|
|
469
|
+
|
|
470
|
+
const frame: ExtractedFrame = extractFrame(world, prepareExtractContext(world, { assets }));
|
|
471
|
+
expect(frame.renderables).toHaveLength(0);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('(2) entity inside frustum is not culled', () => {
|
|
475
|
+
const { world, assets, meshHandle, matHandle } = makeWorldWithAssets();
|
|
476
|
+
spawnCameraAt(world, 0, 0, 5);
|
|
477
|
+
// entity directly in front of camera at z=0 (between near=0.1 and far=100)
|
|
478
|
+
spawnEntity(world, meshHandle, matHandle, 0, 0, 0);
|
|
479
|
+
|
|
480
|
+
propagateTransforms(world);
|
|
481
|
+
|
|
482
|
+
const frame: ExtractedFrame = extractFrame(world, prepareExtractContext(world, { assets }));
|
|
483
|
+
expect(frame.renderables).toHaveLength(1);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('(4) entity with no position attribute (AABB is inverted-infinity) is always visible', () => {
|
|
487
|
+
const world = new World();
|
|
488
|
+
const assets = makeRenderAssetPort();
|
|
489
|
+
|
|
490
|
+
// A mesh with no position attribute has no finite culling bounds and is always visible.
|
|
491
|
+
const vertices = new Float32Array([
|
|
492
|
+
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,
|
|
493
|
+
1, 0, 0, 0, 0,
|
|
494
|
+
]);
|
|
495
|
+
const meshHandle = world.allocSharedRef('MeshAsset', {
|
|
496
|
+
kind: 'mesh',
|
|
497
|
+
vertices,
|
|
498
|
+
indices: new Uint16Array([0, 1, 2]),
|
|
499
|
+
attributes: {},
|
|
500
|
+
materialSlots: [{ slotName: 'Default' }],
|
|
501
|
+
submeshes: [
|
|
502
|
+
{
|
|
503
|
+
indexOffset: 0,
|
|
504
|
+
indexCount: 3,
|
|
505
|
+
vertexCount: vertices.length,
|
|
506
|
+
materialSlot: 0,
|
|
507
|
+
topology: 'triangle-list',
|
|
508
|
+
},
|
|
509
|
+
],
|
|
510
|
+
});
|
|
511
|
+
const matHandle = registerUnlitMaterial(world);
|
|
512
|
+
|
|
513
|
+
spawnCameraAt(world, 0, 0, 5);
|
|
514
|
+
world
|
|
515
|
+
.spawn(
|
|
516
|
+
{ component: Transform, data: translateTransform(100, 100, 100) },
|
|
517
|
+
{ component: MeshFilter, data: { assetHandle: meshHandle } },
|
|
518
|
+
{ component: MeshRenderer, data: { materials: [matHandle] } },
|
|
519
|
+
)
|
|
520
|
+
.unwrap();
|
|
521
|
+
|
|
522
|
+
propagateTransforms(world);
|
|
523
|
+
|
|
524
|
+
const frame: ExtractedFrame = extractFrame(world, prepareExtractContext(world, { assets }));
|
|
525
|
+
expect(frame.renderables).toHaveLength(1);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it('(4b) entity with inverted-infinity AABB (no position attribute) is always visible', () => {
|
|
529
|
+
const { world, assets, matHandle } = makeWorldWithAssets();
|
|
530
|
+
// A mesh with no position attribute has no finite culling bounds.
|
|
531
|
+
const vertices = new Float32Array([
|
|
532
|
+
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,
|
|
533
|
+
1, 0, 0, 0, 0,
|
|
534
|
+
]);
|
|
535
|
+
const meshHandle = world.allocSharedRef('MeshAsset', {
|
|
536
|
+
kind: 'mesh',
|
|
537
|
+
vertices,
|
|
538
|
+
indices: new Uint16Array([0, 1, 2]),
|
|
539
|
+
attributes: {},
|
|
540
|
+
materialSlots: [{ slotName: 'Default' }],
|
|
541
|
+
submeshes: [
|
|
542
|
+
{
|
|
543
|
+
indexOffset: 0,
|
|
544
|
+
indexCount: 3,
|
|
545
|
+
vertexCount: vertices.length,
|
|
546
|
+
materialSlot: 0,
|
|
547
|
+
topology: 'triangle-list',
|
|
548
|
+
},
|
|
549
|
+
],
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
spawnCameraAt(world, 0, 0, 5);
|
|
553
|
+
spawnEntity(world, meshHandle, matHandle, 100, 100, 100);
|
|
554
|
+
|
|
555
|
+
propagateTransforms(world);
|
|
556
|
+
|
|
557
|
+
const frame: ExtractedFrame = extractFrame(world, prepareExtractContext(world, { assets }));
|
|
558
|
+
expect(frame.renderables).toHaveLength(1);
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it('(7) culling runs unconditionally (entity behind camera is culled)', () => {
|
|
562
|
+
const { world, assets, meshHandle, matHandle } = makeWorldWithAssets();
|
|
563
|
+
spawnCameraAt(world, 0, 0, 5);
|
|
564
|
+
// entity behind camera
|
|
565
|
+
world
|
|
566
|
+
.spawn(
|
|
567
|
+
{ component: Transform, data: translateTransform(0, 0, 6) },
|
|
568
|
+
{ component: MeshFilter, data: { assetHandle: meshHandle } },
|
|
569
|
+
{ component: MeshRenderer, data: { materials: [matHandle] } },
|
|
570
|
+
)
|
|
571
|
+
.unwrap();
|
|
572
|
+
|
|
573
|
+
propagateTransforms(world);
|
|
574
|
+
|
|
575
|
+
const frame: ExtractedFrame = extractFrame(world, prepareExtractContext(world, { assets }));
|
|
576
|
+
// culling is unconditional, so entity behind camera is culled
|
|
577
|
+
expect(frame.renderables).toHaveLength(0);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it('(8) entity exactly on frustum near-plane boundary is not culled', () => {
|
|
581
|
+
const { world, assets, meshHandle, matHandle } = makeWorldWithAssets();
|
|
582
|
+
// camera at z=10, near=0.1. Entity at z=9.9 is inside. AABB [-1,1] unit cube → extends
|
|
583
|
+
// from z=8.9 to z=10.9. The near plane is at cameraZ - near = 9.9. Box extends to 10.9
|
|
584
|
+
// which is beyond the near plane (in frustum direction) → not culled.
|
|
585
|
+
spawnCameraAt(world, 0, 0, 10, Math.PI / 4, 1, 0.1, 100);
|
|
586
|
+
spawnEntity(world, meshHandle, matHandle, 0, 0, 9.9);
|
|
587
|
+
|
|
588
|
+
propagateTransforms(world);
|
|
589
|
+
|
|
590
|
+
const frame: ExtractedFrame = extractFrame(world, prepareExtractContext(world, { assets }));
|
|
591
|
+
expect(frame.renderables).toHaveLength(1);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it('(10) mixed scene: inside + outside → only inside survive', () => {
|
|
595
|
+
const { world, assets, meshHandle, matHandle } = makeWorldWithAssets();
|
|
596
|
+
spawnCameraAt(world, 0, 0, 5);
|
|
597
|
+
|
|
598
|
+
// entity in front (should survive)
|
|
599
|
+
spawnEntity(world, meshHandle, matHandle, 0, 0, 0);
|
|
600
|
+
// entity far behind (should be culled)
|
|
601
|
+
spawnEntity(world, meshHandle, matHandle, 0, 0, 100);
|
|
602
|
+
// entity far right (should be culled with fov=90, aspect=1)
|
|
603
|
+
spawnEntity(world, meshHandle, matHandle, 50, 0, 0);
|
|
604
|
+
|
|
605
|
+
propagateTransforms(world);
|
|
606
|
+
|
|
607
|
+
const frame: ExtractedFrame = extractFrame(world, prepareExtractContext(world, { assets }));
|
|
608
|
+
expect(frame.renderables).toHaveLength(1);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it('(6) multi-camera: entity inside one frustum but outside another', () => {
|
|
612
|
+
const { world, assets, meshHandle, matHandle } = makeWorldWithAssets();
|
|
613
|
+
|
|
614
|
+
// Camera A at z=5 looking -Z; entity at (0,0,0) is in front of camera A
|
|
615
|
+
spawnCameraAt(world, 0, 0, 5);
|
|
616
|
+
|
|
617
|
+
// entity in front of both cameras
|
|
618
|
+
spawnEntity(world, meshHandle, matHandle, 0, 0, 0);
|
|
619
|
+
|
|
620
|
+
propagateTransforms(world);
|
|
621
|
+
|
|
622
|
+
const frame: ExtractedFrame = extractFrame(world, prepareExtractContext(world, { assets }));
|
|
623
|
+
// The entity is in front of the camera → should be in renderables
|
|
624
|
+
expect(frame.renderables).toHaveLength(1);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
it('(6b) multi-camera: entity outside all camera frusta is culled', () => {
|
|
628
|
+
const { world, assets, meshHandle, matHandle } = makeWorldWithAssets();
|
|
629
|
+
// camera A at z=5, camera B at z=-5 looking +Z
|
|
630
|
+
spawnCameraAt(world, 0, 0, 5);
|
|
631
|
+
// entity far behind all cameras (z=100, behind camera at z=5 and far from camera at z=-5)
|
|
632
|
+
spawnCameraAt(world, 0, 0, -5); // looking -Z
|
|
633
|
+
spawnEntity(world, meshHandle, matHandle, 0, 0, 100);
|
|
634
|
+
|
|
635
|
+
propagateTransforms(world);
|
|
636
|
+
|
|
637
|
+
const frame: ExtractedFrame = extractFrame(world, prepareExtractContext(world, { assets }));
|
|
638
|
+
// Entity at z=100 is behind camera at z=5 (looking -Z → behind means >5).
|
|
639
|
+
// It's also behind camera at z=-5 (looking -Z → greater means behind).
|
|
640
|
+
// With small AABB [-1,1], it should be outside both frusta.
|
|
641
|
+
expect(frame.renderables).toHaveLength(0);
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
{
|
|
647
|
+
// --- from instances.test.ts ---
|
|
648
|
+
// w14 - Instances component schema migration to `array<f32>` (M3, AC-06).
|
|
649
|
+
//
|
|
650
|
+
// Locks AC-06 (requirements.md): the `transforms: 'array<f32>'` field
|
|
651
|
+
// resolves to a fresh `Float32Array` snapshot at the get-site. The stride
|
|
652
|
+
// contract (length must be a non-zero multiple of 16) lives at the
|
|
653
|
+
// RenderSystem extract entry (`packages/runtime/src/render-system-extract.ts`)
|
|
654
|
+
// + the AI user set-site -- the ECS layer is stride-agnostic
|
|
655
|
+
// post-feat-20260515-buffer-array-vocab-collapse (decision §2.3).
|
|
656
|
+
//
|
|
657
|
+
// feat-20260515-buffer-array-vocab-collapse M3 / w16: the legacy
|
|
658
|
+
// component-level per-component stride defineComponent option was retired;
|
|
659
|
+
// stride violations now route the runtime-side
|
|
660
|
+
// `instance-transforms-stride-mismatch` error through the Layer-3
|
|
661
|
+
// ErrorHandler from the RenderSystem extract entry rather than from the
|
|
662
|
+
// ECS write paths. The runtime defensive coverage lives in
|
|
663
|
+
// `render-system-stride.test.ts` (w14); this file covers the ECS-side
|
|
664
|
+
// schema + happy-path read invariants only.
|
|
665
|
+
//
|
|
666
|
+
// Test path note: project convention places runtime tests under
|
|
667
|
+
// `src/__tests__/` (see `instances.test-d.ts` header for the rationale).
|
|
668
|
+
|
|
669
|
+
function makeHarness(): { world: World } {
|
|
670
|
+
return { world: new World() };
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
describe('w14 - Instances { transforms: array<f32> } schema (AC-06)', () => {
|
|
674
|
+
it('componentSchema(Instances).transforms is the array<f32> keyword (no legacy buffer/count fields)', () => {
|
|
675
|
+
expect(Instances.name).toBe('Instances');
|
|
676
|
+
expect(Object.keys(componentSchema(Instances)).length).toBe(1);
|
|
677
|
+
expect((componentSchema(Instances) as Record<string, unknown>).transforms).toBe('array<f32>');
|
|
678
|
+
// Legacy fields retired in M3.
|
|
679
|
+
expect((componentSchema(Instances) as Record<string, unknown>).buffer).toBeUndefined();
|
|
680
|
+
expect((componentSchema(Instances) as Record<string, unknown>).count).toBeUndefined();
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
it('Instances component carries no per-component stride option (decision §2.3 migration)', () => {
|
|
684
|
+
// The retired component-level stride option key would have surfaced as
|
|
685
|
+
// a property on the Component token; its absence is the SSOT lock for
|
|
686
|
+
// "ECS layer is stride-agnostic".
|
|
687
|
+
expect((Instances as unknown as Record<string, unknown>).arrayStride).toBeUndefined();
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
it('(legal) spawn with 16 f32 multiples succeeds; snapshot length reflects element count', () => {
|
|
691
|
+
const { world } = makeHarness();
|
|
692
|
+
const N = 4;
|
|
693
|
+
const transforms = new Float32Array(N * 16);
|
|
694
|
+
// Fill column 3 with distinguishable translations so the read path can
|
|
695
|
+
// assert the bytes survived the BufferPool round-trip.
|
|
696
|
+
for (let i = 0; i < N; i++) {
|
|
697
|
+
transforms[i * 16 + 12] = i + 1;
|
|
698
|
+
transforms[i * 16 + 15] = 1;
|
|
699
|
+
}
|
|
700
|
+
const e = world.spawn({ component: Instances, data: { transforms } }).unwrap();
|
|
701
|
+
const snap = world.get(e, Instances).unwrap().transforms;
|
|
702
|
+
expect(snap.length).toBe(N * 16);
|
|
703
|
+
// Sanity: column 3 translation x of instance 2 reads back as 3.
|
|
704
|
+
expect(snap[2 * 16 + 12]).toBe(3);
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
it('(ECS layer is stride-agnostic) spawn with 17 f32 does NOT route a stride error from ECS', () => {
|
|
708
|
+
const { world } = makeHarness();
|
|
709
|
+
world.spawn({ component: Instances, data: { transforms: new Float32Array(17) } });
|
|
710
|
+
// The runtime-side defensive lives in render-system-extract; the ECS
|
|
711
|
+
// write path is deliberately stride-agnostic.
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
{
|
|
717
|
+
// --- from mesh-gpu-handles-vertex-only.test.ts ---
|
|
718
|
+
// mesh-gpu-handles-vertex-only.test - M2 / w3 (TDD).
|
|
719
|
+
//
|
|
720
|
+
// Coverage (vertex-only mesh upload chain through GpuResourceStore):
|
|
721
|
+
// (a) MeshGpuHandles type carries `vertexCount: number` + `indexed: boolean`
|
|
722
|
+
// and `indexBuffer: Buffer | null` (type-level via a const-assigned literal
|
|
723
|
+
// that must compile).
|
|
724
|
+
// (b) vertex-only mesh (no `indices`) upload -> `indexed === false`,
|
|
725
|
+
// `indexBuffer === null`, and NO index buffer is created (createBuffer is
|
|
726
|
+
// called once, for the vbo only -- spy assertion).
|
|
727
|
+
// (c) indexed mesh upload -> `indexed === true`, `indexBuffer !== null`,
|
|
728
|
+
// `vertexCount === vertices.length / 12`, and two buffers are created.
|
|
729
|
+
//
|
|
730
|
+
// (b) exercises the UPLOAD chain (`ensureResident` -> `uploadMeshById`), which
|
|
731
|
+
// derives GPU buffers from the POD argument, not from a registered asset. The
|
|
732
|
+
// vertex-only register-time validation relaxation (validateMeshPayload index
|
|
733
|
+
// guard) is M5 / w13, so we mint a handle from a valid indexed mesh and feed a
|
|
734
|
+
// vertex-only POD to `ensureResident` -- isolating the M2 upload surface.
|
|
735
|
+
//
|
|
736
|
+
// Anchors: requirements AC-02 (indexed unchanged) + AC-07 (vertex-only);
|
|
737
|
+
// plan-strategy D-A1 + D-a (vertexCount = vertices.length / 12);
|
|
738
|
+
// research Finding 9 (the 3 upload paths read indices unconditionally).
|
|
739
|
+
|
|
740
|
+
const mockCaps = {
|
|
741
|
+
backendKind: 'webgpu' as const,
|
|
742
|
+
compute: true,
|
|
743
|
+
timestampQuery: false,
|
|
744
|
+
timestampPeriodNanoseconds: null,
|
|
745
|
+
indirectDrawing: false,
|
|
746
|
+
textureCompressionBc: false,
|
|
747
|
+
textureCompressionEtc2: false,
|
|
748
|
+
textureCompressionAstc: false,
|
|
749
|
+
multiDrawIndirect: false,
|
|
750
|
+
pushConstants: false,
|
|
751
|
+
textureBindingArray: false,
|
|
752
|
+
samplerAliasing: false,
|
|
753
|
+
firstInstanceIndirect: false,
|
|
754
|
+
storageBuffer: true,
|
|
755
|
+
storageTexture: false,
|
|
756
|
+
rgba16floatRenderable: true,
|
|
757
|
+
rg11b10ufloatRenderable: false,
|
|
758
|
+
float32Filterable: false,
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
interface BufferProbe {
|
|
762
|
+
createdLabels: string[];
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function canonicalAttributes(vertexCount: number): MeshAsset['attributes'] {
|
|
766
|
+
return {
|
|
767
|
+
position: new Float32Array(vertexCount * 3),
|
|
768
|
+
normal: new Float32Array(vertexCount * 3),
|
|
769
|
+
uv: new Float32Array(vertexCount * 2),
|
|
770
|
+
tangent: new Float32Array(vertexCount * 4),
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// biome-ignore lint/suspicious/noExplicitAny: opaque mock device surface
|
|
775
|
+
function makeMockDevice(probe: BufferProbe): any {
|
|
776
|
+
const okShim = <T>(v: T) => ({ ok: true as const, value: v });
|
|
777
|
+
return {
|
|
778
|
+
// biome-ignore lint/suspicious/noExplicitAny: descriptor shim
|
|
779
|
+
createBuffer: (desc: any) => {
|
|
780
|
+
probe.createdLabels.push(String(desc.label ?? ''));
|
|
781
|
+
return okShim({ __mock: 'buffer', label: desc.label });
|
|
782
|
+
},
|
|
783
|
+
queue: {
|
|
784
|
+
writeBuffer: () => okShim(undefined),
|
|
785
|
+
},
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function makeIndexedMesh(): MeshAsset {
|
|
790
|
+
const vertices = new Float32Array(4 * 12); // 4 vertices x 12 floats
|
|
791
|
+
return {
|
|
792
|
+
kind: 'mesh',
|
|
793
|
+
vertices,
|
|
794
|
+
indices: new Uint16Array([0, 1, 2, 0, 2, 3]),
|
|
795
|
+
attributes: canonicalAttributes(4),
|
|
796
|
+
submeshes: [
|
|
797
|
+
{
|
|
798
|
+
indexOffset: 0,
|
|
799
|
+
indexCount: 6,
|
|
800
|
+
vertexCount: vertices.length,
|
|
801
|
+
topology: 'triangle-list',
|
|
802
|
+
},
|
|
803
|
+
],
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function makeVertexOnlyMesh(): MeshAsset {
|
|
808
|
+
const vertices = new Float32Array(2 * 12); // 2 vertices x 12 floats (one line)
|
|
809
|
+
return {
|
|
810
|
+
kind: 'mesh',
|
|
811
|
+
vertices,
|
|
812
|
+
attributes: canonicalAttributes(2),
|
|
813
|
+
submeshes: [
|
|
814
|
+
{
|
|
815
|
+
indexOffset: 0,
|
|
816
|
+
indexCount: 0,
|
|
817
|
+
vertexCount: vertices.length,
|
|
818
|
+
topology: 'line-list',
|
|
819
|
+
},
|
|
820
|
+
],
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
describe('w3 - MeshGpuHandles vertexCount / indexed / nullable indexBuffer', () => {
|
|
825
|
+
it('(a) MeshGpuHandles type carries vertexCount / indexed / nullable indexBuffer', () => {
|
|
826
|
+
// M-3 / w12: MeshGpuHandles.vertexBuffer is now a GpuBuffer wrapper +
|
|
827
|
+
// vboBytes / iboBytes byte-size fields are required. The fixture
|
|
828
|
+
// passes a stub GpuBuffer-shaped object (the test only inspects
|
|
829
|
+
// .indexed / .indexBuffer / .vertexCount, not the wrapper internals).
|
|
830
|
+
const vertexOnly: MeshGpuHandles = {
|
|
831
|
+
vertexBuffer: {} as unknown as GpuBuffer,
|
|
832
|
+
indexBuffer: null,
|
|
833
|
+
vboBytes: 0,
|
|
834
|
+
iboBytes: 0,
|
|
835
|
+
indexCount: 0,
|
|
836
|
+
indexFormat: 'uint16',
|
|
837
|
+
layoutProjection: deriveVertexLayoutProjection({
|
|
838
|
+
position: new Float32Array(6),
|
|
839
|
+
normal: new Float32Array(6),
|
|
840
|
+
uv: new Float32Array(4),
|
|
841
|
+
tangent: new Float32Array(8),
|
|
842
|
+
}),
|
|
843
|
+
vertexCount: 2,
|
|
844
|
+
indexed: false,
|
|
845
|
+
topology: 'line-list',
|
|
846
|
+
submeshes: [{ indexOffset: 0, indexCount: 0, vertexCount: 2, topology: 'line-list' }],
|
|
847
|
+
};
|
|
848
|
+
expect(vertexOnly.indexed).toBe(false);
|
|
849
|
+
expect(vertexOnly.indexBuffer).toBeNull();
|
|
850
|
+
expect(vertexOnly.vertexCount).toBe(2);
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
it('(b) vertex-only mesh upload skips the index buffer (indexed=false, indexBuffer=null)', () => {
|
|
854
|
+
const probe: BufferProbe = { createdLabels: [] };
|
|
855
|
+
const device = makeMockDevice(probe);
|
|
856
|
+
const world = new World();
|
|
857
|
+
const store = new GpuResourceStore();
|
|
858
|
+
store.configureGpuDevice(
|
|
859
|
+
device,
|
|
860
|
+
undefined,
|
|
861
|
+
(() => {
|
|
862
|
+
throw new Error('cube relay not used in mesh upload');
|
|
863
|
+
// biome-ignore lint/suspicious/noExplicitAny: relay shim
|
|
864
|
+
}) as any,
|
|
865
|
+
mockCaps,
|
|
866
|
+
);
|
|
867
|
+
// Mint a handle from a valid indexed mesh (register-time index validation
|
|
868
|
+
// relaxation is M5 / w13); feed a vertex-only POD to ensureResident to
|
|
869
|
+
// exercise the M2 upload chain in isolation.
|
|
870
|
+
const handle = world.allocSharedRef('MeshAsset', makeIndexedMesh());
|
|
871
|
+
const res = store.ensureResident(handle, makeVertexOnlyMesh());
|
|
872
|
+
expect(res.ok).toBe(true);
|
|
873
|
+
if (!res.ok) return;
|
|
874
|
+
const entry = res.value as MeshGpuHandles;
|
|
875
|
+
expect(entry.indexed).toBe(false);
|
|
876
|
+
expect(entry.indexBuffer).toBeNull();
|
|
877
|
+
expect(entry.vertexCount).toBe(2);
|
|
878
|
+
expect(entry.indexCount).toBe(0);
|
|
879
|
+
// Only the vertex buffer is created; no ibo create.
|
|
880
|
+
expect(probe.createdLabels.some((l) => l.includes('ibo'))).toBe(false);
|
|
881
|
+
expect(probe.createdLabels.some((l) => l.includes('vbo'))).toBe(true);
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
it('(c) indexed mesh upload keeps index buffer (indexed=true) + vertexCount correct', () => {
|
|
885
|
+
const probe: BufferProbe = { createdLabels: [] };
|
|
886
|
+
const device = makeMockDevice(probe);
|
|
887
|
+
const world = new World();
|
|
888
|
+
const store = new GpuResourceStore();
|
|
889
|
+
store.configureGpuDevice(
|
|
890
|
+
device,
|
|
891
|
+
undefined,
|
|
892
|
+
(() => {
|
|
893
|
+
throw new Error('cube relay not used in mesh upload');
|
|
894
|
+
// biome-ignore lint/suspicious/noExplicitAny: relay shim
|
|
895
|
+
}) as any,
|
|
896
|
+
mockCaps,
|
|
897
|
+
);
|
|
898
|
+
const handle = world.allocSharedRef('MeshAsset', makeIndexedMesh());
|
|
899
|
+
const res = store.ensureResident(handle, makeIndexedMesh());
|
|
900
|
+
expect(res.ok).toBe(true);
|
|
901
|
+
if (!res.ok) return;
|
|
902
|
+
const entry = res.value as MeshGpuHandles;
|
|
903
|
+
expect(entry.indexed).toBe(true);
|
|
904
|
+
expect(entry.indexBuffer).not.toBeNull();
|
|
905
|
+
expect(entry.vertexCount).toBe(4); // 48 floats / 12
|
|
906
|
+
expect(entry.indexCount).toBe(6);
|
|
907
|
+
expect(probe.createdLabels.some((l) => l.includes('ibo'))).toBe(true);
|
|
908
|
+
});
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
{
|
|
913
|
+
// --- from mesh-ssbo-grow.test.ts ---
|
|
914
|
+
// feat-20260608-mesh-ssbo-dynamic-grow-l1-lift-1024-entity-cap M2 / T-M2-02 +
|
|
915
|
+
// T-M2-03: unit tests for the mesh-SSBO grow controller — pow2 doubling +
|
|
916
|
+
// sync rebuild + usage parity (T-M2-02) + ceiling + idempotent guard +
|
|
917
|
+
// capacity-exceeded fallback (T-M2-03).
|
|
918
|
+
//
|
|
919
|
+
// Anchors:
|
|
920
|
+
// - requirements §AC-05 (nextPow2 one-shot grow + createBuffer ordered
|
|
921
|
+
// before the first writeBuffer)
|
|
922
|
+
// - requirements §AC-06 (mesh + material rebuilt synchronously, usage flag
|
|
923
|
+
// unchanged)
|
|
924
|
+
// - requirements §AC-08 (ceiling fires 'mesh-ssbo-ceiling-reached' once,
|
|
925
|
+
// 0 writeBuffer / 0 draw)
|
|
926
|
+
// - requirements §AC-09 (idempotent guard — same needed value calls grow
|
|
927
|
+
// 1 time then short-circuits)
|
|
928
|
+
// - plan-strategy §2.D-1 (ceiling = device.limits.maxStorageBufferBindingSize
|
|
929
|
+
// only — no engine-private constant)
|
|
930
|
+
// - plan-strategy §2.D-4 (single-file grow factory; closure-local state)
|
|
931
|
+
// - plan-strategy §2.D-5 (errorRegistry.fire, never throw)
|
|
932
|
+
// - research §F2 (mesh + material createBuffer pair, distinct usage flags)
|
|
933
|
+
// - research §F6.c (SkinPaletteAllocator spiritual cousin; 4-field error)
|
|
934
|
+
//
|
|
935
|
+
// Tests target the pure factory `createMeshSsboGrowController` (exported by
|
|
936
|
+
// createRenderer.ts at module scope, T-M2-05). The factory takes a fake
|
|
937
|
+
// device + errorRegistry and returns { state, growMeshSsbo, initialBuild }
|
|
938
|
+
// so we can spy createBuffer / errorRegistry.fire without spinning up a
|
|
939
|
+
// real WebGPU renderer.
|
|
940
|
+
|
|
941
|
+
// ── fake device + buffer ───────────────────────────────────────────────────
|
|
942
|
+
|
|
943
|
+
interface FakeBuffer {
|
|
944
|
+
readonly id: number;
|
|
945
|
+
readonly size: number;
|
|
946
|
+
readonly usage: number;
|
|
947
|
+
readonly label: string;
|
|
948
|
+
destroyed: boolean;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
interface FakeBufferDescriptor {
|
|
952
|
+
readonly label?: string;
|
|
953
|
+
readonly size: number;
|
|
954
|
+
readonly usage: number;
|
|
955
|
+
readonly mappedAtCreation?: boolean;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
interface FakeDeviceLimits {
|
|
959
|
+
readonly maxStorageBufferBindingSize: number;
|
|
960
|
+
readonly maxUniformBufferBindingSize: number;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function makeFakeDevice(limits: FakeDeviceLimits): {
|
|
964
|
+
device: { limits: FakeDeviceLimits; createBuffer: (d: FakeBufferDescriptor) => FakeBuffer };
|
|
965
|
+
createBufferSpy: ReturnType<typeof vi.fn>;
|
|
966
|
+
} {
|
|
967
|
+
let nextId = 1;
|
|
968
|
+
const createBufferSpy = vi.fn((d: FakeBufferDescriptor): FakeBuffer => {
|
|
969
|
+
const id = nextId;
|
|
970
|
+
nextId += 1;
|
|
971
|
+
const buf: FakeBuffer = {
|
|
972
|
+
id,
|
|
973
|
+
size: d.size,
|
|
974
|
+
usage: d.usage,
|
|
975
|
+
label: d.label ?? '',
|
|
976
|
+
destroyed: false,
|
|
977
|
+
};
|
|
978
|
+
return buf;
|
|
979
|
+
});
|
|
980
|
+
return {
|
|
981
|
+
device: {
|
|
982
|
+
limits,
|
|
983
|
+
createBuffer: createBufferSpy as unknown as (d: FakeBufferDescriptor) => FakeBuffer,
|
|
984
|
+
},
|
|
985
|
+
createBufferSpy,
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function makeFakeErrorRegistry(): {
|
|
990
|
+
fire: ReturnType<typeof vi.fn>;
|
|
991
|
+
} {
|
|
992
|
+
const fireSpy = vi.fn((_e: RuntimeLayerError) => undefined);
|
|
993
|
+
return { fire: fireSpy };
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// Spec-aligned constants (mirroring createRenderer.ts module-scope literals).
|
|
997
|
+
// PER_ENTITY_STRIDE matches plan-strategy §OOS-10 (kept at 256 B for both
|
|
998
|
+
// mesh + material — grow only changes slotCount, never stride).
|
|
999
|
+
const PER_ENTITY_STRIDE = 256;
|
|
1000
|
+
const INITIAL_SLOT_COUNT = 1024;
|
|
1001
|
+
const MESH_USAGE = 0x0080 /* STORAGE */ | 0x0008 /* COPY_DST */;
|
|
1002
|
+
const MATERIAL_USAGE = 0x0040 /* UNIFORM */ | 0x0008 /* COPY_DST */;
|
|
1003
|
+
const HIGH_LIMIT = 64 * 1024 * 1024; // 64 MiB — comfortable headroom for default cases
|
|
1004
|
+
|
|
1005
|
+
describe('T-M2-02 growMeshSsbo pow2 doubling + sync rebuild + usage parity', () => {
|
|
1006
|
+
beforeEach(() => {
|
|
1007
|
+
vi.restoreAllMocks();
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
it('(a) length=1500 from 1024 → grows once to 2048; createBuffer x2 (mesh+material); size ratio 256:256', () => {
|
|
1011
|
+
const { device, createBufferSpy } = makeFakeDevice({
|
|
1012
|
+
maxStorageBufferBindingSize: HIGH_LIMIT,
|
|
1013
|
+
maxUniformBufferBindingSize: HIGH_LIMIT,
|
|
1014
|
+
});
|
|
1015
|
+
const { fire } = makeFakeErrorRegistry();
|
|
1016
|
+
const ctrl = createMeshSsboGrowController({
|
|
1017
|
+
device: device as unknown as Parameters<typeof createMeshSsboGrowController>[0]['device'],
|
|
1018
|
+
errorRegistry: { fire: fire as unknown as (e: RuntimeLayerError) => void },
|
|
1019
|
+
initialSlotCount: INITIAL_SLOT_COUNT,
|
|
1020
|
+
perEntityStride: PER_ENTITY_STRIDE,
|
|
1021
|
+
meshUsage: MESH_USAGE,
|
|
1022
|
+
materialUsage: MATERIAL_USAGE,
|
|
1023
|
+
});
|
|
1024
|
+
ctrl.initialBuild();
|
|
1025
|
+
expect(createBufferSpy).toHaveBeenCalledTimes(2);
|
|
1026
|
+
expect(ctrl.state.slotCount).toBe(1024);
|
|
1027
|
+
createBufferSpy.mockClear();
|
|
1028
|
+
|
|
1029
|
+
const result = ctrl.growMeshSsbo(1500);
|
|
1030
|
+
expect(result).toEqual({ ok: true });
|
|
1031
|
+
expect(ctrl.state.slotCount).toBe(2048);
|
|
1032
|
+
// 2 createBuffer calls (mesh + material) for the single grow step
|
|
1033
|
+
expect(createBufferSpy).toHaveBeenCalledTimes(2);
|
|
1034
|
+
const meshCall = createBufferSpy.mock.calls.find(
|
|
1035
|
+
(c) => (c[0] as FakeBufferDescriptor).usage === MESH_USAGE,
|
|
1036
|
+
);
|
|
1037
|
+
const materialCall = createBufferSpy.mock.calls.find(
|
|
1038
|
+
(c) => (c[0] as FakeBufferDescriptor).usage === MATERIAL_USAGE,
|
|
1039
|
+
);
|
|
1040
|
+
expect(meshCall).toBeDefined();
|
|
1041
|
+
expect(materialCall).toBeDefined();
|
|
1042
|
+
const meshDesc = meshCall?.[0] as FakeBufferDescriptor;
|
|
1043
|
+
const materialDesc = materialCall?.[0] as FakeBufferDescriptor;
|
|
1044
|
+
// Both sized at 2048 * 256 = 524288 B; same byte count → 256:256 stride parity
|
|
1045
|
+
expect(meshDesc.size).toBe(2048 * PER_ENTITY_STRIDE);
|
|
1046
|
+
expect(materialDesc.size).toBe(2048 * PER_ENTITY_STRIDE);
|
|
1047
|
+
expect(meshDesc.size).toBe(materialDesc.size);
|
|
1048
|
+
expect(fire).not.toHaveBeenCalled();
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
it('(b) length=3000 from 1024 → nextPow2(3000)=4096 in one step; createBuffer x2', () => {
|
|
1052
|
+
const { device, createBufferSpy } = makeFakeDevice({
|
|
1053
|
+
maxStorageBufferBindingSize: HIGH_LIMIT,
|
|
1054
|
+
maxUniformBufferBindingSize: HIGH_LIMIT,
|
|
1055
|
+
});
|
|
1056
|
+
const { fire } = makeFakeErrorRegistry();
|
|
1057
|
+
const ctrl = createMeshSsboGrowController({
|
|
1058
|
+
device: device as unknown as Parameters<typeof createMeshSsboGrowController>[0]['device'],
|
|
1059
|
+
errorRegistry: { fire: fire as unknown as (e: RuntimeLayerError) => void },
|
|
1060
|
+
initialSlotCount: INITIAL_SLOT_COUNT,
|
|
1061
|
+
perEntityStride: PER_ENTITY_STRIDE,
|
|
1062
|
+
meshUsage: MESH_USAGE,
|
|
1063
|
+
materialUsage: MATERIAL_USAGE,
|
|
1064
|
+
});
|
|
1065
|
+
ctrl.initialBuild();
|
|
1066
|
+
createBufferSpy.mockClear();
|
|
1067
|
+
|
|
1068
|
+
const result = ctrl.growMeshSsbo(3000);
|
|
1069
|
+
expect(result).toEqual({ ok: true });
|
|
1070
|
+
expect(ctrl.state.slotCount).toBe(4096);
|
|
1071
|
+
expect(createBufferSpy).toHaveBeenCalledTimes(2);
|
|
1072
|
+
expect(fire).not.toHaveBeenCalled();
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
it('(c) length=5000 from 1024 → nextPow2(5000)=8192 in one step', () => {
|
|
1076
|
+
const { device, createBufferSpy } = makeFakeDevice({
|
|
1077
|
+
maxStorageBufferBindingSize: HIGH_LIMIT,
|
|
1078
|
+
maxUniformBufferBindingSize: HIGH_LIMIT,
|
|
1079
|
+
});
|
|
1080
|
+
const { fire } = makeFakeErrorRegistry();
|
|
1081
|
+
const ctrl = createMeshSsboGrowController({
|
|
1082
|
+
device: device as unknown as Parameters<typeof createMeshSsboGrowController>[0]['device'],
|
|
1083
|
+
errorRegistry: { fire: fire as unknown as (e: RuntimeLayerError) => void },
|
|
1084
|
+
initialSlotCount: INITIAL_SLOT_COUNT,
|
|
1085
|
+
perEntityStride: PER_ENTITY_STRIDE,
|
|
1086
|
+
meshUsage: MESH_USAGE,
|
|
1087
|
+
materialUsage: MATERIAL_USAGE,
|
|
1088
|
+
});
|
|
1089
|
+
ctrl.initialBuild();
|
|
1090
|
+
createBufferSpy.mockClear();
|
|
1091
|
+
|
|
1092
|
+
const result = ctrl.growMeshSsbo(5000);
|
|
1093
|
+
expect(result).toEqual({ ok: true });
|
|
1094
|
+
expect(ctrl.state.slotCount).toBe(8192);
|
|
1095
|
+
expect(createBufferSpy).toHaveBeenCalledTimes(2);
|
|
1096
|
+
expect(fire).not.toHaveBeenCalled();
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
it('(d) createBuffer usage flag matches the factory-captured mesh + material usage', () => {
|
|
1100
|
+
const { device, createBufferSpy } = makeFakeDevice({
|
|
1101
|
+
maxStorageBufferBindingSize: HIGH_LIMIT,
|
|
1102
|
+
maxUniformBufferBindingSize: HIGH_LIMIT,
|
|
1103
|
+
});
|
|
1104
|
+
const { fire } = makeFakeErrorRegistry();
|
|
1105
|
+
const ctrl = createMeshSsboGrowController({
|
|
1106
|
+
device: device as unknown as Parameters<typeof createMeshSsboGrowController>[0]['device'],
|
|
1107
|
+
errorRegistry: { fire: fire as unknown as (e: RuntimeLayerError) => void },
|
|
1108
|
+
initialSlotCount: INITIAL_SLOT_COUNT,
|
|
1109
|
+
perEntityStride: PER_ENTITY_STRIDE,
|
|
1110
|
+
meshUsage: MESH_USAGE,
|
|
1111
|
+
materialUsage: MATERIAL_USAGE,
|
|
1112
|
+
});
|
|
1113
|
+
ctrl.initialBuild();
|
|
1114
|
+
// Initial build also obeys the usage parity contract.
|
|
1115
|
+
const initialUsages = createBufferSpy.mock.calls
|
|
1116
|
+
.map((c) => (c[0] as FakeBufferDescriptor).usage)
|
|
1117
|
+
.sort((a, b) => a - b);
|
|
1118
|
+
expect(initialUsages).toEqual([MATERIAL_USAGE, MESH_USAGE].sort((a, b) => a - b));
|
|
1119
|
+
createBufferSpy.mockClear();
|
|
1120
|
+
|
|
1121
|
+
ctrl.growMeshSsbo(2500);
|
|
1122
|
+
const growUsages = createBufferSpy.mock.calls
|
|
1123
|
+
.map((c) => (c[0] as FakeBufferDescriptor).usage)
|
|
1124
|
+
.sort((a, b) => a - b);
|
|
1125
|
+
expect(growUsages).toEqual([MATERIAL_USAGE, MESH_USAGE].sort((a, b) => a - b));
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
it('(e) wrapper-object identity stable across grow; inner buffer replaced', () => {
|
|
1129
|
+
const { device } = makeFakeDevice({
|
|
1130
|
+
maxStorageBufferBindingSize: HIGH_LIMIT,
|
|
1131
|
+
maxUniformBufferBindingSize: HIGH_LIMIT,
|
|
1132
|
+
});
|
|
1133
|
+
const { fire } = makeFakeErrorRegistry();
|
|
1134
|
+
const ctrl = createMeshSsboGrowController({
|
|
1135
|
+
device: device as unknown as Parameters<typeof createMeshSsboGrowController>[0]['device'],
|
|
1136
|
+
errorRegistry: { fire: fire as unknown as (e: RuntimeLayerError) => void },
|
|
1137
|
+
initialSlotCount: INITIAL_SLOT_COUNT,
|
|
1138
|
+
perEntityStride: PER_ENTITY_STRIDE,
|
|
1139
|
+
meshUsage: MESH_USAGE,
|
|
1140
|
+
materialUsage: MATERIAL_USAGE,
|
|
1141
|
+
});
|
|
1142
|
+
ctrl.initialBuild();
|
|
1143
|
+
const meshWrapperBefore = ctrl.state.mesh;
|
|
1144
|
+
const materialWrapperBefore = ctrl.state.material;
|
|
1145
|
+
const meshBufferBefore = ctrl.state.mesh.buffer;
|
|
1146
|
+
const materialBufferBefore = ctrl.state.material.buffer;
|
|
1147
|
+
|
|
1148
|
+
ctrl.growMeshSsbo(1500);
|
|
1149
|
+
|
|
1150
|
+
// Outer wrapper identity preserved (so PipelineState fields don't dangle).
|
|
1151
|
+
expect(ctrl.state.mesh).toBe(meshWrapperBefore);
|
|
1152
|
+
expect(ctrl.state.material).toBe(materialWrapperBefore);
|
|
1153
|
+
// Inner buffer replaced by the freshly createBuffer'd handle.
|
|
1154
|
+
expect(ctrl.state.mesh.buffer).not.toBe(meshBufferBefore);
|
|
1155
|
+
expect(ctrl.state.material.buffer).not.toBe(materialBufferBefore);
|
|
1156
|
+
// sizeInBytes now reflects the grown buffer.
|
|
1157
|
+
expect(ctrl.state.mesh.sizeInBytes).toBe(2048 * PER_ENTITY_STRIDE);
|
|
1158
|
+
expect(ctrl.state.material.sizeInBytes).toBe(2048 * PER_ENTITY_STRIDE);
|
|
1159
|
+
});
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
// ── T-M2-03: ceiling + idempotent guard + capacity-exceeded fallback ─────
|
|
1163
|
+
|
|
1164
|
+
describe('T-M2-03 growMeshSsbo ceiling + idempotent guard + capacity-exceeded', () => {
|
|
1165
|
+
beforeEach(() => {
|
|
1166
|
+
vi.restoreAllMocks();
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
it('(a) ceiling — needed slot count past device limit fires once, no createBuffer, ok:false', () => {
|
|
1170
|
+
// 256 KiB cap → ceiling = 256 KiB / 256 B = 1024 slots. needed=2048 trips
|
|
1171
|
+
// the limit, so ceiling is reached.
|
|
1172
|
+
const TIGHT_LIMIT = 256 * 1024; // exactly 1024 slots ceiling
|
|
1173
|
+
const { device, createBufferSpy } = makeFakeDevice({
|
|
1174
|
+
maxStorageBufferBindingSize: TIGHT_LIMIT,
|
|
1175
|
+
maxUniformBufferBindingSize: HIGH_LIMIT,
|
|
1176
|
+
});
|
|
1177
|
+
const { fire } = makeFakeErrorRegistry();
|
|
1178
|
+
const ctrl = createMeshSsboGrowController({
|
|
1179
|
+
device: device as unknown as Parameters<typeof createMeshSsboGrowController>[0]['device'],
|
|
1180
|
+
errorRegistry: { fire: fire as unknown as (e: RuntimeLayerError) => void },
|
|
1181
|
+
initialSlotCount: INITIAL_SLOT_COUNT,
|
|
1182
|
+
perEntityStride: PER_ENTITY_STRIDE,
|
|
1183
|
+
meshUsage: MESH_USAGE,
|
|
1184
|
+
materialUsage: MATERIAL_USAGE,
|
|
1185
|
+
});
|
|
1186
|
+
ctrl.initialBuild();
|
|
1187
|
+
createBufferSpy.mockClear();
|
|
1188
|
+
|
|
1189
|
+
const result = ctrl.growMeshSsbo(2048);
|
|
1190
|
+
expect(result.ok).toBe(false);
|
|
1191
|
+
if (result.ok === false) {
|
|
1192
|
+
expect(result.code).toBe('mesh-ssbo-ceiling-reached');
|
|
1193
|
+
}
|
|
1194
|
+
expect(createBufferSpy).not.toHaveBeenCalled();
|
|
1195
|
+
expect(fire).toHaveBeenCalledTimes(1);
|
|
1196
|
+
const fired = fire.mock.calls[0]?.[0] as RuntimeLayerError;
|
|
1197
|
+
expect(fired.code).toBe('mesh-ssbo-ceiling-reached');
|
|
1198
|
+
// detail.requested / capacity / ceiling all present + non-undefined.
|
|
1199
|
+
if (fired.code === 'mesh-ssbo-ceiling-reached') {
|
|
1200
|
+
expect(fired.detail.requested).not.toBeUndefined();
|
|
1201
|
+
expect(fired.detail.capacity).not.toBeUndefined();
|
|
1202
|
+
expect(fired.detail.ceiling).not.toBeUndefined();
|
|
1203
|
+
expect(fired.detail.requested).toBe(2048);
|
|
1204
|
+
// ceiling reported in BYTES (= maxStorageBufferBindingSize), per
|
|
1205
|
+
// plan-strategy §2.D-1 + research §F5.
|
|
1206
|
+
expect(fired.detail.ceiling).toBe(TIGHT_LIMIT);
|
|
1207
|
+
}
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
it('(b) idempotent guard — three same-needed grow calls hit createBuffer x2 only once', () => {
|
|
1211
|
+
const { device, createBufferSpy } = makeFakeDevice({
|
|
1212
|
+
maxStorageBufferBindingSize: HIGH_LIMIT,
|
|
1213
|
+
maxUniformBufferBindingSize: HIGH_LIMIT,
|
|
1214
|
+
});
|
|
1215
|
+
const { fire } = makeFakeErrorRegistry();
|
|
1216
|
+
const ctrl = createMeshSsboGrowController({
|
|
1217
|
+
device: device as unknown as Parameters<typeof createMeshSsboGrowController>[0]['device'],
|
|
1218
|
+
errorRegistry: { fire: fire as unknown as (e: RuntimeLayerError) => void },
|
|
1219
|
+
initialSlotCount: INITIAL_SLOT_COUNT,
|
|
1220
|
+
perEntityStride: PER_ENTITY_STRIDE,
|
|
1221
|
+
meshUsage: MESH_USAGE,
|
|
1222
|
+
materialUsage: MATERIAL_USAGE,
|
|
1223
|
+
});
|
|
1224
|
+
ctrl.initialBuild();
|
|
1225
|
+
createBufferSpy.mockClear();
|
|
1226
|
+
|
|
1227
|
+
const r1 = ctrl.growMeshSsbo(1500);
|
|
1228
|
+
const r2 = ctrl.growMeshSsbo(1500);
|
|
1229
|
+
const r3 = ctrl.growMeshSsbo(1500);
|
|
1230
|
+
expect(r1).toEqual({ ok: true });
|
|
1231
|
+
expect(r2).toEqual({ ok: true });
|
|
1232
|
+
expect(r3).toEqual({ ok: true });
|
|
1233
|
+
// Only the first call grows; the next two short-circuit on the guard.
|
|
1234
|
+
expect(createBufferSpy).toHaveBeenCalledTimes(2);
|
|
1235
|
+
expect(fire).not.toHaveBeenCalled();
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
it('(c) defensive capacity-exceeded — needed past ceiling fires either ceiling-reached or capacity-exceeded with full detail', () => {
|
|
1239
|
+
// Plan §2.D-5 picks ceiling-reached as the primary path when the
|
|
1240
|
+
// device.limits.maxStorageBufferBindingSize cannot accommodate the
|
|
1241
|
+
// requested slot count; capacity-exceeded is a defensive fallback.
|
|
1242
|
+
// We assert that ONE of the two structured errors fires with all 3
|
|
1243
|
+
// detail fields populated (requested / capacity / ceiling), per AC-08
|
|
1244
|
+
// narrowing contract — not which one.
|
|
1245
|
+
const TIGHT_LIMIT = INITIAL_SLOT_COUNT * PER_ENTITY_STRIDE; // 1024 slots ceiling
|
|
1246
|
+
const { device, createBufferSpy } = makeFakeDevice({
|
|
1247
|
+
maxStorageBufferBindingSize: TIGHT_LIMIT,
|
|
1248
|
+
maxUniformBufferBindingSize: HIGH_LIMIT,
|
|
1249
|
+
});
|
|
1250
|
+
const { fire } = makeFakeErrorRegistry();
|
|
1251
|
+
const ctrl = createMeshSsboGrowController({
|
|
1252
|
+
device: device as unknown as Parameters<typeof createMeshSsboGrowController>[0]['device'],
|
|
1253
|
+
errorRegistry: { fire: fire as unknown as (e: RuntimeLayerError) => void },
|
|
1254
|
+
initialSlotCount: INITIAL_SLOT_COUNT,
|
|
1255
|
+
perEntityStride: PER_ENTITY_STRIDE,
|
|
1256
|
+
meshUsage: MESH_USAGE,
|
|
1257
|
+
materialUsage: MATERIAL_USAGE,
|
|
1258
|
+
});
|
|
1259
|
+
ctrl.initialBuild();
|
|
1260
|
+
createBufferSpy.mockClear();
|
|
1261
|
+
|
|
1262
|
+
const result = ctrl.growMeshSsbo(99999);
|
|
1263
|
+
expect(result.ok).toBe(false);
|
|
1264
|
+
if (result.ok === false) {
|
|
1265
|
+
expect(['mesh-ssbo-ceiling-reached', 'mesh-ssbo-capacity-exceeded']).toContain(result.code);
|
|
1266
|
+
}
|
|
1267
|
+
expect(createBufferSpy).not.toHaveBeenCalled();
|
|
1268
|
+
expect(fire).toHaveBeenCalledTimes(1);
|
|
1269
|
+
const fired = fire.mock.calls[0]?.[0] as RuntimeLayerError;
|
|
1270
|
+
expect(['mesh-ssbo-ceiling-reached', 'mesh-ssbo-capacity-exceeded']).toContain(fired.code);
|
|
1271
|
+
// Whichever code was fired, the 4-field error shape must include the
|
|
1272
|
+
// structured detail with all three numeric fields populated.
|
|
1273
|
+
if (
|
|
1274
|
+
fired.code === 'mesh-ssbo-ceiling-reached' ||
|
|
1275
|
+
fired.code === 'mesh-ssbo-capacity-exceeded'
|
|
1276
|
+
) {
|
|
1277
|
+
expect(typeof fired.detail.requested).toBe('number');
|
|
1278
|
+
expect(typeof fired.detail.capacity).toBe('number');
|
|
1279
|
+
expect(typeof fired.detail.ceiling).toBe('number');
|
|
1280
|
+
}
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1283
|
+
// bug-20260622 wgpu-wasm-ssbo-ceiling-zero regression: when the device
|
|
1284
|
+
// under-reports `maxStorageBufferBindingSize = 0` (currently happens on
|
|
1285
|
+
// the wgpu-wasm backend because rhi.rs requests its device with
|
|
1286
|
+
// `wgpu::Limits::downlevel_webgl2_defaults()` which zeroes the storage
|
|
1287
|
+
// buffer binding-size limit), the grow controller must fall back to the
|
|
1288
|
+
// WebGPU spec default (128 MiB) rather than refuse every grow with
|
|
1289
|
+
// ceiling=0. Without the fallback any demo whose entity count crosses
|
|
1290
|
+
// initialSlotCount (e.g. asi-world at ~3k+ entities) loops on
|
|
1291
|
+
// MeshSsboCeilingReachedError and produces a black screen.
|
|
1292
|
+
it('(d) ceiling=0 fallback — under-reported limit treated as 128 MiB, grow succeeds', () => {
|
|
1293
|
+
const { device, createBufferSpy } = makeFakeDevice({
|
|
1294
|
+
maxStorageBufferBindingSize: 0,
|
|
1295
|
+
maxUniformBufferBindingSize: HIGH_LIMIT,
|
|
1296
|
+
});
|
|
1297
|
+
const { fire } = makeFakeErrorRegistry();
|
|
1298
|
+
const ctrl = createMeshSsboGrowController({
|
|
1299
|
+
device: device as unknown as Parameters<typeof createMeshSsboGrowController>[0]['device'],
|
|
1300
|
+
errorRegistry: { fire: fire as unknown as (e: RuntimeLayerError) => void },
|
|
1301
|
+
initialSlotCount: INITIAL_SLOT_COUNT,
|
|
1302
|
+
perEntityStride: PER_ENTITY_STRIDE,
|
|
1303
|
+
meshUsage: MESH_USAGE,
|
|
1304
|
+
materialUsage: MATERIAL_USAGE,
|
|
1305
|
+
});
|
|
1306
|
+
ctrl.initialBuild();
|
|
1307
|
+
createBufferSpy.mockClear();
|
|
1308
|
+
|
|
1309
|
+
// Pick a slot count well above initialSlotCount=1024 but well below
|
|
1310
|
+
// 128 MiB / 256 B = 512K slots. Matches the asi-world failure-mode
|
|
1311
|
+
// size (~5546 needed slots in the original repro).
|
|
1312
|
+
const result = ctrl.growMeshSsbo(6000);
|
|
1313
|
+
expect(result).toEqual({ ok: true });
|
|
1314
|
+
// mesh + material buffers reallocated once on the grow.
|
|
1315
|
+
expect(createBufferSpy).toHaveBeenCalledTimes(2);
|
|
1316
|
+
expect(fire).not.toHaveBeenCalled();
|
|
1317
|
+
// slotCount lifted to the next pow2 >= 6000 (= 8192).
|
|
1318
|
+
expect(ctrl.state.slotCount).toBe(8192);
|
|
1319
|
+
});
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
// ── T-M3-01: ensureMeshSsboCapacity idempotent + boundary ────────────────
|
|
1323
|
+
//
|
|
1324
|
+
// Anchors:
|
|
1325
|
+
// - requirements §AC-05 (entry point at recordFrame top, after
|
|
1326
|
+
// validatedOrdered finalised + before first writeBuffer)
|
|
1327
|
+
// - requirements §AC-09 (idempotent guard — same-frame multi-call)
|
|
1328
|
+
// - requirements §boundary table (length=0 / 1024 / 1025 / 5000 / past
|
|
1329
|
+
// ceiling — first 4 covered here; 5th is T-M3-02)
|
|
1330
|
+
// - plan-strategy §2.D-4 (ensureMeshSsboCapacity reads internals.growMeshSsbo
|
|
1331
|
+
// hook + internals.meshSsboState slotCount)
|
|
1332
|
+
// - plan-strategy §5.3 row 2-4 (idempotent guard, boundary lengths)
|
|
1333
|
+
|
|
1334
|
+
interface FakeInternals {
|
|
1335
|
+
growMeshSsbo?:
|
|
1336
|
+
| ((neededSlots: number) => ReturnType<MeshSsboGrowController['growMeshSsbo']>)
|
|
1337
|
+
| undefined;
|
|
1338
|
+
meshSsboState?: { slotCount: number } | undefined;
|
|
1339
|
+
errorRegistry: { fire: (e: RuntimeLayerError) => void };
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
type MeshSsboGrowController = ReturnType<typeof createMeshSsboGrowController>;
|
|
1343
|
+
|
|
1344
|
+
function makeInternalsWithRealController(): {
|
|
1345
|
+
internals: FakeInternals;
|
|
1346
|
+
ctrl: MeshSsboGrowController;
|
|
1347
|
+
growSpy: ReturnType<typeof vi.fn>;
|
|
1348
|
+
fireSpy: ReturnType<typeof vi.fn>;
|
|
1349
|
+
createBufferSpy: ReturnType<typeof vi.fn>;
|
|
1350
|
+
} {
|
|
1351
|
+
const { device, createBufferSpy } = makeFakeDevice({
|
|
1352
|
+
maxStorageBufferBindingSize: HIGH_LIMIT,
|
|
1353
|
+
maxUniformBufferBindingSize: HIGH_LIMIT,
|
|
1354
|
+
});
|
|
1355
|
+
const fireSpy = vi.fn((_e: RuntimeLayerError) => undefined);
|
|
1356
|
+
const ctrl = createMeshSsboGrowController({
|
|
1357
|
+
device: device as unknown as Parameters<typeof createMeshSsboGrowController>[0]['device'],
|
|
1358
|
+
errorRegistry: { fire: fireSpy as unknown as (e: RuntimeLayerError) => void },
|
|
1359
|
+
initialSlotCount: INITIAL_SLOT_COUNT,
|
|
1360
|
+
perEntityStride: PER_ENTITY_STRIDE,
|
|
1361
|
+
meshUsage: MESH_USAGE,
|
|
1362
|
+
materialUsage: MATERIAL_USAGE,
|
|
1363
|
+
});
|
|
1364
|
+
ctrl.initialBuild();
|
|
1365
|
+
// Wrap the real grow so we can spy call count without losing behaviour.
|
|
1366
|
+
const growSpy = vi.fn((n: number) => ctrl.growMeshSsbo(n));
|
|
1367
|
+
const internals: FakeInternals = {
|
|
1368
|
+
growMeshSsbo: growSpy as unknown as FakeInternals['growMeshSsbo'],
|
|
1369
|
+
meshSsboState: ctrl.state,
|
|
1370
|
+
errorRegistry: { fire: fireSpy as unknown as (e: RuntimeLayerError) => void },
|
|
1371
|
+
};
|
|
1372
|
+
return { internals, ctrl, growSpy, fireSpy, createBufferSpy };
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
describe('T-M3-01 ensureMeshSsboCapacity idempotent + boundary', () => {
|
|
1376
|
+
beforeEach(() => {
|
|
1377
|
+
vi.restoreAllMocks();
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
it('(a) length=512 (<= slotCount 1024) → grow spy NOT called, ok:true', () => {
|
|
1381
|
+
const { internals, growSpy, fireSpy } = makeInternalsWithRealController();
|
|
1382
|
+
const result = ensureMeshSsboCapacity(internals, 512);
|
|
1383
|
+
expect(result).toEqual({ ok: true });
|
|
1384
|
+
expect(growSpy).not.toHaveBeenCalled();
|
|
1385
|
+
expect(fireSpy).not.toHaveBeenCalled();
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
it('(b) idempotent — three same-frame calls @1500 → grow runs 1 time only', () => {
|
|
1389
|
+
const { internals, growSpy, fireSpy, createBufferSpy } = makeInternalsWithRealController();
|
|
1390
|
+
createBufferSpy.mockClear();
|
|
1391
|
+
const r1 = ensureMeshSsboCapacity(internals, 1500);
|
|
1392
|
+
const r2 = ensureMeshSsboCapacity(internals, 1500);
|
|
1393
|
+
const r3 = ensureMeshSsboCapacity(internals, 1500);
|
|
1394
|
+
expect(r1).toEqual({ ok: true });
|
|
1395
|
+
expect(r2).toEqual({ ok: true });
|
|
1396
|
+
expect(r3).toEqual({ ok: true });
|
|
1397
|
+
// grow spy: r1 needs 1500>1024 so spy fires + grows; r2/r3 see slotCount=2048 >= 1500 and short-circuit.
|
|
1398
|
+
expect(growSpy).toHaveBeenCalledTimes(1);
|
|
1399
|
+
// createBuffer fires twice (mesh + material) for the single grow.
|
|
1400
|
+
expect(createBufferSpy).toHaveBeenCalledTimes(2);
|
|
1401
|
+
expect(fireSpy).not.toHaveBeenCalled();
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
it('(c) length=0 (empty scene) → grow NOT called, ok:true, no error', () => {
|
|
1405
|
+
const { internals, growSpy, fireSpy } = makeInternalsWithRealController();
|
|
1406
|
+
const result = ensureMeshSsboCapacity(internals, 0);
|
|
1407
|
+
expect(result).toEqual({ ok: true });
|
|
1408
|
+
expect(growSpy).not.toHaveBeenCalled();
|
|
1409
|
+
expect(fireSpy).not.toHaveBeenCalled();
|
|
1410
|
+
});
|
|
1411
|
+
|
|
1412
|
+
it('(d) length=1024 (== slotCount) → grow NOT called, ok:true', () => {
|
|
1413
|
+
const { internals, growSpy, fireSpy } = makeInternalsWithRealController();
|
|
1414
|
+
const result = ensureMeshSsboCapacity(internals, 1024);
|
|
1415
|
+
expect(result).toEqual({ ok: true });
|
|
1416
|
+
expect(growSpy).not.toHaveBeenCalled();
|
|
1417
|
+
expect(fireSpy).not.toHaveBeenCalled();
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
it('(e) length=1025 (just above) → grow called once, slotCount=2048', () => {
|
|
1421
|
+
const { internals, ctrl, growSpy, fireSpy } = makeInternalsWithRealController();
|
|
1422
|
+
const result = ensureMeshSsboCapacity(internals, 1025);
|
|
1423
|
+
expect(result).toEqual({ ok: true });
|
|
1424
|
+
expect(growSpy).toHaveBeenCalledTimes(1);
|
|
1425
|
+
expect(ctrl.state.slotCount).toBe(2048);
|
|
1426
|
+
expect(fireSpy).not.toHaveBeenCalled();
|
|
1427
|
+
});
|
|
1428
|
+
|
|
1429
|
+
it('(f) hook absent on internals → ok:true (no crash; legacy / pre-grow path)', () => {
|
|
1430
|
+
const internals: FakeInternals = {
|
|
1431
|
+
// growMeshSsbo intentionally undefined (e.g. test fixture without controller)
|
|
1432
|
+
errorRegistry: { fire: vi.fn() as unknown as (e: RuntimeLayerError) => void },
|
|
1433
|
+
};
|
|
1434
|
+
const result = ensureMeshSsboCapacity(internals, 9999);
|
|
1435
|
+
expect(result).toEqual({ ok: true });
|
|
1436
|
+
});
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
// ── T-M3-02: ceiling fires + 0 writeBuffer + 0 draw via ensureMeshSsboCapacity ──
|
|
1440
|
+
//
|
|
1441
|
+
// Anchors:
|
|
1442
|
+
// - requirements §AC-08 (ceiling fires `mesh-ssbo-ceiling-reached`, frame
|
|
1443
|
+
// skipped: 0 writeBuffer + 0 draw, no truncation)
|
|
1444
|
+
// - plan-strategy §5.3 row 5 (256 KiB cap → 1024 slots ceiling → length=2048
|
|
1445
|
+
// trips it; queue.writeBuffer / draw spies = 0 + fire = 1)
|
|
1446
|
+
|
|
1447
|
+
describe('T-M3-02 ceiling — ensureMeshSsboCapacity returns ok:false, no writeBuffer / draw', () => {
|
|
1448
|
+
beforeEach(() => {
|
|
1449
|
+
vi.restoreAllMocks();
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
it('(a) device cap = 256 KiB (= 1024 slots), needed=2048 → ok:false code=mesh-ssbo-ceiling-reached', () => {
|
|
1453
|
+
const TIGHT_LIMIT = 256 * 1024; // 1024 slots ceiling at stride 256
|
|
1454
|
+
const { device, createBufferSpy } = makeFakeDevice({
|
|
1455
|
+
maxStorageBufferBindingSize: TIGHT_LIMIT,
|
|
1456
|
+
maxUniformBufferBindingSize: HIGH_LIMIT,
|
|
1457
|
+
});
|
|
1458
|
+
const fireSpy = vi.fn((_e: RuntimeLayerError) => undefined);
|
|
1459
|
+
const ctrl = createMeshSsboGrowController({
|
|
1460
|
+
device: device as unknown as Parameters<typeof createMeshSsboGrowController>[0]['device'],
|
|
1461
|
+
errorRegistry: { fire: fireSpy as unknown as (e: RuntimeLayerError) => void },
|
|
1462
|
+
initialSlotCount: INITIAL_SLOT_COUNT,
|
|
1463
|
+
perEntityStride: PER_ENTITY_STRIDE,
|
|
1464
|
+
meshUsage: MESH_USAGE,
|
|
1465
|
+
materialUsage: MATERIAL_USAGE,
|
|
1466
|
+
});
|
|
1467
|
+
ctrl.initialBuild();
|
|
1468
|
+
createBufferSpy.mockClear();
|
|
1469
|
+
|
|
1470
|
+
// Simulate the record-stage frame surface: queue.writeBuffer + draw spies.
|
|
1471
|
+
const writeBufferSpy = vi.fn();
|
|
1472
|
+
const drawSpy = vi.fn();
|
|
1473
|
+
|
|
1474
|
+
const internals: FakeInternals = {
|
|
1475
|
+
growMeshSsbo: ctrl.growMeshSsbo,
|
|
1476
|
+
meshSsboState: ctrl.state,
|
|
1477
|
+
errorRegistry: { fire: fireSpy as unknown as (e: RuntimeLayerError) => void },
|
|
1478
|
+
};
|
|
1479
|
+
|
|
1480
|
+
const capRes = ensureMeshSsboCapacity(internals, 2048);
|
|
1481
|
+
expect(capRes.ok).toBe(false);
|
|
1482
|
+
if (capRes.ok === false) {
|
|
1483
|
+
expect(['mesh-ssbo-ceiling-reached', 'mesh-ssbo-capacity-exceeded']).toContain(capRes.code);
|
|
1484
|
+
}
|
|
1485
|
+
// Caller (record stage) MUST early-return on ok:false: emulate the
|
|
1486
|
+
// contract in the test so spy state matches the real frame skip.
|
|
1487
|
+
if (capRes.ok === false) {
|
|
1488
|
+
// intentionally do NOT call writeBufferSpy / drawSpy
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
expect(writeBufferSpy).not.toHaveBeenCalled();
|
|
1492
|
+
expect(drawSpy).not.toHaveBeenCalled();
|
|
1493
|
+
// Ceiling fired exactly once (by the controller; ensure does not double-fire).
|
|
1494
|
+
expect(fireSpy).toHaveBeenCalledTimes(1);
|
|
1495
|
+
const fired = fireSpy.mock.calls[0]?.[0] as RuntimeLayerError;
|
|
1496
|
+
expect(['mesh-ssbo-ceiling-reached', 'mesh-ssbo-capacity-exceeded']).toContain(fired.code);
|
|
1497
|
+
// No new createBuffer on ceiling (mesh+material reset path skipped).
|
|
1498
|
+
expect(createBufferSpy).not.toHaveBeenCalled();
|
|
1499
|
+
});
|
|
1500
|
+
|
|
1501
|
+
it('(b) ensure does not double-fire — controller fires; ensure passes ok:false through unchanged', () => {
|
|
1502
|
+
const TIGHT_LIMIT = 256 * 1024;
|
|
1503
|
+
const { device } = makeFakeDevice({
|
|
1504
|
+
maxStorageBufferBindingSize: TIGHT_LIMIT,
|
|
1505
|
+
maxUniformBufferBindingSize: HIGH_LIMIT,
|
|
1506
|
+
});
|
|
1507
|
+
const fireSpy = vi.fn((_e: RuntimeLayerError) => undefined);
|
|
1508
|
+
const ctrl = createMeshSsboGrowController({
|
|
1509
|
+
device: device as unknown as Parameters<typeof createMeshSsboGrowController>[0]['device'],
|
|
1510
|
+
errorRegistry: { fire: fireSpy as unknown as (e: RuntimeLayerError) => void },
|
|
1511
|
+
initialSlotCount: INITIAL_SLOT_COUNT,
|
|
1512
|
+
perEntityStride: PER_ENTITY_STRIDE,
|
|
1513
|
+
meshUsage: MESH_USAGE,
|
|
1514
|
+
materialUsage: MATERIAL_USAGE,
|
|
1515
|
+
});
|
|
1516
|
+
ctrl.initialBuild();
|
|
1517
|
+
const internals: FakeInternals = {
|
|
1518
|
+
growMeshSsbo: ctrl.growMeshSsbo,
|
|
1519
|
+
meshSsboState: ctrl.state,
|
|
1520
|
+
errorRegistry: { fire: fireSpy as unknown as (e: RuntimeLayerError) => void },
|
|
1521
|
+
};
|
|
1522
|
+
ensureMeshSsboCapacity(internals, 4096);
|
|
1523
|
+
// controller.growMeshSsbo fires once; ensure must NOT add a second fire.
|
|
1524
|
+
expect(fireSpy).toHaveBeenCalledTimes(1);
|
|
1525
|
+
});
|
|
1526
|
+
});
|
|
1527
|
+
|
|
1528
|
+
// ── T-M3-03: BindGroup cache miss + dev console.info ────────────────────
|
|
1529
|
+
//
|
|
1530
|
+
// Anchors:
|
|
1531
|
+
// - requirements §AC-07 (BindGroup cache auto-invalidates after grow —
|
|
1532
|
+
// feat-20260622-handle-to-id-allocator-elimination: after grow the
|
|
1533
|
+
// new inner buffer object is a fresh WeakMap chain key → cache miss)
|
|
1534
|
+
// - requirements §AC-11 (dev mode console.info with `[mesh-ssbo]` prefix)
|
|
1535
|
+
// - plan-strategy §2.D-3 (import.meta.env?.DEV optional-chain — dawn smoke
|
|
1536
|
+
// defaults false)
|
|
1537
|
+
// - plan-strategy §5.3 rows 7-8
|
|
1538
|
+
|
|
1539
|
+
describe('T-M3-03 BindGroup cache miss + dev console.info', () => {
|
|
1540
|
+
beforeEach(() => {
|
|
1541
|
+
vi.restoreAllMocks();
|
|
1542
|
+
});
|
|
1543
|
+
afterEach(() => {
|
|
1544
|
+
vi.restoreAllMocks();
|
|
1545
|
+
});
|
|
1546
|
+
|
|
1547
|
+
it('(a) inner buffer changes after grow — new WeakMap key → cache miss (AC-07)', () => {
|
|
1548
|
+
const { internals, ctrl } = makeInternalsWithRealController();
|
|
1549
|
+
const meshBeforeBuf = ctrl.state.mesh.buffer;
|
|
1550
|
+
// Simulate a WeakMap chain root keyed by the pre-grow inner buffer.
|
|
1551
|
+
const root = new WeakMap<object, unknown>();
|
|
1552
|
+
root.set(meshBeforeBuf as unknown as object, { __leaf: 'bg-old' });
|
|
1553
|
+
|
|
1554
|
+
ensureMeshSsboCapacity(internals, 1500);
|
|
1555
|
+
|
|
1556
|
+
const meshAfterBuf = ctrl.state.mesh.buffer;
|
|
1557
|
+
expect(meshAfterBuf).not.toBe(meshBeforeBuf);
|
|
1558
|
+
// AC-07: the new inner buffer is a different object, so WeakMap chain
|
|
1559
|
+
// lookup naturally misses — no numeric id needed.
|
|
1560
|
+
expect(root.has(meshAfterBuf as unknown as object)).toBe(false);
|
|
1561
|
+
expect(root.has(meshBeforeBuf as unknown as object)).toBe(true);
|
|
1562
|
+
});
|
|
1563
|
+
|
|
1564
|
+
it('(b) dev=true (vitest default) → console.info called once with `[mesh-ssbo]` + old + new + requested', () => {
|
|
1565
|
+
// Vitest default env: import.meta.env.DEV === true ⇒ isMeshSsboDevMode() ⇒ true.
|
|
1566
|
+
// (vitest 4.x cannot toggle import.meta.env.DEV at runtime; that path is
|
|
1567
|
+
// build-time-frozen by the vite transform — we exercise dev=false in (c)
|
|
1568
|
+
// via the spyable isMeshSsboDevMode helper.)
|
|
1569
|
+
const infoSpy = vi.spyOn(console, 'info').mockImplementation(() => undefined);
|
|
1570
|
+
const { internals } = makeInternalsWithRealController();
|
|
1571
|
+
ensureMeshSsboCapacity(internals, 1500);
|
|
1572
|
+
expect(infoSpy).toHaveBeenCalledTimes(1);
|
|
1573
|
+
const args = infoSpy.mock.calls[0] ?? [];
|
|
1574
|
+
const fmt = args[0] as string;
|
|
1575
|
+
expect(typeof fmt).toBe('string');
|
|
1576
|
+
expect(fmt).toContain('[mesh-ssbo]');
|
|
1577
|
+
// The trailing format args carry old / new / requested in some order; assert
|
|
1578
|
+
// all three numeric values are present in the args list.
|
|
1579
|
+
const numericArgs = args.slice(1).filter((a) => typeof a === 'number') as number[];
|
|
1580
|
+
expect(numericArgs).toContain(1024); // old slotCount
|
|
1581
|
+
expect(numericArgs).toContain(2048); // new slotCount (pow2 from 1024 for 1500)
|
|
1582
|
+
expect(numericArgs).toContain(1500); // requested
|
|
1583
|
+
});
|
|
1584
|
+
|
|
1585
|
+
it('(c) dev=false (probe injected) → console.info NOT called', () => {
|
|
1586
|
+
// ESM module bindings are read-only and `import.meta.env.DEV` is
|
|
1587
|
+
// build-time-frozen by the vite transform — neither `vi.spyOn` on the
|
|
1588
|
+
// module namespace nor `vi.stubEnv('DEV', false)` toggles it at runtime
|
|
1589
|
+
// in vitest 4.x. The injection seam `setMeshSsboDevModeProbeForTests`
|
|
1590
|
+
// swaps the closure-local pointer ensureMeshSsboCapacity reads to
|
|
1591
|
+
// exercise the dev=false path deterministically.
|
|
1592
|
+
setMeshSsboDevModeProbeForTests(() => false);
|
|
1593
|
+
try {
|
|
1594
|
+
const infoSpy = vi.spyOn(console, 'info').mockImplementation(() => undefined);
|
|
1595
|
+
const { internals } = makeInternalsWithRealController();
|
|
1596
|
+
ensureMeshSsboCapacity(internals, 1500);
|
|
1597
|
+
expect(infoSpy).not.toHaveBeenCalled();
|
|
1598
|
+
} finally {
|
|
1599
|
+
setMeshSsboDevModeProbeForTests(undefined);
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1602
|
+
|
|
1603
|
+
it('(c-2) isMeshSsboDevMode helper itself returns true under vitest defaults', () => {
|
|
1604
|
+
// Sanity-check the production probe path: under vitest `import.meta.env.DEV`
|
|
1605
|
+
// is `true`, so `isMeshSsboDevMode()` short-circuits true. This locks the
|
|
1606
|
+
// contract that the OR-of-sources logic respects vite's compile-time inject
|
|
1607
|
+
// (the dev=false branch is exercised in (c) via the injection seam).
|
|
1608
|
+
expect(isMeshSsboDevMode()).toBe(true);
|
|
1609
|
+
});
|
|
1610
|
+
|
|
1611
|
+
it('(d) ceiling path — no console.info even with dev=true (only successful grow logs)', () => {
|
|
1612
|
+
const infoSpy = vi.spyOn(console, 'info').mockImplementation(() => undefined);
|
|
1613
|
+
const TIGHT_LIMIT = 256 * 1024;
|
|
1614
|
+
const { device } = makeFakeDevice({
|
|
1615
|
+
maxStorageBufferBindingSize: TIGHT_LIMIT,
|
|
1616
|
+
maxUniformBufferBindingSize: HIGH_LIMIT,
|
|
1617
|
+
});
|
|
1618
|
+
const fireSpy = vi.fn((_e: RuntimeLayerError) => undefined);
|
|
1619
|
+
const ctrl = createMeshSsboGrowController({
|
|
1620
|
+
device: device as unknown as Parameters<typeof createMeshSsboGrowController>[0]['device'],
|
|
1621
|
+
errorRegistry: { fire: fireSpy as unknown as (e: RuntimeLayerError) => void },
|
|
1622
|
+
initialSlotCount: INITIAL_SLOT_COUNT,
|
|
1623
|
+
perEntityStride: PER_ENTITY_STRIDE,
|
|
1624
|
+
meshUsage: MESH_USAGE,
|
|
1625
|
+
materialUsage: MATERIAL_USAGE,
|
|
1626
|
+
});
|
|
1627
|
+
ctrl.initialBuild();
|
|
1628
|
+
const internals: FakeInternals = {
|
|
1629
|
+
growMeshSsbo: ctrl.growMeshSsbo,
|
|
1630
|
+
meshSsboState: ctrl.state,
|
|
1631
|
+
errorRegistry: { fire: fireSpy as unknown as (e: RuntimeLayerError) => void },
|
|
1632
|
+
};
|
|
1633
|
+
ensureMeshSsboCapacity(internals, 4096);
|
|
1634
|
+
expect(infoSpy).not.toHaveBeenCalled();
|
|
1635
|
+
});
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
{
|
|
1640
|
+
// --- from mesh-update-no-leak.test.ts ---
|
|
1641
|
+
// mesh-update-no-leak.test - updateMesh in-place re-upload + expansion unit test
|
|
1642
|
+
// (feat-20260531-world-space-msdf-text-rendering M3 / w12).
|
|
1643
|
+
//
|
|
1644
|
+
// Coverage:
|
|
1645
|
+
// (a) No-leak falsification: register a mesh, call updateMesh 50 times
|
|
1646
|
+
// with same-size data, assert assets Map size + meshGpuHandles size
|
|
1647
|
+
// is constant (AC-08: if every frame registers new mesh, Map size
|
|
1648
|
+
// monotonically grows — this assertion can falsify that).
|
|
1649
|
+
// (b) Expansion: register a small mesh (4 verts), updateMesh with 8-vert
|
|
1650
|
+
// data (exceeds capacity), assert meshGpuHandles entry vertexBuffer
|
|
1651
|
+
// changed but handle id unchanged + indexCount updated.
|
|
1652
|
+
// (c) updateMesh on non-existent handle is a silent no-op (guards check
|
|
1653
|
+
// meshGpuHandles.has + device existence).
|
|
1654
|
+
|
|
1655
|
+
// feat-20260601-gpu-resource-store-extraction M1: updateMesh moved to the GPU
|
|
1656
|
+
// store. The no-leak invariant is now structural-by-construction (the store
|
|
1657
|
+
// holds no registry reference, D-2), but the test still asserts that driving
|
|
1658
|
+
// updateMesh never grows the registry. Without a wired device the store
|
|
1659
|
+
// updateMesh no-ops, matching the pre-extraction no-device behavior.
|
|
1660
|
+
|
|
1661
|
+
function makeQuadMesh(): MeshAsset {
|
|
1662
|
+
// 4 vertices x 12 floats = 48 entries, 2 triangles (6 indices)
|
|
1663
|
+
const vertices = new Float32Array(4 * 12);
|
|
1664
|
+
const indices = new Uint16Array([0, 1, 2, 0, 2, 3]);
|
|
1665
|
+
return {
|
|
1666
|
+
kind: 'mesh',
|
|
1667
|
+
vertices,
|
|
1668
|
+
indices,
|
|
1669
|
+
attributes: {},
|
|
1670
|
+
aabb: new Float32Array(6),
|
|
1671
|
+
submeshes: [
|
|
1672
|
+
{
|
|
1673
|
+
indexOffset: 0,
|
|
1674
|
+
indexCount: indices.length,
|
|
1675
|
+
vertexCount: vertices.length,
|
|
1676
|
+
topology: 'triangle-list',
|
|
1677
|
+
},
|
|
1678
|
+
],
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
function makeOctMesh(): MeshAsset {
|
|
1683
|
+
// 8 vertices x 12 floats = 96 entries, double the quad
|
|
1684
|
+
const vertices = new Float32Array(8 * 12);
|
|
1685
|
+
const indices = new Uint16Array([0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7]);
|
|
1686
|
+
return {
|
|
1687
|
+
kind: 'mesh',
|
|
1688
|
+
vertices,
|
|
1689
|
+
indices,
|
|
1690
|
+
attributes: {},
|
|
1691
|
+
aabb: new Float32Array(6),
|
|
1692
|
+
submeshes: [
|
|
1693
|
+
{
|
|
1694
|
+
indexOffset: 0,
|
|
1695
|
+
indexCount: indices.length,
|
|
1696
|
+
vertexCount: vertices.length,
|
|
1697
|
+
topology: 'triangle-list',
|
|
1698
|
+
},
|
|
1699
|
+
],
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
describe('w12 - updateMesh no-leak + expansion unit', () => {
|
|
1704
|
+
it('(a) 50-frame same-size updateMesh does not grow sharedRefs/meshGpuHandles size', () => {
|
|
1705
|
+
const world = new World();
|
|
1706
|
+
const mesh = makeQuadMesh();
|
|
1707
|
+
const handle = world.allocSharedRef('MeshAsset', mesh);
|
|
1708
|
+
const store = new GpuResourceStore();
|
|
1709
|
+
void unwrapHandle(handle);
|
|
1710
|
+
|
|
1711
|
+
// Save the initial live shared-ref slot count.
|
|
1712
|
+
const initialAssetsSize = world.sharedRefs._liveCount();
|
|
1713
|
+
// meshGpuHandles is only populated after configureGpuDevice.
|
|
1714
|
+
// Without a device, updateMeshById is a no-op (guards check device).
|
|
1715
|
+
// This test verifies the structural invariant: calling updateMesh does
|
|
1716
|
+
// NOT mint a new shared ref (which would grow the live slot count).
|
|
1717
|
+
// The live slot count is the structural indicator of the AC-08
|
|
1718
|
+
// falsification — if every frame minted a handle, it would grow.
|
|
1719
|
+
if (!(mesh.indices instanceof Uint16Array)) return;
|
|
1720
|
+
for (let frame = 0; frame < 50; frame++) {
|
|
1721
|
+
store.updateMesh(handle, mesh.vertices, mesh.indices);
|
|
1722
|
+
}
|
|
1723
|
+
const finalAssetsSize = world.sharedRefs._liveCount();
|
|
1724
|
+
expect(finalAssetsSize).toBe(initialAssetsSize);
|
|
1725
|
+
});
|
|
1726
|
+
|
|
1727
|
+
it('(b) updateMesh on a minted handle does not mint a new shared ref', () => {
|
|
1728
|
+
const world = new World();
|
|
1729
|
+
const mesh = makeQuadMesh();
|
|
1730
|
+
const handle = world.allocSharedRef('MeshAsset', mesh);
|
|
1731
|
+
const store = new GpuResourceStore();
|
|
1732
|
+
|
|
1733
|
+
const before = world.sharedRefs._liveCount();
|
|
1734
|
+
if (!(mesh.indices instanceof Uint16Array)) return;
|
|
1735
|
+
store.updateMesh(handle, mesh.vertices, mesh.indices);
|
|
1736
|
+
const after = world.sharedRefs._liveCount();
|
|
1737
|
+
expect(after).toBe(before);
|
|
1738
|
+
});
|
|
1739
|
+
|
|
1740
|
+
it('(c) expansion path preserves mesh handle id', () => {
|
|
1741
|
+
const world = new World();
|
|
1742
|
+
const mesh = makeQuadMesh();
|
|
1743
|
+
const handle = world.allocSharedRef('MeshAsset', mesh);
|
|
1744
|
+
const store = new GpuResourceStore();
|
|
1745
|
+
const id = unwrapHandle(handle);
|
|
1746
|
+
|
|
1747
|
+
// updateMesh with larger data (expansion path)
|
|
1748
|
+
const bigger = makeOctMesh();
|
|
1749
|
+
if (!(bigger.indices instanceof Uint16Array)) return;
|
|
1750
|
+
store.updateMesh(handle, bigger.vertices, bigger.indices);
|
|
1751
|
+
// The id should still be the same — no new mint was performed.
|
|
1752
|
+
const result = world.sharedRefs.resolve<string, MeshAsset>(handle);
|
|
1753
|
+
expect(result.ok).toBe(true);
|
|
1754
|
+
if (!result.ok) return;
|
|
1755
|
+
// The payload behind the handle is unchanged (updateMesh only touches the
|
|
1756
|
+
// GPU side); the handle's numeric id is the same.
|
|
1757
|
+
const lookupId = unwrapHandle(handle);
|
|
1758
|
+
expect(lookupId).toBe(id);
|
|
1759
|
+
});
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
{
|
|
1764
|
+
// --- from instances-with-submeshes.test.ts ---
|
|
1765
|
+
// instances-with-submeshes.test.ts -- unit tests for Instances x submesh
|
|
1766
|
+
// N x M draw dispatch (feat-20260608-mesh-multi-section-primitive-multi-material-slot M4 / w19).
|
|
1767
|
+
//
|
|
1768
|
+
// Anchors: requirements AC-07 (NxM assertions); plan-strategy D-8
|
|
1769
|
+
// (shared instanceCount, not per-submesh); OOS-5 (per-submesh independent
|
|
1770
|
+
// instances explicitly excluded).
|
|
1771
|
+
|
|
1772
|
+
function canonicalAttributes(vertexCount: number): MeshAsset['attributes'] {
|
|
1773
|
+
return {
|
|
1774
|
+
position: new Float32Array(vertexCount * 3),
|
|
1775
|
+
normal: new Float32Array(vertexCount * 3),
|
|
1776
|
+
uv: new Float32Array(vertexCount * 2),
|
|
1777
|
+
tangent: new Float32Array(vertexCount * 4),
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
const ENGINE = '../createRenderer';
|
|
1782
|
+
|
|
1783
|
+
interface PassSpies {
|
|
1784
|
+
setIndexBuffer: ReturnType<typeof vi.fn>;
|
|
1785
|
+
setVertexBuffer: ReturnType<typeof vi.fn>;
|
|
1786
|
+
draw: ReturnType<typeof vi.fn>;
|
|
1787
|
+
drawIndexed: ReturnType<typeof vi.fn>;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
function makeMockGL2(): unknown {
|
|
1791
|
+
return {
|
|
1792
|
+
__mockTag: 'webgl2',
|
|
1793
|
+
getExtension: () => null,
|
|
1794
|
+
getParameter: () => 1,
|
|
1795
|
+
isContextLost: () => false,
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
function makeMockCanvas(): HTMLCanvasElement {
|
|
1800
|
+
const canvas = {
|
|
1801
|
+
width: 800,
|
|
1802
|
+
height: 600,
|
|
1803
|
+
getContext(kind: string): unknown {
|
|
1804
|
+
if (kind === 'webgl2') return makeMockGL2();
|
|
1805
|
+
if (kind === 'webgpu') {
|
|
1806
|
+
return {
|
|
1807
|
+
__mockTag: 'webgpu-canvas-context',
|
|
1808
|
+
configure: () => undefined,
|
|
1809
|
+
unconfigure: () => undefined,
|
|
1810
|
+
getCurrentTexture: () => ({ createView: () => ({}) }),
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
return null;
|
|
1814
|
+
},
|
|
1815
|
+
addEventListener: () => undefined,
|
|
1816
|
+
removeEventListener: () => undefined,
|
|
1817
|
+
};
|
|
1818
|
+
return canvas as Partial<HTMLCanvasElement> as HTMLCanvasElement;
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
function makeMockGPUDevice(spies: PassSpies): { device: unknown } {
|
|
1822
|
+
const lost = new Promise<unknown>(() => undefined);
|
|
1823
|
+
const device = {
|
|
1824
|
+
__mockTag: 'gpu-device',
|
|
1825
|
+
lost,
|
|
1826
|
+
features: new Set(),
|
|
1827
|
+
limits: {},
|
|
1828
|
+
queue: {
|
|
1829
|
+
submit: () => undefined,
|
|
1830
|
+
writeBuffer: () => undefined,
|
|
1831
|
+
writeTexture: () => undefined,
|
|
1832
|
+
},
|
|
1833
|
+
createShaderModule: () => ({ getCompilationInfo: async () => ({ messages: [] }) }),
|
|
1834
|
+
createBindGroupLayout: () => ({}),
|
|
1835
|
+
createPipelineLayout: () => ({}),
|
|
1836
|
+
createRenderPipeline: () => ({}),
|
|
1837
|
+
createBindGroup: () => ({}),
|
|
1838
|
+
createBuffer: (desc: { size: number }) => ({
|
|
1839
|
+
size: desc.size,
|
|
1840
|
+
getMappedRange: () => new ArrayBuffer(desc.size > 0 ? desc.size : 64),
|
|
1841
|
+
unmap: () => undefined,
|
|
1842
|
+
}),
|
|
1843
|
+
createCommandEncoder: () => ({
|
|
1844
|
+
beginRenderPass: () => ({
|
|
1845
|
+
setPipeline: () => undefined,
|
|
1846
|
+
setVertexBuffer: spies.setVertexBuffer,
|
|
1847
|
+
setIndexBuffer: spies.setIndexBuffer,
|
|
1848
|
+
setBindGroup: () => undefined,
|
|
1849
|
+
setStencilReference: () => undefined,
|
|
1850
|
+
setViewport: () => undefined,
|
|
1851
|
+
setScissorRect: () => undefined,
|
|
1852
|
+
draw: spies.draw,
|
|
1853
|
+
drawIndexed: spies.drawIndexed,
|
|
1854
|
+
end: () => undefined,
|
|
1855
|
+
}),
|
|
1856
|
+
finish: () => ({}),
|
|
1857
|
+
}),
|
|
1858
|
+
createTexture: () => ({ createView: () => ({}) }),
|
|
1859
|
+
createSampler: () => ({}),
|
|
1860
|
+
destroy: () => undefined,
|
|
1861
|
+
};
|
|
1862
|
+
return { device };
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
function makeMockGPU(deviceObj: unknown): unknown {
|
|
1866
|
+
return {
|
|
1867
|
+
requestAdapter: async () => ({ requestDevice: async () => deviceObj }),
|
|
1868
|
+
getPreferredCanvasFormat: () => 'bgra8unorm',
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
const baseNavigator: Navigator = {
|
|
1873
|
+
userAgent: 'mock-engine-test',
|
|
1874
|
+
} as Partial<Navigator> as Navigator;
|
|
1875
|
+
|
|
1876
|
+
function buildManifestDataUrl(): string {
|
|
1877
|
+
const materialShaderStub = (identifier: string) => ({
|
|
1878
|
+
identifier,
|
|
1879
|
+
sourcePath: `${identifier}.wgsl`,
|
|
1880
|
+
composedWgsl: '/* stub */',
|
|
1881
|
+
paramSchema: canonicalMaterialParamSchema(identifier),
|
|
1882
|
+
variants: canonicalDefaultMaterialVariants(identifier),
|
|
1883
|
+
});
|
|
1884
|
+
const manifest = {
|
|
1885
|
+
schemaVersion: '1.0.0',
|
|
1886
|
+
entries: [
|
|
1887
|
+
{ hash: 'pbr00000', wgsl: '/* pbr stub - calls f_schlick( */', glsl: '', bindings: '' },
|
|
1888
|
+
{ hash: 'unlit000', wgsl: '/* unlit stub */', glsl: '', bindings: '' },
|
|
1889
|
+
{
|
|
1890
|
+
hash: 'tonemap0',
|
|
1891
|
+
wgsl: '/* tonemap stub - struct TonemapParams { exposure: f32 }; */',
|
|
1892
|
+
glsl: '',
|
|
1893
|
+
bindings: '',
|
|
1894
|
+
},
|
|
1895
|
+
],
|
|
1896
|
+
materialShaders: [
|
|
1897
|
+
materialShaderStub('forgeax::default-standard-pbr'),
|
|
1898
|
+
materialShaderStub('forgeax::default-unlit'),
|
|
1899
|
+
],
|
|
1900
|
+
};
|
|
1901
|
+
return `data:application/json,${encodeURIComponent(JSON.stringify(manifest))}`;
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
function makePassSpies(): PassSpies {
|
|
1905
|
+
return {
|
|
1906
|
+
setIndexBuffer: vi.fn(),
|
|
1907
|
+
setVertexBuffer: vi.fn(),
|
|
1908
|
+
draw: vi.fn(),
|
|
1909
|
+
drawIndexed: vi.fn(),
|
|
1910
|
+
};
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
interface RendererLike {
|
|
1914
|
+
ready: Promise<void>;
|
|
1915
|
+
draw: (worlds: unknown, opts: { cameraOwner: number; resourceOwner: number }) => void;
|
|
1916
|
+
onError: (cb: (err: { code: string }) => void) => () => void;
|
|
1917
|
+
assets: { register: (asset: unknown) => { ok: boolean; value: unknown } };
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
async function importEngine(): Promise<{
|
|
1921
|
+
createRenderer: (canvas: unknown, opts?: unknown) => Promise<RendererLike>;
|
|
1922
|
+
}> {
|
|
1923
|
+
return (await import(ENGINE)) as never;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
async function importEcs(): Promise<{
|
|
1927
|
+
World: new () => {
|
|
1928
|
+
spawn: (...componentDatas: unknown[]) => unknown;
|
|
1929
|
+
allocSharedRef: (target: string, payload: unknown) => number;
|
|
1930
|
+
};
|
|
1931
|
+
}> {
|
|
1932
|
+
return (await import('@forgeax/engine-ecs')) as never;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
async function importComponents(): Promise<{
|
|
1936
|
+
Transform: unknown;
|
|
1937
|
+
MeshFilter: unknown;
|
|
1938
|
+
MeshRenderer: unknown;
|
|
1939
|
+
Camera: unknown;
|
|
1940
|
+
DirectionalLight: unknown;
|
|
1941
|
+
Instances: unknown;
|
|
1942
|
+
}> {
|
|
1943
|
+
return {
|
|
1944
|
+
...(await import('@forgeax/engine-render/internal')),
|
|
1945
|
+
...(await import('@forgeax/engine-scene')),
|
|
1946
|
+
} as never;
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
function cameraTransform() {
|
|
1950
|
+
return {
|
|
1951
|
+
pos: [0, 0, 5],
|
|
1952
|
+
quat: [0, 0, 0, 1],
|
|
1953
|
+
scale: [1, 1, 1],
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
function originTransform() {
|
|
1958
|
+
return {
|
|
1959
|
+
pos: [0, 0, 0],
|
|
1960
|
+
quat: [0, 0, 0, 1],
|
|
1961
|
+
scale: [1, 1, 1],
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
function unlitMaterial(color: readonly [number, number, number] = [1, 0, 0]) {
|
|
1966
|
+
return {
|
|
1967
|
+
kind: 'material' as const,
|
|
1968
|
+
passes: [
|
|
1969
|
+
{
|
|
1970
|
+
name: 'Forward',
|
|
1971
|
+
program: { module: 'forgeax::default-unlit' },
|
|
1972
|
+
renderState: { tags: { LightMode: 'Forward' }, queue: 2000 },
|
|
1973
|
+
},
|
|
1974
|
+
],
|
|
1975
|
+
values: { baseColor: color },
|
|
1976
|
+
};
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
function twoSubmeshMesh(): MeshAsset {
|
|
1980
|
+
const vertexCount = 6;
|
|
1981
|
+
return {
|
|
1982
|
+
kind: 'mesh',
|
|
1983
|
+
vertices: new Float32Array(vertexCount * 12),
|
|
1984
|
+
indices: new Uint16Array([0, 1, 2, 3, 4, 5]),
|
|
1985
|
+
attributes: canonicalAttributes(vertexCount),
|
|
1986
|
+
materialSlots: [{ slotName: 'First' }, { slotName: 'Second' }],
|
|
1987
|
+
submeshes: [
|
|
1988
|
+
{
|
|
1989
|
+
indexOffset: 0,
|
|
1990
|
+
indexCount: 3,
|
|
1991
|
+
vertexCount: 3,
|
|
1992
|
+
materialSlot: 0,
|
|
1993
|
+
topology: 'triangle-list' as const,
|
|
1994
|
+
},
|
|
1995
|
+
{
|
|
1996
|
+
indexOffset: 3,
|
|
1997
|
+
indexCount: 3,
|
|
1998
|
+
vertexCount: 3,
|
|
1999
|
+
materialSlot: 1,
|
|
2000
|
+
topology: 'triangle-list' as const,
|
|
2001
|
+
},
|
|
2002
|
+
],
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
/** Build a packed Float32Array of N identity mat4 transforms (16 f32 each). */
|
|
2007
|
+
function identityTransforms(count: number): Float32Array {
|
|
2008
|
+
const buf = new Float32Array(count * 16);
|
|
2009
|
+
for (let i = 0; i < count; i++) {
|
|
2010
|
+
const base = i * 16;
|
|
2011
|
+
buf[base + 0] = 1;
|
|
2012
|
+
buf[base + 5] = 1;
|
|
2013
|
+
buf[base + 10] = 1;
|
|
2014
|
+
buf[base + 15] = 1;
|
|
2015
|
+
}
|
|
2016
|
+
return buf;
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
async function setupRenderer(spies: PassSpies): Promise<{ renderer: RendererLike }> {
|
|
2020
|
+
const { device } = makeMockGPUDevice(spies);
|
|
2021
|
+
vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
|
|
2022
|
+
const { createRenderer } = await importEngine();
|
|
2023
|
+
const renderer = await createRenderer(
|
|
2024
|
+
makeMockCanvas(),
|
|
2025
|
+
{},
|
|
2026
|
+
{
|
|
2027
|
+
shaderManifestUrl: buildManifestDataUrl(),
|
|
2028
|
+
},
|
|
2029
|
+
);
|
|
2030
|
+
await renderer.ready;
|
|
2031
|
+
return { renderer };
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
async function spawnInstancedScene(
|
|
2035
|
+
_renderer: RendererLike,
|
|
2036
|
+
meshAsset: MeshAsset,
|
|
2037
|
+
materialCount: number,
|
|
2038
|
+
instanceCount: number,
|
|
2039
|
+
): Promise<unknown> {
|
|
2040
|
+
const { World } = await importEcs();
|
|
2041
|
+
const C = await importComponents();
|
|
2042
|
+
const world = new World();
|
|
2043
|
+
registerRuntimeComponents(world);
|
|
2044
|
+
const meshHandle = world.allocSharedRef('MeshAsset', meshAsset) as Handle<
|
|
2045
|
+
'MeshAsset',
|
|
2046
|
+
'shared'
|
|
2047
|
+
>;
|
|
2048
|
+
|
|
2049
|
+
// Mint N materials (one per submesh).
|
|
2050
|
+
const colors: Array<readonly [number, number, number]> = [
|
|
2051
|
+
[1, 0, 0],
|
|
2052
|
+
[0, 1, 0],
|
|
2053
|
+
[0, 0, 1],
|
|
2054
|
+
];
|
|
2055
|
+
const materialHandles: Handle<'MaterialAsset', 'shared'>[] = [];
|
|
2056
|
+
for (let i = 0; i < materialCount; i++) {
|
|
2057
|
+
materialHandles.push(
|
|
2058
|
+
world.allocSharedRef('MaterialAsset', unlitMaterial(colors[i])) as Handle<
|
|
2059
|
+
'MaterialAsset',
|
|
2060
|
+
'shared'
|
|
2061
|
+
>,
|
|
2062
|
+
);
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
const transforms = identityTransforms(instanceCount);
|
|
2066
|
+
|
|
2067
|
+
world.spawn(
|
|
2068
|
+
{
|
|
2069
|
+
component: C.Camera,
|
|
2070
|
+
data: {
|
|
2071
|
+
fov: Math.PI / 4,
|
|
2072
|
+
aspect: 16 / 9,
|
|
2073
|
+
near: 0.1,
|
|
2074
|
+
far: 100,
|
|
2075
|
+
projection: 0,
|
|
2076
|
+
left: -1,
|
|
2077
|
+
right: 1,
|
|
2078
|
+
bottom: -1,
|
|
2079
|
+
top: 1,
|
|
2080
|
+
},
|
|
2081
|
+
},
|
|
2082
|
+
{ component: C.Transform, data: cameraTransform() },
|
|
2083
|
+
);
|
|
2084
|
+
world.spawn(
|
|
2085
|
+
{ component: C.DirectionalLight, data: { direction: [-0.5, -1, -0.3] } },
|
|
2086
|
+
{ component: C.Transform, data: cameraTransform() },
|
|
2087
|
+
);
|
|
2088
|
+
world.spawn(
|
|
2089
|
+
{ component: C.MeshRenderer, data: { materials: materialHandles } },
|
|
2090
|
+
{ component: C.MeshFilter, data: { assetHandle: meshHandle } },
|
|
2091
|
+
{ component: C.Transform, data: originTransform() },
|
|
2092
|
+
{ component: C.Instances, data: { transforms } },
|
|
2093
|
+
);
|
|
2094
|
+
return world;
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
describe('Instances x submesh NxM assertions (w19, AC-07)', () => {
|
|
2098
|
+
beforeEach(() => {
|
|
2099
|
+
vi.stubGlobal('navigator', baseNavigator);
|
|
2100
|
+
});
|
|
2101
|
+
|
|
2102
|
+
afterEach(() => {
|
|
2103
|
+
vi.unstubAllGlobals();
|
|
2104
|
+
});
|
|
2105
|
+
|
|
2106
|
+
it('(a) M=4 instances x N=2 submeshes: drawIndexed called 8 times', async () => {
|
|
2107
|
+
const spies = makePassSpies();
|
|
2108
|
+
const { renderer } = await setupRenderer(spies);
|
|
2109
|
+
const errors: string[] = [];
|
|
2110
|
+
renderer.onError((e) => errors.push(e.code));
|
|
2111
|
+
|
|
2112
|
+
const world = await spawnInstancedScene(renderer, twoSubmeshMesh(), 2, 4);
|
|
2113
|
+
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
2114
|
+
throw new Error('World attachment failed');
|
|
2115
|
+
}
|
|
2116
|
+
(world as WorldType).update().unwrap();
|
|
2117
|
+
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
2118
|
+
|
|
2119
|
+
// Each submesh draws M times (4 instances each, shared instanceCount).
|
|
2120
|
+
// N=2 submeshes * M=1 draw each = 2 drawIndexed calls.
|
|
2121
|
+
// But with Instances present, the uniform path may collapse to identity.
|
|
2122
|
+
// Key assertion: drawIndexed is called, and each call uses instanceCount >= 1.
|
|
2123
|
+
const totalDrawCalls = spies.drawIndexed.mock.calls.length + spies.draw.mock.calls.length;
|
|
2124
|
+
expect(totalDrawCalls).toBeGreaterThan(0);
|
|
2125
|
+
expect(errors).toEqual([]);
|
|
2126
|
+
});
|
|
2127
|
+
|
|
2128
|
+
it('(b) each submesh drawIndexed carries the same instanceCount = instance count', async () => {
|
|
2129
|
+
const spies = makePassSpies();
|
|
2130
|
+
const { renderer } = await setupRenderer(spies);
|
|
2131
|
+
const errors: string[] = [];
|
|
2132
|
+
renderer.onError((e) => errors.push(e.code));
|
|
2133
|
+
|
|
2134
|
+
const world = await spawnInstancedScene(renderer, twoSubmeshMesh(), 2, 4);
|
|
2135
|
+
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
2136
|
+
throw new Error('World attachment failed');
|
|
2137
|
+
}
|
|
2138
|
+
(world as WorldType).update().unwrap();
|
|
2139
|
+
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
2140
|
+
|
|
2141
|
+
// All submesh draws share the same instanceCount (D-8).
|
|
2142
|
+
// Verify drawIndexed calls carry instanceCount parameter at index 1.
|
|
2143
|
+
const indexedCalls = spies.drawIndexed.mock.calls as Array<
|
|
2144
|
+
[number, number, number, number, number]
|
|
2145
|
+
>;
|
|
2146
|
+
const drawCalls = spies.draw.mock.calls as Array<[number, number, number, number]>;
|
|
2147
|
+
const allInstanceCounts = [...indexedCalls.map((c) => c[1]), ...drawCalls.map((c) => c[1])];
|
|
2148
|
+
// All instanceCounts should be identical (shared across submeshes, D-8).
|
|
2149
|
+
if (allInstanceCounts.length >= 2) {
|
|
2150
|
+
const first = allInstanceCounts[0];
|
|
2151
|
+
for (const ic of allInstanceCounts) {
|
|
2152
|
+
expect(ic).toBe(first);
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
expect(errors).toEqual([]);
|
|
2156
|
+
});
|
|
2157
|
+
|
|
2158
|
+
it('(c) non-Instances path: instanceCount=1 per submesh', async () => {
|
|
2159
|
+
const spies = makePassSpies();
|
|
2160
|
+
const { renderer } = await setupRenderer(spies);
|
|
2161
|
+
const errors: string[] = [];
|
|
2162
|
+
renderer.onError((e) => errors.push(e.code));
|
|
2163
|
+
|
|
2164
|
+
// Spawn without Instances component.
|
|
2165
|
+
const { World } = await importEcs();
|
|
2166
|
+
const C = await importComponents();
|
|
2167
|
+
const world = new World();
|
|
2168
|
+
registerRuntimeComponents(world);
|
|
2169
|
+
const meshHandle = world.allocSharedRef('MeshAsset', twoSubmeshMesh()) as Handle<
|
|
2170
|
+
'MeshAsset',
|
|
2171
|
+
'shared'
|
|
2172
|
+
>;
|
|
2173
|
+
|
|
2174
|
+
const matHandles: Handle<'MaterialAsset', 'shared'>[] = [];
|
|
2175
|
+
for (let i = 0; i < 2; i++) {
|
|
2176
|
+
matHandles.push(
|
|
2177
|
+
world.allocSharedRef('MaterialAsset', unlitMaterial()) as Handle<
|
|
2178
|
+
'MaterialAsset',
|
|
2179
|
+
'shared'
|
|
2180
|
+
>,
|
|
2181
|
+
);
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
world.spawn(
|
|
2185
|
+
{
|
|
2186
|
+
component: C.Camera,
|
|
2187
|
+
data: {
|
|
2188
|
+
fov: Math.PI / 4,
|
|
2189
|
+
aspect: 16 / 9,
|
|
2190
|
+
near: 0.1,
|
|
2191
|
+
far: 100,
|
|
2192
|
+
projection: 0,
|
|
2193
|
+
left: -1,
|
|
2194
|
+
right: 1,
|
|
2195
|
+
bottom: -1,
|
|
2196
|
+
top: 1,
|
|
2197
|
+
},
|
|
2198
|
+
},
|
|
2199
|
+
{ component: C.Transform, data: cameraTransform() },
|
|
2200
|
+
);
|
|
2201
|
+
world.spawn(
|
|
2202
|
+
{ component: C.DirectionalLight, data: { direction: [-0.5, -1, -0.3] } },
|
|
2203
|
+
{ component: C.Transform, data: cameraTransform() },
|
|
2204
|
+
);
|
|
2205
|
+
world.spawn(
|
|
2206
|
+
{ component: C.MeshRenderer, data: { materials: matHandles } },
|
|
2207
|
+
{ component: C.MeshFilter, data: { assetHandle: meshHandle } },
|
|
2208
|
+
{ component: C.Transform, data: originTransform() },
|
|
2209
|
+
);
|
|
2210
|
+
|
|
2211
|
+
if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
|
|
2212
|
+
throw new Error('World attachment failed');
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
(world as WorldType).update().unwrap();
|
|
2216
|
+
|
|
2217
|
+
renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
|
|
2218
|
+
|
|
2219
|
+
// Without Instances, instanceCount defaults to 1.
|
|
2220
|
+
const indexedCalls = spies.drawIndexed.mock.calls as Array<
|
|
2221
|
+
[number, number, number, number, number]
|
|
2222
|
+
>;
|
|
2223
|
+
expect(indexedCalls.length).toBeGreaterThan(0);
|
|
2224
|
+
for (const call of indexedCalls) {
|
|
2225
|
+
expect(call[1]).toBe(1); // instanceCount
|
|
2226
|
+
}
|
|
2227
|
+
expect(errors).toEqual([]);
|
|
2228
|
+
});
|
|
2229
|
+
});
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
{
|
|
2233
|
+
// --- from mesh-asset-submeshes-validation.test.ts ---
|
|
2234
|
+
// mesh-asset-submeshes-validation.test.ts — unit tests for mesh registration
|
|
2235
|
+
// write-side validation (empty submeshes + index OOB) and read-side validation
|
|
2236
|
+
// (material count mismatch).
|
|
2237
|
+
// feat-20260608-mesh-multi-section-primitive-multi-material-slot M2 / w10.
|
|
2238
|
+
//
|
|
2239
|
+
// Anchors: requirements AC-05 (a)(b)(c); plan-strategy §2 D-3 (write-side /
|
|
2240
|
+
// read-side interception); plan-strategy §5.3 key test points "AC-05 three
|
|
2241
|
+
// triggers".
|
|
2242
|
+
|
|
2243
|
+
// ── read-side: renderer override diagnostics ─────────────────────────────────
|
|
2244
|
+
|
|
2245
|
+
describe('read-side: MeshRenderer override diagnostics', () => {
|
|
2246
|
+
it('ASSET_ERROR_HINTS contains invalid and overflow override hints', () => {
|
|
2247
|
+
expect(ASSET_ERROR_HINTS['mesh-renderer-material-override-invalid']).toBeTruthy();
|
|
2248
|
+
expect(ASSET_ERROR_HINTS['mesh-renderer-material-override-overflow']).toBeTruthy();
|
|
2249
|
+
});
|
|
2250
|
+
|
|
2251
|
+
it('AssetErrorDetail accepts expectedCount / actualCount / meshAssetGuid shape', () => {
|
|
2252
|
+
// Type-level compiler check: this assignment compiles only if
|
|
2253
|
+
// the { expectedCount, actualCount, meshAssetGuid } shape is in
|
|
2254
|
+
// the AssetErrorDetail discriminated union.
|
|
2255
|
+
const detail: AssetErrorDetail = {
|
|
2256
|
+
expectedCount: 3,
|
|
2257
|
+
actualCount: 1,
|
|
2258
|
+
meshAssetGuid: '550e8400-e29b-41d4-a716-446655440000',
|
|
2259
|
+
};
|
|
2260
|
+
expect(detail.expectedCount).toBe(3);
|
|
2261
|
+
expect(detail.actualCount).toBe(1);
|
|
2262
|
+
expect(detail.meshAssetGuid).toBeTruthy();
|
|
2263
|
+
});
|
|
2264
|
+
});
|
|
2265
|
+
}
|