@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,539 @@
|
|
|
1
|
+
import type { World } from '@forgeax/engine-ecs';
|
|
2
|
+
import type { ProfileFrameToken, Profiler, RecorderSession } from '@forgeax/engine-profiler';
|
|
3
|
+
import type { RenderError, Renderer, RenderWorldLease } from '@forgeax/engine-render';
|
|
4
|
+
import { err, ok, type Result } from '@forgeax/engine-types';
|
|
5
|
+
|
|
6
|
+
import type { AppErrorCode, AppErrorDetailFor } from '../errors';
|
|
7
|
+
import { AppError } from '../errors';
|
|
8
|
+
import { APP_PHASE_CATALOG } from '../types';
|
|
9
|
+
|
|
10
|
+
export type FrameState = 'idle' | 'running' | 'paused' | 'stopped';
|
|
11
|
+
|
|
12
|
+
export interface FrameLoopOptions {
|
|
13
|
+
readonly world: World;
|
|
14
|
+
readonly renderer: Renderer;
|
|
15
|
+
readonly onError?: (e: AppError | RenderError) => void;
|
|
16
|
+
/** Optional dev-only RHI recorder hook; called once after each frame draw. */
|
|
17
|
+
readonly debugRhi?: { onFrameEnd(): void };
|
|
18
|
+
readonly now?: () => number;
|
|
19
|
+
readonly raf?: (cb: (t: number) => void) => number;
|
|
20
|
+
readonly caf?: (id: number) => void;
|
|
21
|
+
readonly profiler?: Profiler;
|
|
22
|
+
readonly drawSource?: () =>
|
|
23
|
+
| { worlds: readonly World[]; cameraOwner: number; resourceOwner: number }
|
|
24
|
+
| undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface FrameLoopHandle {
|
|
28
|
+
start(): Result<void, AppError>;
|
|
29
|
+
stop(): Result<void, AppError>;
|
|
30
|
+
pause(): Result<void, AppError>;
|
|
31
|
+
resume(): Result<void, AppError>;
|
|
32
|
+
/** Run one complete update/draw frame through this loop while paused. */
|
|
33
|
+
stepFrame(deltaSeconds: number): Result<void, AppError | RenderError>;
|
|
34
|
+
/** Replace the per-frame world routing pull without replacing the loop. */
|
|
35
|
+
setDrawSource(drawSource: FrameLoopOptions['drawSource']): void;
|
|
36
|
+
getState(): FrameState;
|
|
37
|
+
setStopped(): void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function beginFrame(session: RecorderSession | undefined, frameId: number): boolean {
|
|
41
|
+
if (session === undefined) return false;
|
|
42
|
+
try {
|
|
43
|
+
return session.beginFrame(frameId).ok;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function beginPhase(session: RecorderSession | undefined, phase: string): boolean {
|
|
50
|
+
if (session === undefined) return false;
|
|
51
|
+
try {
|
|
52
|
+
return session.beginPhase('app', phase).ok;
|
|
53
|
+
} catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function endPhase(session: RecorderSession | undefined): void {
|
|
59
|
+
if (session === undefined) return;
|
|
60
|
+
try {
|
|
61
|
+
session.endPhase();
|
|
62
|
+
} catch {
|
|
63
|
+
// Profiler failures never alter the host loop.
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function endFrame(session: RecorderSession | undefined): void {
|
|
68
|
+
if (session === undefined) return;
|
|
69
|
+
try {
|
|
70
|
+
session.endFrame();
|
|
71
|
+
} catch {
|
|
72
|
+
// Profiler failures never alter the host loop.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function finishProfilerCapture(profiler: Profiler | undefined): void {
|
|
77
|
+
const session = profiler?.activeSession();
|
|
78
|
+
if (session === undefined) return;
|
|
79
|
+
try {
|
|
80
|
+
session.finish();
|
|
81
|
+
} catch {
|
|
82
|
+
// Profiler failures never alter the host stop transition.
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function makeAppError<C extends AppErrorCode>(
|
|
87
|
+
code: C,
|
|
88
|
+
expected: string,
|
|
89
|
+
hint: string,
|
|
90
|
+
detail: AppErrorDetailFor<C>,
|
|
91
|
+
): AppError {
|
|
92
|
+
return new AppError({ code, expected, hint, detail }) as AppError;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function makeWorldUpdateError(cause: unknown): AppError {
|
|
96
|
+
return makeAppError(
|
|
97
|
+
'app-system-update-failed',
|
|
98
|
+
'world.update(deltaSeconds) completes successfully',
|
|
99
|
+
'check detail.cause for the original structured ECS error',
|
|
100
|
+
{ cause },
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function fireWorldUpdateResult(
|
|
105
|
+
result: ReturnType<World['update']>,
|
|
106
|
+
fireError: ((e: AppError | RenderError) => void) | undefined,
|
|
107
|
+
): boolean {
|
|
108
|
+
if (!result.ok && fireError !== undefined) {
|
|
109
|
+
fireError(makeWorldUpdateError(result.error));
|
|
110
|
+
}
|
|
111
|
+
return result.ok;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function updateInjectedWorlds(
|
|
115
|
+
worlds: readonly World[],
|
|
116
|
+
ownWorld: World,
|
|
117
|
+
deltaSeconds: number,
|
|
118
|
+
fireError: ((e: AppError | RenderError) => void) | undefined,
|
|
119
|
+
attachedWorlds: ReadonlySet<World>,
|
|
120
|
+
updatedWorlds: Set<World>,
|
|
121
|
+
): void {
|
|
122
|
+
for (const injectedWorld of worlds) {
|
|
123
|
+
if (
|
|
124
|
+
injectedWorld === ownWorld ||
|
|
125
|
+
updatedWorlds.has(injectedWorld) ||
|
|
126
|
+
!attachedWorlds.has(injectedWorld)
|
|
127
|
+
) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
if (fireWorldUpdateResult(injectedWorld.update(deltaSeconds), fireError)) {
|
|
132
|
+
updatedWorlds.add(injectedWorld);
|
|
133
|
+
}
|
|
134
|
+
} catch (cause: unknown) {
|
|
135
|
+
if (fireError !== undefined) fireError(makeWorldUpdateError(cause));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function resolveNow(opts: FrameLoopOptions): () => number {
|
|
141
|
+
if (opts.now !== undefined) return opts.now;
|
|
142
|
+
return () => {
|
|
143
|
+
const perf = (globalThis as { performance?: { now?: () => number } }).performance;
|
|
144
|
+
const fn = perf?.now;
|
|
145
|
+
return typeof fn === 'function' ? fn.call(perf) : Date.now();
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function resolveRaf(opts: FrameLoopOptions): (cb: (t: number) => void) => number {
|
|
150
|
+
if (opts.raf !== undefined) return opts.raf;
|
|
151
|
+
const g = globalThis as { requestAnimationFrame?: (cb: (t: number) => void) => number };
|
|
152
|
+
return typeof g.requestAnimationFrame === 'function'
|
|
153
|
+
? g.requestAnimationFrame.bind(globalThis)
|
|
154
|
+
: () => 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function resolveCaf(opts: FrameLoopOptions): (id: number) => void {
|
|
158
|
+
if (opts.caf !== undefined) return opts.caf;
|
|
159
|
+
const g = globalThis as { cancelAnimationFrame?: (id: number) => void };
|
|
160
|
+
return typeof g.cancelAnimationFrame === 'function'
|
|
161
|
+
? g.cancelAnimationFrame.bind(globalThis)
|
|
162
|
+
: () => {};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function createFrameLoop(opts: FrameLoopOptions): FrameLoopHandle {
|
|
166
|
+
const { world, renderer } = opts;
|
|
167
|
+
const phaseCatalogRegistration = opts.profiler?.registerPhaseCatalog('app', APP_PHASE_CATALOG);
|
|
168
|
+
let releasePhaseCatalog =
|
|
169
|
+
phaseCatalogRegistration?.ok === true ? phaseCatalogRegistration.value : undefined;
|
|
170
|
+
let drawSource = opts.drawSource;
|
|
171
|
+
const now = resolveNow(opts);
|
|
172
|
+
const raf = resolveRaf(opts);
|
|
173
|
+
const caf = resolveCaf(opts);
|
|
174
|
+
|
|
175
|
+
let state: FrameState = 'idle';
|
|
176
|
+
let lastTimestamp = 0;
|
|
177
|
+
let pendingFrameId = 0;
|
|
178
|
+
let profilerFrameId = 0;
|
|
179
|
+
let profilerCaptureId: string | undefined;
|
|
180
|
+
const leases = new Map<World, RenderWorldLease>();
|
|
181
|
+
|
|
182
|
+
function attachPrimary(fireError: (e: AppError | RenderError) => void): void {
|
|
183
|
+
if (leases.has(world)) return;
|
|
184
|
+
try {
|
|
185
|
+
const result = renderer.attach(world);
|
|
186
|
+
if (result.ok) leases.set(world, result.value);
|
|
187
|
+
else fireError(result.error);
|
|
188
|
+
} catch (cause: unknown) {
|
|
189
|
+
fireError(makeWorldUpdateError(cause));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function syncInjectedAttachments(
|
|
194
|
+
worlds: readonly World[] | undefined,
|
|
195
|
+
fireError?: (e: AppError | RenderError) => void,
|
|
196
|
+
): ReadonlySet<World> | undefined {
|
|
197
|
+
if (worlds === undefined) {
|
|
198
|
+
for (const [attached, lease] of leases) {
|
|
199
|
+
if (attached !== world) {
|
|
200
|
+
lease.dispose();
|
|
201
|
+
leases.delete(attached);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const next = new Set<World>();
|
|
208
|
+
for (const candidate of worlds) {
|
|
209
|
+
if (candidate === world || next.has(candidate)) continue;
|
|
210
|
+
if (leases.has(candidate)) {
|
|
211
|
+
next.add(candidate);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
const result = renderer.attach(candidate);
|
|
216
|
+
if (result.ok) {
|
|
217
|
+
leases.set(candidate, result.value);
|
|
218
|
+
next.add(candidate);
|
|
219
|
+
} else fireError?.(result.error);
|
|
220
|
+
} catch (cause: unknown) {
|
|
221
|
+
fireError?.(makeWorldUpdateError(cause));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
for (const [attached, lease] of leases) {
|
|
225
|
+
if (attached !== world && !next.has(attached)) {
|
|
226
|
+
lease.dispose();
|
|
227
|
+
leases.delete(attached);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return next;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function releaseInjectedAttachments(): void {
|
|
234
|
+
syncInjectedAttachments(undefined);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function releaseAttachments(): void {
|
|
238
|
+
releaseInjectedAttachments();
|
|
239
|
+
const lease = leases.get(world);
|
|
240
|
+
if (lease !== undefined) {
|
|
241
|
+
lease.dispose();
|
|
242
|
+
leases.delete(world);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function runProfiledPhase<T>(
|
|
247
|
+
session: RecorderSession | undefined,
|
|
248
|
+
phase: string,
|
|
249
|
+
action: () => T,
|
|
250
|
+
): T {
|
|
251
|
+
const opened = beginPhase(session, phase);
|
|
252
|
+
try {
|
|
253
|
+
return action();
|
|
254
|
+
} finally {
|
|
255
|
+
if (opened) endPhase(session);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function runFrame(deltaSeconds: number): Result<void, AppError | RenderError> {
|
|
260
|
+
const session = opts.profiler?.activeSession();
|
|
261
|
+
let profileFrame: ProfileFrameToken | undefined;
|
|
262
|
+
let frameError: AppError | RenderError | undefined;
|
|
263
|
+
let primaryUpdated = false;
|
|
264
|
+
const reportError = (error: AppError | RenderError): void => {
|
|
265
|
+
frameError ??= error;
|
|
266
|
+
opts.onError?.(error);
|
|
267
|
+
};
|
|
268
|
+
if (session !== undefined) {
|
|
269
|
+
if (profilerCaptureId !== session.captureId) {
|
|
270
|
+
profilerCaptureId = session.captureId;
|
|
271
|
+
profilerFrameId = 0;
|
|
272
|
+
}
|
|
273
|
+
const frameId = ++profilerFrameId;
|
|
274
|
+
if (beginFrame(session, frameId)) {
|
|
275
|
+
profileFrame = { captureId: session.captureId, frameId };
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
runProfiledPhase(session, 'frame-total', () => {
|
|
280
|
+
attachPrimary(reportError);
|
|
281
|
+
|
|
282
|
+
runProfiledPhase(session, 'world-update-primary', () => {
|
|
283
|
+
try {
|
|
284
|
+
if (fireWorldUpdateResult(world.update(deltaSeconds), reportError)) {
|
|
285
|
+
primaryUpdated = true;
|
|
286
|
+
}
|
|
287
|
+
} catch (cause: unknown) {
|
|
288
|
+
reportError(makeWorldUpdateError(cause));
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
let injected:
|
|
293
|
+
| { worlds: readonly World[]; cameraOwner: number; resourceOwner: number }
|
|
294
|
+
| undefined;
|
|
295
|
+
runProfiledPhase(session, 'draw-source', () => {
|
|
296
|
+
if (drawSource === undefined) return;
|
|
297
|
+
try {
|
|
298
|
+
injected = drawSource();
|
|
299
|
+
} catch (cause: unknown) {
|
|
300
|
+
reportError(makeWorldUpdateError(cause));
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const attachedInjectedWorlds = syncInjectedAttachments(injected?.worlds, reportError);
|
|
305
|
+
const updatedWorlds = injected === undefined ? undefined : new Set<World>();
|
|
306
|
+
if (updatedWorlds !== undefined && primaryUpdated && leases.has(world)) {
|
|
307
|
+
updatedWorlds.add(world);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
runProfiledPhase(session, 'world-update-injected', () => {
|
|
311
|
+
if (
|
|
312
|
+
injected !== undefined &&
|
|
313
|
+
attachedInjectedWorlds !== undefined &&
|
|
314
|
+
updatedWorlds !== undefined
|
|
315
|
+
) {
|
|
316
|
+
updateInjectedWorlds(
|
|
317
|
+
injected.worlds,
|
|
318
|
+
world,
|
|
319
|
+
deltaSeconds,
|
|
320
|
+
reportError,
|
|
321
|
+
attachedInjectedWorlds,
|
|
322
|
+
updatedWorlds,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
runProfiledPhase(session, 'renderer-draw', () => {
|
|
328
|
+
try {
|
|
329
|
+
let drawResult: ReturnType<Renderer['draw']>;
|
|
330
|
+
if (injected === undefined) {
|
|
331
|
+
const primaryLease = leases.get(world);
|
|
332
|
+
if (!primaryUpdated || primaryLease === undefined) return;
|
|
333
|
+
drawResult = renderer.draw({
|
|
334
|
+
leases: [primaryLease],
|
|
335
|
+
camera: { lease: primaryLease },
|
|
336
|
+
environment: { lease: primaryLease },
|
|
337
|
+
...(profileFrame === undefined ? {} : { profileFrame }),
|
|
338
|
+
});
|
|
339
|
+
} else {
|
|
340
|
+
if (updatedWorlds === undefined) return;
|
|
341
|
+
const readyEntries = injected.worlds
|
|
342
|
+
.filter((candidate) => updatedWorlds.has(candidate))
|
|
343
|
+
.map((candidate) => ({ candidate, lease: leases.get(candidate) }))
|
|
344
|
+
.filter(
|
|
345
|
+
(entry): entry is { candidate: World; lease: RenderWorldLease } =>
|
|
346
|
+
entry.lease !== undefined,
|
|
347
|
+
);
|
|
348
|
+
const readyLeases = readyEntries.map((entry) => entry.lease);
|
|
349
|
+
const cameraWorld = injected.worlds[injected.cameraOwner];
|
|
350
|
+
const environmentWorld = injected.worlds[injected.resourceOwner];
|
|
351
|
+
if (cameraWorld === undefined || environmentWorld === undefined) return;
|
|
352
|
+
const cameraLease = leases.get(cameraWorld);
|
|
353
|
+
const environmentLease = leases.get(environmentWorld);
|
|
354
|
+
if (
|
|
355
|
+
readyLeases.length === 0 ||
|
|
356
|
+
cameraLease === undefined ||
|
|
357
|
+
environmentLease === undefined
|
|
358
|
+
)
|
|
359
|
+
return;
|
|
360
|
+
drawResult = renderer.draw({
|
|
361
|
+
leases: readyLeases,
|
|
362
|
+
camera: { lease: cameraLease },
|
|
363
|
+
environment: { lease: environmentLease },
|
|
364
|
+
...(profileFrame === undefined ? {} : { profileFrame }),
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
if (drawResult !== undefined) {
|
|
368
|
+
const result = drawResult as { ok: boolean; error?: RenderError };
|
|
369
|
+
if (!result.ok && result.error !== undefined) {
|
|
370
|
+
reportError(result.error);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
} catch (cause: unknown) {
|
|
374
|
+
reportError(makeWorldUpdateError(cause));
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
endFrame(session);
|
|
379
|
+
try {
|
|
380
|
+
opts.debugRhi?.onFrameEnd();
|
|
381
|
+
} catch {
|
|
382
|
+
// Debug capture is an observational bridge. A recorder failure must not
|
|
383
|
+
// change the App frame result or make the host loop stop.
|
|
384
|
+
}
|
|
385
|
+
return frameError === undefined ? ok(undefined) : err(frameError);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function tick(): void {
|
|
389
|
+
if (state !== 'running') return;
|
|
390
|
+
|
|
391
|
+
// Device loss is a renderer-owned degraded interval, not an application
|
|
392
|
+
// stop. Keep the rAF heartbeat alive while the host performs the explicit
|
|
393
|
+
// Renderer.recover() rebuild, but freeze simulation so recovery does not
|
|
394
|
+
// advance the World against frames that cannot be submitted. The next
|
|
395
|
+
// tick observes `alive` and resumes the normal update/draw sequence.
|
|
396
|
+
const timestamp = now();
|
|
397
|
+
const deltaSeconds = (timestamp - lastTimestamp) / 1000;
|
|
398
|
+
lastTimestamp = timestamp;
|
|
399
|
+
runFrame(deltaSeconds);
|
|
400
|
+
pendingFrameId = raf(tick);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function releaseProfiler(): void {
|
|
404
|
+
finishProfilerCapture(opts.profiler);
|
|
405
|
+
releasePhaseCatalog?.();
|
|
406
|
+
releasePhaseCatalog = undefined;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
setDrawSource(nextDrawSource): void {
|
|
411
|
+
if (drawSource !== nextDrawSource) syncInjectedAttachments(undefined);
|
|
412
|
+
drawSource = nextDrawSource;
|
|
413
|
+
},
|
|
414
|
+
stepFrame(deltaSeconds): Result<void, AppError | RenderError> {
|
|
415
|
+
const reason =
|
|
416
|
+
state !== 'paused'
|
|
417
|
+
? 'state'
|
|
418
|
+
: !Number.isFinite(deltaSeconds) || deltaSeconds < 0
|
|
419
|
+
? 'delta'
|
|
420
|
+
: undefined;
|
|
421
|
+
if (reason !== undefined) {
|
|
422
|
+
return err(
|
|
423
|
+
makeAppError(
|
|
424
|
+
'app-frame-step-invalid',
|
|
425
|
+
'state is "paused" and deltaSeconds is finite and non-negative',
|
|
426
|
+
'pause the App and pass a finite non-negative delta before retrying',
|
|
427
|
+
{ state, deltaSeconds, reason },
|
|
428
|
+
),
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
return runFrame(deltaSeconds);
|
|
432
|
+
},
|
|
433
|
+
start(): Result<void, AppError> {
|
|
434
|
+
if (state === 'running') {
|
|
435
|
+
return err(
|
|
436
|
+
makeAppError(
|
|
437
|
+
'app-already-running',
|
|
438
|
+
'state must be "idle" or "paused" to start',
|
|
439
|
+
'call stop() first or check getState() before retrying',
|
|
440
|
+
{},
|
|
441
|
+
),
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
if (state === 'stopped') {
|
|
445
|
+
return err(
|
|
446
|
+
makeAppError(
|
|
447
|
+
'app-not-started',
|
|
448
|
+
'frame-loop is in terminal "stopped" state',
|
|
449
|
+
'create a new App via createApp({...}); the existing handle is dead',
|
|
450
|
+
{},
|
|
451
|
+
),
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
lastTimestamp = now();
|
|
455
|
+
state = 'running';
|
|
456
|
+
pendingFrameId = raf(tick);
|
|
457
|
+
return ok(undefined);
|
|
458
|
+
},
|
|
459
|
+
|
|
460
|
+
stop(): Result<void, AppError> {
|
|
461
|
+
if (state === 'idle') {
|
|
462
|
+
return err(
|
|
463
|
+
makeAppError(
|
|
464
|
+
'app-not-started',
|
|
465
|
+
'state must be "running" to stop',
|
|
466
|
+
'check getState() before calling stop(); idle handles cannot stop',
|
|
467
|
+
{},
|
|
468
|
+
),
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
if (state === 'stopped') {
|
|
472
|
+
return err(
|
|
473
|
+
makeAppError(
|
|
474
|
+
'app-not-started',
|
|
475
|
+
'frame-loop is in terminal "stopped" state',
|
|
476
|
+
'discard this handle and create a new App',
|
|
477
|
+
{},
|
|
478
|
+
),
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
caf(pendingFrameId);
|
|
482
|
+
pendingFrameId = 0;
|
|
483
|
+
state = 'stopped';
|
|
484
|
+
releaseAttachments();
|
|
485
|
+
releaseProfiler();
|
|
486
|
+
return ok(undefined);
|
|
487
|
+
},
|
|
488
|
+
|
|
489
|
+
pause(): Result<void, AppError> {
|
|
490
|
+
if (state === 'paused') return ok(undefined);
|
|
491
|
+
if (state !== 'running') {
|
|
492
|
+
return err(
|
|
493
|
+
makeAppError(
|
|
494
|
+
'app-not-started',
|
|
495
|
+
'state must be "running" or "paused" to pause',
|
|
496
|
+
'call start() first; idle handles cannot pause',
|
|
497
|
+
{},
|
|
498
|
+
),
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
caf(pendingFrameId);
|
|
502
|
+
pendingFrameId = 0;
|
|
503
|
+
state = 'paused';
|
|
504
|
+
return ok(undefined);
|
|
505
|
+
},
|
|
506
|
+
|
|
507
|
+
resume(): Result<void, AppError> {
|
|
508
|
+
if (state === 'idle' || state === 'stopped') {
|
|
509
|
+
return err(
|
|
510
|
+
makeAppError(
|
|
511
|
+
'app-not-started',
|
|
512
|
+
'state must be "paused" to resume',
|
|
513
|
+
'call start() first to leave idle; resume() expects an active handle',
|
|
514
|
+
{},
|
|
515
|
+
),
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
if (state === 'running') return ok(undefined);
|
|
519
|
+
lastTimestamp = now();
|
|
520
|
+
state = 'running';
|
|
521
|
+
pendingFrameId = raf(tick);
|
|
522
|
+
return ok(undefined);
|
|
523
|
+
},
|
|
524
|
+
|
|
525
|
+
getState(): FrameState {
|
|
526
|
+
return state;
|
|
527
|
+
},
|
|
528
|
+
|
|
529
|
+
setStopped(): void {
|
|
530
|
+
if (pendingFrameId !== 0) {
|
|
531
|
+
caf(pendingFrameId);
|
|
532
|
+
pendingFrameId = 0;
|
|
533
|
+
}
|
|
534
|
+
releaseAttachments();
|
|
535
|
+
releaseProfiler();
|
|
536
|
+
state = 'stopped';
|
|
537
|
+
},
|
|
538
|
+
};
|
|
539
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import {
|
|
2
|
+
attachBrowserInputBackend,
|
|
3
|
+
type BrowserInputBackendOptions,
|
|
4
|
+
type InputBackend,
|
|
5
|
+
type PointerLockProvider,
|
|
6
|
+
type VirtualJoystickConfig,
|
|
7
|
+
} from '@forgeax/engine-input';
|
|
8
|
+
|
|
9
|
+
import { APP_ERROR_HINTS, APP_EXPECTED, AppError } from '../errors';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Handle returned by attachInputAuto. Captures the backend (exposed via
|
|
13
|
+
* App.input), the detach callable, and a cleanup() funnel that the
|
|
14
|
+
* frame-loop / app-stop / device-lost paths share (R-4).
|
|
15
|
+
*/
|
|
16
|
+
export interface InputAttachHandle {
|
|
17
|
+
readonly backend: InputBackend;
|
|
18
|
+
cleanup(): void;
|
|
19
|
+
/**
|
|
20
|
+
* M2: install the error dispatch callback for onLockError events.
|
|
21
|
+
* Must be called by createApp after the ErrorFanoutRegistry is created
|
|
22
|
+
* (the dispatch function is created after attachInputAuto returns).
|
|
23
|
+
* Idempotent — subsequent calls replace the previous callback.
|
|
24
|
+
*/
|
|
25
|
+
setOnErrorDispatch(fn: (err: AppError) => void): void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Acquire a browser input backend. This Host helper owns only DOM listeners;
|
|
30
|
+
* the Cordis input plugin owns every World resource and system contribution.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* Options forwarded to attachBrowserInputBackend at attach time. Currently only
|
|
34
|
+
* the neutral PointerLock gate (host decides whether a canvas click captures the
|
|
35
|
+
* cursor); the backend stays host-opaque. Optional so existing call sites are
|
|
36
|
+
* unchanged.
|
|
37
|
+
*/
|
|
38
|
+
export interface InputAttachOptions {
|
|
39
|
+
/** Host-owned UI root used to route Shadow DOM events away from gameplay. */
|
|
40
|
+
readonly uiRoot?: Node;
|
|
41
|
+
readonly pointerLockAllowed?: () => boolean;
|
|
42
|
+
/** M3: virtual joystick configurations passed through to browser backend. */
|
|
43
|
+
readonly virtualJoysticks?: readonly VirtualJoystickConfig[];
|
|
44
|
+
/**
|
|
45
|
+
* M2: pointer-lock provider injected by the host (e.g. editor play-runtime).
|
|
46
|
+
* Forwarded to BrowserInputBackendOptions.lockProvider. When absent, the
|
|
47
|
+
* backend falls back to the W3C requestPointerLock() path.
|
|
48
|
+
* Type SSOT is @forgeax/engine-input's PointerLockProvider.
|
|
49
|
+
*/
|
|
50
|
+
readonly lockProvider?: PointerLockProvider;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function attachInputAuto(
|
|
54
|
+
canvas: HTMLCanvasElement,
|
|
55
|
+
options: InputAttachOptions = {},
|
|
56
|
+
): InputAttachHandle {
|
|
57
|
+
// Mutable slot for error dispatch. The dispatch function is created by
|
|
58
|
+
// createApp AFTER the ErrorFanoutRegistry is set up, so we need an
|
|
59
|
+
// indirection: the backend's onLockError callback fires synchronously
|
|
60
|
+
// inside the click handler, but the fan-out is ready by then because
|
|
61
|
+
// createApp wires it before calling app.start().
|
|
62
|
+
let onLockErrorDispatch: ((err: AppError) => void) | undefined;
|
|
63
|
+
|
|
64
|
+
const backendOpts: BrowserInputBackendOptions = {
|
|
65
|
+
...(options.uiRoot ? { uiRoot: options.uiRoot } : {}),
|
|
66
|
+
...(options.pointerLockAllowed ? { pointerLockAllowed: options.pointerLockAllowed } : {}),
|
|
67
|
+
...(options.virtualJoysticks ? { virtualJoysticks: options.virtualJoysticks } : {}),
|
|
68
|
+
...(options.lockProvider ? { lockProvider: options.lockProvider } : {}),
|
|
69
|
+
onLockError: (detail: { path: 'w3c' | 'provider'; cause: unknown }) => {
|
|
70
|
+
// D-4: wrap the backend's onLockError signal into a structured AppError
|
|
71
|
+
// and fan-out through createApp's onError channel. The dispatch function
|
|
72
|
+
// is installed by setOnErrorDispatch after createApp creates the
|
|
73
|
+
// ErrorFanoutRegistry. If no dispatch is set yet (e.g. during unit tests
|
|
74
|
+
// that don't call setOnErrorDispatch), the error is silently dropped.
|
|
75
|
+
if (onLockErrorDispatch) {
|
|
76
|
+
const err = new AppError({
|
|
77
|
+
code: 'app-pointer-lock-failed',
|
|
78
|
+
expected: APP_EXPECTED['app-pointer-lock-failed'],
|
|
79
|
+
hint: APP_ERROR_HINTS['app-pointer-lock-failed'],
|
|
80
|
+
detail,
|
|
81
|
+
});
|
|
82
|
+
onLockErrorDispatch(err);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
const detach = attachBrowserInputBackend(canvas, backendOpts);
|
|
87
|
+
const backend = detach.backend;
|
|
88
|
+
|
|
89
|
+
let cleanedUp = false;
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
backend,
|
|
93
|
+
setOnErrorDispatch(fn: (err: AppError) => void): void {
|
|
94
|
+
onLockErrorDispatch = fn;
|
|
95
|
+
},
|
|
96
|
+
cleanup(): void {
|
|
97
|
+
if (cleanedUp) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
cleanedUp = true;
|
|
101
|
+
detach();
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|