@forgeax/engine-app 0.1.4 → 0.1.7

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 (148) 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/browser-frame-signal.d.ts +17 -0
  10. package/dist/browser-frame-signal.d.ts.map +1 -0
  11. package/dist/create-app.d.ts +31 -10
  12. package/dist/create-app.d.ts.map +1 -1
  13. package/dist/engine-worker-runtime.mjs +72 -129
  14. package/dist/engine-worker-runtime.mjs.map +1 -1
  15. package/dist/errors.d.ts +3 -12
  16. package/dist/errors.d.ts.map +1 -1
  17. package/dist/execution/attached-world-swap.d.ts +1 -12
  18. package/dist/execution/attached-world-swap.d.ts.map +1 -1
  19. package/dist/execution/bootstrap-url.d.ts +5 -0
  20. package/dist/execution/bootstrap-url.d.ts.map +1 -0
  21. package/dist/execution/control.d.ts +0 -2
  22. package/dist/execution/control.d.ts.map +1 -1
  23. package/dist/execution/engine-worker.d.ts +1 -0
  24. package/dist/execution/engine-worker.d.ts.map +1 -1
  25. package/dist/execution/host-controller.d.ts.map +1 -1
  26. package/dist/execution/index.d.ts +1 -1
  27. package/dist/execution/index.d.ts.map +1 -1
  28. package/dist/execution/protocol.d.ts +4 -1
  29. package/dist/execution/protocol.d.ts.map +1 -1
  30. package/dist/execution/types.d.ts +17 -1
  31. package/dist/execution/types.d.ts.map +1 -1
  32. package/dist/game-context.d.ts +1 -1
  33. package/dist/game-context.d.ts.map +1 -1
  34. package/dist/index.d.ts +5 -2
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.mjs +357 -359
  37. package/dist/index.mjs.map +1 -1
  38. package/dist/internal/assembled-engine-profile.d.ts.map +1 -1
  39. package/dist/internal/assets-world-plugin.d.ts +3 -17
  40. package/dist/internal/assets-world-plugin.d.ts.map +1 -1
  41. package/dist/internal/browser-remote-bridge.d.ts +1 -2
  42. package/dist/internal/browser-remote-bridge.d.ts.map +1 -1
  43. package/dist/internal/browser-rhi-debug-runtime.d.ts +1 -1
  44. package/dist/internal/browser-rhi-debug-runtime.d.ts.map +1 -1
  45. package/dist/internal/debug-draw.d.ts +9 -0
  46. package/dist/internal/debug-draw.d.ts.map +1 -0
  47. package/dist/internal/engine-profile-common.d.ts +2 -2
  48. package/dist/internal/engine-profile-common.d.ts.map +1 -1
  49. package/dist/internal/frame-loop.d.ts +7 -4
  50. package/dist/internal/frame-loop.d.ts.map +1 -1
  51. package/dist/internal/main-engine-profile.d.ts +4 -0
  52. package/dist/internal/main-engine-profile.d.ts.map +1 -1
  53. package/dist/internal/remote-serve-flag.d.ts +3 -7
  54. package/dist/internal/remote-serve-flag.d.ts.map +1 -1
  55. package/dist/internal/rhi-capture.d.ts +1 -1
  56. package/dist/internal/rhi-capture.d.ts.map +1 -1
  57. package/dist/internal/worker-engine-profile.d.ts +2 -0
  58. package/dist/internal/worker-engine-profile.d.ts.map +1 -1
  59. package/dist/renderer-plugin.d.ts +25 -0
  60. package/dist/renderer-plugin.d.ts.map +1 -0
  61. package/dist/tool-preview/bootstrap.d.ts.map +1 -1
  62. package/dist/types.d.ts +21 -61
  63. package/dist/types.d.ts.map +1 -1
  64. package/package.json +25 -26
  65. package/src/__tests__/callback-inference.test-d.ts +7 -3
  66. package/src/__tests__/consumer-migration.unit.test.ts +13 -0
  67. package/src/__tests__/create-app-error-retention.integration.test.ts +125 -0
  68. package/src/__tests__/create-app-features.test.ts +21 -10
  69. package/src/__tests__/create-app-listener-sync.test.ts +23 -19
  70. package/src/__tests__/create-app-physics-plugin-failure.integration.test.ts +142 -0
  71. package/src/__tests__/create-app-stop.test.ts +4 -12
  72. package/src/__tests__/create-app.test-d.ts +12 -11
  73. package/src/__tests__/create-app.test.ts +1 -60
  74. package/src/__tests__/create-renderer-error.integration.test.ts +3 -2
  75. package/src/__tests__/create-renderer-lifecycle.integration.test.ts +10 -5
  76. package/src/__tests__/draw-source-injected-world-update-visibility.test.ts +7 -13
  77. package/src/__tests__/draw-source-single-world-regression.test.ts +23 -24
  78. package/src/__tests__/engine-worker-bootstrap-channel.unit.test.ts +8 -0
  79. package/src/__tests__/engine-worker-world-swap.unit.test.ts +15 -22
  80. package/src/__tests__/errors-pointer-lock-failed.test.ts +4 -7
  81. package/src/__tests__/execution-bootstrap-isolation.unit.test.ts +5 -6
  82. package/src/__tests__/execution-frame-order.unit.test.ts +2 -2
  83. package/src/__tests__/execution-frame-protocol.unit.test.ts +3 -4
  84. package/src/__tests__/execution-public-api.test-d.ts +4 -0
  85. package/src/__tests__/frame-loop-time-elapsed.test.ts +1 -1
  86. package/src/__tests__/frame-loop-time.unit.test.ts +1 -1
  87. package/src/__tests__/frame-loop-world-array.test.ts +82 -73
  88. package/src/__tests__/plugin-profiles.unit.test.ts +3 -3
  89. package/src/__tests__/profiler-default-off.test.ts +1 -1
  90. package/src/__tests__/profiler-frame-token.test.ts +17 -30
  91. package/src/__tests__/profiler-phase-integration.test.ts +15 -27
  92. package/src/__tests__/surface-handoff.test.ts +1 -4
  93. package/src/__tests__/visibility-remote.integration.test.ts +2 -5
  94. package/src/__tests__/worker-execution-stop.test.ts +64 -7
  95. package/src/animation-asset-lookup.ts +2 -2
  96. package/src/browser-frame-signal.ts +44 -0
  97. package/src/create-app.ts +208 -295
  98. package/src/errors.ts +27 -39
  99. package/src/execution/attached-world-swap.ts +4 -38
  100. package/src/execution/bootstrap-url.ts +25 -0
  101. package/src/execution/control.ts +3 -7
  102. package/src/execution/engine-worker-runtime.ts +23 -40
  103. package/src/execution/engine-worker.ts +2 -0
  104. package/src/execution/host-controller.ts +8 -2
  105. package/src/execution/index.ts +1 -0
  106. package/src/execution/protocol.ts +4 -1
  107. package/src/execution/types.ts +18 -1
  108. package/src/game-context.ts +1 -1
  109. package/src/index.ts +18 -4
  110. package/src/internal/assembled-engine-profile.ts +3 -11
  111. package/src/internal/assets-world-plugin.ts +7 -53
  112. package/src/internal/browser-remote-bridge.ts +2 -3
  113. package/src/internal/browser-rhi-debug-runtime.ts +1 -1
  114. package/src/internal/debug-draw.ts +38 -0
  115. package/src/internal/engine-profile-common.ts +2 -3
  116. package/src/internal/frame-loop.ts +77 -63
  117. package/src/internal/main-engine-profile.ts +21 -17
  118. package/src/internal/remote-serve-flag.ts +5 -14
  119. package/src/internal/rhi-capture.ts +1 -1
  120. package/src/internal/worker-engine-profile.ts +10 -10
  121. package/src/renderer-plugin.ts +96 -0
  122. package/src/tool-preview/bootstrap.ts +1 -14
  123. package/src/types.ts +22 -77
  124. package/src/vite-env.d.ts +0 -2
  125. package/dist/__tests__/app-rebuild.test.d.ts +0 -2
  126. package/dist/__tests__/app-rebuild.test.d.ts.map +0 -1
  127. package/dist/__tests__/asset-registry-types.test-d.d.ts +0 -2
  128. package/dist/__tests__/asset-registry-types.test-d.d.ts.map +0 -1
  129. package/dist/__tests__/assets-runtime-assembly.integration.test.d.ts +0 -2
  130. package/dist/__tests__/assets-runtime-assembly.integration.test.d.ts.map +0 -1
  131. package/dist/__tests__/draw-source-headless-full-chain.test.d.ts +0 -2
  132. package/dist/__tests__/draw-source-headless-full-chain.test.d.ts.map +0 -1
  133. package/dist/__tests__/ecs-import.unit.test.d.ts +0 -2
  134. package/dist/__tests__/ecs-import.unit.test.d.ts.map +0 -1
  135. package/dist/__tests__/execution-world-health-owner.test-d.d.ts +0 -2
  136. package/dist/__tests__/execution-world-health-owner.test-d.d.ts.map +0 -1
  137. package/dist/__tests__/remote-serve-flag.test.d.ts +0 -2
  138. package/dist/__tests__/remote-serve-flag.test.d.ts.map +0 -1
  139. package/dist/assets-runtime-assembly.d.ts +0 -19
  140. package/dist/assets-runtime-assembly.d.ts.map +0 -1
  141. package/src/__tests__/app-rebuild.test.ts +0 -31
  142. package/src/__tests__/asset-registry-types.test-d.ts +0 -16
  143. package/src/__tests__/assets-runtime-assembly.integration.test.ts +0 -68
  144. package/src/__tests__/draw-source-headless-full-chain.test.ts +0 -226
  145. package/src/__tests__/ecs-import.unit.test.ts +0 -40
  146. package/src/__tests__/execution-world-health-owner.test-d.ts +0 -9
  147. package/src/__tests__/remote-serve-flag.test.ts +0 -24
  148. package/src/assets-runtime-assembly.ts +0 -70
