@vgai/engine 0.5.9 → 0.5.11

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.
@@ -107,6 +107,12 @@ export interface VgaiGameContract {
107
107
  * Read the declared contract, if any. The `contractVersion` gate is the
108
108
  * forward-compatibility hinge: a future v2 game on a v1 host is ignored
109
109
  * (pre-contract fallbacks apply) rather than half-interpreted.
110
+ *
111
+ * `scope` is the REALM to read from, and defaults to this module's own `window`
112
+ * — correct for a same-realm mount, where the game's modules and the host share
113
+ * one global. An iframe-hosted game declares on ITS realm's window, so the host
114
+ * passes `iframe.contentWindow`: the contract is a property of the realm the
115
+ * game runs in, never of the realm that happens to be asking.
110
116
  */
111
- export declare function readGameContract(): VgaiGameContract | null;
117
+ export declare function readGameContract(scope?: Window | null | undefined): VgaiGameContract | null;
112
118
  //# sourceMappingURL=game-contract.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"game-contract.d.ts","sourceRoot":"","sources":["../../../src/adapter/ingest/game-contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;;;;;;GASG;AACH,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,IAAI,IAAI,CAAC;IACf,KAAK,CAAC,IAAI,IAAI,CAAC;IACf,MAAM,CAAC,IAAI,IAAI,CAAC;CACjB;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+EAA+E;IAC/E,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACrD;AAED;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,YAAY,GAAG,UAAU,CAAC;IACjC,IAAI,IAAI,OAAO,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,eAAe;IAC9B,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,eAAe,EAAE,CAAC;IAC7B,mFAAmF;IACnF,KAAK,CAAC,EAAE,qBAAqB,EAAE,CAAC;CACjC;AAED,MAAM,WAAW,gBAAgB;IAC/B,8EAA8E;IAC9E,eAAe,EAAE,CAAC,CAAC;IACnB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,SAAS,CAAC,EAAE,iBAAiB,CAAC;IAC9B;;;;OAIG;IACH,OAAO,CAAC,EAAE,eAAe,CAAC;CAC3B;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,gBAAgB,GAAG,IAAI,CAI1D"}
1
+ {"version":3,"file":"game-contract.d.ts","sourceRoot":"","sources":["../../../src/adapter/ingest/game-contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;;;;;;GASG;AACH,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,IAAI,IAAI,CAAC;IACf,KAAK,CAAC,IAAI,IAAI,CAAC;IACf,MAAM,CAAC,IAAI,IAAI,CAAC;CACjB;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+EAA+E;IAC/E,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACrD;AAED;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,YAAY,GAAG,UAAU,CAAC;IACjC,IAAI,IAAI,OAAO,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,eAAe;IAC9B,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,eAAe,EAAE,CAAC;IAC7B,mFAAmF;IACnF,KAAK,CAAC,EAAE,qBAAqB,EAAE,CAAC;CACjC;AAED,MAAM,WAAW,gBAAgB;IAC/B,8EAA8E;IAC9E,eAAe,EAAE,CAAC,CAAC;IACnB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,SAAS,CAAC,EAAE,iBAAiB,CAAC;IAC9B;;;;OAIG;IACH,OAAO,CAAC,EAAE,eAAe,CAAC;CAC3B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,GAAE,MAAM,GAAG,IAAI,GAAG,SAAyD,GAC/E,gBAAgB,GAAG,IAAI,CAKzB"}
@@ -21,9 +21,16 @@
21
21
  * Read the declared contract, if any. The `contractVersion` gate is the
22
22
  * forward-compatibility hinge: a future v2 game on a v1 host is ignored
23
23
  * (pre-contract fallbacks apply) rather than half-interpreted.
24
+ *
25
+ * `scope` is the REALM to read from, and defaults to this module's own `window`
26
+ * — correct for a same-realm mount, where the game's modules and the host share
27
+ * one global. An iframe-hosted game declares on ITS realm's window, so the host
28
+ * passes `iframe.contentWindow`: the contract is a property of the realm the
29
+ * game runs in, never of the realm that happens to be asking.
24
30
  */
25
- export function readGameContract() {
26
- const declared = window.vgaiGame;
31
+ export function readGameContract(scope = typeof window === 'undefined' ? null : window) {
32
+ const declared = scope
33
+ ?.vgaiGame;
27
34
  if (!declared || declared.contractVersion !== 1)
28
35
  return null;
29
36
  return declared;
@@ -1 +1 @@
1
- {"version":3,"file":"setup-three-root-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/setup-three-root-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAmCrD,OAAO,EAAE,KAAK,mBAAmB,EAA2B,MAAM,4BAA4B,CAAC;AAO/F,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAe,WAAW,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAM7F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAEvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAGpE,sFAAsF;AACtF,MAAM,WAAW,oBAAoB;IACnC,gEAAgE;IAChE,KAAK,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAChC,wEAAwE;IACxE,aAAa,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IAC1C,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,+EAA+E;IAC/E,EAAE,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACzB;AAED;;;;GAIG;AACH,MAAM,WAAW,qBAAsB,SAAQ,gBAAgB;IAC7D;;;;;;;;OAQG;IACH,QAAQ,CAAC,UAAU,EAAE,IAAI,CAAC;IAC1B,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI,CAAC;IACzC,+FAA+F;IAC/F,sBAAsB,CAAC,IAAI,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACxD,kFAAkF;IAClF,SAAS,CAAC,QAAQ,EAAE,WAAW,EAAE,aAAa,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/E;;;;;;;;;OASG;IACH,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;CAChC;AAeD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,WAAW,CAErE;AAED,qBAAa,qBAAsB,YAAW,WAAW;IAE3C,OAAO,CAAC,QAAQ,CAAC,MAAM;IADnC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;gBACS,MAAM,GAAE,oBAAyB;IAKxD,KAAK,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,qBAAqB,CAAC;CA4zBpE"}
1
+ {"version":3,"file":"setup-three-root-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/setup-three-root-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAmCrD,OAAO,EAAE,KAAK,mBAAmB,EAA2B,MAAM,4BAA4B,CAAC;AAO/F,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAe,WAAW,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAM7F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAEvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAGpE,sFAAsF;AACtF,MAAM,WAAW,oBAAoB;IACnC,gEAAgE;IAChE,KAAK,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAChC,wEAAwE;IACxE,aAAa,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IAC1C,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,+EAA+E;IAC/E,EAAE,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACzB;AAED;;;;GAIG;AACH,MAAM,WAAW,qBAAsB,SAAQ,gBAAgB;IAC7D;;;;;;;;OAQG;IACH,QAAQ,CAAC,UAAU,EAAE,IAAI,CAAC;IAC1B,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI,CAAC;IACzC,+FAA+F;IAC/F,sBAAsB,CAAC,IAAI,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACxD,kFAAkF;IAClF,SAAS,CAAC,QAAQ,EAAE,WAAW,EAAE,aAAa,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/E;;;;;;;;;OASG;IACH,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;CAChC;AAeD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,WAAW,CAErE;AAED,qBAAa,qBAAsB,YAAW,WAAW;IAE3C,OAAO,CAAC,QAAQ,CAAC,MAAM;IADnC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;gBACS,MAAM,GAAE,oBAAyB;IAKxD,KAAK,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,qBAAqB,CAAC;CAu0BpE"}
@@ -497,7 +497,18 @@ export class SetupThreeRootAdapter {
497
497
  // dev session that enabled it at mount keeps it enabled.
498
498
  let renderVitals = null;
499
499
  const seedRenderVitals = () => {
500
- if (!host.game || headless || !devLayersEnabled())
500
+ // `editorPreview ? true : undefined`: an adapter holding an editor
501
+ // preview is being authored IN the editor — a dev context by
502
+ // definition, even when the editor SPA itself is a production build.
503
+ // The packaged runtime bakes `import.meta.env.DEV = false` into its
504
+ // prebuilt bundle, which is a fact about how the EDITOR was built, not
505
+ // about whether this mount is a dev session — found live: a
506
+ // package-native project's vitals never seeded while its dev menu,
507
+ // capabilities, and every project-graph dev layer ran fine. The
508
+ // override argument is dev-layers' own documented seam for exactly
509
+ // this caller-knows-better case; a standalone/ship mount (no
510
+ // editorPreview) still resolves purely from the build env.
511
+ if (!host.game || headless || !devLayersEnabled(editorPreview ? true : undefined))
501
512
  return;
502
513
  renderVitals?.dispose();
503
514
  host.game.profiler.enabled = true;
@@ -6,10 +6,19 @@
6
6
  * through `vgai eval`, and `game.providers()`/`game.commands()` to discover
7
7
  * them without being told they exist.
8
8
  *
9
- * Engine-owned and first-party, seeded by the three adapter
10
- * (`adapter/setup-three-root-adapter.ts`) at the same spot it seeds
11
- * `systemAdapters.renderDebug` a game writes nothing to get these, and there
12
- * is no capability to install. The measurement half is `dev/render-vitals.ts`
9
+ * Engine-owned and first-party, seeded by BOTH three-root mount paths — a game
10
+ * writes nothing to get these, and there is no capability to install:
11
+ * - `adapter/setup-three-root-adapter.ts` (the classic imperative
12
+ * `setup(ctx)` world), at the same spot it seeds
13
+ * `systemAdapters.renderDebug`;
14
+ * - `world3d-react/r3f-adapter.tsx` (a TSX/R3F world — the shape EVERY
15
+ * scaffolded project's three root has), right after fiber's first commit
16
+ * resolves the scene.
17
+ * Both is the whole point and not redundancy: they are two independent mounts
18
+ * onto the same `Game`, and a door wired into only one of them is a door half
19
+ * the engine's worlds never get (issue #1518 — the R3F half was dark
20
+ * everywhere, found in the field on a package-native project).
21
+ * The measurement half is `dev/render-vitals.ts`
13
22
  * (derived entirely from profiler frames); the address-book half is
14
23
  * `dev/render-census.ts` (pure walks over the live scene). This module is only
15
24
  * the wiring between them and the registry.
@@ -27,9 +36,11 @@
27
36
  * the drawing buffer halves was never fill-bound.
28
37
  *
29
38
  * ── GATING ──────────────────────────────────────────────────────────────────
30
- * The caller gates. `setup-three-root-adapter.ts` calls this only under
31
- * `devLayersEnabled()` (`runtime/dev-layers.ts` — the ONE owner of "is this a
32
- * dev context"), so a ship build registers nothing and pays nothing.
39
+ * The caller gates. Both seeders call this only under `devLayersEnabled()`
40
+ * (`runtime/dev-layers.ts` — the ONE owner of "is this a dev context"), so a
41
+ * ship build registers nothing and pays nothing. Each also requires a `Game`
42
+ * shell (there is no profiler to fold without one) and a non-headless mount
43
+ * (nothing brackets a submission, so the readings could only ever be empty).
33
44
  *
34
45
  * ── RESOURCE OWNERSHIP ──────────────────────────────────────────────────────
35
46
  * OWNER: {@link registerRenderVitals}'s caller. It allocates one
@@ -1 +1 @@
1
- {"version":3,"file":"register-render-vitals.d.ts","sourceRoot":"","sources":["../../src/dev/register-render-vitals.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAElE,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAOxE,wEAAwE;AACxE,eAAO,MAAM,sBAAsB,kBAAkB,CAAC;AACtD,eAAO,MAAM,qBAAqB,kBAAkB,CAAC;AACrD,eAAO,MAAM,uBAAuB,oBAAoB,CAAC;AACzD,eAAO,MAAM,qBAAqB,kBAAkB,CAAC;AACrD,eAAO,MAAM,kBAAkB,eAAe,CAAC;AAE/C;0EAC0E;AAC1E,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;IAC9C,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACpE,aAAa,IAAI,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1E;AAED,MAAM,WAAW,wBAAwB;IACvC,wEAAwE;IACxE,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,kEAAkE;IAClE,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IACjC;uEACmE;IACnE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC;IACvC,uCAAuC;IACvC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,QAAQ,CAAC;IAC/B;uCACmC;IACnC,QAAQ,CAAC,QAAQ,EAAE,oBAAoB,GAAG,IAAI,CAAC;CAChD;AAMD;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,wBAAwB,GAC7B,wBAAwB,GAAG,IAAI,CA2IjC"}
1
+ {"version":3,"file":"register-render-vitals.d.ts","sourceRoot":"","sources":["../../src/dev/register-render-vitals.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAElE,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAOxE,wEAAwE;AACxE,eAAO,MAAM,sBAAsB,kBAAkB,CAAC;AACtD,eAAO,MAAM,qBAAqB,kBAAkB,CAAC;AACrD,eAAO,MAAM,uBAAuB,oBAAoB,CAAC;AACzD,eAAO,MAAM,qBAAqB,kBAAkB,CAAC;AACrD,eAAO,MAAM,kBAAkB,eAAe,CAAC;AAE/C;0EAC0E;AAC1E,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;IAC9C,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACpE,aAAa,IAAI,MAAM,CAAC;IACxB,QAAQ,CAAC,UAAU,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1E;AAED,MAAM,WAAW,wBAAwB;IACvC,wEAAwE;IACxE,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,kEAAkE;IAClE,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IACjC;uEACmE;IACnE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC;IACvC,uCAAuC;IACvC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,QAAQ,CAAC;IAC/B;uCACmC;IACnC,QAAQ,CAAC,QAAQ,EAAE,oBAAoB,GAAG,IAAI,CAAC;CAChD;AAMD;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,wBAAwB,GAC7B,wBAAwB,GAAG,IAAI,CA8JjC"}
@@ -6,10 +6,19 @@
6
6
  * through `vgai eval`, and `game.providers()`/`game.commands()` to discover
7
7
  * them without being told they exist.
8
8
  *
9
- * Engine-owned and first-party, seeded by the three adapter
10
- * (`adapter/setup-three-root-adapter.ts`) at the same spot it seeds
11
- * `systemAdapters.renderDebug` a game writes nothing to get these, and there
12
- * is no capability to install. The measurement half is `dev/render-vitals.ts`
9
+ * Engine-owned and first-party, seeded by BOTH three-root mount paths — a game
10
+ * writes nothing to get these, and there is no capability to install:
11
+ * - `adapter/setup-three-root-adapter.ts` (the classic imperative
12
+ * `setup(ctx)` world), at the same spot it seeds
13
+ * `systemAdapters.renderDebug`;
14
+ * - `world3d-react/r3f-adapter.tsx` (a TSX/R3F world — the shape EVERY
15
+ * scaffolded project's three root has), right after fiber's first commit
16
+ * resolves the scene.
17
+ * Both is the whole point and not redundancy: they are two independent mounts
18
+ * onto the same `Game`, and a door wired into only one of them is a door half
19
+ * the engine's worlds never get (issue #1518 — the R3F half was dark
20
+ * everywhere, found in the field on a package-native project).
21
+ * The measurement half is `dev/render-vitals.ts`
13
22
  * (derived entirely from profiler frames); the address-book half is
14
23
  * `dev/render-census.ts` (pure walks over the live scene). This module is only
15
24
  * the wiring between them and the registry.
@@ -27,9 +36,11 @@
27
36
  * the drawing buffer halves was never fill-bound.
28
37
  *
29
38
  * ── GATING ──────────────────────────────────────────────────────────────────
30
- * The caller gates. `setup-three-root-adapter.ts` calls this only under
31
- * `devLayersEnabled()` (`runtime/dev-layers.ts` — the ONE owner of "is this a
32
- * dev context"), so a ship build registers nothing and pays nothing.
39
+ * The caller gates. Both seeders call this only under `devLayersEnabled()`
40
+ * (`runtime/dev-layers.ts` — the ONE owner of "is this a dev context"), so a
41
+ * ship build registers nothing and pays nothing. Each also requires a `Game`
42
+ * shell (there is no profiler to fold without one) and a non-headless mount
43
+ * (nothing brackets a submission, so the readings could only ever be empty).
33
44
  *
34
45
  * ── RESOURCE OWNERSHIP ──────────────────────────────────────────────────────
35
46
  * OWNER: {@link registerRenderVitals}'s caller. It allocates one
@@ -69,8 +80,24 @@ export function registerRenderVitals(deps) {
69
80
  const taken = registry.adapter
70
81
  .providers()
71
82
  .some((provider) => provider.name === RENDER_VITALS_PROVIDER);
72
- if (taken)
83
+ if (taken) {
84
+ // Loud, by name. This branch used to return `null` in silence, and a
85
+ // silent decline is indistinguishable from a door that was never seeded —
86
+ // which is precisely how issue #1518 stayed a mystery through a field
87
+ // session: every hypothesis about WHY the door was closed had to be
88
+ // guessed, because nothing on any path said anything. It costs one line to
89
+ // make the one legitimate decline explain itself, and the same line names
90
+ // the mount/teardown OVERLAP case (a re-mount seeding before the outgoing
91
+ // mount's `strip()` runs), where the second world would otherwise lose the
92
+ // door and no log would mention it.
93
+ console.warn(`[render-vitals] world "${worldId}" did not get the ${RENDER_VITALS_PROVIDER} door: ` +
94
+ 'another live world already owns it (the names are fixed, so the first three root to ' +
95
+ 'mount wins). Its draws still fold into the readings — the profiler is game-scoped — ' +
96
+ 'but render.census/families/toggle address the owning root. If this world IS the ' +
97
+ 'current one, the previous mount was torn down AFTER this one seeded and its ' +
98
+ 'strip() took the door with it.');
73
99
  return null;
100
+ }
74
101
  const vitals = createRenderVitals(profiler);
75
102
  const debug = registry.forRoot(worldId);
76
103
  debug.registerStateProvider(RENDER_VITALS_PROVIDER, () => vitals.read());
@@ -15,6 +15,15 @@ export type ResolvedAdapter = {
15
15
  readonly surface: 'three' | 'canvas' | 'dom';
16
16
  readonly strategy: IngestStrategy;
17
17
  readonly entryHtml: string | undefined;
18
+ /**
19
+ * Project-relative ES module declaring `window.vgaiGame` beside a
20
+ * pristine copy. OPTIONAL rather than required-and-possibly-undefined
21
+ * (the shape its siblings above use) because it is read by exactly one
22
+ * mount route: every other construction site of this type — the ingest
23
+ * routes' own fixtures included — has nothing to say about it, and
24
+ * requiring the key would only make them all say `undefined`.
25
+ */
26
+ readonly contractShim?: string | undefined;
18
27
  readonly assets: Record<string, string> | undefined;
19
28
  readonly domStubs: string[] | undefined;
20
29
  readonly captureTimeoutMs: number | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../../src/manifest/load.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../../src/manifest/load.ts"],"names":[],"mappings":"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"}
@@ -125,6 +125,7 @@ function resolveAdapter(root) {
125
125
  surface: adapter.surface,
126
126
  strategy: ingest.strategy,
127
127
  entryHtml: ingest.entryHtml,
128
+ contractShim: ingest.contractShim,
128
129
  assets: ingest.assets,
129
130
  domStubs: ingest.domStubs,
130
131
  captureTimeoutMs: ingest.captureTimeoutMs,
@@ -45,6 +45,7 @@ export declare const RootAdapterSchema: z.ZodPipe<z.ZodTransform<unknown, unknow
45
45
  "opaque-embed": "opaque-embed";
46
46
  }>;
47
47
  entryHtml: z.ZodOptional<z.ZodString>;
48
+ contractShim: z.ZodOptional<z.ZodString>;
48
49
  assets: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
49
50
  domStubs: z.ZodOptional<z.ZodArray<z.ZodString>>;
50
51
  captureTimeoutMs: z.ZodOptional<z.ZodNumber>;
@@ -85,6 +86,7 @@ export declare const AdapterRootSchema: z.ZodObject<{
85
86
  "opaque-embed": "opaque-embed";
86
87
  }>;
87
88
  entryHtml: z.ZodOptional<z.ZodString>;
89
+ contractShim: z.ZodOptional<z.ZodString>;
88
90
  assets: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
89
91
  domStubs: z.ZodOptional<z.ZodArray<z.ZodString>>;
90
92
  captureTimeoutMs: z.ZodOptional<z.ZodNumber>;
@@ -194,6 +196,7 @@ export declare const GameManifestSchema: z.ZodObject<{
194
196
  "opaque-embed": "opaque-embed";
195
197
  }>;
196
198
  entryHtml: z.ZodOptional<z.ZodString>;
199
+ contractShim: z.ZodOptional<z.ZodString>;
197
200
  assets: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
198
201
  domStubs: z.ZodOptional<z.ZodArray<z.ZodString>>;
199
202
  captureTimeoutMs: z.ZodOptional<z.ZodNumber>;
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/manifest/schema.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAG,CAAU,CAAC;AAMhD,eAAO,MAAM,oBAAoB;;;;;EAK/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAMlE,eAAO,MAAM,UAAU;;;;;;;EAOrB,CAAC;AACH,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAsF9C,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAkM3B,CAAC;AACJ,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAM5D,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAoEnB,CAAC;AACZ,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAkB5D,eAAO,MAAM,eAAe;;;;;EAMzB,CAAC;AACJ,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAExD,eAAO,MAAM,qBAAqB;;;;EAEa,CAAC;AAChD,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;kBAqC7B,CAAC;AACJ,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAMhE,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA+RyD,CAAC;AAEzF,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/manifest/schema.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAG,CAAU,CAAC;AAMhD,eAAO,MAAM,oBAAoB;;;;;EAK/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAMlE,eAAO,MAAM,UAAU;;;;;;;EAOrB,CAAC;AACH,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAsF9C,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAmP3B,CAAC;AACJ,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAM5D,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAoEnB,CAAC;AACZ,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAkB5D,eAAO,MAAM,eAAe;;;;;EAMzB,CAAC;AACJ,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAExD,eAAO,MAAM,qBAAqB;;;;EAEa,CAAC;AAChD,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;kBAqC7B,CAAC;AACJ,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAMhE,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA+RyD,CAAC;AAEzF,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC"}
@@ -151,6 +151,24 @@ export const RootAdapterSchema = z
151
151
  'introspection), for both canvas and three roots. Legal but NOT wired to a ' +
152
152
  'mount for iframe-reachable (a named error at resolve names this — use `bundleUrl` ' +
153
153
  'instead, the wired reachable route).'),
154
+ contractShim: z
155
+ .string()
156
+ .optional()
157
+ .describe("Project-relative path to a HOST-ADDED ES module that declares this game's " +
158
+ '`window.vgaiGame` contract (adapter/ingest/game-contract.ts) WITHOUT editing a ' +
159
+ 'vendored byte — the pristine-copy door. The host injects it as a ' +
160
+ '`<script type="module">` into the assembled iframe document AFTER the game`s own ' +
161
+ 'importmap (moving a module load ahead of an import map invalidates the map) and ' +
162
+ 'BEFORE the game`s entry module, so deferred module scripts run it first. ' +
163
+ 'TIMING CONTRACT, which a shim author must obey: because it runs before the ' +
164
+ 'game`s entry, it MUST assign `window.vgaiGame` SYNCHRONOUSLY at top level — the ' +
165
+ 'host waits for that assignment and fails the mount by name if it never arrives. ' +
166
+ 'Anything that needs the game`s own modules must be LAZY: a dynamic `import()` ' +
167
+ 'INSIDE each verb/provider closure, resolved at call time. Importing a game module ' +
168
+ 'eagerly from the shim would evaluate it ahead of the game`s own entry and reorder ' +
169
+ 'its module side effects. Read by the vendored/self-hosted three route only ' +
170
+ '(strategy `iframe-reachable` with `entryHtml`); declaring it elsewhere is a ' +
171
+ 'schema error rather than a silently dead field.'),
154
172
  assets: z
155
173
  .record(z.string(), z.string())
156
174
  .optional()
@@ -226,6 +244,32 @@ export const RootAdapterSchema = z
226
244
  })
227
245
  .describe('Ingest adapter configuration for an unmodified game (a repo-vendored game or your ' +
228
246
  'own external folder)')
247
+ // `contractShim` is injected into the assembled ENTRY DOCUMENT, so
248
+ // the ONLY route that can read it is the self-hosted one
249
+ // (`iframe-reachable` + `entryHtml`,
250
+ // ingest-self-hosted-three-adapter.ts). An `opaque-embed` entry
251
+ // document is hosted sandboxed with no host wiring, and a `bundleUrl`
252
+ // mount has no game document at all — under either, a declared shim
253
+ // would be a silently dead declaration, the exact failure the
254
+ // dead-field-under-wrong-variant checks below exist to prevent. Its
255
+ // own `.superRefine` rather than a branch inside the next one: they
256
+ // check unrelated invariants, and folding them together only made
257
+ // one function harder to read.
258
+ .superRefine((ingest, ctx) => {
259
+ if (ingest.contractShim === undefined ||
260
+ (ingest.strategy === 'iframe-reachable' && ingest.entryHtml !== undefined)) {
261
+ return;
262
+ }
263
+ ctx.addIssue({
264
+ code: z.ZodIssueCode.custom,
265
+ message: '`contractShim` is read ONLY by the self-hosted entry-document route, so it ' +
266
+ "requires strategy 'iframe-reachable' together with `entryHtml` (got strategy " +
267
+ `"${ingest.strategy}"${ingest.entryHtml === undefined ? ' and no `entryHtml`' : ''}) — ` +
268
+ "the host injects the shim module into the GAME'S OWN entry HTML " +
269
+ '(ingest-self-hosted-three-adapter.ts), and no other mount has such a document.',
270
+ path: ['contractShim'],
271
+ });
272
+ })
229
273
  .superRefine((ingest, ctx) => {
230
274
  // The six iframe-reachable-multi fields only make sense for the
231
275
  // iframe-reachable-multi mount — reject them under any other
@@ -1 +1 @@
1
- {"version":3,"file":"r3f-adapter.d.ts","sourceRoot":"","sources":["../../src/world3d-react/r3f-adapter.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AASH,OAAO,EAAiB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACtD,OAAO,KAAK,EAAoB,WAAW,EAAoB,MAAM,YAAY,CAAC;AAGlF,OAAO,EAA4B,KAAK,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAGvF,sEAAsE;AACtE,MAAM,WAAW,uBAAuB;IACtC,qEAAqE;IACrE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB;;uBAEmB;IACnB,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC;IAC5B;;;;0CAIsC;IACtC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAClD;;;iDAG6C;IAC7C,QAAQ,CAAC,QAAQ,CAAC,EAAE,mBAAmB,GAAG,SAAS,CAAC;CACrD;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,WAAW,CA2W9E"}
1
+ {"version":3,"file":"r3f-adapter.d.ts","sourceRoot":"","sources":["../../src/world3d-react/r3f-adapter.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AASH,OAAO,EAAiB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACtD,OAAO,KAAK,EAAoB,WAAW,EAAoB,MAAM,YAAY,CAAC;AAMlF,OAAO,EAA4B,KAAK,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAYvF,sEAAsE;AACtE,MAAM,WAAW,uBAAuB;IACtC,qEAAqE;IACrE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB;;uBAEmB;IACnB,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC;IAC5B;;;;0CAIsC;IACtC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAClD;;;iDAG6C;IAC7C,QAAQ,CAAC,QAAQ,CAAC,EAAE,mBAAmB,GAAG,SAAS,CAAC;CACrD;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,WAAW,CAqc9E"}
@@ -20,7 +20,10 @@
20
20
  */
21
21
  import { advance, createRoot, extend, events as pointerEvents, } from '@react-three/fiber';
22
22
  import { createElement } from 'react';
23
+ import { registerRenderVitals } from '../dev/register-render-vitals';
24
+ import { RENDER_SUBMIT_PHASE } from '../dev/render-vitals';
23
25
  import { WorldProvider } from '../react/world-state';
26
+ import { devLayersEnabled } from '../runtime/dev-layers';
24
27
  import { EngineBridge } from './engine-bridge';
25
28
  import { applyWorldRendererConfig } from './renderer-config';
26
29
  import { createR3FRootContext } from './world-context';
@@ -279,6 +282,52 @@ export function createR3FAdapter(options) {
279
282
  // `RootState` for precisely this reason, so reading it per frame is the
280
283
  // supported way to see the live root. It is a plain map read.
281
284
  const live = () => state.get();
285
+ // --- Live render vitals (issue #1504), for an R3F world -----------------
286
+ // The SAME door the classic adapter seeds
287
+ // (`../adapter/setup-three-root-adapter.ts`), seeded here for the same
288
+ // reason: a running game must be able to explain its own frame cost
289
+ // through the debug registry, with no capability to install and nothing
290
+ // for a game to write. It has to be seeded in BOTH adapters because they
291
+ // are two independent mount paths onto the same `Game` — and a TSX/R3F
292
+ // world is the only shape a scaffolded project's three root has, so a
293
+ // door wired only into the classic path is a door no scaffolded project
294
+ // ever gets (issue #1518: `game.state('render.vitals')` answered "no
295
+ // state provider registered" in the field while every other dev layer of
296
+ // the same world worked).
297
+ //
298
+ // The gate is the same three conditions, minus the one that cannot
299
+ // arise: `devLayersEnabled()` (`../runtime/dev-layers.ts`, the ONE owner
300
+ // of "is this a dev context" — a ship build registers nothing and pays
301
+ // nothing), and a `Game` shell (the readings are folded out of that
302
+ // game's profiler frames). "Not headless" needs no term here: a headless
303
+ // mount returned above, before fiber was ever configured.
304
+ //
305
+ // Two module instances, one answer: under the packaged editor this
306
+ // module is served out of the PROJECT's own Vite graph (see
307
+ // `packages/editor/vite-plugin-react-world-runtime.ts` — the packaged
308
+ // shell imports `resolveR3FEntryAdapter` through the project-rooted
309
+ // server precisely so fiber/react/three identity is the project's), so
310
+ // `import.meta.env.DEV` here is the project server's `true`, not the
311
+ // prebuilt shell's `false`.
312
+ //
313
+ // OWNER of the registration+fold: this mount, ended by `dispose()` below
314
+ // (the fold) and `runtime.dispose()`'s scoped `strip(id)` (the
315
+ // registrations) — the same split the classic adapter documents.
316
+ let renderVitals = null;
317
+ if (host.game && devLayersEnabled()) {
318
+ // Same auto-enable as the classic adapter: the readings are folded out
319
+ // of profiler frames, and the profiler is a flag the editor already
320
+ // sets on play — under the dev gate this is the same cost arriving a
321
+ // little earlier, not a new one.
322
+ host.game.profiler.enabled = true;
323
+ renderVitals = registerRenderVitals({
324
+ registry: runtime.debugRegistry,
325
+ worldId: id,
326
+ profiler: host.game.profiler,
327
+ scene: live().scene,
328
+ renderer: host.renderer,
329
+ });
330
+ }
282
331
  // The engine drives every `useFrame` through the mounted world's
283
332
  // `update(dt)` hook, never off a raw host-loop callback. That is the
284
333
  // whole pause story: `runFrameImpl` (`runtime/game.ts`) calls
@@ -350,7 +399,44 @@ export function createR3FAdapter(options) {
350
399
  // then fiber's advance (useFrame callbacks + the actual render see
351
400
  // the fresh state). Both are host-gated: neither runs while frozen.
352
401
  systems.run(dt);
353
- advance(elapsed, true, current);
402
+ const profiler = host.game?.profiler;
403
+ if (!profiler?.enabled) {
404
+ advance(elapsed, true, current);
405
+ return;
406
+ }
407
+ // Issue #1504's measurement, produced where the draw actually
408
+ // happens. `advance()` under `frameloop: 'never'` is what calls
409
+ // `gl.render(scene, camera)`, so this bracket — not a phase span
410
+ // inside `systems` — is the frame's CPU submission cost, and it is
411
+ // also what makes this frame a PRESENTATION as far as the vitals
412
+ // fold is concerned (`../dev/render-vitals.ts`: no bracket, no
413
+ // display frame). The profiler's phase clock is a stack, so
414
+ // bracketing here cannot truncate an enclosing phase.
415
+ const info = host.renderer.info;
416
+ const passesBefore = info?.render.frame ?? 0;
417
+ profiler.beginPhase();
418
+ try {
419
+ advance(elapsed, true, current);
420
+ }
421
+ finally {
422
+ profiler.endPhase(RENDER_SUBMIT_PHASE);
423
+ }
424
+ // Checked, not assumed — a duck-typed/design-time renderer supplies
425
+ // only what a mount needs, and a mount with no counters must report
426
+ // NOTHING rather than throw once per frame (the classic adapter's
427
+ // `postFrame` pays for the same assumption for the same reason).
428
+ // `gpuMs` is null: this path runs no GPU timer, and the profiler
429
+ // keeps the previous reading rather than inventing a zero.
430
+ if (info) {
431
+ profiler.reportRender({
432
+ gpuMs: null,
433
+ drawCalls: info.render.calls,
434
+ triangles: info.render.triangles,
435
+ geometries: info.memory.geometries,
436
+ textures: info.memory.textures,
437
+ renderPasses: info.render.frame - passesBefore,
438
+ });
439
+ }
354
440
  },
355
441
  resize(width, height) {
356
442
  live().setSize(width, height);
@@ -359,6 +445,13 @@ export function createR3FAdapter(options) {
359
445
  // Unmount FIRST so component effect cleanups run, then tear the
360
446
  // runtime down (scoped debug-registry strip).
361
447
  root.unmount();
448
+ // The vitals fold owns two `profiler.subscribe` registrations, and
449
+ // `runtime.dispose()`'s `strip(id)` removes the REGISTRATIONS only —
450
+ // this is the one path that ends the subscriptions, exactly as the
451
+ // classic adapter's teardown documents. Before the strip, so a
452
+ // subscription can never outlive the door it feeds.
453
+ renderVitals?.dispose();
454
+ renderVitals = null;
362
455
  runtime.dispose();
363
456
  restoreRendererConfig();
364
457
  },
@@ -35,6 +35,7 @@ import type * as THREE from 'three';
35
35
  import type { ThreeHostContext } from '../adapter';
36
36
  import type { SystemAdapters } from '../adapter/system-adapter';
37
37
  import { createSystemRunner } from '../core/system-runner';
38
+ import { type DebugRegistry } from '../runtime/debug-registry';
38
39
  import type { GameContext } from '../runtime/types';
39
40
  /** The classic adapter's default input-map path — the conventional location
40
41
  * every scaffolded project ships (`public/inputmaps/default.inputmap.json`). */
@@ -99,6 +100,14 @@ export interface R3FRootRuntime {
99
100
  /** The mounted world's `SystemAdapters` bag (`mounted.systems`) —
100
101
  * `ctx.registerSystemAdapter` writes into it; `debug` is pre-seeded. */
101
102
  readonly systemAdapters: SystemAdapters;
103
+ /** The registry every registration of this world's lands in — the game's
104
+ * own when a `Game` hosts it, a private mount-local one otherwise. Exposed
105
+ * (rather than re-derived by the caller) because `createR3FAdapter` seeds
106
+ * the render-vitals door onto THIS registry after fiber's first commit
107
+ * resolves the scene, and two `getDebugRegistry` calls that are "obviously"
108
+ * the same object is exactly the second source of truth the classic
109
+ * adapter's single `debugRegistry` local avoids. */
110
+ readonly debugRegistry: DebugRegistry;
102
111
  /** Resolves once the input map load settles (immediately when skipped).
103
112
  * Never rejects — a failed load reports loudly and resolves. */
104
113
  readonly inputMapReady: Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"world-context.d.ts","sourceRoot":"","sources":["../../src/world3d-react/world-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAgCK;AAEL,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AACpC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAOhE,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAQ3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD;iFACiF;AACjF,eAAO,MAAM,sBAAsB,qCAAqC,CAAC;AAEzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,MAAM,MAAM,cAAc,GAAG,IAAI,CAC/B,WAAW,EACT,OAAO,GACP,QAAQ,GACR,YAAY,GACZ,OAAO,GACP,QAAQ,GACR,SAAS,GACT,OAAO,GACP,OAAO,GACP,QAAQ,GACR,MAAM,GACN,OAAO,GACP,UAAU,GACV,uBAAuB,CAC1B,CAAC;AAEF,MAAM,WAAW,qBAAqB;IACpC;gFAC4E;IAC5E,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CACnD;AAED;+EAC+E;AAC/E,MAAM,WAAW,cAAc;IAC7B;6EACyE;IACzE,QAAQ,CAAC,GAAG,EAAE,cAAc,CAAC;IAC7B,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;IACxD;6EACyE;IACzE,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;IACxC;qEACiE;IACjE,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC;;;4DAGwD;IACxD,cAAc,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC;IAC/D;;gFAE4E;IAC5E,OAAO,IAAI,IAAI,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,gBAAgB,EACtB,OAAO,EAAE,qBAAqB,GAC7B,cAAc,CAqKhB"}
1
+ {"version":3,"file":"world-context.d.ts","sourceRoot":"","sources":["../../src/world3d-react/world-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAgCK;AAEL,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AACpC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAOhE,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,EAEL,KAAK,aAAa,EAEnB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD;iFACiF;AACjF,eAAO,MAAM,sBAAsB,qCAAqC,CAAC;AAEzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,MAAM,MAAM,cAAc,GAAG,IAAI,CAC/B,WAAW,EACT,OAAO,GACP,QAAQ,GACR,YAAY,GACZ,OAAO,GACP,QAAQ,GACR,SAAS,GACT,OAAO,GACP,OAAO,GACP,QAAQ,GACR,MAAM,GACN,OAAO,GACP,UAAU,GACV,uBAAuB,CAC1B,CAAC;AAEF,MAAM,WAAW,qBAAqB;IACpC;gFAC4E;IAC5E,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CACnD;AAED;+EAC+E;AAC/E,MAAM,WAAW,cAAc;IAC7B;6EACyE;IACzE,QAAQ,CAAC,GAAG,EAAE,cAAc,CAAC;IAC7B,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;IACxD;6EACyE;IACzE,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;IACxC;;;;;;yDAMqD;IACrD,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;IACtC;qEACiE;IACjE,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC;;;4DAGwD;IACxD,cAAc,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC;IAC/D;;gFAE4E;IAC5E,OAAO,IAAI,IAAI,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,gBAAgB,EACtB,OAAO,EAAE,qBAAqB,GAC7B,cAAc,CAsKhB"}
@@ -164,6 +164,7 @@ export function createR3FRootContext(host, options) {
164
164
  ctx,
165
165
  systems,
166
166
  systemAdapters,
167
+ debugRegistry,
167
168
  inputMapReady,
168
169
  setSceneCamera(scene, camera) {
169
170
  const sameScene = ctxRaw['scene'] === scene && sceneIndex !== null;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@vgai/engine",
3
3
  "author": "Volter AI, Inc.",
4
4
  "license": "Apache-2.0",
5
- "version": "0.5.9",
5
+ "version": "0.5.11",
6
6
  "description": "Readable TypeScript game engine and universal host for Three.js, PixiJS, and React games.",
7
7
  "keywords": [
8
8
  "game-engine",
@@ -15,7 +15,7 @@
15
15
  ],
16
16
  "notes": "Grepped packages/engine/src for ccd/CCD/setCcdEnabled: zero matches. The engine constructs no rigid bodies itself — games build their own descs via ctx.RAPIER — and nothing engine-side ever enables ccd. Workaround: keep dynamic-body per-step travel small relative to the thinnest collider, or add intermediate colliders; a game may also call setCcdEnabled on its own bodies directly.",
17
17
  "evidenceHashes": {
18
- "packages/engine/src/adapter/setup-three-root-adapter.ts": "b55b5aa1b032a813ddeaa1dedb6488326ed4cf3497e8cf76a07bada1e40243ea"
18
+ "packages/engine/src/adapter/setup-three-root-adapter.ts": "f1b599b9fd25dfacd1c155ce83418734c984ac843d7173f573947fceb860cf51"
19
19
  }
20
20
  },
21
21
  {
@@ -73,7 +73,7 @@
73
73
  "notes": "Track P shipped 2026-07-03; proven: bubbo-bubbo/puzzling-potions/flappy-pixi. Tiers/limits: ARCHITECTURE-CORE.md.",
74
74
  "evidenceHashes": {
75
75
  "vendor/games/verify-unaltered.mjs": "df875b09d698b3cfc03443ff3dba4929f5f08ea8913009d4fae253d478282312",
76
- "packages/engine/src/manifest/schema.ts": "63d3f6fdbe3e9323ecbd4910c540d8a77e57d95e9044016424b46ca15a5c0bea"
76
+ "packages/engine/src/manifest/schema.ts": "d689135c4702e09be1de6a66c8625e64be10ca80deb988f30430678fa77b88db"
77
77
  }
78
78
  },
79
79
  {
@@ -114,6 +114,10 @@
114
114
  "description": "Entry HTML path, project-relative. WIRED for opaque-embed (D-W2): the file`s bytes are read verbatim through the project-static route and hosted in a sandboxed iframe (embed-only, no scene introspection), for both canvas and three roots. Legal but NOT wired to a mount for iframe-reachable (a named error at resolve names this — use `bundleUrl` instead, the wired reachable route).",
115
115
  "type": "string"
116
116
  },
117
+ "contractShim": {
118
+ "description": "Project-relative path to a HOST-ADDED ES module that declares this game's `window.vgaiGame` contract (adapter/ingest/game-contract.ts) WITHOUT editing a vendored byte — the pristine-copy door. The host injects it as a `<script type=\"module\">` into the assembled iframe document AFTER the game`s own importmap (moving a module load ahead of an import map invalidates the map) and BEFORE the game`s entry module, so deferred module scripts run it first. TIMING CONTRACT, which a shim author must obey: because it runs before the game`s entry, it MUST assign `window.vgaiGame` SYNCHRONOUSLY at top level — the host waits for that assignment and fails the mount by name if it never arrives. Anything that needs the game`s own modules must be LAZY: a dynamic `import()` INSIDE each verb/provider closure, resolved at call time. Importing a game module eagerly from the shim would evaluate it ahead of the game`s own entry and reorder its module side effects. Read by the vendored/self-hosted three route only (strategy `iframe-reachable` with `entryHtml`); declaring it elsewhere is a schema error rather than a silently dead field.",
119
+ "type": "string"
120
+ },
117
121
  "assets": {
118
122
  "description": "Path-substring -> served-URL rewrites for this ingested game (IngestGame.assets today)",
119
123
  "type": "object",
@@ -113,9 +113,18 @@ export interface VgaiGameContract {
113
113
  * Read the declared contract, if any. The `contractVersion` gate is the
114
114
  * forward-compatibility hinge: a future v2 game on a v1 host is ignored
115
115
  * (pre-contract fallbacks apply) rather than half-interpreted.
116
+ *
117
+ * `scope` is the REALM to read from, and defaults to this module's own `window`
118
+ * — correct for a same-realm mount, where the game's modules and the host share
119
+ * one global. An iframe-hosted game declares on ITS realm's window, so the host
120
+ * passes `iframe.contentWindow`: the contract is a property of the realm the
121
+ * game runs in, never of the realm that happens to be asking.
116
122
  */
117
- export function readGameContract(): VgaiGameContract | null {
118
- const declared = (window as unknown as { vgaiGame?: VgaiGameContract }).vgaiGame;
123
+ export function readGameContract(
124
+ scope: Window | null | undefined = typeof window === 'undefined' ? null : window,
125
+ ): VgaiGameContract | null {
126
+ const declared = (scope as unknown as { vgaiGame?: VgaiGameContract } | null | undefined)
127
+ ?.vgaiGame;
119
128
  if (!declared || declared.contractVersion !== 1) return null;
120
129
  return declared;
121
130
  }
@@ -604,7 +604,18 @@ export class SetupThreeRootAdapter implements RootAdapter {
604
604
  // dev session that enabled it at mount keeps it enabled.
605
605
  let renderVitals: RenderVitalsRegistration | null = null;
606
606
  const seedRenderVitals = (): void => {
607
- if (!host.game || headless || !devLayersEnabled()) return;
607
+ // `editorPreview ? true : undefined`: an adapter holding an editor
608
+ // preview is being authored IN the editor — a dev context by
609
+ // definition, even when the editor SPA itself is a production build.
610
+ // The packaged runtime bakes `import.meta.env.DEV = false` into its
611
+ // prebuilt bundle, which is a fact about how the EDITOR was built, not
612
+ // about whether this mount is a dev session — found live: a
613
+ // package-native project's vitals never seeded while its dev menu,
614
+ // capabilities, and every project-graph dev layer ran fine. The
615
+ // override argument is dev-layers' own documented seam for exactly
616
+ // this caller-knows-better case; a standalone/ship mount (no
617
+ // editorPreview) still resolves purely from the build env.
618
+ if (!host.game || headless || !devLayersEnabled(editorPreview ? true : undefined)) return;
608
619
  renderVitals?.dispose();
609
620
  host.game.profiler.enabled = true;
610
621
  renderVitals = registerRenderVitals({
@@ -6,10 +6,19 @@
6
6
  * through `vgai eval`, and `game.providers()`/`game.commands()` to discover
7
7
  * them without being told they exist.
8
8
  *
9
- * Engine-owned and first-party, seeded by the three adapter
10
- * (`adapter/setup-three-root-adapter.ts`) at the same spot it seeds
11
- * `systemAdapters.renderDebug` a game writes nothing to get these, and there
12
- * is no capability to install. The measurement half is `dev/render-vitals.ts`
9
+ * Engine-owned and first-party, seeded by BOTH three-root mount paths — a game
10
+ * writes nothing to get these, and there is no capability to install:
11
+ * - `adapter/setup-three-root-adapter.ts` (the classic imperative
12
+ * `setup(ctx)` world), at the same spot it seeds
13
+ * `systemAdapters.renderDebug`;
14
+ * - `world3d-react/r3f-adapter.tsx` (a TSX/R3F world — the shape EVERY
15
+ * scaffolded project's three root has), right after fiber's first commit
16
+ * resolves the scene.
17
+ * Both is the whole point and not redundancy: they are two independent mounts
18
+ * onto the same `Game`, and a door wired into only one of them is a door half
19
+ * the engine's worlds never get (issue #1518 — the R3F half was dark
20
+ * everywhere, found in the field on a package-native project).
21
+ * The measurement half is `dev/render-vitals.ts`
13
22
  * (derived entirely from profiler frames); the address-book half is
14
23
  * `dev/render-census.ts` (pure walks over the live scene). This module is only
15
24
  * the wiring between them and the registry.
@@ -27,9 +36,11 @@
27
36
  * the drawing buffer halves was never fill-bound.
28
37
  *
29
38
  * ── GATING ──────────────────────────────────────────────────────────────────
30
- * The caller gates. `setup-three-root-adapter.ts` calls this only under
31
- * `devLayersEnabled()` (`runtime/dev-layers.ts` — the ONE owner of "is this a
32
- * dev context"), so a ship build registers nothing and pays nothing.
39
+ * The caller gates. Both seeders call this only under `devLayersEnabled()`
40
+ * (`runtime/dev-layers.ts` — the ONE owner of "is this a dev context"), so a
41
+ * ship build registers nothing and pays nothing. Each also requires a `Game`
42
+ * shell (there is no profiler to fold without one) and a non-headless mount
43
+ * (nothing brackets a submission, so the readings could only ever be empty).
33
44
  *
34
45
  * ── RESOURCE OWNERSHIP ──────────────────────────────────────────────────────
35
46
  * OWNER: {@link registerRenderVitals}'s caller. It allocates one
@@ -112,7 +123,26 @@ export function registerRenderVitals(
112
123
  const taken = registry.adapter
113
124
  .providers()
114
125
  .some((provider) => provider.name === RENDER_VITALS_PROVIDER);
115
- if (taken) return null;
126
+ if (taken) {
127
+ // Loud, by name. This branch used to return `null` in silence, and a
128
+ // silent decline is indistinguishable from a door that was never seeded —
129
+ // which is precisely how issue #1518 stayed a mystery through a field
130
+ // session: every hypothesis about WHY the door was closed had to be
131
+ // guessed, because nothing on any path said anything. It costs one line to
132
+ // make the one legitimate decline explain itself, and the same line names
133
+ // the mount/teardown OVERLAP case (a re-mount seeding before the outgoing
134
+ // mount's `strip()` runs), where the second world would otherwise lose the
135
+ // door and no log would mention it.
136
+ console.warn(
137
+ `[render-vitals] world "${worldId}" did not get the ${RENDER_VITALS_PROVIDER} door: ` +
138
+ 'another live world already owns it (the names are fixed, so the first three root to ' +
139
+ 'mount wins). Its draws still fold into the readings — the profiler is game-scoped — ' +
140
+ 'but render.census/families/toggle address the owning root. If this world IS the ' +
141
+ 'current one, the previous mount was torn down AFTER this one seeded and its ' +
142
+ 'strip() took the door with it.',
143
+ );
144
+ return null;
145
+ }
116
146
 
117
147
  const vitals = createRenderVitals(profiler);
118
148
  const debug = registry.forRoot(worldId);
@@ -57,6 +57,15 @@ export type ResolvedAdapter =
57
57
  readonly surface: 'three' | 'canvas' | 'dom';
58
58
  readonly strategy: IngestStrategy;
59
59
  readonly entryHtml: string | undefined;
60
+ /**
61
+ * Project-relative ES module declaring `window.vgaiGame` beside a
62
+ * pristine copy. OPTIONAL rather than required-and-possibly-undefined
63
+ * (the shape its siblings above use) because it is read by exactly one
64
+ * mount route: every other construction site of this type — the ingest
65
+ * routes' own fixtures included — has nothing to say about it, and
66
+ * requiring the key would only make them all say `undefined`.
67
+ */
68
+ readonly contractShim?: string | undefined;
60
69
  readonly assets: Record<string, string> | undefined;
61
70
  readonly domStubs: string[] | undefined;
62
71
  readonly captureTimeoutMs: number | undefined;
@@ -274,6 +283,7 @@ function resolveAdapter(root: AdapterRoot): ResolvedAdapter {
274
283
  surface: adapter.surface,
275
284
  strategy: ingest.strategy,
276
285
  entryHtml: ingest.entryHtml,
286
+ contractShim: ingest.contractShim,
277
287
  assets: ingest.assets,
278
288
  domStubs: ingest.domStubs,
279
289
  captureTimeoutMs: ingest.captureTimeoutMs,
@@ -174,6 +174,26 @@ export const RootAdapterSchema = z
174
174
  'mount for iframe-reachable (a named error at resolve names this — use `bundleUrl` ' +
175
175
  'instead, the wired reachable route).',
176
176
  ),
177
+ contractShim: z
178
+ .string()
179
+ .optional()
180
+ .describe(
181
+ "Project-relative path to a HOST-ADDED ES module that declares this game's " +
182
+ '`window.vgaiGame` contract (adapter/ingest/game-contract.ts) WITHOUT editing a ' +
183
+ 'vendored byte — the pristine-copy door. The host injects it as a ' +
184
+ '`<script type="module">` into the assembled iframe document AFTER the game`s own ' +
185
+ 'importmap (moving a module load ahead of an import map invalidates the map) and ' +
186
+ 'BEFORE the game`s entry module, so deferred module scripts run it first. ' +
187
+ 'TIMING CONTRACT, which a shim author must obey: because it runs before the ' +
188
+ 'game`s entry, it MUST assign `window.vgaiGame` SYNCHRONOUSLY at top level — the ' +
189
+ 'host waits for that assignment and fails the mount by name if it never arrives. ' +
190
+ 'Anything that needs the game`s own modules must be LAZY: a dynamic `import()` ' +
191
+ 'INSIDE each verb/provider closure, resolved at call time. Importing a game module ' +
192
+ 'eagerly from the shim would evaluate it ahead of the game`s own entry and reorder ' +
193
+ 'its module side effects. Read by the vendored/self-hosted three route only ' +
194
+ '(strategy `iframe-reachable` with `entryHtml`); declaring it elsewhere is a ' +
195
+ 'schema error rather than a silently dead field.',
196
+ ),
177
197
  assets: z
178
198
  .record(z.string(), z.string())
179
199
  .optional()
@@ -269,6 +289,35 @@ export const RootAdapterSchema = z
269
289
  'Ingest adapter configuration for an unmodified game (a repo-vendored game or your ' +
270
290
  'own external folder)',
271
291
  )
292
+ // `contractShim` is injected into the assembled ENTRY DOCUMENT, so
293
+ // the ONLY route that can read it is the self-hosted one
294
+ // (`iframe-reachable` + `entryHtml`,
295
+ // ingest-self-hosted-three-adapter.ts). An `opaque-embed` entry
296
+ // document is hosted sandboxed with no host wiring, and a `bundleUrl`
297
+ // mount has no game document at all — under either, a declared shim
298
+ // would be a silently dead declaration, the exact failure the
299
+ // dead-field-under-wrong-variant checks below exist to prevent. Its
300
+ // own `.superRefine` rather than a branch inside the next one: they
301
+ // check unrelated invariants, and folding them together only made
302
+ // one function harder to read.
303
+ .superRefine((ingest, ctx) => {
304
+ if (
305
+ ingest.contractShim === undefined ||
306
+ (ingest.strategy === 'iframe-reachable' && ingest.entryHtml !== undefined)
307
+ ) {
308
+ return;
309
+ }
310
+ ctx.addIssue({
311
+ code: z.ZodIssueCode.custom,
312
+ message:
313
+ '`contractShim` is read ONLY by the self-hosted entry-document route, so it ' +
314
+ "requires strategy 'iframe-reachable' together with `entryHtml` (got strategy " +
315
+ `"${ingest.strategy}"${ingest.entryHtml === undefined ? ' and no `entryHtml`' : ''}) — ` +
316
+ "the host injects the shim module into the GAME'S OWN entry HTML " +
317
+ '(ingest-self-hosted-three-adapter.ts), and no other mount has such a document.',
318
+ path: ['contractShim'],
319
+ });
320
+ })
272
321
  .superRefine((ingest, ctx) => {
273
322
  // The six iframe-reachable-multi fields only make sense for the
274
323
  // iframe-reachable-multi mount — reject them under any other
@@ -28,11 +28,23 @@ import {
28
28
  } from '@react-three/fiber';
29
29
  import { createElement, type ReactNode } from 'react';
30
30
  import type { MountedThreeRoot, RootAdapter, ThreeHostContext } from '../adapter';
31
+ import { type RenderVitalsRegistration, registerRenderVitals } from '../dev/register-render-vitals';
32
+ import { RENDER_SUBMIT_PHASE } from '../dev/render-vitals';
31
33
  import { WorldProvider } from '../react/world-state';
34
+ import { devLayersEnabled } from '../runtime/dev-layers';
32
35
  import { EngineBridge, type EngineBridgeValue } from './engine-bridge';
33
36
  import { applyWorldRendererConfig, type WorldRendererConfig } from './renderer-config';
34
37
  import { createR3FRootContext } from './world-context';
35
38
 
39
+ /** The slice of `WebGLRenderer.info` the vitals reporter reads. Declared
40
+ * structurally rather than imported from `three`, per this module's own
41
+ * identity rule (see the header): a duck-typed host renderer legitimately has
42
+ * no `info` at all, which is why every read of it is guarded. */
43
+ interface RendererCounters {
44
+ readonly render: { readonly calls: number; readonly triangles: number; readonly frame: number };
45
+ readonly memory: { readonly geometries: number; readonly textures: number };
46
+ }
47
+
36
48
  /** What {@link createR3FAdapter} needs to build one `RootAdapter`. */
37
49
  export interface CreateR3FAdapterOptions {
38
50
  /** Stable id (telemetry/registry/conformance) — `RootAdapter.id`. */
@@ -336,6 +348,53 @@ export function createR3FAdapter(options: CreateR3FAdapterOptions): RootAdapter
336
348
  // supported way to see the live root. It is a plain map read.
337
349
  const live = (): RootState => state.get();
338
350
 
351
+ // --- Live render vitals (issue #1504), for an R3F world -----------------
352
+ // The SAME door the classic adapter seeds
353
+ // (`../adapter/setup-three-root-adapter.ts`), seeded here for the same
354
+ // reason: a running game must be able to explain its own frame cost
355
+ // through the debug registry, with no capability to install and nothing
356
+ // for a game to write. It has to be seeded in BOTH adapters because they
357
+ // are two independent mount paths onto the same `Game` — and a TSX/R3F
358
+ // world is the only shape a scaffolded project's three root has, so a
359
+ // door wired only into the classic path is a door no scaffolded project
360
+ // ever gets (issue #1518: `game.state('render.vitals')` answered "no
361
+ // state provider registered" in the field while every other dev layer of
362
+ // the same world worked).
363
+ //
364
+ // The gate is the same three conditions, minus the one that cannot
365
+ // arise: `devLayersEnabled()` (`../runtime/dev-layers.ts`, the ONE owner
366
+ // of "is this a dev context" — a ship build registers nothing and pays
367
+ // nothing), and a `Game` shell (the readings are folded out of that
368
+ // game's profiler frames). "Not headless" needs no term here: a headless
369
+ // mount returned above, before fiber was ever configured.
370
+ //
371
+ // Two module instances, one answer: under the packaged editor this
372
+ // module is served out of the PROJECT's own Vite graph (see
373
+ // `packages/editor/vite-plugin-react-world-runtime.ts` — the packaged
374
+ // shell imports `resolveR3FEntryAdapter` through the project-rooted
375
+ // server precisely so fiber/react/three identity is the project's), so
376
+ // `import.meta.env.DEV` here is the project server's `true`, not the
377
+ // prebuilt shell's `false`.
378
+ //
379
+ // OWNER of the registration+fold: this mount, ended by `dispose()` below
380
+ // (the fold) and `runtime.dispose()`'s scoped `strip(id)` (the
381
+ // registrations) — the same split the classic adapter documents.
382
+ let renderVitals: RenderVitalsRegistration | null = null;
383
+ if (host.game && devLayersEnabled()) {
384
+ // Same auto-enable as the classic adapter: the readings are folded out
385
+ // of profiler frames, and the profiler is a flag the editor already
386
+ // sets on play — under the dev gate this is the same cost arriving a
387
+ // little earlier, not a new one.
388
+ host.game.profiler.enabled = true;
389
+ renderVitals = registerRenderVitals({
390
+ registry: runtime.debugRegistry,
391
+ worldId: id,
392
+ profiler: host.game.profiler,
393
+ scene: live().scene,
394
+ renderer: host.renderer,
395
+ });
396
+ }
397
+
339
398
  // The engine drives every `useFrame` through the mounted world's
340
399
  // `update(dt)` hook, never off a raw host-loop callback. That is the
341
400
  // whole pause story: `runFrameImpl` (`runtime/game.ts`) calls
@@ -408,7 +467,43 @@ export function createR3FAdapter(options: CreateR3FAdapterOptions): RootAdapter
408
467
  // then fiber's advance (useFrame callbacks + the actual render see
409
468
  // the fresh state). Both are host-gated: neither runs while frozen.
410
469
  systems.run(dt);
411
- advance(elapsed, true, current);
470
+ const profiler = host.game?.profiler;
471
+ if (!profiler?.enabled) {
472
+ advance(elapsed, true, current);
473
+ return;
474
+ }
475
+ // Issue #1504's measurement, produced where the draw actually
476
+ // happens. `advance()` under `frameloop: 'never'` is what calls
477
+ // `gl.render(scene, camera)`, so this bracket — not a phase span
478
+ // inside `systems` — is the frame's CPU submission cost, and it is
479
+ // also what makes this frame a PRESENTATION as far as the vitals
480
+ // fold is concerned (`../dev/render-vitals.ts`: no bracket, no
481
+ // display frame). The profiler's phase clock is a stack, so
482
+ // bracketing here cannot truncate an enclosing phase.
483
+ const info = host.renderer.info as RendererCounters | undefined;
484
+ const passesBefore = info?.render.frame ?? 0;
485
+ profiler.beginPhase();
486
+ try {
487
+ advance(elapsed, true, current);
488
+ } finally {
489
+ profiler.endPhase(RENDER_SUBMIT_PHASE);
490
+ }
491
+ // Checked, not assumed — a duck-typed/design-time renderer supplies
492
+ // only what a mount needs, and a mount with no counters must report
493
+ // NOTHING rather than throw once per frame (the classic adapter's
494
+ // `postFrame` pays for the same assumption for the same reason).
495
+ // `gpuMs` is null: this path runs no GPU timer, and the profiler
496
+ // keeps the previous reading rather than inventing a zero.
497
+ if (info) {
498
+ profiler.reportRender({
499
+ gpuMs: null,
500
+ drawCalls: info.render.calls,
501
+ triangles: info.render.triangles,
502
+ geometries: info.memory.geometries,
503
+ textures: info.memory.textures,
504
+ renderPasses: info.render.frame - passesBefore,
505
+ });
506
+ }
412
507
  },
413
508
  resize(width: number, height: number): void {
414
509
  live().setSize(width, height);
@@ -417,6 +512,13 @@ export function createR3FAdapter(options: CreateR3FAdapterOptions): RootAdapter
417
512
  // Unmount FIRST so component effect cleanups run, then tear the
418
513
  // runtime down (scoped debug-registry strip).
419
514
  root.unmount();
515
+ // The vitals fold owns two `profiler.subscribe` registrations, and
516
+ // `runtime.dispose()`'s `strip(id)` removes the REGISTRATIONS only —
517
+ // this is the one path that ends the subscriptions, exactly as the
518
+ // classic adapter's teardown documents. Before the strip, so a
519
+ // subscription can never outlive the door it feeds.
520
+ renderVitals?.dispose();
521
+ renderVitals = null;
420
522
  runtime.dispose();
421
523
  restoreRendererConfig();
422
524
  },
@@ -132,6 +132,14 @@ export interface R3FRootRuntime {
132
132
  /** The mounted world's `SystemAdapters` bag (`mounted.systems`) —
133
133
  * `ctx.registerSystemAdapter` writes into it; `debug` is pre-seeded. */
134
134
  readonly systemAdapters: SystemAdapters;
135
+ /** The registry every registration of this world's lands in — the game's
136
+ * own when a `Game` hosts it, a private mount-local one otherwise. Exposed
137
+ * (rather than re-derived by the caller) because `createR3FAdapter` seeds
138
+ * the render-vitals door onto THIS registry after fiber's first commit
139
+ * resolves the scene, and two `getDebugRegistry` calls that are "obviously"
140
+ * the same object is exactly the second source of truth the classic
141
+ * adapter's single `debugRegistry` local avoids. */
142
+ readonly debugRegistry: DebugRegistry;
135
143
  /** Resolves once the input map load settles (immediately when skipped).
136
144
  * Never rejects — a failed load reports loudly and resolves. */
137
145
  readonly inputMapReady: Promise<void>;
@@ -288,6 +296,7 @@ export function createR3FRootContext(
288
296
  ctx,
289
297
  systems,
290
298
  systemAdapters,
299
+ debugRegistry,
291
300
  inputMapReady,
292
301
  setSceneCamera(scene, camera): void {
293
302
  const sameScene = ctxRaw['scene'] === scene && sceneIndex !== null;