@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.
- 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/browser-frame-signal.d.ts +17 -0
- package/dist/browser-frame-signal.d.ts.map +1 -0
- 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 +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +357 -359
- 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/browser-frame-signal.ts +44 -0
- package/src/create-app.ts +208 -295
- 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 +18 -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,31 @@
|
|
|
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';
|
|
50
|
+
import { publishBrowserFrameSubmitted, resetBrowserFrameSubmitted } from './browser-frame-signal';
|
|
60
51
|
import type { AppErrorCode, AppErrorDetailFor } from './errors';
|
|
61
52
|
import { APP_ERROR_HINTS, APP_EXPECTED, AppError } from './errors';
|
|
62
53
|
import {
|
|
@@ -69,6 +60,7 @@ import {
|
|
|
69
60
|
selectExecutionTier,
|
|
70
61
|
unavailableExecutionCapabilities,
|
|
71
62
|
} from './execution';
|
|
63
|
+
import { normalizeExecutionBootstrapUrl } from './execution/bootstrap-url';
|
|
72
64
|
import { createLocalExecutionControl } from './execution/control';
|
|
73
65
|
import { createWorkerExecutionApp } from './execution/host-controller';
|
|
74
66
|
import { assembledEngineProfile } from './internal/assembled-engine-profile';
|
|
@@ -86,6 +78,7 @@ import {
|
|
|
86
78
|
type RhiCapture,
|
|
87
79
|
} from './internal/rhi-capture';
|
|
88
80
|
import { resolveRhiDebugFlag } from './internal/rhi-debug-flag';
|
|
81
|
+
import { createRenderFeatureHost, type RenderFeatureHost } from './renderer-plugin';
|
|
89
82
|
import type {
|
|
90
83
|
App,
|
|
91
84
|
AppAssembleArgs,
|
|
@@ -93,35 +86,10 @@ import type {
|
|
|
93
86
|
AssembleAppError,
|
|
94
87
|
BundlerOptions,
|
|
95
88
|
CanvasAppError,
|
|
96
|
-
CanvasDrawingBufferSize,
|
|
97
89
|
CreateAppOptions,
|
|
98
90
|
ExecutionApp,
|
|
99
91
|
} from './types';
|
|
100
92
|
|
|
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
93
|
function makeAppError<C extends AppErrorCode>(
|
|
126
94
|
code: C,
|
|
127
95
|
expected: string,
|
|
@@ -131,23 +99,7 @@ function makeAppError<C extends AppErrorCode>(
|
|
|
131
99
|
return new AppError({ code, expected, hint, detail }) as AppError;
|
|
132
100
|
}
|
|
133
101
|
|
|
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 {
|
|
102
|
+
function canvasAspectPlugin(canvas: HTMLCanvasElement): Plugin {
|
|
151
103
|
return {
|
|
152
104
|
name: 'canvas-aspect',
|
|
153
105
|
inject: ['world'],
|
|
@@ -157,7 +109,7 @@ function canvasAspectPlugin(canvas: HTMLCanvasElement, maxCanvasPixelRatio?: num
|
|
|
157
109
|
name: 'app-sync-camera-aspect',
|
|
158
110
|
queries: [],
|
|
159
111
|
fn: () => {
|
|
160
|
-
syncCanvasDrawingBuffer(canvas
|
|
112
|
+
syncCanvasDrawingBuffer(canvas);
|
|
161
113
|
syncCameraAspect(ctx.world, canvas.width, canvas.height);
|
|
162
114
|
},
|
|
163
115
|
};
|
|
@@ -210,7 +162,8 @@ export function createApp(
|
|
|
210
162
|
): Promise<Result<App, CanvasAppError>>;
|
|
211
163
|
|
|
212
164
|
/**
|
|
213
|
-
*
|
|
165
|
+
* Create the browser host for the single RenderScene -> FrameReceipt path.
|
|
166
|
+
* `createApp({ renderer, world, input?, schedule?, ... })` is the assemble form
|
|
214
167
|
* SSOT (per plan-strategy D-5). Host already owns renderer / world; the
|
|
215
168
|
* returned App holds them by reference equality (per AC-02).
|
|
216
169
|
*
|
|
@@ -251,20 +204,6 @@ async function createAppFromCanvas(
|
|
|
251
204
|
// document.createElement('canvas') without appendChild. Returning
|
|
252
205
|
// Result.err here short-circuits before createRenderer fires off any
|
|
253
206
|
// 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
207
|
if (!canvas.isConnected) {
|
|
269
208
|
return err(
|
|
270
209
|
makeAppError(
|
|
@@ -276,31 +215,23 @@ async function createAppFromCanvas(
|
|
|
276
215
|
);
|
|
277
216
|
}
|
|
278
217
|
|
|
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
218
|
// The DOM canvas starts with a 300x150 drawing buffer even when CSS lays it
|
|
289
219
|
// out at a different size. Set the physical buffer before the renderer
|
|
290
220
|
// configures its swap chain; the same helper runs before each frame so a CSS
|
|
291
221
|
// resize remains visible to both rendering and camera policy.
|
|
292
|
-
syncCanvasDrawingBuffer(canvas
|
|
222
|
+
syncCanvasDrawingBuffer(canvas);
|
|
293
223
|
|
|
294
|
-
|
|
224
|
+
let executionBootstrapUrl: string | undefined;
|
|
225
|
+
if (opts?.execution !== undefined) {
|
|
226
|
+
const normalizedBootstrap = normalizeExecutionBootstrapUrl(opts.execution.bootstrap);
|
|
227
|
+
if (!normalizedBootstrap.ok) return err(normalizedBootstrap.error);
|
|
228
|
+
executionBootstrapUrl = normalizedBootstrap.value;
|
|
295
229
|
const realmBoundOption = [
|
|
296
230
|
['features', opts.features],
|
|
297
231
|
['plugins', opts.plugins],
|
|
298
232
|
['rhi', opts.rhi],
|
|
299
|
-
['
|
|
233
|
+
['rhiInstrumentation', opts.rhiInstrumentation],
|
|
300
234
|
['drawSource', opts.drawSource],
|
|
301
|
-
['membershipTiming', opts.membershipTiming],
|
|
302
|
-
['assetCatalog', opts.assetCatalog],
|
|
303
|
-
['assetDecoders', opts.assetDecoders],
|
|
304
235
|
['bundler.importTransport', bundler?.importTransport],
|
|
305
236
|
].find(([, value]) => value !== undefined)?.[0];
|
|
306
237
|
if (realmBoundOption !== undefined) {
|
|
@@ -311,7 +242,7 @@ async function createAppFromCanvas(
|
|
|
311
242
|
APP_ERROR_HINTS['app-execution-bootstrap-failed'],
|
|
312
243
|
{
|
|
313
244
|
phase: 'prepare',
|
|
314
|
-
moduleUrl:
|
|
245
|
+
moduleUrl: executionBootstrapUrl,
|
|
315
246
|
cause: new TypeError(
|
|
316
247
|
`${realmBoundOption} must be constructed by the execution bootstrap module`,
|
|
317
248
|
),
|
|
@@ -328,11 +259,10 @@ async function createAppFromCanvas(
|
|
|
328
259
|
}
|
|
329
260
|
| undefined;
|
|
330
261
|
let preparedExecutionBootstrap: PreparedExecutionBootstrap | undefined;
|
|
331
|
-
if (opts?.execution !== undefined
|
|
332
|
-
const executionOptions = opts.execution;
|
|
262
|
+
if (opts?.execution !== undefined) {
|
|
333
263
|
const capabilities = await probeExecutionCapabilities(canvas);
|
|
334
264
|
const selected = selectExecutionTier({
|
|
335
|
-
requestedTier:
|
|
265
|
+
requestedTier: opts.execution.tier ?? 'auto',
|
|
336
266
|
capabilities,
|
|
337
267
|
sharedEvidencePassed: true,
|
|
338
268
|
});
|
|
@@ -342,15 +272,18 @@ async function createAppFromCanvas(
|
|
|
342
272
|
return createWorkerExecutionApp({
|
|
343
273
|
canvas,
|
|
344
274
|
appOptions: opts,
|
|
345
|
-
syncCanvas: () => measureCanvasDrawingBuffer(canvas
|
|
275
|
+
syncCanvas: () => measureCanvasDrawingBuffer(canvas),
|
|
346
276
|
...(bundler !== undefined ? { bundler } : {}),
|
|
347
277
|
capabilities,
|
|
348
278
|
selection: selected.value,
|
|
349
279
|
});
|
|
350
280
|
}
|
|
281
|
+
if (executionBootstrapUrl === undefined) {
|
|
282
|
+
throw new Error('execution bootstrap URL was not normalized');
|
|
283
|
+
}
|
|
351
284
|
const prepared = await prepareBootstrapEntry(
|
|
352
|
-
|
|
353
|
-
|
|
285
|
+
executionBootstrapUrl,
|
|
286
|
+
opts.execution.bootstrapData,
|
|
354
287
|
);
|
|
355
288
|
if (!prepared.ok) return err(prepared.error);
|
|
356
289
|
preparedExecutionBootstrap = prepared.value;
|
|
@@ -367,40 +300,34 @@ async function createAppFromCanvas(
|
|
|
367
300
|
// forwarded to keep the contract honest (AI users walk the union
|
|
368
301
|
// discriminant rather than parse error.message strings).
|
|
369
302
|
// feat-20260608 / M2 / D-3: CreateAppOptions stops `extends RendererOptions`,
|
|
370
|
-
// so the
|
|
303
|
+
// so the RHI escape hatch (rhi) is
|
|
371
304
|
// forwarded explicitly. Build a RendererOptions object out of just those
|
|
372
305
|
// fields when present; an empty {} keeps createRenderer on its default path.
|
|
373
306
|
const rendererOpts: import('@forgeax/engine-render').RendererOptions = {};
|
|
374
307
|
if (opts?.rhi !== undefined) {
|
|
375
308
|
Object.assign(rendererOpts, { rhi: opts.rhi });
|
|
376
309
|
}
|
|
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
310
|
if (opts?.profiler !== undefined) {
|
|
386
311
|
Object.assign(rendererOpts, { profiler: opts.profiler });
|
|
387
312
|
}
|
|
388
|
-
if (opts?.membershipTiming !== undefined) {
|
|
389
|
-
Object.assign(rendererOpts, { membershipTiming: opts.membershipTiming });
|
|
390
|
-
}
|
|
391
313
|
const rendererFeatures = preparedExecutionBootstrap?.features ?? opts?.features;
|
|
392
314
|
if (rendererFeatures !== undefined) {
|
|
393
315
|
Object.assign(rendererOpts, { features: rendererFeatures });
|
|
394
316
|
}
|
|
317
|
+
if (opts?.standardProfile !== undefined) {
|
|
318
|
+
Object.assign(rendererOpts, { standardProfile: opts.standardProfile });
|
|
319
|
+
}
|
|
320
|
+
if (opts?.rhiInstrumentation !== undefined) {
|
|
321
|
+
Object.assign(rendererOpts, { rhiInstrumentation: opts.rhiInstrumentation });
|
|
322
|
+
}
|
|
395
323
|
|
|
396
|
-
//
|
|
397
|
-
//
|
|
324
|
+
// FORGEAX_ENGINE_RHI_DEBUG=1 attaches the recorder at the Runtime backend
|
|
325
|
+
// seam. Render remains the owner of device/surface lifecycle; App only owns
|
|
326
|
+
// the optional capture capability and its frame transaction.
|
|
398
327
|
let rhiAttachment: RecorderAttachment | undefined;
|
|
399
328
|
let rhiCapture: RhiCapture | undefined;
|
|
400
329
|
let rhiDebugGlobal:
|
|
401
|
-
| {
|
|
402
|
-
captureFrame(options?: CaptureFrameOptions): ReturnType<RhiCapture['captureFrame']>;
|
|
403
|
-
}
|
|
330
|
+
| { captureFrame(options?: CaptureFrameOptions): ReturnType<RhiCapture['captureFrame']> }
|
|
404
331
|
| undefined;
|
|
405
332
|
const cleanupRhiDebugHost = (): void => {
|
|
406
333
|
const host = globalThis as { __forgeax?: typeof rhiDebugGlobal };
|
|
@@ -445,65 +372,55 @@ async function createAppFromCanvas(
|
|
|
445
372
|
: undefined;
|
|
446
373
|
const hasWebGPU = nav !== undefined && 'gpu' in nav && nav.gpu !== undefined;
|
|
447
374
|
if (
|
|
448
|
-
opts?.rhi === undefined &&
|
|
449
375
|
(browserBuildRhiDebugFlag === undefined || browserBuildRhiDebugFlag === '1') &&
|
|
450
376
|
rhiDebugFlag === '1'
|
|
451
377
|
) {
|
|
452
|
-
const
|
|
378
|
+
const realBackend = (hasWebGPU
|
|
453
379
|
? await import('@forgeax/engine-rhi-webgpu')
|
|
454
|
-
: await import('@forgeax/engine-rhi-wgpu')
|
|
455
|
-
if (!hasWebGPU && 'ensureReady' in
|
|
456
|
-
|
|
457
|
-
|
|
380
|
+
: await import('@forgeax/engine-rhi-wgpu')) as unknown as Record<string, unknown>;
|
|
381
|
+
if (!hasWebGPU && 'ensureReady' in realBackend) {
|
|
382
|
+
await (realBackend.ensureReady as () => Promise<unknown>)();
|
|
383
|
+
}
|
|
384
|
+
const pack = loadRhiPack(realBackend);
|
|
458
385
|
if (pack.createShaderModule === undefined) {
|
|
459
|
-
throw new Error('RHI
|
|
386
|
+
throw new Error('RHI-debug requires a backend createShaderModule capability');
|
|
460
387
|
}
|
|
461
|
-
const
|
|
462
|
-
const attached = debugModule.attachRecorder({
|
|
388
|
+
const attached = attachRecorder({
|
|
463
389
|
rhi: pack.rhi,
|
|
464
|
-
createShaderModule,
|
|
390
|
+
createShaderModule: pack.createShaderModule,
|
|
465
391
|
});
|
|
466
392
|
if (!attached.ok) throw new Error(attached.error.hint);
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const capture =
|
|
470
|
-
|
|
471
|
-
|
|
393
|
+
rhiAttachment = attached.value;
|
|
394
|
+
rhiCapture = createRhiCapture(attached.value);
|
|
395
|
+
const capture = rhiCapture;
|
|
396
|
+
rhiDebugGlobal = { captureFrame: (options) => capture.captureFrame(options) };
|
|
472
397
|
Object.assign(rendererOpts, {
|
|
473
|
-
rhi:
|
|
474
|
-
rhiInstrumentation: createRhiInstrumentation(
|
|
398
|
+
rhi: attached.value.backend.rhi,
|
|
399
|
+
rhiInstrumentation: createRhiInstrumentation(attached.value),
|
|
475
400
|
});
|
|
476
|
-
|
|
477
|
-
rhiDebugGlobal = {
|
|
478
|
-
captureFrame: (options) => capture.captureFrame(options),
|
|
479
|
-
};
|
|
480
401
|
(globalThis as { __forgeax?: typeof rhiDebugGlobal }).__forgeax = rhiDebugGlobal;
|
|
481
402
|
}
|
|
482
403
|
|
|
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
404
|
let renderer: Renderer;
|
|
405
|
+
let rendererDebugDrawHost: RendererHostAssembly['debugDrawHost'];
|
|
406
|
+
let rendererFeatureHost: RenderFeatureHost;
|
|
407
|
+
let assets: AssetRegistry;
|
|
490
408
|
try {
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
409
|
+
const constructed = await constructRuntimeRendererHost(canvas, rendererOpts, bundler);
|
|
410
|
+
if (!constructed.ok) throw constructed.error;
|
|
411
|
+
renderer = constructed.value.renderer;
|
|
412
|
+
rendererDebugDrawHost = constructed.value.debugDrawHost;
|
|
413
|
+
rendererFeatureHost = createRenderFeatureHost(constructed.value.featureHost);
|
|
414
|
+
assets = constructed.value.assets;
|
|
495
415
|
} catch (e: unknown) {
|
|
496
416
|
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;
|
|
417
|
+
if (e instanceof EngineEnvironmentError) return err(e);
|
|
418
|
+
const detail = e instanceof Error ? e : new Error(String(e));
|
|
419
|
+
return err(
|
|
420
|
+
new EngineEnvironmentError('renderer construction failed', {
|
|
421
|
+
webgpuError: detail,
|
|
422
|
+
}),
|
|
423
|
+
);
|
|
507
424
|
}
|
|
508
425
|
|
|
509
426
|
// Step 2.4 decision: resolve whether the remote eval server should start
|
|
@@ -516,7 +433,6 @@ async function createAppFromCanvas(
|
|
|
516
433
|
? (import.meta as { env?: { DEV?: boolean } }).env?.DEV
|
|
517
434
|
: undefined,
|
|
518
435
|
(globalThis as { process?: { env?: { FORGEAX_ENGINE_REMOTE_SERVE?: string } } }).process?.env,
|
|
519
|
-
typeof window !== 'undefined',
|
|
520
436
|
);
|
|
521
437
|
|
|
522
438
|
let debugDraw: DebugDraw | undefined;
|
|
@@ -564,8 +480,10 @@ async function createAppFromCanvas(
|
|
|
564
480
|
world,
|
|
565
481
|
mainEngineProfile({
|
|
566
482
|
renderer,
|
|
567
|
-
|
|
568
|
-
|
|
483
|
+
rendererDebugDrawHost,
|
|
484
|
+
rendererFeatureHost,
|
|
485
|
+
assets,
|
|
486
|
+
animationPayloads: createAnimationPayloadLookup(assets),
|
|
569
487
|
...(inputBackend === undefined ? {} : { input: inputBackend }),
|
|
570
488
|
...(inputHandle === undefined ? {} : { inputDispose: inputHandle.cleanup }),
|
|
571
489
|
...(opts?.inputMap === undefined ? {} : { inputMap: opts.inputMap }),
|
|
@@ -575,14 +493,12 @@ async function createAppFromCanvas(
|
|
|
575
493
|
extensions: [
|
|
576
494
|
rhiDebugHostPlugin(cleanupRhiDebugHost),
|
|
577
495
|
...userPlugins,
|
|
578
|
-
canvasAspectPlugin(canvas
|
|
496
|
+
canvasAspectPlugin(canvas),
|
|
579
497
|
],
|
|
580
498
|
}),
|
|
581
499
|
);
|
|
582
500
|
} catch (cause) {
|
|
583
501
|
cleanupRhiDebugHost();
|
|
584
|
-
assetAssembly.dispose();
|
|
585
|
-
assetAssembly.registry.dispose();
|
|
586
502
|
return err(
|
|
587
503
|
makeAppError(
|
|
588
504
|
'app-plugin-activation-failed',
|
|
@@ -639,23 +555,17 @@ async function createAppFromCanvas(
|
|
|
639
555
|
// of static dep on @forgeax/engine-remote. Component reflection crosses
|
|
640
556
|
// this boundary as JSON-safe host data; app does not own any component.
|
|
641
557
|
const remoteHandle = shouldStartRemote
|
|
642
|
-
? await startRemoteServer(
|
|
643
|
-
world,
|
|
644
|
-
renderer,
|
|
645
|
-
assetAssembly.registry,
|
|
646
|
-
rhiCapture,
|
|
647
|
-
opts?.profiler,
|
|
648
|
-
executionControl,
|
|
649
|
-
)
|
|
558
|
+
? await startRemoteServer(world, renderer, assets, rhiCapture, opts?.profiler, executionControl)
|
|
650
559
|
: undefined;
|
|
651
560
|
if (remoteHandle !== undefined) await pluginContext.plugin(remoteServerPlugin(remoteHandle));
|
|
652
561
|
|
|
653
562
|
const buildArgs: BuildAppArgs = {
|
|
654
563
|
renderer,
|
|
655
|
-
assets
|
|
564
|
+
assets,
|
|
656
565
|
world,
|
|
657
566
|
pluginContext,
|
|
658
567
|
executionControl,
|
|
568
|
+
onFrameSubmitted: (event) => publishBrowserFrameSubmitted(canvas, event),
|
|
659
569
|
...(inputBackend !== undefined ? { inputBackend } : {}),
|
|
660
570
|
...(inputHandle === undefined
|
|
661
571
|
? {}
|
|
@@ -681,10 +591,7 @@ async function createAppFromCanvas(
|
|
|
681
591
|
Object.assign(buildArgs, { profiler: opts.profiler });
|
|
682
592
|
}
|
|
683
593
|
if (rhiCapture !== undefined) {
|
|
684
|
-
Object.assign(buildArgs, {
|
|
685
|
-
rhiCapture,
|
|
686
|
-
...(rhiAttachment === undefined ? {} : { rhiAttachment }),
|
|
687
|
-
});
|
|
594
|
+
Object.assign(buildArgs, { rhiCapture });
|
|
688
595
|
}
|
|
689
596
|
if (debugDraw !== undefined) {
|
|
690
597
|
Object.assign(buildArgs, { debugDraw });
|
|
@@ -695,6 +602,7 @@ async function createAppFromCanvas(
|
|
|
695
602
|
|
|
696
603
|
// The aspect-sync sidecar belongs to the canvas path because it owns the DOM
|
|
697
604
|
// canvas. It runs as an Update system so it shares World scheduling semantics.
|
|
605
|
+
resetBrowserFrameSubmitted(canvas);
|
|
698
606
|
const built = await buildApp(buildArgs);
|
|
699
607
|
if (!built.ok) {
|
|
700
608
|
await pluginContext.fiber.dispose();
|
|
@@ -727,17 +635,17 @@ async function createAppFromCanvas(
|
|
|
727
635
|
?.VITE_FORGEAX_ENGINE_BRIDGE_PORT ?? '5733';
|
|
728
636
|
await pluginContext.plugin({
|
|
729
637
|
name: 'browser-remote-bridge',
|
|
730
|
-
inject: ['world', 'renderer', '
|
|
638
|
+
inject: ['world', 'renderer', 'assets'],
|
|
731
639
|
async apply(ctx) {
|
|
732
|
-
if (ctx.renderer === undefined || ctx.
|
|
733
|
-
throw new Error('browser remote bridge requires renderer and
|
|
640
|
+
if (ctx.renderer === undefined || ctx.assets === undefined) {
|
|
641
|
+
throw new Error('browser remote bridge requires renderer and assets services');
|
|
734
642
|
}
|
|
735
643
|
try {
|
|
736
644
|
const bridge = await import('./internal/browser-remote-bridge');
|
|
737
645
|
const teardown = await bridge.installBrowserRemoteBridge({
|
|
738
646
|
world: ctx.world,
|
|
739
647
|
renderer: ctx.renderer,
|
|
740
|
-
assets: ctx.
|
|
648
|
+
assets: ctx.assets,
|
|
741
649
|
runtimeModule: engineRuntimeModule,
|
|
742
650
|
...(rhiCapture !== undefined ? { rhiCapture } : {}),
|
|
743
651
|
...(opts?.profiler !== undefined ? { profiler: opts.profiler } : {}),
|
|
@@ -764,47 +672,14 @@ export function syncCanvasDrawingBuffer(
|
|
|
764
672
|
canvas: Pick<HTMLCanvasElement, 'clientWidth' | 'clientHeight' | 'width' | 'height'> & {
|
|
765
673
|
readonly style?: Pick<CSSStyleDeclaration, 'width' | 'height'>;
|
|
766
674
|
},
|
|
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 {
|
|
675
|
+
): void {
|
|
801
676
|
if (
|
|
802
677
|
!Number.isFinite(canvas.clientWidth) ||
|
|
803
678
|
!Number.isFinite(canvas.clientHeight) ||
|
|
804
679
|
canvas.clientWidth <= 0 ||
|
|
805
680
|
canvas.clientHeight <= 0
|
|
806
681
|
)
|
|
807
|
-
return
|
|
682
|
+
return;
|
|
808
683
|
// An intrinsic canvas with no CSS dimensions already has an explicit
|
|
809
684
|
// drawing-buffer contract. `clientWidth` mirrors `width` for this shape;
|
|
810
685
|
// multiplying it by DPR would change the caller's requested pixel size and
|
|
@@ -818,15 +693,9 @@ function resolveCanvasDrawingBufferSize(
|
|
|
818
693
|
canvas.clientWidth === canvas.width &&
|
|
819
694
|
canvas.clientHeight === canvas.height
|
|
820
695
|
) {
|
|
821
|
-
return
|
|
696
|
+
return;
|
|
822
697
|
}
|
|
823
|
-
const
|
|
824
|
-
const dpr =
|
|
825
|
-
maxCanvasPixelRatio === undefined ||
|
|
826
|
-
!Number.isFinite(maxCanvasPixelRatio) ||
|
|
827
|
-
maxCanvasPixelRatio <= 0
|
|
828
|
-
? devicePixelRatio
|
|
829
|
-
: Math.min(devicePixelRatio, maxCanvasPixelRatio);
|
|
698
|
+
const dpr = Math.max(1, globalThis.devicePixelRatio || 1);
|
|
830
699
|
const previous = syncedCanvasSizes.get(canvas);
|
|
831
700
|
const drawingBufferIsTheLayoutMeasurement =
|
|
832
701
|
previous !== undefined &&
|
|
@@ -838,13 +707,60 @@ function resolveCanvasDrawingBufferSize(
|
|
|
838
707
|
const cssHeight = drawingBufferIsTheLayoutMeasurement ? previous.cssHeight : canvas.clientHeight;
|
|
839
708
|
const width = Math.max(1, Math.round(cssWidth * dpr));
|
|
840
709
|
const height = Math.max(1, Math.round(cssHeight * dpr));
|
|
710
|
+
if (canvas.width !== width) canvas.width = width;
|
|
711
|
+
if (canvas.height !== height) canvas.height = height;
|
|
841
712
|
syncedCanvasSizes.set(canvas, {
|
|
842
713
|
cssWidth,
|
|
843
714
|
cssHeight,
|
|
844
715
|
drawingWidth: width,
|
|
845
716
|
drawingHeight: height,
|
|
846
717
|
});
|
|
847
|
-
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/** Measure a CSS-sized canvas without mutating its drawing buffer. */
|
|
721
|
+
export function measureCanvasDrawingBuffer(
|
|
722
|
+
canvas: Pick<HTMLCanvasElement, 'clientWidth' | 'clientHeight' | 'width' | 'height'> & {
|
|
723
|
+
readonly style?: Pick<CSSStyleDeclaration, 'width' | 'height'>;
|
|
724
|
+
},
|
|
725
|
+
maxCanvasPixelRatio?: number,
|
|
726
|
+
): { readonly width: number; readonly height: number } {
|
|
727
|
+
if (
|
|
728
|
+
!Number.isFinite(canvas.clientWidth) ||
|
|
729
|
+
!Number.isFinite(canvas.clientHeight) ||
|
|
730
|
+
canvas.clientWidth <= 0 ||
|
|
731
|
+
canvas.clientHeight <= 0
|
|
732
|
+
) {
|
|
733
|
+
return { width: canvas.width, height: canvas.height };
|
|
734
|
+
}
|
|
735
|
+
const hasExplicitCssSize =
|
|
736
|
+
(canvas.style?.width ?? '') !== '' || (canvas.style?.height ?? '') !== '';
|
|
737
|
+
if (
|
|
738
|
+
!hasExplicitCssSize &&
|
|
739
|
+
canvas.clientWidth === canvas.width &&
|
|
740
|
+
canvas.clientHeight === canvas.height
|
|
741
|
+
) {
|
|
742
|
+
return { width: canvas.width, height: canvas.height };
|
|
743
|
+
}
|
|
744
|
+
const devicePixelRatio = Math.max(1, globalThis.devicePixelRatio || 1);
|
|
745
|
+
const dpr =
|
|
746
|
+
maxCanvasPixelRatio === undefined ||
|
|
747
|
+
!Number.isFinite(maxCanvasPixelRatio) ||
|
|
748
|
+
maxCanvasPixelRatio <= 0
|
|
749
|
+
? devicePixelRatio
|
|
750
|
+
: Math.min(devicePixelRatio, maxCanvasPixelRatio);
|
|
751
|
+
const previous = syncedCanvasSizes.get(canvas);
|
|
752
|
+
const drawingBufferIsTheLayoutMeasurement =
|
|
753
|
+
previous !== undefined &&
|
|
754
|
+
canvas.width === previous.drawingWidth &&
|
|
755
|
+
canvas.height === previous.drawingHeight &&
|
|
756
|
+
canvas.clientWidth === previous.drawingWidth &&
|
|
757
|
+
canvas.clientHeight === previous.drawingHeight;
|
|
758
|
+
const cssWidth = drawingBufferIsTheLayoutMeasurement ? previous.cssWidth : canvas.clientWidth;
|
|
759
|
+
const cssHeight = drawingBufferIsTheLayoutMeasurement ? previous.cssHeight : canvas.clientHeight;
|
|
760
|
+
return {
|
|
761
|
+
width: Math.max(1, Math.round(cssWidth * dpr)),
|
|
762
|
+
height: Math.max(1, Math.round(cssHeight * dpr)),
|
|
763
|
+
};
|
|
848
764
|
}
|
|
849
765
|
|
|
850
766
|
const syncedCanvasSizes = new WeakMap<
|
|
@@ -857,10 +773,49 @@ const syncedCanvasSizes = new WeakMap<
|
|
|
857
773
|
}
|
|
858
774
|
>();
|
|
859
775
|
|
|
776
|
+
/**
|
|
777
|
+
* Per-frame aspect-sync body for the createApp(canvas) path (feat-20260617
|
|
778
|
+
* M3 / w13). Walks every Camera entity and, for perspective cameras with
|
|
779
|
+
* `autoAspect === true`, writes `canvasW / canvasH` into `Camera.aspect`.
|
|
780
|
+
*
|
|
781
|
+
* Read discipline (D-5 / research Finding 2): `autoAspect` is read through
|
|
782
|
+
* `world.get` (the readRow path narrows the bool column to a JS boolean).
|
|
783
|
+
* The query bundle is used only to enumerate the entity handles -- reading
|
|
784
|
+
* the bool column off the bundle would return a raw 0/1 number, so a
|
|
785
|
+
* `!== 0` test is always true (the
|
|
786
|
+
* bool-field-compared-with-not-equal-zero-always-true trap).
|
|
787
|
+
*
|
|
788
|
+
* Best-effort + side-effect-isolated:
|
|
789
|
+
* - canvas size 0 (detached / display:none) -> skip entirely so `aspect`
|
|
790
|
+
* never becomes NaN / 0.
|
|
791
|
+
* - orthographic cameras and `autoAspect === false` cameras are left
|
|
792
|
+
* untouched.
|
|
793
|
+
*
|
|
794
|
+
* @see {@link https://github.com/Ubpa/forgeax-engine/blob/main/docs/how-to/2026-06-18-host-engine-contract.md | Host-engine contract SSOT}
|
|
795
|
+
*/
|
|
796
|
+
export function syncCameraAspect(world: World, canvasW: number, canvasH: number): void {
|
|
797
|
+
// Guard against detached / zero-sized canvases: a 0 width or height would
|
|
798
|
+
// write NaN (0 / 0) or 0 into aspect and corrupt the projection matrix.
|
|
799
|
+
if (canvasW <= 0 || canvasH <= 0) return;
|
|
800
|
+
const aspect = canvasW / canvasH;
|
|
801
|
+
|
|
802
|
+
const query = world.query({ with: [Camera] }).unwrap();
|
|
803
|
+
for (const row of query) {
|
|
804
|
+
const entity = row.entity;
|
|
805
|
+
const r = world.get(entity, Camera);
|
|
806
|
+
if (!r.ok) continue;
|
|
807
|
+
// world.get narrows the bool column to a real boolean (D-5); the
|
|
808
|
+
// perspective discriminator is the numeric column value.
|
|
809
|
+
if (r.value.autoAspect !== true) continue;
|
|
810
|
+
if (r.value.projection !== CAMERA_PROJECTION_PERSPECTIVE) continue;
|
|
811
|
+
world.set(entity, Camera, { aspect });
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
860
815
|
async function startRemoteServer(
|
|
861
816
|
world: World,
|
|
862
817
|
renderer: Renderer,
|
|
863
|
-
assets: AssetRegistry,
|
|
818
|
+
assets: AssetRegistry | undefined,
|
|
864
819
|
rhiCapture: RhiCapture | undefined,
|
|
865
820
|
profiler: import('@forgeax/engine-profiler').Profiler | undefined,
|
|
866
821
|
execution: ExecutionControl,
|
|
@@ -907,22 +862,17 @@ async function startRemoteServer(
|
|
|
907
862
|
async function createAppFromAssemble(
|
|
908
863
|
args: AppAssembleArgs,
|
|
909
864
|
): 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
865
|
let pluginContext: Context;
|
|
914
866
|
try {
|
|
915
867
|
pluginContext = await createWorldContext(
|
|
916
868
|
args.world,
|
|
917
869
|
assembledEngineProfile({
|
|
918
870
|
renderer: args.renderer,
|
|
919
|
-
|
|
871
|
+
...(args.assets === undefined ? {} : { assets: args.assets }),
|
|
920
872
|
extensions: args.plugins ?? [],
|
|
921
873
|
}),
|
|
922
874
|
);
|
|
923
875
|
} catch (cause) {
|
|
924
|
-
assetAssembly.dispose();
|
|
925
|
-
assetAssembly.registry.dispose();
|
|
926
876
|
return err(
|
|
927
877
|
makeAppError(
|
|
928
878
|
'app-plugin-activation-failed',
|
|
@@ -968,7 +918,7 @@ async function createAppFromAssemble(
|
|
|
968
918
|
? await startRemoteServer(
|
|
969
919
|
args.world,
|
|
970
920
|
args.renderer,
|
|
971
|
-
|
|
921
|
+
args.assets,
|
|
972
922
|
undefined,
|
|
973
923
|
args.profiler,
|
|
974
924
|
executionControl,
|
|
@@ -978,7 +928,6 @@ async function createAppFromAssemble(
|
|
|
978
928
|
|
|
979
929
|
const buildArgs: BuildAppArgs = {
|
|
980
930
|
renderer: args.renderer,
|
|
981
|
-
assets: assetAssembly.registry,
|
|
982
931
|
world: args.world,
|
|
983
932
|
pluginContext,
|
|
984
933
|
executionControl,
|
|
@@ -1013,7 +962,7 @@ async function createAppFromAssemble(
|
|
|
1013
962
|
|
|
1014
963
|
interface BuildAppArgs {
|
|
1015
964
|
readonly renderer: Renderer;
|
|
1016
|
-
readonly assets
|
|
965
|
+
readonly assets?: AssetRegistry;
|
|
1017
966
|
readonly world: World;
|
|
1018
967
|
readonly pluginContext: Context;
|
|
1019
968
|
readonly inputBackend?: InputBackend;
|
|
@@ -1027,10 +976,8 @@ interface BuildAppArgs {
|
|
|
1027
976
|
readonly wireOnLockErrorDispatch?: (dispatch: (err: AppError) => void) => void;
|
|
1028
977
|
readonly audioBackend?: AudioBackend;
|
|
1029
978
|
readonly silenceUnhandledErrors?: boolean;
|
|
1030
|
-
/**
|
|
979
|
+
/** Optional App-owned RHI capture capability. */
|
|
1031
980
|
readonly rhiCapture?: RhiCapture;
|
|
1032
|
-
/** Internal attachment used to bind capture to this App's frame authority. */
|
|
1033
|
-
readonly rhiAttachment?: RecorderAttachment;
|
|
1034
981
|
/** feat-20260615 debug-draw M5: DebugDraw instance created by createDebugDrawOnReady. */
|
|
1035
982
|
readonly debugDraw?: DebugDraw;
|
|
1036
983
|
/** feat-20260629 M4 / w20: remote eval server handle from createAppFromCanvas. */
|
|
@@ -1050,6 +997,11 @@ interface BuildAppArgs {
|
|
|
1050
997
|
| undefined;
|
|
1051
998
|
readonly profiler?: import('@forgeax/engine-profiler').Profiler;
|
|
1052
999
|
readonly executionControl?: import('./execution/control').LocalExecutionControl;
|
|
1000
|
+
/** Canvas-form projection of the Renderer frame-submitted event. */
|
|
1001
|
+
readonly onFrameSubmitted?: (event: {
|
|
1002
|
+
readonly frameId: number;
|
|
1003
|
+
readonly deviceGeneration: number;
|
|
1004
|
+
}) => void;
|
|
1053
1005
|
}
|
|
1054
1006
|
|
|
1055
1007
|
/**
|
|
@@ -1060,14 +1012,12 @@ interface BuildAppArgs {
|
|
|
1060
1012
|
async function buildApp(args: BuildAppArgs): Promise<Result<App, AppError | RhiError>> {
|
|
1061
1013
|
const {
|
|
1062
1014
|
renderer,
|
|
1063
|
-
assets,
|
|
1064
1015
|
world,
|
|
1065
1016
|
pluginContext,
|
|
1066
1017
|
inputBackend,
|
|
1067
1018
|
audioBackend,
|
|
1068
1019
|
silenceUnhandledErrors,
|
|
1069
1020
|
rhiCapture,
|
|
1070
|
-
rhiAttachment,
|
|
1071
1021
|
debugDraw,
|
|
1072
1022
|
remoteHandle,
|
|
1073
1023
|
profiler,
|
|
@@ -1088,6 +1038,7 @@ async function buildApp(args: BuildAppArgs): Promise<Result<App, AppError | RhiE
|
|
|
1088
1038
|
const fanout = new ErrorFanoutRegistry(
|
|
1089
1039
|
silenceUnhandledErrors !== undefined ? { silenceUnhandledErrors } : {},
|
|
1090
1040
|
);
|
|
1041
|
+
let lastError: AppDispatchError | undefined;
|
|
1091
1042
|
const execution =
|
|
1092
1043
|
args.executionControl ??
|
|
1093
1044
|
createLocalExecutionControl(
|
|
@@ -1116,6 +1067,7 @@ async function buildApp(args: BuildAppArgs): Promise<Result<App, AppError | RhiE
|
|
|
1116
1067
|
);
|
|
1117
1068
|
|
|
1118
1069
|
function dispatch(e: AppDispatchError): void {
|
|
1070
|
+
lastError = e;
|
|
1119
1071
|
fanout.fire(e);
|
|
1120
1072
|
}
|
|
1121
1073
|
|
|
@@ -1141,8 +1093,7 @@ async function buildApp(args: BuildAppArgs): Promise<Result<App, AppError | RhiE
|
|
|
1141
1093
|
Object.assign(loopOpts, { profiler });
|
|
1142
1094
|
}
|
|
1143
1095
|
const loop = createFrameLoop(loopOpts);
|
|
1144
|
-
|
|
1145
|
-
if (rhiCapture !== undefined && rhiAttachment !== undefined) {
|
|
1096
|
+
if (rhiCapture !== undefined) {
|
|
1146
1097
|
bindRhiCaptureFrameDriver(rhiCapture, {
|
|
1147
1098
|
getState: () => loop.getState(),
|
|
1148
1099
|
pause: () => {
|
|
@@ -1158,9 +1109,6 @@ async function buildApp(args: BuildAppArgs): Promise<Result<App, AppError | RhiE
|
|
|
1158
1109
|
stepFrame: (deltaSeconds) => loop.stepFrame(deltaSeconds),
|
|
1159
1110
|
});
|
|
1160
1111
|
}
|
|
1161
|
-
|
|
1162
|
-
let lastError: AppDispatchError | undefined;
|
|
1163
|
-
|
|
1164
1112
|
// M4 (w13) device-lost internal subscription. R-1 timing contract:
|
|
1165
1113
|
// app.start() arms the rAF handle BEFORE this listener subscribes, so
|
|
1166
1114
|
// a synchronous late-attach replay of a persisted device-lost event
|
|
@@ -1177,32 +1125,13 @@ async function buildApp(args: BuildAppArgs): Promise<Result<App, AppError | RhiE
|
|
|
1177
1125
|
if (rendererUnsubscribe !== undefined) {
|
|
1178
1126
|
return;
|
|
1179
1127
|
}
|
|
1180
|
-
rendererUnsubscribe = renderer.
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
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;
|
|
1128
|
+
rendererUnsubscribe = renderer.subscribe((event) => {
|
|
1129
|
+
if (event.kind === 'frame-submitted') {
|
|
1130
|
+
args.onFrameSubmitted?.(event);
|
|
1131
|
+
return;
|
|
1202
1132
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
// can decide whether to rebuild the renderer).
|
|
1133
|
+
if (event.kind !== 'error') return;
|
|
1134
|
+
const e: RenderError = event.error;
|
|
1206
1135
|
dispatch(e);
|
|
1207
1136
|
});
|
|
1208
1137
|
}
|
|
@@ -1216,10 +1145,10 @@ async function buildApp(args: BuildAppArgs): Promise<Result<App, AppError | RhiE
|
|
|
1216
1145
|
|
|
1217
1146
|
const stub: App = {
|
|
1218
1147
|
renderer,
|
|
1219
|
-
assets,
|
|
1148
|
+
...(args.assets === undefined ? {} : { assets: args.assets }),
|
|
1220
1149
|
world,
|
|
1221
1150
|
execution,
|
|
1222
|
-
async releaseSurfacePreserveWorld(): Promise<Result<void, RhiError>> {
|
|
1151
|
+
async releaseSurfacePreserveWorld(): Promise<Result<void, RhiError | RenderError>> {
|
|
1223
1152
|
if (loop.getState() === 'running') {
|
|
1224
1153
|
const paused = loop.pause();
|
|
1225
1154
|
if (!paused.ok) {
|
|
@@ -1246,7 +1175,7 @@ async function buildApp(args: BuildAppArgs): Promise<Result<App, AppError | RhiE
|
|
|
1246
1175
|
}
|
|
1247
1176
|
return released.ok ? ok(undefined) : err(released.error);
|
|
1248
1177
|
},
|
|
1249
|
-
async restoreSurface(): Promise<Result<void, RhiError>> {
|
|
1178
|
+
async restoreSurface(): Promise<Result<void, RhiError | RenderError>> {
|
|
1250
1179
|
const restored = renderer.restoreSurface();
|
|
1251
1180
|
if (!restored.ok) return err(restored.error);
|
|
1252
1181
|
if (resumeAfterSurfaceRestore) {
|
|
@@ -1276,7 +1205,7 @@ async function buildApp(args: BuildAppArgs): Promise<Result<App, AppError | RhiE
|
|
|
1276
1205
|
},
|
|
1277
1206
|
start(): Result<void, AppError> {
|
|
1278
1207
|
// R-1: arm the rAF handle FIRST (loop.start schedules raf(tick))
|
|
1279
|
-
// and only THEN subscribe to renderer.
|
|
1208
|
+
// and only THEN subscribe to renderer events. If the renderer
|
|
1280
1209
|
// late-attach replays a persisted device-lost event during the
|
|
1281
1210
|
// subscribe call, the loop is already armed and can retain the error.
|
|
1282
1211
|
const r = loop.start();
|
|
@@ -1321,7 +1250,7 @@ async function buildApp(args: BuildAppArgs): Promise<Result<App, AppError | RhiE
|
|
|
1321
1250
|
loop.setDrawSource(drawSource);
|
|
1322
1251
|
},
|
|
1323
1252
|
/**
|
|
1324
|
-
* Most recent
|
|
1253
|
+
* Most recent dispatched error retained for host self-inspection.
|
|
1325
1254
|
*/
|
|
1326
1255
|
get lastError(): AppDispatchError | undefined {
|
|
1327
1256
|
return lastError;
|
|
@@ -1331,21 +1260,5 @@ async function buildApp(args: BuildAppArgs): Promise<Result<App, AppError | RhiE
|
|
|
1331
1260
|
...(remoteHandle !== undefined ? { remote: remoteHandle } : {}),
|
|
1332
1261
|
};
|
|
1333
1262
|
|
|
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
1263
|
return ok(stub);
|
|
1351
1264
|
}
|