@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.
- 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 +331 -362
- 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 +2 -3
- 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
|
@@ -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
|
|
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 (
|
|
17
|
-
ctx.provide('assets', ctx.renderer.assets);
|
|
13
|
+
if (assets !== undefined) ctx.provide('assets', assets);
|
|
18
14
|
},
|
|
19
15
|
};
|
|
20
16
|
}
|
|
21
17
|
|
|
22
|
-
/**
|
|
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(
|
|
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-
|
|
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:
|
|
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:
|
|
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
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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 |
|
|
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
|
-
|
|
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 |
|
|
185
|
-
if (
|
|
182
|
+
function attachPrimary(fireError: (e: AppError | RenderError) => void): void {
|
|
183
|
+
if (leases.has(world)) return;
|
|
186
184
|
try {
|
|
187
|
-
const result = renderer.
|
|
188
|
-
|
|
189
|
-
|
|
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 |
|
|
195
|
+
fireError?: (e: AppError | RenderError) => void,
|
|
198
196
|
): ReadonlySet<World> | undefined {
|
|
199
197
|
if (worlds === undefined) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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 =
|
|
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 (
|
|
210
|
+
if (leases.has(candidate)) {
|
|
214
211
|
next.add(candidate);
|
|
215
212
|
continue;
|
|
216
213
|
}
|
|
217
214
|
try {
|
|
218
|
-
const result = renderer.
|
|
219
|
-
if (result.ok)
|
|
220
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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 |
|
|
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 |
|
|
262
|
+
let frameError: AppError | RenderError | undefined;
|
|
264
263
|
let primaryUpdated = false;
|
|
265
|
-
const reportError = (error: AppError |
|
|
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 &&
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
|
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
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
if (
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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?:
|
|
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 |
|
|
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 {
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
60
|
-
...(options.assetAssembly === undefined
|
|
61
|
+
...(options.rendererFeatureHost === undefined
|
|
61
62
|
? []
|
|
62
|
-
: [
|
|
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
|
|
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
|
|
5
|
-
//
|
|
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)
|
|
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
|
-
* -
|
|
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
|
|
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 {
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
34
|
-
...(options.assetAssembly === undefined
|
|
33
|
+
...(options.rendererFeatureHost === undefined
|
|
35
34
|
? []
|
|
36
|
-
: [
|
|
35
|
+
: [renderFeatureHostPlugin(options.rendererFeatureHost)]),
|
|
36
|
+
rendererAssetsPlugin(options.assets),
|
|
37
37
|
assetsWorldPlugin(),
|
|
38
38
|
inputBackendPlugin(options.input),
|
|
39
39
|
audioBackendPlugin(options.audio),
|