@forgeax/engine-app 0.1.3 → 0.1.6
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/README.md +20 -14
- package/dist/.tsbuildinfo +1 -1
- 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-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/animation-asset-lookup.d.ts +1 -2
- package/dist/animation-asset-lookup.d.ts.map +1 -1
- package/dist/create-app.d.ts +31 -10
- package/dist/create-app.d.ts.map +1 -1
- package/dist/engine-worker-runtime.mjs +72 -129
- package/dist/engine-worker-runtime.mjs.map +1 -1
- package/dist/errors.d.ts +3 -12
- package/dist/errors.d.ts.map +1 -1
- package/dist/execution/attached-world-swap.d.ts +1 -12
- package/dist/execution/attached-world-swap.d.ts.map +1 -1
- package/dist/execution/bootstrap-url.d.ts +5 -0
- package/dist/execution/bootstrap-url.d.ts.map +1 -0
- package/dist/execution/control.d.ts +0 -2
- package/dist/execution/control.d.ts.map +1 -1
- package/dist/execution/engine-worker.d.ts +1 -0
- package/dist/execution/engine-worker.d.ts.map +1 -1
- package/dist/execution/host-controller.d.ts.map +1 -1
- package/dist/execution/index.d.ts +1 -1
- package/dist/execution/index.d.ts.map +1 -1
- package/dist/execution/protocol.d.ts +4 -1
- package/dist/execution/protocol.d.ts.map +1 -1
- package/dist/execution/types.d.ts +17 -1
- package/dist/execution/types.d.ts.map +1 -1
- package/dist/game-context.d.ts +1 -1
- package/dist/game-context.d.ts.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +332 -366
- package/dist/index.mjs.map +1 -1
- package/dist/internal/assembled-engine-profile.d.ts.map +1 -1
- package/dist/internal/assets-world-plugin.d.ts +3 -17
- package/dist/internal/assets-world-plugin.d.ts.map +1 -1
- package/dist/internal/browser-remote-bridge.d.ts +1 -2
- package/dist/internal/browser-remote-bridge.d.ts.map +1 -1
- package/dist/internal/browser-rhi-debug-runtime.d.ts +1 -1
- package/dist/internal/browser-rhi-debug-runtime.d.ts.map +1 -1
- package/dist/internal/debug-draw.d.ts +9 -0
- package/dist/internal/debug-draw.d.ts.map +1 -0
- package/dist/internal/engine-profile-common.d.ts +2 -2
- package/dist/internal/engine-profile-common.d.ts.map +1 -1
- package/dist/internal/frame-loop.d.ts +7 -4
- package/dist/internal/frame-loop.d.ts.map +1 -1
- package/dist/internal/main-engine-profile.d.ts +4 -0
- package/dist/internal/main-engine-profile.d.ts.map +1 -1
- package/dist/internal/remote-serve-flag.d.ts +3 -7
- package/dist/internal/remote-serve-flag.d.ts.map +1 -1
- package/dist/internal/rhi-capture.d.ts +1 -1
- package/dist/internal/rhi-capture.d.ts.map +1 -1
- package/dist/internal/worker-engine-profile.d.ts +2 -0
- package/dist/internal/worker-engine-profile.d.ts.map +1 -1
- package/dist/renderer-plugin.d.ts +25 -0
- package/dist/renderer-plugin.d.ts.map +1 -0
- package/dist/tool-preview/bootstrap.d.ts.map +1 -1
- package/dist/types.d.ts +21 -61
- package/dist/types.d.ts.map +1 -1
- package/package.json +25 -26
- package/src/__tests__/callback-inference.test-d.ts +7 -3
- package/src/__tests__/consumer-migration.unit.test.ts +13 -0
- package/src/__tests__/create-app-error-retention.integration.test.ts +125 -0
- package/src/__tests__/create-app-features.test.ts +21 -10
- package/src/__tests__/create-app-listener-sync.test.ts +23 -19
- package/src/__tests__/create-app-physics-plugin-failure.integration.test.ts +142 -0
- package/src/__tests__/create-app-stop.test.ts +4 -12
- package/src/__tests__/create-app.test-d.ts +12 -11
- package/src/__tests__/create-app.test.ts +1 -60
- package/src/__tests__/create-renderer-error.integration.test.ts +3 -2
- package/src/__tests__/create-renderer-lifecycle.integration.test.ts +10 -5
- package/src/__tests__/draw-source-injected-world-update-visibility.test.ts +7 -13
- package/src/__tests__/draw-source-single-world-regression.test.ts +23 -24
- package/src/__tests__/engine-worker-bootstrap-channel.unit.test.ts +8 -0
- package/src/__tests__/engine-worker-world-swap.unit.test.ts +15 -22
- package/src/__tests__/errors-pointer-lock-failed.test.ts +4 -7
- package/src/__tests__/execution-bootstrap-isolation.unit.test.ts +5 -6
- package/src/__tests__/execution-frame-order.unit.test.ts +2 -2
- package/src/__tests__/execution-frame-protocol.unit.test.ts +3 -4
- package/src/__tests__/execution-public-api.test-d.ts +4 -0
- package/src/__tests__/frame-loop-time-elapsed.test.ts +1 -1
- package/src/__tests__/frame-loop-time.unit.test.ts +1 -1
- package/src/__tests__/frame-loop-world-array.test.ts +82 -73
- package/src/__tests__/plugin-profiles.unit.test.ts +3 -3
- package/src/__tests__/profiler-default-off.test.ts +1 -1
- package/src/__tests__/profiler-frame-token.test.ts +17 -30
- package/src/__tests__/profiler-phase-integration.test.ts +15 -27
- package/src/__tests__/surface-handoff.test.ts +1 -4
- package/src/__tests__/visibility-remote.integration.test.ts +2 -5
- package/src/__tests__/worker-execution-stop.test.ts +64 -7
- package/src/animation-asset-lookup.ts +2 -2
- package/src/create-app.ts +197 -296
- package/src/errors.ts +27 -39
- package/src/execution/attached-world-swap.ts +4 -38
- package/src/execution/bootstrap-url.ts +25 -0
- package/src/execution/control.ts +3 -7
- package/src/execution/engine-worker-runtime.ts +23 -40
- package/src/execution/engine-worker.ts +2 -0
- package/src/execution/host-controller.ts +8 -2
- package/src/execution/index.ts +1 -0
- package/src/execution/protocol.ts +4 -1
- package/src/execution/types.ts +18 -1
- package/src/game-context.ts +1 -1
- package/src/index.ts +11 -4
- package/src/internal/assembled-engine-profile.ts +3 -11
- package/src/internal/assets-world-plugin.ts +7 -53
- package/src/internal/browser-remote-bridge.ts +12 -10
- package/src/internal/browser-rhi-debug-runtime.ts +1 -1
- package/src/internal/debug-draw.ts +38 -0
- package/src/internal/engine-profile-common.ts +2 -3
- package/src/internal/frame-loop.ts +77 -63
- package/src/internal/main-engine-profile.ts +21 -17
- package/src/internal/remote-serve-flag.ts +5 -14
- package/src/internal/rhi-capture.ts +1 -1
- package/src/internal/worker-engine-profile.ts +10 -10
- package/src/renderer-plugin.ts +96 -0
- package/src/tool-preview/bootstrap.ts +1 -14
- package/src/types.ts +22 -77
- package/src/vite-env.d.ts +0 -2
- package/dist/__tests__/app-rebuild.test.d.ts +0 -2
- package/dist/__tests__/app-rebuild.test.d.ts.map +0 -1
- package/dist/__tests__/asset-registry-types.test-d.d.ts +0 -2
- package/dist/__tests__/asset-registry-types.test-d.d.ts.map +0 -1
- package/dist/__tests__/assets-runtime-assembly.integration.test.d.ts +0 -2
- package/dist/__tests__/assets-runtime-assembly.integration.test.d.ts.map +0 -1
- package/dist/__tests__/draw-source-headless-full-chain.test.d.ts +0 -2
- package/dist/__tests__/draw-source-headless-full-chain.test.d.ts.map +0 -1
- package/dist/__tests__/ecs-import.unit.test.d.ts +0 -2
- package/dist/__tests__/ecs-import.unit.test.d.ts.map +0 -1
- package/dist/__tests__/execution-world-health-owner.test-d.d.ts +0 -2
- package/dist/__tests__/execution-world-health-owner.test-d.d.ts.map +0 -1
- package/dist/__tests__/remote-serve-flag.test.d.ts +0 -2
- package/dist/__tests__/remote-serve-flag.test.d.ts.map +0 -1
- package/dist/assets-runtime-assembly.d.ts +0 -19
- package/dist/assets-runtime-assembly.d.ts.map +0 -1
- package/src/__tests__/app-rebuild.test.ts +0 -31
- package/src/__tests__/asset-registry-types.test-d.ts +0 -16
- package/src/__tests__/assets-runtime-assembly.integration.test.ts +0 -68
- package/src/__tests__/draw-source-headless-full-chain.test.ts +0 -226
- package/src/__tests__/ecs-import.unit.test.ts +0 -40
- package/src/__tests__/execution-world-health-owner.test-d.ts +0 -9
- package/src/__tests__/remote-serve-flag.test.ts +0 -24
- package/src/assets-runtime-assembly.ts +0 -70
package/src/errors.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// @forgeax/engine-app -- AppError + closed AppErrorCode union (
|
|
1
|
+
// @forgeax/engine-app -- AppError + closed AppErrorCode union (12 members) +
|
|
2
2
|
// APP_ERROR_HINTS / APP_EXPECTED + discriminated AppErrorDetail per code.
|
|
3
3
|
//
|
|
4
4
|
// Shape:
|
|
5
|
-
// - AppErrorCode = closed union
|
|
5
|
+
// - AppErrorCode = closed union 12 members (charter P4 closed-union
|
|
6
6
|
// exhaustive switch needs no default fallback; tsc strict mode guards
|
|
7
7
|
// completeness; AGENTS.md "Errors are structured" + AC-07).
|
|
8
8
|
// Members:
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
// non-canonical fields like `{ state: 'running' }` on the
|
|
36
36
|
// 'app-already-running' arm.
|
|
37
37
|
//
|
|
38
|
-
// - APP_ERROR_HINTS / APP_EXPECTED are
|
|
38
|
+
// - APP_ERROR_HINTS / APP_EXPECTED are 12-key Records keyed by AppErrorCode;
|
|
39
39
|
// one private policy owner supplies both projections. The focused policy-owner
|
|
40
40
|
// proof asserts that every code has the exact expected and hint strings.
|
|
41
41
|
//
|
|
@@ -50,18 +50,17 @@ import type { RhiError } from '@forgeax/engine-rhi/errors';
|
|
|
50
50
|
import type { ExecutionCapabilityName, ExecutionTier } from './execution/types';
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
|
-
* Closed AppErrorCode union (
|
|
53
|
+
* Closed AppErrorCode union (12 members).
|
|
54
54
|
*
|
|
55
55
|
* | code | trigger |
|
|
56
56
|
* |:--|:--|
|
|
57
57
|
* | `'app-not-started'` | `stop()` / `pause()` / `resume()` invoked while the rAF loop is in the terminal `'idle'` or `'stopped'` state (post-device-lost terminal sink). The handle has no live frame to interrupt; AI users either call `start()` first or accept that this handle is dead and rebuild via `createApp({...})`. |
|
|
58
58
|
* | `'app-already-running'` | second `start()` invocation against an already-running handle; the call is a no-op state-machine-wise (state preserved). |
|
|
59
59
|
* | `'app-canvas-detached'` | `createApp(canvas)` thin wrapper found `canvas.isConnected === false` at entry. Detail carries optional `canvasId` so the host can surface the offending canvas in a multi-canvas page (D-2 minor). |
|
|
60
|
-
* | `'app-invalid-canvas-pixel-ratio'` | canvas-form Host policy supplied a non-finite or non-positive `maxCanvasPixelRatio`. Detail carries the rejected value. |
|
|
61
60
|
* | `'app-system-update-failed'` | World update, renderer draw, or a frame-loop callback failed; the original failure value is forwarded on `detail.cause`. `detail.systemName` is optional when the call site can name the failing owner. |
|
|
62
61
|
* | `'app-pointer-lock-failed'` | `attachInputAuto`'s `onLockError` callback received a lock failure from the input backend. `detail.path` carries `'w3c'` (W3C `requestPointerLock` rejection) or `'provider'` (host-injected `lockProvider.requestLock` throw/reject). `detail.cause` carries the original rejection value verbatim. The host recovers by remaining in unlocked state; the next trusted click will retry the lock request. |
|
|
63
62
|
*
|
|
64
|
-
* Plan-strategy D-4 locked the
|
|
63
|
+
* Plan-strategy D-4 locked the count at 6; device-lost rides RhiErrorCode.
|
|
65
64
|
*/
|
|
66
65
|
export type AppErrorCode = keyof typeof appErrorPolicy;
|
|
67
66
|
|
|
@@ -76,11 +75,6 @@ export interface AppDetailCanvasDetached {
|
|
|
76
75
|
readonly canvasId?: string | undefined;
|
|
77
76
|
}
|
|
78
77
|
|
|
79
|
-
/** Invalid host-owned canvas pixel-ratio cap. */
|
|
80
|
-
export interface AppDetailInvalidCanvasPixelRatio {
|
|
81
|
-
readonly value: number;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
78
|
/**
|
|
85
79
|
* Detail variant for the `'app-system-update-failed'` arm (plan-strategy D-4).
|
|
86
80
|
*
|
|
@@ -174,29 +168,27 @@ export type AppDetailEmpty = Readonly<Record<string, never>>;
|
|
|
174
168
|
*/
|
|
175
169
|
export type AppErrorDetailFor<C extends AppErrorCode> = C extends 'app-canvas-detached'
|
|
176
170
|
? AppDetailCanvasDetached
|
|
177
|
-
: C extends 'app-
|
|
178
|
-
?
|
|
179
|
-
: C extends 'app-
|
|
180
|
-
?
|
|
181
|
-
: C extends 'app-
|
|
182
|
-
?
|
|
183
|
-
: C extends 'app-
|
|
184
|
-
?
|
|
185
|
-
: C extends 'app-
|
|
186
|
-
?
|
|
187
|
-
: C extends 'app-execution-
|
|
188
|
-
?
|
|
189
|
-
: C extends 'app-execution-
|
|
190
|
-
?
|
|
191
|
-
: C extends 'app-execution-
|
|
192
|
-
?
|
|
193
|
-
: C extends 'app-execution-
|
|
194
|
-
?
|
|
195
|
-
: C extends 'app-execution-
|
|
196
|
-
?
|
|
197
|
-
:
|
|
198
|
-
? AppDetailExecutionRebuildFailed
|
|
199
|
-
: AppDetailEmpty;
|
|
171
|
+
: C extends 'app-frame-step-invalid'
|
|
172
|
+
? AppDetailFrameStepInvalid
|
|
173
|
+
: C extends 'app-system-update-failed'
|
|
174
|
+
? AppDetailSystemUpdateFailed
|
|
175
|
+
: C extends 'app-pointer-lock-failed'
|
|
176
|
+
? AppDetailPointerLockFailed
|
|
177
|
+
: C extends 'app-plugin-activation-failed'
|
|
178
|
+
? AppDetailPluginActivationFailed
|
|
179
|
+
: C extends 'app-execution-tier-unavailable'
|
|
180
|
+
? AppDetailExecutionTierUnavailable
|
|
181
|
+
: C extends 'app-execution-bootstrap-failed'
|
|
182
|
+
? AppDetailExecutionBootstrapFailed
|
|
183
|
+
: C extends 'app-execution-deadline-exceeded'
|
|
184
|
+
? AppDetailExecutionDeadlineExceeded
|
|
185
|
+
: C extends 'app-execution-kernel-failed'
|
|
186
|
+
? AppDetailExecutionKernelFailed
|
|
187
|
+
: C extends 'app-execution-stale-world'
|
|
188
|
+
? AppDetailExecutionStaleWorld
|
|
189
|
+
: C extends 'app-execution-rebuild-failed'
|
|
190
|
+
? AppDetailExecutionRebuildFailed
|
|
191
|
+
: AppDetailEmpty;
|
|
200
192
|
|
|
201
193
|
/**
|
|
202
194
|
* Tagged union of `.detail` payloads carried by structured AppError.
|
|
@@ -353,7 +345,7 @@ export const AppError: AppErrorConstructor = AppErrorClass as unknown as AppErro
|
|
|
353
345
|
* code surfaces. AI users read this as the L2 detail (charter F2 priority
|
|
354
346
|
* text); `.hint` carries the recovery action.
|
|
355
347
|
*
|
|
356
|
-
*
|
|
348
|
+
* 12 keys; the focused policy-owner proof locks the exact key set and values.
|
|
357
349
|
* locks the count and non-emptiness of every entry.
|
|
358
350
|
*/
|
|
359
351
|
type AppErrorPolicy = { readonly expected: string; readonly hint: string };
|
|
@@ -373,10 +365,6 @@ const appErrorPolicy = {
|
|
|
373
365
|
expected: 'canvas.isConnected === true at createApp(canvas) entry',
|
|
374
366
|
hint: 'append the canvas to the document tree before calling createApp(canvas), or use the assemble entry createApp({ renderer, world }) when the host already manages canvas lifetime',
|
|
375
367
|
},
|
|
376
|
-
'app-invalid-canvas-pixel-ratio': {
|
|
377
|
-
expected: 'maxCanvasPixelRatio is finite and greater than 0',
|
|
378
|
-
hint: 'omit maxCanvasPixelRatio to keep the device pixel ratio, or pass a finite value > 0 from the Host policy',
|
|
379
|
-
},
|
|
380
368
|
'app-frame-step-invalid': {
|
|
381
369
|
expected:
|
|
382
370
|
'stepFrame(deltaSeconds) runs only while the App is paused and deltaSeconds is finite and non-negative',
|
|
@@ -12,56 +12,22 @@ export class SerializedRebuildQueue {
|
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export interface RebuildCleanupFailure {
|
|
16
|
-
readonly phase: 'candidate' | 'previous';
|
|
17
|
-
readonly cause: unknown;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Primary rebuild failures stay first; cleanup failures are ordered detail. */
|
|
21
|
-
export class RebuildLifecycleError extends Error {
|
|
22
|
-
readonly primary: unknown;
|
|
23
|
-
readonly cleanup: readonly RebuildCleanupFailure[];
|
|
24
|
-
|
|
25
|
-
constructor(primary: unknown, cleanup: readonly RebuildCleanupFailure[]) {
|
|
26
|
-
super(
|
|
27
|
-
'World rebuild failed; cleanup details are attached without replacing the primary cause.',
|
|
28
|
-
);
|
|
29
|
-
this.name = 'RebuildLifecycleError';
|
|
30
|
-
this.primary = primary;
|
|
31
|
-
this.cleanup = cleanup;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export async function disposeWithOrderedDetail(
|
|
36
|
-
phase: RebuildCleanupFailure['phase'],
|
|
37
|
-
dispose: () => Promise<void> | void,
|
|
38
|
-
failures: RebuildCleanupFailure[],
|
|
39
|
-
): Promise<void> {
|
|
40
|
-
try {
|
|
41
|
-
await dispose();
|
|
42
|
-
} catch (cause) {
|
|
43
|
-
failures.push({ phase, cause });
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
15
|
/** Transactionally replace one Renderer-attached World. */
|
|
48
16
|
export async function commitAttachedWorld(
|
|
49
|
-
renderer: Pick<Renderer, '
|
|
50
|
-
previousWorld: World | undefined,
|
|
17
|
+
renderer: Pick<Renderer, 'attach'>,
|
|
51
18
|
nextWorld: World,
|
|
52
19
|
initializeCandidate: () => Promise<boolean>,
|
|
53
20
|
): Promise<boolean> {
|
|
54
|
-
const attached = renderer.
|
|
21
|
+
const attached = renderer.attach(nextWorld);
|
|
55
22
|
if (!attached.ok) throw attached.error;
|
|
56
23
|
try {
|
|
57
24
|
if (!(await initializeCandidate())) {
|
|
58
|
-
|
|
25
|
+
attached.value.dispose();
|
|
59
26
|
return false;
|
|
60
27
|
}
|
|
61
28
|
} catch (cause) {
|
|
62
|
-
|
|
29
|
+
attached.value.dispose();
|
|
63
30
|
throw cause;
|
|
64
31
|
}
|
|
65
|
-
if (previousWorld !== undefined) renderer.detachWorld(previousWorld);
|
|
66
32
|
return true;
|
|
67
33
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { err, ok, type Result } from '@forgeax/engine-types';
|
|
2
|
+
import { APP_ERROR_HINTS, APP_EXPECTED, AppError, type AppError as AppErrorType } from '../errors';
|
|
3
|
+
|
|
4
|
+
/** Normalize a bootstrap module before any realm probing or worker startup. */
|
|
5
|
+
export function normalizeExecutionBootstrapUrl(
|
|
6
|
+
bootstrap: string | URL,
|
|
7
|
+
): Result<string, AppErrorType> {
|
|
8
|
+
const moduleUrl = typeof bootstrap === 'string' ? bootstrap : bootstrap.href;
|
|
9
|
+
try {
|
|
10
|
+
return ok(new URL(bootstrap, globalThis.location?.href).href);
|
|
11
|
+
} catch (cause) {
|
|
12
|
+
return err(
|
|
13
|
+
new AppError({
|
|
14
|
+
code: 'app-execution-bootstrap-failed',
|
|
15
|
+
expected: APP_EXPECTED['app-execution-bootstrap-failed'],
|
|
16
|
+
hint: APP_ERROR_HINTS['app-execution-bootstrap-failed'],
|
|
17
|
+
detail: {
|
|
18
|
+
phase: 'prepare',
|
|
19
|
+
moduleUrl,
|
|
20
|
+
cause: cause instanceof TypeError ? cause : new TypeError(String(cause)),
|
|
21
|
+
},
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/execution/control.ts
CHANGED
|
@@ -15,8 +15,6 @@ export interface LocalExecutionControl extends ExecutionControl {
|
|
|
15
15
|
export interface LocalExecutionReportProviders {
|
|
16
16
|
readonly audio?: () => AudioState;
|
|
17
17
|
readonly world?: () => ExecutionReport['world'];
|
|
18
|
-
/** Host-owned poisoned-World replacement transaction. */
|
|
19
|
-
readonly rebuild?: ExecutionControl['rebuild'];
|
|
20
18
|
}
|
|
21
19
|
|
|
22
20
|
export function createLocalExecutionControl(
|
|
@@ -34,9 +32,8 @@ export function createLocalExecutionControl(
|
|
|
34
32
|
world: providers.world?.() ?? current.world,
|
|
35
33
|
audio: executionAudioReport(providers.audio?.()),
|
|
36
34
|
}),
|
|
37
|
-
rebuild: async () =>
|
|
38
|
-
|
|
39
|
-
return err(
|
|
35
|
+
rebuild: async () =>
|
|
36
|
+
err(
|
|
40
37
|
new AppError({
|
|
41
38
|
code: 'app-execution-rebuild-failed',
|
|
42
39
|
expected: APP_EXPECTED['app-execution-rebuild-failed'],
|
|
@@ -46,7 +43,6 @@ export function createLocalExecutionControl(
|
|
|
46
43
|
cause: new Error('This local assembly has no bootstrap module.'),
|
|
47
44
|
},
|
|
48
45
|
}),
|
|
49
|
-
)
|
|
50
|
-
},
|
|
46
|
+
),
|
|
51
47
|
};
|
|
52
48
|
}
|
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import { createAssetRegistry, createCatalogSource } from '@forgeax/engine-assets-runtime';
|
|
2
1
|
import { type AudioIntent, createAudioIntentBackend } from '@forgeax/engine-audio';
|
|
3
2
|
import { createWorldContext, World } from '@forgeax/engine-ecs';
|
|
4
3
|
import type { SharedKernelExecutor } from '@forgeax/engine-ecs/shared';
|
|
5
4
|
import type { InputBackend, InputBackendSample } from '@forgeax/engine-input';
|
|
6
5
|
import type { Context, Plugin } from '@forgeax/engine-plugin';
|
|
7
6
|
import type { Renderer } from '@forgeax/engine-render';
|
|
8
|
-
import {
|
|
7
|
+
import { constructRuntimeRendererHost } from '@forgeax/engine-runtime/internal/renderer-host';
|
|
9
8
|
import { createAnimationPayloadLookup } from '../animation-asset-lookup';
|
|
10
|
-
import { type AssetRuntimeAssembly, assembleAssetRuntime } from '../assets-runtime-assembly';
|
|
11
9
|
import { syncCameraAspect } from '../canvas-policy';
|
|
12
10
|
import { workerEngineProfile } from '../internal/worker-engine-profile';
|
|
11
|
+
import { createRenderFeatureHost } from '../renderer-plugin';
|
|
13
12
|
import { commitAttachedWorld, SerializedRebuildQueue } from './attached-world-swap';
|
|
14
13
|
import {
|
|
15
14
|
executionBootstrapHostPlugin,
|
|
@@ -32,6 +31,7 @@ const scope = globalThis as unknown as {
|
|
|
32
31
|
};
|
|
33
32
|
|
|
34
33
|
let renderer: Renderer | undefined;
|
|
34
|
+
let assets: import('@forgeax/engine-assets-runtime').AssetRegistry | undefined;
|
|
35
35
|
let currentSample: InputBackendSample = {
|
|
36
36
|
downKeys: new Set(),
|
|
37
37
|
upKeys: new Set(),
|
|
@@ -54,6 +54,7 @@ interface WorkerRealm {
|
|
|
54
54
|
|
|
55
55
|
let realm: WorkerRealm | undefined;
|
|
56
56
|
const rebuildQueue = new SerializedRebuildQueue();
|
|
57
|
+
|
|
57
58
|
const inputBackend: InputBackend = {
|
|
58
59
|
sample: () => currentSample,
|
|
59
60
|
detach: () => {},
|
|
@@ -96,13 +97,6 @@ function serializableDetail(cause: unknown): unknown {
|
|
|
96
97
|
if (cause instanceof Error) return serializableCause(cause);
|
|
97
98
|
if (Array.isArray(cause)) return cause.map(serializableDetail);
|
|
98
99
|
if (typeof cause === 'object' && cause !== null) {
|
|
99
|
-
const named = cause as { readonly name?: unknown; readonly message?: unknown };
|
|
100
|
-
if (typeof named.name === 'string' || typeof named.message === 'string') {
|
|
101
|
-
return {
|
|
102
|
-
name: typeof named.name === 'string' ? named.name : 'Error',
|
|
103
|
-
message: typeof named.message === 'string' ? named.message : String(cause),
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
100
|
return Object.fromEntries(
|
|
107
101
|
Object.entries(cause).map(([key, value]) => [key, serializableDetail(value)]),
|
|
108
102
|
);
|
|
@@ -168,7 +162,6 @@ async function createRealm(init: ExecutionInitMessage): Promise<boolean> {
|
|
|
168
162
|
emit: (intent) => candidate.pendingAudioIntents.push(intent),
|
|
169
163
|
});
|
|
170
164
|
let candidateRenderer: Renderer | undefined;
|
|
171
|
-
let assetAssembly: AssetRuntimeAssembly | undefined;
|
|
172
165
|
let rendererLifecycleTransferred = false;
|
|
173
166
|
const previousRenderer = renderer;
|
|
174
167
|
let previousSurfaceReleased = false;
|
|
@@ -178,35 +171,26 @@ async function createRealm(init: ExecutionInitMessage): Promise<boolean> {
|
|
|
178
171
|
if (!released.ok) throw released.error;
|
|
179
172
|
previousSurfaceReleased = true;
|
|
180
173
|
}
|
|
181
|
-
|
|
174
|
+
const constructed = await constructRuntimeRendererHost(
|
|
182
175
|
init.canvas,
|
|
183
176
|
prepared.features === undefined ? {} : { features: prepared.features },
|
|
184
177
|
init.shaderManifestUrl === undefined
|
|
185
178
|
? undefined
|
|
186
179
|
: { shaderManifestUrl: init.shaderManifestUrl },
|
|
187
180
|
);
|
|
188
|
-
|
|
189
|
-
|
|
181
|
+
if (!constructed.ok) throw constructed.error;
|
|
182
|
+
candidateRenderer = constructed.value.renderer;
|
|
183
|
+
assets = constructed.value.assets;
|
|
190
184
|
rendererLifecycleTransferred = true;
|
|
191
|
-
const assetRegistry = createAssetRegistry({
|
|
192
|
-
catalog: createCatalogSource({
|
|
193
|
-
url: new URL('pack-index.json', globalThis.location?.href ?? 'http://localhost/').href,
|
|
194
|
-
}),
|
|
195
|
-
});
|
|
196
|
-
const assembledAssets = assembleAssetRuntime(assetRegistry, []);
|
|
197
|
-
if (!assembledAssets.ok) {
|
|
198
|
-
assetRegistry.dispose();
|
|
199
|
-
throw assembledAssets.error;
|
|
200
|
-
}
|
|
201
|
-
assetAssembly = assembledAssets.value;
|
|
202
185
|
const pluginContext = await createWorldContext(
|
|
203
186
|
nextWorld,
|
|
204
187
|
workerEngineProfile({
|
|
205
188
|
renderer: candidateRenderer,
|
|
206
|
-
|
|
189
|
+
rendererFeatureHost: createRenderFeatureHost(constructed.value.featureHost),
|
|
190
|
+
assets,
|
|
207
191
|
input: inputBackend,
|
|
208
192
|
audio: audioBackend,
|
|
209
|
-
animationPayloads: createAnimationPayloadLookup(
|
|
193
|
+
animationPayloads: createAnimationPayloadLookup(assets),
|
|
210
194
|
extensions: [
|
|
211
195
|
...(init.tier === 'shared' ? [sharedKernelPlugin(candidate)] : []),
|
|
212
196
|
executionBootstrapHostPlugin({
|
|
@@ -226,15 +210,10 @@ async function createRealm(init: ExecutionInitMessage): Promise<boolean> {
|
|
|
226
210
|
candidate.pluginContext = pluginContext;
|
|
227
211
|
const activeRenderer = candidateRenderer;
|
|
228
212
|
const previousRealm = realm;
|
|
229
|
-
const committed = await commitAttachedWorld(
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
async () => {
|
|
234
|
-
await candidate.kernelPool?.ready();
|
|
235
|
-
return true;
|
|
236
|
-
},
|
|
237
|
-
);
|
|
213
|
+
const committed = await commitAttachedWorld(candidateRenderer, nextWorld, async () => {
|
|
214
|
+
await candidate.kernelPool?.ready();
|
|
215
|
+
return true;
|
|
216
|
+
});
|
|
238
217
|
if (!committed) {
|
|
239
218
|
await disposeRealm(candidate);
|
|
240
219
|
if (previousSurfaceReleased) previousRenderer?.restoreSurface();
|
|
@@ -247,8 +226,6 @@ async function createRealm(init: ExecutionInitMessage): Promise<boolean> {
|
|
|
247
226
|
return true;
|
|
248
227
|
} catch (cause) {
|
|
249
228
|
await disposeRealm(candidate);
|
|
250
|
-
assetAssembly?.dispose();
|
|
251
|
-
assetAssembly?.registry.dispose();
|
|
252
229
|
if (!rendererLifecycleTransferred) candidateRenderer?.dispose();
|
|
253
230
|
if (previousSurfaceReleased) previousRenderer?.restoreSurface();
|
|
254
231
|
throw cause;
|
|
@@ -314,8 +291,14 @@ function runFrame(message: ExecutionFrameMessage): void {
|
|
|
314
291
|
return;
|
|
315
292
|
}
|
|
316
293
|
const updateFinished = performance.now();
|
|
317
|
-
const
|
|
318
|
-
if (
|
|
294
|
+
const attached = renderer.attach(world);
|
|
295
|
+
if (!attached.ok) throw attached.error;
|
|
296
|
+
const draw = renderer.draw({
|
|
297
|
+
leases: [attached.value],
|
|
298
|
+
camera: { lease: attached.value },
|
|
299
|
+
environment: { lease: attached.value },
|
|
300
|
+
});
|
|
301
|
+
if (!draw.ok) throw draw.error;
|
|
319
302
|
lastFrameId = message.frameId;
|
|
320
303
|
const kernelDispatch = activeRealm.kernelPool?.takeLastDispatch() ?? null;
|
|
321
304
|
const audioIntents = activeRealm.pendingAudioIntents;
|
|
@@ -22,6 +22,7 @@ export interface StartEngineWorkerOptions {
|
|
|
22
22
|
readonly bootstrapUrl: string;
|
|
23
23
|
readonly bootstrapData?: import('./types').ExecutionBootstrapValue;
|
|
24
24
|
readonly bootstrapPort?: MessagePort;
|
|
25
|
+
readonly assetCatalog?: import('./types').ExecutionAssetCatalog;
|
|
25
26
|
readonly shaderManifestUrl?: string;
|
|
26
27
|
readonly time?: import('@forgeax/engine-ecs').TimePolicy;
|
|
27
28
|
readonly timeoutMs: number;
|
|
@@ -130,6 +131,7 @@ export async function startEngineWorker(
|
|
|
130
131
|
bootstrapUrl: options.bootstrapUrl,
|
|
131
132
|
...(options.bootstrapData === undefined ? {} : { bootstrapData: options.bootstrapData }),
|
|
132
133
|
...(options.bootstrapPort === undefined ? {} : { bootstrapPort: options.bootstrapPort }),
|
|
134
|
+
...(options.assetCatalog === undefined ? {} : { assetCatalog: options.assetCatalog }),
|
|
133
135
|
...(options.shaderManifestUrl !== undefined
|
|
134
136
|
? { shaderManifestUrl: options.shaderManifestUrl }
|
|
135
137
|
: {}),
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type CreateAppOptions,
|
|
11
11
|
type ExecutionApp,
|
|
12
12
|
} from '../types';
|
|
13
|
+
import { normalizeExecutionBootstrapUrl } from './bootstrap-url';
|
|
13
14
|
import { cloneExecutionReport } from './control';
|
|
14
15
|
import { type EngineWorkerSession, startEngineWorker } from './engine-worker';
|
|
15
16
|
import { createMeasurementSeries } from './measurement';
|
|
@@ -73,10 +74,12 @@ export async function createWorkerExecutionApp(
|
|
|
73
74
|
): Promise<Result<ExecutionApp, AppErrorType>> {
|
|
74
75
|
const executionOptions = options.appOptions.execution;
|
|
75
76
|
if (executionOptions === undefined) throw new Error('execution options are required');
|
|
76
|
-
const
|
|
77
|
-
|
|
77
|
+
const normalizedBootstrap = normalizeExecutionBootstrapUrl(executionOptions.bootstrap);
|
|
78
|
+
if (!normalizedBootstrap.ok) return normalizedBootstrap;
|
|
79
|
+
const bootstrapUrl = normalizedBootstrap.value;
|
|
78
80
|
const startupTimeoutMs = executionOptions.startupTimeoutMs ?? 10_000;
|
|
79
81
|
const frameTimeoutMs = executionOptions.frameTimeoutMs ?? 2_000;
|
|
82
|
+
const syncCanvas = options.syncCanvas;
|
|
80
83
|
const started = await startEngineWorker({
|
|
81
84
|
canvas: options.canvas,
|
|
82
85
|
bootstrapUrl,
|
|
@@ -86,6 +89,9 @@ export async function createWorkerExecutionApp(
|
|
|
86
89
|
...(executionOptions.bootstrapPort === undefined
|
|
87
90
|
? {}
|
|
88
91
|
: { bootstrapPort: executionOptions.bootstrapPort }),
|
|
92
|
+
...(executionOptions.assetCatalog === undefined
|
|
93
|
+
? {}
|
|
94
|
+
: { assetCatalog: executionOptions.assetCatalog }),
|
|
89
95
|
...(options.bundler?.shaderManifestUrl !== undefined
|
|
90
96
|
? { shaderManifestUrl: options.bundler.shaderManifestUrl }
|
|
91
97
|
: {}),
|
package/src/execution/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ export { createExecutionReport } from './report';
|
|
|
19
19
|
export { EXECUTION_REPORT_SCHEMA_VERSION, isExecutionReport } from './schema';
|
|
20
20
|
export { type ExecutionSelectionInput, selectExecutionTier } from './selector';
|
|
21
21
|
export type {
|
|
22
|
+
ExecutionAssetCatalog,
|
|
22
23
|
ExecutionBootstrapValue,
|
|
23
24
|
ExecutionCapabilities,
|
|
24
25
|
ExecutionCapabilityFact,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AudioIntent } from '@forgeax/engine-audio';
|
|
2
2
|
import type { InputBackendSample } from '@forgeax/engine-input';
|
|
3
3
|
import type { CanvasDrawingBufferSize } from '../types';
|
|
4
|
+
import type { ExecutionAssetCatalog } from './types';
|
|
4
5
|
|
|
5
6
|
export interface ExecutionFrameMessage {
|
|
6
7
|
readonly kind: 'frame';
|
|
@@ -40,7 +41,7 @@ export class FrameCreditLedger {
|
|
|
40
41
|
issue(
|
|
41
42
|
deltaSeconds: number,
|
|
42
43
|
sampleInput: () => InputBackendSample,
|
|
43
|
-
canvasSize: CanvasDrawingBufferSize,
|
|
44
|
+
canvasSize: CanvasDrawingBufferSize = { width: 0, height: 0 },
|
|
44
45
|
): ExecutionFrameMessage | undefined {
|
|
45
46
|
if (this.inFlight !== null) return undefined;
|
|
46
47
|
const frameId = this.nextFrameId;
|
|
@@ -77,6 +78,8 @@ export interface ExecutionInitMessage {
|
|
|
77
78
|
readonly bootstrapUrl: string;
|
|
78
79
|
readonly bootstrapData?: import('./types').ExecutionBootstrapValue;
|
|
79
80
|
readonly bootstrapPort?: MessagePort;
|
|
81
|
+
/** Realm-serializable asset catalog configuration. */
|
|
82
|
+
readonly assetCatalog?: ExecutionAssetCatalog;
|
|
80
83
|
readonly shaderManifestUrl?: string;
|
|
81
84
|
readonly time?: import('@forgeax/engine-ecs').TimePolicy;
|
|
82
85
|
readonly tier: import('./types').ExecutionTier;
|
package/src/execution/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { WorldExecutionHealth } from '@forgeax/engine-ecs/shared';
|
|
2
|
-
import type { Result } from '@forgeax/engine-types';
|
|
2
|
+
import type { Result, RuntimeAssetBinding } from '@forgeax/engine-types';
|
|
3
3
|
import type { AppError } from '../errors';
|
|
4
4
|
|
|
5
5
|
export const EXECUTION_TIERS = ['main-serial', 'engine-worker', 'shared'] as const;
|
|
@@ -83,6 +83,16 @@ export interface ExecutionAudioReport {
|
|
|
83
83
|
} | null;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Serializable asset-catalog configuration copied into the selected Engine
|
|
88
|
+
* realm. The URL is resolved by that realm, so a Worker never closes over a
|
|
89
|
+
* Host-side CatalogSource or Registry instance.
|
|
90
|
+
*/
|
|
91
|
+
export interface ExecutionAssetCatalog {
|
|
92
|
+
readonly url: string;
|
|
93
|
+
readonly expectedScope?: Pick<RuntimeAssetBinding, 'scopeId' | 'generation'>;
|
|
94
|
+
}
|
|
95
|
+
|
|
86
96
|
export interface ExecutionReport {
|
|
87
97
|
readonly schemaVersion: 1;
|
|
88
98
|
readonly requestedTier: ExecutionRequestedTier;
|
|
@@ -128,6 +138,13 @@ export interface ExecutionOptions {
|
|
|
128
138
|
* to a Worker when needed and otherwise preserves the same port contract.
|
|
129
139
|
*/
|
|
130
140
|
readonly bootstrapPort?: MessagePort;
|
|
141
|
+
/**
|
|
142
|
+
* Serializable CatalogSource configuration constructed inside the selected
|
|
143
|
+
* execution realm. This is the asset delivery seam for Worker execution;
|
|
144
|
+
* `CreateAppOptions.assetCatalog` remains realm-bound and is rejected when
|
|
145
|
+
* execution is requested.
|
|
146
|
+
*/
|
|
147
|
+
readonly assetCatalog?: ExecutionAssetCatalog;
|
|
131
148
|
readonly startupTimeoutMs?: number;
|
|
132
149
|
readonly frameTimeoutMs?: number;
|
|
133
150
|
}
|
package/src/game-context.ts
CHANGED
|
@@ -70,7 +70,7 @@ export interface GameHost {
|
|
|
70
70
|
readonly canvas: HTMLCanvasElement;
|
|
71
71
|
/** The WebGPU Renderer (optional — some hosts may not expose it). */
|
|
72
72
|
readonly renderer?: Renderer;
|
|
73
|
-
/** The AssetRegistry owned by the
|
|
73
|
+
/** The AssetRegistry owned by the Renderer. */
|
|
74
74
|
readonly assets: AssetRegistry;
|
|
75
75
|
/** The App handle for lifecycle introspection. */
|
|
76
76
|
readonly app: App;
|
package/src/index.ts
CHANGED
|
@@ -36,10 +36,9 @@ export type {
|
|
|
36
36
|
} from '@forgeax/engine-plugin';
|
|
37
37
|
export { Context } from '@forgeax/engine-plugin';
|
|
38
38
|
export {
|
|
39
|
-
|
|
40
|
-
type
|
|
41
|
-
|
|
42
|
-
} from './assets-runtime-assembly';
|
|
39
|
+
createFullscreenRenderFeature,
|
|
40
|
+
type FullscreenRenderFeatureOptions,
|
|
41
|
+
} from '@forgeax/engine-render/authoring';
|
|
43
42
|
export { createApp, measureCanvasDrawingBuffer, syncCanvasDrawingBuffer } from './create-app';
|
|
44
43
|
export type {
|
|
45
44
|
AppDetailCanvasDetached,
|
|
@@ -63,6 +62,7 @@ export {
|
|
|
63
62
|
isAppError,
|
|
64
63
|
} from './errors';
|
|
65
64
|
export type {
|
|
65
|
+
ExecutionAssetCatalog,
|
|
66
66
|
ExecutionBootstrapEntry,
|
|
67
67
|
ExecutionBootstrapHost,
|
|
68
68
|
ExecutionBootstrapValue,
|
|
@@ -110,6 +110,13 @@ export type {
|
|
|
110
110
|
} from './game-context';
|
|
111
111
|
export { gameHostPlugin } from './game-context';
|
|
112
112
|
export { inputPlugin } from './plugin-factories';
|
|
113
|
+
export type { RenderFeatureHost } from './renderer-plugin';
|
|
114
|
+
export {
|
|
115
|
+
ownedRendererPlugin,
|
|
116
|
+
rendererPlugin,
|
|
117
|
+
renderFeatureHostPlugin,
|
|
118
|
+
renderFeaturePlugin,
|
|
119
|
+
} from './renderer-plugin';
|
|
113
120
|
|
|
114
121
|
import {
|
|
115
122
|
isLoadGameError,
|
|
@@ -1,21 +1,13 @@
|
|
|
1
1
|
import type { Plugin } from '@forgeax/engine-plugin';
|
|
2
|
-
import { rendererPlugin } from '
|
|
3
|
-
import {
|
|
4
|
-
assetRegistryPlugin,
|
|
5
|
-
assetRegistryWorldPlugin,
|
|
6
|
-
assetsWorldPlugin,
|
|
7
|
-
rendererAssetsPlugin,
|
|
8
|
-
} from './assets-world-plugin';
|
|
2
|
+
import { rendererPlugin } from '../renderer-plugin';
|
|
3
|
+
import { assetsWorldPlugin, rendererAssetsPlugin } from './assets-world-plugin';
|
|
9
4
|
import type { EngineProfileBase } from './engine-profile-common';
|
|
10
5
|
|
|
11
6
|
/** Assemble-form roots plus explicitly selected host capabilities. */
|
|
12
7
|
export function assembledEngineProfile(options: EngineProfileBase): Plugin[] {
|
|
13
8
|
return [
|
|
14
9
|
rendererPlugin(options.renderer),
|
|
15
|
-
rendererAssetsPlugin(),
|
|
16
|
-
...(options.assetAssembly === undefined
|
|
17
|
-
? []
|
|
18
|
-
: [assetRegistryPlugin(options.assetAssembly), assetRegistryWorldPlugin()]),
|
|
10
|
+
rendererAssetsPlugin(options.assets),
|
|
19
11
|
assetsWorldPlugin(),
|
|
20
12
|
...(options.extensions ?? []),
|
|
21
13
|
];
|