@vgai/engine 0.5.15 → 0.5.16
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/dist/adapter/adapter-module.d.ts +228 -17
- package/dist/adapter/adapter-module.d.ts.map +1 -1
- package/dist/adapter/adapter-module.js +224 -42
- package/dist/adapter/authoring-seam-contract.d.ts +530 -0
- package/dist/adapter/authoring-seam-contract.d.ts.map +1 -0
- package/dist/adapter/authoring-seam-contract.js +162 -0
- package/dist/adapter/authoring.d.ts +237 -50
- package/dist/adapter/authoring.d.ts.map +1 -1
- package/dist/adapter/authoring.js +2 -1
- package/dist/adapter/entrypoint-selection-source.d.ts +35 -0
- package/dist/adapter/entrypoint-selection-source.d.ts.map +1 -0
- package/dist/adapter/entrypoint-selection-source.js +151 -0
- package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts +7 -1
- package/dist/adapter/finders/scenes-from-entrypoint-selection.d.ts.map +1 -1
- package/dist/adapter/finders/scenes-from-entrypoint-selection.js +142 -21
- package/dist/adapter/first-party-systems.d.ts +13 -3
- package/dist/adapter/first-party-systems.d.ts.map +1 -1
- package/dist/adapter/first-party-systems.js +10 -3
- package/dist/adapter/index.d.ts +7 -2
- package/dist/adapter/index.d.ts.map +1 -1
- package/dist/adapter/index.js +4 -0
- package/dist/adapter/ingest/contract-system-adapters.d.ts +1 -1
- package/dist/adapter/ingest/contract-system-adapters.d.ts.map +1 -1
- package/dist/adapter/ingest/contract-system-adapters.js +13 -25
- package/dist/adapter/ingest/game-contract-seams.d.ts +214 -0
- package/dist/adapter/ingest/game-contract-seams.d.ts.map +1 -0
- package/dist/adapter/ingest/game-contract-seams.js +62 -0
- package/dist/adapter/ingest/game-contract.d.ts +5 -3
- package/dist/adapter/ingest/game-contract.d.ts.map +1 -1
- package/dist/adapter/ingest/game-contract.js +4 -1
- package/dist/adapter/ingest/merge-debug-adapters.d.ts +52 -0
- package/dist/adapter/ingest/merge-debug-adapters.d.ts.map +1 -0
- package/dist/adapter/ingest/merge-debug-adapters.js +141 -0
- package/dist/adapter/ingest/observation-debug-adapter.d.ts +56 -0
- package/dist/adapter/ingest/observation-debug-adapter.d.ts.map +1 -0
- package/dist/adapter/ingest/observation-debug-adapter.js +131 -0
- package/dist/adapter/ingest/scene-capture.d.ts +38 -13
- package/dist/adapter/ingest/scene-capture.d.ts.map +1 -1
- package/dist/adapter/ingest/scene-capture.js +101 -24
- package/dist/adapter/ingest/visible-capture-window.d.ts +22 -5
- package/dist/adapter/ingest/visible-capture-window.d.ts.map +1 -1
- package/dist/adapter/ingest/visible-capture-window.js +46 -7
- package/dist/adapter/native-debug-module.d.ts +43 -0
- package/dist/adapter/native-debug-module.d.ts.map +1 -0
- package/dist/adapter/native-debug-module.js +230 -0
- package/dist/adapter/object3d-authoring-subject.d.ts +32 -0
- package/dist/adapter/object3d-authoring-subject.d.ts.map +1 -0
- package/dist/adapter/object3d-authoring-subject.js +25 -0
- package/dist/adapter/root-adapter.d.ts +21 -56
- package/dist/adapter/root-adapter.d.ts.map +1 -1
- package/dist/adapter/root-adapter.js +4 -20
- package/dist/adapter/root-seam-contract.d.ts +124 -0
- package/dist/adapter/root-seam-contract.d.ts.map +1 -0
- package/dist/adapter/root-seam-contract.js +36 -0
- package/dist/adapter/seam-evidence.d.ts +118 -0
- package/dist/adapter/seam-evidence.d.ts.map +1 -0
- package/dist/adapter/seam-evidence.js +231 -0
- package/dist/adapter/setup-three-root-adapter.d.ts +2 -3
- package/dist/adapter/setup-three-root-adapter.d.ts.map +1 -1
- package/dist/adapter/setup-three-root-adapter.js +2 -3
- package/dist/adapter/system-adapter.d.ts +6 -1
- package/dist/adapter/system-adapter.d.ts.map +1 -1
- package/dist/adapter/system-seam-contract.d.ts +354 -0
- package/dist/adapter/system-seam-contract.d.ts.map +1 -0
- package/dist/adapter/system-seam-contract.js +95 -0
- package/dist/animation/xstate-animation-binding.d.ts +6 -0
- package/dist/animation/xstate-animation-binding.d.ts.map +1 -1
- package/dist/animation/xstate-animation-binding.js +142 -4
- package/dist/asset-formats/collider.d.ts +1 -1
- package/dist/asset-formats/mesh.d.ts +2 -2
- package/dist/asset-formats/particles.d.ts +1 -1
- package/dist/canvas-react/engine-bridge.d.ts +6 -6
- package/dist/canvas-react/engine-bridge.d.ts.map +1 -1
- package/dist/canvas-react/engine-bridge.js +2 -2
- package/dist/canvas-react/index.d.ts +2 -2
- package/dist/canvas-react/index.d.ts.map +1 -1
- package/dist/canvas-react/index.js +1 -1
- package/dist/canvas-react/pixi-react-adapter.d.ts +15 -9
- package/dist/canvas-react/pixi-react-adapter.d.ts.map +1 -1
- package/dist/canvas-react/pixi-react-adapter.js +43 -29
- package/dist/canvas-react/pixi-react-root-factory.d.ts.map +1 -1
- package/dist/canvas-react/pixi-react-root-factory.js +4 -0
- package/dist/canvas-react/world-context.d.ts +22 -12
- package/dist/canvas-react/world-context.d.ts.map +1 -1
- package/dist/canvas-react/world-context.js +12 -51
- package/dist/ecs/user-data.d.ts +5 -0
- package/dist/ecs/user-data.d.ts.map +1 -1
- package/dist/ecs/user-data.js +3 -0
- package/dist/input/input-manager.d.ts +49 -0
- package/dist/input/input-manager.d.ts.map +1 -1
- package/dist/input/input-manager.js +127 -0
- package/dist/manifest/schema.d.ts +9 -9
- package/dist/pixi/authoring.d.ts.map +1 -1
- package/dist/pixi/authoring.js +220 -1
- package/dist/pixi/ingest.d.ts +2 -1
- package/dist/pixi/ingest.d.ts.map +1 -1
- package/dist/pixi/ingest.js +4 -2
- package/dist/pixi/scene-capture.d.ts +9 -1
- package/dist/pixi/scene-capture.d.ts.map +1 -1
- package/dist/pixi/scene-capture.js +9 -6
- package/dist/render/particles-factory.d.ts +13 -6
- package/dist/render/particles-factory.d.ts.map +1 -1
- package/dist/render/particles-factory.js +6 -3
- package/dist/runtime/create-runtime.d.ts +23 -46
- package/dist/runtime/create-runtime.d.ts.map +1 -1
- package/dist/runtime/create-runtime.js +22 -67
- package/dist/runtime/game.d.ts +29 -71
- package/dist/runtime/game.d.ts.map +1 -1
- package/dist/runtime/game.js +15 -54
- package/dist/runtime/mount-game.d.ts.map +1 -1
- package/dist/runtime/mount-game.js +12 -1
- package/dist/runtime/mount-manifest.d.ts.map +1 -1
- package/dist/runtime/mount-manifest.js +4 -5
- package/dist/runtime/types.d.ts +18 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/setup/setup-renderer.d.ts +3 -4
- package/dist/setup/setup-renderer.d.ts.map +1 -1
- package/dist/setup/setup-renderer.js +3 -4
- package/dist/world3d-react/engine-bridge.d.ts +1 -1
- package/dist/world3d-react/engine-bridge.js +1 -1
- package/dist/world3d-react/r3f-adapter.d.ts +7 -0
- package/dist/world3d-react/r3f-adapter.d.ts.map +1 -1
- package/dist/world3d-react/r3f-adapter.js +50 -21
- package/dist/world3d-react/r3f-root-factory.d.ts.map +1 -1
- package/dist/world3d-react/r3f-root-factory.js +4 -0
- package/dist/world3d-react/world-context.d.ts +9 -3
- package/dist/world3d-react/world-context.d.ts.map +1 -1
- package/dist/world3d-react/world-context.js +5 -18
- package/package.json +1 -1
- package/schemas/engine-capabilities.json +7 -7
- package/src/adapter/adapter-module.ts +407 -50
- package/src/adapter/authoring-seam-contract.ts +211 -0
- package/src/adapter/authoring.ts +242 -50
- package/src/adapter/entrypoint-selection-source.ts +159 -0
- package/src/adapter/finders/scenes-from-entrypoint-selection.ts +159 -28
- package/src/adapter/first-party-systems.ts +17 -3
- package/src/adapter/index.ts +51 -1
- package/src/adapter/ingest/contract-system-adapters.ts +26 -30
- package/src/adapter/ingest/game-contract-seams.ts +85 -0
- package/src/adapter/ingest/game-contract.ts +9 -4
- package/src/adapter/ingest/merge-debug-adapters.ts +193 -0
- package/src/adapter/ingest/observation-debug-adapter.ts +158 -0
- package/src/adapter/ingest/scene-capture.ts +126 -23
- package/src/adapter/ingest/visible-capture-window.ts +63 -9
- package/src/adapter/native-debug-module.ts +307 -0
- package/src/adapter/object3d-authoring-subject.ts +50 -0
- package/src/adapter/root-adapter.ts +21 -56
- package/src/adapter/root-seam-contract.ts +56 -0
- package/src/adapter/seam-evidence.ts +336 -0
- package/src/adapter/setup-three-root-adapter.ts +4 -6
- package/src/adapter/system-adapter.ts +6 -1
- package/src/adapter/system-seam-contract.ts +121 -0
- package/src/animation/xstate-animation-binding.ts +151 -4
- package/src/canvas-react/engine-bridge.ts +6 -6
- package/src/canvas-react/index.ts +2 -1
- package/src/canvas-react/pixi-react-adapter.tsx +55 -34
- package/src/canvas-react/pixi-react-root-factory.tsx +4 -0
- package/src/canvas-react/world-context.ts +26 -101
- package/src/ecs/user-data.ts +5 -0
- package/src/input/input-manager.ts +122 -0
- package/src/pixi/authoring.ts +236 -1
- package/src/pixi/ingest.ts +9 -2
- package/src/pixi/scene-capture.ts +20 -5
- package/src/render/particles-factory.ts +16 -6
- package/src/runtime/create-runtime.ts +41 -106
- package/src/runtime/game.ts +41 -111
- package/src/runtime/mount-game.ts +19 -1
- package/src/runtime/mount-manifest.ts +4 -5
- package/src/runtime/types.ts +22 -0
- package/src/setup/setup-renderer.ts +3 -4
- package/src/world3d-react/engine-bridge.ts +1 -1
- package/src/world3d-react/r3f-adapter.tsx +64 -24
- package/src/world3d-react/r3f-root-factory.tsx +4 -0
- package/src/world3d-react/world-context.ts +11 -36
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Scene capture — the core primitive of unmodified-game ingestion.
|
|
3
3
|
*
|
|
4
|
-
* An external three.js game owns its own
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* first renders.
|
|
8
|
-
* `WebGLRenderer.prototype.render`:
|
|
4
|
+
* An external three.js game owns its own renderer, `Scene`, camera, and render
|
|
5
|
+
* loop. To let the editor inspect/edit that live scene WITHOUT touching the
|
|
6
|
+
* game's code, we need a handle to the game's `Scene`+camera the moment it
|
|
7
|
+
* first renders. For WebGL, the robust way to obtain it is an **accessor trap**
|
|
8
|
+
* on `WebGLRenderer.prototype.render`:
|
|
9
9
|
*
|
|
10
10
|
* - `WebGLRenderer` assigns `this.render` as an OWN instance property inside
|
|
11
11
|
* its constructor (not on the prototype), and `THREE.WebGLRenderer` is a
|
|
@@ -20,12 +20,16 @@
|
|
|
20
20
|
* (`docs/ingest-study-spike/vgai-ingest-adapter.js`), proven against an
|
|
21
21
|
* unmodified `three.js/examples/games_fps` game.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
23
|
+
* DOM-backed addon renderers use ordinary prototype methods instead. The host
|
|
24
|
+
* supplies those exact shared classes through `additionalRendererCtors`, and
|
|
25
|
+
* the same observer captures their `(scene, camera)` pair without inventing a
|
|
26
|
+
* second world model. CSS3DRenderer is the first implementer.
|
|
27
|
+
*
|
|
28
|
+
* CRITICAL: every trap must be installed on the SAME `three` module/addon
|
|
29
|
+
* instance the game uses. In a bundler/dev-server that dedupes `three` (one
|
|
30
|
+
* `node_modules/three`), an external ESM game's imports resolve to those shared
|
|
31
|
+
* classes. A game that bundles its own copy cannot be captured this way (the
|
|
32
|
+
* module-identity gatekeeper).
|
|
29
33
|
*
|
|
30
34
|
* Wave 17 adds an OPTIONAL, ADDITIVE composer capture: a game rendering through
|
|
31
35
|
* its own three.js addon `EffectComposer`
|
|
@@ -51,20 +55,43 @@
|
|
|
51
55
|
|
|
52
56
|
import type * as THREE from 'three';
|
|
53
57
|
import {
|
|
58
|
+
documentVisibilityClock,
|
|
54
59
|
startVisibleCaptureWindow,
|
|
55
60
|
type VisibilityClock,
|
|
56
61
|
type VisibleCaptureWindow,
|
|
57
62
|
} from './visible-capture-window';
|
|
58
63
|
|
|
64
|
+
/** The renderer operations the ingest host may lawfully use after capture.
|
|
65
|
+
* WebGLRenderer supplies every member; DOM-backed Three addon renderers such as
|
|
66
|
+
* CSS3DRenderer deliberately omit the WebGL-only lifecycle operations. */
|
|
67
|
+
export interface CapturedThreeRenderer {
|
|
68
|
+
readonly domElement: HTMLElement;
|
|
69
|
+
readonly info?: THREE.WebGLInfo | undefined;
|
|
70
|
+
render(scene: THREE.Scene, camera: THREE.Camera): unknown;
|
|
71
|
+
setSize(width: number, height: number, updateStyle?: boolean): void;
|
|
72
|
+
setAnimationLoop?(callback: ((time: number) => void) | null): void;
|
|
73
|
+
dispose?(): void;
|
|
74
|
+
getPixelRatio?(): number;
|
|
75
|
+
getContext?(): unknown;
|
|
76
|
+
getRenderTarget?(): unknown;
|
|
77
|
+
}
|
|
78
|
+
|
|
59
79
|
/** A live runtime captured from an external game on its first rendered frame. */
|
|
60
80
|
export interface CapturedRuntime {
|
|
61
81
|
scene: THREE.Scene;
|
|
62
82
|
camera: THREE.Camera;
|
|
63
|
-
renderer:
|
|
83
|
+
renderer: CapturedThreeRenderer;
|
|
64
84
|
}
|
|
65
85
|
|
|
66
86
|
/** Options for {@link installSceneCapture}. */
|
|
67
87
|
export interface SceneCaptureOptions {
|
|
88
|
+
/**
|
|
89
|
+
* Additional shared Three addon renderer classes whose prototype `render`
|
|
90
|
+
* method carries the same `(scene, camera)` pair as WebGLRenderer. CSS3DRenderer
|
|
91
|
+
* is the first implementer. The caller must pass the exact class its game
|
|
92
|
+
* imports; a bundled private copy remains deliberately uncapturable.
|
|
93
|
+
*/
|
|
94
|
+
additionalRendererCtors?: readonly unknown[];
|
|
68
95
|
/**
|
|
69
96
|
* True when `renderer` is one the HOST constructed for its own drawing.
|
|
70
97
|
*
|
|
@@ -230,7 +257,7 @@ export interface SceneCaptureHandle {
|
|
|
230
257
|
getDrawCount(): number;
|
|
231
258
|
/** The game's last (non-null) `setAnimationLoop` callback for a renderer, so the
|
|
232
259
|
* host can pause (set null) and resume (re-set it) the game's own loop. */
|
|
233
|
-
getAnimationLoop(renderer:
|
|
260
|
+
getAnimationLoop(renderer: CapturedThreeRenderer): ((time: number) => void) | null;
|
|
234
261
|
/**
|
|
235
262
|
* Wave 17 (D-C3): resize every captured `EffectComposer` that renders
|
|
236
263
|
* through the captured renderer to `w`×`h`, matching its pixel ratio to
|
|
@@ -265,6 +292,10 @@ interface EffectComposerCtorLike {
|
|
|
265
292
|
prototype: { render?: (...args: unknown[]) => unknown };
|
|
266
293
|
}
|
|
267
294
|
|
|
295
|
+
interface PrototypeRendererCtorLike {
|
|
296
|
+
prototype: { render?: (...args: unknown[]) => unknown };
|
|
297
|
+
}
|
|
298
|
+
|
|
268
299
|
/**
|
|
269
300
|
* Install the render accessor trap on `threeNamespace.WebGLRenderer.prototype`.
|
|
270
301
|
* Pass the host's `three` module so the game (which shares it) is trapped.
|
|
@@ -318,7 +349,29 @@ export function installSceneCapture(
|
|
|
318
349
|
// the GETTER returns a wrapper that records each non-null callback before forwarding.
|
|
319
350
|
const REAL_SAL = Symbol('vgai.realSetAnimationLoop');
|
|
320
351
|
const loopCallbacks = new WeakMap<object, (time: number) => void>();
|
|
352
|
+
const loopedRenderers = new Set<object>();
|
|
353
|
+
/** Visibility clock of the in-flight `waitForCapture`, if any. */
|
|
354
|
+
let waitingVisibility: VisibilityClock | null = null;
|
|
321
355
|
const priorSAL = Object.getOwnPropertyDescriptor(proto, 'setAnimationLoop');
|
|
356
|
+
|
|
357
|
+
function pumpHiddenLoops(): void {
|
|
358
|
+
if (
|
|
359
|
+
captured ||
|
|
360
|
+
!waitingVisibility ||
|
|
361
|
+
!(waitingVisibility.suspended?.() ?? waitingVisibility.hidden())
|
|
362
|
+
) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
for (const renderer of loopedRenderers) {
|
|
366
|
+
try {
|
|
367
|
+
loopCallbacks.get(renderer)?.(0);
|
|
368
|
+
} catch {
|
|
369
|
+
/* a throwing game frame must not kill the waiter */
|
|
370
|
+
}
|
|
371
|
+
if (captured) return;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
322
375
|
Object.defineProperty(proto, 'setAnimationLoop', {
|
|
323
376
|
configurable: true,
|
|
324
377
|
set(this: Record<symbol, unknown>, fn: unknown) {
|
|
@@ -327,7 +380,14 @@ export function installSceneCapture(
|
|
|
327
380
|
get(this: Record<symbol, unknown>) {
|
|
328
381
|
const self = this;
|
|
329
382
|
return function setAnimationLoop(this: unknown, cb: unknown) {
|
|
330
|
-
if (cb)
|
|
383
|
+
if (cb) {
|
|
384
|
+
loopCallbacks.set(self as object, cb as (time: number) => void);
|
|
385
|
+
loopedRenderers.add(self as object);
|
|
386
|
+
// Hidden tabs park rAF. Play/eval still need a first frame, so when
|
|
387
|
+
// a waiter is parked we drive the game's own loop once — the same
|
|
388
|
+
// class of tick `waitSimTime` already uses for a hidden document.
|
|
389
|
+
queueMicrotask(pumpHiddenLoops);
|
|
390
|
+
}
|
|
331
391
|
const real = self[REAL_SAL];
|
|
332
392
|
return typeof real === 'function'
|
|
333
393
|
? (real as (...a: unknown[]) => unknown).call(self, cb)
|
|
@@ -371,7 +431,7 @@ export function installSceneCapture(
|
|
|
371
431
|
captured = {
|
|
372
432
|
scene: scene as THREE.Scene,
|
|
373
433
|
camera: camera as THREE.Camera,
|
|
374
|
-
renderer: self as
|
|
434
|
+
renderer: self as CapturedThreeRenderer,
|
|
375
435
|
};
|
|
376
436
|
opts?.onWorldAdoption?.({
|
|
377
437
|
phase: 'adopted',
|
|
@@ -405,6 +465,17 @@ export function installSceneCapture(
|
|
|
405
465
|
});
|
|
406
466
|
}
|
|
407
467
|
|
|
468
|
+
/** Observe the common `(scene, camera)` render contract once, regardless of
|
|
469
|
+
* whether it came from WebGLRenderer's constructor-assigned method or an
|
|
470
|
+
* addon's ordinary prototype method. */
|
|
471
|
+
function observeRendererRender(self: unknown, scene: unknown, camera: unknown): void {
|
|
472
|
+
if (!captured || self === captured.renderer) drawCount++;
|
|
473
|
+
if (opts?.isHostRenderer?.(self) === true) return;
|
|
474
|
+
if (!(scene as { isScene?: boolean })?.isScene) return;
|
|
475
|
+
if (captured) recordAlternateWorld(self, scene, camera);
|
|
476
|
+
else adoptWorld(self, scene, camera);
|
|
477
|
+
}
|
|
478
|
+
|
|
408
479
|
Object.defineProperty(proto, 'render', {
|
|
409
480
|
configurable: true,
|
|
410
481
|
set(this: Record<symbol, unknown>, fn: unknown) {
|
|
@@ -423,7 +494,6 @@ export function installSceneCapture(
|
|
|
423
494
|
// `setAnimationLoop`, so they ARE gateable) and the instrument was
|
|
424
495
|
// reporting someone else's frames. Before capture every render still
|
|
425
496
|
// counts: that is how the first game frame is detected at all.
|
|
426
|
-
if (!captured || (self as unknown) === captured.renderer) drawCount++;
|
|
427
497
|
// Never capture a scene the HOST owns. The trap sits on the shared
|
|
428
498
|
// `WebGLRenderer.prototype`, so the editor's own viewport renders
|
|
429
499
|
// arrive here too — and when a game bundles its own MISMATCHED three,
|
|
@@ -434,15 +504,39 @@ export function installSceneCapture(
|
|
|
434
504
|
// Whether that happened at all came down to whether the editor
|
|
435
505
|
// rendered a frame inside the game's capture window, so the same
|
|
436
506
|
// session could pass or fail on timing alone.
|
|
437
|
-
|
|
438
|
-
if (captured) recordAlternateWorld(self, scene, camera);
|
|
439
|
-
else adoptWorld(self, scene, camera);
|
|
440
|
-
}
|
|
507
|
+
observeRendererRender(self, scene, camera);
|
|
441
508
|
return forwardRender(self, args);
|
|
442
509
|
};
|
|
443
510
|
},
|
|
444
511
|
});
|
|
445
512
|
|
|
513
|
+
// DOM-backed Three renderers (CSS3DRenderer is the first) use a normal
|
|
514
|
+
// prototype method rather than WebGLRenderer's constructor assignment. Trap
|
|
515
|
+
// only classes explicitly supplied by the host, and restore each byte-for-byte.
|
|
516
|
+
const additionalRendererRestores: Array<() => void> = [];
|
|
517
|
+
for (const candidate of opts?.additionalRendererCtors ?? []) {
|
|
518
|
+
const ctor = candidate as PrototypeRendererCtorLike;
|
|
519
|
+
const rendererProto = ctor?.prototype;
|
|
520
|
+
const priorRender = rendererProto?.render;
|
|
521
|
+
if (!rendererProto || typeof priorRender !== 'function' || rendererProto === proto) continue;
|
|
522
|
+
rendererProto.render = function (this: unknown, ...args: unknown[]) {
|
|
523
|
+
observeRendererRender(this, args[0], args[1]);
|
|
524
|
+
const hooks = renderPassHooks;
|
|
525
|
+
if (hooks === null || !captured || this !== captured.renderer) {
|
|
526
|
+
return priorRender.apply(this, args);
|
|
527
|
+
}
|
|
528
|
+
hooks.before();
|
|
529
|
+
try {
|
|
530
|
+
return priorRender.apply(this, args);
|
|
531
|
+
} finally {
|
|
532
|
+
hooks.after();
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
additionalRendererRestores.push(() => {
|
|
536
|
+
rendererProto.render = priorRender;
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
446
540
|
// ---- Wave 17 (D-C2): additive composer capture, only when a composer ctor
|
|
447
541
|
// was passed. `EffectComposer` methods (including `render`) live on the
|
|
448
542
|
// PROTOTYPE (a plain ES class — the constructor never does `this.render =
|
|
@@ -494,7 +588,8 @@ export function installSceneCapture(
|
|
|
494
588
|
// touches `renderer.getPixelRatio()` — a non-composer game's captured
|
|
495
589
|
// renderer need not even expose that method for this to stay a no-op.
|
|
496
590
|
if (!captured || composers.size === 0) return;
|
|
497
|
-
const pixelRatio = captured.renderer.getPixelRatio();
|
|
591
|
+
const pixelRatio = captured.renderer.getPixelRatio?.();
|
|
592
|
+
if (pixelRatio === undefined) return;
|
|
498
593
|
for (const composer of composers) {
|
|
499
594
|
composer.setSize(w, h);
|
|
500
595
|
composer.setPixelRatio(pixelRatio);
|
|
@@ -517,12 +612,13 @@ export function installSceneCapture(
|
|
|
517
612
|
onExpire: () => {
|
|
518
613
|
const i = waiters.indexOf(wrapped);
|
|
519
614
|
if (i >= 0) waiters.splice(i, 1);
|
|
615
|
+
waitingVisibility = null;
|
|
520
616
|
opts.onWait?.(null);
|
|
521
617
|
reject(
|
|
522
618
|
new Error(
|
|
523
619
|
`Scene capture timed out after ${timeoutMs}ms of VISIBLE time ` +
|
|
524
|
-
`(${Math.round(captureWindow.elapsedHiddenMs())}ms
|
|
525
|
-
'
|
|
620
|
+
`(${Math.round(captureWindow.elapsedHiddenMs())}ms browser-suspended, which is ` +
|
|
621
|
+
'not counted because no frame can be presented) — the game never rendered, ' +
|
|
526
622
|
'or it bundles its own (un-shared) copy of three.',
|
|
527
623
|
),
|
|
528
624
|
);
|
|
@@ -530,11 +626,17 @@ export function installSceneCapture(
|
|
|
530
626
|
});
|
|
531
627
|
const wrapped = (rt: CapturedRuntime) => {
|
|
532
628
|
captureWindow.cancel();
|
|
629
|
+
waitingVisibility = null;
|
|
533
630
|
opts.onWait?.(null);
|
|
534
631
|
resolve(rt);
|
|
535
632
|
};
|
|
536
633
|
waiters.push(wrapped);
|
|
634
|
+
waitingVisibility = opts.visibility ?? documentVisibilityClock();
|
|
537
635
|
opts.onWait?.(captureWindow);
|
|
636
|
+
// A waiter that starts already-hidden (the normal `vgai play` path
|
|
637
|
+
// against a backgrounded tab) must not wait for a human to foreground
|
|
638
|
+
// it. Pump any loop the game has already registered.
|
|
639
|
+
queueMicrotask(pumpHiddenLoops);
|
|
538
640
|
});
|
|
539
641
|
},
|
|
540
642
|
uninstall() {
|
|
@@ -577,6 +679,7 @@ export function installSceneCapture(
|
|
|
577
679
|
// prototype method — no per-instance own-property to restore, unlike
|
|
578
680
|
// the renderer/loop traps above).
|
|
579
681
|
restoreComposerTrap();
|
|
682
|
+
for (const restore of additionalRendererRestores) restore();
|
|
580
683
|
},
|
|
581
684
|
};
|
|
582
685
|
}
|
|
@@ -46,6 +46,16 @@ export interface VisibilityClock {
|
|
|
46
46
|
now(): number;
|
|
47
47
|
/** True while the document is hidden (no rAF, so no frame can be captured). */
|
|
48
48
|
hidden(): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* True while the browser is not presenting frames. This is wider than
|
|
51
|
+
* `hidden()`: WebKit also stops rAF for an unfocused window (and can suspend
|
|
52
|
+
* the page entirely) while `document.hidden` still reads false.
|
|
53
|
+
*
|
|
54
|
+
* Optional for compatibility with injected clocks; absent means `hidden()`.
|
|
55
|
+
*/
|
|
56
|
+
suspended?(): boolean;
|
|
57
|
+
/** Why {@link suspended} is true, when the clock can say. */
|
|
58
|
+
suspensionReason?(): 'hidden' | 'unfocused' | 'page-suspended' | null;
|
|
49
59
|
/** Subscribe to visibility transitions; returns the unsubscribe. */
|
|
50
60
|
subscribe(onChange: () => void): () => void;
|
|
51
61
|
}
|
|
@@ -55,13 +65,46 @@ export interface VisibilityClock {
|
|
|
55
65
|
* honest answer for a headless caller — it has no tab to background. */
|
|
56
66
|
export function documentVisibilityClock(): VisibilityClock {
|
|
57
67
|
const doc = typeof document === 'undefined' ? null : document;
|
|
68
|
+
const win = doc?.defaultView ?? null;
|
|
69
|
+
let pageSuspended = false;
|
|
70
|
+
const reason = (): 'hidden' | 'unfocused' | 'page-suspended' | null => {
|
|
71
|
+
if (pageSuspended) return 'page-suspended';
|
|
72
|
+
if (doc?.hidden === true) return 'hidden';
|
|
73
|
+
// WebKit stops rAF when the browser window becomes inactive, including
|
|
74
|
+
// when another app is in front. Page Visibility can still say `visible`
|
|
75
|
+
// in that state. Focus is a platform fact, not a Safari/UA sniff, and in
|
|
76
|
+
// browsers that keep rendering while unfocused this merely parks the
|
|
77
|
+
// expiry clock until either a frame arrives or focus returns.
|
|
78
|
+
if (doc && typeof doc.hasFocus === 'function' && !doc.hasFocus()) return 'unfocused';
|
|
79
|
+
return null;
|
|
80
|
+
};
|
|
58
81
|
return {
|
|
59
82
|
now: () => (typeof performance === 'undefined' ? Date.now() : performance.now()),
|
|
60
83
|
hidden: () => doc?.hidden === true,
|
|
84
|
+
suspended: () => reason() !== null,
|
|
85
|
+
suspensionReason: reason,
|
|
61
86
|
subscribe(onChange) {
|
|
62
87
|
if (!doc) return () => {};
|
|
88
|
+
const onPageHide = () => {
|
|
89
|
+
pageSuspended = true;
|
|
90
|
+
onChange();
|
|
91
|
+
};
|
|
92
|
+
const onPageShow = () => {
|
|
93
|
+
pageSuspended = false;
|
|
94
|
+
onChange();
|
|
95
|
+
};
|
|
63
96
|
doc.addEventListener('visibilitychange', onChange);
|
|
64
|
-
|
|
97
|
+
win?.addEventListener('blur', onChange);
|
|
98
|
+
win?.addEventListener('focus', onChange);
|
|
99
|
+
win?.addEventListener('pagehide', onPageHide);
|
|
100
|
+
win?.addEventListener('pageshow', onPageShow);
|
|
101
|
+
return () => {
|
|
102
|
+
doc.removeEventListener('visibilitychange', onChange);
|
|
103
|
+
win?.removeEventListener('blur', onChange);
|
|
104
|
+
win?.removeEventListener('focus', onChange);
|
|
105
|
+
win?.removeEventListener('pagehide', onPageHide);
|
|
106
|
+
win?.removeEventListener('pageshow', onPageShow);
|
|
107
|
+
};
|
|
65
108
|
},
|
|
66
109
|
};
|
|
67
110
|
}
|
|
@@ -137,19 +180,26 @@ export interface VisibleCaptureWindow {
|
|
|
137
180
|
readonly budgetMs: number;
|
|
138
181
|
/** Visible milliseconds spent so far. */
|
|
139
182
|
elapsedVisibleMs(): number;
|
|
140
|
-
/** Milliseconds this window has spent
|
|
183
|
+
/** Milliseconds this window has spent browser-suspended (not counted). */
|
|
141
184
|
elapsedHiddenMs(): number;
|
|
142
|
-
/**
|
|
185
|
+
/**
|
|
186
|
+
* Whether the document itself is hidden. Kept distinct from browser
|
|
187
|
+
* suspension so status never calls an unfocused, still-visible window hidden.
|
|
188
|
+
*/
|
|
143
189
|
isHidden(): boolean;
|
|
190
|
+
/** Whether the expiry budget is parked because the browser is not presenting frames. */
|
|
191
|
+
isSuspended(): boolean;
|
|
192
|
+
/** The browser condition parking the budget, when observable. */
|
|
193
|
+
suspensionReason(): 'hidden' | 'unfocused' | 'page-suspended' | null;
|
|
144
194
|
/** Stop the timer and drop the visibility listener. Idempotent. */
|
|
145
195
|
cancel(): void;
|
|
146
196
|
}
|
|
147
197
|
|
|
148
198
|
/**
|
|
149
199
|
* Start a window that calls `onExpire` after `budgetMs` of VISIBLE time.
|
|
150
|
-
* While
|
|
151
|
-
* background timer cannot fire it late either) and re-armed
|
|
152
|
-
* budget when
|
|
200
|
+
* While browser frame presentation is suspended the timer is disarmed entirely
|
|
201
|
+
* (so a throttled background timer cannot fire it late either) and re-armed
|
|
202
|
+
* with the remaining budget when frame presentation resumes.
|
|
153
203
|
*/
|
|
154
204
|
export function startVisibleCaptureWindow(opts: {
|
|
155
205
|
budgetMs: number;
|
|
@@ -164,7 +214,8 @@ export function startVisibleCaptureWindow(opts: {
|
|
|
164
214
|
const setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
|
|
165
215
|
const clearTimer = opts.clearTimer ?? ((h) => clearTimeout(h as ReturnType<typeof setTimeout>));
|
|
166
216
|
|
|
167
|
-
|
|
217
|
+
const suspended = () => clock.suspended?.() ?? clock.hidden();
|
|
218
|
+
let state = beginVisibleBudget(opts.budgetMs, clock.now(), suspended());
|
|
168
219
|
let timer: unknown = null;
|
|
169
220
|
let done = false;
|
|
170
221
|
|
|
@@ -192,7 +243,7 @@ export function startVisibleCaptureWindow(opts: {
|
|
|
192
243
|
|
|
193
244
|
const unsubscribe = clock.subscribe(() => {
|
|
194
245
|
if (done) return;
|
|
195
|
-
state = applyVisibility(state, clock.now(),
|
|
246
|
+
state = applyVisibility(state, clock.now(), suspended());
|
|
196
247
|
arm();
|
|
197
248
|
});
|
|
198
249
|
|
|
@@ -202,7 +253,10 @@ export function startVisibleCaptureWindow(opts: {
|
|
|
202
253
|
budgetMs: opts.budgetMs,
|
|
203
254
|
elapsedVisibleMs: () => visibleElapsedMs(state, clock.now()),
|
|
204
255
|
elapsedHiddenMs: () => hiddenElapsedMs(state, clock.now()),
|
|
205
|
-
isHidden: () =>
|
|
256
|
+
isHidden: () => clock.hidden(),
|
|
257
|
+
isSuspended: () => state.hidden,
|
|
258
|
+
suspensionReason: () =>
|
|
259
|
+
state.hidden ? (clock.suspensionReason?.() ?? (clock.hidden() ? 'hidden' : null)) : null,
|
|
206
260
|
cancel() {
|
|
207
261
|
if (done) return;
|
|
208
262
|
done = true;
|