@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,190 @@
|
|
|
1
|
+
import type { AudioBackend } from '@forgeax/engine-audio';
|
|
2
|
+
import { createWorldContext, World } from '@forgeax/engine-ecs';
|
|
3
|
+
import { type InputBackend, ownedInputBackendPlugin } from '@forgeax/engine-input';
|
|
4
|
+
import { Context, type Plugin } from '@forgeax/engine-plugin';
|
|
5
|
+
import type { Renderer } from '@forgeax/engine-render';
|
|
6
|
+
import { describe, expect, it } from 'vitest';
|
|
7
|
+
import { assembledEngineProfile } from '../internal/assembled-engine-profile';
|
|
8
|
+
import { mainEngineProfile } from '../internal/main-engine-profile';
|
|
9
|
+
import { remoteServerPlugin } from '../internal/remote-server-plugin';
|
|
10
|
+
import { workerEngineProfile } from '../internal/worker-engine-profile';
|
|
11
|
+
import { ownedRendererPlugin } from '../renderer-plugin';
|
|
12
|
+
|
|
13
|
+
interface PluginMetadata {
|
|
14
|
+
readonly name?: string;
|
|
15
|
+
readonly inject?: readonly string[] | Readonly<Record<string, unknown>>;
|
|
16
|
+
readonly provide?: string | readonly string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function metadata(plugin: Plugin): PluginMetadata {
|
|
20
|
+
return plugin as PluginMetadata;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function names(value: string | readonly string[] | undefined): readonly string[] {
|
|
24
|
+
if (value === undefined) return [];
|
|
25
|
+
return typeof value === 'string' ? [value] : value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function injects(value: PluginMetadata['inject']): readonly string[] {
|
|
29
|
+
if (value === undefined) return [];
|
|
30
|
+
return Array.isArray(value) ? value : Object.keys(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function expectDag(profile: readonly Plugin[], roots: readonly string[]): void {
|
|
34
|
+
const providers = new Map<string, number>();
|
|
35
|
+
for (const root of roots) providers.set(root, -1);
|
|
36
|
+
for (const [index, plugin] of profile.entries()) {
|
|
37
|
+
for (const service of names(metadata(plugin).provide)) {
|
|
38
|
+
expect(providers.has(service), `duplicate provider for ${service}`).toBe(false);
|
|
39
|
+
providers.set(service, index);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const outgoing = profile.map(() => new Set<number>());
|
|
44
|
+
const indegree = profile.map(() => 0);
|
|
45
|
+
for (const [consumer, plugin] of profile.entries()) {
|
|
46
|
+
for (const service of injects(metadata(plugin).inject)) {
|
|
47
|
+
const provider = providers.get(service);
|
|
48
|
+
expect(
|
|
49
|
+
provider,
|
|
50
|
+
`${metadata(plugin).name ?? consumer} requires ${service}`,
|
|
51
|
+
).not.toBeUndefined();
|
|
52
|
+
expect(
|
|
53
|
+
provider === consumer,
|
|
54
|
+
`${metadata(plugin).name ?? consumer} provides and requires ${service}`,
|
|
55
|
+
).toBe(false);
|
|
56
|
+
if (provider === undefined || provider < 0) continue;
|
|
57
|
+
if (outgoing[provider]?.has(consumer)) continue;
|
|
58
|
+
outgoing[provider]?.add(consumer);
|
|
59
|
+
indegree[consumer] = (indegree[consumer] ?? 0) + 1;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const ready = indegree.flatMap((degree, index) => (degree === 0 ? [index] : []));
|
|
64
|
+
let visited = 0;
|
|
65
|
+
while (ready.length > 0) {
|
|
66
|
+
const current = ready.shift();
|
|
67
|
+
if (current === undefined) break;
|
|
68
|
+
visited += 1;
|
|
69
|
+
for (const consumer of outgoing[current] ?? []) {
|
|
70
|
+
indegree[consumer] = (indegree[consumer] ?? 0) - 1;
|
|
71
|
+
if (indegree[consumer] === 0) ready.push(consumer);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
expect(visited, 'built-in profile dependency cycle').toBe(profile.length);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const renderer = { assets: {} } as unknown as Renderer;
|
|
78
|
+
const input = { sample: () => ({}) } as InputBackend;
|
|
79
|
+
const audio = {} as AudioBackend;
|
|
80
|
+
describe('Engine built-in plugin profiles', () => {
|
|
81
|
+
it('keeps browser-main capabilities in one acyclic declared graph', () => {
|
|
82
|
+
const profile = mainEngineProfile({
|
|
83
|
+
renderer,
|
|
84
|
+
input,
|
|
85
|
+
inputMap: [],
|
|
86
|
+
animationPayloads: () => undefined,
|
|
87
|
+
});
|
|
88
|
+
expect(profile.map((plugin) => metadata(plugin).name)).toEqual([
|
|
89
|
+
'renderer',
|
|
90
|
+
'render-components',
|
|
91
|
+
'renderer-assets',
|
|
92
|
+
'assets-world',
|
|
93
|
+
'input-backend',
|
|
94
|
+
'scene',
|
|
95
|
+
'animation-payloads',
|
|
96
|
+
'animation',
|
|
97
|
+
'state',
|
|
98
|
+
'input',
|
|
99
|
+
'input-map',
|
|
100
|
+
]);
|
|
101
|
+
expectDag(profile, ['world']);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('keeps Worker and assembled selections acyclic without copying App wiring', () => {
|
|
105
|
+
expectDag(
|
|
106
|
+
workerEngineProfile({
|
|
107
|
+
renderer,
|
|
108
|
+
input,
|
|
109
|
+
audio,
|
|
110
|
+
animationPayloads: () => undefined,
|
|
111
|
+
}),
|
|
112
|
+
['world'],
|
|
113
|
+
);
|
|
114
|
+
expectDag(assembledEngineProfile({ renderer }), ['world']);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('closes an App-owned remote server with its Fiber', async () => {
|
|
118
|
+
let closeCount = 0;
|
|
119
|
+
const context = new Context();
|
|
120
|
+
await context.plugin(
|
|
121
|
+
remoteServerPlugin({
|
|
122
|
+
port: 5732,
|
|
123
|
+
async close() {
|
|
124
|
+
closeCount += 1;
|
|
125
|
+
},
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
await context.fiber.dispose();
|
|
129
|
+
expect(closeCount).toBe(1);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('releases an owned Renderer through its dependent Fiber', async () => {
|
|
133
|
+
const disposalOrder: string[] = [];
|
|
134
|
+
const ownedRenderer = {
|
|
135
|
+
dispose() {
|
|
136
|
+
disposalOrder.push('renderer');
|
|
137
|
+
},
|
|
138
|
+
} as Renderer;
|
|
139
|
+
const context = new Context();
|
|
140
|
+
await context.plugin(ownedRendererPlugin(ownedRenderer));
|
|
141
|
+
await context.plugin({
|
|
142
|
+
name: 'renderer-consumer',
|
|
143
|
+
inject: ['renderer'],
|
|
144
|
+
apply(ctx) {
|
|
145
|
+
ctx.effect(
|
|
146
|
+
() => () => {
|
|
147
|
+
disposalOrder.push('consumer');
|
|
148
|
+
},
|
|
149
|
+
'test/renderer-consumer',
|
|
150
|
+
);
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
await context.fiber.dispose();
|
|
154
|
+
expect(disposalOrder).toEqual(['consumer', 'renderer']);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('releases an App-acquired input backend through its provider Fiber', async () => {
|
|
158
|
+
let disposeCount = 0;
|
|
159
|
+
const context = new Context();
|
|
160
|
+
await context.plugin(
|
|
161
|
+
ownedInputBackendPlugin(input, () => {
|
|
162
|
+
disposeCount += 1;
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
await context.fiber.dispose();
|
|
166
|
+
expect(disposeCount).toBe(1);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('releases an owned Renderer once when later activation rolls back', async () => {
|
|
170
|
+
let disposeCount = 0;
|
|
171
|
+
const ownedRenderer = {
|
|
172
|
+
dispose() {
|
|
173
|
+
disposeCount += 1;
|
|
174
|
+
},
|
|
175
|
+
} as Renderer;
|
|
176
|
+
|
|
177
|
+
await expect(
|
|
178
|
+
createWorldContext(new World(), [
|
|
179
|
+
ownedRendererPlugin(ownedRenderer),
|
|
180
|
+
{
|
|
181
|
+
name: 'activation-failure',
|
|
182
|
+
apply() {
|
|
183
|
+
throw new Error('activation failed');
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
]),
|
|
187
|
+
).rejects.toThrow('activation failed');
|
|
188
|
+
expect(disposeCount).toBe(1);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { World } from '@forgeax/engine-ecs';
|
|
2
|
+
import { createProfiler } from '@forgeax/engine-profiler';
|
|
3
|
+
import type { Renderer } from '@forgeax/engine-render';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { createFrameLoop } from '../internal/frame-loop';
|
|
7
|
+
|
|
8
|
+
function makeRenderer(): Renderer {
|
|
9
|
+
return {
|
|
10
|
+
backend: 'webgpu',
|
|
11
|
+
ready: Promise.resolve({ ok: true, value: undefined }),
|
|
12
|
+
attach: () => ({ ok: true, value: undefined }),
|
|
13
|
+
detachWorld: () => {},
|
|
14
|
+
draw: () => ({ ok: true, value: undefined }),
|
|
15
|
+
onError: () => () => {},
|
|
16
|
+
onLost: () => () => {},
|
|
17
|
+
dispose: () => {},
|
|
18
|
+
} as unknown as Renderer;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('App profiler default-off allocation boundary', () => {
|
|
22
|
+
it('reports zero profiler-owned event objects and no artifact on the full loop', () => {
|
|
23
|
+
const allocationReport = { profilerEventObjectAllocations: 0 };
|
|
24
|
+
const profiler = createProfiler({ enabled: false, allocationReport });
|
|
25
|
+
const scheduler = {
|
|
26
|
+
callback: undefined as ((timestamp: number) => void) | undefined,
|
|
27
|
+
raf(callback: (timestamp: number) => void): number {
|
|
28
|
+
this.callback = callback;
|
|
29
|
+
return 1;
|
|
30
|
+
},
|
|
31
|
+
caf(): void {
|
|
32
|
+
this.callback = undefined;
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
const loop = createFrameLoop({
|
|
36
|
+
world: new World(),
|
|
37
|
+
renderer: makeRenderer(),
|
|
38
|
+
now: () => 1000,
|
|
39
|
+
raf: scheduler.raf.bind(scheduler),
|
|
40
|
+
caf: scheduler.caf.bind(scheduler),
|
|
41
|
+
profiler,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
loop.start().unwrap();
|
|
45
|
+
scheduler.callback?.(1000);
|
|
46
|
+
loop.stop().unwrap();
|
|
47
|
+
|
|
48
|
+
expect({
|
|
49
|
+
profilerEventObjectAllocations: allocationReport.profilerEventObjectAllocations,
|
|
50
|
+
artifact: profiler.latestCapture(),
|
|
51
|
+
}).toEqual({ profilerEventObjectAllocations: 0, artifact: undefined });
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { World } from '@forgeax/engine-ecs';
|
|
2
|
+
import { createRenderReadLease } from '@forgeax/engine-ecs/projection';
|
|
3
|
+
import { createProfiler, type ProfileCapture } from '@forgeax/engine-profiler';
|
|
4
|
+
import type { Renderer, RenderFrameInput } from '@forgeax/engine-render';
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { createFrameLoop } from '../internal/frame-loop';
|
|
8
|
+
import { APP_PHASE_CATALOG } from '../types';
|
|
9
|
+
|
|
10
|
+
const FRAME_LOOP_PHASES = [
|
|
11
|
+
'frame-total',
|
|
12
|
+
'draw-source',
|
|
13
|
+
'world-update-primary',
|
|
14
|
+
'world-update-injected',
|
|
15
|
+
'renderer-draw',
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
function makeScheduler() {
|
|
19
|
+
let callback: ((timestamp: number) => void) | undefined;
|
|
20
|
+
return {
|
|
21
|
+
raf(next: (timestamp: number) => void): number {
|
|
22
|
+
callback = next;
|
|
23
|
+
return 1;
|
|
24
|
+
},
|
|
25
|
+
caf(): void {
|
|
26
|
+
callback = undefined;
|
|
27
|
+
},
|
|
28
|
+
tick(timestamp: number): void {
|
|
29
|
+
const next = callback;
|
|
30
|
+
callback = undefined;
|
|
31
|
+
next?.(timestamp);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function makeRenderer(onDraw: (profileFrame: unknown) => void = () => {}): Renderer {
|
|
37
|
+
return {
|
|
38
|
+
attach: (world: World) => ({ ok: true, value: createRenderReadLease(world) }),
|
|
39
|
+
draw: (request: RenderFrameInput) => {
|
|
40
|
+
onDraw(request.profileFrame);
|
|
41
|
+
return { ok: true, value: undefined };
|
|
42
|
+
},
|
|
43
|
+
subscribe: () => () => {},
|
|
44
|
+
dispose: () => {},
|
|
45
|
+
} as unknown as Renderer;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function startSession(
|
|
49
|
+
profiler: ReturnType<typeof createProfiler>,
|
|
50
|
+
frameLimit = 2,
|
|
51
|
+
eventLimit = 64,
|
|
52
|
+
) {
|
|
53
|
+
const result = profiler.startCapture({ frameLimit, eventLimit });
|
|
54
|
+
if (!result.ok) throw new Error(result.error.code);
|
|
55
|
+
return result.value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function finish(profiler: ReturnType<typeof createProfiler>): ProfileCapture {
|
|
59
|
+
const session = profiler.activeSession();
|
|
60
|
+
if (session === undefined) {
|
|
61
|
+
const latest = profiler.latestCapture();
|
|
62
|
+
expect(latest).toBeDefined();
|
|
63
|
+
if (latest === undefined) throw new Error('capture did not publish');
|
|
64
|
+
return latest;
|
|
65
|
+
}
|
|
66
|
+
const result = session?.finish();
|
|
67
|
+
expect(result?.ok).toBe(true);
|
|
68
|
+
if (result === undefined || !result.ok) throw new Error('capture did not finish');
|
|
69
|
+
return result.value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe('App profiler frame token', () => {
|
|
73
|
+
it('records the local frame phases with one token per logical frame', () => {
|
|
74
|
+
const profiler = createProfiler();
|
|
75
|
+
expect(startSession(profiler).captureId).toBe('capture-0001');
|
|
76
|
+
const scheduler = makeScheduler();
|
|
77
|
+
const drawOptions: unknown[] = [];
|
|
78
|
+
const loop = createFrameLoop({
|
|
79
|
+
world: new World(),
|
|
80
|
+
renderer: makeRenderer((options) => drawOptions.push(options)),
|
|
81
|
+
now: (() => {
|
|
82
|
+
const values = [1000, 1016, 1032];
|
|
83
|
+
return () => values.shift() ?? 1032;
|
|
84
|
+
})(),
|
|
85
|
+
raf: scheduler.raf,
|
|
86
|
+
caf: scheduler.caf,
|
|
87
|
+
profiler,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
loop.start().unwrap();
|
|
91
|
+
scheduler.tick(1016);
|
|
92
|
+
scheduler.tick(1032);
|
|
93
|
+
loop.stop().unwrap();
|
|
94
|
+
|
|
95
|
+
const capture = finish(profiler);
|
|
96
|
+
expect(capture.phaseCatalog.app).toEqual(APP_PHASE_CATALOG);
|
|
97
|
+
expect(capture.captureId).toBe('capture-0001');
|
|
98
|
+
expect(capture.records.filter((record) => record.source === 'app')).toHaveLength(10);
|
|
99
|
+
expect(
|
|
100
|
+
capture.records
|
|
101
|
+
.filter((record) => record.source === 'app' && record.kind === 'phase')
|
|
102
|
+
.map((record) => record.phase),
|
|
103
|
+
).toEqual(expect.arrayContaining([...FRAME_LOOP_PHASES]));
|
|
104
|
+
expect(
|
|
105
|
+
new Set(
|
|
106
|
+
capture.records
|
|
107
|
+
.filter((record) => record.source === 'app' && record.kind === 'phase')
|
|
108
|
+
.map((record) => record.phase),
|
|
109
|
+
),
|
|
110
|
+
).toEqual(new Set(FRAME_LOOP_PHASES));
|
|
111
|
+
expect(
|
|
112
|
+
new Set(
|
|
113
|
+
capture.records.filter((record) => record.source === 'app').map((record) => record.frameId),
|
|
114
|
+
),
|
|
115
|
+
).toEqual(new Set([1, 2]));
|
|
116
|
+
expect(drawOptions).toHaveLength(2);
|
|
117
|
+
expect(drawOptions[0]).toMatchObject({
|
|
118
|
+
captureId: 'capture-0001',
|
|
119
|
+
frameId: 1,
|
|
120
|
+
});
|
|
121
|
+
expect(drawOptions[1]).toMatchObject({
|
|
122
|
+
captureId: 'capture-0001',
|
|
123
|
+
frameId: 2,
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('does not reuse capture identity and isolates observer and sink failures', () => {
|
|
128
|
+
const profiler = createProfiler({
|
|
129
|
+
sink: {
|
|
130
|
+
write: () => {
|
|
131
|
+
throw new Error('sink failure');
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
const session = startSession(profiler, 1);
|
|
136
|
+
const scheduler = makeScheduler();
|
|
137
|
+
let draws = 0;
|
|
138
|
+
const loop = createFrameLoop({
|
|
139
|
+
world: new World(),
|
|
140
|
+
renderer: makeRenderer(() => {
|
|
141
|
+
draws += 1;
|
|
142
|
+
}),
|
|
143
|
+
now: () => 1000,
|
|
144
|
+
raf: scheduler.raf,
|
|
145
|
+
caf: scheduler.caf,
|
|
146
|
+
profiler,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
loop.start().unwrap();
|
|
150
|
+
scheduler.tick(1000);
|
|
151
|
+
loop.stop().unwrap();
|
|
152
|
+
expect(draws).toBe(1);
|
|
153
|
+
expect(session.finish().ok).toBe(false);
|
|
154
|
+
|
|
155
|
+
const secondSession = startSession(profiler, 1);
|
|
156
|
+
expect(secondSession.captureId).not.toBe(session.captureId);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('recaptures complete data after an overflow without stale capture state', () => {
|
|
160
|
+
const profiler = createProfiler();
|
|
161
|
+
const scheduler = makeScheduler();
|
|
162
|
+
const loop = createFrameLoop({
|
|
163
|
+
world: new World(),
|
|
164
|
+
renderer: makeRenderer(),
|
|
165
|
+
now: () => 1000,
|
|
166
|
+
raf: scheduler.raf,
|
|
167
|
+
caf: scheduler.caf,
|
|
168
|
+
profiler,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const overflowSession = startSession(profiler, 1, 1);
|
|
172
|
+
loop.start().unwrap();
|
|
173
|
+
scheduler.tick(1000);
|
|
174
|
+
|
|
175
|
+
const overflow = profiler.latestCapture();
|
|
176
|
+
expect(overflow?.captureId).toBe(overflowSession.captureId);
|
|
177
|
+
expect(overflow?.completeness.status).toBe('overflow');
|
|
178
|
+
expect(overflow?.completeness.droppedEventCount).toBeGreaterThan(0);
|
|
179
|
+
|
|
180
|
+
const completeSession = startSession(profiler, 1, 64);
|
|
181
|
+
scheduler.tick(1016);
|
|
182
|
+
loop.stop().unwrap();
|
|
183
|
+
|
|
184
|
+
const complete = profiler.latestCapture();
|
|
185
|
+
expect(complete?.captureId).toBe(completeSession.captureId);
|
|
186
|
+
expect(complete?.captureId).not.toBe(overflow?.captureId);
|
|
187
|
+
expect(complete?.completeness).toMatchObject({ status: 'complete', droppedEventCount: 0 });
|
|
188
|
+
expect(overflow?.completeness.status).toBe('overflow');
|
|
189
|
+
expect(profiler.activeCaptureId()).toBeUndefined();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('publishes a partial artifact when the host stops before its frame boundary', () => {
|
|
193
|
+
const profiler = createProfiler();
|
|
194
|
+
startSession(profiler, 2);
|
|
195
|
+
const scheduler = makeScheduler();
|
|
196
|
+
const loop = createFrameLoop({
|
|
197
|
+
world: new World(),
|
|
198
|
+
renderer: makeRenderer(),
|
|
199
|
+
now: () => 1000,
|
|
200
|
+
raf: scheduler.raf,
|
|
201
|
+
caf: scheduler.caf,
|
|
202
|
+
profiler,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
loop.start().unwrap();
|
|
206
|
+
scheduler.tick(1000);
|
|
207
|
+
loop.stop().unwrap();
|
|
208
|
+
|
|
209
|
+
expect(profiler.latestCapture()?.completeness).toMatchObject({
|
|
210
|
+
status: 'partial',
|
|
211
|
+
incompleteReason: 'stopped-before-frame',
|
|
212
|
+
});
|
|
213
|
+
expect(profiler.activeCaptureId()).toBeUndefined();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('keeps the default-off path free of profiler records', () => {
|
|
217
|
+
const scheduler = makeScheduler();
|
|
218
|
+
let draws = 0;
|
|
219
|
+
const loop = createFrameLoop({
|
|
220
|
+
world: new World(),
|
|
221
|
+
renderer: makeRenderer(() => {
|
|
222
|
+
draws += 1;
|
|
223
|
+
}),
|
|
224
|
+
now: () => 1000,
|
|
225
|
+
raf: scheduler.raf,
|
|
226
|
+
caf: scheduler.caf,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
loop.start().unwrap();
|
|
230
|
+
scheduler.tick(1000);
|
|
231
|
+
loop.stop().unwrap();
|
|
232
|
+
|
|
233
|
+
expect(draws).toBe(1);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { createProfiler } from '@forgeax/engine-profiler';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { APP_PHASE_CATALOG } from '../types';
|
|
6
|
+
|
|
7
|
+
describe('App profiler phase catalog ownership', () => {
|
|
8
|
+
it('matches the profiler receiver set by definition and has five unique phases', () => {
|
|
9
|
+
const profiler = createProfiler();
|
|
10
|
+
expect(profiler.registerPhaseCatalog('app', APP_PHASE_CATALOG).ok).toBe(true);
|
|
11
|
+
expect(APP_PHASE_CATALOG).toHaveLength(9);
|
|
12
|
+
expect(new Set(APP_PHASE_CATALOG).size).toBe(APP_PHASE_CATALOG.length);
|
|
13
|
+
expect(new Set(APP_PHASE_CATALOG)).toEqual(new Set(profiler.phaseCatalog.app));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('rejects a second App catalog definition', () => {
|
|
17
|
+
const profiler = createProfiler();
|
|
18
|
+
expect(profiler.registerPhaseCatalog('app', APP_PHASE_CATALOG).ok).toBe(true);
|
|
19
|
+
const duplicate = profiler.registerPhaseCatalog('app', APP_PHASE_CATALOG);
|
|
20
|
+
expect(duplicate.ok).toBe(false);
|
|
21
|
+
if (!duplicate.ok) expect(duplicate.error.code).toBe('phase-catalog-conflict');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('keeps the phase declaration in the App owner module', () => {
|
|
25
|
+
const source = readFileSync(new URL('../types.ts', import.meta.url), 'utf8');
|
|
26
|
+
expect(source.match(/'frame-total'/g)).toHaveLength(1);
|
|
27
|
+
expect(source).not.toContain('FramePhaseObserver');
|
|
28
|
+
expect(source).not.toContain('frameSeq');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { World } from '@forgeax/engine-ecs';
|
|
2
|
+
import { createRenderReadLease } from '@forgeax/engine-ecs/projection';
|
|
3
|
+
import { createProfiler } from '@forgeax/engine-profiler';
|
|
4
|
+
import type { Renderer, RenderFrameInput } from '@forgeax/engine-render';
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { createFrameLoop } from '../internal/frame-loop';
|
|
8
|
+
import { APP_PHASE_CATALOG } from '../types';
|
|
9
|
+
|
|
10
|
+
function makeScheduler() {
|
|
11
|
+
let callback: ((timestamp: number) => void) | undefined;
|
|
12
|
+
return {
|
|
13
|
+
raf(next: (timestamp: number) => void): number {
|
|
14
|
+
callback = next;
|
|
15
|
+
return 1;
|
|
16
|
+
},
|
|
17
|
+
caf(): void {
|
|
18
|
+
callback = undefined;
|
|
19
|
+
},
|
|
20
|
+
tick(timestamp: number): void {
|
|
21
|
+
const next = callback;
|
|
22
|
+
callback = undefined;
|
|
23
|
+
next?.(timestamp);
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function makeRenderer(draw: (profileFrame: unknown) => void): Renderer {
|
|
29
|
+
return {
|
|
30
|
+
attach: (world: World) => ({ ok: true, value: createRenderReadLease(world) }),
|
|
31
|
+
draw: (request: RenderFrameInput) => {
|
|
32
|
+
draw(request.profileFrame);
|
|
33
|
+
return { ok: true, value: undefined };
|
|
34
|
+
},
|
|
35
|
+
subscribe: () => () => {},
|
|
36
|
+
dispose: () => {},
|
|
37
|
+
} as unknown as Renderer;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function start(profiler: ReturnType<typeof createProfiler>) {
|
|
41
|
+
const result = profiler.startCapture({ frameLimit: 2, eventLimit: 64 });
|
|
42
|
+
if (!result.ok) throw new Error(result.error.code);
|
|
43
|
+
return result.value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function finish(profiler: ReturnType<typeof createProfiler>) {
|
|
47
|
+
const session = profiler.activeSession();
|
|
48
|
+
if (session === undefined) {
|
|
49
|
+
const latest = profiler.latestCapture();
|
|
50
|
+
if (latest === undefined) throw new Error('capture did not publish');
|
|
51
|
+
return latest;
|
|
52
|
+
}
|
|
53
|
+
const result = session.finish();
|
|
54
|
+
if (!result.ok) throw new Error(result.error.code);
|
|
55
|
+
return result.value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('App and Render profiler integration', () => {
|
|
59
|
+
it('keeps normal and App-only frames on one capture identity', () => {
|
|
60
|
+
const profiler = createProfiler();
|
|
61
|
+
start(profiler);
|
|
62
|
+
const scheduler = makeScheduler();
|
|
63
|
+
const drawOptions: unknown[] = [];
|
|
64
|
+
const loop = createFrameLoop({
|
|
65
|
+
world: new World(),
|
|
66
|
+
renderer: makeRenderer((options) => drawOptions.push(options)),
|
|
67
|
+
now: () => 1000,
|
|
68
|
+
raf: scheduler.raf,
|
|
69
|
+
caf: scheduler.caf,
|
|
70
|
+
profiler,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
loop.start().unwrap();
|
|
74
|
+
scheduler.tick(1000);
|
|
75
|
+
loop.stop().unwrap();
|
|
76
|
+
|
|
77
|
+
const capture = finish(profiler);
|
|
78
|
+
expect(capture.records.filter((record) => record.source === 'app')).toHaveLength(5);
|
|
79
|
+
expect(capture.records.every((record) => record.frameId === 1)).toBe(true);
|
|
80
|
+
expect(drawOptions[0]).toMatchObject({
|
|
81
|
+
captureId: capture.captureId,
|
|
82
|
+
frameId: 1,
|
|
83
|
+
});
|
|
84
|
+
expect(capture.phaseCatalog.app).toEqual(APP_PHASE_CATALOG);
|
|
85
|
+
expect(profiler.phaseCatalog.app).toEqual([]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('isolates a renderer failure while the host frame loop continues', () => {
|
|
89
|
+
const profiler = createProfiler();
|
|
90
|
+
start(profiler);
|
|
91
|
+
const scheduler = makeScheduler();
|
|
92
|
+
let draws = 0;
|
|
93
|
+
const errors: unknown[] = [];
|
|
94
|
+
const loop = createFrameLoop({
|
|
95
|
+
world: new World(),
|
|
96
|
+
renderer: makeRenderer(() => {
|
|
97
|
+
draws += 1;
|
|
98
|
+
throw new Error('render failure');
|
|
99
|
+
}),
|
|
100
|
+
now: () => 1000,
|
|
101
|
+
raf: scheduler.raf,
|
|
102
|
+
caf: scheduler.caf,
|
|
103
|
+
onError: (error) => errors.push(error),
|
|
104
|
+
profiler,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
loop.start().unwrap();
|
|
108
|
+
scheduler.tick(1000);
|
|
109
|
+
scheduler.tick(1000);
|
|
110
|
+
loop.stop().unwrap();
|
|
111
|
+
|
|
112
|
+
const capture = finish(profiler);
|
|
113
|
+
expect(draws).toBe(2);
|
|
114
|
+
expect(errors).toHaveLength(2);
|
|
115
|
+
expect(capture.records.filter((record) => record.source === 'app')).toHaveLength(10);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
const BRIDGE_SOURCE = fileURLToPath(
|
|
6
|
+
new URL('../internal/browser-remote-bridge.ts', import.meta.url),
|
|
7
|
+
);
|
|
8
|
+
const CREATE_APP_SOURCE = fileURLToPath(new URL('../create-app.ts', import.meta.url));
|
|
9
|
+
|
|
10
|
+
describe('browser remote profiler bridge', () => {
|
|
11
|
+
const bridge = readFileSync(BRIDGE_SOURCE, 'utf8');
|
|
12
|
+
const createApp = readFileSync(CREATE_APP_SOURCE, 'utf8');
|
|
13
|
+
|
|
14
|
+
it('propagates the explicit profiler root through the existing bridge context', () => {
|
|
15
|
+
expect(bridge).toContain('readonly profiler?: unknown');
|
|
16
|
+
expect(bridge).toContain('profiler,');
|
|
17
|
+
expect(bridge).toContain('profiler: profiler');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('keeps the bridge opt-in and free of a static remote dependency', () => {
|
|
21
|
+
expect(createApp).toContain('VITE_FORGEAX_ENGINE_BRIDGE');
|
|
22
|
+
expect(createApp).toContain("import('./internal/browser-remote-bridge')");
|
|
23
|
+
expect(bridge).not.toMatch(/^import .*@forgeax\/engine-remote/m);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('keeps profiler failures in the JSON-safe eval envelope', () => {
|
|
27
|
+
expect(bridge).toContain("code: typeof e.code === 'string' ? e.code : 'script-runtime-error'");
|
|
28
|
+
expect(bridge).toContain('detail');
|
|
29
|
+
expect(bridge).toContain('JSON.stringify(envelope)');
|
|
30
|
+
});
|
|
31
|
+
});
|