@forgeax/engine-app 0.0.0-dev.8d955ade1c79
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 +424 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/animation-asset-lookup.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-asset-lookup.unit.test.d.ts.map +1 -0
- package/dist/__tests__/callback-deletion.unit.test.d.ts +2 -0
- package/dist/__tests__/callback-deletion.unit.test.d.ts.map +1 -0
- package/dist/__tests__/callback-inference.test-d.d.ts +2 -0
- package/dist/__tests__/callback-inference.test-d.d.ts.map +1 -0
- package/dist/__tests__/component-introspection.unit.test.d.ts +2 -0
- package/dist/__tests__/component-introspection.unit.test.d.ts.map +1 -0
- package/dist/__tests__/consumer-migration.unit.test.d.ts +2 -0
- package/dist/__tests__/consumer-migration.unit.test.d.ts.map +1 -0
- package/dist/__tests__/create-app-error-retention.integration.test.d.ts +2 -0
- package/dist/__tests__/create-app-error-retention.integration.test.d.ts.map +1 -0
- package/dist/__tests__/create-app-features.test.d.ts +2 -0
- package/dist/__tests__/create-app-features.test.d.ts.map +1 -0
- package/dist/__tests__/create-app-listener-sync.test.d.ts +2 -0
- package/dist/__tests__/create-app-listener-sync.test.d.ts.map +1 -0
- package/dist/__tests__/create-app-lock-provider.test.d.ts +2 -0
- package/dist/__tests__/create-app-lock-provider.test.d.ts.map +1 -0
- package/dist/__tests__/create-app-physics-plugin-failure.integration.test.d.ts +2 -0
- package/dist/__tests__/create-app-physics-plugin-failure.integration.test.d.ts.map +1 -0
- package/dist/__tests__/create-app-plugin.test.d.ts +2 -0
- package/dist/__tests__/create-app-plugin.test.d.ts.map +1 -0
- package/dist/__tests__/create-app-stop.test.d.ts +2 -0
- package/dist/__tests__/create-app-stop.test.d.ts.map +1 -0
- package/dist/__tests__/create-app-time.unit.test.d.ts +2 -0
- package/dist/__tests__/create-app-time.unit.test.d.ts.map +1 -0
- package/dist/__tests__/create-app.test-d.d.ts +2 -0
- package/dist/__tests__/create-app.test-d.d.ts.map +1 -0
- package/dist/__tests__/create-app.test.d.ts +2 -0
- package/dist/__tests__/create-app.test.d.ts.map +1 -0
- package/dist/__tests__/create-renderer-error.integration.test.d.ts +2 -0
- package/dist/__tests__/create-renderer-error.integration.test.d.ts.map +1 -0
- package/dist/__tests__/create-renderer-lifecycle.integration.test.d.ts +2 -0
- package/dist/__tests__/create-renderer-lifecycle.integration.test.d.ts.map +1 -0
- package/dist/__tests__/docs-gate-green.unit.test.d.ts +2 -0
- package/dist/__tests__/docs-gate-green.unit.test.d.ts.map +1 -0
- package/dist/__tests__/draw-source-injected-world-update-visibility.test.d.ts +2 -0
- package/dist/__tests__/draw-source-injected-world-update-visibility.test.d.ts.map +1 -0
- package/dist/__tests__/draw-source-single-world-regression.test.d.ts +2 -0
- package/dist/__tests__/draw-source-single-world-regression.test.d.ts.map +1 -0
- package/dist/__tests__/engine-worker-bootstrap-channel.unit.test.d.ts +2 -0
- package/dist/__tests__/engine-worker-bootstrap-channel.unit.test.d.ts.map +1 -0
- package/dist/__tests__/engine-worker-world-swap.unit.test.d.ts +2 -0
- package/dist/__tests__/engine-worker-world-swap.unit.test.d.ts.map +1 -0
- package/dist/__tests__/errors-pointer-lock-failed.test.d.ts +2 -0
- package/dist/__tests__/errors-pointer-lock-failed.test.d.ts.map +1 -0
- package/dist/__tests__/execution-bootstrap-isolation.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-bootstrap-isolation.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-error-detail.test-d.d.ts +2 -0
- package/dist/__tests__/execution-error-detail.test-d.d.ts.map +1 -0
- package/dist/__tests__/execution-error-detail.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-error-detail.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-frame-order.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-frame-order.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-frame-protocol.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-frame-protocol.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-input-sample.contract.test.d.ts +2 -0
- package/dist/__tests__/execution-input-sample.contract.test.d.ts.map +1 -0
- package/dist/__tests__/execution-measurement.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-measurement.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-public-api.test-d.d.ts +2 -0
- package/dist/__tests__/execution-public-api.test-d.d.ts.map +1 -0
- package/dist/__tests__/execution-public-api.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-public-api.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-rebuild.contract.test.d.ts +2 -0
- package/dist/__tests__/execution-rebuild.contract.test.d.ts.map +1 -0
- package/dist/__tests__/execution-report.contract.test.d.ts +2 -0
- package/dist/__tests__/execution-report.contract.test.d.ts.map +1 -0
- package/dist/__tests__/execution-report.types.test-d.d.ts +2 -0
- package/dist/__tests__/execution-report.types.test-d.d.ts.map +1 -0
- package/dist/__tests__/execution-schema-freshness.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-schema-freshness.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-selector.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-selector.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-shared-kernel.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-shared-kernel.unit.test.d.ts.map +1 -0
- package/dist/__tests__/fallback-camera.unit.test.d.ts +2 -0
- package/dist/__tests__/fallback-camera.unit.test.d.ts.map +1 -0
- package/dist/__tests__/frame-loop-time-elapsed.test.d.ts +2 -0
- package/dist/__tests__/frame-loop-time-elapsed.test.d.ts.map +1 -0
- package/dist/__tests__/frame-loop-time.unit.test.d.ts +2 -0
- package/dist/__tests__/frame-loop-time.unit.test.d.ts.map +1 -0
- package/dist/__tests__/frame-loop-world-array.test.d.ts +2 -0
- package/dist/__tests__/frame-loop-world-array.test.d.ts.map +1 -0
- package/dist/__tests__/kernel-worker-runtime.unit.test.d.ts +2 -0
- package/dist/__tests__/kernel-worker-runtime.unit.test.d.ts.map +1 -0
- package/dist/__tests__/migration-surface-inventory.unit.test.d.ts +2 -0
- package/dist/__tests__/migration-surface-inventory.unit.test.d.ts.map +1 -0
- package/dist/__tests__/plugin-policy.integration.test.d.ts +2 -0
- package/dist/__tests__/plugin-policy.integration.test.d.ts.map +1 -0
- package/dist/__tests__/plugin-profiles.unit.test.d.ts +2 -0
- package/dist/__tests__/plugin-profiles.unit.test.d.ts.map +1 -0
- package/dist/__tests__/profiler-default-off.test.d.ts +2 -0
- package/dist/__tests__/profiler-default-off.test.d.ts.map +1 -0
- package/dist/__tests__/profiler-frame-token.test.d.ts +2 -0
- package/dist/__tests__/profiler-frame-token.test.d.ts.map +1 -0
- package/dist/__tests__/profiler-phase-catalog.test.d.ts +2 -0
- package/dist/__tests__/profiler-phase-catalog.test.d.ts.map +1 -0
- package/dist/__tests__/profiler-phase-integration.test.d.ts +2 -0
- package/dist/__tests__/profiler-phase-integration.test.d.ts.map +1 -0
- package/dist/__tests__/profiler-remote-bridge.test.d.ts +2 -0
- package/dist/__tests__/profiler-remote-bridge.test.d.ts.map +1 -0
- package/dist/__tests__/remote-integration.test.d.ts +2 -0
- package/dist/__tests__/remote-integration.test.d.ts.map +1 -0
- package/dist/__tests__/rhi-capture-capability.test-d.d.ts +2 -0
- package/dist/__tests__/rhi-capture-capability.test-d.d.ts.map +1 -0
- package/dist/__tests__/rhi-capture-host.integration.test.d.ts +2 -0
- package/dist/__tests__/rhi-capture-host.integration.test.d.ts.map +1 -0
- package/dist/__tests__/rhi-capture.browser.test.d.ts +2 -0
- package/dist/__tests__/rhi-capture.browser.test.d.ts.map +1 -0
- package/dist/__tests__/surface-handoff.test.d.ts +2 -0
- package/dist/__tests__/surface-handoff.test.d.ts.map +1 -0
- package/dist/__tests__/types-m2.test-d.d.ts +2 -0
- package/dist/__tests__/types-m2.test-d.d.ts.map +1 -0
- package/dist/__tests__/visibility-remote.integration.test.d.ts +2 -0
- package/dist/__tests__/visibility-remote.integration.test.d.ts.map +1 -0
- package/dist/__tests__/worker-execution-stop.test.d.ts +2 -0
- package/dist/__tests__/worker-execution-stop.test.d.ts.map +1 -0
- package/dist/animation-asset-lookup.d.ts +7 -0
- package/dist/animation-asset-lookup.d.ts.map +1 -0
- package/dist/browser-frame-signal.d.ts +17 -0
- package/dist/browser-frame-signal.d.ts.map +1 -0
- package/dist/canvas-policy.d.ts +4 -0
- package/dist/canvas-policy.d.ts.map +1 -0
- package/dist/create-app.d.ts +78 -0
- package/dist/create-app.d.ts.map +1 -0
- package/dist/engine-worker-runtime.mjs +827 -0
- package/dist/engine-worker-runtime.mjs.map +1 -0
- package/dist/errors.d.ts +254 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/execution/attached-world-swap.d.ts +10 -0
- package/dist/execution/attached-world-swap.d.ts.map +1 -0
- package/dist/execution/bootstrap-entry.d.ts +30 -0
- package/dist/execution/bootstrap-entry.d.ts.map +1 -0
- package/dist/execution/bootstrap-url.d.ts +5 -0
- package/dist/execution/bootstrap-url.d.ts.map +1 -0
- package/dist/execution/capabilities.d.ts +5 -0
- package/dist/execution/capabilities.d.ts.map +1 -0
- package/dist/execution/control.d.ts +12 -0
- package/dist/execution/control.d.ts.map +1 -0
- package/dist/execution/engine-worker-runtime.d.ts +2 -0
- package/dist/execution/engine-worker-runtime.d.ts.map +1 -0
- package/dist/execution/engine-worker.d.ts +24 -0
- package/dist/execution/engine-worker.d.ts.map +1 -0
- package/dist/execution/host-controller.d.ts +16 -0
- package/dist/execution/host-controller.d.ts.map +1 -0
- package/dist/execution/index.d.ts +10 -0
- package/dist/execution/index.d.ts.map +1 -0
- package/dist/execution/kernel-pool.d.ts +14 -0
- package/dist/execution/kernel-pool.d.ts.map +1 -0
- package/dist/execution/kernel-worker-runtime.d.ts +2 -0
- package/dist/execution/kernel-worker-runtime.d.ts.map +1 -0
- package/dist/execution/measurement.d.ts +7 -0
- package/dist/execution/measurement.d.ts.map +1 -0
- package/dist/execution/protocol.d.ts +87 -0
- package/dist/execution/protocol.d.ts.map +1 -0
- package/dist/execution/report.d.ts +5 -0
- package/dist/execution/report.d.ts.map +1 -0
- package/dist/execution/schema.d.ts +4 -0
- package/dist/execution/schema.d.ts.map +1 -0
- package/dist/execution/selector.d.ts +10 -0
- package/dist/execution/selector.d.ts.map +1 -0
- package/dist/execution/types.d.ts +123 -0
- package/dist/execution/types.d.ts.map +1 -0
- package/dist/fallback-camera.d.ts +8 -0
- package/dist/fallback-camera.d.ts.map +1 -0
- package/dist/game-context.d.ts +98 -0
- package/dist/game-context.d.ts.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +3980 -0
- package/dist/index.mjs.map +1 -0
- package/dist/input-map-plugin.d.ts +5 -0
- package/dist/input-map-plugin.d.ts.map +1 -0
- package/dist/input-plugin.d.ts +4 -0
- package/dist/input-plugin.d.ts.map +1 -0
- package/dist/internal/assembled-engine-profile.d.ts +5 -0
- package/dist/internal/assembled-engine-profile.d.ts.map +1 -0
- package/dist/internal/assets-world-plugin.d.ts +7 -0
- package/dist/internal/assets-world-plugin.d.ts.map +1 -0
- package/dist/internal/browser-remote-bridge.d.ts +24 -0
- package/dist/internal/browser-remote-bridge.d.ts.map +1 -0
- package/dist/internal/browser-rhi-debug-runtime.d.ts +18 -0
- package/dist/internal/browser-rhi-debug-runtime.d.ts.map +1 -0
- package/dist/internal/component-introspection.d.ts +24 -0
- package/dist/internal/component-introspection.d.ts.map +1 -0
- package/dist/internal/debug-draw.d.ts +9 -0
- package/dist/internal/debug-draw.d.ts.map +1 -0
- package/dist/internal/ecs-import.d.ts +6 -0
- package/dist/internal/ecs-import.d.ts.map +1 -0
- package/dist/internal/engine-profile-common.d.ts +9 -0
- package/dist/internal/engine-profile-common.d.ts.map +1 -0
- package/dist/internal/error-fanout.d.ts +66 -0
- package/dist/internal/error-fanout.d.ts.map +1 -0
- package/dist/internal/frame-loop.d.ts +38 -0
- package/dist/internal/frame-loop.d.ts.map +1 -0
- package/dist/internal/input-attach.d.ts +44 -0
- package/dist/internal/input-attach.d.ts.map +1 -0
- package/dist/internal/main-engine-profile.d.ts +19 -0
- package/dist/internal/main-engine-profile.d.ts.map +1 -0
- package/dist/internal/remote-serve-flag.d.ts +19 -0
- package/dist/internal/remote-serve-flag.d.ts.map +1 -0
- package/dist/internal/remote-server-plugin.d.ts +8 -0
- package/dist/internal/remote-server-plugin.d.ts.map +1 -0
- package/dist/internal/rhi-capture.d.ts +19 -0
- package/dist/internal/rhi-capture.d.ts.map +1 -0
- package/dist/internal/rhi-debug-flag.d.ts +18 -0
- package/dist/internal/rhi-debug-flag.d.ts.map +1 -0
- package/dist/internal/worker-engine-profile.d.ts +15 -0
- package/dist/internal/worker-engine-profile.d.ts.map +1 -0
- package/dist/kernel-worker-runtime.mjs +69 -0
- package/dist/kernel-worker-runtime.mjs.map +1 -0
- package/dist/load-game-errors.d.ts +131 -0
- package/dist/load-game-errors.d.ts.map +1 -0
- package/dist/load-game.d.ts +34 -0
- package/dist/load-game.d.ts.map +1 -0
- package/dist/plugin-factories.d.ts +3 -0
- package/dist/plugin-factories.d.ts.map +1 -0
- package/dist/renderer-plugin.d.ts +25 -0
- package/dist/renderer-plugin.d.ts.map +1 -0
- package/dist/tool-preview/bootstrap.d.ts +113 -0
- package/dist/tool-preview/bootstrap.d.ts.map +1 -0
- package/dist/tool-preview/evidence.d.ts +20 -0
- package/dist/tool-preview/evidence.d.ts.map +1 -0
- package/dist/tool-preview/framing.d.ts +13 -0
- package/dist/tool-preview/framing.d.ts.map +1 -0
- package/dist/tool-preview/recipe.d.ts +54 -0
- package/dist/tool-preview/recipe.d.ts.map +1 -0
- package/dist/types.d.ts +404 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +85 -0
- package/schema/execution-report.schema.json +168 -0
- package/src/__tests__/animation-asset-lookup.unit.test.ts +20 -0
- package/src/__tests__/callback-deletion.unit.test.ts +14 -0
- package/src/__tests__/callback-inference.test-d.ts +36 -0
- package/src/__tests__/component-introspection.unit.test.ts +56 -0
- package/src/__tests__/consumer-migration.unit.test.ts +34 -0
- package/src/__tests__/create-app-error-retention.integration.test.ts +125 -0
- package/src/__tests__/create-app-features.test.ts +51 -0
- package/src/__tests__/create-app-listener-sync.test.ts +381 -0
- package/src/__tests__/create-app-lock-provider.test.ts +212 -0
- package/src/__tests__/create-app-physics-plugin-failure.integration.test.ts +142 -0
- package/src/__tests__/create-app-plugin.test.ts +217 -0
- package/src/__tests__/create-app-stop.test.ts +609 -0
- package/src/__tests__/create-app-time.unit.test.ts +26 -0
- package/src/__tests__/create-app.test-d.ts +83 -0
- package/src/__tests__/create-app.test.ts +287 -0
- package/src/__tests__/create-renderer-error.integration.test.ts +25 -0
- package/src/__tests__/create-renderer-lifecycle.integration.test.ts +24 -0
- package/src/__tests__/docs-gate-green.unit.test.ts +61 -0
- package/src/__tests__/draw-source-injected-world-update-visibility.test.ts +176 -0
- package/src/__tests__/draw-source-single-world-regression.test.ts +184 -0
- package/src/__tests__/engine-worker-bootstrap-channel.unit.test.ts +82 -0
- package/src/__tests__/engine-worker-world-swap.unit.test.ts +69 -0
- package/src/__tests__/errors-pointer-lock-failed.test.ts +265 -0
- package/src/__tests__/execution-bootstrap-isolation.unit.test.ts +79 -0
- package/src/__tests__/execution-error-detail.test-d.ts +13 -0
- package/src/__tests__/execution-error-detail.unit.test.ts +35 -0
- package/src/__tests__/execution-frame-order.unit.test.ts +38 -0
- package/src/__tests__/execution-frame-protocol.unit.test.ts +60 -0
- package/src/__tests__/execution-input-sample.contract.test.ts +23 -0
- package/src/__tests__/execution-measurement.unit.test.ts +20 -0
- package/src/__tests__/execution-public-api.test-d.ts +14 -0
- package/src/__tests__/execution-public-api.unit.test.ts +9 -0
- package/src/__tests__/execution-rebuild.contract.test.ts +36 -0
- package/src/__tests__/execution-report.contract.test.ts +50 -0
- package/src/__tests__/execution-report.types.test-d.ts +16 -0
- package/src/__tests__/execution-schema-freshness.unit.test.ts +20 -0
- package/src/__tests__/execution-selector.unit.test.ts +72 -0
- package/src/__tests__/execution-shared-kernel.unit.test.ts +180 -0
- package/src/__tests__/fallback-camera.unit.test.ts +32 -0
- package/src/__tests__/frame-loop-time-elapsed.test.ts +115 -0
- package/src/__tests__/frame-loop-time.unit.test.ts +61 -0
- package/src/__tests__/frame-loop-world-array.test.ts +450 -0
- package/src/__tests__/kernel-worker-runtime.unit.test.ts +50 -0
- package/src/__tests__/migration-surface-inventory.unit.test.ts +13 -0
- package/src/__tests__/plugin-policy.integration.test.ts +15 -0
- package/src/__tests__/plugin-profiles.unit.test.ts +190 -0
- package/src/__tests__/profiler-default-off.test.ts +53 -0
- package/src/__tests__/profiler-frame-token.test.ts +235 -0
- package/src/__tests__/profiler-phase-catalog.test.ts +30 -0
- package/src/__tests__/profiler-phase-integration.test.ts +117 -0
- package/src/__tests__/profiler-remote-bridge.test.ts +31 -0
- package/src/__tests__/remote-integration.test.ts +224 -0
- package/src/__tests__/rhi-capture-capability.test-d.ts +22 -0
- package/src/__tests__/rhi-capture-host.integration.test.ts +157 -0
- package/src/__tests__/rhi-capture.browser.test.ts +51 -0
- package/src/__tests__/surface-handoff.test.ts +89 -0
- package/src/__tests__/types-m2.test-d.ts +81 -0
- package/src/__tests__/visibility-remote.integration.test.ts +116 -0
- package/src/__tests__/worker-execution-stop.test.ts +173 -0
- package/src/animation-asset-lookup.ts +13 -0
- package/src/browser-frame-signal.ts +44 -0
- package/src/canvas-policy.ts +16 -0
- package/src/create-app.ts +1264 -0
- package/src/errors.ts +442 -0
- package/src/execution/attached-world-swap.ts +33 -0
- package/src/execution/bootstrap-entry.ts +122 -0
- package/src/execution/bootstrap-url.ts +25 -0
- package/src/execution/capabilities.ts +102 -0
- package/src/execution/control.ts +48 -0
- package/src/execution/engine-worker-runtime.ts +383 -0
- package/src/execution/engine-worker.ts +163 -0
- package/src/execution/host-controller.ts +460 -0
- package/src/execution/index.ts +44 -0
- package/src/execution/kernel-pool.ts +236 -0
- package/src/execution/kernel-worker-runtime.ts +103 -0
- package/src/execution/measurement.ts +39 -0
- package/src/execution/protocol.ts +134 -0
- package/src/execution/report.ts +66 -0
- package/src/execution/schema.ts +168 -0
- package/src/execution/selector.ts +98 -0
- package/src/execution/types.ts +163 -0
- package/src/fallback-camera.ts +20 -0
- package/src/game-context.ts +126 -0
- package/src/index.ts +190 -0
- package/src/input-map-plugin.ts +21 -0
- package/src/input-plugin.ts +26 -0
- package/src/internal/assembled-engine-profile.ts +14 -0
- package/src/internal/assets-world-plugin.ts +33 -0
- package/src/internal/browser-remote-bridge.ts +269 -0
- package/src/internal/browser-rhi-debug-runtime.ts +67 -0
- package/src/internal/component-introspection.ts +116 -0
- package/src/internal/debug-draw.ts +38 -0
- package/src/internal/ecs-import.ts +41 -0
- package/src/internal/engine-profile-common.ts +9 -0
- package/src/internal/error-fanout.ts +129 -0
- package/src/internal/frame-loop.ts +539 -0
- package/src/internal/input-attach.ts +104 -0
- package/src/internal/main-engine-profile.ts +84 -0
- package/src/internal/remote-serve-flag.ts +41 -0
- package/src/internal/remote-server-plugin.ts +16 -0
- package/src/internal/rhi-capture.ts +211 -0
- package/src/internal/rhi-debug-flag.ts +40 -0
- package/src/internal/worker-engine-profile.ts +47 -0
- package/src/load-game-errors.ts +211 -0
- package/src/load-game.ts +115 -0
- package/src/plugin-factories.ts +2 -0
- package/src/renderer-plugin.ts +96 -0
- package/src/tool-preview/bootstrap.ts +835 -0
- package/src/tool-preview/evidence.ts +59 -0
- package/src/tool-preview/framing.ts +52 -0
- package/src/tool-preview/recipe.ts +137 -0
- package/src/types.ts +450 -0
- package/src/vite-env.d.ts +10 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { expectTypeOf, it } from 'vitest';
|
|
2
|
+
import type { AppError } from '../index';
|
|
3
|
+
|
|
4
|
+
it('narrows execution error detail by code', () => {
|
|
5
|
+
const error = null as unknown as AppError;
|
|
6
|
+
if (error.code === 'app-execution-kernel-failed') {
|
|
7
|
+
expectTypeOf(error.detail.partialWrite).toEqualTypeOf<true>();
|
|
8
|
+
expectTypeOf(error.detail.retryable).toEqualTypeOf<false>();
|
|
9
|
+
}
|
|
10
|
+
if (error.code === 'app-execution-deadline-exceeded') {
|
|
11
|
+
expectTypeOf(error.detail.phase).toEqualTypeOf<'startup' | 'handshake' | 'frame'>();
|
|
12
|
+
}
|
|
13
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { APP_ERROR_HINTS, APP_EXPECTED, AppError, type AppErrorCode } from '../index';
|
|
3
|
+
|
|
4
|
+
const EXECUTION_CODES = [
|
|
5
|
+
'app-execution-tier-unavailable',
|
|
6
|
+
'app-execution-bootstrap-failed',
|
|
7
|
+
'app-execution-deadline-exceeded',
|
|
8
|
+
'app-execution-kernel-failed',
|
|
9
|
+
'app-execution-stale-world',
|
|
10
|
+
'app-execution-rebuild-failed',
|
|
11
|
+
] as const satisfies readonly AppErrorCode[];
|
|
12
|
+
|
|
13
|
+
describe('execution errors', () => {
|
|
14
|
+
it('has executable expected and hint tables for every execution code', () => {
|
|
15
|
+
for (const code of EXECUTION_CODES) {
|
|
16
|
+
expect(APP_EXPECTED[code].length).toBeGreaterThan(0);
|
|
17
|
+
expect(APP_ERROR_HINTS[code].length).toBeGreaterThan(0);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('exposes unavailable facts without message parsing', () => {
|
|
22
|
+
const error = new AppError({
|
|
23
|
+
code: 'app-execution-tier-unavailable',
|
|
24
|
+
expected: APP_EXPECTED['app-execution-tier-unavailable'],
|
|
25
|
+
hint: APP_ERROR_HINTS['app-execution-tier-unavailable'],
|
|
26
|
+
detail: {
|
|
27
|
+
requestedTier: 'shared',
|
|
28
|
+
missingCapabilities: ['sharedArrayBuffer'],
|
|
29
|
+
sharedEvidencePassed: true,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
expect(error.detail.requestedTier).toBe('shared');
|
|
33
|
+
expect(error.detail.missingCapabilities).toEqual(['sharedArrayBuffer']);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { InputBackendSample } from '@forgeax/engine-input';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { FrameCreditLedger } from '../execution/protocol';
|
|
4
|
+
|
|
5
|
+
describe('execution frame ordering', () => {
|
|
6
|
+
it('samples once and cannot issue the next credit before update, draw and completion', () => {
|
|
7
|
+
const trace: string[] = [];
|
|
8
|
+
let samples = 0;
|
|
9
|
+
const sample = (): InputBackendSample => {
|
|
10
|
+
samples += 1;
|
|
11
|
+
trace.push('sample');
|
|
12
|
+
return {
|
|
13
|
+
downKeys: new Set(),
|
|
14
|
+
upKeys: new Set(),
|
|
15
|
+
buttons: [false, false, false],
|
|
16
|
+
movementX: 0,
|
|
17
|
+
movementY: 0,
|
|
18
|
+
wheelDelta: 0,
|
|
19
|
+
focused: true,
|
|
20
|
+
pointerLocked: false,
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
const ledger = new FrameCreditLedger('world');
|
|
24
|
+
const frame = ledger.issue(1 / 60, sample);
|
|
25
|
+
expect(ledger.issue(1 / 60, sample)).toBeUndefined();
|
|
26
|
+
trace.push('update', 'draw');
|
|
27
|
+
ledger.complete({
|
|
28
|
+
kind: 'frame-complete',
|
|
29
|
+
worldIdentity: 'world',
|
|
30
|
+
frameId: frame?.frameId ?? 0,
|
|
31
|
+
engineUpdateMs: 1,
|
|
32
|
+
kernelWaitMs: 0,
|
|
33
|
+
});
|
|
34
|
+
trace.push('complete');
|
|
35
|
+
expect(trace).toEqual(['sample', 'update', 'draw', 'complete']);
|
|
36
|
+
expect(samples).toBe(1);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { InputBackendSample } from '@forgeax/engine-input';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { FrameCreditLedger } from '../execution/protocol';
|
|
4
|
+
|
|
5
|
+
const sample: InputBackendSample = {
|
|
6
|
+
downKeys: new Set(),
|
|
7
|
+
upKeys: new Set(),
|
|
8
|
+
buttons: [false, false, false],
|
|
9
|
+
movementX: 0,
|
|
10
|
+
movementY: 0,
|
|
11
|
+
wheelDelta: 0,
|
|
12
|
+
focused: true,
|
|
13
|
+
pointerLocked: false,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
describe('frame credit protocol', () => {
|
|
17
|
+
it('allows one in-flight frame and handles duplicate, late and old identities without writes', () => {
|
|
18
|
+
const ledger = new FrameCreditLedger('world-1');
|
|
19
|
+
const first = ledger.issue(0.016, () => sample);
|
|
20
|
+
expect(first?.frameId).toBe(1);
|
|
21
|
+
expect(ledger.issue(0.016, () => sample)).toBeUndefined();
|
|
22
|
+
expect(
|
|
23
|
+
ledger.complete({
|
|
24
|
+
kind: 'frame-complete',
|
|
25
|
+
worldIdentity: 'old',
|
|
26
|
+
frameId: 1,
|
|
27
|
+
engineUpdateMs: 1,
|
|
28
|
+
kernelWaitMs: 0,
|
|
29
|
+
}),
|
|
30
|
+
).toBe('stale-world');
|
|
31
|
+
expect(
|
|
32
|
+
ledger.complete({
|
|
33
|
+
kind: 'frame-complete',
|
|
34
|
+
worldIdentity: 'world-1',
|
|
35
|
+
frameId: 2,
|
|
36
|
+
engineUpdateMs: 1,
|
|
37
|
+
kernelWaitMs: 0,
|
|
38
|
+
}),
|
|
39
|
+
).toBe('late');
|
|
40
|
+
expect(
|
|
41
|
+
ledger.complete({
|
|
42
|
+
kind: 'frame-complete',
|
|
43
|
+
worldIdentity: 'world-1',
|
|
44
|
+
frameId: 1,
|
|
45
|
+
engineUpdateMs: 1,
|
|
46
|
+
kernelWaitMs: 0,
|
|
47
|
+
}),
|
|
48
|
+
).toBe('accepted');
|
|
49
|
+
expect(
|
|
50
|
+
ledger.complete({
|
|
51
|
+
kind: 'frame-complete',
|
|
52
|
+
worldIdentity: 'world-1',
|
|
53
|
+
frameId: 1,
|
|
54
|
+
engineUpdateMs: 1,
|
|
55
|
+
kernelWaitMs: 0,
|
|
56
|
+
}),
|
|
57
|
+
).toBe('duplicate');
|
|
58
|
+
expect(ledger.issue(0.016, () => sample)?.frameId).toBe(2);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type InputBackendSample, snapshotFromSample } from '@forgeax/engine-input';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
describe('Host to Engine input boundary', () => {
|
|
5
|
+
it('reconstructs the existing InputSnapshot from one structured-cloneable sample', () => {
|
|
6
|
+
const sample: InputBackendSample = {
|
|
7
|
+
downKeys: new Set(['w']),
|
|
8
|
+
upKeys: new Set(),
|
|
9
|
+
buttons: [false, false, false],
|
|
10
|
+
movementX: 4,
|
|
11
|
+
movementY: -2,
|
|
12
|
+
wheelDelta: 1,
|
|
13
|
+
focused: true,
|
|
14
|
+
pointerLocked: true,
|
|
15
|
+
};
|
|
16
|
+
const clone = structuredClone(sample);
|
|
17
|
+
const snapshot = snapshotFromSample(clone);
|
|
18
|
+
expect(snapshot.keyboard.down('w')).toBe(true);
|
|
19
|
+
expect(snapshot.mouse.movementDelta).toEqual({ x: 4, y: -2 });
|
|
20
|
+
expect(clone).not.toHaveProperty('window');
|
|
21
|
+
expect(clone).not.toHaveProperty('document');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createMeasurementSeries } from '../execution/measurement';
|
|
3
|
+
|
|
4
|
+
describe('execution measurement series', () => {
|
|
5
|
+
it('reports bounded percentiles while retaining the total sample count', () => {
|
|
6
|
+
const series = createMeasurementSeries(3);
|
|
7
|
+
series.add(100);
|
|
8
|
+
series.add(2);
|
|
9
|
+
series.add(3);
|
|
10
|
+
const report = series.add(4);
|
|
11
|
+
expect(report).toEqual({ samples: 4, p50: 3, p95: 4, p99: 4, jitter: 1 });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('clears samples for a rebuilt World identity', () => {
|
|
15
|
+
const series = createMeasurementSeries();
|
|
16
|
+
series.add(1);
|
|
17
|
+
series.clear();
|
|
18
|
+
expect(series.add(5).samples).toBe(1);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { expectTypeOf, it } from 'vitest';
|
|
2
|
+
import type { App, CreateAppOptions, ExecutionControl, ExecutionReport } from '../index';
|
|
3
|
+
|
|
4
|
+
it('discovers execution from the public App surface', () => {
|
|
5
|
+
const app = null as unknown as App;
|
|
6
|
+
const options = null as unknown as CreateAppOptions;
|
|
7
|
+
expectTypeOf(app.execution).toEqualTypeOf<ExecutionControl>();
|
|
8
|
+
expectTypeOf(app.execution.report()).toEqualTypeOf<ExecutionReport>();
|
|
9
|
+
expectTypeOf(options.execution?.bootstrap).toEqualTypeOf<string | URL | undefined>();
|
|
10
|
+
expectTypeOf(options.execution?.assetCatalog?.url).toEqualTypeOf<string | undefined>();
|
|
11
|
+
expectTypeOf(options.execution?.assetCatalog?.expectedScope).toEqualTypeOf<
|
|
12
|
+
{ readonly scopeId: string; readonly generation: number } | undefined
|
|
13
|
+
>();
|
|
14
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { EXECUTION_REQUESTED_TIERS, EXECUTION_TIERS } from '../index';
|
|
3
|
+
|
|
4
|
+
describe('execution public affordance', () => {
|
|
5
|
+
it('advertises only stable tier names', () => {
|
|
6
|
+
expect(EXECUTION_TIERS).toEqual(['main-serial', 'engine-worker', 'shared']);
|
|
7
|
+
expect(EXECUTION_REQUESTED_TIERS).toEqual(['auto', ...EXECUTION_TIERS]);
|
|
8
|
+
});
|
|
9
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createExecutionReport, unavailableExecutionCapabilities } from '../execution';
|
|
3
|
+
import { createLocalExecutionControl } from '../execution/control';
|
|
4
|
+
|
|
5
|
+
describe('execution rebuild contract', () => {
|
|
6
|
+
it('keeps low-level local assembly explicit when no bootstrap exists', async () => {
|
|
7
|
+
const control = createLocalExecutionControl(
|
|
8
|
+
createExecutionReport('main-serial', unavailableExecutionCapabilities('local')),
|
|
9
|
+
);
|
|
10
|
+
const before = control.report();
|
|
11
|
+
const result = await control.rebuild();
|
|
12
|
+
expect(result.ok).toBe(false);
|
|
13
|
+
if (!result.ok) expect(result.error.code).toBe('app-execution-rebuild-failed');
|
|
14
|
+
expect(control.report()).toEqual(before);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('projects live local lifecycle and World state without a duplicate ledger', () => {
|
|
18
|
+
let world: ReturnType<typeof createExecutionReport>['world'] = {
|
|
19
|
+
identity: 'world-1',
|
|
20
|
+
health: 'healthy',
|
|
21
|
+
partialWrite: false,
|
|
22
|
+
retryable: true,
|
|
23
|
+
};
|
|
24
|
+
const control = createLocalExecutionControl(
|
|
25
|
+
createExecutionReport('main-serial', unavailableExecutionCapabilities('local')),
|
|
26
|
+
{ world: () => world },
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
control.setEngineHealth('running');
|
|
30
|
+
expect(control.report().engine.health).toBe('running');
|
|
31
|
+
world = { ...world, health: 'poisoned', partialWrite: true, retryable: false };
|
|
32
|
+
expect(control.report().world).toEqual(world);
|
|
33
|
+
control.setEngineHealth('stopped');
|
|
34
|
+
expect(control.report().engine.health).toBe('stopped');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
createExecutionReport,
|
|
4
|
+
EXECUTION_CAPABILITY_NAMES,
|
|
5
|
+
type ExecutionCapabilities,
|
|
6
|
+
isExecutionReport,
|
|
7
|
+
selectExecutionTier,
|
|
8
|
+
} from '../index';
|
|
9
|
+
|
|
10
|
+
function capabilities(available = true): ExecutionCapabilities {
|
|
11
|
+
return Object.fromEntries(
|
|
12
|
+
EXECUTION_CAPABILITY_NAMES.map((name) => [
|
|
13
|
+
name,
|
|
14
|
+
{ available, reason: available ? 'observed' : 'missing' },
|
|
15
|
+
]),
|
|
16
|
+
) as unknown as ExecutionCapabilities;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('ExecutionReport contract', () => {
|
|
20
|
+
it('is the closed requested/actual/capability/health/performance/fault snapshot', () => {
|
|
21
|
+
const selection = selectExecutionTier({
|
|
22
|
+
requestedTier: 'auto',
|
|
23
|
+
capabilities: capabilities(),
|
|
24
|
+
sharedEvidencePassed: true,
|
|
25
|
+
}).unwrap();
|
|
26
|
+
const report = createExecutionReport('auto', capabilities(), selection);
|
|
27
|
+
expect(report.requestedTier).toBe('auto');
|
|
28
|
+
expect(report.actualTier).toBe('shared');
|
|
29
|
+
expect(report.capabilities.worker.available).toBe(true);
|
|
30
|
+
expect(report.engine.health).toBe('idle');
|
|
31
|
+
expect(report.world.health).toBe('healthy');
|
|
32
|
+
expect(report.kernelDispatch.reason).toBe('no-eligible-kernel');
|
|
33
|
+
expect(report.performance.kernelWaitMs).toBeNull();
|
|
34
|
+
expect(report.audio).toEqual({
|
|
35
|
+
owner: 'host',
|
|
36
|
+
contextState: 'suspended',
|
|
37
|
+
activeSourceCount: 0,
|
|
38
|
+
lastError: null,
|
|
39
|
+
});
|
|
40
|
+
expect(report.fault).toBeNull();
|
|
41
|
+
expect(isExecutionReport(report)).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('rejects missing and extra schema fields', () => {
|
|
45
|
+
const report = createExecutionReport('main-serial', capabilities(false));
|
|
46
|
+
const { world: _world, ...missing } = report;
|
|
47
|
+
expect(isExecutionReport(missing)).toBe(false);
|
|
48
|
+
expect(isExecutionReport({ ...report, workerId: 1 })).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { expectTypeOf, it } from 'vitest';
|
|
2
|
+
import type { ExecutionReport, ExecutionTier } from '../index';
|
|
3
|
+
|
|
4
|
+
it('exposes stable report fields without transport internals', () => {
|
|
5
|
+
const report = null as unknown as ExecutionReport;
|
|
6
|
+
expectTypeOf(report.actualTier).toEqualTypeOf<ExecutionTier | null>();
|
|
7
|
+
expectTypeOf(report.world.partialWrite).toBeBoolean();
|
|
8
|
+
expectTypeOf(report.world.retryable).toBeBoolean();
|
|
9
|
+
expectTypeOf(report.kernelDispatch.dispatched).toBeNumber();
|
|
10
|
+
expectTypeOf(report.performance.hostFrameMs).toMatchTypeOf<object | null>();
|
|
11
|
+
expectTypeOf(report.audio.owner).toEqualTypeOf<'host'>();
|
|
12
|
+
// @ts-expect-error transport identities are private
|
|
13
|
+
void report.workerId;
|
|
14
|
+
// @ts-expect-error SAB shards are private
|
|
15
|
+
void report.shards;
|
|
16
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import {
|
|
4
|
+
EXECUTION_CAPABILITY_NAMES,
|
|
5
|
+
EXECUTION_REPORT_SCHEMA_VERSION,
|
|
6
|
+
EXECUTION_REQUESTED_TIERS,
|
|
7
|
+
EXECUTION_TIERS,
|
|
8
|
+
} from '../index';
|
|
9
|
+
|
|
10
|
+
describe('execution schema freshness', () => {
|
|
11
|
+
it('keeps runtime catalogs aligned with the JSON schema authority', () => {
|
|
12
|
+
const schema = JSON.parse(
|
|
13
|
+
readFileSync(new URL('../../schema/execution-report.schema.json', import.meta.url), 'utf8'),
|
|
14
|
+
) as { properties: Record<string, { const?: number; enum?: unknown[]; required?: string[] }> };
|
|
15
|
+
expect(schema.properties.schemaVersion?.const).toBe(EXECUTION_REPORT_SCHEMA_VERSION);
|
|
16
|
+
expect(schema.properties.requestedTier?.enum).toEqual(EXECUTION_REQUESTED_TIERS);
|
|
17
|
+
expect(schema.properties.actualTier?.enum).toEqual([null, ...EXECUTION_TIERS]);
|
|
18
|
+
expect(schema.properties.capabilities?.required).toEqual(EXECUTION_CAPABILITY_NAMES);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
EXECUTION_CAPABILITY_NAMES,
|
|
4
|
+
type ExecutionCapabilities,
|
|
5
|
+
type ExecutionCapabilityName,
|
|
6
|
+
selectExecutionTier,
|
|
7
|
+
} from '../index';
|
|
8
|
+
|
|
9
|
+
function facts(missing: readonly ExecutionCapabilityName[] = []): ExecutionCapabilities {
|
|
10
|
+
return Object.fromEntries(
|
|
11
|
+
EXECUTION_CAPABILITY_NAMES.map((name) => [
|
|
12
|
+
name,
|
|
13
|
+
{
|
|
14
|
+
available: !missing.includes(name),
|
|
15
|
+
reason: missing.includes(name) ? 'fixture missing' : 'fixture observed',
|
|
16
|
+
},
|
|
17
|
+
]),
|
|
18
|
+
) as unknown as ExecutionCapabilities;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('execution tier selector', () => {
|
|
22
|
+
it('orders auto shared to engine-worker to main-serial', () => {
|
|
23
|
+
expect(
|
|
24
|
+
selectExecutionTier({
|
|
25
|
+
requestedTier: 'auto',
|
|
26
|
+
capabilities: facts(),
|
|
27
|
+
sharedEvidencePassed: true,
|
|
28
|
+
}).unwrap().actualTier,
|
|
29
|
+
).toBe('shared');
|
|
30
|
+
expect(
|
|
31
|
+
selectExecutionTier({
|
|
32
|
+
requestedTier: 'auto',
|
|
33
|
+
capabilities: facts(['sharedArrayBuffer']),
|
|
34
|
+
sharedEvidencePassed: true,
|
|
35
|
+
}).unwrap().actualTier,
|
|
36
|
+
).toBe('engine-worker');
|
|
37
|
+
expect(
|
|
38
|
+
selectExecutionTier({
|
|
39
|
+
requestedTier: 'auto',
|
|
40
|
+
capabilities: facts(['workerWebGpu']),
|
|
41
|
+
sharedEvidencePassed: true,
|
|
42
|
+
}).unwrap().actualTier,
|
|
43
|
+
).toBe('main-serial');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('does not conflate failed shipped evidence with a missing browser capability', () => {
|
|
47
|
+
const result = selectExecutionTier({
|
|
48
|
+
requestedTier: 'shared',
|
|
49
|
+
capabilities: facts(),
|
|
50
|
+
sharedEvidencePassed: false,
|
|
51
|
+
});
|
|
52
|
+
expect(result.ok).toBe(false);
|
|
53
|
+
if (!result.ok) {
|
|
54
|
+
if (result.error.code !== 'app-execution-tier-unavailable') throw result.error;
|
|
55
|
+
expect(result.error.detail.missingCapabilities).toEqual([]);
|
|
56
|
+
expect(result.error.detail.sharedEvidencePassed).toBe(false);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('never silently downgrades an explicit tier', () => {
|
|
61
|
+
const result = selectExecutionTier({
|
|
62
|
+
requestedTier: 'engine-worker',
|
|
63
|
+
capabilities: facts(['offscreenCanvas']),
|
|
64
|
+
sharedEvidencePassed: true,
|
|
65
|
+
});
|
|
66
|
+
expect(result.ok).toBe(false);
|
|
67
|
+
if (!result.ok) {
|
|
68
|
+
if (result.error.code !== 'app-execution-tier-unavailable') throw result.error;
|
|
69
|
+
expect(result.error.detail.missingCapabilities).toEqual(['offscreenCanvas']);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { defineComponent, World } from '@forgeax/engine-ecs';
|
|
2
|
+
import { defineSharedKernel, type KernelDispatchSpan } from '@forgeax/engine-ecs/shared';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { createKernelPool } from '../execution/kernel-pool';
|
|
5
|
+
|
|
6
|
+
const Position = defineComponent('ExecutionKernelPosition', { x: 'f32' });
|
|
7
|
+
|
|
8
|
+
function runPositionKernel(spans: readonly import('@forgeax/engine-ecs').QuerySpan[]): void {
|
|
9
|
+
for (const span of spans) {
|
|
10
|
+
const position = span.mut(Position);
|
|
11
|
+
for (let index = 0; index < span.length; index += 1) {
|
|
12
|
+
position.x[index] = (position.x[index] ?? 0) + 2;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const kernel = defineSharedKernel('https://example.test/position-kernel.mjs', {
|
|
18
|
+
name: 'execution-position-kernel',
|
|
19
|
+
queries: [{ write: [Position] }],
|
|
20
|
+
minimumRows: 1,
|
|
21
|
+
run: runPositionKernel,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
interface TestMessage {
|
|
25
|
+
readonly kind: 'kernel-init' | 'kernel-preload' | 'kernel-job';
|
|
26
|
+
readonly ready?: Int32Array;
|
|
27
|
+
readonly control?: Int32Array;
|
|
28
|
+
readonly status?: Int32Array;
|
|
29
|
+
readonly jobIndex?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface TestJob {
|
|
33
|
+
readonly kind: 'kernel-job';
|
|
34
|
+
readonly binding: {
|
|
35
|
+
readonly length: number;
|
|
36
|
+
readonly write: Readonly<Record<string, Readonly<Record<string, Float32Array>>>>;
|
|
37
|
+
};
|
|
38
|
+
readonly control: Int32Array;
|
|
39
|
+
readonly status: Int32Array;
|
|
40
|
+
readonly jobIndex: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function sharedDispatchSpans(): {
|
|
44
|
+
readonly world: World;
|
|
45
|
+
readonly spans: readonly KernelDispatchSpan[];
|
|
46
|
+
} {
|
|
47
|
+
const world = new World({ storage: 'shared' });
|
|
48
|
+
for (let index = 0; index < 8; index += 1) {
|
|
49
|
+
world.spawn({ component: Position, data: { x: index } }).unwrap();
|
|
50
|
+
}
|
|
51
|
+
const query = world.query({ write: [Position] }).unwrap();
|
|
52
|
+
return {
|
|
53
|
+
world,
|
|
54
|
+
spans: [...query.spans().unwrap()].map((span) => ({ queryIndex: 0, span })),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function workerFactory(run: (job: TestJob) => void, preloadStatus = 1): () => Worker {
|
|
59
|
+
return () =>
|
|
60
|
+
({
|
|
61
|
+
postMessage(message: TestMessage): void {
|
|
62
|
+
if (message.kind === 'kernel-init' && message.ready !== undefined) {
|
|
63
|
+
Atomics.add(message.ready, 0, 1);
|
|
64
|
+
Atomics.notify(message.ready, 0);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (
|
|
68
|
+
message.kind === 'kernel-preload' &&
|
|
69
|
+
message.control !== undefined &&
|
|
70
|
+
message.status !== undefined &&
|
|
71
|
+
message.jobIndex !== undefined
|
|
72
|
+
) {
|
|
73
|
+
Atomics.store(message.status, message.jobIndex, preloadStatus);
|
|
74
|
+
Atomics.add(message.control, 0, 1);
|
|
75
|
+
Atomics.notify(message.control, 0);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
run(message as TestJob);
|
|
79
|
+
Atomics.add(message.control as Int32Array, 0, 1);
|
|
80
|
+
Atomics.notify(message.control as Int32Array, 0);
|
|
81
|
+
},
|
|
82
|
+
terminate(): void {},
|
|
83
|
+
}) as unknown as Worker;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
describe('shared kernel pool', () => {
|
|
87
|
+
it('shards SAB spans without copying and joins all lanes', () => {
|
|
88
|
+
const { world, spans } = sharedDispatchSpans();
|
|
89
|
+
const pool = createKernelPool({
|
|
90
|
+
lanes: 2,
|
|
91
|
+
workerFactory: workerFactory((job) => {
|
|
92
|
+
const x = job.binding.write.ExecutionKernelPosition?.x;
|
|
93
|
+
if (x === undefined) throw new Error('position binding missing');
|
|
94
|
+
for (let index = 0; index < job.binding.length; index += 1) {
|
|
95
|
+
x[index] = (x[index] ?? 0) + 2;
|
|
96
|
+
}
|
|
97
|
+
Atomics.store(job.status, job.jobIndex, 1);
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
pool.warmup?.(kernel);
|
|
101
|
+
|
|
102
|
+
const result = pool.execute(kernel, spans);
|
|
103
|
+
expect(result).toMatchObject({ mode: 'shared', dispatched: 2, completed: 2 });
|
|
104
|
+
expect(pool.takeLastDispatch()).toEqual(result);
|
|
105
|
+
expect(pool.takeLastDispatch()).toBeNull();
|
|
106
|
+
expect(
|
|
107
|
+
Array.from(world.query({ read: [Position] }).unwrap(), (row) => row.get(Position).x),
|
|
108
|
+
).toEqual([2, 3, 4, 5, 6, 7, 8, 9]);
|
|
109
|
+
pool.dispose();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('reports a partial write when one lane fails after another completes', () => {
|
|
113
|
+
const { spans } = sharedDispatchSpans();
|
|
114
|
+
const pool = createKernelPool({
|
|
115
|
+
lanes: 2,
|
|
116
|
+
workerFactory: workerFactory((job) => {
|
|
117
|
+
Atomics.store(job.status, job.jobIndex, job.jobIndex === 0 ? 1 : -1);
|
|
118
|
+
}),
|
|
119
|
+
});
|
|
120
|
+
pool.warmup?.(kernel);
|
|
121
|
+
|
|
122
|
+
expect(pool.execute(kernel, spans)).toMatchObject({
|
|
123
|
+
dispatched: 2,
|
|
124
|
+
completed: 1,
|
|
125
|
+
partialWrite: true,
|
|
126
|
+
});
|
|
127
|
+
pool.dispose();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('fails a non-responsive lane at the bounded Atomics deadline', () => {
|
|
131
|
+
const { spans } = sharedDispatchSpans();
|
|
132
|
+
const pool = createKernelPool({
|
|
133
|
+
lanes: 1,
|
|
134
|
+
timeoutMs: 1,
|
|
135
|
+
workerFactory: () =>
|
|
136
|
+
({
|
|
137
|
+
postMessage(message: TestMessage): void {
|
|
138
|
+
if (message.kind === 'kernel-init' && message.ready !== undefined) {
|
|
139
|
+
Atomics.add(message.ready, 0, 1);
|
|
140
|
+
Atomics.notify(message.ready, 0);
|
|
141
|
+
}
|
|
142
|
+
if (
|
|
143
|
+
message.kind === 'kernel-preload' &&
|
|
144
|
+
message.control !== undefined &&
|
|
145
|
+
message.status !== undefined &&
|
|
146
|
+
message.jobIndex !== undefined
|
|
147
|
+
) {
|
|
148
|
+
Atomics.store(message.status, message.jobIndex, 1);
|
|
149
|
+
Atomics.add(message.control, 0, 1);
|
|
150
|
+
Atomics.notify(message.control, 0);
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
terminate(): void {},
|
|
154
|
+
}) as unknown as Worker,
|
|
155
|
+
});
|
|
156
|
+
pool.warmup?.(kernel);
|
|
157
|
+
|
|
158
|
+
expect(pool.execute(kernel, spans)).toMatchObject({
|
|
159
|
+
dispatched: 1,
|
|
160
|
+
completed: 0,
|
|
161
|
+
partialWrite: true,
|
|
162
|
+
});
|
|
163
|
+
pool.dispose();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('rejects an invalid module during readiness before any kernel dispatch', async () => {
|
|
167
|
+
let dispatched = false;
|
|
168
|
+
const pool = createKernelPool({
|
|
169
|
+
lanes: 1,
|
|
170
|
+
workerFactory: workerFactory(() => {
|
|
171
|
+
dispatched = true;
|
|
172
|
+
}, -1),
|
|
173
|
+
});
|
|
174
|
+
pool.warmup?.(kernel);
|
|
175
|
+
|
|
176
|
+
await expect(pool.ready()).rejects.toThrow('module preflight failed');
|
|
177
|
+
expect(dispatched).toBe(false);
|
|
178
|
+
pool.dispose();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { World } from '@forgeax/engine-ecs';
|
|
2
|
+
import { Camera } from '@forgeax/engine-render';
|
|
3
|
+
import { Transform } from '@forgeax/engine-scene';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { ensureFallbackCamera } from '../fallback-camera';
|
|
6
|
+
|
|
7
|
+
describe('ensureFallbackCamera', () => {
|
|
8
|
+
it('installs one neutral host fallback with the requested aspect', () => {
|
|
9
|
+
const world = new World();
|
|
10
|
+
const spawned = ensureFallbackCamera(world, 16 / 9);
|
|
11
|
+
expect(spawned).toBeDefined();
|
|
12
|
+
if (spawned === undefined) return;
|
|
13
|
+
expect(spawned.ok).toBe(true);
|
|
14
|
+
if (!spawned.ok) return;
|
|
15
|
+
|
|
16
|
+
expect(world.get(spawned.value, Transform).unwrap().pos).toEqual(new Float32Array([0, 0.6, 5]));
|
|
17
|
+
expect(world.get(spawned.value, Camera).unwrap().aspect).toBeCloseTo(16 / 9);
|
|
18
|
+
expect([...world.query({ with: [Camera] }).unwrap()]).toHaveLength(1);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('preserves an authored scene camera', () => {
|
|
22
|
+
const world = new World();
|
|
23
|
+
const authored = world.spawn(
|
|
24
|
+
{ component: Transform, data: { pos: [1, 2, 3] } },
|
|
25
|
+
{ component: Camera, data: {} },
|
|
26
|
+
);
|
|
27
|
+
expect(authored.ok).toBe(true);
|
|
28
|
+
|
|
29
|
+
expect(ensureFallbackCamera(world, 16 / 9)).toBeUndefined();
|
|
30
|
+
expect([...world.query({ with: [Camera] }).unwrap()]).toHaveLength(1);
|
|
31
|
+
});
|
|
32
|
+
});
|