@forgeax/engine-app 0.1.4 → 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.
Files changed (145) hide show
  1. package/README.md +20 -14
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/create-app-error-retention.integration.test.d.ts +2 -0
  4. package/dist/__tests__/create-app-error-retention.integration.test.d.ts.map +1 -0
  5. package/dist/__tests__/create-app-physics-plugin-failure.integration.test.d.ts +2 -0
  6. package/dist/__tests__/create-app-physics-plugin-failure.integration.test.d.ts.map +1 -0
  7. package/dist/animation-asset-lookup.d.ts +1 -2
  8. package/dist/animation-asset-lookup.d.ts.map +1 -1
  9. package/dist/create-app.d.ts +31 -10
  10. package/dist/create-app.d.ts.map +1 -1
  11. package/dist/engine-worker-runtime.mjs +72 -129
  12. package/dist/engine-worker-runtime.mjs.map +1 -1
  13. package/dist/errors.d.ts +3 -12
  14. package/dist/errors.d.ts.map +1 -1
  15. package/dist/execution/attached-world-swap.d.ts +1 -12
  16. package/dist/execution/attached-world-swap.d.ts.map +1 -1
  17. package/dist/execution/bootstrap-url.d.ts +5 -0
  18. package/dist/execution/bootstrap-url.d.ts.map +1 -0
  19. package/dist/execution/control.d.ts +0 -2
  20. package/dist/execution/control.d.ts.map +1 -1
  21. package/dist/execution/engine-worker.d.ts +1 -0
  22. package/dist/execution/engine-worker.d.ts.map +1 -1
  23. package/dist/execution/host-controller.d.ts.map +1 -1
  24. package/dist/execution/index.d.ts +1 -1
  25. package/dist/execution/index.d.ts.map +1 -1
  26. package/dist/execution/protocol.d.ts +4 -1
  27. package/dist/execution/protocol.d.ts.map +1 -1
  28. package/dist/execution/types.d.ts +17 -1
  29. package/dist/execution/types.d.ts.map +1 -1
  30. package/dist/game-context.d.ts +1 -1
  31. package/dist/game-context.d.ts.map +1 -1
  32. package/dist/index.d.ts +4 -2
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.mjs +331 -362
  35. package/dist/index.mjs.map +1 -1
  36. package/dist/internal/assembled-engine-profile.d.ts.map +1 -1
  37. package/dist/internal/assets-world-plugin.d.ts +3 -17
  38. package/dist/internal/assets-world-plugin.d.ts.map +1 -1
  39. package/dist/internal/browser-remote-bridge.d.ts +1 -2
  40. package/dist/internal/browser-remote-bridge.d.ts.map +1 -1
  41. package/dist/internal/browser-rhi-debug-runtime.d.ts +1 -1
  42. package/dist/internal/browser-rhi-debug-runtime.d.ts.map +1 -1
  43. package/dist/internal/debug-draw.d.ts +9 -0
  44. package/dist/internal/debug-draw.d.ts.map +1 -0
  45. package/dist/internal/engine-profile-common.d.ts +2 -2
  46. package/dist/internal/engine-profile-common.d.ts.map +1 -1
  47. package/dist/internal/frame-loop.d.ts +7 -4
  48. package/dist/internal/frame-loop.d.ts.map +1 -1
  49. package/dist/internal/main-engine-profile.d.ts +4 -0
  50. package/dist/internal/main-engine-profile.d.ts.map +1 -1
  51. package/dist/internal/remote-serve-flag.d.ts +3 -7
  52. package/dist/internal/remote-serve-flag.d.ts.map +1 -1
  53. package/dist/internal/rhi-capture.d.ts +1 -1
  54. package/dist/internal/rhi-capture.d.ts.map +1 -1
  55. package/dist/internal/worker-engine-profile.d.ts +2 -0
  56. package/dist/internal/worker-engine-profile.d.ts.map +1 -1
  57. package/dist/renderer-plugin.d.ts +25 -0
  58. package/dist/renderer-plugin.d.ts.map +1 -0
  59. package/dist/tool-preview/bootstrap.d.ts.map +1 -1
  60. package/dist/types.d.ts +21 -61
  61. package/dist/types.d.ts.map +1 -1
  62. package/package.json +25 -26
  63. package/src/__tests__/callback-inference.test-d.ts +7 -3
  64. package/src/__tests__/consumer-migration.unit.test.ts +13 -0
  65. package/src/__tests__/create-app-error-retention.integration.test.ts +125 -0
  66. package/src/__tests__/create-app-features.test.ts +21 -10
  67. package/src/__tests__/create-app-listener-sync.test.ts +23 -19
  68. package/src/__tests__/create-app-physics-plugin-failure.integration.test.ts +142 -0
  69. package/src/__tests__/create-app-stop.test.ts +4 -12
  70. package/src/__tests__/create-app.test-d.ts +12 -11
  71. package/src/__tests__/create-app.test.ts +1 -60
  72. package/src/__tests__/create-renderer-error.integration.test.ts +3 -2
  73. package/src/__tests__/create-renderer-lifecycle.integration.test.ts +10 -5
  74. package/src/__tests__/draw-source-injected-world-update-visibility.test.ts +7 -13
  75. package/src/__tests__/draw-source-single-world-regression.test.ts +23 -24
  76. package/src/__tests__/engine-worker-bootstrap-channel.unit.test.ts +8 -0
  77. package/src/__tests__/engine-worker-world-swap.unit.test.ts +15 -22
  78. package/src/__tests__/errors-pointer-lock-failed.test.ts +4 -7
  79. package/src/__tests__/execution-bootstrap-isolation.unit.test.ts +5 -6
  80. package/src/__tests__/execution-frame-order.unit.test.ts +2 -2
  81. package/src/__tests__/execution-frame-protocol.unit.test.ts +3 -4
  82. package/src/__tests__/execution-public-api.test-d.ts +4 -0
  83. package/src/__tests__/frame-loop-time-elapsed.test.ts +1 -1
  84. package/src/__tests__/frame-loop-time.unit.test.ts +1 -1
  85. package/src/__tests__/frame-loop-world-array.test.ts +82 -73
  86. package/src/__tests__/plugin-profiles.unit.test.ts +3 -3
  87. package/src/__tests__/profiler-default-off.test.ts +1 -1
  88. package/src/__tests__/profiler-frame-token.test.ts +17 -30
  89. package/src/__tests__/profiler-phase-integration.test.ts +15 -27
  90. package/src/__tests__/surface-handoff.test.ts +1 -4
  91. package/src/__tests__/visibility-remote.integration.test.ts +2 -5
  92. package/src/__tests__/worker-execution-stop.test.ts +64 -7
  93. package/src/animation-asset-lookup.ts +2 -2
  94. package/src/create-app.ts +197 -296
  95. package/src/errors.ts +27 -39
  96. package/src/execution/attached-world-swap.ts +4 -38
  97. package/src/execution/bootstrap-url.ts +25 -0
  98. package/src/execution/control.ts +3 -7
  99. package/src/execution/engine-worker-runtime.ts +23 -40
  100. package/src/execution/engine-worker.ts +2 -0
  101. package/src/execution/host-controller.ts +8 -2
  102. package/src/execution/index.ts +1 -0
  103. package/src/execution/protocol.ts +4 -1
  104. package/src/execution/types.ts +18 -1
  105. package/src/game-context.ts +1 -1
  106. package/src/index.ts +11 -4
  107. package/src/internal/assembled-engine-profile.ts +3 -11
  108. package/src/internal/assets-world-plugin.ts +7 -53
  109. package/src/internal/browser-remote-bridge.ts +2 -3
  110. package/src/internal/browser-rhi-debug-runtime.ts +1 -1
  111. package/src/internal/debug-draw.ts +38 -0
  112. package/src/internal/engine-profile-common.ts +2 -3
  113. package/src/internal/frame-loop.ts +77 -63
  114. package/src/internal/main-engine-profile.ts +21 -17
  115. package/src/internal/remote-serve-flag.ts +5 -14
  116. package/src/internal/rhi-capture.ts +1 -1
  117. package/src/internal/worker-engine-profile.ts +10 -10
  118. package/src/renderer-plugin.ts +96 -0
  119. package/src/tool-preview/bootstrap.ts +1 -14
  120. package/src/types.ts +22 -77
  121. package/src/vite-env.d.ts +0 -2
  122. package/dist/__tests__/app-rebuild.test.d.ts +0 -2
  123. package/dist/__tests__/app-rebuild.test.d.ts.map +0 -1
  124. package/dist/__tests__/asset-registry-types.test-d.d.ts +0 -2
  125. package/dist/__tests__/asset-registry-types.test-d.d.ts.map +0 -1
  126. package/dist/__tests__/assets-runtime-assembly.integration.test.d.ts +0 -2
  127. package/dist/__tests__/assets-runtime-assembly.integration.test.d.ts.map +0 -1
  128. package/dist/__tests__/draw-source-headless-full-chain.test.d.ts +0 -2
  129. package/dist/__tests__/draw-source-headless-full-chain.test.d.ts.map +0 -1
  130. package/dist/__tests__/ecs-import.unit.test.d.ts +0 -2
  131. package/dist/__tests__/ecs-import.unit.test.d.ts.map +0 -1
  132. package/dist/__tests__/execution-world-health-owner.test-d.d.ts +0 -2
  133. package/dist/__tests__/execution-world-health-owner.test-d.d.ts.map +0 -1
  134. package/dist/__tests__/remote-serve-flag.test.d.ts +0 -2
  135. package/dist/__tests__/remote-serve-flag.test.d.ts.map +0 -1
  136. package/dist/assets-runtime-assembly.d.ts +0 -19
  137. package/dist/assets-runtime-assembly.d.ts.map +0 -1
  138. package/src/__tests__/app-rebuild.test.ts +0 -31
  139. package/src/__tests__/asset-registry-types.test-d.ts +0 -16
  140. package/src/__tests__/assets-runtime-assembly.integration.test.ts +0 -68
  141. package/src/__tests__/draw-source-headless-full-chain.test.ts +0 -226
  142. package/src/__tests__/ecs-import.unit.test.ts +0 -40
  143. package/src/__tests__/execution-world-health-owner.test-d.ts +0 -9
  144. package/src/__tests__/remote-serve-flag.test.ts +0 -24
  145. 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 (14 members) +
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 14 members (charter P4 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 14-key Records keyed by AppErrorCode;
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 (14 members).
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 original lifecycle subset; device-lost rides RhiErrorCode.
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-invalid-canvas-pixel-ratio'
178
- ? AppDetailInvalidCanvasPixelRatio
179
- : C extends 'app-frame-step-invalid'
180
- ? AppDetailFrameStepInvalid
181
- : C extends 'app-system-update-failed'
182
- ? AppDetailSystemUpdateFailed
183
- : C extends 'app-pointer-lock-failed'
184
- ? AppDetailPointerLockFailed
185
- : C extends 'app-plugin-activation-failed'
186
- ? AppDetailPluginActivationFailed
187
- : C extends 'app-execution-tier-unavailable'
188
- ? AppDetailExecutionTierUnavailable
189
- : C extends 'app-execution-bootstrap-failed'
190
- ? AppDetailExecutionBootstrapFailed
191
- : C extends 'app-execution-deadline-exceeded'
192
- ? AppDetailExecutionDeadlineExceeded
193
- : C extends 'app-execution-kernel-failed'
194
- ? AppDetailExecutionKernelFailed
195
- : C extends 'app-execution-stale-world'
196
- ? AppDetailExecutionStaleWorld
197
- : C extends 'app-execution-rebuild-failed'
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
- * 14 keys; the focused policy-owner proof locks the exact key set and values.
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, 'attachWorld' | 'detachWorld'>,
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.attachWorld(nextWorld);
21
+ const attached = renderer.attach(nextWorld);
55
22
  if (!attached.ok) throw attached.error;
56
23
  try {
57
24
  if (!(await initializeCandidate())) {
58
- renderer.detachWorld(nextWorld);
25
+ attached.value.dispose();
59
26
  return false;
60
27
  }
61
28
  } catch (cause) {
62
- renderer.detachWorld(nextWorld);
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
+ }
@@ -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
- if (providers.rebuild !== undefined) return providers.rebuild();
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 { createRenderer } from '@forgeax/engine-runtime';
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
- candidateRenderer = await createRenderer(
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
- const ready = await candidateRenderer.ready;
189
- if (!ready.ok) throw ready.error;
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
- assetAssembly,
189
+ rendererFeatureHost: createRenderFeatureHost(constructed.value.featureHost),
190
+ assets,
207
191
  input: inputBackend,
208
192
  audio: audioBackend,
209
- animationPayloads: createAnimationPayloadLookup(candidateRenderer.assets),
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
- candidateRenderer,
231
- previousRealm?.world,
232
- nextWorld,
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 draw = renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
318
- if (draw !== undefined && !draw.ok) throw draw.error;
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 syncCanvas = options.syncCanvas;
77
- const bootstrapUrl = new URL(executionOptions.bootstrap, globalThis.location?.href).href;
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
  : {}),
@@ -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;
@@ -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
  }
@@ -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 App realm. */
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
- type AssetRuntimeAssembly,
40
- type AssetRuntimeAssemblyError,
41
- assembleAssetRuntime,
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 '@forgeax/engine-render';
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
  ];