@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,1264 @@
|
|
|
1
|
+
// @forgeax/engine-app -- createApp double-SSOT entry (M3 wiring).
|
|
2
|
+
//
|
|
3
|
+
// This file ships the final-shape signatures for both overload routes
|
|
4
|
+
// (canvas thin wrapper + assemble form per plan-strategy D-5):
|
|
5
|
+
//
|
|
6
|
+
// - createApp({ renderer, world, ... }) -- assemble form: returns an
|
|
7
|
+
// App with the supplied renderer / world wired through; the M2 rAF
|
|
8
|
+
// frame-loop + state machine + dt clamp drive start / stop / pause /
|
|
9
|
+
// resume. M3 lands listener-registry-backed onError + input-attach
|
|
10
|
+
// plumbing: when the host passes a pre-built InputBackend through
|
|
11
|
+
// args.input, the assemble form treats it as host-managed (no auto
|
|
12
|
+
// attach, no auto cleanup -- the host owns the lifetime).
|
|
13
|
+
//
|
|
14
|
+
// - createApp(canvas, opts?) -- canvas form (M3 partial):
|
|
15
|
+
// calls createRenderer(canvas, opts?), constructs a new World, and
|
|
16
|
+
// (when opts.input !== false) wires the auto input-attach helper so
|
|
17
|
+
// world.getResource('InputSnapshot') is populated each frame.
|
|
18
|
+
// Falls through to the assemble form for the rest of the wiring.
|
|
19
|
+
// Full canvas form (canvas-detached check, EngineEnvironmentError
|
|
20
|
+
// catch, console.error fallback) lands in M4 (plan-strategy section 7).
|
|
21
|
+
//
|
|
22
|
+
// 'tagName' in arg dispatch (per plan-strategy D-5): HTMLCanvasElement
|
|
23
|
+
// inherits .tagName from HTMLElement, so the property test cleanly
|
|
24
|
+
// separates the canvas argument from the AppAssembleArgs plain object.
|
|
25
|
+
|
|
26
|
+
import type { AssetRegistry } from '@forgeax/engine-assets-runtime';
|
|
27
|
+
import type { AudioBackend } from '@forgeax/engine-audio';
|
|
28
|
+
import type { DebugDraw } from '@forgeax/engine-debug-draw';
|
|
29
|
+
import { createWorldContext, Update, World } from '@forgeax/engine-ecs';
|
|
30
|
+
import type { InputBackend } from '@forgeax/engine-input';
|
|
31
|
+
import type { Context, Plugin } from '@forgeax/engine-plugin';
|
|
32
|
+
import type { RenderError } from '@forgeax/engine-render';
|
|
33
|
+
import { CAMERA_PROJECTION_PERSPECTIVE, Camera, type Renderer } from '@forgeax/engine-render';
|
|
34
|
+
import type { RendererHostAssembly } from '@forgeax/engine-render/internal/construct-renderer';
|
|
35
|
+
import { RhiError } from '@forgeax/engine-rhi';
|
|
36
|
+
import {
|
|
37
|
+
attachRecorder,
|
|
38
|
+
type CaptureFrameOptions,
|
|
39
|
+
type RecorderAttachment,
|
|
40
|
+
} from '@forgeax/engine-rhi-debug';
|
|
41
|
+
import * as engineRuntimeModule from '@forgeax/engine-runtime';
|
|
42
|
+
import { EngineEnvironmentError } from '@forgeax/engine-runtime';
|
|
43
|
+
import {
|
|
44
|
+
constructRuntimeRendererHost,
|
|
45
|
+
loadRhiPack,
|
|
46
|
+
} from '@forgeax/engine-runtime/internal/renderer-host';
|
|
47
|
+
import { err, ok, type Result } from '@forgeax/engine-types';
|
|
48
|
+
|
|
49
|
+
import { createAnimationPayloadLookup } from './animation-asset-lookup';
|
|
50
|
+
import { publishBrowserFrameSubmitted, resetBrowserFrameSubmitted } from './browser-frame-signal';
|
|
51
|
+
import type { AppErrorCode, AppErrorDetailFor } from './errors';
|
|
52
|
+
import { APP_ERROR_HINTS, APP_EXPECTED, AppError } from './errors';
|
|
53
|
+
import {
|
|
54
|
+
createExecutionReport,
|
|
55
|
+
type ExecutionControl,
|
|
56
|
+
executionBootstrapHostPlugin,
|
|
57
|
+
type PreparedExecutionBootstrap,
|
|
58
|
+
prepareBootstrapEntry,
|
|
59
|
+
probeExecutionCapabilities,
|
|
60
|
+
selectExecutionTier,
|
|
61
|
+
unavailableExecutionCapabilities,
|
|
62
|
+
} from './execution';
|
|
63
|
+
import { normalizeExecutionBootstrapUrl } from './execution/bootstrap-url';
|
|
64
|
+
import { createLocalExecutionControl } from './execution/control';
|
|
65
|
+
import { createWorkerExecutionApp } from './execution/host-controller';
|
|
66
|
+
import { assembledEngineProfile } from './internal/assembled-engine-profile';
|
|
67
|
+
import { projectComponentIntrospection } from './internal/component-introspection';
|
|
68
|
+
import { ErrorFanoutRegistry } from './internal/error-fanout';
|
|
69
|
+
import { createFrameLoop } from './internal/frame-loop';
|
|
70
|
+
import { attachInputAuto } from './internal/input-attach';
|
|
71
|
+
import { mainEngineProfile } from './internal/main-engine-profile';
|
|
72
|
+
import { resolveRemoteServeFlag } from './internal/remote-serve-flag';
|
|
73
|
+
import { remoteServerPlugin } from './internal/remote-server-plugin';
|
|
74
|
+
import {
|
|
75
|
+
bindRhiCaptureFrameDriver,
|
|
76
|
+
createRhiCapture,
|
|
77
|
+
createRhiInstrumentation,
|
|
78
|
+
type RhiCapture,
|
|
79
|
+
} from './internal/rhi-capture';
|
|
80
|
+
import { resolveRhiDebugFlag } from './internal/rhi-debug-flag';
|
|
81
|
+
import { createRenderFeatureHost, type RenderFeatureHost } from './renderer-plugin';
|
|
82
|
+
import type {
|
|
83
|
+
App,
|
|
84
|
+
AppAssembleArgs,
|
|
85
|
+
AppDispatchError,
|
|
86
|
+
AssembleAppError,
|
|
87
|
+
BundlerOptions,
|
|
88
|
+
CanvasAppError,
|
|
89
|
+
CreateAppOptions,
|
|
90
|
+
ExecutionApp,
|
|
91
|
+
} from './types';
|
|
92
|
+
|
|
93
|
+
function makeAppError<C extends AppErrorCode>(
|
|
94
|
+
code: C,
|
|
95
|
+
expected: string,
|
|
96
|
+
hint: string,
|
|
97
|
+
detail: AppErrorDetailFor<C>,
|
|
98
|
+
): AppError {
|
|
99
|
+
return new AppError({ code, expected, hint, detail }) as AppError;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function canvasAspectPlugin(canvas: HTMLCanvasElement): Plugin {
|
|
103
|
+
return {
|
|
104
|
+
name: 'canvas-aspect',
|
|
105
|
+
inject: ['world'],
|
|
106
|
+
apply(ctx) {
|
|
107
|
+
ctx.effect(() => {
|
|
108
|
+
const system = {
|
|
109
|
+
name: 'app-sync-camera-aspect',
|
|
110
|
+
queries: [],
|
|
111
|
+
fn: () => {
|
|
112
|
+
syncCanvasDrawingBuffer(canvas);
|
|
113
|
+
syncCameraAspect(ctx.world, canvas.width, canvas.height);
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
ctx.world.addSystem(Update, system).unwrap();
|
|
117
|
+
return () => ctx.world.removeSystem(Update, system.name).unwrap();
|
|
118
|
+
}, 'app/canvas-aspect');
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function rhiDebugHostPlugin(dispose: () => void): Plugin {
|
|
124
|
+
return {
|
|
125
|
+
name: 'rhi-debug-host',
|
|
126
|
+
apply(ctx) {
|
|
127
|
+
ctx.effect(() => dispose, 'rhi-debug/host');
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* createApp(canvas, opts?, bundler?) -- canvas thin wrapper SSOT (per
|
|
134
|
+
* plan-strategy D-5). Resolves with Result.ok(app) on success; failure routes
|
|
135
|
+
* through AppError | RhiError | EngineEnvironmentError per requirements AC-01.
|
|
136
|
+
*
|
|
137
|
+
* feat-20260608-create-app-param-surface-trim / M2 / D-3: the third arg is
|
|
138
|
+
* `BundlerOptions` (importTransport + shaderManifestUrl) -- the SSOT for
|
|
139
|
+
* host-injected build-tool emit knowledge. M3 demos collapse the third-arg
|
|
140
|
+
* literal to `forgeaxBundlerAdapter()` exported by `virtual:forgeax/bundler`.
|
|
141
|
+
*
|
|
142
|
+
* M3 ships the path needed by AC-05 (auto input acquisition plus an ECS scan
|
|
143
|
+
* plugin). M4 finalises the canvas-detached guard +
|
|
144
|
+
* EngineEnvironmentError try/catch + onError default fallback.
|
|
145
|
+
*
|
|
146
|
+
* The host-engine contract defines the boundary between host (DOM, canvas, UI)
|
|
147
|
+
* and engine (renderer, world, frame loop). Only the `createApp(canvas)` path
|
|
148
|
+
* auto-wires the aspect-sync sidecar; the assemble form and bare
|
|
149
|
+
* `createRenderer` path do not.
|
|
150
|
+
*
|
|
151
|
+
* @see {@link https://github.com/Ubpa/forgeax-engine/blob/main/docs/how-to/2026-06-18-host-engine-contract.md | Host-engine contract SSOT}
|
|
152
|
+
*/
|
|
153
|
+
export function createApp(
|
|
154
|
+
canvas: HTMLCanvasElement,
|
|
155
|
+
opts: CreateAppOptions & { readonly execution: NonNullable<CreateAppOptions['execution']> },
|
|
156
|
+
bundler?: BundlerOptions,
|
|
157
|
+
): Promise<Result<ExecutionApp, CanvasAppError>>;
|
|
158
|
+
export function createApp(
|
|
159
|
+
canvas: HTMLCanvasElement,
|
|
160
|
+
opts?: CreateAppOptions,
|
|
161
|
+
bundler?: BundlerOptions,
|
|
162
|
+
): Promise<Result<App, CanvasAppError>>;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Create the browser host for the single RenderScene -> FrameReceipt path.
|
|
166
|
+
* `createApp({ renderer, world, input?, schedule?, ... })` is the assemble form
|
|
167
|
+
* SSOT (per plan-strategy D-5). Host already owns renderer / world; the
|
|
168
|
+
* returned App holds them by reference equality (per AC-02).
|
|
169
|
+
*
|
|
170
|
+
* M3 wires the rAF frame-loop + listener-registry-backed onError. When
|
|
171
|
+
* args.input is supplied, the assemble form treats it as host-managed:
|
|
172
|
+
* it is exposed verbatim as app.input, and the host is responsible for
|
|
173
|
+
* detaching it (no auto-cleanup on stop). The canvas form is the entry
|
|
174
|
+
* that engages the auto-attach helper (input-attach.ts).
|
|
175
|
+
*/
|
|
176
|
+
export function createApp(args: AppAssembleArgs): Promise<Result<App, AssembleAppError>>;
|
|
177
|
+
|
|
178
|
+
export function createApp(
|
|
179
|
+
arg: HTMLCanvasElement | AppAssembleArgs,
|
|
180
|
+
opts?: CreateAppOptions,
|
|
181
|
+
bundler?: BundlerOptions,
|
|
182
|
+
): Promise<Result<App | ExecutionApp, CanvasAppError>> {
|
|
183
|
+
if ('tagName' in arg) {
|
|
184
|
+
return createAppFromCanvas(arg, opts, bundler);
|
|
185
|
+
}
|
|
186
|
+
return createAppFromAssemble(arg);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function createAppFromCanvas(
|
|
190
|
+
canvas: HTMLCanvasElement,
|
|
191
|
+
opts: CreateAppOptions | undefined,
|
|
192
|
+
// feat-20260608-create-app-param-surface-trim / M2 / D-3: BundlerOptions is
|
|
193
|
+
// the host-injection SSOT (importTransport + shaderManifestUrl). Forwarded
|
|
194
|
+
// verbatim to createRenderer's third arg, so the engine reads
|
|
195
|
+
// shaderManifestUrl in its ShaderRegistry fallback (D-2 q5-A) and threads
|
|
196
|
+
// importTransport to AssetRegistry (AC-05 / R-4: keeps build-tool
|
|
197
|
+
// injection out of RendererOptions / CreateAppOptions).
|
|
198
|
+
bundler: BundlerOptions | undefined,
|
|
199
|
+
): Promise<Result<App | ExecutionApp, CanvasAppError>> {
|
|
200
|
+
// M4: 4-step thin wrapper per plan-strategy D-5.
|
|
201
|
+
//
|
|
202
|
+
// Step 1: canvas-detached fail-fast guard (AC-08). isConnected returns
|
|
203
|
+
// false when the canvas is not in the document tree, including freshly
|
|
204
|
+
// document.createElement('canvas') without appendChild. Returning
|
|
205
|
+
// Result.err here short-circuits before createRenderer fires off any
|
|
206
|
+
// async adapter / device / shader work that would only fail later.
|
|
207
|
+
if (!canvas.isConnected) {
|
|
208
|
+
return err(
|
|
209
|
+
makeAppError(
|
|
210
|
+
'app-canvas-detached',
|
|
211
|
+
'canvas.isConnected === true at createApp(canvas) entry',
|
|
212
|
+
'append the canvas to the document tree before calling createApp; or use the assemble entry createApp({ renderer, world }) when the host already manages canvas lifetime',
|
|
213
|
+
{},
|
|
214
|
+
),
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// The DOM canvas starts with a 300x150 drawing buffer even when CSS lays it
|
|
219
|
+
// out at a different size. Set the physical buffer before the renderer
|
|
220
|
+
// configures its swap chain; the same helper runs before each frame so a CSS
|
|
221
|
+
// resize remains visible to both rendering and camera policy.
|
|
222
|
+
syncCanvasDrawingBuffer(canvas);
|
|
223
|
+
|
|
224
|
+
let executionBootstrapUrl: string | undefined;
|
|
225
|
+
if (opts?.execution !== undefined) {
|
|
226
|
+
const normalizedBootstrap = normalizeExecutionBootstrapUrl(opts.execution.bootstrap);
|
|
227
|
+
if (!normalizedBootstrap.ok) return err(normalizedBootstrap.error);
|
|
228
|
+
executionBootstrapUrl = normalizedBootstrap.value;
|
|
229
|
+
const realmBoundOption = [
|
|
230
|
+
['features', opts.features],
|
|
231
|
+
['plugins', opts.plugins],
|
|
232
|
+
['rhi', opts.rhi],
|
|
233
|
+
['rhiInstrumentation', opts.rhiInstrumentation],
|
|
234
|
+
['drawSource', opts.drawSource],
|
|
235
|
+
['bundler.importTransport', bundler?.importTransport],
|
|
236
|
+
].find(([, value]) => value !== undefined)?.[0];
|
|
237
|
+
if (realmBoundOption !== undefined) {
|
|
238
|
+
return err(
|
|
239
|
+
makeAppError(
|
|
240
|
+
'app-execution-bootstrap-failed',
|
|
241
|
+
APP_EXPECTED['app-execution-bootstrap-failed'],
|
|
242
|
+
APP_ERROR_HINTS['app-execution-bootstrap-failed'],
|
|
243
|
+
{
|
|
244
|
+
phase: 'prepare',
|
|
245
|
+
moduleUrl: executionBootstrapUrl,
|
|
246
|
+
cause: new TypeError(
|
|
247
|
+
`${realmBoundOption} must be constructed by the execution bootstrap module`,
|
|
248
|
+
),
|
|
249
|
+
},
|
|
250
|
+
),
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let executionContext:
|
|
256
|
+
| {
|
|
257
|
+
readonly capabilities: import('./execution').ExecutionCapabilities;
|
|
258
|
+
readonly selection: import('./execution').ExecutionSelection;
|
|
259
|
+
}
|
|
260
|
+
| undefined;
|
|
261
|
+
let preparedExecutionBootstrap: PreparedExecutionBootstrap | undefined;
|
|
262
|
+
if (opts?.execution !== undefined) {
|
|
263
|
+
const capabilities = await probeExecutionCapabilities(canvas);
|
|
264
|
+
const selected = selectExecutionTier({
|
|
265
|
+
requestedTier: opts.execution.tier ?? 'auto',
|
|
266
|
+
capabilities,
|
|
267
|
+
sharedEvidencePassed: true,
|
|
268
|
+
});
|
|
269
|
+
if (!selected.ok) return err(selected.error);
|
|
270
|
+
executionContext = { capabilities, selection: selected.value };
|
|
271
|
+
if (selected.value.actualTier !== 'main-serial') {
|
|
272
|
+
return createWorkerExecutionApp({
|
|
273
|
+
canvas,
|
|
274
|
+
appOptions: opts,
|
|
275
|
+
syncCanvas: () => measureCanvasDrawingBuffer(canvas),
|
|
276
|
+
...(bundler !== undefined ? { bundler } : {}),
|
|
277
|
+
capabilities,
|
|
278
|
+
selection: selected.value,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
if (executionBootstrapUrl === undefined) {
|
|
282
|
+
throw new Error('execution bootstrap URL was not normalized');
|
|
283
|
+
}
|
|
284
|
+
const prepared = await prepareBootstrapEntry(
|
|
285
|
+
executionBootstrapUrl,
|
|
286
|
+
opts.execution.bootstrapData,
|
|
287
|
+
);
|
|
288
|
+
if (!prepared.ok) return err(prepared.error);
|
|
289
|
+
preparedExecutionBootstrap = prepared.value;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Step 2: createRenderer try/catch -> Result.err(EngineEnvironmentError)
|
|
293
|
+
// (AC-01 / research section 2.2). createRenderer throws at
|
|
294
|
+
// createRenderer.ts:400 / :429 (rhi pack load failure /
|
|
295
|
+
// all WebGPU channels unavailable); we forward the original
|
|
296
|
+
// EngineEnvironmentError instance verbatim to preserve the
|
|
297
|
+
// .detail.webgpuError surface. RhiError instances raised mid-
|
|
298
|
+
// construction are also forwarded -- the AppError | RhiError leg of
|
|
299
|
+
// the union covers them. Any other unexpected throw is also
|
|
300
|
+
// forwarded to keep the contract honest (AI users walk the union
|
|
301
|
+
// discriminant rather than parse error.message strings).
|
|
302
|
+
// feat-20260608 / M2 / D-3: CreateAppOptions stops `extends RendererOptions`,
|
|
303
|
+
// so the RHI escape hatch (rhi) is
|
|
304
|
+
// forwarded explicitly. Build a RendererOptions object out of just those
|
|
305
|
+
// fields when present; an empty {} keeps createRenderer on its default path.
|
|
306
|
+
const rendererOpts: import('@forgeax/engine-render').RendererOptions = {};
|
|
307
|
+
if (opts?.rhi !== undefined) {
|
|
308
|
+
Object.assign(rendererOpts, { rhi: opts.rhi });
|
|
309
|
+
}
|
|
310
|
+
if (opts?.profiler !== undefined) {
|
|
311
|
+
Object.assign(rendererOpts, { profiler: opts.profiler });
|
|
312
|
+
}
|
|
313
|
+
const rendererFeatures = preparedExecutionBootstrap?.features ?? opts?.features;
|
|
314
|
+
if (rendererFeatures !== undefined) {
|
|
315
|
+
Object.assign(rendererOpts, { features: rendererFeatures });
|
|
316
|
+
}
|
|
317
|
+
if (opts?.standardProfile !== undefined) {
|
|
318
|
+
Object.assign(rendererOpts, { standardProfile: opts.standardProfile });
|
|
319
|
+
}
|
|
320
|
+
if (opts?.rhiInstrumentation !== undefined) {
|
|
321
|
+
Object.assign(rendererOpts, { rhiInstrumentation: opts.rhiInstrumentation });
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// FORGEAX_ENGINE_RHI_DEBUG=1 attaches the recorder at the Runtime backend
|
|
325
|
+
// seam. Render remains the owner of device/surface lifecycle; App only owns
|
|
326
|
+
// the optional capture capability and its frame transaction.
|
|
327
|
+
let rhiAttachment: RecorderAttachment | undefined;
|
|
328
|
+
let rhiCapture: RhiCapture | undefined;
|
|
329
|
+
let rhiDebugGlobal:
|
|
330
|
+
| { captureFrame(options?: CaptureFrameOptions): ReturnType<RhiCapture['captureFrame']> }
|
|
331
|
+
| undefined;
|
|
332
|
+
const cleanupRhiDebugHost = (): void => {
|
|
333
|
+
const host = globalThis as { __forgeax?: typeof rhiDebugGlobal };
|
|
334
|
+
if (host.__forgeax === rhiDebugGlobal) delete host.__forgeax;
|
|
335
|
+
rhiDebugGlobal = undefined;
|
|
336
|
+
const attachment = rhiAttachment;
|
|
337
|
+
rhiAttachment = undefined;
|
|
338
|
+
if (attachment !== undefined) void attachment.dispose();
|
|
339
|
+
};
|
|
340
|
+
// Read FORGEAX_ENGINE_RHI_DEBUG from two sources (plan-strategy D-4):
|
|
341
|
+
// - browser: import.meta.env, statically replaced by the
|
|
342
|
+
// vite-plugin-rhi-debug `define` hook. The `typeof import.meta !==
|
|
343
|
+
// 'undefined'` prefix short-circuits under dawn-node, where import.meta
|
|
344
|
+
// itself can be undefined (C5).
|
|
345
|
+
// - dawn-node: globalThis.process.env (no vite define on the native path).
|
|
346
|
+
// resolveRhiDebugFlag (internal/rhi-debug-flag) is the SSOT for the `??`
|
|
347
|
+
// precedence; the source bags are computed inline here so the
|
|
348
|
+
// typeof-import.meta prefix stays at the call site. Keeps this file
|
|
349
|
+
// @types/node-free (engine-app ships ESM into both browser + dawn-node;
|
|
350
|
+
// same pattern as runtime/src/render-system-record.ts:isMeshSsboDevMode).
|
|
351
|
+
// Keep the Vite define key in a direct property expression. Wrapping
|
|
352
|
+
// `import.meta` in a structural cast hides the token from Vite's define
|
|
353
|
+
// pass, so production builds retain the debug-only dynamic chunks even when
|
|
354
|
+
// the Preview host explicitly sets the flag to `0`. Optional chaining keeps
|
|
355
|
+
// the package safe on the Dawn/Node path where `import.meta.env` is absent.
|
|
356
|
+
const importMetaEnv =
|
|
357
|
+
typeof import.meta !== 'undefined'
|
|
358
|
+
? { FORGEAX_ENGINE_RHI_DEBUG: import.meta.env?.FORGEAX_ENGINE_RHI_DEBUG }
|
|
359
|
+
: undefined;
|
|
360
|
+
// A browser production build must be able to erase the entire recorder
|
|
361
|
+
// branch. The `undefined` arm preserves Dawn/Node's process.env fallback;
|
|
362
|
+
// Vite replaces the direct property with `"0"` or `"1"`, making this outer
|
|
363
|
+
// guard a literal false/true before Rollup sees the debug-only imports.
|
|
364
|
+
const browserBuildRhiDebugFlag =
|
|
365
|
+
typeof import.meta !== 'undefined' ? import.meta.env?.FORGEAX_ENGINE_RHI_DEBUG : undefined;
|
|
366
|
+
const processEnv = (globalThis as { process?: { env?: { FORGEAX_ENGINE_RHI_DEBUG?: string } } })
|
|
367
|
+
.process?.env;
|
|
368
|
+
const rhiDebugFlag = resolveRhiDebugFlag(importMetaEnv, processEnv);
|
|
369
|
+
const nav: { gpu?: unknown } | undefined =
|
|
370
|
+
typeof globalThis !== 'undefined'
|
|
371
|
+
? (globalThis as { navigator?: { gpu?: unknown } }).navigator
|
|
372
|
+
: undefined;
|
|
373
|
+
const hasWebGPU = nav !== undefined && 'gpu' in nav && nav.gpu !== undefined;
|
|
374
|
+
if (
|
|
375
|
+
(browserBuildRhiDebugFlag === undefined || browserBuildRhiDebugFlag === '1') &&
|
|
376
|
+
rhiDebugFlag === '1'
|
|
377
|
+
) {
|
|
378
|
+
const realBackend = (hasWebGPU
|
|
379
|
+
? await import('@forgeax/engine-rhi-webgpu')
|
|
380
|
+
: await import('@forgeax/engine-rhi-wgpu')) as unknown as Record<string, unknown>;
|
|
381
|
+
if (!hasWebGPU && 'ensureReady' in realBackend) {
|
|
382
|
+
await (realBackend.ensureReady as () => Promise<unknown>)();
|
|
383
|
+
}
|
|
384
|
+
const pack = loadRhiPack(realBackend);
|
|
385
|
+
if (pack.createShaderModule === undefined) {
|
|
386
|
+
throw new Error('RHI-debug requires a backend createShaderModule capability');
|
|
387
|
+
}
|
|
388
|
+
const attached = attachRecorder({
|
|
389
|
+
rhi: pack.rhi,
|
|
390
|
+
createShaderModule: pack.createShaderModule,
|
|
391
|
+
});
|
|
392
|
+
if (!attached.ok) throw new Error(attached.error.hint);
|
|
393
|
+
rhiAttachment = attached.value;
|
|
394
|
+
rhiCapture = createRhiCapture(attached.value);
|
|
395
|
+
const capture = rhiCapture;
|
|
396
|
+
rhiDebugGlobal = { captureFrame: (options) => capture.captureFrame(options) };
|
|
397
|
+
Object.assign(rendererOpts, {
|
|
398
|
+
rhi: attached.value.backend.rhi,
|
|
399
|
+
rhiInstrumentation: createRhiInstrumentation(attached.value),
|
|
400
|
+
});
|
|
401
|
+
(globalThis as { __forgeax?: typeof rhiDebugGlobal }).__forgeax = rhiDebugGlobal;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
let renderer: Renderer;
|
|
405
|
+
let rendererDebugDrawHost: RendererHostAssembly['debugDrawHost'];
|
|
406
|
+
let rendererFeatureHost: RenderFeatureHost;
|
|
407
|
+
let assets: AssetRegistry;
|
|
408
|
+
try {
|
|
409
|
+
const constructed = await constructRuntimeRendererHost(canvas, rendererOpts, bundler);
|
|
410
|
+
if (!constructed.ok) throw constructed.error;
|
|
411
|
+
renderer = constructed.value.renderer;
|
|
412
|
+
rendererDebugDrawHost = constructed.value.debugDrawHost;
|
|
413
|
+
rendererFeatureHost = createRenderFeatureHost(constructed.value.featureHost);
|
|
414
|
+
assets = constructed.value.assets;
|
|
415
|
+
} catch (e: unknown) {
|
|
416
|
+
cleanupRhiDebugHost();
|
|
417
|
+
if (e instanceof EngineEnvironmentError) return err(e);
|
|
418
|
+
const detail = e instanceof Error ? e : new Error(String(e));
|
|
419
|
+
return err(
|
|
420
|
+
new EngineEnvironmentError('renderer construction failed', {
|
|
421
|
+
webgpuError: detail,
|
|
422
|
+
}),
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Step 2.4 decision: resolve whether the remote eval server should start
|
|
427
|
+
// (feat-20260629-inspector-two-layer-model M4 / w20). Dual-source gating
|
|
428
|
+
// mirrors the rhi-debug-flag pattern. The actual startServer call is
|
|
429
|
+
// deferred to after World creation (Step 3) because the server needs
|
|
430
|
+
// a live World reference.
|
|
431
|
+
const shouldStartRemote = resolveRemoteServeFlag(
|
|
432
|
+
typeof import.meta !== 'undefined'
|
|
433
|
+
? (import.meta as { env?: { DEV?: boolean } }).env?.DEV
|
|
434
|
+
: undefined,
|
|
435
|
+
(globalThis as { process?: { env?: { FORGEAX_ENGINE_REMOTE_SERVE?: string } } }).process?.env,
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
let debugDraw: DebugDraw | undefined;
|
|
439
|
+
|
|
440
|
+
// Step 3: new World() -- the canvas form owns world lifetime, in
|
|
441
|
+
// contrast to the assemble form where the host owns it.
|
|
442
|
+
const world = new World(opts?.time !== undefined ? { time: opts.time } : {});
|
|
443
|
+
// Step 3.1 (M2 plugin-system-unify / D-4): app-layer side effects that the
|
|
444
|
+
// plugins consume via pre-injected world resources.
|
|
445
|
+
//
|
|
446
|
+
// Animation keeps durable clip GUIDs in graph payloads. The canvas default
|
|
447
|
+
// plugin receives the renderer-owned GUID catalogue as a lookup bridge; it
|
|
448
|
+
// still projects payloads into this World through the animation owner.
|
|
449
|
+
// transform + animation system registration lives in the plugins (default set).
|
|
450
|
+
|
|
451
|
+
// A normal canvas app owns its browser acquisition. An embedding host that
|
|
452
|
+
// shares this physical canvas with another world supplies its routed view via
|
|
453
|
+
// opts.input; createApp then consumes that one boundary instead of attaching a
|
|
454
|
+
// second listener set. The host owns the supplied backend's lifetime.
|
|
455
|
+
const inputHandle =
|
|
456
|
+
opts?.input === undefined
|
|
457
|
+
? attachInputAuto(canvas, {
|
|
458
|
+
...(opts?.uiRoot ? { uiRoot: opts.uiRoot } : {}),
|
|
459
|
+
...(opts?.pointerLockAllowed ? { pointerLockAllowed: opts.pointerLockAllowed } : {}),
|
|
460
|
+
...(opts?.virtualJoysticks ? { virtualJoysticks: opts.virtualJoysticks } : {}),
|
|
461
|
+
...(opts?.lockProvider ? { lockProvider: opts.lockProvider } : {}),
|
|
462
|
+
})
|
|
463
|
+
: undefined;
|
|
464
|
+
const inputBackend = opts?.input ?? inputHandle?.backend;
|
|
465
|
+
const userPlugins =
|
|
466
|
+
preparedExecutionBootstrap === undefined
|
|
467
|
+
? (opts?.plugins ?? [])
|
|
468
|
+
: [
|
|
469
|
+
executionBootstrapHostPlugin({
|
|
470
|
+
...(opts?.execution?.bootstrapPort === undefined
|
|
471
|
+
? {}
|
|
472
|
+
: { port: opts.execution.bootstrapPort }),
|
|
473
|
+
setPointerLockAllowed: (allowed) => inputBackend?.setPointerLockAllowed?.(allowed),
|
|
474
|
+
}),
|
|
475
|
+
...(preparedExecutionBootstrap.plugins ?? []),
|
|
476
|
+
];
|
|
477
|
+
let pluginContext: Context;
|
|
478
|
+
try {
|
|
479
|
+
pluginContext = await createWorldContext(
|
|
480
|
+
world,
|
|
481
|
+
mainEngineProfile({
|
|
482
|
+
renderer,
|
|
483
|
+
rendererDebugDrawHost,
|
|
484
|
+
rendererFeatureHost,
|
|
485
|
+
assets,
|
|
486
|
+
animationPayloads: createAnimationPayloadLookup(assets),
|
|
487
|
+
...(inputBackend === undefined ? {} : { input: inputBackend }),
|
|
488
|
+
...(inputHandle === undefined ? {} : { inputDispose: inputHandle.cleanup }),
|
|
489
|
+
...(opts?.inputMap === undefined ? {} : { inputMap: opts.inputMap }),
|
|
490
|
+
onDebugDrawReady: (value) => {
|
|
491
|
+
debugDraw = value;
|
|
492
|
+
},
|
|
493
|
+
extensions: [
|
|
494
|
+
rhiDebugHostPlugin(cleanupRhiDebugHost),
|
|
495
|
+
...userPlugins,
|
|
496
|
+
canvasAspectPlugin(canvas),
|
|
497
|
+
],
|
|
498
|
+
}),
|
|
499
|
+
);
|
|
500
|
+
} catch (cause) {
|
|
501
|
+
cleanupRhiDebugHost();
|
|
502
|
+
return err(
|
|
503
|
+
makeAppError(
|
|
504
|
+
'app-plugin-activation-failed',
|
|
505
|
+
APP_EXPECTED['app-plugin-activation-failed'],
|
|
506
|
+
APP_ERROR_HINTS['app-plugin-activation-failed'],
|
|
507
|
+
{ cause },
|
|
508
|
+
),
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
const audioBackend = pluginContext.audio;
|
|
512
|
+
|
|
513
|
+
const executionControl = createLocalExecutionControl(
|
|
514
|
+
executionContext === undefined
|
|
515
|
+
? {
|
|
516
|
+
...createExecutionReport('main-serial', unavailableExecutionCapabilities('not required')),
|
|
517
|
+
actualTier: 'main-serial',
|
|
518
|
+
selectionReason: 'explicit-request',
|
|
519
|
+
sharedEvidencePassed: true,
|
|
520
|
+
engine: { realm: 'host', health: 'idle' },
|
|
521
|
+
world: {
|
|
522
|
+
identity: world.identity,
|
|
523
|
+
health: world.execution.health,
|
|
524
|
+
partialWrite: false,
|
|
525
|
+
retryable: true,
|
|
526
|
+
},
|
|
527
|
+
}
|
|
528
|
+
: {
|
|
529
|
+
...createExecutionReport(
|
|
530
|
+
opts?.execution?.tier ?? 'auto',
|
|
531
|
+
executionContext.capabilities,
|
|
532
|
+
executionContext.selection,
|
|
533
|
+
),
|
|
534
|
+
engine: { realm: 'host', health: 'idle' },
|
|
535
|
+
world: {
|
|
536
|
+
identity: world.identity,
|
|
537
|
+
health: world.execution.health,
|
|
538
|
+
partialWrite: false,
|
|
539
|
+
retryable: true,
|
|
540
|
+
},
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
...(audioBackend === undefined ? {} : { audio: () => audioBackend.getState() }),
|
|
544
|
+
world: () => ({
|
|
545
|
+
identity: world.identity,
|
|
546
|
+
health: world.execution.health,
|
|
547
|
+
partialWrite: world.execution.fault?.partialWrite ?? false,
|
|
548
|
+
retryable: world.execution.fault?.retryable ?? true,
|
|
549
|
+
}),
|
|
550
|
+
},
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
// Step 3.3: Remote eval server auto-start (deferred from Step 2.4 so
|
|
554
|
+
// World is available). Dynamic import keeps @forgeax/engine-app free
|
|
555
|
+
// of static dep on @forgeax/engine-remote. Component reflection crosses
|
|
556
|
+
// this boundary as JSON-safe host data; app does not own any component.
|
|
557
|
+
const remoteHandle = shouldStartRemote
|
|
558
|
+
? await startRemoteServer(world, renderer, assets, rhiCapture, opts?.profiler, executionControl)
|
|
559
|
+
: undefined;
|
|
560
|
+
if (remoteHandle !== undefined) await pluginContext.plugin(remoteServerPlugin(remoteHandle));
|
|
561
|
+
|
|
562
|
+
const buildArgs: BuildAppArgs = {
|
|
563
|
+
renderer,
|
|
564
|
+
assets,
|
|
565
|
+
world,
|
|
566
|
+
pluginContext,
|
|
567
|
+
executionControl,
|
|
568
|
+
onFrameSubmitted: (event) => publishBrowserFrameSubmitted(canvas, event),
|
|
569
|
+
...(inputBackend !== undefined ? { inputBackend } : {}),
|
|
570
|
+
...(inputHandle === undefined
|
|
571
|
+
? {}
|
|
572
|
+
: {
|
|
573
|
+
wireOnLockErrorDispatch: (dispatch: (err: AppError) => void) => {
|
|
574
|
+
inputHandle.setOnErrorDispatch(dispatch);
|
|
575
|
+
},
|
|
576
|
+
}),
|
|
577
|
+
};
|
|
578
|
+
if (audioBackend !== undefined) {
|
|
579
|
+
Object.assign(buildArgs, {
|
|
580
|
+
audioBackend,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
if (opts?.silenceUnhandledErrors !== undefined) {
|
|
584
|
+
Object.assign(buildArgs, { silenceUnhandledErrors: opts.silenceUnhandledErrors });
|
|
585
|
+
}
|
|
586
|
+
// M2 / D-3: canvas form forwards the host-supplied draw-source pull.
|
|
587
|
+
if (opts?.drawSource !== undefined) {
|
|
588
|
+
Object.assign(buildArgs, { drawSource: opts.drawSource });
|
|
589
|
+
}
|
|
590
|
+
if (opts?.profiler !== undefined) {
|
|
591
|
+
Object.assign(buildArgs, { profiler: opts.profiler });
|
|
592
|
+
}
|
|
593
|
+
if (rhiCapture !== undefined) {
|
|
594
|
+
Object.assign(buildArgs, { rhiCapture });
|
|
595
|
+
}
|
|
596
|
+
if (debugDraw !== undefined) {
|
|
597
|
+
Object.assign(buildArgs, { debugDraw });
|
|
598
|
+
}
|
|
599
|
+
if (remoteHandle !== undefined) {
|
|
600
|
+
Object.assign(buildArgs, { remoteHandle });
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// The aspect-sync sidecar belongs to the canvas path because it owns the DOM
|
|
604
|
+
// canvas. It runs as an Update system so it shares World scheduling semantics.
|
|
605
|
+
resetBrowserFrameSubmitted(canvas);
|
|
606
|
+
const built = await buildApp(buildArgs);
|
|
607
|
+
if (!built.ok) {
|
|
608
|
+
await pluginContext.fiber.dispose();
|
|
609
|
+
return built;
|
|
610
|
+
}
|
|
611
|
+
if (built.ok) {
|
|
612
|
+
// DEV-only browser remote bridge (remote-live). A browser cannot host the
|
|
613
|
+
// Node WS server that @forgeax/engine-remote/server needs, so the running
|
|
614
|
+
// engine would be unreachable from a CLI in a real dev browser. Instead the
|
|
615
|
+
// page dials OUT to a loopback relay and runs the ws-free eval core against
|
|
616
|
+
// the live world/renderer/assets/rhiCapture.
|
|
617
|
+
//
|
|
618
|
+
// OPT-IN via VITE_FORGEAX_ENGINE_BRIDGE=1 (set by scripts/dev-live.mjs), NOT
|
|
619
|
+
// on-by-default: a page that dials a relay which is not running makes the
|
|
620
|
+
// BROWSER itself log "WebSocket connection failed" to the console — noise a
|
|
621
|
+
// JS catch cannot suppress — which trips every zero-console-error browser
|
|
622
|
+
// smoke (collectathon / hello-*). So a plain `pnpm --filter <app> dev` (and
|
|
623
|
+
// CI) stays silent; only dev-live.mjs, which also launches the relay, turns
|
|
624
|
+
// it on. Production DCE's the whole block (import.meta.env.DEV === false).
|
|
625
|
+
// Additive: the Node startServer path above and app.remote are untouched.
|
|
626
|
+
if (
|
|
627
|
+
typeof import.meta !== 'undefined' &&
|
|
628
|
+
(import.meta as { env?: { DEV?: boolean; VITE_FORGEAX_ENGINE_BRIDGE?: string } }).env?.DEV ===
|
|
629
|
+
true &&
|
|
630
|
+
(import.meta as { env?: { VITE_FORGEAX_ENGINE_BRIDGE?: string } }).env
|
|
631
|
+
?.VITE_FORGEAX_ENGINE_BRIDGE === '1'
|
|
632
|
+
) {
|
|
633
|
+
const bridgePort =
|
|
634
|
+
(import.meta as { env?: { VITE_FORGEAX_ENGINE_BRIDGE_PORT?: string } }).env
|
|
635
|
+
?.VITE_FORGEAX_ENGINE_BRIDGE_PORT ?? '5733';
|
|
636
|
+
await pluginContext.plugin({
|
|
637
|
+
name: 'browser-remote-bridge',
|
|
638
|
+
inject: ['world', 'renderer', 'assets'],
|
|
639
|
+
async apply(ctx) {
|
|
640
|
+
if (ctx.renderer === undefined || ctx.assets === undefined) {
|
|
641
|
+
throw new Error('browser remote bridge requires renderer and assets services');
|
|
642
|
+
}
|
|
643
|
+
try {
|
|
644
|
+
const bridge = await import('./internal/browser-remote-bridge');
|
|
645
|
+
const teardown = await bridge.installBrowserRemoteBridge({
|
|
646
|
+
world: ctx.world,
|
|
647
|
+
renderer: ctx.renderer,
|
|
648
|
+
assets: ctx.assets,
|
|
649
|
+
runtimeModule: engineRuntimeModule,
|
|
650
|
+
...(rhiCapture !== undefined ? { rhiCapture } : {}),
|
|
651
|
+
...(opts?.profiler !== undefined ? { profiler: opts.profiler } : {}),
|
|
652
|
+
execution: built.value.execution,
|
|
653
|
+
port: bridgePort,
|
|
654
|
+
});
|
|
655
|
+
ctx.effect(() => teardown, 'remote/browser-bridge');
|
|
656
|
+
} catch {
|
|
657
|
+
// The optional live bridge does not make App creation fail.
|
|
658
|
+
}
|
|
659
|
+
},
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return built;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Synchronize a DOM canvas drawing buffer with its CSS size and device pixel
|
|
668
|
+
* ratio. Detached or zero-sized hosts are left untouched so a hidden canvas
|
|
669
|
+
* cannot be converted into a 1x1 render target accidentally.
|
|
670
|
+
*/
|
|
671
|
+
export function syncCanvasDrawingBuffer(
|
|
672
|
+
canvas: Pick<HTMLCanvasElement, 'clientWidth' | 'clientHeight' | 'width' | 'height'> & {
|
|
673
|
+
readonly style?: Pick<CSSStyleDeclaration, 'width' | 'height'>;
|
|
674
|
+
},
|
|
675
|
+
): void {
|
|
676
|
+
if (
|
|
677
|
+
!Number.isFinite(canvas.clientWidth) ||
|
|
678
|
+
!Number.isFinite(canvas.clientHeight) ||
|
|
679
|
+
canvas.clientWidth <= 0 ||
|
|
680
|
+
canvas.clientHeight <= 0
|
|
681
|
+
)
|
|
682
|
+
return;
|
|
683
|
+
// An intrinsic canvas with no CSS dimensions already has an explicit
|
|
684
|
+
// drawing-buffer contract. `clientWidth` mirrors `width` for this shape;
|
|
685
|
+
// multiplying it by DPR would change the caller's requested pixel size and
|
|
686
|
+
// can feed that new width back as the next layout measurement. CSS-sized
|
|
687
|
+
// canvases take the DPR path below (including stylesheet-sized canvases
|
|
688
|
+
// whose layout width differs from the initial intrinsic width).
|
|
689
|
+
const hasExplicitCssSize =
|
|
690
|
+
(canvas.style?.width ?? '') !== '' || (canvas.style?.height ?? '') !== '';
|
|
691
|
+
if (
|
|
692
|
+
!hasExplicitCssSize &&
|
|
693
|
+
canvas.clientWidth === canvas.width &&
|
|
694
|
+
canvas.clientHeight === canvas.height
|
|
695
|
+
) {
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
const dpr = Math.max(1, globalThis.devicePixelRatio || 1);
|
|
699
|
+
const previous = syncedCanvasSizes.get(canvas);
|
|
700
|
+
const drawingBufferIsTheLayoutMeasurement =
|
|
701
|
+
previous !== undefined &&
|
|
702
|
+
canvas.width === previous.drawingWidth &&
|
|
703
|
+
canvas.height === previous.drawingHeight &&
|
|
704
|
+
canvas.clientWidth === previous.drawingWidth &&
|
|
705
|
+
canvas.clientHeight === previous.drawingHeight;
|
|
706
|
+
const cssWidth = drawingBufferIsTheLayoutMeasurement ? previous.cssWidth : canvas.clientWidth;
|
|
707
|
+
const cssHeight = drawingBufferIsTheLayoutMeasurement ? previous.cssHeight : canvas.clientHeight;
|
|
708
|
+
const width = Math.max(1, Math.round(cssWidth * dpr));
|
|
709
|
+
const height = Math.max(1, Math.round(cssHeight * dpr));
|
|
710
|
+
if (canvas.width !== width) canvas.width = width;
|
|
711
|
+
if (canvas.height !== height) canvas.height = height;
|
|
712
|
+
syncedCanvasSizes.set(canvas, {
|
|
713
|
+
cssWidth,
|
|
714
|
+
cssHeight,
|
|
715
|
+
drawingWidth: width,
|
|
716
|
+
drawingHeight: height,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/** Measure a CSS-sized canvas without mutating its drawing buffer. */
|
|
721
|
+
export function measureCanvasDrawingBuffer(
|
|
722
|
+
canvas: Pick<HTMLCanvasElement, 'clientWidth' | 'clientHeight' | 'width' | 'height'> & {
|
|
723
|
+
readonly style?: Pick<CSSStyleDeclaration, 'width' | 'height'>;
|
|
724
|
+
},
|
|
725
|
+
maxCanvasPixelRatio?: number,
|
|
726
|
+
): { readonly width: number; readonly height: number } {
|
|
727
|
+
if (
|
|
728
|
+
!Number.isFinite(canvas.clientWidth) ||
|
|
729
|
+
!Number.isFinite(canvas.clientHeight) ||
|
|
730
|
+
canvas.clientWidth <= 0 ||
|
|
731
|
+
canvas.clientHeight <= 0
|
|
732
|
+
) {
|
|
733
|
+
return { width: canvas.width, height: canvas.height };
|
|
734
|
+
}
|
|
735
|
+
const hasExplicitCssSize =
|
|
736
|
+
(canvas.style?.width ?? '') !== '' || (canvas.style?.height ?? '') !== '';
|
|
737
|
+
if (
|
|
738
|
+
!hasExplicitCssSize &&
|
|
739
|
+
canvas.clientWidth === canvas.width &&
|
|
740
|
+
canvas.clientHeight === canvas.height
|
|
741
|
+
) {
|
|
742
|
+
return { width: canvas.width, height: canvas.height };
|
|
743
|
+
}
|
|
744
|
+
const devicePixelRatio = Math.max(1, globalThis.devicePixelRatio || 1);
|
|
745
|
+
const dpr =
|
|
746
|
+
maxCanvasPixelRatio === undefined ||
|
|
747
|
+
!Number.isFinite(maxCanvasPixelRatio) ||
|
|
748
|
+
maxCanvasPixelRatio <= 0
|
|
749
|
+
? devicePixelRatio
|
|
750
|
+
: Math.min(devicePixelRatio, maxCanvasPixelRatio);
|
|
751
|
+
const previous = syncedCanvasSizes.get(canvas);
|
|
752
|
+
const drawingBufferIsTheLayoutMeasurement =
|
|
753
|
+
previous !== undefined &&
|
|
754
|
+
canvas.width === previous.drawingWidth &&
|
|
755
|
+
canvas.height === previous.drawingHeight &&
|
|
756
|
+
canvas.clientWidth === previous.drawingWidth &&
|
|
757
|
+
canvas.clientHeight === previous.drawingHeight;
|
|
758
|
+
const cssWidth = drawingBufferIsTheLayoutMeasurement ? previous.cssWidth : canvas.clientWidth;
|
|
759
|
+
const cssHeight = drawingBufferIsTheLayoutMeasurement ? previous.cssHeight : canvas.clientHeight;
|
|
760
|
+
return {
|
|
761
|
+
width: Math.max(1, Math.round(cssWidth * dpr)),
|
|
762
|
+
height: Math.max(1, Math.round(cssHeight * dpr)),
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const syncedCanvasSizes = new WeakMap<
|
|
767
|
+
object,
|
|
768
|
+
{
|
|
769
|
+
readonly cssWidth: number;
|
|
770
|
+
readonly cssHeight: number;
|
|
771
|
+
readonly drawingWidth: number;
|
|
772
|
+
readonly drawingHeight: number;
|
|
773
|
+
}
|
|
774
|
+
>();
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Per-frame aspect-sync body for the createApp(canvas) path (feat-20260617
|
|
778
|
+
* M3 / w13). Walks every Camera entity and, for perspective cameras with
|
|
779
|
+
* `autoAspect === true`, writes `canvasW / canvasH` into `Camera.aspect`.
|
|
780
|
+
*
|
|
781
|
+
* Read discipline (D-5 / research Finding 2): `autoAspect` is read through
|
|
782
|
+
* `world.get` (the readRow path narrows the bool column to a JS boolean).
|
|
783
|
+
* The query bundle is used only to enumerate the entity handles -- reading
|
|
784
|
+
* the bool column off the bundle would return a raw 0/1 number, so a
|
|
785
|
+
* `!== 0` test is always true (the
|
|
786
|
+
* bool-field-compared-with-not-equal-zero-always-true trap).
|
|
787
|
+
*
|
|
788
|
+
* Best-effort + side-effect-isolated:
|
|
789
|
+
* - canvas size 0 (detached / display:none) -> skip entirely so `aspect`
|
|
790
|
+
* never becomes NaN / 0.
|
|
791
|
+
* - orthographic cameras and `autoAspect === false` cameras are left
|
|
792
|
+
* untouched.
|
|
793
|
+
*
|
|
794
|
+
* @see {@link https://github.com/Ubpa/forgeax-engine/blob/main/docs/how-to/2026-06-18-host-engine-contract.md | Host-engine contract SSOT}
|
|
795
|
+
*/
|
|
796
|
+
export function syncCameraAspect(world: World, canvasW: number, canvasH: number): void {
|
|
797
|
+
// Guard against detached / zero-sized canvases: a 0 width or height would
|
|
798
|
+
// write NaN (0 / 0) or 0 into aspect and corrupt the projection matrix.
|
|
799
|
+
if (canvasW <= 0 || canvasH <= 0) return;
|
|
800
|
+
const aspect = canvasW / canvasH;
|
|
801
|
+
|
|
802
|
+
const query = world.query({ with: [Camera] }).unwrap();
|
|
803
|
+
for (const row of query) {
|
|
804
|
+
const entity = row.entity;
|
|
805
|
+
const r = world.get(entity, Camera);
|
|
806
|
+
if (!r.ok) continue;
|
|
807
|
+
// world.get narrows the bool column to a real boolean (D-5); the
|
|
808
|
+
// perspective discriminator is the numeric column value.
|
|
809
|
+
if (r.value.autoAspect !== true) continue;
|
|
810
|
+
if (r.value.projection !== CAMERA_PROJECTION_PERSPECTIVE) continue;
|
|
811
|
+
world.set(entity, Camera, { aspect });
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
async function startRemoteServer(
|
|
816
|
+
world: World,
|
|
817
|
+
renderer: Renderer,
|
|
818
|
+
assets: AssetRegistry | undefined,
|
|
819
|
+
rhiCapture: RhiCapture | undefined,
|
|
820
|
+
profiler: import('@forgeax/engine-profiler').Profiler | undefined,
|
|
821
|
+
execution: ExecutionControl,
|
|
822
|
+
): Promise<{ readonly port: number; close(): Promise<void> } | undefined> {
|
|
823
|
+
try {
|
|
824
|
+
const remoteServerMod = (await import(
|
|
825
|
+
/* @vite-ignore */ '@forgeax/engine-remote/server'
|
|
826
|
+
)) as unknown as {
|
|
827
|
+
startServer: (opts: {
|
|
828
|
+
port: number;
|
|
829
|
+
host?: string;
|
|
830
|
+
world: unknown;
|
|
831
|
+
renderer?: unknown;
|
|
832
|
+
assets?: unknown;
|
|
833
|
+
rhiCapture?: unknown;
|
|
834
|
+
introspection?: readonly unknown[];
|
|
835
|
+
profiler?: unknown;
|
|
836
|
+
execution?: unknown;
|
|
837
|
+
}) => Promise<{
|
|
838
|
+
ok: boolean;
|
|
839
|
+
value?: { port: number; close(): Promise<void> };
|
|
840
|
+
}>;
|
|
841
|
+
};
|
|
842
|
+
const serverResult = await remoteServerMod.startServer({
|
|
843
|
+
port: 0,
|
|
844
|
+
host: '127.0.0.1',
|
|
845
|
+
world,
|
|
846
|
+
renderer,
|
|
847
|
+
assets,
|
|
848
|
+
introspection: projectComponentIntrospection(world.components.entries()),
|
|
849
|
+
...(rhiCapture !== undefined ? { rhiCapture } : {}),
|
|
850
|
+
...(profiler !== undefined ? { profiler } : {}),
|
|
851
|
+
execution,
|
|
852
|
+
});
|
|
853
|
+
if (serverResult.ok && serverResult.value !== undefined) {
|
|
854
|
+
return { port: serverResult.value.port, close: serverResult.value.close };
|
|
855
|
+
}
|
|
856
|
+
} catch (_error) {
|
|
857
|
+
// Dynamic import or server start failed; the app continues without remote.
|
|
858
|
+
}
|
|
859
|
+
return undefined;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
async function createAppFromAssemble(
|
|
863
|
+
args: AppAssembleArgs,
|
|
864
|
+
): Promise<Result<App, AssembleAppError>> {
|
|
865
|
+
let pluginContext: Context;
|
|
866
|
+
try {
|
|
867
|
+
pluginContext = await createWorldContext(
|
|
868
|
+
args.world,
|
|
869
|
+
assembledEngineProfile({
|
|
870
|
+
renderer: args.renderer,
|
|
871
|
+
...(args.assets === undefined ? {} : { assets: args.assets }),
|
|
872
|
+
extensions: args.plugins ?? [],
|
|
873
|
+
}),
|
|
874
|
+
);
|
|
875
|
+
} catch (cause) {
|
|
876
|
+
return err(
|
|
877
|
+
makeAppError(
|
|
878
|
+
'app-plugin-activation-failed',
|
|
879
|
+
APP_EXPECTED['app-plugin-activation-failed'],
|
|
880
|
+
APP_ERROR_HINTS['app-plugin-activation-failed'],
|
|
881
|
+
{ cause },
|
|
882
|
+
),
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const shouldStartRemote = resolveRemoteServeFlag(
|
|
887
|
+
undefined,
|
|
888
|
+
(globalThis as { process?: { env?: { FORGEAX_ENGINE_REMOTE_SERVE?: string } } }).process?.env,
|
|
889
|
+
);
|
|
890
|
+
const assembledAudioBackend = pluginContext.audio;
|
|
891
|
+
const executionControl = createLocalExecutionControl(
|
|
892
|
+
{
|
|
893
|
+
...createExecutionReport('main-serial', unavailableExecutionCapabilities('not required')),
|
|
894
|
+
actualTier: 'main-serial',
|
|
895
|
+
selectionReason: 'explicit-request',
|
|
896
|
+
sharedEvidencePassed: true,
|
|
897
|
+
engine: { realm: 'host', health: 'idle' },
|
|
898
|
+
world: {
|
|
899
|
+
identity: args.world.identity,
|
|
900
|
+
health: args.world.execution.health,
|
|
901
|
+
partialWrite: false,
|
|
902
|
+
retryable: true,
|
|
903
|
+
},
|
|
904
|
+
},
|
|
905
|
+
{
|
|
906
|
+
...(assembledAudioBackend === undefined
|
|
907
|
+
? {}
|
|
908
|
+
: { audio: () => assembledAudioBackend.getState() }),
|
|
909
|
+
world: () => ({
|
|
910
|
+
identity: args.world.identity,
|
|
911
|
+
health: args.world.execution.health,
|
|
912
|
+
partialWrite: args.world.execution.fault?.partialWrite ?? false,
|
|
913
|
+
retryable: args.world.execution.fault?.retryable ?? true,
|
|
914
|
+
}),
|
|
915
|
+
},
|
|
916
|
+
);
|
|
917
|
+
const remoteHandle = shouldStartRemote
|
|
918
|
+
? await startRemoteServer(
|
|
919
|
+
args.world,
|
|
920
|
+
args.renderer,
|
|
921
|
+
args.assets,
|
|
922
|
+
undefined,
|
|
923
|
+
args.profiler,
|
|
924
|
+
executionControl,
|
|
925
|
+
)
|
|
926
|
+
: undefined;
|
|
927
|
+
if (remoteHandle !== undefined) await pluginContext.plugin(remoteServerPlugin(remoteHandle));
|
|
928
|
+
|
|
929
|
+
const buildArgs: BuildAppArgs = {
|
|
930
|
+
renderer: args.renderer,
|
|
931
|
+
world: args.world,
|
|
932
|
+
pluginContext,
|
|
933
|
+
executionControl,
|
|
934
|
+
...(remoteHandle !== undefined ? { remoteHandle } : {}),
|
|
935
|
+
};
|
|
936
|
+
|
|
937
|
+
const assembledInputBackend = pluginContext.input;
|
|
938
|
+
if (assembledInputBackend !== undefined) {
|
|
939
|
+
Object.assign(buildArgs, {
|
|
940
|
+
inputBackend: assembledInputBackend,
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
if (assembledAudioBackend !== undefined) {
|
|
944
|
+
Object.assign(buildArgs, {
|
|
945
|
+
audioBackend: assembledAudioBackend,
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
if (args.silenceUnhandledErrors !== undefined) {
|
|
949
|
+
Object.assign(buildArgs, { silenceUnhandledErrors: args.silenceUnhandledErrors });
|
|
950
|
+
}
|
|
951
|
+
// M2 / D-3: assemble form forwards the host-supplied draw-source pull.
|
|
952
|
+
if (args.drawSource !== undefined) {
|
|
953
|
+
Object.assign(buildArgs, { drawSource: args.drawSource });
|
|
954
|
+
}
|
|
955
|
+
if (args.profiler !== undefined) {
|
|
956
|
+
Object.assign(buildArgs, { profiler: args.profiler });
|
|
957
|
+
}
|
|
958
|
+
const built = await buildApp(buildArgs);
|
|
959
|
+
if (!built.ok) await pluginContext.fiber.dispose();
|
|
960
|
+
return built;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
interface BuildAppArgs {
|
|
964
|
+
readonly renderer: Renderer;
|
|
965
|
+
readonly assets?: AssetRegistry;
|
|
966
|
+
readonly world: World;
|
|
967
|
+
readonly pluginContext: Context;
|
|
968
|
+
readonly inputBackend?: InputBackend;
|
|
969
|
+
/**
|
|
970
|
+
* M2 D-4: callback that buildApp calls after creating the ErrorFanoutRegistry
|
|
971
|
+
* dispatch function. The input handle's onLockError callback needs the dispatch
|
|
972
|
+
* function to fan out 'app-pointer-lock-failed' errors, but the dispatch function
|
|
973
|
+
* is created inside buildApp (after the input handle is already constructed).
|
|
974
|
+
* This callback bridges the gap.
|
|
975
|
+
*/
|
|
976
|
+
readonly wireOnLockErrorDispatch?: (dispatch: (err: AppError) => void) => void;
|
|
977
|
+
readonly audioBackend?: AudioBackend;
|
|
978
|
+
readonly silenceUnhandledErrors?: boolean;
|
|
979
|
+
/** Optional App-owned RHI capture capability. */
|
|
980
|
+
readonly rhiCapture?: RhiCapture;
|
|
981
|
+
/** feat-20260615 debug-draw M5: DebugDraw instance created by createDebugDrawOnReady. */
|
|
982
|
+
readonly debugDraw?: DebugDraw;
|
|
983
|
+
/** feat-20260629 M4 / w20: remote eval server handle from createAppFromCanvas. */
|
|
984
|
+
readonly remoteHandle?: { readonly port: number; close(): Promise<void> };
|
|
985
|
+
/**
|
|
986
|
+
* feat-20260709-editor-world-partition-editorworld-super-composite / M2 / D-3:
|
|
987
|
+
* per-frame draw-source pull forwarded verbatim into the frame-loop. Absent =>
|
|
988
|
+
* the allocation-free primary-World path. Both createApp forms (canvas + assemble) forward
|
|
989
|
+
* it from their respective options object.
|
|
990
|
+
*/
|
|
991
|
+
readonly drawSource?: () =>
|
|
992
|
+
| {
|
|
993
|
+
worlds: readonly import('@forgeax/engine-ecs').World[];
|
|
994
|
+
cameraOwner: number;
|
|
995
|
+
resourceOwner: number;
|
|
996
|
+
}
|
|
997
|
+
| undefined;
|
|
998
|
+
readonly profiler?: import('@forgeax/engine-profiler').Profiler;
|
|
999
|
+
readonly executionControl?: import('./execution/control').LocalExecutionControl;
|
|
1000
|
+
/** Canvas-form projection of the Renderer frame-submitted event. */
|
|
1001
|
+
readonly onFrameSubmitted?: (event: {
|
|
1002
|
+
readonly frameId: number;
|
|
1003
|
+
readonly deviceGeneration: number;
|
|
1004
|
+
}) => void;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Internal builder shared by both overloads. Wires the frame-loop +
|
|
1009
|
+
* listener-registry onError and returns the App handle. Effectful ownership
|
|
1010
|
+
* has already moved into pluginContext before this boundary runs.
|
|
1011
|
+
*/
|
|
1012
|
+
async function buildApp(args: BuildAppArgs): Promise<Result<App, AppError | RhiError>> {
|
|
1013
|
+
const {
|
|
1014
|
+
renderer,
|
|
1015
|
+
world,
|
|
1016
|
+
pluginContext,
|
|
1017
|
+
inputBackend,
|
|
1018
|
+
audioBackend,
|
|
1019
|
+
silenceUnhandledErrors,
|
|
1020
|
+
rhiCapture,
|
|
1021
|
+
debugDraw,
|
|
1022
|
+
remoteHandle,
|
|
1023
|
+
profiler,
|
|
1024
|
+
} = args;
|
|
1025
|
+
|
|
1026
|
+
// Physics is a Context service; the App exposes the active provider without
|
|
1027
|
+
// storing a second mutable slot.
|
|
1028
|
+
function readPhysicsWorld():
|
|
1029
|
+
| import('@forgeax/engine-physics').PhysicsWorld
|
|
1030
|
+
| import('@forgeax/engine-physics').PhysicsWorld2D
|
|
1031
|
+
| undefined {
|
|
1032
|
+
return pluginContext.physics;
|
|
1033
|
+
}
|
|
1034
|
+
// M4 (w11): listener registry replaces the M3 inline Set so console.error
|
|
1035
|
+
// fallback + duplicate-add no-op + unsubscribe handle behaviour matches
|
|
1036
|
+
// packages/runtime/src/createRenderer.ts:532-566 LostListenerRegistry
|
|
1037
|
+
// (plan-strategy D-9). silenceUnhandledErrors threads through verbatim.
|
|
1038
|
+
const fanout = new ErrorFanoutRegistry(
|
|
1039
|
+
silenceUnhandledErrors !== undefined ? { silenceUnhandledErrors } : {},
|
|
1040
|
+
);
|
|
1041
|
+
let lastError: AppDispatchError | undefined;
|
|
1042
|
+
const execution =
|
|
1043
|
+
args.executionControl ??
|
|
1044
|
+
createLocalExecutionControl(
|
|
1045
|
+
{
|
|
1046
|
+
...createExecutionReport('main-serial', unavailableExecutionCapabilities('not required')),
|
|
1047
|
+
actualTier: 'main-serial',
|
|
1048
|
+
selectionReason: 'explicit-request',
|
|
1049
|
+
sharedEvidencePassed: true,
|
|
1050
|
+
engine: { realm: 'host', health: 'idle' },
|
|
1051
|
+
world: {
|
|
1052
|
+
identity: world.identity,
|
|
1053
|
+
health: world.execution.health,
|
|
1054
|
+
partialWrite: world.execution.fault?.partialWrite ?? false,
|
|
1055
|
+
retryable: world.execution.fault?.retryable ?? true,
|
|
1056
|
+
},
|
|
1057
|
+
},
|
|
1058
|
+
{
|
|
1059
|
+
...(audioBackend === undefined ? {} : { audio: () => audioBackend.getState() }),
|
|
1060
|
+
world: () => ({
|
|
1061
|
+
identity: world.identity,
|
|
1062
|
+
health: world.execution.health,
|
|
1063
|
+
partialWrite: world.execution.fault?.partialWrite ?? false,
|
|
1064
|
+
retryable: world.execution.fault?.retryable ?? true,
|
|
1065
|
+
}),
|
|
1066
|
+
},
|
|
1067
|
+
);
|
|
1068
|
+
|
|
1069
|
+
function dispatch(e: AppDispatchError): void {
|
|
1070
|
+
lastError = e;
|
|
1071
|
+
fanout.fire(e);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// M2 D-4: wire the input handle's onLockError callback to the error fan-out.
|
|
1075
|
+
// The dispatch function is created here; the input handle was created earlier
|
|
1076
|
+
// in createAppFromCanvas and passed to buildApp with a wireOnLockErrorDispatch
|
|
1077
|
+
// callback that calls setOnErrorDispatch on the handle.
|
|
1078
|
+
if (args.wireOnLockErrorDispatch) {
|
|
1079
|
+
args.wireOnLockErrorDispatch(dispatch);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
const loopOpts: Parameters<typeof createFrameLoop>[0] = {
|
|
1083
|
+
world,
|
|
1084
|
+
renderer,
|
|
1085
|
+
onError: dispatch,
|
|
1086
|
+
};
|
|
1087
|
+
// M2 / D-3: forward the optional draw-source pull into the frame-loop. Absent
|
|
1088
|
+
// => the loop keeps the allocation-free primary-World draw path.
|
|
1089
|
+
if (args.drawSource !== undefined) {
|
|
1090
|
+
Object.assign(loopOpts, { drawSource: args.drawSource });
|
|
1091
|
+
}
|
|
1092
|
+
if (profiler !== undefined) {
|
|
1093
|
+
Object.assign(loopOpts, { profiler });
|
|
1094
|
+
}
|
|
1095
|
+
const loop = createFrameLoop(loopOpts);
|
|
1096
|
+
if (rhiCapture !== undefined) {
|
|
1097
|
+
bindRhiCaptureFrameDriver(rhiCapture, {
|
|
1098
|
+
getState: () => loop.getState(),
|
|
1099
|
+
pause: () => {
|
|
1100
|
+
const result = loop.pause();
|
|
1101
|
+
if (result.ok) execution.setEngineHealth('idle');
|
|
1102
|
+
return result;
|
|
1103
|
+
},
|
|
1104
|
+
resume: () => {
|
|
1105
|
+
const result = loop.resume();
|
|
1106
|
+
if (result.ok) execution.setEngineHealth('running');
|
|
1107
|
+
return result;
|
|
1108
|
+
},
|
|
1109
|
+
stepFrame: (deltaSeconds) => loop.stepFrame(deltaSeconds),
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
// M4 (w13) device-lost internal subscription. R-1 timing contract:
|
|
1113
|
+
// app.start() arms the rAF handle BEFORE this listener subscribes, so
|
|
1114
|
+
// a synchronous late-attach replay of a persisted device-lost event
|
|
1115
|
+
// (LostListenerRegistry replay -- runtime/src/renderer.ts:337-345)
|
|
1116
|
+
// hits a frame-loop with a real rAF handle to cancel (M2 setStopped
|
|
1117
|
+
// tolerates pendingFrameId === 0 as a no-op so even pre-rAF replays
|
|
1118
|
+
// do not NPE). The subscription remains active across pause / resume;
|
|
1119
|
+
// unsubscribe runs only on stop / disposal (charter P3:
|
|
1120
|
+
// device-lost is a terminal lifecycle signal, not a transient blip).
|
|
1121
|
+
let rendererUnsubscribe: (() => void) | undefined;
|
|
1122
|
+
let resumeAfterSurfaceRestore = false;
|
|
1123
|
+
|
|
1124
|
+
function subscribeRendererErrors(): void {
|
|
1125
|
+
if (rendererUnsubscribe !== undefined) {
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
rendererUnsubscribe = renderer.subscribe((event) => {
|
|
1129
|
+
if (event.kind === 'frame-submitted') {
|
|
1130
|
+
args.onFrameSubmitted?.(event);
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
if (event.kind !== 'error') return;
|
|
1134
|
+
const e: RenderError = event.error;
|
|
1135
|
+
dispatch(e);
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function unsubscribeRendererErrors(): void {
|
|
1140
|
+
if (rendererUnsubscribe !== undefined) {
|
|
1141
|
+
rendererUnsubscribe();
|
|
1142
|
+
rendererUnsubscribe = undefined;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
const stub: App = {
|
|
1147
|
+
renderer,
|
|
1148
|
+
...(args.assets === undefined ? {} : { assets: args.assets }),
|
|
1149
|
+
world,
|
|
1150
|
+
execution,
|
|
1151
|
+
async releaseSurfacePreserveWorld(): Promise<Result<void, RhiError | RenderError>> {
|
|
1152
|
+
if (loop.getState() === 'running') {
|
|
1153
|
+
const paused = loop.pause();
|
|
1154
|
+
if (!paused.ok) {
|
|
1155
|
+
return err(
|
|
1156
|
+
new RhiError({
|
|
1157
|
+
code: 'rhi-not-available',
|
|
1158
|
+
expected: 'running App pauses before releasing its presentation surface',
|
|
1159
|
+
hint: paused.error.hint,
|
|
1160
|
+
}),
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
resumeAfterSurfaceRestore = true;
|
|
1164
|
+
execution.setEngineHealth('idle');
|
|
1165
|
+
}
|
|
1166
|
+
// A surface handoff can be requested by an Update system while the
|
|
1167
|
+
// current frame is still in progress. Let that frame reach renderer.draw
|
|
1168
|
+
// before unconfiguring the surface; pause() only cancels the next rAF.
|
|
1169
|
+
await Promise.resolve();
|
|
1170
|
+
const released = renderer.releaseSurface();
|
|
1171
|
+
if (!released.ok && resumeAfterSurfaceRestore) {
|
|
1172
|
+
loop.resume();
|
|
1173
|
+
resumeAfterSurfaceRestore = false;
|
|
1174
|
+
execution.setEngineHealth('running');
|
|
1175
|
+
}
|
|
1176
|
+
return released.ok ? ok(undefined) : err(released.error);
|
|
1177
|
+
},
|
|
1178
|
+
async restoreSurface(): Promise<Result<void, RhiError | RenderError>> {
|
|
1179
|
+
const restored = renderer.restoreSurface();
|
|
1180
|
+
if (!restored.ok) return err(restored.error);
|
|
1181
|
+
if (resumeAfterSurfaceRestore) {
|
|
1182
|
+
const resumed = loop.resume();
|
|
1183
|
+
if (!resumed.ok) {
|
|
1184
|
+
return err(
|
|
1185
|
+
new RhiError({
|
|
1186
|
+
code: 'rhi-not-available',
|
|
1187
|
+
expected: 'paused App resumes after restoring its presentation surface',
|
|
1188
|
+
hint: resumed.error.hint,
|
|
1189
|
+
}),
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
resumeAfterSurfaceRestore = false;
|
|
1193
|
+
execution.setEngineHealth('running');
|
|
1194
|
+
}
|
|
1195
|
+
return ok(undefined);
|
|
1196
|
+
},
|
|
1197
|
+
pluginContext,
|
|
1198
|
+
...(inputBackend !== undefined ? { input: inputBackend } : {}),
|
|
1199
|
+
...(audioBackend !== undefined ? { audio: audioBackend } : {}),
|
|
1200
|
+
get physics():
|
|
1201
|
+
| import('@forgeax/engine-physics').PhysicsWorld
|
|
1202
|
+
| import('@forgeax/engine-physics').PhysicsWorld2D
|
|
1203
|
+
| undefined {
|
|
1204
|
+
return readPhysicsWorld();
|
|
1205
|
+
},
|
|
1206
|
+
start(): Result<void, AppError> {
|
|
1207
|
+
// R-1: arm the rAF handle FIRST (loop.start schedules raf(tick))
|
|
1208
|
+
// and only THEN subscribe to renderer events. If the renderer
|
|
1209
|
+
// late-attach replays a persisted device-lost event during the
|
|
1210
|
+
// subscribe call, the loop is already armed and can retain the error.
|
|
1211
|
+
const r = loop.start();
|
|
1212
|
+
if (r.ok) {
|
|
1213
|
+
execution.setEngineHealth('running');
|
|
1214
|
+
subscribeRendererErrors();
|
|
1215
|
+
}
|
|
1216
|
+
return r;
|
|
1217
|
+
},
|
|
1218
|
+
stop(): Result<void, AppError> {
|
|
1219
|
+
const r = loop.stop();
|
|
1220
|
+
if (r.ok) execution.setEngineHealth('stopped');
|
|
1221
|
+
unsubscribeRendererErrors();
|
|
1222
|
+
return r;
|
|
1223
|
+
},
|
|
1224
|
+
async dispose(): Promise<Result<void, AppError>> {
|
|
1225
|
+
const state = loop.getState();
|
|
1226
|
+
if (state === 'running' || state === 'paused') loop.stop();
|
|
1227
|
+
else if (state !== 'stopped') loop.setStopped();
|
|
1228
|
+
await pluginContext.fiber.dispose();
|
|
1229
|
+
unsubscribeRendererErrors();
|
|
1230
|
+
execution.setEngineHealth('stopped');
|
|
1231
|
+
return ok(undefined);
|
|
1232
|
+
},
|
|
1233
|
+
pause(): Result<void, AppError> {
|
|
1234
|
+
const r = loop.pause();
|
|
1235
|
+
if (r.ok) execution.setEngineHealth('idle');
|
|
1236
|
+
return r;
|
|
1237
|
+
},
|
|
1238
|
+
resume(): Result<void, AppError> {
|
|
1239
|
+
const r = loop.resume();
|
|
1240
|
+
if (r.ok) execution.setEngineHealth('running');
|
|
1241
|
+
return r;
|
|
1242
|
+
},
|
|
1243
|
+
stepFrame(deltaSeconds): Result<void, AppDispatchError> {
|
|
1244
|
+
return loop.stepFrame(deltaSeconds);
|
|
1245
|
+
},
|
|
1246
|
+
onError(cb: (e: AppDispatchError) => void): () => void {
|
|
1247
|
+
return fanout.add(cb);
|
|
1248
|
+
},
|
|
1249
|
+
setDrawSource(drawSource): void {
|
|
1250
|
+
loop.setDrawSource(drawSource);
|
|
1251
|
+
},
|
|
1252
|
+
/**
|
|
1253
|
+
* Most recent dispatched error retained for host self-inspection.
|
|
1254
|
+
*/
|
|
1255
|
+
get lastError(): AppDispatchError | undefined {
|
|
1256
|
+
return lastError;
|
|
1257
|
+
},
|
|
1258
|
+
...(rhiCapture !== undefined ? { rhiCapture } : {}),
|
|
1259
|
+
...(debugDraw !== undefined ? { debugDraw } : {}),
|
|
1260
|
+
...(remoteHandle !== undefined ? { remote: remoteHandle } : {}),
|
|
1261
|
+
};
|
|
1262
|
+
|
|
1263
|
+
return ok(stub);
|
|
1264
|
+
}
|