@forgeax/engine-app 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 +418 -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__/app-rebuild.test.d.ts +2 -0
- package/dist/__tests__/app-rebuild.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-types.test-d.d.ts +2 -0
- package/dist/__tests__/asset-registry-types.test-d.d.ts.map +1 -0
- package/dist/__tests__/assets-runtime-assembly.integration.test.d.ts +2 -0
- package/dist/__tests__/assets-runtime-assembly.integration.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-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-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-headless-full-chain.test.d.ts +2 -0
- package/dist/__tests__/draw-source-headless-full-chain.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__/ecs-import.unit.test.d.ts +2 -0
- package/dist/__tests__/ecs-import.unit.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__/execution-world-health-owner.test-d.d.ts +2 -0
- package/dist/__tests__/execution-world-health-owner.test-d.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__/remote-serve-flag.test.d.ts +2 -0
- package/dist/__tests__/remote-serve-flag.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 +8 -0
- package/dist/animation-asset-lookup.d.ts.map +1 -0
- package/dist/assets-runtime-assembly.d.ts +19 -0
- package/dist/assets-runtime-assembly.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 +57 -0
- package/dist/create-app.d.ts.map +1 -0
- package/dist/engine-worker-runtime.mjs +884 -0
- package/dist/engine-worker-runtime.mjs.map +1 -0
- package/dist/errors.d.ts +263 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/execution/attached-world-swap.d.ts +21 -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/capabilities.d.ts +5 -0
- package/dist/execution/capabilities.d.ts.map +1 -0
- package/dist/execution/control.d.ts +14 -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 +23 -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 +84 -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 +107 -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 +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +3985 -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 +21 -0
- package/dist/internal/assets-world-plugin.d.ts.map +1 -0
- package/dist/internal/browser-remote-bridge.d.ts +25 -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/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 +35 -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 +15 -0
- package/dist/internal/main-engine-profile.d.ts.map +1 -0
- package/dist/internal/remote-serve-flag.d.ts +23 -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 +13 -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/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 +444 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +86 -0
- package/schema/execution-report.schema.json +168 -0
- package/src/__tests__/animation-asset-lookup.unit.test.ts +20 -0
- package/src/__tests__/app-rebuild.test.ts +31 -0
- package/src/__tests__/asset-registry-types.test-d.ts +16 -0
- package/src/__tests__/assets-runtime-assembly.integration.test.ts +68 -0
- package/src/__tests__/callback-deletion.unit.test.ts +14 -0
- package/src/__tests__/callback-inference.test-d.ts +32 -0
- package/src/__tests__/component-introspection.unit.test.ts +56 -0
- package/src/__tests__/consumer-migration.unit.test.ts +21 -0
- package/src/__tests__/create-app-features.test.ts +40 -0
- package/src/__tests__/create-app-listener-sync.test.ts +377 -0
- package/src/__tests__/create-app-lock-provider.test.ts +212 -0
- package/src/__tests__/create-app-plugin.test.ts +217 -0
- package/src/__tests__/create-app-stop.test.ts +617 -0
- package/src/__tests__/create-app-time.unit.test.ts +26 -0
- package/src/__tests__/create-app.test-d.ts +82 -0
- package/src/__tests__/create-app.test.ts +346 -0
- package/src/__tests__/create-renderer-error.integration.test.ts +24 -0
- package/src/__tests__/create-renderer-lifecycle.integration.test.ts +19 -0
- package/src/__tests__/docs-gate-green.unit.test.ts +61 -0
- package/src/__tests__/draw-source-headless-full-chain.test.ts +226 -0
- package/src/__tests__/draw-source-injected-world-update-visibility.test.ts +182 -0
- package/src/__tests__/draw-source-single-world-regression.test.ts +185 -0
- package/src/__tests__/ecs-import.unit.test.ts +40 -0
- package/src/__tests__/engine-worker-bootstrap-channel.unit.test.ts +74 -0
- package/src/__tests__/engine-worker-world-swap.unit.test.ts +76 -0
- package/src/__tests__/errors-pointer-lock-failed.test.ts +268 -0
- package/src/__tests__/execution-bootstrap-isolation.unit.test.ts +80 -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 +61 -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 +10 -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__/execution-world-health-owner.test-d.ts +9 -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 +441 -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 +248 -0
- package/src/__tests__/profiler-phase-catalog.test.ts +30 -0
- package/src/__tests__/profiler-phase-integration.test.ts +129 -0
- package/src/__tests__/profiler-remote-bridge.test.ts +31 -0
- package/src/__tests__/remote-integration.test.ts +224 -0
- package/src/__tests__/remote-serve-flag.test.ts +24 -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 +92 -0
- package/src/__tests__/types-m2.test-d.ts +81 -0
- package/src/__tests__/visibility-remote.integration.test.ts +119 -0
- package/src/__tests__/worker-execution-stop.test.ts +116 -0
- package/src/animation-asset-lookup.ts +13 -0
- package/src/assets-runtime-assembly.ts +70 -0
- package/src/canvas-policy.ts +16 -0
- package/src/create-app.ts +1351 -0
- package/src/errors.ts +454 -0
- package/src/execution/attached-world-swap.ts +67 -0
- package/src/execution/bootstrap-entry.ts +122 -0
- package/src/execution/capabilities.ts +102 -0
- package/src/execution/control.ts +52 -0
- package/src/execution/engine-worker-runtime.ts +400 -0
- package/src/execution/engine-worker.ts +161 -0
- package/src/execution/host-controller.ts +454 -0
- package/src/execution/index.ts +43 -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 +131 -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 +146 -0
- package/src/fallback-camera.ts +20 -0
- package/src/game-context.ts +126 -0
- package/src/index.ts +176 -0
- package/src/input-map-plugin.ts +21 -0
- package/src/input-plugin.ts +26 -0
- package/src/internal/assembled-engine-profile.ts +22 -0
- package/src/internal/assets-world-plugin.ts +79 -0
- package/src/internal/browser-remote-bridge.ts +267 -0
- package/src/internal/browser-rhi-debug-runtime.ts +67 -0
- package/src/internal/component-introspection.ts +116 -0
- package/src/internal/ecs-import.ts +41 -0
- package/src/internal/engine-profile-common.ts +10 -0
- package/src/internal/error-fanout.ts +129 -0
- package/src/internal/frame-loop.ts +525 -0
- package/src/internal/input-attach.ts +104 -0
- package/src/internal/main-engine-profile.ts +80 -0
- package/src/internal/remote-serve-flag.ts +50 -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/tool-preview/bootstrap.ts +848 -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 +505 -0
- package/src/vite-env.d.ts +12 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// feat-20260709-editor-world-partition-editorworld-super-composite / M2 / w10
|
|
2
|
+
// (RED — impl lands in w12). Full-chain consumer test on the rhi-null headless
|
|
3
|
+
// backend: the drawSource seam drives a REAL Renderer (createRenderer +
|
|
4
|
+
// RhiNull) end-to-end — update -> extract -> record — with no GPU.
|
|
5
|
+
//
|
|
6
|
+
// Unlike w8 (spy renderer, call-shape) and w9 (spy renderer, stale-matrix), this
|
|
7
|
+
// test exercises the actual runtime render pipeline through the seam. The camera
|
|
8
|
+
// + light + visible mesh live ONLY in the injected world; the frame-loop's own
|
|
9
|
+
// world is empty. So a non-zero draw count proves three things at once:
|
|
10
|
+
// 1. drawSource is consumed (else the empty loop world is drawn -> 0 draws),
|
|
11
|
+
// 2. the injected world was world.update(1 / 60).unwrap()'d (propagate ran; extract finds the
|
|
12
|
+
// camera + resolved Transform.world),
|
|
13
|
+
// 3. the full URP record path fired headlessly (bookkeeper counts draws).
|
|
14
|
+
//
|
|
15
|
+
// rhi-null needs no GPU: navigator.gpu is stubbed undefined so createRenderer
|
|
16
|
+
// selects the injected RhiNull instance, and the RhiNullDevice bookkeeper counts
|
|
17
|
+
// draw / bind-group / pipeline records. File suffix `.test.ts` runs in the node
|
|
18
|
+
// vitest project (no dawn / no browser).
|
|
19
|
+
//
|
|
20
|
+
// The frame-loop is driven directly (createFrameLoop + injected now/raf/caf)
|
|
21
|
+
// rather than through createApp, because createApp's assemble form does not (yet
|
|
22
|
+
// — that is w11) forward drawSource; w12 threads it into the loop. Driving the
|
|
23
|
+
// loop directly keeps this test focused on the seam's runtime contract. The
|
|
24
|
+
// drawSource option is reached through a typed alias before w12 widens the type.
|
|
25
|
+
//
|
|
26
|
+
// Anchors:
|
|
27
|
+
// plan-strategy §2 D-3 (drawSource pull -> update injected worlds -> draw)
|
|
28
|
+
// research F1 (frame-loop is the update -> draw driver; the seam insertion pt)
|
|
29
|
+
// feat-20260623 rhi-null-command-flow.unit.test.ts (headless full-chain
|
|
30
|
+
// precedent: createRenderer + RhiNull + bookkeeper counters)
|
|
31
|
+
|
|
32
|
+
import { World } from '@forgeax/engine-ecs';
|
|
33
|
+
import { createPrimitiveMesh } from '@forgeax/engine-geometry';
|
|
34
|
+
import {
|
|
35
|
+
Camera,
|
|
36
|
+
DirectionalLight,
|
|
37
|
+
MeshFilter,
|
|
38
|
+
MeshRenderer,
|
|
39
|
+
type Renderer,
|
|
40
|
+
} from '@forgeax/engine-render';
|
|
41
|
+
import type { RhiNullDevice } from '@forgeax/engine-rhi-null';
|
|
42
|
+
import { rhi } from '@forgeax/engine-rhi-null';
|
|
43
|
+
import { createRenderer } from '@forgeax/engine-runtime';
|
|
44
|
+
import { registerPropagateTransforms, Transform } from '@forgeax/engine-scene';
|
|
45
|
+
|
|
46
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
47
|
+
import { createFrameLoop, type FrameLoopOptions } from '../internal/frame-loop';
|
|
48
|
+
|
|
49
|
+
function makeStubCanvas(w = 800, h = 600): HTMLCanvasElement {
|
|
50
|
+
return {
|
|
51
|
+
width: w,
|
|
52
|
+
height: h,
|
|
53
|
+
getContext(_kind: string): unknown {
|
|
54
|
+
return null;
|
|
55
|
+
},
|
|
56
|
+
addEventListener: () => undefined,
|
|
57
|
+
removeEventListener: () => undefined,
|
|
58
|
+
} as unknown as HTMLCanvasElement;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function buildManifestDataUrl(): string {
|
|
62
|
+
const manifest = {
|
|
63
|
+
schemaVersion: '1.0.0',
|
|
64
|
+
entries: [
|
|
65
|
+
{ hash: 'pbr00000', wgsl: '/* pbr stub - calls f_schlick( */', glsl: '', bindings: '' },
|
|
66
|
+
{ hash: 'unlit000', wgsl: '/* unlit stub */', glsl: '', bindings: '' },
|
|
67
|
+
{
|
|
68
|
+
hash: 'tonemap0',
|
|
69
|
+
wgsl: '/* tonemap stub - struct TonemapParams { exposure: f32 }; */',
|
|
70
|
+
glsl: '',
|
|
71
|
+
bindings: '',
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
return `data:application/json,${encodeURIComponent(JSON.stringify(manifest))}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function identityTransform(
|
|
79
|
+
overrides: Partial<Record<string, number[]>> = {},
|
|
80
|
+
): Record<string, number[]> {
|
|
81
|
+
return {
|
|
82
|
+
pos: [0, 0, 0],
|
|
83
|
+
quat: [0, 0, 0, 1],
|
|
84
|
+
scale: [1, 1, 1],
|
|
85
|
+
...overrides,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// The injected world carries the FULL renderable set (camera + light + visible
|
|
90
|
+
// mesh). It also registers propagateTransforms so the frame-loop's world.update
|
|
91
|
+
// resolves Transform.world before extract reads it.
|
|
92
|
+
function makeInjectedWorld(): World {
|
|
93
|
+
const world = new World();
|
|
94
|
+
const cube = createPrimitiveMesh('cube').unwrap();
|
|
95
|
+
const cubeHandle = world.internSharedRef('MeshAsset', cube);
|
|
96
|
+
registerPropagateTransforms(world);
|
|
97
|
+
world.spawn(
|
|
98
|
+
{ component: Transform, data: identityTransform({ pos: [0, 0, 5] }) },
|
|
99
|
+
{ component: Camera, data: { fov: 60, near: 0.1, far: 1000, tonemap: 4 } },
|
|
100
|
+
);
|
|
101
|
+
world.spawn(
|
|
102
|
+
{ component: Transform, data: identityTransform() },
|
|
103
|
+
{
|
|
104
|
+
component: DirectionalLight,
|
|
105
|
+
data: { direction: [0.5, -1, -0.5], castShadow: true, intensity: 1 },
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
world.spawn(
|
|
109
|
+
{ component: Transform, data: identityTransform() },
|
|
110
|
+
{ component: MeshFilter, data: { assetHandle: cubeHandle } },
|
|
111
|
+
{ component: MeshRenderer, data: {} },
|
|
112
|
+
);
|
|
113
|
+
return world;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function makeSyncScheduler() {
|
|
117
|
+
let pending: ((t: number) => void) | null = null;
|
|
118
|
+
let clock = 0;
|
|
119
|
+
const raf = (cb: (t: number) => void): number => {
|
|
120
|
+
pending = cb;
|
|
121
|
+
return 1;
|
|
122
|
+
};
|
|
123
|
+
const caf = (): void => {
|
|
124
|
+
pending = null;
|
|
125
|
+
};
|
|
126
|
+
const now = (): number => {
|
|
127
|
+
clock += 16;
|
|
128
|
+
return clock;
|
|
129
|
+
};
|
|
130
|
+
const pump = (frames: number): void => {
|
|
131
|
+
for (let i = 0; i < frames; i++) {
|
|
132
|
+
const cb = pending;
|
|
133
|
+
pending = null;
|
|
134
|
+
if (cb === null) break;
|
|
135
|
+
cb(clock);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
return { raf, caf, now, pump };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
type DrawSourceCallback = () =>
|
|
142
|
+
| { worlds: readonly World[]; cameraOwner: number; resourceOwner: number }
|
|
143
|
+
| undefined;
|
|
144
|
+
|
|
145
|
+
type FrameLoopOptionsWithDrawSource = FrameLoopOptions & {
|
|
146
|
+
drawSource?: DrawSourceCallback;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const createFrameLoopWithDrawSource = createFrameLoop as unknown as (
|
|
150
|
+
opts: FrameLoopOptionsWithDrawSource,
|
|
151
|
+
) => ReturnType<typeof createFrameLoop>;
|
|
152
|
+
|
|
153
|
+
// The RhiNull instance is a structural stand-in for a real RhiInstance in this
|
|
154
|
+
// headless test; cast the options bag to RendererOptions so the direct
|
|
155
|
+
// createRenderer import (strict RendererOptions) accepts the injected backend.
|
|
156
|
+
const rhiOptions = {
|
|
157
|
+
rhi: rhi as unknown as import('@forgeax/engine-render').RendererOptions['rhi'],
|
|
158
|
+
} as import('@forgeax/engine-render').RendererOptions;
|
|
159
|
+
|
|
160
|
+
let renderer: Renderer | null = null;
|
|
161
|
+
|
|
162
|
+
beforeEach(() => {
|
|
163
|
+
vi.stubGlobal('navigator', { gpu: undefined });
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
afterEach(() => {
|
|
167
|
+
vi.unstubAllGlobals();
|
|
168
|
+
if (renderer) {
|
|
169
|
+
try {
|
|
170
|
+
renderer.dispose();
|
|
171
|
+
} catch {
|
|
172
|
+
// ignore
|
|
173
|
+
}
|
|
174
|
+
renderer = null;
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('drawSource full chain on rhi-null headless (w10)', () => {
|
|
179
|
+
it('drives update -> extract -> record for an injected world with no GPU', async () => {
|
|
180
|
+
const canvas = makeStubCanvas();
|
|
181
|
+
renderer = await createRenderer(canvas, rhiOptions, {
|
|
182
|
+
shaderManifestUrl: buildManifestDataUrl(),
|
|
183
|
+
});
|
|
184
|
+
await renderer.ready;
|
|
185
|
+
|
|
186
|
+
const rhiNullDevice = renderer.device as unknown as RhiNullDevice;
|
|
187
|
+
|
|
188
|
+
// The frame-loop's own world is EMPTY — it has no camera / mesh, so it
|
|
189
|
+
// records zero draws on its own. Only the injected world is renderable.
|
|
190
|
+
const loopWorld = new World();
|
|
191
|
+
const injectedWorld = makeInjectedWorld();
|
|
192
|
+
|
|
193
|
+
const drawSource: DrawSourceCallback = () => ({
|
|
194
|
+
worlds: [injectedWorld],
|
|
195
|
+
cameraOwner: 0,
|
|
196
|
+
resourceOwner: 0,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const { raf, caf, now, pump } = makeSyncScheduler();
|
|
200
|
+
const loop = createFrameLoopWithDrawSource({
|
|
201
|
+
world: loopWorld,
|
|
202
|
+
renderer: renderer as Renderer,
|
|
203
|
+
now,
|
|
204
|
+
raf,
|
|
205
|
+
caf,
|
|
206
|
+
drawSource,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Reset per-frame counters immediately before the observed frame.
|
|
210
|
+
rhiNullDevice.totalDrawCount = 0;
|
|
211
|
+
rhiNullDevice.totalBindGroupCount = 0;
|
|
212
|
+
rhiNullDevice.framePassNames = [];
|
|
213
|
+
|
|
214
|
+
expect(loop.start().ok).toBe(true);
|
|
215
|
+
pump(1);
|
|
216
|
+
loop.stop();
|
|
217
|
+
|
|
218
|
+
// The full URP record path fired for the INJECTED world: a non-zero draw
|
|
219
|
+
// count is only reachable if drawSource was consumed AND the injected world
|
|
220
|
+
// was updated (camera + resolved Transform.world present for extract).
|
|
221
|
+
expect(rhiNullDevice.totalDrawCount).toBeGreaterThanOrEqual(1);
|
|
222
|
+
expect(rhiNullDevice.totalBindGroupCount).toBeGreaterThanOrEqual(1);
|
|
223
|
+
expect(renderer.perFramePassNames.length).toBeGreaterThan(0);
|
|
224
|
+
expect(renderer.perFramePassNames).toContain('main');
|
|
225
|
+
});
|
|
226
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// feat-20260709-editor-world-partition-editorworld-super-composite / M2 / w9
|
|
2
|
+
// (RED — impl lands in w12). Contract test: a world injected through drawSource
|
|
3
|
+
// MUST be world.update(1 / 60).unwrap()'d by the frame-loop in the SAME frame it is drawn, so
|
|
4
|
+
// the renderer reads a freshly-propagated Transform.world mat4 — never a stale
|
|
5
|
+
// one.
|
|
6
|
+
//
|
|
7
|
+
// Why this matters (Strategist D-3, not covered by research): the renderer's
|
|
8
|
+
// extract stage reads the DERIVED `Transform.world` column, whose sole writer is
|
|
9
|
+
// the propagateTransforms system (plugin-factories.ts / systems/propagate-
|
|
10
|
+
// transforms.ts). That writer only runs inside `world.update(1 / 60).unwrap()`. If drawSource
|
|
11
|
+
// merely fed injected worlds into `renderer.draw(worlds, ...)` WITHOUT the
|
|
12
|
+
// frame-loop running `world.update(1 / 60).unwrap()` on them first, the renderer would read the
|
|
13
|
+
// injected world's Transform.world at its previous (or default identity) value —
|
|
14
|
+
// a stale-matrix bug that renders the injected world one frame behind (or at the
|
|
15
|
+
// origin on the first frame).
|
|
16
|
+
//
|
|
17
|
+
// This test spawns an entity in the injected world with a DISTINGUISHING
|
|
18
|
+
// non-identity translation and never calls world.update(1 / 60).unwrap() on that world itself.
|
|
19
|
+
// The spy renderer reads `Transform.world` at draw time. A frame-loop that
|
|
20
|
+
// updates injected worlds surfaces the translation in the mat4's column-major
|
|
21
|
+
// [12]/[13]/[14] slots; a draw-only frame-loop surfaces the identity default
|
|
22
|
+
// (0/0/0), which fails the assertion. So the test has genuine discriminating
|
|
23
|
+
// power over the stale-matrix regression.
|
|
24
|
+
//
|
|
25
|
+
// Driven through createFrameLoop directly with injected now / raf / caf seams,
|
|
26
|
+
// mirroring frame-loop-world-array.test.ts. The drawSource option is reached
|
|
27
|
+
// through a typed alias before w12 widens the real FrameLoopOptions type.
|
|
28
|
+
//
|
|
29
|
+
// Anchors:
|
|
30
|
+
// plan-strategy §2 D-3 (frame-loop MUST update injected worlds — transform
|
|
31
|
+
// propagation — before draw; not draw-only)
|
|
32
|
+
// research F1 (frame-loop is the world.update -> renderer.draw driver)
|
|
33
|
+
// AGENTS.md invariant 6 (Transform.world is the round-trip fidelity seam)
|
|
34
|
+
|
|
35
|
+
import type { EntityHandle } from '@forgeax/engine-ecs';
|
|
36
|
+
import { World } from '@forgeax/engine-ecs';
|
|
37
|
+
import type { Renderer } from '@forgeax/engine-render';
|
|
38
|
+
import { registerPropagateTransforms, Transform } from '@forgeax/engine-scene';
|
|
39
|
+
import { describe, expect, it } from 'vitest';
|
|
40
|
+
import { createFrameLoop, type FrameLoopOptions } from '../internal/frame-loop';
|
|
41
|
+
|
|
42
|
+
// Distinguishing translation for the injected world's entity. Column-major mat4
|
|
43
|
+
// carries translation at indices [12] (x), [13] (y), [14] (z).
|
|
44
|
+
const INJECTED_TX = 5;
|
|
45
|
+
const INJECTED_TY = -3;
|
|
46
|
+
const INJECTED_TZ = 7;
|
|
47
|
+
|
|
48
|
+
function makeSyncScheduler() {
|
|
49
|
+
let pending: ((t: number) => void) | null = null;
|
|
50
|
+
let clock = 0;
|
|
51
|
+
const raf = (cb: (t: number) => void): number => {
|
|
52
|
+
pending = cb;
|
|
53
|
+
return 1;
|
|
54
|
+
};
|
|
55
|
+
const caf = (): void => {
|
|
56
|
+
pending = null;
|
|
57
|
+
};
|
|
58
|
+
const now = (): number => {
|
|
59
|
+
clock += 16;
|
|
60
|
+
return clock;
|
|
61
|
+
};
|
|
62
|
+
const pump = (frames: number): void => {
|
|
63
|
+
for (let i = 0; i < frames; i++) {
|
|
64
|
+
const cb = pending;
|
|
65
|
+
pending = null;
|
|
66
|
+
if (cb === null) break;
|
|
67
|
+
cb(clock);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
return { raf, caf, now, pump };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type DrawSourceCallback = () =>
|
|
74
|
+
| { worlds: readonly World[]; cameraOwner: number; resourceOwner: number }
|
|
75
|
+
| undefined;
|
|
76
|
+
|
|
77
|
+
type FrameLoopOptionsWithDrawSource = FrameLoopOptions & {
|
|
78
|
+
drawSource?: DrawSourceCallback;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const createFrameLoopWithDrawSource = createFrameLoop as unknown as (
|
|
82
|
+
opts: FrameLoopOptionsWithDrawSource,
|
|
83
|
+
) => ReturnType<typeof createFrameLoop>;
|
|
84
|
+
|
|
85
|
+
describe('drawSource injected worlds are updated same-frame (w9, stale-matrix regression)', () => {
|
|
86
|
+
it('renderer reads a freshly-propagated Transform.world for the injected world at draw time', () => {
|
|
87
|
+
// The loop's OWN world — the single-world identity path. Never carries the
|
|
88
|
+
// injected entity, so it cannot mask a stale read on the injected world.
|
|
89
|
+
const loopWorld = new World();
|
|
90
|
+
|
|
91
|
+
// The injected world: register propagateTransforms (the sole Transform.world
|
|
92
|
+
// writer) and spawn an entity with a distinguishing non-identity translation.
|
|
93
|
+
// We deliberately do NOT call injectedWorld.update() here — the frame-loop
|
|
94
|
+
// must run it for the translation to reach Transform.world.
|
|
95
|
+
const injectedWorld = new World();
|
|
96
|
+
registerPropagateTransforms(injectedWorld);
|
|
97
|
+
const spawned = injectedWorld
|
|
98
|
+
.spawn({
|
|
99
|
+
component: Transform,
|
|
100
|
+
data: {
|
|
101
|
+
pos: [INJECTED_TX, INJECTED_TY, INJECTED_TZ],
|
|
102
|
+
quat: [0, 0, 0, 1],
|
|
103
|
+
scale: [1, 1, 1],
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
.unwrap();
|
|
107
|
+
const entity = spawned as unknown as EntityHandle;
|
|
108
|
+
|
|
109
|
+
// Sanity: BEFORE any frame, Transform.world is still the identity default
|
|
110
|
+
// (translation columns are 0). This proves the assertion below measures the
|
|
111
|
+
// frame-loop's update, not a pre-propagated value.
|
|
112
|
+
const before = injectedWorld.get(entity, Transform);
|
|
113
|
+
expect(before.ok).toBe(true);
|
|
114
|
+
if (before.ok) {
|
|
115
|
+
const w = before.value.world as unknown as ArrayLike<number>;
|
|
116
|
+
expect(w[12]).toBe(0);
|
|
117
|
+
expect(w[13]).toBe(0);
|
|
118
|
+
expect(w[14]).toBe(0);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Spy renderer: at draw time, read the injected world's Transform.world and
|
|
122
|
+
// record the translation columns. This is the exact read the real extract
|
|
123
|
+
// stage performs (Transform.world is the extract SSOT).
|
|
124
|
+
const observed: Array<[number, number, number]> = [];
|
|
125
|
+
const renderer = {
|
|
126
|
+
backend: 'webgpu' as const,
|
|
127
|
+
ready: Promise.resolve({ ok: true, value: undefined }),
|
|
128
|
+
attachWorld: () => ({ ok: true, value: undefined }),
|
|
129
|
+
detachWorld: () => {},
|
|
130
|
+
draw(worlds: readonly World[]): { ok: true; value: undefined } {
|
|
131
|
+
// worlds is what drawSource returned; find the injected world in it.
|
|
132
|
+
const iw = worlds.find((w) => w === injectedWorld);
|
|
133
|
+
if (iw !== undefined) {
|
|
134
|
+
const r = iw.get(entity, Transform);
|
|
135
|
+
if (r.ok) {
|
|
136
|
+
const m = r.value.world as unknown as ArrayLike<number>;
|
|
137
|
+
observed.push([m[12] as number, m[13] as number, m[14] as number]);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return { ok: true, value: undefined };
|
|
141
|
+
},
|
|
142
|
+
onError(): () => void {
|
|
143
|
+
return () => {
|
|
144
|
+
// no-op
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
dispose(): void {
|
|
148
|
+
// no-op
|
|
149
|
+
},
|
|
150
|
+
} as unknown as Renderer;
|
|
151
|
+
|
|
152
|
+
const drawSource: DrawSourceCallback = () => ({
|
|
153
|
+
worlds: [injectedWorld],
|
|
154
|
+
cameraOwner: 0,
|
|
155
|
+
resourceOwner: 0,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const { raf, caf, now, pump } = makeSyncScheduler();
|
|
159
|
+
const loop = createFrameLoopWithDrawSource({
|
|
160
|
+
world: loopWorld,
|
|
161
|
+
renderer,
|
|
162
|
+
now,
|
|
163
|
+
raf,
|
|
164
|
+
caf,
|
|
165
|
+
drawSource,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(loop.start().ok).toBe(true);
|
|
169
|
+
pump(2);
|
|
170
|
+
loop.stop();
|
|
171
|
+
|
|
172
|
+
// At least one frame observed the injected world, and every observation saw
|
|
173
|
+
// the FRESH translation — the frame-loop ran injectedWorld.update() before
|
|
174
|
+
// draw. A draw-only frame-loop would observe [0, 0, 0] (identity) and fail.
|
|
175
|
+
expect(observed.length).toBeGreaterThanOrEqual(1);
|
|
176
|
+
for (const [x, y, z] of observed) {
|
|
177
|
+
expect(x).toBeCloseTo(INJECTED_TX);
|
|
178
|
+
expect(y).toBeCloseTo(INJECTED_TY);
|
|
179
|
+
expect(z).toBeCloseTo(INJECTED_TZ);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// feat-20260709-editor-world-partition-editorworld-super-composite / M2 / w8
|
|
2
|
+
// (RED — impl lands in w12). Contract test: the drawSource injection seam MUST
|
|
3
|
+
// degrade to the existing single-world path byte-identically when the host does
|
|
4
|
+
// not opt in.
|
|
5
|
+
//
|
|
6
|
+
// Two non-opt-in shapes must both reproduce the legacy call
|
|
7
|
+
// `renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 })` (feat-20260708 M3 / AC-03 wrapping):
|
|
8
|
+
//
|
|
9
|
+
// 1. `drawSource` absent entirely — the createApp/frame-loop caller passes no
|
|
10
|
+
// drawSource (the single-world AI-user default).
|
|
11
|
+
// 2. `drawSource` present but returning `undefined` on the frame — a host
|
|
12
|
+
// that wired the seam but, this frame, has nothing multi-world to inject
|
|
13
|
+
// (e.g. editor with no partitioned viewport active).
|
|
14
|
+
//
|
|
15
|
+
// Both cases MUST call `renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 })` with the loop's
|
|
16
|
+
// own single world and owner 0. This pins the AC-03 regression guarantee across
|
|
17
|
+
// the new seam: adding drawSource never perturbs the single-world path.
|
|
18
|
+
//
|
|
19
|
+
// This test drives createFrameLoop directly (injected now / raf / caf seams)
|
|
20
|
+
// with a spy renderer, mirroring frame-loop-world-array.test.ts. The drawSource
|
|
21
|
+
// option is reached through a typed alias so the intent reads clearly before w12
|
|
22
|
+
// widens the real FrameLoopOptions type (test-first RED window).
|
|
23
|
+
//
|
|
24
|
+
// Anchors:
|
|
25
|
+
// plan-strategy §2 D-3 (drawSource pull callback; absent / undefined -> single
|
|
26
|
+
// world path byte-identical to draw([world], { cameraOwner: 0, resourceOwner: 0 }))
|
|
27
|
+
// research F1 (frame-loop hardcodes renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 }) — the
|
|
28
|
+
// seam insertion point)
|
|
29
|
+
|
|
30
|
+
import { World } from '@forgeax/engine-ecs';
|
|
31
|
+
import type { Renderer } from '@forgeax/engine-render';
|
|
32
|
+
import { describe, expect, it } from 'vitest';
|
|
33
|
+
import { createFrameLoop, type FrameLoopOptions } from '../internal/frame-loop';
|
|
34
|
+
|
|
35
|
+
interface DrawCall {
|
|
36
|
+
readonly worlds: unknown;
|
|
37
|
+
readonly options: unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function makeSpyRenderer(): { renderer: Renderer; calls: DrawCall[] } {
|
|
41
|
+
const calls: DrawCall[] = [];
|
|
42
|
+
const renderer = {
|
|
43
|
+
backend: 'webgpu' as const,
|
|
44
|
+
ready: Promise.resolve({ ok: true, value: undefined }),
|
|
45
|
+
attachWorld: () => ({ ok: true, value: undefined }),
|
|
46
|
+
detachWorld: () => {},
|
|
47
|
+
draw(worlds: unknown, options: unknown): { ok: true; value: undefined } {
|
|
48
|
+
calls.push({ worlds, options });
|
|
49
|
+
return { ok: true, value: undefined };
|
|
50
|
+
},
|
|
51
|
+
onError(): () => void {
|
|
52
|
+
return () => {
|
|
53
|
+
// no-op unsubscribe
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
dispose(): void {
|
|
57
|
+
// no-op
|
|
58
|
+
},
|
|
59
|
+
} as unknown as Renderer;
|
|
60
|
+
return { renderer, calls };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Synchronous rAF driver: fire the scheduled tick exactly `frames` times.
|
|
65
|
+
* Mirrors frame-loop-world-array.test.ts so both single-world contract tests
|
|
66
|
+
* step the loop identically.
|
|
67
|
+
*/
|
|
68
|
+
function makeSyncScheduler() {
|
|
69
|
+
let pending: ((t: number) => void) | null = null;
|
|
70
|
+
let clock = 0;
|
|
71
|
+
const raf = (cb: (t: number) => void): number => {
|
|
72
|
+
pending = cb;
|
|
73
|
+
return 1;
|
|
74
|
+
};
|
|
75
|
+
const caf = (): void => {
|
|
76
|
+
pending = null;
|
|
77
|
+
};
|
|
78
|
+
const now = (): number => {
|
|
79
|
+
clock += 16;
|
|
80
|
+
return clock;
|
|
81
|
+
};
|
|
82
|
+
const pump = (frames: number): void => {
|
|
83
|
+
for (let i = 0; i < frames; i++) {
|
|
84
|
+
const cb = pending;
|
|
85
|
+
pending = null;
|
|
86
|
+
if (cb === null) break;
|
|
87
|
+
cb(clock);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
return { raf, caf, now, pump };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// The drawSource pull callback shape w11/w12 will introduce on FrameLoopOptions.
|
|
94
|
+
// Declared locally so this test states the contract independent of the (not-yet
|
|
95
|
+
// widened) source type. `undefined` return = "no multi-world injection this
|
|
96
|
+
// frame" (degrade to single world).
|
|
97
|
+
type DrawSourceCallback = () =>
|
|
98
|
+
| { worlds: readonly World[]; cameraOwner: number; resourceOwner: number }
|
|
99
|
+
| undefined;
|
|
100
|
+
|
|
101
|
+
type FrameLoopOptionsWithDrawSource = FrameLoopOptions & {
|
|
102
|
+
drawSource?: DrawSourceCallback;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const createFrameLoopWithDrawSource = createFrameLoop as unknown as (
|
|
106
|
+
opts: FrameLoopOptionsWithDrawSource,
|
|
107
|
+
) => ReturnType<typeof createFrameLoop>;
|
|
108
|
+
|
|
109
|
+
describe('drawSource seam degrades to single-world path (w8, AC-03 regression)', () => {
|
|
110
|
+
it('no drawSource: draws the primary World with explicit camera and resource owners', () => {
|
|
111
|
+
const world = new World();
|
|
112
|
+
const { renderer, calls } = makeSpyRenderer();
|
|
113
|
+
const { raf, caf, now, pump } = makeSyncScheduler();
|
|
114
|
+
|
|
115
|
+
// No drawSource field at all — the single-world AI-user default.
|
|
116
|
+
const loop = createFrameLoop({ world, renderer, now, raf, caf });
|
|
117
|
+
expect(loop.start().ok).toBe(true);
|
|
118
|
+
pump(3);
|
|
119
|
+
|
|
120
|
+
expect(calls.length).toBeGreaterThanOrEqual(3);
|
|
121
|
+
for (const call of calls) {
|
|
122
|
+
expect(Array.isArray(call.worlds)).toBe(true);
|
|
123
|
+
const arr = call.worlds as unknown[];
|
|
124
|
+
expect(arr.length).toBe(1);
|
|
125
|
+
expect(arr[0]).toBe(world);
|
|
126
|
+
expect(call.options).toEqual({ cameraOwner: 0, resourceOwner: 0 });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
loop.stop();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('drawSource returns undefined: still draws [world] with { cameraOwner: 0, resourceOwner: 0 } (no-inject frame)', () => {
|
|
133
|
+
const world = new World();
|
|
134
|
+
const { renderer, calls } = makeSpyRenderer();
|
|
135
|
+
const { raf, caf, now, pump } = makeSyncScheduler();
|
|
136
|
+
|
|
137
|
+
let pulls = 0;
|
|
138
|
+
const drawSource: DrawSourceCallback = () => {
|
|
139
|
+
pulls++;
|
|
140
|
+
// Host wired the seam but has nothing multi-world to inject this frame.
|
|
141
|
+
return undefined;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const loop = createFrameLoopWithDrawSource({ world, renderer, now, raf, caf, drawSource });
|
|
145
|
+
expect(loop.start().ok).toBe(true);
|
|
146
|
+
pump(3);
|
|
147
|
+
|
|
148
|
+
// The seam was consulted (pull callback fired) but the resulting draw is
|
|
149
|
+
// identical to the primary-World call.
|
|
150
|
+
expect(pulls).toBeGreaterThanOrEqual(3);
|
|
151
|
+
expect(calls.length).toBeGreaterThanOrEqual(3);
|
|
152
|
+
for (const call of calls) {
|
|
153
|
+
expect(Array.isArray(call.worlds)).toBe(true);
|
|
154
|
+
const arr = call.worlds as unknown[];
|
|
155
|
+
expect(arr.length).toBe(1);
|
|
156
|
+
expect(arr[0]).toBe(world);
|
|
157
|
+
expect(call.options).toEqual({ cameraOwner: 0, resourceOwner: 0 });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
loop.stop();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('setDrawSource switches routing without creating a second frame loop', () => {
|
|
164
|
+
const world = new World();
|
|
165
|
+
const overlay = new World();
|
|
166
|
+
const { renderer, calls } = makeSpyRenderer();
|
|
167
|
+
const { raf, caf, now, pump } = makeSyncScheduler();
|
|
168
|
+
const loop = createFrameLoop({ world, renderer, now, raf, caf });
|
|
169
|
+
|
|
170
|
+
expect(loop.start().ok).toBe(true);
|
|
171
|
+
pump(1);
|
|
172
|
+
loop.setDrawSource(() => ({ worlds: [world, overlay], cameraOwner: 0, resourceOwner: 0 }));
|
|
173
|
+
pump(1);
|
|
174
|
+
loop.setDrawSource(undefined);
|
|
175
|
+
pump(1);
|
|
176
|
+
|
|
177
|
+
expect(calls).toHaveLength(3);
|
|
178
|
+
expect(calls[0]?.options).toEqual({ cameraOwner: 0, resourceOwner: 0 });
|
|
179
|
+
expect(calls[1]?.worlds).toEqual([world, overlay]);
|
|
180
|
+
expect(calls[1]?.options).toEqual({ cameraOwner: 0, resourceOwner: 0 });
|
|
181
|
+
expect(calls[2]?.worlds).toEqual([world]);
|
|
182
|
+
expect(calls[2]?.options).toEqual({ cameraOwner: 0, resourceOwner: 0 });
|
|
183
|
+
loop.stop();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
createEcsImportModule,
|
|
4
|
+
ECS_PUBLIC_SYMBOLS,
|
|
5
|
+
projectEcsPublicModule,
|
|
6
|
+
} from '../internal/ecs-import';
|
|
7
|
+
|
|
8
|
+
describe('host-owned ECS import projection', () => {
|
|
9
|
+
it('keeps the explicit six-symbol surface and drops implementation exports', () => {
|
|
10
|
+
expect(ECS_PUBLIC_SYMBOLS).toEqual([
|
|
11
|
+
'World',
|
|
12
|
+
'Entity',
|
|
13
|
+
'Update',
|
|
14
|
+
'FixedUpdate',
|
|
15
|
+
'Time',
|
|
16
|
+
'FixedTime',
|
|
17
|
+
]);
|
|
18
|
+
expect(
|
|
19
|
+
projectEcsPublicModule({
|
|
20
|
+
World: 'World',
|
|
21
|
+
Entity: 'Entity',
|
|
22
|
+
defineComponent: 'implementation-only',
|
|
23
|
+
}),
|
|
24
|
+
).toEqual({ World: 'World', Entity: 'Entity' });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('projects only the ECS specifier and preserves other host modules', async () => {
|
|
28
|
+
const calls: string[] = [];
|
|
29
|
+
const importModule = createEcsImportModule(async (specifier) => {
|
|
30
|
+
calls.push(specifier);
|
|
31
|
+
return specifier === '@forgeax/engine-ecs'
|
|
32
|
+
? { World: 'World', defineComponent: 'implementation-only' }
|
|
33
|
+
: { value: 'other-module' };
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
await expect(importModule('@forgeax/engine-ecs')).resolves.toEqual({ World: 'World' });
|
|
37
|
+
await expect(importModule('virtual:host-module')).resolves.toEqual({ value: 'other-module' });
|
|
38
|
+
expect(calls).toEqual(['@forgeax/engine-ecs', 'virtual:host-module']);
|
|
39
|
+
});
|
|
40
|
+
});
|