@@ -1,79 +1,33 @@
1
1
  import type { AssetRegistry } from '@forgeax/engine-assets-runtime';
2
2
  import type { Plugin } from '@forgeax/engine-plugin';
3
- import type { RenderAssetPort } from '@forgeax/engine-render/internal';
4
- import type { AssetRuntimeAssembly } from '../assets-runtime-assembly';
5
3
 
6
- const RENDER_ASSET_PORT_RESOURCE_KEY = 'RenderAssetPort' as const;
7
4
  const ASSET_REGISTRY_RESOURCE_KEY = 'AssetRegistry' as const;
8
5
 
9
- /** Project the Renderer-owned render-only asset view as a declared service. */
10
- export function rendererAssetsPlugin(): Plugin {
6
+ /** Project the Renderer-owned AssetRegistry as a declared dependent service. */
7
+ export function rendererAssetsPlugin(assets: AssetRegistry | undefined): Plugin {
11
8
  return {
12
9
  name: 'renderer-assets',
13
10
  inject: ['renderer'],
14
11
  provide: 'assets',
15
12
  apply(ctx) {
16
- if (ctx.renderer === undefined) throw new Error('assets require the renderer service');
17
- ctx.provide('assets', ctx.renderer.assets);
13
+ if (assets !== undefined) ctx.provide('assets', assets);
18
14
  },
19
15
  };
20
16
  }
21
17
 
22
- /** Adapt the Registry and its decoder leases to the App Fiber lifecycle. */
23
- export function assetRegistryPlugin(assembly: AssetRuntimeAssembly): Plugin {
24
- return {
25
- name: 'asset-registry',
26
- provide: 'assetRegistry',
27
- apply(ctx) {
28
- ctx.provide('assetRegistry', assembly.registry);
29
- ctx.effect(
30
- () => () => {
31
- assembly.dispose();
32
- assembly.registry.dispose();
33
- },
34
- 'assets/registry',
35
- );
36
- },
37
- };
38
- }
39
-
40
- /** Project the render-only view into World resource lookup reversibly. */
18
+ /** Project the realm AssetRegistry into legacy World resource lookup reversibly. */
41
19
  export function assetsWorldPlugin(): Plugin {
42
20
  return {
43
21
  name: 'assets-world',
44
22
  inject: ['world', 'assets'],
45
23
  apply(ctx) {
24
+ if (ctx.assets === undefined) return;
46
25
  ctx.effect(() => {
47
- ctx.world.insertResource(RENDER_ASSET_PORT_RESOURCE_KEY, ctx.assets);
48
- return () => {
49
- ctx.world.removeResource(RENDER_ASSET_PORT_RESOURCE_KEY);
50
- };
51
- }, 'assets/world-render-projection');
52
- },
53
- };
54
- }
55
-
56
- /** Project the App-owned Registry into World resource lookup reversibly. */
57
- export function assetRegistryWorldPlugin(): Plugin {
58
- return {
59
- name: 'asset-registry-world',
60
- inject: ['world', 'assetRegistry'],
61
- apply(ctx) {
62
- ctx.effect(() => {
63
- ctx.world.insertResource(ASSET_REGISTRY_RESOURCE_KEY, ctx.assetRegistry);
26
+ ctx.world.insertResource(ASSET_REGISTRY_RESOURCE_KEY, ctx.assets);
64
27
  return () => {
65
28
  ctx.world.removeResource(ASSET_REGISTRY_RESOURCE_KEY);
66
29
  };
67
- }, 'assets/world-registry');
30
+ }, 'assets/world-resource');
68
31
  },
69
32
  };
