@forgeax/engine-app 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +418 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/animation-asset-lookup.unit.test.d.ts +2 -0
- package/dist/__tests__/animation-asset-lookup.unit.test.d.ts.map +1 -0
- package/dist/__tests__/app-rebuild.test.d.ts +2 -0
- package/dist/__tests__/app-rebuild.test.d.ts.map +1 -0
- package/dist/__tests__/asset-registry-types.test-d.d.ts +2 -0
- package/dist/__tests__/asset-registry-types.test-d.d.ts.map +1 -0
- package/dist/__tests__/assets-runtime-assembly.integration.test.d.ts +2 -0
- package/dist/__tests__/assets-runtime-assembly.integration.test.d.ts.map +1 -0
- package/dist/__tests__/callback-deletion.unit.test.d.ts +2 -0
- package/dist/__tests__/callback-deletion.unit.test.d.ts.map +1 -0
- package/dist/__tests__/callback-inference.test-d.d.ts +2 -0
- package/dist/__tests__/callback-inference.test-d.d.ts.map +1 -0
- package/dist/__tests__/component-introspection.unit.test.d.ts +2 -0
- package/dist/__tests__/component-introspection.unit.test.d.ts.map +1 -0
- package/dist/__tests__/consumer-migration.unit.test.d.ts +2 -0
- package/dist/__tests__/consumer-migration.unit.test.d.ts.map +1 -0
- package/dist/__tests__/create-app-features.test.d.ts +2 -0
- package/dist/__tests__/create-app-features.test.d.ts.map +1 -0
- package/dist/__tests__/create-app-listener-sync.test.d.ts +2 -0
- package/dist/__tests__/create-app-listener-sync.test.d.ts.map +1 -0
- package/dist/__tests__/create-app-lock-provider.test.d.ts +2 -0
- package/dist/__tests__/create-app-lock-provider.test.d.ts.map +1 -0
- package/dist/__tests__/create-app-plugin.test.d.ts +2 -0
- package/dist/__tests__/create-app-plugin.test.d.ts.map +1 -0
- package/dist/__tests__/create-app-stop.test.d.ts +2 -0
- package/dist/__tests__/create-app-stop.test.d.ts.map +1 -0
- package/dist/__tests__/create-app-time.unit.test.d.ts +2 -0
- package/dist/__tests__/create-app-time.unit.test.d.ts.map +1 -0
- package/dist/__tests__/create-app.test-d.d.ts +2 -0
- package/dist/__tests__/create-app.test-d.d.ts.map +1 -0
- package/dist/__tests__/create-app.test.d.ts +2 -0
- package/dist/__tests__/create-app.test.d.ts.map +1 -0
- package/dist/__tests__/create-renderer-error.integration.test.d.ts +2 -0
- package/dist/__tests__/create-renderer-error.integration.test.d.ts.map +1 -0
- package/dist/__tests__/create-renderer-lifecycle.integration.test.d.ts +2 -0
- package/dist/__tests__/create-renderer-lifecycle.integration.test.d.ts.map +1 -0
- package/dist/__tests__/docs-gate-green.unit.test.d.ts +2 -0
- package/dist/__tests__/docs-gate-green.unit.test.d.ts.map +1 -0
- package/dist/__tests__/draw-source-headless-full-chain.test.d.ts +2 -0
- package/dist/__tests__/draw-source-headless-full-chain.test.d.ts.map +1 -0
- package/dist/__tests__/draw-source-injected-world-update-visibility.test.d.ts +2 -0
- package/dist/__tests__/draw-source-injected-world-update-visibility.test.d.ts.map +1 -0
- package/dist/__tests__/draw-source-single-world-regression.test.d.ts +2 -0
- package/dist/__tests__/draw-source-single-world-regression.test.d.ts.map +1 -0
- package/dist/__tests__/ecs-import.unit.test.d.ts +2 -0
- package/dist/__tests__/ecs-import.unit.test.d.ts.map +1 -0
- package/dist/__tests__/engine-worker-bootstrap-channel.unit.test.d.ts +2 -0
- package/dist/__tests__/engine-worker-bootstrap-channel.unit.test.d.ts.map +1 -0
- package/dist/__tests__/engine-worker-world-swap.unit.test.d.ts +2 -0
- package/dist/__tests__/engine-worker-world-swap.unit.test.d.ts.map +1 -0
- package/dist/__tests__/errors-pointer-lock-failed.test.d.ts +2 -0
- package/dist/__tests__/errors-pointer-lock-failed.test.d.ts.map +1 -0
- package/dist/__tests__/execution-bootstrap-isolation.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-bootstrap-isolation.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-error-detail.test-d.d.ts +2 -0
- package/dist/__tests__/execution-error-detail.test-d.d.ts.map +1 -0
- package/dist/__tests__/execution-error-detail.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-error-detail.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-frame-order.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-frame-order.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-frame-protocol.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-frame-protocol.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-input-sample.contract.test.d.ts +2 -0
- package/dist/__tests__/execution-input-sample.contract.test.d.ts.map +1 -0
- package/dist/__tests__/execution-measurement.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-measurement.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-public-api.test-d.d.ts +2 -0
- package/dist/__tests__/execution-public-api.test-d.d.ts.map +1 -0
- package/dist/__tests__/execution-public-api.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-public-api.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-rebuild.contract.test.d.ts +2 -0
- package/dist/__tests__/execution-rebuild.contract.test.d.ts.map +1 -0
- package/dist/__tests__/execution-report.contract.test.d.ts +2 -0
- package/dist/__tests__/execution-report.contract.test.d.ts.map +1 -0
- package/dist/__tests__/execution-report.types.test-d.d.ts +2 -0
- package/dist/__tests__/execution-report.types.test-d.d.ts.map +1 -0
- package/dist/__tests__/execution-schema-freshness.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-schema-freshness.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-selector.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-selector.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-shared-kernel.unit.test.d.ts +2 -0
- package/dist/__tests__/execution-shared-kernel.unit.test.d.ts.map +1 -0
- package/dist/__tests__/execution-world-health-owner.test-d.d.ts +2 -0
- package/dist/__tests__/execution-world-health-owner.test-d.d.ts.map +1 -0
- package/dist/__tests__/fallback-camera.unit.test.d.ts +2 -0
- package/dist/__tests__/fallback-camera.unit.test.d.ts.map +1 -0
- package/dist/__tests__/frame-loop-time-elapsed.test.d.ts +2 -0
- package/dist/__tests__/frame-loop-time-elapsed.test.d.ts.map +1 -0
- package/dist/__tests__/frame-loop-time.unit.test.d.ts +2 -0
- package/dist/__tests__/frame-loop-time.unit.test.d.ts.map +1 -0
- package/dist/__tests__/frame-loop-world-array.test.d.ts +2 -0
- package/dist/__tests__/frame-loop-world-array.test.d.ts.map +1 -0
- package/dist/__tests__/kernel-worker-runtime.unit.test.d.ts +2 -0
- package/dist/__tests__/kernel-worker-runtime.unit.test.d.ts.map +1 -0
- package/dist/__tests__/migration-surface-inventory.unit.test.d.ts +2 -0
- package/dist/__tests__/migration-surface-inventory.unit.test.d.ts.map +1 -0
- package/dist/__tests__/plugin-policy.integration.test.d.ts +2 -0
- package/dist/__tests__/plugin-policy.integration.test.d.ts.map +1 -0
- package/dist/__tests__/plugin-profiles.unit.test.d.ts +2 -0
- package/dist/__tests__/plugin-profiles.unit.test.d.ts.map +1 -0
- package/dist/__tests__/profiler-default-off.test.d.ts +2 -0
- package/dist/__tests__/profiler-default-off.test.d.ts.map +1 -0
- package/dist/__tests__/profiler-frame-token.test.d.ts +2 -0
- package/dist/__tests__/profiler-frame-token.test.d.ts.map +1 -0
- package/dist/__tests__/profiler-phase-catalog.test.d.ts +2 -0
- package/dist/__tests__/profiler-phase-catalog.test.d.ts.map +1 -0
- package/dist/__tests__/profiler-phase-integration.test.d.ts +2 -0
- package/dist/__tests__/profiler-phase-integration.test.d.ts.map +1 -0
- package/dist/__tests__/profiler-remote-bridge.test.d.ts +2 -0
- package/dist/__tests__/profiler-remote-bridge.test.d.ts.map +1 -0
- package/dist/__tests__/remote-integration.test.d.ts +2 -0
- package/dist/__tests__/remote-integration.test.d.ts.map +1 -0
- package/dist/__tests__/remote-serve-flag.test.d.ts +2 -0
- package/dist/__tests__/remote-serve-flag.test.d.ts.map +1 -0
- package/dist/__tests__/rhi-capture-capability.test-d.d.ts +2 -0
- package/dist/__tests__/rhi-capture-capability.test-d.d.ts.map +1 -0
- package/dist/__tests__/rhi-capture-host.integration.test.d.ts +2 -0
- package/dist/__tests__/rhi-capture-host.integration.test.d.ts.map +1 -0
- package/dist/__tests__/rhi-capture.browser.test.d.ts +2 -0
- package/dist/__tests__/rhi-capture.browser.test.d.ts.map +1 -0
- package/dist/__tests__/surface-handoff.test.d.ts +2 -0
- package/dist/__tests__/surface-handoff.test.d.ts.map +1 -0
- package/dist/__tests__/types-m2.test-d.d.ts +2 -0
- package/dist/__tests__/types-m2.test-d.d.ts.map +1 -0
- package/dist/__tests__/visibility-remote.integration.test.d.ts +2 -0
- package/dist/__tests__/visibility-remote.integration.test.d.ts.map +1 -0
- package/dist/__tests__/worker-execution-stop.test.d.ts +2 -0
- package/dist/__tests__/worker-execution-stop.test.d.ts.map +1 -0
- package/dist/animation-asset-lookup.d.ts +8 -0
- package/dist/animation-asset-lookup.d.ts.map +1 -0
- package/dist/assets-runtime-assembly.d.ts +19 -0
- package/dist/assets-runtime-assembly.d.ts.map +1 -0
- package/dist/canvas-policy.d.ts +4 -0
- package/dist/canvas-policy.d.ts.map +1 -0
- package/dist/create-app.d.ts +57 -0
- package/dist/create-app.d.ts.map +1 -0
- package/dist/engine-worker-runtime.mjs +884 -0
- package/dist/engine-worker-runtime.mjs.map +1 -0
- package/dist/errors.d.ts +263 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/execution/attached-world-swap.d.ts +21 -0
- package/dist/execution/attached-world-swap.d.ts.map +1 -0
- package/dist/execution/bootstrap-entry.d.ts +30 -0
- package/dist/execution/bootstrap-entry.d.ts.map +1 -0
- package/dist/execution/capabilities.d.ts +5 -0
- package/dist/execution/capabilities.d.ts.map +1 -0
- package/dist/execution/control.d.ts +14 -0
- package/dist/execution/control.d.ts.map +1 -0
- package/dist/execution/engine-worker-runtime.d.ts +2 -0
- package/dist/execution/engine-worker-runtime.d.ts.map +1 -0
- package/dist/execution/engine-worker.d.ts +23 -0
- package/dist/execution/engine-worker.d.ts.map +1 -0
- package/dist/execution/host-controller.d.ts +16 -0
- package/dist/execution/host-controller.d.ts.map +1 -0
- package/dist/execution/index.d.ts +10 -0
- package/dist/execution/index.d.ts.map +1 -0
- package/dist/execution/kernel-pool.d.ts +14 -0
- package/dist/execution/kernel-pool.d.ts.map +1 -0
- package/dist/execution/kernel-worker-runtime.d.ts +2 -0
- package/dist/execution/kernel-worker-runtime.d.ts.map +1 -0
- package/dist/execution/measurement.d.ts +7 -0
- package/dist/execution/measurement.d.ts.map +1 -0
- package/dist/execution/protocol.d.ts +84 -0
- package/dist/execution/protocol.d.ts.map +1 -0
- package/dist/execution/report.d.ts +5 -0
- package/dist/execution/report.d.ts.map +1 -0
- package/dist/execution/schema.d.ts +4 -0
- package/dist/execution/schema.d.ts.map +1 -0
- package/dist/execution/selector.d.ts +10 -0
- package/dist/execution/selector.d.ts.map +1 -0
- package/dist/execution/types.d.ts +107 -0
- package/dist/execution/types.d.ts.map +1 -0
- package/dist/fallback-camera.d.ts +8 -0
- package/dist/fallback-camera.d.ts.map +1 -0
- package/dist/game-context.d.ts +98 -0
- package/dist/game-context.d.ts.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +3985 -0
- package/dist/index.mjs.map +1 -0
- package/dist/input-map-plugin.d.ts +5 -0
- package/dist/input-map-plugin.d.ts.map +1 -0
- package/dist/input-plugin.d.ts +4 -0
- package/dist/input-plugin.d.ts.map +1 -0
- package/dist/internal/assembled-engine-profile.d.ts +5 -0
- package/dist/internal/assembled-engine-profile.d.ts.map +1 -0
- package/dist/internal/assets-world-plugin.d.ts +21 -0
- package/dist/internal/assets-world-plugin.d.ts.map +1 -0
- package/dist/internal/browser-remote-bridge.d.ts +25 -0
- package/dist/internal/browser-remote-bridge.d.ts.map +1 -0
- package/dist/internal/browser-rhi-debug-runtime.d.ts +18 -0
- package/dist/internal/browser-rhi-debug-runtime.d.ts.map +1 -0
- package/dist/internal/component-introspection.d.ts +24 -0
- package/dist/internal/component-introspection.d.ts.map +1 -0
- package/dist/internal/ecs-import.d.ts +6 -0
- package/dist/internal/ecs-import.d.ts.map +1 -0
- package/dist/internal/engine-profile-common.d.ts +9 -0
- package/dist/internal/engine-profile-common.d.ts.map +1 -0
- package/dist/internal/error-fanout.d.ts +66 -0
- package/dist/internal/error-fanout.d.ts.map +1 -0
- package/dist/internal/frame-loop.d.ts +35 -0
- package/dist/internal/frame-loop.d.ts.map +1 -0
- package/dist/internal/input-attach.d.ts +44 -0
- package/dist/internal/input-attach.d.ts.map +1 -0
- package/dist/internal/main-engine-profile.d.ts +15 -0
- package/dist/internal/main-engine-profile.d.ts.map +1 -0
- package/dist/internal/remote-serve-flag.d.ts +23 -0
- package/dist/internal/remote-serve-flag.d.ts.map +1 -0
- package/dist/internal/remote-server-plugin.d.ts +8 -0
- package/dist/internal/remote-server-plugin.d.ts.map +1 -0
- package/dist/internal/rhi-capture.d.ts +19 -0
- package/dist/internal/rhi-capture.d.ts.map +1 -0
- package/dist/internal/rhi-debug-flag.d.ts +18 -0
- package/dist/internal/rhi-debug-flag.d.ts.map +1 -0
- package/dist/internal/worker-engine-profile.d.ts +13 -0
- package/dist/internal/worker-engine-profile.d.ts.map +1 -0
- package/dist/kernel-worker-runtime.mjs +69 -0
- package/dist/kernel-worker-runtime.mjs.map +1 -0
- package/dist/load-game-errors.d.ts +131 -0
- package/dist/load-game-errors.d.ts.map +1 -0
- package/dist/load-game.d.ts +34 -0
- package/dist/load-game.d.ts.map +1 -0
- package/dist/plugin-factories.d.ts +3 -0
- package/dist/plugin-factories.d.ts.map +1 -0
- package/dist/tool-preview/bootstrap.d.ts +113 -0
- package/dist/tool-preview/bootstrap.d.ts.map +1 -0
- package/dist/tool-preview/evidence.d.ts +20 -0
- package/dist/tool-preview/evidence.d.ts.map +1 -0
- package/dist/tool-preview/framing.d.ts +13 -0
- package/dist/tool-preview/framing.d.ts.map +1 -0
- package/dist/tool-preview/recipe.d.ts +54 -0
- package/dist/tool-preview/recipe.d.ts.map +1 -0
- package/dist/types.d.ts +444 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +86 -0
- package/schema/execution-report.schema.json +168 -0
- package/src/__tests__/animation-asset-lookup.unit.test.ts +20 -0
- package/src/__tests__/app-rebuild.test.ts +31 -0
- package/src/__tests__/asset-registry-types.test-d.ts +16 -0
- package/src/__tests__/assets-runtime-assembly.integration.test.ts +68 -0
- package/src/__tests__/callback-deletion.unit.test.ts +14 -0
- package/src/__tests__/callback-inference.test-d.ts +32 -0
- package/src/__tests__/component-introspection.unit.test.ts +56 -0
- package/src/__tests__/consumer-migration.unit.test.ts +21 -0
- package/src/__tests__/create-app-features.test.ts +40 -0
- package/src/__tests__/create-app-listener-sync.test.ts +377 -0
- package/src/__tests__/create-app-lock-provider.test.ts +212 -0
- package/src/__tests__/create-app-plugin.test.ts +217 -0
- package/src/__tests__/create-app-stop.test.ts +617 -0
- package/src/__tests__/create-app-time.unit.test.ts +26 -0
- package/src/__tests__/create-app.test-d.ts +82 -0
- package/src/__tests__/create-app.test.ts +346 -0
- package/src/__tests__/create-renderer-error.integration.test.ts +24 -0
- package/src/__tests__/create-renderer-lifecycle.integration.test.ts +19 -0
- package/src/__tests__/docs-gate-green.unit.test.ts +61 -0
- package/src/__tests__/draw-source-headless-full-chain.test.ts +226 -0
- package/src/__tests__/draw-source-injected-world-update-visibility.test.ts +182 -0
- package/src/__tests__/draw-source-single-world-regression.test.ts +185 -0
- package/src/__tests__/ecs-import.unit.test.ts +40 -0
- package/src/__tests__/engine-worker-bootstrap-channel.unit.test.ts +74 -0
- package/src/__tests__/engine-worker-world-swap.unit.test.ts +76 -0
- package/src/__tests__/errors-pointer-lock-failed.test.ts +268 -0
- package/src/__tests__/execution-bootstrap-isolation.unit.test.ts +80 -0
- package/src/__tests__/execution-error-detail.test-d.ts +13 -0
- package/src/__tests__/execution-error-detail.unit.test.ts +35 -0
- package/src/__tests__/execution-frame-order.unit.test.ts +38 -0
- package/src/__tests__/execution-frame-protocol.unit.test.ts +61 -0
- package/src/__tests__/execution-input-sample.contract.test.ts +23 -0
- package/src/__tests__/execution-measurement.unit.test.ts +20 -0
- package/src/__tests__/execution-public-api.test-d.ts +10 -0
- package/src/__tests__/execution-public-api.unit.test.ts +9 -0
- package/src/__tests__/execution-rebuild.contract.test.ts +36 -0
- package/src/__tests__/execution-report.contract.test.ts +50 -0
- package/src/__tests__/execution-report.types.test-d.ts +16 -0
- package/src/__tests__/execution-schema-freshness.unit.test.ts +20 -0
- package/src/__tests__/execution-selector.unit.test.ts +72 -0
- package/src/__tests__/execution-shared-kernel.unit.test.ts +180 -0
- package/src/__tests__/execution-world-health-owner.test-d.ts +9 -0
- package/src/__tests__/fallback-camera.unit.test.ts +32 -0
- package/src/__tests__/frame-loop-time-elapsed.test.ts +115 -0
- package/src/__tests__/frame-loop-time.unit.test.ts +61 -0
- package/src/__tests__/frame-loop-world-array.test.ts +441 -0
- package/src/__tests__/kernel-worker-runtime.unit.test.ts +50 -0
- package/src/__tests__/migration-surface-inventory.unit.test.ts +13 -0
- package/src/__tests__/plugin-policy.integration.test.ts +15 -0
- package/src/__tests__/plugin-profiles.unit.test.ts +190 -0
- package/src/__tests__/profiler-default-off.test.ts +53 -0
- package/src/__tests__/profiler-frame-token.test.ts +248 -0
- package/src/__tests__/profiler-phase-catalog.test.ts +30 -0
- package/src/__tests__/profiler-phase-integration.test.ts +129 -0
- package/src/__tests__/profiler-remote-bridge.test.ts +31 -0
- package/src/__tests__/remote-integration.test.ts +224 -0
- package/src/__tests__/remote-serve-flag.test.ts +24 -0
- package/src/__tests__/rhi-capture-capability.test-d.ts +22 -0
- package/src/__tests__/rhi-capture-host.integration.test.ts +157 -0
- package/src/__tests__/rhi-capture.browser.test.ts +51 -0
- package/src/__tests__/surface-handoff.test.ts +92 -0
- package/src/__tests__/types-m2.test-d.ts +81 -0
- package/src/__tests__/visibility-remote.integration.test.ts +119 -0
- package/src/__tests__/worker-execution-stop.test.ts +116 -0
- package/src/animation-asset-lookup.ts +13 -0
- package/src/assets-runtime-assembly.ts +70 -0
- package/src/canvas-policy.ts +16 -0
- package/src/create-app.ts +1351 -0
- package/src/errors.ts +454 -0
- package/src/execution/attached-world-swap.ts +67 -0
- package/src/execution/bootstrap-entry.ts +122 -0
- package/src/execution/capabilities.ts +102 -0
- package/src/execution/control.ts +52 -0
- package/src/execution/engine-worker-runtime.ts +400 -0
- package/src/execution/engine-worker.ts +161 -0
- package/src/execution/host-controller.ts +454 -0
- package/src/execution/index.ts +43 -0
- package/src/execution/kernel-pool.ts +236 -0
- package/src/execution/kernel-worker-runtime.ts +103 -0
- package/src/execution/measurement.ts +39 -0
- package/src/execution/protocol.ts +131 -0
- package/src/execution/report.ts +66 -0
- package/src/execution/schema.ts +168 -0
- package/src/execution/selector.ts +98 -0
- package/src/execution/types.ts +146 -0
- package/src/fallback-camera.ts +20 -0
- package/src/game-context.ts +126 -0
- package/src/index.ts +176 -0
- package/src/input-map-plugin.ts +21 -0
- package/src/input-plugin.ts +26 -0
- package/src/internal/assembled-engine-profile.ts +22 -0
- package/src/internal/assets-world-plugin.ts +79 -0
- package/src/internal/browser-remote-bridge.ts +267 -0
- package/src/internal/browser-rhi-debug-runtime.ts +67 -0
- package/src/internal/component-introspection.ts +116 -0
- package/src/internal/ecs-import.ts +41 -0
- package/src/internal/engine-profile-common.ts +10 -0
- package/src/internal/error-fanout.ts +129 -0
- package/src/internal/frame-loop.ts +525 -0
- package/src/internal/input-attach.ts +104 -0
- package/src/internal/main-engine-profile.ts +80 -0
- package/src/internal/remote-serve-flag.ts +50 -0
- package/src/internal/remote-server-plugin.ts +16 -0
- package/src/internal/rhi-capture.ts +211 -0
- package/src/internal/rhi-debug-flag.ts +40 -0
- package/src/internal/worker-engine-profile.ts +47 -0
- package/src/load-game-errors.ts +211 -0
- package/src/load-game.ts +115 -0
- package/src/plugin-factories.ts +2 -0
- package/src/tool-preview/bootstrap.ts +848 -0
- package/src/tool-preview/evidence.ts +59 -0
- package/src/tool-preview/framing.ts +52 -0
- package/src/tool-preview/recipe.ts +137 -0
- package/src/types.ts +505 -0
- package/src/vite-env.d.ts +12 -0
|
@@ -0,0 +1,848 @@
|
|
|
1
|
+
import { createProfiler } from '@forgeax/engine-profiler';
|
|
2
|
+
import {
|
|
3
|
+
buildTapeIndex,
|
|
4
|
+
decodeTape,
|
|
5
|
+
decodeToRgba8,
|
|
6
|
+
openReplay,
|
|
7
|
+
type V7Tape,
|
|
8
|
+
} from '@forgeax/engine-rhi-debug';
|
|
9
|
+
import {
|
|
10
|
+
type ArtifactRef,
|
|
11
|
+
createPreviewArtifactManifest,
|
|
12
|
+
type PreviewArtifactManifest,
|
|
13
|
+
type SnapshotRef,
|
|
14
|
+
type ToolTiming,
|
|
15
|
+
} from '@forgeax/engine-tool-runtime';
|
|
16
|
+
import { err, ok, type Result } from '@forgeax/engine-types';
|
|
17
|
+
import { createApp } from '../create-app';
|
|
18
|
+
import { createBrowserRhiDebugRuntime } from '../internal/browser-rhi-debug-runtime';
|
|
19
|
+
import type { App, BundlerOptions, CreateAppOptions } from '../types';
|
|
20
|
+
import {
|
|
21
|
+
createToolPreviewRecipe,
|
|
22
|
+
type ToolPreviewAction,
|
|
23
|
+
type ToolPreviewRecipe,
|
|
24
|
+
type ToolPreviewTrace,
|
|
25
|
+
} from './recipe';
|
|
26
|
+
|
|
27
|
+
export type ToolPreviewResourceKind = 'material' | 'mesh' | 'texture' | 'vfx';
|
|
28
|
+
|
|
29
|
+
export interface ToolPreviewResourceRequest {
|
|
30
|
+
readonly kind: ToolPreviewResourceKind;
|
|
31
|
+
readonly guid: string;
|
|
32
|
+
readonly size?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ToolPreviewResourceFacts {
|
|
36
|
+
readonly kind: ToolPreviewResourceKind;
|
|
37
|
+
readonly guid: string;
|
|
38
|
+
readonly asset: unknown;
|
|
39
|
+
readonly digest?: string;
|
|
40
|
+
readonly ownerFacts?: Readonly<Record<string, string | number | boolean>>;
|
|
41
|
+
readonly observation?: Readonly<Record<string, string | number | boolean | readonly number[]>>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ToolPreviewHostOptions {
|
|
45
|
+
readonly runId?: string;
|
|
46
|
+
readonly recipe: ToolPreviewRecipe;
|
|
47
|
+
readonly snapshot: SnapshotRef;
|
|
48
|
+
readonly canvas?: HTMLCanvasElement;
|
|
49
|
+
readonly root?: Document | HTMLElement;
|
|
50
|
+
readonly app?: Omit<CreateAppOptions, 'rhi'>;
|
|
51
|
+
readonly bundler?: BundlerOptions;
|
|
52
|
+
readonly resource?: ToolPreviewResourceRequest;
|
|
53
|
+
readonly prepare?: (
|
|
54
|
+
app: App,
|
|
55
|
+
) => void | ToolPreviewResourceFacts | Promise<void | ToolPreviewResourceFacts>;
|
|
56
|
+
/** Refresh producer-owned resource facts after the bounded real frame run. */
|
|
57
|
+
readonly collectResourceFacts?: (
|
|
58
|
+
app: App,
|
|
59
|
+
current: ToolPreviewResourceFacts | undefined,
|
|
60
|
+
) => void | ToolPreviewResourceFacts | Promise<void | ToolPreviewResourceFacts>;
|
|
61
|
+
/** Dispose producer-owned preview attachments before the App. */
|
|
62
|
+
readonly onDispose?: (app: App) => void | Promise<void>;
|
|
63
|
+
readonly executeAction?: (action: ToolPreviewAction, app: App) => boolean | Promise<boolean>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ToolPreviewHostError {
|
|
67
|
+
readonly code: 'tool-preview-capability-unavailable' | 'tool-preview-bootstrap-failed';
|
|
68
|
+
readonly expected: string;
|
|
69
|
+
readonly hint: string;
|
|
70
|
+
readonly detail: { readonly phase: string; readonly cause?: unknown };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ToolPreviewHost {
|
|
74
|
+
readonly app: App;
|
|
75
|
+
readonly recipe: ToolPreviewRecipe;
|
|
76
|
+
readonly trace: ToolPreviewTrace;
|
|
77
|
+
capture(): Promise<Result<ToolPreviewCaptureResult, ToolPreviewHostError>>;
|
|
78
|
+
run(): Promise<Result<ToolPreviewRunResult, ToolPreviewHostError>>;
|
|
79
|
+
dispose(): Promise<Result<void, ToolPreviewHostError>>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ToolPreviewCaptureResult {
|
|
83
|
+
readonly recipe: ToolPreviewRecipe;
|
|
84
|
+
readonly snapshot: SnapshotRef;
|
|
85
|
+
readonly trace: ToolPreviewTrace;
|
|
86
|
+
readonly captureId: string;
|
|
87
|
+
readonly actionTrace: readonly ToolPreviewAction[];
|
|
88
|
+
readonly tape: {
|
|
89
|
+
readonly runId: string;
|
|
90
|
+
readonly jsonUri: string;
|
|
91
|
+
readonly blobUri: string;
|
|
92
|
+
readonly byteLength: number;
|
|
93
|
+
};
|
|
94
|
+
readonly profile: { readonly captureId: string; readonly uri: string };
|
|
95
|
+
readonly capturePng: { readonly uri: string; readonly width: number; readonly height: number };
|
|
96
|
+
readonly executeDurationMs: number;
|
|
97
|
+
readonly captureDurationMs: number;
|
|
98
|
+
readonly browserJsHeapBytes?: number;
|
|
99
|
+
readonly appErrors: readonly unknown[];
|
|
100
|
+
readonly resource?: ToolPreviewResourceFacts;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface ToolPreviewRunResult {
|
|
104
|
+
readonly trace: ToolPreviewTrace;
|
|
105
|
+
readonly captureId: string;
|
|
106
|
+
readonly drawCalls: number;
|
|
107
|
+
readonly committedDrawIndex: number;
|
|
108
|
+
readonly nonBlackPixels: number;
|
|
109
|
+
readonly actionTrace: readonly ToolPreviewAction[];
|
|
110
|
+
readonly tape: {
|
|
111
|
+
readonly runId: string;
|
|
112
|
+
readonly jsonUri: string;
|
|
113
|
+
readonly blobUri: string;
|
|
114
|
+
readonly byteLength: number;
|
|
115
|
+
};
|
|
116
|
+
readonly profile: { readonly captureId: string; readonly uri: string };
|
|
117
|
+
readonly capturePng: { readonly uri: string; readonly width: number; readonly height: number };
|
|
118
|
+
readonly png: { readonly uri: string; readonly width: number; readonly height: number };
|
|
119
|
+
readonly manifest: PreviewArtifactManifest;
|
|
120
|
+
readonly artifacts: readonly ArtifactRef[];
|
|
121
|
+
readonly operationTiming: ToolTiming;
|
|
122
|
+
readonly resource?: ToolPreviewResourceFacts;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function layoutCanvas(canvas: HTMLCanvasElement, recipe: ToolPreviewRecipe): void {
|
|
126
|
+
canvas.width = recipe.viewport.width;
|
|
127
|
+
canvas.height = recipe.viewport.height;
|
|
128
|
+
canvas.style.position = 'fixed';
|
|
129
|
+
canvas.style.inset = '0';
|
|
130
|
+
canvas.style.width = `${recipe.viewport.width}px`;
|
|
131
|
+
canvas.style.height = `${recipe.viewport.height}px`;
|
|
132
|
+
canvas.style.opacity = '1';
|
|
133
|
+
canvas.style.visibility = 'visible';
|
|
134
|
+
canvas.style.pointerEvents = 'auto';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function appendCanvas(canvas: HTMLCanvasElement, root: Document | HTMLElement | undefined): void {
|
|
138
|
+
if (canvas.isConnected) return;
|
|
139
|
+
const parent =
|
|
140
|
+
typeof Document !== 'undefined' && root instanceof Document
|
|
141
|
+
? root.body
|
|
142
|
+
: (root ?? (typeof document === 'undefined' ? undefined : document.body));
|
|
143
|
+
parent?.appendChild(canvas);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function removeOwnedCanvas(canvas: HTMLCanvasElement, owned: boolean): void {
|
|
147
|
+
if (owned && canvas.isConnected) canvas.remove();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function bytesToDataUri(bytes: Uint8Array, mediaType: string): string {
|
|
151
|
+
let binary = '';
|
|
152
|
+
for (let offset = 0; offset < bytes.length; offset += 0x2000) {
|
|
153
|
+
binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x2000));
|
|
154
|
+
}
|
|
155
|
+
return `data:${mediaType};base64,${btoa(binary)}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function sha256(bytes: Uint8Array): Promise<string> {
|
|
159
|
+
const digest = await crypto.subtle.digest('SHA-256', new Uint8Array(bytes).buffer as ArrayBuffer);
|
|
160
|
+
return `sha256:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, '0')).join('')}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function dataUriBytes(uri: string): Uint8Array {
|
|
164
|
+
const encoded = uri.slice(uri.indexOf(',') + 1);
|
|
165
|
+
const binary = atob(encoded);
|
|
166
|
+
return Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function generateRunId(): string {
|
|
170
|
+
return `preview-${crypto.randomUUID()}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function countDrawCalls(tape: V7Tape): number {
|
|
174
|
+
return buildTapeIndex(tape).works.filter((work) => isRenderDraw(work.kind)).length;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function lastRenderDrawIndex(tape: V7Tape): number {
|
|
178
|
+
const works = buildTapeIndex(tape).works;
|
|
179
|
+
return works.reduce((last, work) => (isRenderDraw(work.kind) ? work.workIndex : last), -1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function isRenderDraw(kind: ReturnType<typeof buildTapeIndex>['works'][number]['kind']): boolean {
|
|
183
|
+
return (
|
|
184
|
+
kind === 'draw' ||
|
|
185
|
+
kind === 'drawIndexed' ||
|
|
186
|
+
kind === 'drawIndirect' ||
|
|
187
|
+
kind === 'drawIndexedIndirect'
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function resourceSubjectDigest(resource: ToolPreviewResourceFacts | undefined): string | undefined {
|
|
192
|
+
if (resource === undefined) return undefined;
|
|
193
|
+
const asset =
|
|
194
|
+
typeof resource.asset === 'object' && resource.asset !== null
|
|
195
|
+
? (resource.asset as Record<string, unknown>)
|
|
196
|
+
: undefined;
|
|
197
|
+
const candidates = [
|
|
198
|
+
resource.observation?.subjectDigest,
|
|
199
|
+
resource.digest,
|
|
200
|
+
asset?.digest,
|
|
201
|
+
asset?.programFingerprint,
|
|
202
|
+
];
|
|
203
|
+
return candidates.find((value): value is string => typeof value === 'string' && value.length > 0);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function replayToolPreviewCapture(
|
|
207
|
+
capture: ToolPreviewCaptureResult,
|
|
208
|
+
): Promise<Result<ToolPreviewRunResult, ToolPreviewHostError>> {
|
|
209
|
+
const analyzeStartedAtMs = performance.now();
|
|
210
|
+
const tapeBytes = dataUriBytes(capture.tape.jsonUri);
|
|
211
|
+
const parsed = decodeTape(tapeBytes);
|
|
212
|
+
if (!parsed.ok) {
|
|
213
|
+
return err({
|
|
214
|
+
code: 'tool-preview-bootstrap-failed',
|
|
215
|
+
expected: 'the finalized tape to deserialize for fresh-device replay',
|
|
216
|
+
hint: parsed.error.hint,
|
|
217
|
+
detail: { phase: 'tape-parse', cause: parsed.error },
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
const subjectDigest = resourceSubjectDigest(capture.resource);
|
|
221
|
+
if (capture.resource !== undefined && subjectDigest === undefined) {
|
|
222
|
+
return err({
|
|
223
|
+
code: 'tool-preview-bootstrap-failed',
|
|
224
|
+
expected: 'the resource owner to publish a subject identity before evidence publication',
|
|
225
|
+
hint: 'repair AssetRegistry owner facts or post-frame observation; recipe and trace are not subject identity',
|
|
226
|
+
detail: { phase: 'resource-identity' },
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
let runtime: Awaited<ReturnType<typeof createBrowserRhiDebugRuntime>>;
|
|
230
|
+
try {
|
|
231
|
+
runtime = await createBrowserRhiDebugRuntime();
|
|
232
|
+
} catch (cause) {
|
|
233
|
+
return err({
|
|
234
|
+
code: 'tool-preview-capability-unavailable',
|
|
235
|
+
expected: 'a fresh browser WebGPU runtime for offline replay',
|
|
236
|
+
hint: 'Run replay in an isolated browser process; do not reuse the capture device.',
|
|
237
|
+
detail: { phase: 'replay-runtime', cause },
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
const replayDevice = await runtime.createReplayDevice();
|
|
241
|
+
if (!replayDevice.ok) {
|
|
242
|
+
return err({
|
|
243
|
+
code: 'tool-preview-capability-unavailable',
|
|
244
|
+
expected: 'a fresh WebGPU device for offline replay',
|
|
245
|
+
hint: 'the capture device cannot be reused as a substitute for fresh-device replay',
|
|
246
|
+
detail: { phase: 'replay-device', cause: replayDevice.error },
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
const replayResult = await openReplay(parsed.value, {
|
|
250
|
+
device: replayDevice.value,
|
|
251
|
+
createShaderModule: runtime.createShaderModule,
|
|
252
|
+
});
|
|
253
|
+
if (!replayResult.ok) {
|
|
254
|
+
return err({
|
|
255
|
+
code: 'tool-preview-bootstrap-failed',
|
|
256
|
+
expected: 'the self-contained tape to replay on a fresh WebGPU device',
|
|
257
|
+
hint: replayResult.error.hint,
|
|
258
|
+
detail: { phase: 'replay-create', cause: replayResult.error },
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
const replay = replayResult.value;
|
|
262
|
+
const drawCalls = countDrawCalls(parsed.value);
|
|
263
|
+
if (drawCalls === 0) {
|
|
264
|
+
await replay.dispose();
|
|
265
|
+
return err({
|
|
266
|
+
code: 'tool-preview-bootstrap-failed',
|
|
267
|
+
expected: 'the preview recipe to submit at least one render draw',
|
|
268
|
+
hint: 'seed a visible mesh and camera before capturing the preview frame',
|
|
269
|
+
detail: { phase: 'replay-draws', cause: { appErrors: capture.appErrors } },
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
const committedDrawIndex = lastRenderDrawIndex(parsed.value);
|
|
273
|
+
if (committedDrawIndex < 0) {
|
|
274
|
+
await replay.dispose();
|
|
275
|
+
return err({
|
|
276
|
+
code: 'tool-preview-bootstrap-failed',
|
|
277
|
+
expected: 'the preview tape to contain a render draw after compute work',
|
|
278
|
+
hint: 'seed a visible render output after simulation dispatches before capturing the preview frame',
|
|
279
|
+
detail: { phase: 'replay-draws', cause: { appErrors: capture.appErrors } },
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
const committed = await replay.inspectWork(committedDrawIndex, ['pixels']);
|
|
283
|
+
if (!committed.ok || committed.value.attachment === undefined) {
|
|
284
|
+
await replay.dispose();
|
|
285
|
+
return err({
|
|
286
|
+
code: 'tool-preview-bootstrap-failed',
|
|
287
|
+
expected: 'the fresh replay session to expose a color attachment for the final render work',
|
|
288
|
+
hint: committed.ok
|
|
289
|
+
? 'the tape contains no readable color attachment for PNG evidence'
|
|
290
|
+
: committed.error.hint,
|
|
291
|
+
detail: { phase: 'replay-inspect', cause: committed.ok ? undefined : committed.error },
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
const attachment = committed.value.attachment;
|
|
295
|
+
const width = attachment.width ?? 0;
|
|
296
|
+
const height = attachment.height ?? 0;
|
|
297
|
+
const rgba =
|
|
298
|
+
attachment.format === undefined
|
|
299
|
+
? undefined
|
|
300
|
+
: decodeToRgba8(attachment.bytes, attachment.format, width, height);
|
|
301
|
+
if (rgba === null || rgba === undefined || width <= 0 || height <= 0) {
|
|
302
|
+
await replay.dispose();
|
|
303
|
+
return err({
|
|
304
|
+
code: 'tool-preview-bootstrap-failed',
|
|
305
|
+
expected: 'the replayed color attachment to have a supported display format',
|
|
306
|
+
hint: 'move PNG encoding to the producer-owned preview shell for unsupported formats',
|
|
307
|
+
detail: { phase: 'png-readback', cause: { format: attachment.format, width, height } },
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
const outputCanvas = document.createElement('canvas');
|
|
311
|
+
outputCanvas.width = width;
|
|
312
|
+
outputCanvas.height = height;
|
|
313
|
+
const outputContext = outputCanvas.getContext('2d');
|
|
314
|
+
if (outputContext === null) {
|
|
315
|
+
await replay.dispose();
|
|
316
|
+
return err({
|
|
317
|
+
code: 'tool-preview-bootstrap-failed',
|
|
318
|
+
expected: 'the browser to provide a 2D canvas context for replay PNG encoding',
|
|
319
|
+
hint: 'enable the browser canvas 2D context before running the preview host',
|
|
320
|
+
detail: { phase: 'png-encode' },
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
outputContext.putImageData(new ImageData(rgba, width, height), 0, 0);
|
|
324
|
+
await replay.dispose();
|
|
325
|
+
const pixels = outputContext.getImageData(0, 0, width, height).data;
|
|
326
|
+
let nonBlackPixels = 0;
|
|
327
|
+
if (pixels !== undefined) {
|
|
328
|
+
for (let offset = 0; offset < pixels.length; offset += 4) {
|
|
329
|
+
if ((pixels[offset] ?? 0) + (pixels[offset + 1] ?? 0) + (pixels[offset + 2] ?? 0) > 0) {
|
|
330
|
+
nonBlackPixels += 1;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (nonBlackPixels === 0) {
|
|
335
|
+
return err({
|
|
336
|
+
code: 'tool-preview-bootstrap-failed',
|
|
337
|
+
expected: 'the replayed PNG to contain non-black pixels',
|
|
338
|
+
hint: 'inspect the RHI tape and renderer draw submission before accepting the preview',
|
|
339
|
+
detail: { phase: 'png-non-black' },
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
const analyzeDurationMs = performance.now() - analyzeStartedAtMs;
|
|
343
|
+
const finalizeStartedAtMs = performance.now();
|
|
344
|
+
const pngUri = outputCanvas.toDataURL('image/png');
|
|
345
|
+
const pngBytes = dataUriBytes(pngUri);
|
|
346
|
+
if (
|
|
347
|
+
!capture.capturePng.uri.startsWith('data:image/png') ||
|
|
348
|
+
capture.capturePng.width <= 0 ||
|
|
349
|
+
capture.capturePng.height <= 0
|
|
350
|
+
) {
|
|
351
|
+
return err({
|
|
352
|
+
code: 'tool-preview-bootstrap-failed',
|
|
353
|
+
expected: 'the capture browser to provide a PNG before fresh replay',
|
|
354
|
+
hint: 'capture the rendered canvas before closing the capture carrier; do not reuse the replay PNG',
|
|
355
|
+
detail: { phase: 'capture-png' },
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
const capturePngBytes = dataUriBytes(capture.capturePng.uri);
|
|
359
|
+
if (capturePngBytes.byteLength === 0) {
|
|
360
|
+
return err({
|
|
361
|
+
code: 'tool-preview-bootstrap-failed',
|
|
362
|
+
expected: 'the capture PNG to contain encoded bytes',
|
|
363
|
+
hint: 'repair the capture carrier readback before publishing evidence',
|
|
364
|
+
detail: { phase: 'capture-png' },
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
const profileBytes = dataUriBytes(capture.profile.uri);
|
|
368
|
+
const tapeDigest = await sha256(tapeBytes);
|
|
369
|
+
const capturePngDigest = await sha256(capturePngBytes);
|
|
370
|
+
const pngDigest = await sha256(pngBytes);
|
|
371
|
+
const profileDigest = await sha256(profileBytes);
|
|
372
|
+
const presentationDigest = await sha256(new TextEncoder().encode(capture.trace.presentation));
|
|
373
|
+
const manifest = createPreviewArtifactManifest({
|
|
374
|
+
schemaVersion: '2.0.0',
|
|
375
|
+
identity: {
|
|
376
|
+
runId: capture.tape.runId,
|
|
377
|
+
snapshotDigest: capture.snapshot.digest,
|
|
378
|
+
subjectDigest: subjectDigest ?? capture.snapshot.digest,
|
|
379
|
+
presentationDigest,
|
|
380
|
+
frameId: 0,
|
|
381
|
+
captureId: capture.captureId,
|
|
382
|
+
},
|
|
383
|
+
artifacts: [
|
|
384
|
+
{
|
|
385
|
+
owner: 'rhi-debug',
|
|
386
|
+
kind: 'rhi-tape',
|
|
387
|
+
role: 'rhi-tape',
|
|
388
|
+
uri: capture.tape.jsonUri,
|
|
389
|
+
digest: tapeDigest,
|
|
390
|
+
byteLength: tapeBytes.byteLength,
|
|
391
|
+
mediaType: 'application/json',
|
|
392
|
+
derivedFrom: [],
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
owner: 'visual',
|
|
396
|
+
kind: 'png',
|
|
397
|
+
role: 'capture',
|
|
398
|
+
uri: capture.capturePng.uri,
|
|
399
|
+
digest: capturePngDigest,
|
|
400
|
+
byteLength: capturePngBytes.byteLength,
|
|
401
|
+
mediaType: 'image/png',
|
|
402
|
+
derivedFrom: [tapeDigest],
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
owner: 'rhi-debug-replay',
|
|
406
|
+
kind: 'png',
|
|
407
|
+
role: 'fresh-replay',
|
|
408
|
+
uri: pngUri,
|
|
409
|
+
digest: pngDigest,
|
|
410
|
+
byteLength: pngBytes.byteLength,
|
|
411
|
+
mediaType: 'image/png',
|
|
412
|
+
derivedFrom: [tapeDigest, capturePngDigest],
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
owner: 'profiler',
|
|
416
|
+
kind: 'profile-capture',
|
|
417
|
+
role: 'profile-capture',
|
|
418
|
+
uri: capture.profile.uri,
|
|
419
|
+
digest: profileDigest,
|
|
420
|
+
byteLength: profileBytes.byteLength,
|
|
421
|
+
mediaType: 'application/json',
|
|
422
|
+
derivedFrom: [],
|
|
423
|
+
},
|
|
424
|
+
],
|
|
425
|
+
});
|
|
426
|
+
const finalizeDurationMs = performance.now() - finalizeStartedAtMs;
|
|
427
|
+
const durationMs =
|
|
428
|
+
capture.executeDurationMs + capture.captureDurationMs + analyzeDurationMs + finalizeDurationMs;
|
|
429
|
+
const memory = (
|
|
430
|
+
performance as Performance & {
|
|
431
|
+
readonly memory?: { readonly usedJSHeapSize?: number };
|
|
432
|
+
}
|
|
433
|
+
).memory?.usedJSHeapSize;
|
|
434
|
+
const browserJsHeapBytes =
|
|
435
|
+
typeof memory === 'number' && Number.isFinite(memory) && memory >= 0
|
|
436
|
+
? Math.max(Math.round(memory), capture.browserJsHeapBytes ?? 0)
|
|
437
|
+
: capture.browserJsHeapBytes;
|
|
438
|
+
return ok({
|
|
439
|
+
trace: capture.trace,
|
|
440
|
+
captureId: capture.captureId,
|
|
441
|
+
drawCalls,
|
|
442
|
+
committedDrawIndex,
|
|
443
|
+
nonBlackPixels,
|
|
444
|
+
actionTrace: capture.actionTrace,
|
|
445
|
+
tape: capture.tape,
|
|
446
|
+
profile: capture.profile,
|
|
447
|
+
capturePng: capture.capturePng,
|
|
448
|
+
png: { uri: pngUri, width: outputCanvas.width, height: outputCanvas.height },
|
|
449
|
+
manifest,
|
|
450
|
+
artifacts: manifest.artifacts.flatMap((artifact) => {
|
|
451
|
+
if (
|
|
452
|
+
artifact.kind !== 'rhi-tape' &&
|
|
453
|
+
artifact.kind !== 'png' &&
|
|
454
|
+
artifact.kind !== 'profile-capture'
|
|
455
|
+
) {
|
|
456
|
+
return [];
|
|
457
|
+
}
|
|
458
|
+
return [
|
|
459
|
+
{
|
|
460
|
+
kind: artifact.kind,
|
|
461
|
+
digest: artifact.digest,
|
|
462
|
+
uri: artifact.uri,
|
|
463
|
+
sizeBytes: artifact.byteLength,
|
|
464
|
+
},
|
|
465
|
+
];
|
|
466
|
+
}),
|
|
467
|
+
operationTiming: {
|
|
468
|
+
startedAtMs: 0,
|
|
469
|
+
endedAtMs: durationMs,
|
|
470
|
+
durationMs,
|
|
471
|
+
phases: {
|
|
472
|
+
lookup: { status: 'not-applicable' },
|
|
473
|
+
lease: { status: 'not-applicable' },
|
|
474
|
+
transport: { status: 'not-applicable' },
|
|
475
|
+
execute: {
|
|
476
|
+
status: 'observed',
|
|
477
|
+
durationMs: capture.executeDurationMs,
|
|
478
|
+
workUnits: capture.recipe.frames,
|
|
479
|
+
},
|
|
480
|
+
capture: { status: 'observed', durationMs: capture.captureDurationMs },
|
|
481
|
+
finalize: { status: 'observed', durationMs: finalizeDurationMs },
|
|
482
|
+
analyze: { status: 'observed', durationMs: analyzeDurationMs },
|
|
483
|
+
},
|
|
484
|
+
unattributedMs: 0,
|
|
485
|
+
resources: {
|
|
486
|
+
'node-rss': { status: 'not-applicable' },
|
|
487
|
+
'browser-js-heap':
|
|
488
|
+
browserJsHeapBytes === undefined
|
|
489
|
+
? { status: 'unavailable', reason: 'performance.memory is not exposed' }
|
|
490
|
+
: {
|
|
491
|
+
status: 'observed',
|
|
492
|
+
bytes: browserJsHeapBytes,
|
|
493
|
+
source: 'maximum performance.memory.usedJSHeapSize across capture and replay',
|
|
494
|
+
},
|
|
495
|
+
'artifact-bytes': {
|
|
496
|
+
status: 'observed',
|
|
497
|
+
bytes:
|
|
498
|
+
tapeBytes.byteLength +
|
|
499
|
+
dataUriBytes(capture.capturePng.uri).byteLength +
|
|
500
|
+
pngBytes.byteLength +
|
|
501
|
+
profileBytes.byteLength,
|
|
502
|
+
source: 'browser-produced artifact payloads',
|
|
503
|
+
},
|
|
504
|
+
'gpu-memory': {
|
|
505
|
+
status: 'unavailable',
|
|
506
|
+
reason: 'WebGPU does not expose portable allocation totals',
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
...(capture.resource === undefined ? {} : { resource: capture.resource }),
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export async function createToolPreviewHost(
|
|
515
|
+
options: ToolPreviewHostOptions,
|
|
516
|
+
): Promise<Result<ToolPreviewHost, ToolPreviewHostError>> {
|
|
517
|
+
const recipe = createToolPreviewRecipe(options.recipe);
|
|
518
|
+
const runId = options.runId ?? generateRunId();
|
|
519
|
+
const doc = options.root ?? (typeof document === 'undefined' ? undefined : document);
|
|
520
|
+
if (options.canvas === undefined && doc === undefined) {
|
|
521
|
+
return err({
|
|
522
|
+
code: 'tool-preview-capability-unavailable',
|
|
523
|
+
expected: 'a DOM document or caller-owned HTMLCanvasElement',
|
|
524
|
+
hint: 'run the browser host or provide an attached canvas; headless does not mean RHI-null',
|
|
525
|
+
detail: { phase: 'canvas' },
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
const canvas =
|
|
529
|
+
options.canvas ??
|
|
530
|
+
(typeof Document !== 'undefined' && doc instanceof Document
|
|
531
|
+
? doc.createElement('canvas')
|
|
532
|
+
: undefined);
|
|
533
|
+
const ownsCanvas = options.canvas === undefined;
|
|
534
|
+
if (canvas === undefined) {
|
|
535
|
+
return err({
|
|
536
|
+
code: 'tool-preview-capability-unavailable',
|
|
537
|
+
expected: 'root document can create an HTMLCanvasElement',
|
|
538
|
+
hint: 'provide a document root or an existing canvas',
|
|
539
|
+
detail: { phase: 'canvas' },
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
layoutCanvas(canvas, recipe);
|
|
543
|
+
appendCanvas(canvas, options.root);
|
|
544
|
+
if (!canvas.isConnected) {
|
|
545
|
+
return err({
|
|
546
|
+
code: 'tool-preview-bootstrap-failed',
|
|
547
|
+
expected: 'preview canvas is connected before App creation',
|
|
548
|
+
hint: 'attach the canvas to the document or pass a connected caller-owned canvas',
|
|
549
|
+
detail: { phase: 'canvas' },
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
let runtime: Awaited<ReturnType<typeof createBrowserRhiDebugRuntime>>;
|
|
554
|
+
try {
|
|
555
|
+
runtime = await createBrowserRhiDebugRuntime();
|
|
556
|
+
} catch (cause) {
|
|
557
|
+
removeOwnedCanvas(canvas, ownsCanvas);
|
|
558
|
+
return err({
|
|
559
|
+
code: 'tool-preview-capability-unavailable',
|
|
560
|
+
expected: 'a real WebGPU adapter and device',
|
|
561
|
+
hint: 'install or enable WebGPU; do not replace this route with RHI-null',
|
|
562
|
+
detail: { phase: 'rhi-debug', cause },
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
const profiler = createProfiler();
|
|
566
|
+
const appResult = await createApp(
|
|
567
|
+
canvas,
|
|
568
|
+
{
|
|
569
|
+
...(options.app ?? {}),
|
|
570
|
+
rhi: runtime.rhi,
|
|
571
|
+
rhiInstrumentation: runtime.rhiInstrumentation,
|
|
572
|
+
profiler,
|
|
573
|
+
},
|
|
574
|
+
options.bundler,
|
|
575
|
+
);
|
|
576
|
+
if (!appResult.ok) {
|
|
577
|
+
await runtime.attachment.dispose();
|
|
578
|
+
removeOwnedCanvas(canvas, ownsCanvas);
|
|
579
|
+
return err({
|
|
580
|
+
code: 'tool-preview-bootstrap-failed',
|
|
581
|
+
expected: 'createApp constructs a real WebGPU Renderer',
|
|
582
|
+
hint: 'inspect the structured App/RHI error; the preview must not fall back to RHI-null',
|
|
583
|
+
detail: { phase: 'renderer', cause: appResult.error },
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
const app = appResult.value;
|
|
587
|
+
const appErrors: unknown[] = [];
|
|
588
|
+
const unsubscribeAppErrors = app.onError((error) => appErrors.push(error));
|
|
589
|
+
const rendererReady = await app.renderer.ready;
|
|
590
|
+
if (!rendererReady.ok) {
|
|
591
|
+
unsubscribeAppErrors();
|
|
592
|
+
await app.dispose();
|
|
593
|
+
await runtime.attachment.dispose();
|
|
594
|
+
removeOwnedCanvas(canvas, ownsCanvas);
|
|
595
|
+
return err({
|
|
596
|
+
code: 'tool-preview-bootstrap-failed',
|
|
597
|
+
expected: 'the real WebGPU Renderer to finish pipeline initialization before preview frames',
|
|
598
|
+
hint: rendererReady.error.hint,
|
|
599
|
+
detail: { phase: 'renderer-ready', cause: rendererReady.error },
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
let resourceFacts: ToolPreviewResourceFacts | undefined;
|
|
603
|
+
try {
|
|
604
|
+
const prepared = await options.prepare?.(app);
|
|
605
|
+
if (prepared !== undefined) resourceFacts = prepared;
|
|
606
|
+
} catch (cause) {
|
|
607
|
+
unsubscribeAppErrors();
|
|
608
|
+
await app.dispose();
|
|
609
|
+
await runtime.attachment.dispose();
|
|
610
|
+
removeOwnedCanvas(canvas, ownsCanvas);
|
|
611
|
+
const bootstrapFailure =
|
|
612
|
+
options.resource === undefined
|
|
613
|
+
? {
|
|
614
|
+
expected: 'the project bootstrap to prepare the preview World',
|
|
615
|
+
hint: 'Repair the project Entry, assets, or scene before retrying the same recipe.',
|
|
616
|
+
phase: 'project-bootstrap',
|
|
617
|
+
}
|
|
618
|
+
: {
|
|
619
|
+
expected: 'the resource preview bootstrap to prepare the preview World',
|
|
620
|
+
hint: 'Repair the resource owner, cooked payload, or preview environment before retrying the same recipe.',
|
|
621
|
+
phase: 'resource-bootstrap',
|
|
622
|
+
};
|
|
623
|
+
return err({
|
|
624
|
+
code: 'tool-preview-bootstrap-failed',
|
|
625
|
+
...bootstrapFailure,
|
|
626
|
+
detail: { phase: bootstrapFailure.phase, cause },
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
const attached = app.renderer.attachWorld(app.world);
|
|
630
|
+
if (!attached.ok) {
|
|
631
|
+
unsubscribeAppErrors();
|
|
632
|
+
await app.dispose();
|
|
633
|
+
await runtime.attachment.dispose();
|
|
634
|
+
removeOwnedCanvas(canvas, ownsCanvas);
|
|
635
|
+
return err({
|
|
636
|
+
code: 'tool-preview-bootstrap-failed',
|
|
637
|
+
expected: 'the preview World to attach to the real Renderer before stepping',
|
|
638
|
+
hint: attached.error.hint,
|
|
639
|
+
detail: { phase: 'world-attach', cause: attached.error },
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
const profileSession = profiler.startCapture({ frameLimit: recipe.frames, eventLimit: 1024 });
|
|
643
|
+
if (!profileSession.ok) {
|
|
644
|
+
await app.dispose();
|
|
645
|
+
await runtime.attachment.dispose();
|
|
646
|
+
removeOwnedCanvas(canvas, ownsCanvas);
|
|
647
|
+
return err({
|
|
648
|
+
code: 'tool-preview-bootstrap-failed',
|
|
649
|
+
expected: 'the App profiler to start a bounded capture',
|
|
650
|
+
hint: profileSession.error.hint,
|
|
651
|
+
detail: { phase: 'profiler', cause: profileSession.error },
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
const detach = runtime.attachRenderer(app.renderer);
|
|
655
|
+
const trace: ToolPreviewTrace = {
|
|
656
|
+
events: ['canvas-created', 'rhi-debug-armed', 'renderer-created'],
|
|
657
|
+
backend: 'webgpu',
|
|
658
|
+
adapter: 'webgpu-adapter',
|
|
659
|
+
presentation: recipe.presentation,
|
|
660
|
+
};
|
|
661
|
+
let disposed = false;
|
|
662
|
+
const capturePreview = async (): Promise<
|
|
663
|
+
Result<ToolPreviewCaptureResult, ToolPreviewHostError>
|
|
664
|
+
> => {
|
|
665
|
+
if (disposed) {
|
|
666
|
+
return err({
|
|
667
|
+
code: 'tool-preview-bootstrap-failed',
|
|
668
|
+
expected: 'preview host remains alive until capture completes',
|
|
669
|
+
hint: 'create a new preview host after dispose',
|
|
670
|
+
detail: { phase: 'capture' },
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
const started = app.start();
|
|
674
|
+
if (!started.ok)
|
|
675
|
+
return err({
|
|
676
|
+
...started.error,
|
|
677
|
+
code: 'tool-preview-bootstrap-failed',
|
|
678
|
+
detail: { phase: 'start', cause: started.error },
|
|
679
|
+
});
|
|
680
|
+
const paused = app.pause();
|
|
681
|
+
if (!paused.ok)
|
|
682
|
+
return err({
|
|
683
|
+
...paused.error,
|
|
684
|
+
code: 'tool-preview-bootstrap-failed',
|
|
685
|
+
detail: { phase: 'pause', cause: paused.error },
|
|
686
|
+
});
|
|
687
|
+
const executeStartedAtMs = performance.now();
|
|
688
|
+
const actionTrace: ToolPreviewAction[] = [];
|
|
689
|
+
const encodedCapture = runtime.attachment.captureFrame();
|
|
690
|
+
for (let frame = 0; frame < recipe.frames; frame += 1) {
|
|
691
|
+
for (const action of recipe.actions.filter((candidate) => candidate.frame === frame)) {
|
|
692
|
+
let accepted = false;
|
|
693
|
+
try {
|
|
694
|
+
accepted = (await options.executeAction?.(action, app)) === true;
|
|
695
|
+
} catch (cause) {
|
|
696
|
+
return err({
|
|
697
|
+
code: 'tool-preview-bootstrap-failed',
|
|
698
|
+
expected: `project action '${action.name}' to execute at frame ${frame}`,
|
|
699
|
+
hint: 'Repair the project-owned preview action handler and rerun the same recipe.',
|
|
700
|
+
detail: { phase: 'action', cause },
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
if (!accepted) {
|
|
704
|
+
return err({
|
|
705
|
+
code: 'tool-preview-bootstrap-failed',
|
|
706
|
+
expected: `project action '${action.name}' to have a registered handler`,
|
|
707
|
+
hint: 'Register the action in the project plugin or remove it from the recipe.',
|
|
708
|
+
detail: { phase: 'action', cause: { frame, name: action.name } },
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
actionTrace.push(action);
|
|
712
|
+
}
|
|
713
|
+
const stepped = app.stepFrame(recipe.deltaSeconds);
|
|
714
|
+
if (!stepped.ok)
|
|
715
|
+
return err({
|
|
716
|
+
...stepped.error,
|
|
717
|
+
code: 'tool-preview-bootstrap-failed',
|
|
718
|
+
detail: { phase: 'frame', cause: stepped.error },
|
|
719
|
+
});
|
|
720
|
+
// Render features may start an asynchronous shader/module bake on their
|
|
721
|
+
// first prepare pass. Yield once between bounded frames so the bake can
|
|
722
|
+
// settle before the next retry; a synchronous tight loop would capture
|
|
723
|
+
// only the feature's intentional `next-frame` pending state.
|
|
724
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
|
725
|
+
}
|
|
726
|
+
// RecorderAttachment captures one complete frame. The first boundary
|
|
727
|
+
// snapshots live resources and the following boundary finalizes it;
|
|
728
|
+
// explicit boundaries keep stepFrame() deterministic for preview hosts.
|
|
729
|
+
await runtime.attachment.frameBoundary();
|
|
730
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
|
731
|
+
await runtime.attachment.frameBoundary();
|
|
732
|
+
if (options.collectResourceFacts !== undefined) {
|
|
733
|
+
try {
|
|
734
|
+
const collected = await options.collectResourceFacts(app, resourceFacts);
|
|
735
|
+
if (collected !== undefined) resourceFacts = collected;
|
|
736
|
+
} catch (cause) {
|
|
737
|
+
return err({
|
|
738
|
+
code: 'tool-preview-bootstrap-failed',
|
|
739
|
+
expected: 'the resource owner to publish post-frame preview facts',
|
|
740
|
+
hint: 'Repair the producer-owned preview observation and retry the same recipe.',
|
|
741
|
+
detail: { phase: 'resource-observation', cause },
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
const executeDurationMs = performance.now() - executeStartedAtMs;
|
|
746
|
+
const finalTrace: ToolPreviewTrace = {
|
|
747
|
+
...trace,
|
|
748
|
+
events: [...trace.events, 'world-updated', 'draw-submitted', 'validation-complete'],
|
|
749
|
+
};
|
|
750
|
+
const captureStartedAtMs = performance.now();
|
|
751
|
+
const profile = profileSession.value.finish();
|
|
752
|
+
if (!profile.ok) {
|
|
753
|
+
return err({
|
|
754
|
+
code: 'tool-preview-bootstrap-failed',
|
|
755
|
+
expected: 'the bounded App profiler to finish with ProfileCapture',
|
|
756
|
+
hint: profile.error.hint,
|
|
757
|
+
detail: { phase: 'profiler-finish', cause: profile.error },
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
const tape = await encodedCapture;
|
|
761
|
+
if (!tape.ok)
|
|
762
|
+
return err({
|
|
763
|
+
code: 'tool-preview-bootstrap-failed',
|
|
764
|
+
expected: 'RHI-debug produces a self-contained tape',
|
|
765
|
+
hint: tape.error.hint,
|
|
766
|
+
detail: { phase: 'rhi-finalize', cause: tape.error },
|
|
767
|
+
});
|
|
768
|
+
const captureDurationMs = performance.now() - captureStartedAtMs;
|
|
769
|
+
const tapeBytes = tape.value.bytes;
|
|
770
|
+
const profileBytes = new TextEncoder().encode(JSON.stringify(profile.value));
|
|
771
|
+
const capturePng = {
|
|
772
|
+
uri: canvas.toDataURL('image/png'),
|
|
773
|
+
width: canvas.width,
|
|
774
|
+
height: canvas.height,
|
|
775
|
+
};
|
|
776
|
+
const memory = (
|
|
777
|
+
performance as Performance & {
|
|
778
|
+
readonly memory?: { readonly usedJSHeapSize?: number };
|
|
779
|
+
}
|
|
780
|
+
).memory?.usedJSHeapSize;
|
|
781
|
+
return ok({
|
|
782
|
+
recipe,
|
|
783
|
+
snapshot: options.snapshot,
|
|
784
|
+
trace: finalTrace,
|
|
785
|
+
captureId: profile.value.captureId,
|
|
786
|
+
actionTrace,
|
|
787
|
+
tape: {
|
|
788
|
+
runId,
|
|
789
|
+
jsonUri: bytesToDataUri(tapeBytes, 'application/octet-stream'),
|
|
790
|
+
blobUri: bytesToDataUri(new Uint8Array(), 'application/octet-stream'),
|
|
791
|
+
byteLength: tapeBytes.byteLength,
|
|
792
|
+
},
|
|
793
|
+
profile: {
|
|
794
|
+
captureId: profile.value.captureId,
|
|
795
|
+
uri: bytesToDataUri(profileBytes, 'application/json'),
|
|
796
|
+
},
|
|
797
|
+
capturePng,
|
|
798
|
+
executeDurationMs,
|
|
799
|
+
captureDurationMs,
|
|
800
|
+
...(typeof memory === 'number' && Number.isFinite(memory) && memory >= 0
|
|
801
|
+
? { browserJsHeapBytes: Math.round(memory) }
|
|
802
|
+
: {}),
|
|
803
|
+
appErrors,
|
|
804
|
+
...(resourceFacts === undefined ? {} : { resource: resourceFacts }),
|
|
805
|
+
});
|
|
806
|
+
};
|
|
807
|
+
return ok({
|
|
808
|
+
app,
|
|
809
|
+
recipe,
|
|
810
|
+
trace,
|
|
811
|
+
capture: capturePreview,
|
|
812
|
+
async run() {
|
|
813
|
+
const capture = await capturePreview();
|
|
814
|
+
if (!capture.ok) return capture;
|
|
815
|
+
return replayToolPreviewCapture(capture.value);
|
|
816
|
+
},
|
|
817
|
+
async dispose() {
|
|
818
|
+
if (disposed) return ok(undefined);
|
|
819
|
+
disposed = true;
|
|
820
|
+
detach();
|
|
821
|
+
unsubscribeAppErrors();
|
|
822
|
+
let disposeError: unknown;
|
|
823
|
+
try {
|
|
824
|
+
await options.onDispose?.(app);
|
|
825
|
+
} catch (cause) {
|
|
826
|
+
disposeError = cause;
|
|
827
|
+
}
|
|
828
|
+
const result = await app.dispose();
|
|
829
|
+
removeOwnedCanvas(canvas, ownsCanvas);
|
|
830
|
+
if (disposeError !== undefined) {
|
|
831
|
+
return err({
|
|
832
|
+
code: 'tool-preview-bootstrap-failed',
|
|
833
|
+
expected: 'producer-owned preview attachments to dispose before the App',
|
|
834
|
+
hint: 'Repair the preview owner lifecycle and retry the same operation.',
|
|
835
|
+
detail: { phase: 'producer-dispose', cause: disposeError },
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
return result.ok
|
|
839
|
+
? ok(undefined)
|
|
840
|
+
: err({
|
|
841
|
+
code: 'tool-preview-bootstrap-failed',
|
|
842
|
+
expected: 'preview app disposes cleanly',
|
|
843
|
+
hint: result.error.hint,
|
|
844
|
+
detail: { phase: 'dispose', cause: result.error },
|
|
845
|
+
});
|
|
846
|
+
},
|
|
847
|
+
});
|
|
848
|
+
}
|