@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
package/src/create-app.ts
CHANGED
|
@@ -23,40 +23,30 @@
|
|
|
23
23
|
// inherits .tagName from HTMLElement, so the property test cleanly
|
|
24
24
|
// separates the canvas argument from the AppAssembleArgs plain object.
|
|
25
25
|
|
|
26
|
-
import {
|
|
27
|
-
type AssetRegistry,
|
|
28
|
-
type CatalogSource,
|
|
29
|
-
createAssetRegistry,
|
|
30
|
-
createCatalogSource,
|
|
31
|
-
} from '@forgeax/engine-assets-runtime';
|
|
32
|
-
import { getAssetRegistryResolver } from '@forgeax/engine-assets-runtime/internal';
|
|
26
|
+
import type { AssetRegistry } from '@forgeax/engine-assets-runtime';
|
|
33
27
|
import type { AudioBackend } from '@forgeax/engine-audio';
|
|
34
28
|
import type { DebugDraw } from '@forgeax/engine-debug-draw';
|
|
35
29
|
import { createWorldContext, Update, World } from '@forgeax/engine-ecs';
|
|
36
30
|
import type { InputBackend } from '@forgeax/engine-input';
|
|
37
31
|
import type { Context, Plugin } from '@forgeax/engine-plugin';
|
|
38
|
-
import type {
|
|
39
|
-
import {
|
|
32
|
+
import type { RenderError } from '@forgeax/engine-render';
|
|
33
|
+
import { CAMERA_PROJECTION_PERSPECTIVE, Camera, type Renderer } from '@forgeax/engine-render';
|
|
34
|
+
import type { RendererHostAssembly } from '@forgeax/engine-render/internal/construct-renderer';
|
|
40
35
|
import { RhiError } from '@forgeax/engine-rhi';
|
|
41
|
-
import
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
RecorderAttachment,
|
|
36
|
+
import {
|
|
37
|
+
attachRecorder,
|
|
38
|
+
type CaptureFrameOptions,
|
|
39
|
+
type RecorderAttachment,
|
|
45
40
|
} from '@forgeax/engine-rhi-debug';
|
|
46
41
|
import * as engineRuntimeModule from '@forgeax/engine-runtime';
|
|
47
|
-
import {
|
|
48
|
-
import {
|
|
42
|
+
import { EngineEnvironmentError } from '@forgeax/engine-runtime';
|
|
43
|
+
import {
|
|
44
|
+
constructRuntimeRendererHost,
|
|
45
|
+
loadRhiPack,
|
|
46
|
+
} from '@forgeax/engine-runtime/internal/renderer-host';
|
|
47
|
+
import { err, ok, type Result } from '@forgeax/engine-types';
|
|
49
48
|
|
|
50
49
|
import { createAnimationPayloadLookup } from './animation-asset-lookup';
|
|
51
|
-
|
|
52
|
-
export { syncCameraAspect } from './canvas-policy';
|
|
53
|
-
|
|
54
|
-
import {
|
|
55
|
-
type AssetRuntimeAssembly,
|
|
56
|
-
type AssetRuntimeAssemblyError,
|
|
57
|
-
assembleAssetRuntime,
|
|
58
|
-
} from './assets-runtime-assembly';
|
|
59
|
-
import { syncCameraAspect } from './canvas-policy';
|
|
60
50
|
import type { AppErrorCode, AppErrorDetailFor } from './errors';
|
|
61
51
|
import { APP_ERROR_HINTS, APP_EXPECTED, AppError } from './errors';
|
|
62
52
|
import {
|
|
@@ -69,6 +59,7 @@ import {
|
|
|
69
59
|
selectExecutionTier,
|
|
70
60
|
unavailableExecutionCapabilities,
|
|
71
61
|
} from './execution';
|
|
62
|
+
import { normalizeExecutionBootstrapUrl } from './execution/bootstrap-url';
|
|
72
63
|
import { createLocalExecutionControl } from './execution/control';
|
|
73
64
|
import { createWorkerExecutionApp } from './execution/host-controller';
|
|
74
65
|
import { assembledEngineProfile } from './internal/assembled-engine-profile';
|
|
@@ -86,6 +77,7 @@ import {
|
|
|
86
77
|
type RhiCapture,
|
|
87
78
|
} from './internal/rhi-capture';
|
|
88
79
|
import { resolveRhiDebugFlag } from './internal/rhi-debug-flag';
|
|
80
|
+
import { createRenderFeatureHost, type RenderFeatureHost } from './renderer-plugin';
|
|
89
81
|
import type {
|
|
90
82
|
App,
|
|
91
83
|
AppAssembleArgs,
|
|
@@ -93,35 +85,10 @@ import type {
|
|
|
93
85
|
AssembleAppError,
|
|
94
86
|
BundlerOptions,
|
|
95
87
|
CanvasAppError,
|
|
96
|
-
CanvasDrawingBufferSize,
|
|
97
88
|
CreateAppOptions,
|
|
98
89
|
ExecutionApp,
|
|
99
90
|
} from './types';
|
|
100
91
|
|
|
101
|
-
const DEFAULT_ASSET_CATALOG_PATH = 'pack-index.json';
|
|
102
|
-
|
|
103
|
-
function defaultAssetCatalogUrl(): string {
|
|
104
|
-
const base =
|
|
105
|
-
typeof document !== 'undefined'
|
|
106
|
-
? document.baseURI
|
|
107
|
-
: (globalThis as { location?: { href?: string } }).location?.href;
|
|
108
|
-
return base === undefined
|
|
109
|
-
? DEFAULT_ASSET_CATALOG_PATH
|
|
110
|
-
: new URL(DEFAULT_ASSET_CATALOG_PATH, base).href;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function assembleAppAssetRuntime(
|
|
114
|
-
catalog: CatalogSource | undefined,
|
|
115
|
-
contributions: readonly AssetDecoderContributionRef[] | undefined,
|
|
116
|
-
): Result<AssetRuntimeAssembly, AssetRuntimeAssemblyError> {
|
|
117
|
-
const registry = createAssetRegistry({
|
|
118
|
-
catalog: catalog ?? createCatalogSource({ url: defaultAssetCatalogUrl() }),
|
|
119
|
-
});
|
|
120
|
-
const assembled = assembleAssetRuntime(registry, contributions ?? []);
|
|
121
|
-
if (!assembled.ok) registry.dispose();
|
|
122
|
-
return assembled;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
92
|
function makeAppError<C extends AppErrorCode>(
|
|
126
93
|
code: C,
|
|
127
94
|
expected: string,
|
|
@@ -131,23 +98,7 @@ function makeAppError<C extends AppErrorCode>(
|
|
|
131
98
|
return new AppError({ code, expected, hint, detail }) as AppError;
|
|
132
99
|
}
|
|
133
100
|
|
|
134
|
-
function
|
|
135
|
-
const moduleUrl = typeof bootstrap === 'string' ? bootstrap : bootstrap.href;
|
|
136
|
-
try {
|
|
137
|
-
return ok(new URL(bootstrap, globalThis.location?.href).href);
|
|
138
|
-
} catch (cause) {
|
|
139
|
-
return err(
|
|
140
|
-
makeAppError(
|
|
141
|
-
'app-execution-bootstrap-failed',
|
|
142
|
-
APP_EXPECTED['app-execution-bootstrap-failed'],
|
|
143
|
-
APP_ERROR_HINTS['app-execution-bootstrap-failed'],
|
|
144
|
-
{ phase: 'prepare', moduleUrl, cause },
|
|
145
|
-
),
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function canvasAspectPlugin(canvas: HTMLCanvasElement, maxCanvasPixelRatio?: number): Plugin {
|
|
101
|
+
function canvasAspectPlugin(canvas: HTMLCanvasElement): Plugin {
|
|
151
102
|
return {
|
|
152
103
|
name: 'canvas-aspect',
|
|
153
104
|
inject: ['world'],
|
|
@@ -157,7 +108,7 @@ function canvasAspectPlugin(canvas: HTMLCanvasElement, maxCanvasPixelRatio?: num
|
|
|
157
108
|
name: 'app-sync-camera-aspect',
|
|
158
109
|
queries: [],
|
|
159
110
|
fn: () => {
|
|
160
|
-
syncCanvasDrawingBuffer(canvas
|
|
111
|
+
syncCanvasDrawingBuffer(canvas);
|
|
161
112
|
syncCameraAspect(ctx.world, canvas.width, canvas.height);
|
|
162
113
|
},
|
|
163
114
|
};
|
|
@@ -210,7 +161,8 @@ export function createApp(
|
|
|
210
161
|
): Promise<Result<App, CanvasAppError>>;
|
|
211
162
|
|
|
212
163
|
/**
|
|
213
|
-
*
|
|
164
|
+
* Create the browser host for the single RenderScene -> FrameReceipt path.
|
|
165
|
+
* `createApp({ renderer, world, input?, schedule?, ... })` is the assemble form
|
|
214
166
|
* SSOT (per plan-strategy D-5). Host already owns renderer / world; the
|
|
215
167
|
* returned App holds them by reference equality (per AC-02).
|
|
216
168
|
*
|
|
@@ -251,20 +203,6 @@ async function createAppFromCanvas(
|
|
|
251
203
|
// document.createElement('canvas') without appendChild. Returning
|
|
252
204
|
// Result.err here short-circuits before createRenderer fires off any
|
|
253
205
|
// async adapter / device / shader work that would only fail later.
|
|
254
|
-
const maxCanvasPixelRatio = opts?.maxCanvasPixelRatio;
|
|
255
|
-
if (
|
|
256
|
-
maxCanvasPixelRatio !== undefined &&
|
|
257
|
-
(!Number.isFinite(maxCanvasPixelRatio) || maxCanvasPixelRatio <= 0)
|
|
258
|
-
) {
|
|
259
|
-
return err(
|
|
260
|
-
makeAppError(
|
|
261
|
-
'app-invalid-canvas-pixel-ratio',
|
|
262
|
-
APP_EXPECTED['app-invalid-canvas-pixel-ratio'],
|
|
263
|
-
APP_ERROR_HINTS['app-invalid-canvas-pixel-ratio'],
|
|
264
|
-
{ value: maxCanvasPixelRatio },
|
|
265
|
-
),
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
206
|
if (!canvas.isConnected) {
|
|
269
207
|
return err(
|
|
270
208
|
makeAppError(
|
|
@@ -276,31 +214,23 @@ async function createAppFromCanvas(
|
|
|
276
214
|
);
|
|
277
215
|
}
|
|
278
216
|
|
|
279
|
-
const executionOptions = opts?.execution;
|
|
280
|
-
const normalizedExecutionBootstrap =
|
|
281
|
-
executionOptions === undefined
|
|
282
|
-
? undefined
|
|
283
|
-
: normalizeExecutionBootstrapUrl(executionOptions.bootstrap);
|
|
284
|
-
if (normalizedExecutionBootstrap?.ok === false) {
|
|
285
|
-
return err(normalizedExecutionBootstrap.error);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
217
|
// The DOM canvas starts with a 300x150 drawing buffer even when CSS lays it
|
|
289
218
|
// out at a different size. Set the physical buffer before the renderer
|
|
290
219
|
// configures its swap chain; the same helper runs before each frame so a CSS
|
|
291
220
|
// resize remains visible to both rendering and camera policy.
|
|
292
|
-
syncCanvasDrawingBuffer(canvas
|
|
221
|
+
syncCanvasDrawingBuffer(canvas);
|
|
293
222
|
|
|
294
|
-
|
|
223
|
+
let executionBootstrapUrl: string | undefined;
|
|
224
|
+
if (opts?.execution !== undefined) {
|
|
225
|
+
const normalizedBootstrap = normalizeExecutionBootstrapUrl(opts.execution.bootstrap);
|
|
226
|
+
if (!normalizedBootstrap.ok) return err(normalizedBootstrap.error);
|
|
227
|
+
executionBootstrapUrl = normalizedBootstrap.value;
|
|
295
228
|
const realmBoundOption = [
|
|
296
229
|
['features', opts.features],
|
|
297
230
|
['plugins', opts.plugins],
|
|
298
231
|
['rhi', opts.rhi],
|
|
299
|
-
['
|
|
232
|
+
['rhiInstrumentation', opts.rhiInstrumentation],
|
|
300
233
|
['drawSource', opts.drawSource],
|
|
301
|
-
['membershipTiming', opts.membershipTiming],
|
|
302
|
-
['assetCatalog', opts.assetCatalog],
|
|
303
|
-
['assetDecoders', opts.assetDecoders],
|
|
304
234
|
['bundler.importTransport', bundler?.importTransport],
|
|
305
235
|
].find(([, value]) => value !== undefined)?.[0];
|
|
306
236
|
if (realmBoundOption !== undefined) {
|
|
@@ -311,7 +241,7 @@ async function createAppFromCanvas(
|
|
|
311
241
|
APP_ERROR_HINTS['app-execution-bootstrap-failed'],
|
|
312
242
|
{
|
|
313
243
|
phase: 'prepare',
|
|
314
|
-
moduleUrl:
|
|
244
|
+
moduleUrl: executionBootstrapUrl,
|
|
315
245
|
cause: new TypeError(
|
|
316
246
|
`${realmBoundOption} must be constructed by the execution bootstrap module`,
|
|
317
247
|
),
|
|
@@ -328,11 +258,10 @@ async function createAppFromCanvas(
|
|
|
328
258
|
}
|
|
329
259
|
| undefined;
|
|
330
260
|
let preparedExecutionBootstrap: PreparedExecutionBootstrap | undefined;
|
|
331
|
-
if (opts?.execution !== undefined
|
|
332
|
-
const executionOptions = opts.execution;
|
|
261
|
+
if (opts?.execution !== undefined) {
|
|
333
262
|
const capabilities = await probeExecutionCapabilities(canvas);
|
|
334
263
|
const selected = selectExecutionTier({
|
|
335
|
-
requestedTier:
|
|
264
|
+
requestedTier: opts.execution.tier ?? 'auto',
|
|
336
265
|
capabilities,
|
|
337
266
|
sharedEvidencePassed: true,
|
|
338
267
|
});
|
|
@@ -342,15 +271,18 @@ async function createAppFromCanvas(
|
|
|
342
271
|
return createWorkerExecutionApp({
|
|
343
272
|
canvas,
|
|
344
273
|
appOptions: opts,
|
|
345
|
-
syncCanvas: () => measureCanvasDrawingBuffer(canvas
|
|
274
|
+
syncCanvas: () => measureCanvasDrawingBuffer(canvas),
|
|
346
275
|
...(bundler !== undefined ? { bundler } : {}),
|
|
347
276
|
capabilities,
|
|
348
277
|
selection: selected.value,
|
|
349
278
|
});
|
|
350
279
|
}
|
|
280
|
+
if (executionBootstrapUrl === undefined) {
|
|
281
|
+
throw new Error('execution bootstrap URL was not normalized');
|
|
282
|
+
}
|
|
351
283
|
const prepared = await prepareBootstrapEntry(
|
|
352
|
-
|
|
353
|
-
|
|
284
|
+
executionBootstrapUrl,
|
|
285
|
+
opts.execution.bootstrapData,
|
|
354
286
|
);
|
|
355
287
|
if (!prepared.ok) return err(prepared.error);
|
|
356
288
|
preparedExecutionBootstrap = prepared.value;
|
|
@@ -367,40 +299,34 @@ async function createAppFromCanvas(
|
|
|
367
299
|
// forwarded to keep the contract honest (AI users walk the union
|
|
368
300
|
// discriminant rather than parse error.message strings).
|
|
369
301
|
// feat-20260608 / M2 / D-3: CreateAppOptions stops `extends RendererOptions`,
|
|
370
|
-
// so the
|
|
302
|
+
// so the RHI escape hatch (rhi) is
|
|
371
303
|
// forwarded explicitly. Build a RendererOptions object out of just those
|
|
372
304
|
// fields when present; an empty {} keeps createRenderer on its default path.
|
|
373
305
|
const rendererOpts: import('@forgeax/engine-render').RendererOptions = {};
|
|
374
306
|
if (opts?.rhi !== undefined) {
|
|
375
307
|
Object.assign(rendererOpts, { rhi: opts.rhi });
|
|
376
308
|
}
|
|
377
|
-
if (opts?.rhiInstrumentation !== undefined) {
|
|
378
|
-
Object.assign(rendererOpts, { rhiInstrumentation: opts.rhiInstrumentation });
|
|
379
|
-
}
|
|
380
|
-
if (opts?.rawDeviceForContextConfigure !== undefined) {
|
|
381
|
-
Object.assign(rendererOpts, {
|
|
382
|
-
rawDeviceForContextConfigure: opts.rawDeviceForContextConfigure,
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
309
|
if (opts?.profiler !== undefined) {
|
|
386
310
|
Object.assign(rendererOpts, { profiler: opts.profiler });
|
|
387
311
|
}
|
|
388
|
-
if (opts?.membershipTiming !== undefined) {
|
|
389
|
-
Object.assign(rendererOpts, { membershipTiming: opts.membershipTiming });
|
|
390
|
-
}
|
|
391
312
|
const rendererFeatures = preparedExecutionBootstrap?.features ?? opts?.features;
|
|
392
313
|
if (rendererFeatures !== undefined) {
|
|
393
314
|
Object.assign(rendererOpts, { features: rendererFeatures });
|
|
394
315
|
}
|
|
316
|
+
if (opts?.standardProfile !== undefined) {
|
|
317
|
+
Object.assign(rendererOpts, { standardProfile: opts.standardProfile });
|
|
318
|
+
}
|
|
319
|
+
if (opts?.rhiInstrumentation !== undefined) {
|
|
320
|
+
Object.assign(rendererOpts, { rhiInstrumentation: opts.rhiInstrumentation });
|
|
321
|
+
}
|
|
395
322
|
|
|
396
|
-
//
|
|
397
|
-
//
|
|
323
|
+
// FORGEAX_ENGINE_RHI_DEBUG=1 attaches the recorder at the Runtime backend
|
|
324
|
+
// seam. Render remains the owner of device/surface lifecycle; App only owns
|
|
325
|
+
// the optional capture capability and its frame transaction.
|
|
398
326
|
let rhiAttachment: RecorderAttachment | undefined;
|
|
399
327
|
let rhiCapture: RhiCapture | undefined;
|
|
400
328
|
let rhiDebugGlobal:
|
|
401
|
-
| {
|
|
402
|
-
captureFrame(options?: CaptureFrameOptions): ReturnType<RhiCapture['captureFrame']>;
|
|
403
|
-
}
|
|
329
|
+
| { captureFrame(options?: CaptureFrameOptions): ReturnType<RhiCapture['captureFrame']> }
|
|
404
330
|
| undefined;
|
|
405
331
|
const cleanupRhiDebugHost = (): void => {
|
|
406
332
|
const host = globalThis as { __forgeax?: typeof rhiDebugGlobal };
|
|
@@ -445,65 +371,55 @@ async function createAppFromCanvas(
|
|
|
445
371
|
: undefined;
|
|
446
372
|
const hasWebGPU = nav !== undefined && 'gpu' in nav && nav.gpu !== undefined;
|
|
447
373
|
if (
|
|
448
|
-
opts?.rhi === undefined &&
|
|
449
374
|
(browserBuildRhiDebugFlag === undefined || browserBuildRhiDebugFlag === '1') &&
|
|
450
375
|
rhiDebugFlag === '1'
|
|
451
376
|
) {
|
|
452
|
-
const
|
|
377
|
+
const realBackend = (hasWebGPU
|
|
453
378
|
? await import('@forgeax/engine-rhi-webgpu')
|
|
454
|
-
: await import('@forgeax/engine-rhi-wgpu')
|
|
455
|
-
if (!hasWebGPU && 'ensureReady' in
|
|
456
|
-
|
|
457
|
-
|
|
379
|
+
: await import('@forgeax/engine-rhi-wgpu')) as unknown as Record<string, unknown>;
|
|
380
|
+
if (!hasWebGPU && 'ensureReady' in realBackend) {
|
|
381
|
+
await (realBackend.ensureReady as () => Promise<unknown>)();
|
|
382
|
+
}
|
|
383
|
+
const pack = loadRhiPack(realBackend);
|
|
458
384
|
if (pack.createShaderModule === undefined) {
|
|
459
|
-
throw new Error('RHI
|
|
385
|
+
throw new Error('RHI-debug requires a backend createShaderModule capability');
|
|
460
386
|
}
|
|
461
|
-
const
|
|
462
|
-
const attached = debugModule.attachRecorder({
|
|
387
|
+
const attached = attachRecorder({
|
|
463
388
|
rhi: pack.rhi,
|
|
464
|
-
createShaderModule,
|
|
389
|
+
createShaderModule: pack.createShaderModule,
|
|
465
390
|
});
|
|
466
391
|
if (!attached.ok) throw new Error(attached.error.hint);
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const capture =
|
|
470
|
-
|
|
471
|
-
|
|
392
|
+
rhiAttachment = attached.value;
|
|
393
|
+
rhiCapture = createRhiCapture(attached.value);
|
|
394
|
+
const capture = rhiCapture;
|
|
395
|
+
rhiDebugGlobal = { captureFrame: (options) => capture.captureFrame(options) };
|
|
472
396
|
Object.assign(rendererOpts, {
|
|
473
|
-
rhi:
|
|
474
|
-
rhiInstrumentation: createRhiInstrumentation(
|
|
397
|
+
rhi: attached.value.backend.rhi,
|
|
398
|
+
rhiInstrumentation: createRhiInstrumentation(attached.value),
|
|
475
399
|
});
|
|
476
|
-
|
|
477
|
-
rhiDebugGlobal = {
|
|
478
|
-
captureFrame: (options) => capture.captureFrame(options),
|
|
479
|
-
};
|
|
480
400
|
(globalThis as { __forgeax?: typeof rhiDebugGlobal }).__forgeax = rhiDebugGlobal;
|
|
481
401
|
}
|
|
482
402
|
|
|
483
|
-
const assetAssemblyResult = assembleAppAssetRuntime(opts?.assetCatalog, opts?.assetDecoders);
|
|
484
|
-
if (!assetAssemblyResult.ok) {
|
|
485
|
-
cleanupRhiDebugHost();
|
|
486
|
-
return err(assetAssemblyResult.error);
|
|
487
|
-
}
|
|
488
|
-
const assetAssembly = assetAssemblyResult.value;
|
|
489
403
|
let renderer: Renderer;
|
|
404
|
+
let rendererDebugDrawHost: RendererHostAssembly['debugDrawHost'];
|
|
405
|
+
let rendererFeatureHost: RenderFeatureHost;
|
|
406
|
+
let assets: AssetRegistry;
|
|
490
407
|
try {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
408
|
+
const constructed = await constructRuntimeRendererHost(canvas, rendererOpts, bundler);
|
|
409
|
+
if (!constructed.ok) throw constructed.error;
|
|
410
|
+
renderer = constructed.value.renderer;
|
|
411
|
+
rendererDebugDrawHost = constructed.value.debugDrawHost;
|
|
412
|
+
rendererFeatureHost = createRenderFeatureHost(constructed.value.featureHost);
|
|
413
|
+
assets = constructed.value.assets;
|
|
495
414
|
} catch (e: unknown) {
|
|
496
415
|
cleanupRhiDebugHost();
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
// EngineEnvironmentError as the only construction-time failure
|
|
505
|
-
// shape; anything else is an engine bug, not an app-shell concern.
|
|
506
|
-
throw e;
|
|
416
|
+
if (e instanceof EngineEnvironmentError) return err(e);
|
|
417
|
+
const detail = e instanceof Error ? e : new Error(String(e));
|
|
418
|
+
return err(
|
|
419
|
+
new EngineEnvironmentError('renderer construction failed', {
|
|
420
|
+
webgpuError: detail,
|
|
421
|
+
}),
|
|
422
|
+
);
|
|
507
423
|
}
|
|
508
424
|
|
|
509
425
|
// Step 2.4 decision: resolve whether the remote eval server should start
|
|
@@ -516,7 +432,6 @@ async function createAppFromCanvas(
|
|
|
516
432
|
? (import.meta as { env?: { DEV?: boolean } }).env?.DEV
|
|
517
433
|
: undefined,
|
|
518
434
|
(globalThis as { process?: { env?: { FORGEAX_ENGINE_REMOTE_SERVE?: string } } }).process?.env,
|
|
519
|
-
typeof window !== 'undefined',
|
|
520
435
|
);
|
|
521
436
|
|
|
522
437
|
let debugDraw: DebugDraw | undefined;
|
|
@@ -564,8 +479,10 @@ async function createAppFromCanvas(
|
|
|
564
479
|
world,
|
|
565
480
|
mainEngineProfile({
|
|
566
481
|
renderer,
|
|
567
|
-
|
|
568
|
-
|
|
482
|
+
rendererDebugDrawHost,
|
|
483
|
+
rendererFeatureHost,
|
|
484
|
+
assets,
|
|
485
|
+
animationPayloads: createAnimationPayloadLookup(assets),
|
|
569
486
|
...(inputBackend === undefined ? {} : { input: inputBackend }),
|
|
570
487
|
...(inputHandle === undefined ? {} : { inputDispose: inputHandle.cleanup }),
|
|
571
488
|
...(opts?.inputMap === undefined ? {} : { inputMap: opts.inputMap }),
|
|
@@ -575,14 +492,12 @@ async function createAppFromCanvas(
|
|
|
575
492
|
extensions: [
|
|
576
493
|
rhiDebugHostPlugin(cleanupRhiDebugHost),
|
|
577
494
|
...userPlugins,
|
|
578
|
-
canvasAspectPlugin(canvas
|
|
495
|
+
canvasAspectPlugin(canvas),
|
|
579
496
|
],
|
|
580
497
|
}),
|
|
581
498
|
);
|
|
582
499
|
} catch (cause) {
|
|
583
500
|
cleanupRhiDebugHost();
|
|
584
|
-
assetAssembly.dispose();
|
|
585
|
-
assetAssembly.registry.dispose();
|
|
586
501
|
return err(
|
|
587
502
|
makeAppError(
|
|
588
503
|
'app-plugin-activation-failed',
|
|
@@ -639,20 +554,13 @@ async function createAppFromCanvas(
|
|
|
639
554
|
// of static dep on @forgeax/engine-remote. Component reflection crosses
|
|
640
555
|
// this boundary as JSON-safe host data; app does not own any component.
|
|
641
556
|
const remoteHandle = shouldStartRemote
|
|
642
|
-
? await startRemoteServer(
|
|
643
|
-
world,
|
|
644
|
-
renderer,
|
|
645
|
-
assetAssembly.registry,
|
|
646
|
-
rhiCapture,
|
|
647
|
-
opts?.profiler,
|
|
648
|
-
executionControl,
|
|
649
|
-
)
|
|
557
|
+
? await startRemoteServer(world, renderer, assets, rhiCapture, opts?.profiler, executionControl)
|
|
650
558
|
: undefined;
|
|
651
559
|
if (remoteHandle !== undefined) await pluginContext.plugin(remoteServerPlugin(remoteHandle));
|
|
652
560
|
|
|
653
561
|
const buildArgs: BuildAppArgs = {
|
|
654
562
|
renderer,
|
|
655
|
-
assets
|
|
563
|
+
assets,
|
|
656
564
|
world,
|
|
657
565
|
pluginContext,
|
|
658
566
|
executionControl,
|
|
@@ -681,10 +589,7 @@ async function createAppFromCanvas(
|
|
|
681
589
|
Object.assign(buildArgs, { profiler: opts.profiler });
|
|
682
590
|
}
|
|
683
591
|
if (rhiCapture !== undefined) {
|
|
684
|
-
Object.assign(buildArgs, {
|
|
685
|
-
rhiCapture,
|
|
686
|
-
...(rhiAttachment === undefined ? {} : { rhiAttachment }),
|
|
687
|
-
});
|
|
592
|
+
Object.assign(buildArgs, { rhiCapture });
|
|
688
593
|
}
|
|
689
594
|
if (debugDraw !== undefined) {
|
|
690
595
|
Object.assign(buildArgs, { debugDraw });
|
|
@@ -727,17 +632,17 @@ async function createAppFromCanvas(
|
|
|
727
632
|
?.VITE_FORGEAX_ENGINE_BRIDGE_PORT ?? '5733';
|
|
728
633
|
await pluginContext.plugin({
|
|
729
634
|
name: 'browser-remote-bridge',
|
|
730
|
-
inject: ['world', 'renderer', '
|
|
635
|
+
inject: ['world', 'renderer', 'assets'],
|
|
731
636
|
async apply(ctx) {
|
|
732
|
-
if (ctx.renderer === undefined || ctx.
|
|
733
|
-
throw new Error('browser remote bridge requires renderer and
|
|
637
|
+
if (ctx.renderer === undefined || ctx.assets === undefined) {
|
|
638
|
+
throw new Error('browser remote bridge requires renderer and assets services');
|
|
734
639
|
}
|
|
735
640
|
try {
|
|
736
641
|
const bridge = await import('./internal/browser-remote-bridge');
|
|
737
642
|
const teardown = await bridge.installBrowserRemoteBridge({
|
|
738
643
|
world: ctx.world,
|
|
739
644
|
renderer: ctx.renderer,
|
|
740
|
-
assets: ctx.
|
|
645
|
+
assets: ctx.assets,
|
|
741
646
|
runtimeModule: engineRuntimeModule,
|
|
742
647
|
...(rhiCapture !== undefined ? { rhiCapture } : {}),
|
|
743
648
|
...(opts?.profiler !== undefined ? { profiler: opts.profiler } : {}),
|
|
@@ -764,47 +669,14 @@ export function syncCanvasDrawingBuffer(
|
|
|
764
669
|
canvas: Pick<HTMLCanvasElement, 'clientWidth' | 'clientHeight' | 'width' | 'height'> & {
|
|
765
670
|
readonly style?: Pick<CSSStyleDeclaration, 'width' | 'height'>;
|
|
766
671
|
},
|
|
767
|
-
|
|
768
|
-
): CanvasDrawingBufferSize {
|
|
769
|
-
const size = resolveCanvasDrawingBufferSize(canvas, maxCanvasPixelRatio);
|
|
770
|
-
if (size === undefined) return { width: canvas.width, height: canvas.height };
|
|
771
|
-
if (canvas.width !== size.width) canvas.width = size.width;
|
|
772
|
-
if (canvas.height !== size.height) canvas.height = size.height;
|
|
773
|
-
return size;
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
/**
|
|
777
|
-
* Measure the next effective drawing buffer without writing the DOM canvas.
|
|
778
|
-
* This is the Worker host path because transferControlToOffscreen makes the
|
|
779
|
-
* HTML canvas dimensions immutable; the Worker owns the transferred surface.
|
|
780
|
-
*/
|
|
781
|
-
export function measureCanvasDrawingBuffer(
|
|
782
|
-
canvas: Pick<HTMLCanvasElement, 'clientWidth' | 'clientHeight' | 'width' | 'height'> & {
|
|
783
|
-
readonly style?: Pick<CSSStyleDeclaration, 'width' | 'height'>;
|
|
784
|
-
},
|
|
785
|
-
maxCanvasPixelRatio?: number,
|
|
786
|
-
): CanvasDrawingBufferSize {
|
|
787
|
-
return (
|
|
788
|
-
resolveCanvasDrawingBufferSize(canvas, maxCanvasPixelRatio) ?? {
|
|
789
|
-
width: canvas.width,
|
|
790
|
-
height: canvas.height,
|
|
791
|
-
}
|
|
792
|
-
);
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
function resolveCanvasDrawingBufferSize(
|
|
796
|
-
canvas: Pick<HTMLCanvasElement, 'clientWidth' | 'clientHeight' | 'width' | 'height'> & {
|
|
797
|
-
readonly style?: Pick<CSSStyleDeclaration, 'width' | 'height'>;
|
|
798
|
-
},
|
|
799
|
-
maxCanvasPixelRatio?: number,
|
|
800
|
-
): CanvasDrawingBufferSize | undefined {
|
|
672
|
+
): void {
|
|
801
673
|
if (
|
|
802
674
|
!Number.isFinite(canvas.clientWidth) ||
|
|
803
675
|
!Number.isFinite(canvas.clientHeight) ||
|
|
804
676
|
canvas.clientWidth <= 0 ||
|
|
805
677
|
canvas.clientHeight <= 0
|
|
806
678
|
)
|
|
807
|
-
return
|
|
679
|
+
return;
|
|
808
680
|
// An intrinsic canvas with no CSS dimensions already has an explicit
|
|
809
681
|
// drawing-buffer contract. `clientWidth` mirrors `width` for this shape;
|
|
810
682
|
// multiplying it by DPR would change the caller's requested pixel size and
|
|
@@ -818,15 +690,9 @@ function resolveCanvasDrawingBufferSize(
|
|
|
818
690
|
canvas.clientWidth === canvas.width &&
|
|
819
691
|
canvas.clientHeight === canvas.height
|
|
820
692
|
) {
|
|
821
|
-
return
|
|
693
|
+
return;
|
|
822
694
|
}
|
|
823
|
-
const
|
|
824
|
-
const dpr =
|
|
825
|
-
maxCanvasPixelRatio === undefined ||
|
|
826
|
-
!Number.isFinite(maxCanvasPixelRatio) ||
|
|
827
|
-
maxCanvasPixelRatio <= 0
|
|
828
|
-
? devicePixelRatio
|
|
829
|
-
: Math.min(devicePixelRatio, maxCanvasPixelRatio);
|
|
695
|
+
const dpr = Math.max(1, globalThis.devicePixelRatio || 1);
|
|
830
696
|
const previous = syncedCanvasSizes.get(canvas);
|
|
831
697
|
const drawingBufferIsTheLayoutMeasurement =
|
|
832
698
|
previous !== undefined &&
|
|
@@ -838,13 +704,60 @@ function resolveCanvasDrawingBufferSize(
|
|
|
838
704
|
const cssHeight = drawingBufferIsTheLayoutMeasurement ? previous.cssHeight : canvas.clientHeight;
|
|
839
705
|
const width = Math.max(1, Math.round(cssWidth * dpr));
|
|
840
706
|
const height = Math.max(1, Math.round(cssHeight * dpr));
|
|
707
|
+
if (canvas.width !== width) canvas.width = width;
|
|
708
|
+
if (canvas.height !== height) canvas.height = height;
|
|
841
709
|
syncedCanvasSizes.set(canvas, {
|
|
842
710
|
cssWidth,
|
|
843
711
|
cssHeight,
|
|
844
712
|
drawingWidth: width,
|
|
845
713
|
drawingHeight: height,
|
|
846
714
|
});
|
|
847
|
-
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/** Measure a CSS-sized canvas without mutating its drawing buffer. */
|
|
718
|
+
export function measureCanvasDrawingBuffer(
|
|
719
|
+
canvas: Pick<HTMLCanvasElement, 'clientWidth' | 'clientHeight' | 'width' | 'height'> & {
|
|
720
|
+
readonly style?: Pick<CSSStyleDeclaration, 'width' | 'height'>;
|
|
721
|
+
},
|
|
722
|
+
maxCanvasPixelRatio?: number,
|
|
723
|
+
): { readonly width: number; readonly height: number } {
|
|
724
|
+
if (
|
|
725
|
+
!Number.isFinite(canvas.clientWidth) ||
|
|
726
|
+
!Number.isFinite(canvas.clientHeight) ||
|
|
727
|
+
canvas.clientWidth <= 0 ||
|
|
728
|
+
canvas.clientHeight <= 0
|
|
729
|
+
) {
|
|
730
|
+
return { width: canvas.width, height: canvas.height };
|
|
731
|
+
}
|
|
732
|
+
const hasExplicitCssSize =
|
|
733
|
+
(canvas.style?.width ?? '') !== '' || (canvas.style?.height ?? '') !== '';
|
|
734
|
+
if (
|
|
735
|
+
!hasExplicitCssSize &&
|
|
736
|
+
canvas.clientWidth === canvas.width &&
|
|
737
|
+
canvas.clientHeight === canvas.height
|
|
738
|
+
) {
|
|
739
|
+
return { width: canvas.width, height: canvas.height };
|
|
740
|
+
}
|
|
741
|
+
const devicePixelRatio = Math.max(1, globalThis.devicePixelRatio || 1);
|
|
742
|
+
const dpr =
|
|
743
|
+
maxCanvasPixelRatio === undefined ||
|
|
744
|
+
!Number.isFinite(maxCanvasPixelRatio) ||
|
|
745
|
+
maxCanvasPixelRatio <= 0
|
|
746
|
+
? devicePixelRatio
|
|
747
|
+
: Math.min(devicePixelRatio, maxCanvasPixelRatio);
|
|
748
|
+
const previous = syncedCanvasSizes.get(canvas);
|
|
749
|
+
const drawingBufferIsTheLayoutMeasurement =
|
|
750
|
+
previous !== undefined &&
|
|
751
|
+
canvas.width === previous.drawingWidth &&
|
|
752
|
+
canvas.height === previous.drawingHeight &&
|
|
753
|
+
canvas.clientWidth === previous.drawingWidth &&
|
|
754
|
+
canvas.clientHeight === previous.drawingHeight;
|
|
755
|
+
const cssWidth = drawingBufferIsTheLayoutMeasurement ? previous.cssWidth : canvas.clientWidth;
|
|
756
|
+
const cssHeight = drawingBufferIsTheLayoutMeasurement ? previous.cssHeight : canvas.clientHeight;
|
|
757
|
+
return {
|
|
758
|
+
width: Math.max(1, Math.round(cssWidth * dpr)),
|
|
759
|
+
height: Math.max(1, Math.round(cssHeight * dpr)),
|
|
760
|
+
};
|
|
848
761
|
}
|
|
849
762
|
|
|
850
763
|
const syncedCanvasSizes = new WeakMap<
|
|
@@ -857,10 +770,49 @@ const syncedCanvasSizes = new WeakMap<
|
|
|
857
770
|
}
|
|
858
771
|
>();
|
|
859
772
|
|
|
773
|
+
/**
|
|
774
|
+
* Per-frame aspect-sync body for the createApp(canvas) path (feat-20260617
|
|
775
|
+
* M3 / w13). Walks every Camera entity and, for perspective cameras with
|
|
776
|
+
* `autoAspect === true`, writes `canvasW / canvasH` into `Camera.aspect`.
|
|
777
|
+
*
|
|
778
|
+
* Read discipline (D-5 / research Finding 2): `autoAspect` is read through
|
|
779
|
+
* `world.get` (the readRow path narrows the bool column to a JS boolean).
|
|
780
|
+
* The query bundle is used only to enumerate the entity handles -- reading
|
|
781
|
+
* the bool column off the bundle would return a raw 0/1 number, so a
|
|
782
|
+
* `!== 0` test is always true (the
|
|
783
|
+
* bool-field-compared-with-not-equal-zero-always-true trap).
|
|
784
|
+
*
|
|
785
|
+
* Best-effort + side-effect-isolated:
|
|
786
|
+
* - canvas size 0 (detached / display:none) -> skip entirely so `aspect`
|
|
787
|
+
* never becomes NaN / 0.
|
|
788
|
+
* - orthographic cameras and `autoAspect === false` cameras are left
|
|
789
|
+
* untouched.
|
|
790
|
+
*
|
|
791
|
+
* @see {@link https://github.com/Ubpa/forgeax-engine/blob/main/docs/how-to/2026-06-18-host-engine-contract.md | Host-engine contract SSOT}
|
|
792
|
+
*/
|
|
793
|
+
export function syncCameraAspect(world: World, canvasW: number, canvasH: number): void {
|
|
794
|
+
// Guard against detached / zero-sized canvases: a 0 width or height would
|
|
795
|
+
// write NaN (0 / 0) or 0 into aspect and corrupt the projection matrix.
|
|
796
|
+
if (canvasW <= 0 || canvasH <= 0) return;
|
|
797
|
+
const aspect = canvasW / canvasH;
|
|
798
|
+
|
|
799
|
+
const query = world.query({ with: [Camera] }).unwrap();
|
|
800
|
+
for (const row of query) {
|
|
801
|
+
const entity = row.entity;
|
|
802
|
+
const r = world.get(entity, Camera);
|
|
803
|
+
if (!r.ok) continue;
|
|
804
|
+
// world.get narrows the bool column to a real boolean (D-5); the
|
|
805
|
+
// perspective discriminator is the numeric column value.
|
|
806
|
+
if (r.value.autoAspect !== true) continue;
|
|
807
|
+
if (r.value.projection !== CAMERA_PROJECTION_PERSPECTIVE) continue;
|
|
808
|
+
world.set(entity, Camera, { aspect });
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
860
812
|
async function startRemoteServer(
|
|
861
813
|
world: World,
|
|
862
814
|
renderer: Renderer,
|
|
863
|
-
assets: AssetRegistry,
|
|
815
|
+
assets: AssetRegistry | undefined,
|
|
864
816
|
rhiCapture: RhiCapture | undefined,
|
|
865
817
|
profiler: import('@forgeax/engine-profiler').Profiler | undefined,
|
|
866
818
|
execution: ExecutionControl,
|
|
@@ -907,22 +859,17 @@ async function startRemoteServer(
|
|
|
907
859
|
async function createAppFromAssemble(
|
|
908
860
|
args: AppAssembleArgs,
|
|
909
861
|
): Promise<Result<App, AssembleAppError>> {
|
|
910
|
-
const assetAssemblyResult = assembleAppAssetRuntime(args.assetCatalog, args.assetDecoders);
|
|
911
|
-
if (!assetAssemblyResult.ok) return err(assetAssemblyResult.error);
|
|
912
|
-
const assetAssembly = assetAssemblyResult.value;
|
|
913
862
|
let pluginContext: Context;
|
|
914
863
|
try {
|
|
915
864
|
pluginContext = await createWorldContext(
|
|
916
865
|
args.world,
|
|
917
866
|
assembledEngineProfile({
|
|
918
867
|
renderer: args.renderer,
|
|
919
|
-
|
|
868
|
+
...(args.assets === undefined ? {} : { assets: args.assets }),
|
|
920
869
|
extensions: args.plugins ?? [],
|
|
921
870
|
}),
|
|
922
871
|
);
|
|
923
872
|
} catch (cause) {
|
|
924
|
-
assetAssembly.dispose();
|
|
925
|
-
assetAssembly.registry.dispose();
|
|
926
873
|
return err(
|
|
927
874
|
makeAppError(
|
|
928
875
|
'app-plugin-activation-failed',
|
|
@@ -968,7 +915,7 @@ async function createAppFromAssemble(
|
|
|
968
915
|
? await startRemoteServer(
|
|
969
916
|
args.world,
|
|
970
917
|
args.renderer,
|
|
971
|
-
|
|
918
|
+
args.assets,
|
|
972
919
|
undefined,
|
|
973
920
|
args.profiler,
|
|
974
921
|
executionControl,
|
|
@@ -978,7 +925,6 @@ async function createAppFromAssemble(
|
|
|
978
925
|
|
|
979
926
|
const buildArgs: BuildAppArgs = {
|
|
980
927
|
renderer: args.renderer,
|
|
981
|
-
assets: assetAssembly.registry,
|
|
982
928
|
world: args.world,
|
|
983
929
|
pluginContext,
|
|
984
930
|
executionControl,
|
|
@@ -1013,7 +959,7 @@ async function createAppFromAssemble(
|
|
|
1013
959
|
|
|
1014
960
|
interface BuildAppArgs {
|
|
1015
961
|
readonly renderer: Renderer;
|
|
1016
|
-
readonly assets
|
|
962
|
+
readonly assets?: AssetRegistry;
|
|
1017
963
|
readonly world: World;
|
|
1018
964
|
readonly pluginContext: Context;
|
|
1019
965
|
readonly inputBackend?: InputBackend;
|
|
@@ -1027,10 +973,8 @@ interface BuildAppArgs {
|
|
|
1027
973
|
readonly wireOnLockErrorDispatch?: (dispatch: (err: AppError) => void) => void;
|
|
1028
974
|
readonly audioBackend?: AudioBackend;
|
|
1029
975
|
readonly silenceUnhandledErrors?: boolean;
|
|
1030
|
-
/**
|
|
976
|
+
/** Optional App-owned RHI capture capability. */
|
|
1031
977
|
readonly rhiCapture?: RhiCapture;
|
|
1032
|
-
/** Internal attachment used to bind capture to this App's frame authority. */
|
|
1033
|
-
readonly rhiAttachment?: RecorderAttachment;
|
|
1034
978
|
/** feat-20260615 debug-draw M5: DebugDraw instance created by createDebugDrawOnReady. */
|
|
1035
979
|
readonly debugDraw?: DebugDraw;
|
|
1036
980
|
/** feat-20260629 M4 / w20: remote eval server handle from createAppFromCanvas. */
|
|
@@ -1060,14 +1004,12 @@ interface BuildAppArgs {
|
|
|
1060
1004
|
async function buildApp(args: BuildAppArgs): Promise<Result<App, AppError | RhiError>> {
|
|
1061
1005
|
const {
|
|
1062
1006
|
renderer,
|
|
1063
|
-
assets,
|
|
1064
1007
|
world,
|
|
1065
1008
|
pluginContext,
|
|
1066
1009
|
inputBackend,
|
|
1067
1010
|
audioBackend,
|
|
1068
1011
|
silenceUnhandledErrors,
|
|
1069
1012
|
rhiCapture,
|
|
1070
|
-
rhiAttachment,
|
|
1071
1013
|
debugDraw,
|
|
1072
1014
|
remoteHandle,
|
|
1073
1015
|
profiler,
|
|
@@ -1088,6 +1030,7 @@ async function buildApp(args: BuildAppArgs): Promise<Result<App, AppError | RhiE
|
|
|
1088
1030
|
const fanout = new ErrorFanoutRegistry(
|
|
1089
1031
|
silenceUnhandledErrors !== undefined ? { silenceUnhandledErrors } : {},
|
|
1090
1032
|
);
|
|
1033
|
+
let lastError: AppDispatchError | undefined;
|
|
1091
1034
|
const execution =
|
|
1092
1035
|
args.executionControl ??
|
|
1093
1036
|
createLocalExecutionControl(
|
|
@@ -1116,6 +1059,7 @@ async function buildApp(args: BuildAppArgs): Promise<Result<App, AppError | RhiE
|
|
|
1116
1059
|
);
|
|
1117
1060
|
|
|
1118
1061
|
function dispatch(e: AppDispatchError): void {
|
|
1062
|
+
lastError = e;
|
|
1119
1063
|
fanout.fire(e);
|
|
1120
1064
|
}
|
|
1121
1065
|
|
|
@@ -1141,8 +1085,7 @@ async function buildApp(args: BuildAppArgs): Promise<Result<App, AppError | RhiE
|
|
|
1141
1085
|
Object.assign(loopOpts, { profiler });
|
|
1142
1086
|
}
|
|
1143
1087
|
const loop = createFrameLoop(loopOpts);
|
|
1144
|
-
|
|
1145
|
-
if (rhiCapture !== undefined && rhiAttachment !== undefined) {
|
|
1088
|
+
if (rhiCapture !== undefined) {
|
|
1146
1089
|
bindRhiCaptureFrameDriver(rhiCapture, {
|
|
1147
1090
|
getState: () => loop.getState(),
|
|
1148
1091
|
pause: () => {
|
|
@@ -1158,9 +1101,6 @@ async function buildApp(args: BuildAppArgs): Promise<Result<App, AppError | RhiE
|
|
|
1158
1101
|
stepFrame: (deltaSeconds) => loop.stepFrame(deltaSeconds),
|
|
1159
1102
|
});
|
|
1160
1103
|
}
|
|
1161
|
-
|
|
1162
|
-
let lastError: AppDispatchError | undefined;
|
|
1163
|
-
|
|
1164
1104
|
// M4 (w13) device-lost internal subscription. R-1 timing contract:
|
|
1165
1105
|
// app.start() arms the rAF handle BEFORE this listener subscribes, so
|
|
1166
1106
|
// a synchronous late-attach replay of a persisted device-lost event
|
|
@@ -1177,32 +1117,9 @@ async function buildApp(args: BuildAppArgs): Promise<Result<App, AppError | RhiE
|
|
|
1177
1117
|
if (rendererUnsubscribe !== undefined) {
|
|
1178
1118
|
return;
|
|
1179
1119
|
}
|
|
1180
|
-
rendererUnsubscribe = renderer.
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
// error verbatim through the fanout dispatch below.
|
|
1184
|
-
//
|
|
1185
|
-
// feat-20260531-skybox-env-background F-1: the renderer onError channel
|
|
1186
|
-
// now fans out RhiError | RuntimeError (e.g. 'equirect-projection-failed').
|
|
1187
|
-
// Device loss is recoverable at the Renderer boundary: the frame-loop
|
|
1188
|
-
// heartbeat remains armed and freezes World/update work until the host
|
|
1189
|
-
// explicitly calls `renderer.recover()`. Stopping the loop here would
|
|
1190
|
-
// leave a successful recovery
|
|
1191
|
-
// with no frame submitter.
|
|
1192
|
-
//
|
|
1193
|
-
// Note: we discriminate by .code rather than instanceof RhiError
|
|
1194
|
-
// because the listener may be invoked across module boundaries
|
|
1195
|
-
// (re-export from @forgeax/engine-runtime vs direct
|
|
1196
|
-
// @forgeax/engine-rhi/errors import). Bundler dedup is not
|
|
1197
|
-
// guaranteed on subpath exports, so an instanceof check is a
|
|
1198
|
-
// false-negative trap. The union .code type still provides static
|
|
1199
|
-
// safety on .code access.
|
|
1200
|
-
if (e?.code === 'device-lost') {
|
|
1201
|
-
lastError = e;
|
|
1202
|
-
}
|
|
1203
|
-
// Always fan out to host listeners (D-2 last bullet: device-lost
|
|
1204
|
-
// error is forwarded as-is to host onError listener so the host
|
|
1205
|
-
// can decide whether to rebuild the renderer).
|
|
1120
|
+
rendererUnsubscribe = renderer.subscribe((event) => {
|
|
1121
|
+
if (event.kind !== 'error') return;
|
|
1122
|
+
const e: RenderError = event.error;
|
|
1206
1123
|
dispatch(e);
|
|
1207
1124
|
});
|
|
1208
1125
|
}
|
|
@@ -1216,10 +1133,10 @@ async function buildApp(args: BuildAppArgs): Promise<Result<App, AppError | RhiE
|
|
|
1216
1133
|
|
|
1217
1134
|
const stub: App = {
|
|
1218
1135
|
renderer,
|
|
1219
|
-
assets,
|
|
1136
|
+
...(args.assets === undefined ? {} : { assets: args.assets }),
|
|
1220
1137
|
world,
|
|
1221
1138
|
execution,
|
|
1222
|
-
async releaseSurfacePreserveWorld(): Promise<Result<void, RhiError>> {
|
|
1139
|
+
async releaseSurfacePreserveWorld(): Promise<Result<void, RhiError | RenderError>> {
|
|
1223
1140
|
if (loop.getState() === 'running') {
|
|
1224
1141
|
const paused = loop.pause();
|
|
1225
1142
|
if (!paused.ok) {
|
|
@@ -1246,7 +1163,7 @@ async function buildApp(args: BuildAppArgs): Promise<Result<App, AppError | RhiE
|
|
|
1246
1163
|
}
|
|
1247
1164
|
return released.ok ? ok(undefined) : err(released.error);
|
|
1248
1165
|
},
|
|
1249
|
-
async restoreSurface(): Promise<Result<void, RhiError>> {
|
|
1166
|
+
async restoreSurface(): Promise<Result<void, RhiError | RenderError>> {
|
|
1250
1167
|
const restored = renderer.restoreSurface();
|
|
1251
1168
|
if (!restored.ok) return err(restored.error);
|
|
1252
1169
|
if (resumeAfterSurfaceRestore) {
|
|
@@ -1276,7 +1193,7 @@ async function buildApp(args: BuildAppArgs): Promise<Result<App, AppError | RhiE
|
|
|
1276
1193
|
},
|
|
1277
1194
|
start(): Result<void, AppError> {
|
|
1278
1195
|
// R-1: arm the rAF handle FIRST (loop.start schedules raf(tick))
|
|
1279
|
-
// and only THEN subscribe to renderer.
|
|
1196
|
+
// and only THEN subscribe to renderer events. If the renderer
|
|
1280
1197
|
// late-attach replays a persisted device-lost event during the
|
|
1281
1198
|
// subscribe call, the loop is already armed and can retain the error.
|
|
1282
1199
|
const r = loop.start();
|
|
@@ -1321,7 +1238,7 @@ async function buildApp(args: BuildAppArgs): Promise<Result<App, AppError | RhiE
|
|
|
1321
1238
|
loop.setDrawSource(drawSource);
|
|
1322
1239
|
},
|
|
1323
1240
|
/**
|
|
1324
|
-
* Most recent
|
|
1241
|
+
* Most recent dispatched error retained for host self-inspection.
|
|
1325
1242
|
*/
|
|
1326
1243
|
get lastError(): AppDispatchError | undefined {
|
|
1327
1244
|
return lastError;
|
|
@@ -1331,21 +1248,5 @@ async function buildApp(args: BuildAppArgs): Promise<Result<App, AppError | RhiE
|
|
|
1331
1248
|
...(remoteHandle !== undefined ? { remote: remoteHandle } : {}),
|
|
1332
1249
|
};
|
|
1333
1250
|
|
|
1334
|
-
// Readiness barrier (charter Fail Fast). createRenderer resolves before
|
|
1335
|
-
// its `ready` Promise (manifest -> pipeline -> asset upload three-step
|
|
1336
|
-
// chain) settles, so a host that calls app.start() immediately would arm
|
|
1337
|
-
// the rAF loop while renderer.draw(world) still returns 'rhi-not-available'
|
|
1338
|
-
// every frame -- a startup race that surfaces as intermittent console.error
|
|
1339
|
-
// spam on cold loads. Awaiting ready here makes "App ready" mean "renderer
|
|
1340
|
-
// ready": start() never observes a pre-ready frame, and a genuine pipeline
|
|
1341
|
-
// build failure fail-fasts as Result.err(rhiError) (caught by the canonical
|
|
1342
|
-
// `if (!app.ok) reportError(app.error)` takeoff) instead of per-frame noise.
|
|
1343
|
-
// The audio tick system is registered by audioPlugin before buildApp as the 'audio-tick'
|
|
1344
|
-
// world system, so there is no buildApp-side Update system anymore.
|
|
1345
|
-
const readyResult = await renderer.ready;
|
|
1346
|
-
if (!readyResult.ok) {
|
|
1347
|
-
return err(readyResult.error);
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
1251
|
return ok(stub);
|
|
1351
1252
|
}
|