70
33
  }
71
-
72
- declare module '@forgeax/engine-plugin' {
73
- interface EngineContextServices {
74
- /** Renderer-owned render projection; GameHost owns the load registry separately. */
75
- assets?: RenderAssetPort;
76
- /** App-owned typed runtime Registry for GUID loads and snapshots. */
77
- assetRegistry?: AssetRegistry;
78
- }
79
- }
@@ -23,7 +23,6 @@
23
23
  // installed path, while @vite-ignore would make a browser resolve the bare
24
24
  // specifier from the consumer document root and yield a 500.
25
25
 
26
- import type { AssetRegistry } from '@forgeax/engine-assets-runtime';
27
26
  import { Update, type World } from '@forgeax/engine-ecs';
28
27
  import { createEcsImportModule } from './ecs-import';
29
28
 
@@ -35,7 +34,7 @@ type ExecuteModule = {
35
34
  ctx: {
36
35
  world: unknown;
37
36
  renderer: unknown;
38
- assets: AssetRegistry;
37
+ assets: unknown;
39
38
  rhiCapture?: unknown;
40
39
  profiler?: unknown;
41
40
  execution?: unknown;
@@ -73,7 +72,7 @@ function canonicalRuntimeModule(moduleValue: unknown, world: World): unknown {
73
72
  export interface BrowserRemoteBridgeDeps {
74
73
  readonly world: World;
75
74
  readonly renderer: unknown;
76
- readonly assets: AssetRegistry;
75
+ readonly assets: unknown;
77
76
  /** The host's already-loaded runtime namespace; preserves component-token identity. */
78
77
  readonly runtimeModule: unknown;
79
78
  readonly rhiCapture?: unknown;
@@ -15,7 +15,7 @@ export interface BrowserRhiDebugRuntime {
15
15
  import('@forgeax/engine-types').Result<RhiDevice, unknown>
16
16
  >;
17
17
  readonly createShaderModule: CreateShaderModuleFn;
18
- readonly rhiInstrumentation: import('@forgeax/engine-render/internal').RhiBackendInstrumentation;
18
+ readonly rhiInstrumentation: import('@forgeax/engine-render/internal/construct-renderer').RhiBackendInstrumentation;
19
19
  attachRenderer(renderer: Renderer): () => void;
20
20
  }
21
21
 
@@ -0,0 +1,38 @@
1
+ import type { DebugDraw } from '@forgeax/engine-debug-draw';
2
+ import { createDebugDraw } from '@forgeax/engine-debug-draw';
3
+ import type { RendererHostAssembly } from '@forgeax/engine-render/internal/construct-renderer';
4
+ import type { TextureFormat } from '@forgeax/engine-rhi';
5
+ import { selectSwapChainFormat } from '../../../render/src/record/render-context';
6
+
7
+ export type RendererDebugDrawHost = Pick<
8
+ RendererHostAssembly['debugDrawHost'],
9
+ 'initialization' | 'device' | '_internal_createShaderModule' | '_internal_setRenderOverlay'
10
+ >;
11
+
12
+ /** Create the App-owned debug overlay after the renderer has initialized. */
13
+ export async function createDebugDrawOnReady(
14
+ context: RendererDebugDrawHost,
15
+ format?: TextureFormat,
16
+ ): Promise<DebugDraw> {
17
+ const ready = await context.initialization;
18
+ if (!ready.ok) throw ready.error;
19
+
20
+ const resolvedFormat =
21
+ format ??
22
+ (selectSwapChainFormat(context.device.caps.storageBuffer).view as unknown as TextureFormat);
23
+ const result = await createDebugDraw({
24
+ device: context.device,
25
+ queue: context.device.queue,
26
+ createShaderModule: context._internal_createShaderModule,
27
+ format: resolvedFormat,
28
+ });
29
+ if (!result.ok) throw result.error;
30
+ context._internal_setRenderOverlay(result.value);
31
+ return result.value;
32
+ }
33
+
34
+ /** Release the App-owned debug overlay and clear its Render capability. */
35
+ export function releaseDebugDraw(context: RendererDebugDrawHost, debugDraw: DebugDraw): void {
36
+ context._internal_setRenderOverlay(undefined);
37
+ debugDraw.destroy();
38
+ }
@@ -1,10 +1,9 @@
1
+ import type { AssetRegistry } from '@forgeax/engine-assets-runtime';
1
2
  import type { Plugin } from '@forgeax/engine-plugin';
2
3
  import type { Renderer } from '@forgeax/engine-render';
3
4
 
4
- import type { AssetRuntimeAssembly } from '../assets-runtime-assembly';
5
-
6
5
  export interface EngineProfileBase {
7
6
  readonly renderer: Renderer;
8
- readonly assetAssembly?: AssetRuntimeAssembly;
7
+ readonly assets?: AssetRegistry;
9
8
  readonly extensions?: readonly Plugin[];
10
9
  }
@@ -1,7 +1,6 @@
1
1
  import type { World } from '@forgeax/engine-ecs';
2
2
  import type { ProfileFrameToken, Profiler, RecorderSession } from '@forgeax/engine-profiler';
3
- import type { Renderer } from '@forgeax/engine-render';
4
- import type { RhiError } from '@forgeax/engine-rhi/errors';
3
+ import type { RenderError, Renderer, RenderWorldLease } from '@forgeax/engine-render';
5
4
  import { err, ok, type Result } from '@forgeax/engine-types';
6
5
 
7
6
  import type { AppErrorCode, AppErrorDetailFor } from '../errors';
@@ -13,7 +12,9 @@ export type FrameState = 'idle' | 'running' | 'paused' | 'stopped';
13
12
  export interface FrameLoopOptions {
14
13
  readonly world: World;
15
14
  readonly renderer: Renderer;
16
- readonly onError?: (e: AppError | RhiError) => void;
15
+ readonly onError?: (e: AppError | RenderError) => void;
16
+ /** Optional dev-only RHI recorder hook; called once after each frame draw. */
17
+ readonly debugRhi?: { onFrameEnd(): void };
17
18
  readonly now?: () => number;
18
19
  readonly raf?: (cb: (t: number) => void) => number;
19
20
  readonly caf?: (id: number) => void;
@@ -29,7 +30,7 @@ export interface FrameLoopHandle {
29
30
  pause(): Result<void, AppError>;
30
31
  resume(): Result<void, AppError>;
31
32
  /** Run one complete update/draw frame through this loop while paused. */
32
- stepFrame(deltaSeconds: number): Result<void, AppError | RhiError>;
33
+ stepFrame(deltaSeconds: number): Result<void, AppError | RenderError>;
33
34
  /** Replace the per-frame world routing pull without replacing the loop. */
34
35
  setDrawSource(drawSource: FrameLoopOptions['drawSource']): void;
35
36
  getState(): FrameState;
@@ -102,7 +103,7 @@ function makeWorldUpdateError(cause: unknown): AppError {
102
103
 
103
104
  function fireWorldUpdateResult(
104
105
  result: ReturnType<World['update']>,
105
- fireError: ((e: AppError | RhiError) => void) | undefined,
106
+ fireError: ((e: AppError | RenderError) => void) | undefined,
106
107
  ): boolean {
107
108
  if (!result.ok && fireError !== undefined) {
108
109
  fireError(makeWorldUpdateError(result.error));
@@ -114,7 +115,7 @@ function updateInjectedWorlds(
114
115
  worlds: readonly World[],
115
116
  ownWorld: World,
116
117
  deltaSeconds: number,
117
- fireError: ((e: AppError | RhiError) => void) | undefined,
118
+ fireError: ((e: AppError | RenderError) => void) | undefined,
118
119
  attachedWorlds: ReadonlySet<World>,
119
120
  updatedWorlds: Set<World>,
120
121
  ): void {
@@ -163,7 +164,6 @@ function resolveCaf(opts: FrameLoopOptions): (id: number) => void {
163
164
 
164
165
  export function createFrameLoop(opts: FrameLoopOptions): FrameLoopHandle {
165
166
  const { world, renderer } = opts;
166
- const primaryDrawWorlds: readonly World[] = [world];
167
167
  const phaseCatalogRegistration = opts.profiler?.registerPhaseCatalog('app', APP_PHASE_CATALOG);
168
168
  let releasePhaseCatalog =
169
169
  phaseCatalogRegistration?.ok === true ? phaseCatalogRegistration.value : undefined;
@@ -177,16 +177,14 @@ export function createFrameLoop(opts: FrameLoopOptions): FrameLoopHandle {
177
177
  let pendingFrameId = 0;
178
178
  let profilerFrameId = 0;
179
179
  let profilerCaptureId: string | undefined;
180
- let primaryAttached = false;
181
- let activeInjectedWorlds: Set<World> | undefined;
182
- let nextInjectedWorlds: Set<World> | undefined;
180
+ const leases = new Map<World, RenderWorldLease>();
183
181
 
184
- function attachPrimary(fireError: (e: AppError | RhiError) => void): void {
185
- if (primaryAttached) return;
182
+ function attachPrimary(fireError: (e: AppError | RenderError) => void): void {
183
+ if (leases.has(world)) return;
186
184
  try {
187
- const result = renderer.attachWorld(world);
188
- primaryAttached = result.ok;
189
- if (!result.ok) fireError(result.error);
185
+ const result = renderer.attach(world);
186
+ if (result.ok) leases.set(world, result.value);
187
+ else fireError(result.error);
190
188
  } catch (cause: unknown) {
191
189
  fireError(makeWorldUpdateError(cause));
192
190
  }
@@ -194,42 +192,41 @@ export function createFrameLoop(opts: FrameLoopOptions): FrameLoopHandle {
194
192
 
195
193
  function syncInjectedAttachments(
196
194
  worlds: readonly World[] | undefined,
197
- fireError?: (e: AppError | RhiError) => void,
195
+ fireError?: (e: AppError | RenderError) => void,
198
196
  ): ReadonlySet<World> | undefined {
199
197
  if (worlds === undefined) {
200
- if (activeInjectedWorlds !== undefined) {
201
- for (const attached of activeInjectedWorlds) renderer.detachWorld(attached);
202
- activeInjectedWorlds.clear();
198
+ for (const [attached, lease] of leases) {
199
+ if (attached !== world) {
200
+ lease.dispose();
201
+ leases.delete(attached);
202
+ }
203
203
  }
204
204
  return undefined;
205
205
  }
206
206
 
207
- const next = nextInjectedWorlds ?? new Set<World>();
208
- nextInjectedWorlds = next;
209
- next.clear();
210
- const active = activeInjectedWorlds;
207
+ const next = new Set<World>();
211
208
  for (const candidate of worlds) {
212
209
  if (candidate === world || next.has(candidate)) continue;
213
- if (active?.has(candidate) === true) {
210
+ if (leases.has(candidate)) {
214
211
  next.add(candidate);
215
212
  continue;
216
213
  }
217
214
  try {
218
- const result = renderer.attachWorld(candidate);
219
- if (result.ok) next.add(candidate);
220
- else fireError?.(result.error);
215
+ const result = renderer.attach(candidate);
216
+ if (result.ok) {
217
+ leases.set(candidate, result.value);
218
+ next.add(candidate);
219
+ } else fireError?.(result.error);
221
220
  } catch (cause: unknown) {
222
221
  fireError?.(makeWorldUpdateError(cause));
223
222
  }
224
223
  }
225
- if (active !== undefined) {
226
- for (const attached of active) {
227
- if (!next.has(attached)) renderer.detachWorld(attached);
224
+ for (const [attached, lease] of leases) {
225
+ if (attached !== world && !next.has(attached)) {
226
+ lease.dispose();
227
+ leases.delete(attached);
228
228
  }
229
- active.clear();
230
229
  }
231
- activeInjectedWorlds = next;
232
- nextInjectedWorlds = active;
233
230
  return next;
234
231
  }
235
232
 
@@ -239,9 +236,11 @@ export function createFrameLoop(opts: FrameLoopOptions): FrameLoopHandle {
239
236
 
240
237
  function releaseAttachments(): void {
241
238
  releaseInjectedAttachments();
242
- if (!primaryAttached) return;
243
- renderer.detachWorld(world);
244
- primaryAttached = false;
239
+ const lease = leases.get(world);
240
+ if (lease !== undefined) {
241
+ lease.dispose();
242
+ leases.delete(world);
243
+ }
245
244
  }
246
245
 
247
246
  function runProfiledPhase<T>(
@@ -257,12 +256,12 @@ export function createFrameLoop(opts: FrameLoopOptions): FrameLoopHandle {
257
256
  }
258
257
  }
259
258
 
260
- function runFrame(deltaSeconds: number): Result<void, AppError | RhiError> {
259
+ function runFrame(deltaSeconds: number): Result<void, AppError | RenderError> {
261
260
  const session = opts.profiler?.activeSession();
262
261
  let profileFrame: ProfileFrameToken | undefined;
263
- let frameError: AppError | RhiError | undefined;
262
+ let frameError: AppError | RenderError | undefined;
264
263
  let primaryUpdated = false;
265
- const reportError = (error: AppError | RhiError): void => {
264
+ const reportError = (error: AppError | RenderError): void => {
266
265
  frameError ??= error;
267
266
  opts.onError?.(error);
268
267
  };
@@ -304,7 +303,7 @@ export function createFrameLoop(opts: FrameLoopOptions): FrameLoopHandle {
304
303
 
305
304
  const attachedInjectedWorlds = syncInjectedAttachments(injected?.worlds, reportError);
306
305
  const updatedWorlds = injected === undefined ? undefined : new Set<World>();
307
- if (updatedWorlds !== undefined && primaryUpdated && primaryAttached) {
306
+ if (updatedWorlds !== undefined && primaryUpdated && leases.has(world)) {
308
307
  updatedWorlds.add(world);
309
308
  }
310
309
 
@@ -327,32 +326,46 @@ export function createFrameLoop(opts: FrameLoopOptions): FrameLoopHandle {
327
326
 
328
327
  runProfiledPhase(session, 'renderer-draw', () => {
329
328
  try {
330
- const profileOptions = profileFrame === undefined ? {} : { profileFrame };
331
329
  let drawResult: ReturnType<Renderer['draw']>;
332
330
  if (injected === undefined) {
333
- if (!primaryUpdated || !primaryAttached) return;
334
- drawResult = renderer.draw(primaryDrawWorlds, {
335
- cameraOwner: 0,
336
- resourceOwner: 0,
337
- ...profileOptions,
331
+ const primaryLease = leases.get(world);
332
+ if (!primaryUpdated || primaryLease === undefined) return;
333
+ drawResult = renderer.draw({
334
+ leases: [primaryLease],
335
+ camera: { lease: primaryLease },
336
+ environment: { lease: primaryLease },
337
+ ...(profileFrame === undefined ? {} : { profileFrame }),
338
338
  });
339
339
  } else {
340
340
  if (updatedWorlds === undefined) return;
341
- const readyWorlds = injected.worlds.filter((candidate) => updatedWorlds.has(candidate));
341
+ const readyEntries = injected.worlds
342
+ .filter((candidate) => updatedWorlds.has(candidate))
343
+ .map((candidate) => ({ candidate, lease: leases.get(candidate) }))
344
+ .filter(
345
+ (entry): entry is { candidate: World; lease: RenderWorldLease } =>
346
+ entry.lease !== undefined,
347
+ );
348
+ const readyLeases = readyEntries.map((entry) => entry.lease);
342
349
  const cameraWorld = injected.worlds[injected.cameraOwner];
343
- const resourceWorld = injected.worlds[injected.resourceOwner];
344
- const cameraOwner = cameraWorld === undefined ? -1 : readyWorlds.indexOf(cameraWorld);
345
- const resourceOwner =
346
- resourceWorld === undefined ? -1 : readyWorlds.indexOf(resourceWorld);
347
- if (readyWorlds.length === 0 || cameraOwner < 0 || resourceOwner < 0) return;
348
- drawResult = renderer.draw(readyWorlds, {
349
- cameraOwner,
350
- resourceOwner,
351
- ...profileOptions,
350
+ const environmentWorld = injected.worlds[injected.resourceOwner];
351
+ if (cameraWorld === undefined || environmentWorld === undefined) return;
352
+ const cameraLease = leases.get(cameraWorld);
353
+ const environmentLease = leases.get(environmentWorld);
354
+ if (
355
+ readyLeases.length === 0 ||
356
+ cameraLease === undefined ||
357
+ environmentLease === undefined
358
+ )
359
+ return;
360
+ drawResult = renderer.draw({
361
+ leases: readyLeases,
362
+ camera: { lease: cameraLease },
363
+ environment: { lease: environmentLease },
364
+ ...(profileFrame === undefined ? {} : { profileFrame }),
352
365
  });
353
366
  }
354
367
  if (drawResult !== undefined) {
355
- const result = drawResult as { ok: boolean; error?: RhiError };
368
+ const result = drawResult as { ok: boolean; error?: RenderError };
356
369
  if (!result.ok && result.error !== undefined) {
357
370
  reportError(result.error);
358
371
  }
@@ -363,6 +376,12 @@ export function createFrameLoop(opts: FrameLoopOptions): FrameLoopHandle {
363
376
  });
364
377
  });
365
378
  endFrame(session);
379
+ try {
380
+ opts.debugRhi?.onFrameEnd();
381
+ } catch {
382
+ // Debug capture is an observational bridge. A recorder failure must not
383
+ // change the App frame result or make the host loop stop.
384
+ }
366
385
  return frameError === undefined ? ok(undefined) : err(frameError);
367
386
  }
368
387
 
@@ -374,11 +393,6 @@ export function createFrameLoop(opts: FrameLoopOptions): FrameLoopHandle {
374
393
  // Renderer.recover() rebuild, but freeze simulation so recovery does not
375
394
  // advance the World against frames that cannot be submitted. The next
376
395
  // tick observes `alive` and resumes the normal update/draw sequence.
377
- if (renderer.health?.().reason === 'device-lost') {
378
- pendingFrameId = raf(tick);
379
- return;
380
- }
381
-
382
396
  const timestamp = now();
383
397
  const deltaSeconds = (timestamp - lastTimestamp) / 1000;
384
398
  lastTimestamp = timestamp;
@@ -397,7 +411,7 @@ export function createFrameLoop(opts: FrameLoopOptions): FrameLoopHandle {
397
411
  if (drawSource !== nextDrawSource) syncInjectedAttachments(undefined);
398
412
  drawSource = nextDrawSource;
399
413
  },
400
- stepFrame(deltaSeconds): Result<void, AppError | RhiError> {
414
+ stepFrame(deltaSeconds): Result<void, AppError | RenderError> {
401
415
  const reason =
402
416
  state !== 'paused'
403
417
  ? 'state'
@@ -11,22 +11,23 @@ import {
11
11
  ownedInputBackendPlugin,
12
12
  } from '@forgeax/engine-input';
13
13
  import type { Plugin } from '@forgeax/engine-plugin';
14
- import { ownedRendererPlugin, renderComponentsPlugin } from '@forgeax/engine-render';
15
- import { createDebugDrawOnReady, releaseDebugDraw } from '@forgeax/engine-render/internal';
14
+ import { renderComponentsPlugin } from '@forgeax/engine-render';
16
15
  import { scenePlugin } from '@forgeax/engine-scene';
17
16
  import { statePlugin } from '@forgeax/engine-state';
18
-
19
17
  import { inputMapPlugin } from '../input-map-plugin';
20
18
  import { inputPlugin } from '../input-plugin';
21
19
  import {
22
- assetRegistryPlugin,
23
- assetRegistryWorldPlugin,
24
- assetsWorldPlugin,
25
- rendererAssetsPlugin,
26
- } from './assets-world-plugin';
20
+ ownedRendererPlugin,
21
+ type RenderFeatureHost,
22
+ renderFeatureHostPlugin,
23
+ } from '../renderer-plugin';
24
+ import { assetsWorldPlugin, rendererAssetsPlugin } from './assets-world-plugin';
25
+ import { createDebugDrawOnReady, type RendererDebugDrawHost, releaseDebugDraw } from './debug-draw';
27
26
  import type { EngineProfileBase } from './engine-profile-common';
28
27
 
29
28
  export interface MainEngineProfileOptions extends EngineProfileBase {
29
+ readonly rendererDebugDrawHost?: RendererDebugDrawHost;
30
+ readonly rendererFeatureHost?: RenderFeatureHost;
30
31
  readonly animationPayloads: AnimationPayloadLookup;
31
32
  readonly onDebugDrawReady?: (debugDraw: DebugDraw) => void;
32
33
  readonly input?: InputBackend;
@@ -34,16 +35,17 @@ export interface MainEngineProfileOptions extends EngineProfileBase {
34
35
  readonly inputMap?: readonly ActionConfig[];
35
36
  }
36
37
 
37
- function debugDrawPlugin(onReady: (debugDraw: DebugDraw) => void): Plugin {
38
+ function debugDrawPlugin(
39
+ context: RendererDebugDrawHost,
40
+ onReady: (debugDraw: DebugDraw) => void,
41
+ ): Plugin {
38
42
  return {
39
43
  name: 'debug-draw',
40
- inject: ['renderer'],
41
44
  async apply(ctx) {
42
- if (ctx.renderer === undefined) throw new Error('debug draw requires the renderer service');
43
45
  try {
44
- const debugDraw = await createDebugDrawOnReady(ctx.renderer);
46
+ const debugDraw = await createDebugDrawOnReady(context);
45
47
  onReady(debugDraw);
46
- ctx.effect(() => () => releaseDebugDraw(debugDraw), 'render/debug-draw');
48
+ ctx.effect(() => () => releaseDebugDraw(context, debugDraw), 'render/debug-draw');
47
49
  } catch {
48
50
  // DebugDraw is optional; renderer error reporting owns creation failures.
49
51
  }
@@ -56,10 +58,10 @@ export function mainEngineProfile(options: MainEngineProfileOptions): Plugin[] {
56
58
  return [
57
59
  ownedRendererPlugin(options.renderer),
58
60
  renderComponentsPlugin(),
59
- rendererAssetsPlugin(),
60
- ...(options.assetAssembly === undefined
61
+ ...(options.rendererFeatureHost === undefined
61
62
  ? []
62
- : [assetRegistryPlugin(options.assetAssembly), assetRegistryWorldPlugin()]),
63
+ : [renderFeatureHostPlugin(options.rendererFeatureHost)]),
64
+ rendererAssetsPlugin(options.assets),
63
65
  assetsWorldPlugin(),
64
66
  ...(options.input === undefined
65
67
  ? []
@@ -72,7 +74,9 @@ export function mainEngineProfile(options: MainEngineProfileOptions): Plugin[] {
72
74
  animationPayloadsPlugin(options.animationPayloads),
73
75
  animationRuntimePlugin(),
74
76
  statePlugin(),
75
- ...(options.onDebugDrawReady === undefined ? [] : [debugDrawPlugin(options.onDebugDrawReady)]),
77
+ ...(options.onDebugDrawReady === undefined || options.rendererDebugDrawHost === undefined
78
+ ? []
79
+ : [debugDrawPlugin(options.rendererDebugDrawHost, options.onDebugDrawReady)]),
76
80
  ...(options.input === undefined ? [] : [inputPlugin()]),
77
81
  ...(options.inputMap === undefined ? [] : [inputMapPlugin(options.inputMap)]),
78
82
  ...(options.extensions ?? []),
@@ -1,16 +1,13 @@
1
1
  // @forgeax/engine-app/internal/remote-serve-flag -- FORGEAX_ENGINE_REMOTE_SERVE
2
2
  // dual-source resolution (plan-strategy secondary D-7).
3
3
  //
4
- // The remote eval server is auto-started in dev mode on Node hosts (vite's
5
- // import.meta.env.DEV) and production builds skip it. Browsers never start this
6
- // server: it owns a Node WebSocket listener and the browser has a separate
7
- // opt-in bridge. Headless/dawn-node can opt in via the
4
+ // The remote eval server is auto-started in dev mode (vite's import.meta.env.DEV)
5
+ // and production builds skip it. Headless/dawn-node can opt in via the
8
6
  // FORGEAX_ENGINE_REMOTE_SERVE=1 env var.
9
7
  //
10
8
  // Dual sources (mirrors rhi-debug-flag.ts pattern):
11
9
  // browser (vite): import.meta.env.DEV is statically replaced by vite
12
- // (true in dev server, false in production build), but the browser guard
13
- // below keeps the Node-only server import out of the page.
10
+ // (true in dev server, false in production build).
14
11
  // dawn-node : import.meta may be undefined; fallback to
15
12
  // globalThis.process.env.FORGEAX_ENGINE_REMOTE_SERVE === '1'.
16
13
  //
@@ -26,24 +23,18 @@ interface RemoteServeEnv {
26
23
  * Resolve whether the remote eval server should be started.
27
24
  *
28
25
  * Returns `true` when:
29
- * - a Node host is in vite dev mode (import.meta.env.DEV === true), OR
26
+ * - vite dev mode (import.meta.env.DEV === true), OR
30
27
  * - headless/dawn-node with FORGEAX_ENGINE_REMOTE_SERVE=1 opt-in
31
28
  *
32
- * Returns `false` in browsers, production builds, and headless hosts without
33
- * explicit opt-in. The browser path must not even request the Node-only
34
- * `@forgeax/engine-remote/server` module: Vite would otherwise discover it
35
- * during the first Play boot and refresh the live dependency graph.
29
+ * Returns `false` in production builds and headless without explicit opt-in.
36
30
  *
37
31
  * @param isDev flag from import.meta.env.DEV (undefined when import.meta absent)
38
32
  * @param processEnv globalThis.process?.env when present, else undefined
39
- * @param isBrowser true for the DOM canvas host; false for Node/headless hosts
40
33
  */
41
34
  export function resolveRemoteServeFlag(
42
35
  isDev: boolean | undefined,
43
36
  processEnv: RemoteServeEnv | undefined,
44
- isBrowser = false,
45
37
  ): boolean {
46
- if (isBrowser) return false;
47
38
  if (isDev === true) return true;
48
39
  if (processEnv?.FORGEAX_ENGINE_REMOTE_SERVE === '1') return true;
49
40
  return false;
@@ -1,4 +1,4 @@
1
- import type { RhiBackendInstrumentation } from '@forgeax/engine-render/internal';
1
+ import type { RhiBackendInstrumentation } from '@forgeax/engine-render/internal/construct-renderer';
2
2
  import { RhiError } from '@forgeax/engine-rhi';
3
3
  import type {
4
4
  CaptureFrameOptions,
@@ -6,23 +6,23 @@ import {
6
6
  import { type AudioBackend, audioBackendPlugin } from '@forgeax/engine-audio';
7
7
  import { type InputBackend, inputBackendPlugin } from '@forgeax/engine-input';
8
8
  import type { Plugin } from '@forgeax/engine-plugin';
9
- import { ownedRendererPlugin, renderComponentsPlugin } from '@forgeax/engine-render';
9
+ import { renderComponentsPlugin } from '@forgeax/engine-render';
10
10
  import { scenePlugin } from '@forgeax/engine-scene';
11
11
  import { statePlugin } from '@forgeax/engine-state';
12
-
13
12
  import { inputPlugin } from '../input-plugin';
14
13
  import {
15
- assetRegistryPlugin,
16
- assetRegistryWorldPlugin,
17
- assetsWorldPlugin,
18
- rendererAssetsPlugin,
19
- } from './assets-world-plugin';
14
+ ownedRendererPlugin,
15
+ type RenderFeatureHost,
16
+ renderFeatureHostPlugin,
17
+ } from '../renderer-plugin';
18
+ import { assetsWorldPlugin, rendererAssetsPlugin } from './assets-world-plugin';
20
19
  import type { EngineProfileBase } from './engine-profile-common';
21
20
 
22
21
  export interface WorkerEngineProfileOptions extends EngineProfileBase {
23
22
  readonly animationPayloads: AnimationPayloadLookup;
24
23
  readonly input: InputBackend;
25
24
  readonly audio: AudioBackend;
25
+ readonly rendererFeatureHost?: RenderFeatureHost;
26
26
  }
27
27
 
28
28
  /** Static Engine Worker selection; Host-only acquisition stays outside this realm. */
@@ -30,10 +30,10 @@ export function workerEngineProfile(options: WorkerEngineProfileOptions): Plugin
30
30
  return [
31
31
  ownedRendererPlugin(options.renderer),
32
32
  renderComponentsPlugin(),
33
- rendererAssetsPlugin(),
34
- ...(options.assetAssembly === undefined
33
+ ...(options.rendererFeatureHost === undefined
35
34
  ? []
36
- : [assetRegistryPlugin(options.assetAssembly), assetRegistryWorldPlugin()]),
35
+ : [renderFeatureHostPlugin(options.rendererFeatureHost)]),
36
+ rendererAssetsPlugin(options.assets),
37
37
  assetsWorldPlugin(),
38
38
  inputBackendPlugin(options.input),
39
39
  audioBackendPlugin(options.audio),