@vgai/engine 0.5.8 → 0.5.10
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/ingest/contract-debug-adapter.d.ts +35 -0
- package/dist/adapter/ingest/contract-debug-adapter.d.ts.map +1 -0
- package/dist/adapter/ingest/contract-debug-adapter.js +90 -0
- package/dist/adapter/ingest/game-contract.d.ts +60 -1
- package/dist/adapter/ingest/game-contract.d.ts.map +1 -1
- package/dist/adapter/ingest/game-contract.js +1 -1
- package/dist/adapter/setup-three-root-adapter.d.ts.map +1 -1
- package/dist/adapter/setup-three-root-adapter.js +105 -2
- package/dist/dev/performance-profiler.d.ts +13 -7
- package/dist/dev/performance-profiler.d.ts.map +1 -1
- package/dist/dev/performance-profiler.js +31 -3
- package/dist/dev/register-render-vitals.d.ts +95 -0
- package/dist/dev/register-render-vitals.d.ts.map +1 -0
- package/dist/dev/register-render-vitals.js +182 -0
- package/dist/dev/render-census.d.ts +135 -0
- package/dist/dev/render-census.d.ts.map +1 -0
- package/dist/dev/render-census.js +257 -0
- package/dist/dev/render-vitals.d.ts +181 -0
- package/dist/dev/render-vitals.d.ts.map +1 -0
- package/dist/dev/render-vitals.js +232 -0
- package/dist/dev/static-batch-advisor.d.ts +106 -0
- package/dist/dev/static-batch-advisor.d.ts.map +1 -0
- package/dist/dev/static-batch-advisor.js +141 -0
- package/dist/render/render-batch-system.d.ts.map +1 -1
- package/dist/render/render-batch-system.js +7 -18
- package/dist/render/structural-signature.d.ts +148 -0
- package/dist/render/structural-signature.d.ts.map +1 -0
- package/dist/render/structural-signature.js +193 -0
- package/dist/runtime/dev-layers.d.ts.map +1 -1
- package/dist/runtime/dev-layers.js +6 -0
- package/package.json +1 -1
- package/schemas/engine-capabilities.json +5 -5
- package/src/adapter/ingest/contract-debug-adapter.ts +110 -0
- package/src/adapter/ingest/game-contract.ts +63 -1
- package/src/adapter/setup-three-root-adapter.ts +105 -2
- package/src/dev/performance-profiler.ts +47 -12
- package/src/dev/register-render-vitals.ts +249 -0
- package/src/dev/render-census.ts +351 -0
- package/src/dev/render-vitals.ts +338 -0
- package/src/dev/static-batch-advisor.ts +186 -0
- package/src/render/render-batch-system.ts +16 -19
- package/src/render/structural-signature.ts +231 -0
- package/src/runtime/dev-layers.ts +7 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projects a game's DECLARED system surface (`window.vgaiGame.systems`, see
|
|
3
|
+
* `game-contract.ts`) onto the host's existing {@link DebugAdapter} — the one
|
|
4
|
+
* seam `game.commands()` / `game.state()` / `game.providers()` already read
|
|
5
|
+
* through for first-party content (`command-listener.ts`'s
|
|
6
|
+
* `dispatchBridgeMethod` → `getActiveSystems().debug`).
|
|
7
|
+
*
|
|
8
|
+
* Why a projection rather than a second door: an agent driving an ingested
|
|
9
|
+
* game must not have to learn a parallel vocabulary. A first-party game
|
|
10
|
+
* registers verbs with `ctx.debug.registerCommand`; an ingested game declares
|
|
11
|
+
* them in its entry shim. Both arrive at the SAME `DebugAdapter`, so every
|
|
12
|
+
* consumer downstream — the CLI's `vgai eval`, `@vgai/e2e`'s `GameClient`, the
|
|
13
|
+
* editor's Debug Console and State Watch panels — works unchanged and unaware
|
|
14
|
+
* of the provenance.
|
|
15
|
+
*
|
|
16
|
+
* What this deliberately does NOT do:
|
|
17
|
+
* - **Validate arguments.** The first-party path validates against a declared
|
|
18
|
+
* Zod tuple; a shim beside a foreign game is plain JS and has none. A verb
|
|
19
|
+
* validates itself and throws its own error, which surfaces wrapped as
|
|
20
|
+
* `DEBUG_COMMAND_FAILED` — the host never invents a schema to check against.
|
|
21
|
+
* - **Fabricate an event ring.** There is no tick loop behind an ingested
|
|
22
|
+
* game's declarations, so `events()` is empty. That is accurate (nothing was
|
|
23
|
+
* emitted), not a stub standing in for a missing feature.
|
|
24
|
+
*/
|
|
25
|
+
import type { DebugAdapter } from '../system-adapter';
|
|
26
|
+
import type { VgaiGameSystems } from './game-contract';
|
|
27
|
+
/**
|
|
28
|
+
* Build a {@link DebugAdapter} over a declared system surface, or `null` when
|
|
29
|
+
* the game declared no verbs and no state. `null` is the honest floor: the
|
|
30
|
+
* bridge then reports `DEBUG_ADAPTER_UNAVAILABLE` exactly as it did before,
|
|
31
|
+
* rather than serving an empty adapter that reads as "this game has no state"
|
|
32
|
+
* when the truth is "this game declared nothing".
|
|
33
|
+
*/
|
|
34
|
+
export declare function createContractDebugAdapter(systems: VgaiGameSystems | undefined | null): DebugAdapter | null;
|
|
35
|
+
//# sourceMappingURL=contract-debug-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract-debug-adapter.d.ts","sourceRoot":"","sources":["../../../src/adapter/ingest/contract-debug-adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAsC,MAAM,mBAAmB,CAAC;AAC1F,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,eAAe,GAAG,SAAS,GAAG,IAAI,GAC1C,YAAY,GAAG,IAAI,CAuErB"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projects a game's DECLARED system surface (`window.vgaiGame.systems`, see
|
|
3
|
+
* `game-contract.ts`) onto the host's existing {@link DebugAdapter} — the one
|
|
4
|
+
* seam `game.commands()` / `game.state()` / `game.providers()` already read
|
|
5
|
+
* through for first-party content (`command-listener.ts`'s
|
|
6
|
+
* `dispatchBridgeMethod` → `getActiveSystems().debug`).
|
|
7
|
+
*
|
|
8
|
+
* Why a projection rather than a second door: an agent driving an ingested
|
|
9
|
+
* game must not have to learn a parallel vocabulary. A first-party game
|
|
10
|
+
* registers verbs with `ctx.debug.registerCommand`; an ingested game declares
|
|
11
|
+
* them in its entry shim. Both arrive at the SAME `DebugAdapter`, so every
|
|
12
|
+
* consumer downstream — the CLI's `vgai eval`, `@vgai/e2e`'s `GameClient`, the
|
|
13
|
+
* editor's Debug Console and State Watch panels — works unchanged and unaware
|
|
14
|
+
* of the provenance.
|
|
15
|
+
*
|
|
16
|
+
* What this deliberately does NOT do:
|
|
17
|
+
* - **Validate arguments.** The first-party path validates against a declared
|
|
18
|
+
* Zod tuple; a shim beside a foreign game is plain JS and has none. A verb
|
|
19
|
+
* validates itself and throws its own error, which surfaces wrapped as
|
|
20
|
+
* `DEBUG_COMMAND_FAILED` — the host never invents a schema to check against.
|
|
21
|
+
* - **Fabricate an event ring.** There is no tick loop behind an ingested
|
|
22
|
+
* game's declarations, so `events()` is empty. That is accurate (nothing was
|
|
23
|
+
* emitted), not a stub standing in for a missing feature.
|
|
24
|
+
*/
|
|
25
|
+
import { DebugError } from '../../runtime/debug-registry';
|
|
26
|
+
/**
|
|
27
|
+
* Build a {@link DebugAdapter} over a declared system surface, or `null` when
|
|
28
|
+
* the game declared no verbs and no state. `null` is the honest floor: the
|
|
29
|
+
* bridge then reports `DEBUG_ADAPTER_UNAVAILABLE` exactly as it did before,
|
|
30
|
+
* rather than serving an empty adapter that reads as "this game has no state"
|
|
31
|
+
* when the truth is "this game declared nothing".
|
|
32
|
+
*/
|
|
33
|
+
export function createContractDebugAdapter(systems) {
|
|
34
|
+
const commandList = systems?.commands ?? [];
|
|
35
|
+
const providerList = systems?.state ?? [];
|
|
36
|
+
if (commandList.length === 0 && providerList.length === 0)
|
|
37
|
+
return null;
|
|
38
|
+
// Snapshot into name-keyed maps once. A later duplicate shadows an earlier
|
|
39
|
+
// one and the listing de-duplicates with it, so `commands()` can never
|
|
40
|
+
// advertise a name `invoke()` would resolve differently.
|
|
41
|
+
const byCommand = new Map(commandList.map((c) => [c.name, c]));
|
|
42
|
+
const byProvider = new Map(providerList.map((p) => [p.name, p]));
|
|
43
|
+
return {
|
|
44
|
+
providers: () => [...byProvider.values()].map((p) => ({ name: p.name, tier: p.tier ?? 'observable' })),
|
|
45
|
+
state: (name) => {
|
|
46
|
+
const provider = byProvider.get(name);
|
|
47
|
+
if (!provider) {
|
|
48
|
+
throw new DebugError('STATE_PROVIDER_NOT_FOUND', `No state provider named "${name}" is declared by this game.`, { registered: [...byProvider.keys()] });
|
|
49
|
+
}
|
|
50
|
+
return provider.read();
|
|
51
|
+
},
|
|
52
|
+
stateAll: () => {
|
|
53
|
+
const out = {};
|
|
54
|
+
for (const [name, provider] of byProvider) {
|
|
55
|
+
// One throwing provider must not cost the caller every other reading —
|
|
56
|
+
// the same per-key isolation the first-party registry gives.
|
|
57
|
+
try {
|
|
58
|
+
out[name] = provider.read();
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
out[name] = { __error: String(err) };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
},
|
|
66
|
+
commands: () => [...byCommand.values()].map((c) => ({
|
|
67
|
+
name: c.name,
|
|
68
|
+
description: c.description,
|
|
69
|
+
argsJsonSchema: c.argsJsonSchema,
|
|
70
|
+
// An ingested game runs entirely in the browser realm the host mounted
|
|
71
|
+
// it in; there is no server leg for its verbs to route to.
|
|
72
|
+
locus: 'client',
|
|
73
|
+
})),
|
|
74
|
+
invoke: async (name, args) => {
|
|
75
|
+
const command = byCommand.get(name);
|
|
76
|
+
if (!command) {
|
|
77
|
+
throw new DebugError('DEBUG_COMMAND_NOT_REGISTERED', `No command named "${name}" is declared by this game.`, { registered: [...byCommand.keys()] });
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
return await command.run(...args);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
throw new DebugError('DEBUG_COMMAND_FAILED', `Command "${name}" threw: ${String(err)}`, {
|
|
84
|
+
name,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
events: () => [],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* endpoint by endpoint: capture infers what it can (the scene), the game
|
|
16
16
|
* declares what inference can't reach (its DOM root, its session lifecycle).
|
|
17
17
|
*
|
|
18
|
-
* Origin (root), F17+F21 (lifecycle).
|
|
18
|
+
* Origin (root), F17+F21 (lifecycle), WO-SYS1 (systems).
|
|
19
19
|
*/
|
|
20
20
|
/**
|
|
21
21
|
* Session lifecycle endpoints. Declaring `start` means "I support cold
|
|
@@ -32,6 +32,59 @@ export interface VgaiGameLifecycle {
|
|
|
32
32
|
pause?(): void;
|
|
33
33
|
resume?(): void;
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* One invokable verb the game exposes. Deliberately the SAME listing shape a
|
|
37
|
+
* first-party game gets from `ctx.debug.registerCommand`
|
|
38
|
+
* (`DebugCommandInfo`/`DebugAdapter.invoke` in `adapter/system-adapter.ts`) —
|
|
39
|
+
* an ingested game's verbs must enumerate through `game.commands()` and run
|
|
40
|
+
* through `game.command(...)` with no second vocabulary for agents to learn.
|
|
41
|
+
*
|
|
42
|
+
* `run` receives the positional argument list exactly as invoked. There is no
|
|
43
|
+
* host-side validation: the contract is plain JS declared beside a foreign
|
|
44
|
+
* game, so it cannot carry a Zod tuple the way `registerCommand` does.
|
|
45
|
+
* `argsJsonSchema` is therefore DOCUMENTATION for listing surfaces, not a
|
|
46
|
+
* gate — a verb validates its own arguments and throws its own errors.
|
|
47
|
+
*/
|
|
48
|
+
export interface VgaiGameCommand {
|
|
49
|
+
name: string;
|
|
50
|
+
description?: string;
|
|
51
|
+
/** JSON-Schema projection of the argument list, when the game declares one. */
|
|
52
|
+
argsJsonSchema?: unknown;
|
|
53
|
+
run(...args: unknown[]): unknown | Promise<unknown>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* One named, JSON-serializable state read — the `ctx.debug.registerStateProvider`
|
|
57
|
+
* shape. `tier` carries the same meaning as the first-party one: `observable`
|
|
58
|
+
* is a value the game already computes, `assisted` is one the shim derives.
|
|
59
|
+
*/
|
|
60
|
+
export interface VgaiGameStateProvider {
|
|
61
|
+
name: string;
|
|
62
|
+
tier?: 'observable' | 'assisted';
|
|
63
|
+
read(): unknown;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The game's SYSTEM surface: what its content actually IS, as opposed to how
|
|
67
|
+
* it renders. An ingested game's content is not always its scene graph — in a
|
|
68
|
+
* system-driven game the authorable content lives in the game's own model and
|
|
69
|
+
* is reached through the game's own verbs, which `root`/`lifecycle` could not
|
|
70
|
+
* express.
|
|
71
|
+
*
|
|
72
|
+
* Both members are read by exactly one consumer,
|
|
73
|
+
* `adapter/ingest/contract-debug-adapter.ts`, which projects them onto the
|
|
74
|
+
* host's ordinary `DebugAdapter` (`adapter/system-adapter.ts`) — so
|
|
75
|
+
* `game.commands()`/`game.state()` reach an ingested game through the same
|
|
76
|
+
* bridge first-party content uses, with no second vocabulary for agents to
|
|
77
|
+
* learn. That is deliberate scope: this
|
|
78
|
+
* interface describes what a foreign game's entry shim may declare, so every
|
|
79
|
+
* member here is a promise the host must already keep. A member nothing reads
|
|
80
|
+
* would be a shim author implementing a hook that is never called.
|
|
81
|
+
*/
|
|
82
|
+
export interface VgaiGameSystems {
|
|
83
|
+
/** Verbs, enumerated by `game.commands()` and run by `game.command(...)`. */
|
|
84
|
+
commands?: VgaiGameCommand[];
|
|
85
|
+
/** State reads, enumerated by `game.providers()` and read by `game.state(...)`. */
|
|
86
|
+
state?: VgaiGameStateProvider[];
|
|
87
|
+
}
|
|
35
88
|
export interface VgaiGameContract {
|
|
36
89
|
/** Bump only on breaking shape changes; additive endpoints keep version 1. */
|
|
37
90
|
contractVersion: 1;
|
|
@@ -43,6 +96,12 @@ export interface VgaiGameContract {
|
|
|
43
96
|
*/
|
|
44
97
|
root?: HTMLElement;
|
|
45
98
|
lifecycle?: VgaiGameLifecycle;
|
|
99
|
+
/**
|
|
100
|
+
* The game's own systems. Absent means "this game declared no system
|
|
101
|
+
* surface" — every dependent editor surface then shows a named absence, not
|
|
102
|
+
* an empty pretend-palette.
|
|
103
|
+
*/
|
|
104
|
+
systems?: VgaiGameSystems;
|
|
46
105
|
}
|
|
47
106
|
/**
|
|
48
107
|
* Read the declared contract, if any. The `contractVersion` gate is the
|
|
@@ -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,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;
|
|
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"}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* endpoint by endpoint: capture infers what it can (the scene), the game
|
|
16
16
|
* declares what inference can't reach (its DOM root, its session lifecycle).
|
|
17
17
|
*
|
|
18
|
-
* Origin (root), F17+F21 (lifecycle).
|
|
18
|
+
* Origin (root), F17+F21 (lifecycle), WO-SYS1 (systems).
|
|
19
19
|
*/
|
|
20
20
|
/**
|
|
21
21
|
* Read the declared contract, if any. The `contractVersion` gate is the
|
|
@@ -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;
|
|
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"}
|
|
@@ -26,8 +26,10 @@ import { createSeededRandom, DEFAULT_SEEDED_RANDOM_SEED, getSeededRandom, } from
|
|
|
26
26
|
import { createSimClock, getSimClock } from '../core/sim-clock';
|
|
27
27
|
import { createSystemRunner } from '../core/system-runner';
|
|
28
28
|
import { createDebugDraw } from '../dev/debug-draw';
|
|
29
|
+
import { registerRenderVitals } from '../dev/register-render-vitals';
|
|
29
30
|
import { createRenderDebugAdapter, frameCaptureContextFor, } from '../dev/render-debug-adapter';
|
|
30
31
|
import { collectRenderMemory } from '../dev/render-memory';
|
|
32
|
+
import { RENDER_SUBMIT_PHASE } from '../dev/render-vitals';
|
|
31
33
|
import { createWebGLFrameCapture } from '../dev/webgl-frame-capture';
|
|
32
34
|
import { createWebGLGpuTimer } from '../dev/webgl-gpu-timer';
|
|
33
35
|
import { createSceneIndex } from '../ecs/scene-index';
|
|
@@ -41,6 +43,7 @@ import { updateSceneLODs } from '../render/lod';
|
|
|
41
43
|
import { disposeSparkRendererWhenIdle, SPARK_DISCOVERY_INTERVAL_MS, sceneHasGaussianSplat, shouldDiscoverGaussianSplat, } from '../render/spark-renderer-lifecycle';
|
|
42
44
|
import { ViewportShadingRenderer } from '../render/viewport-shading';
|
|
43
45
|
import { createDebugRegistry, getDebugRegistry, } from '../runtime/debug-registry';
|
|
46
|
+
import { devLayersEnabled } from '../runtime/dev-layers';
|
|
44
47
|
import { disposeDebrisSubtree } from '../runtime/game';
|
|
45
48
|
import { setupAudio } from '../setup/setup-audio';
|
|
46
49
|
import { setupParticles } from '../setup/setup-particles';
|
|
@@ -150,6 +153,10 @@ export class SetupThreeRootAdapter {
|
|
|
150
153
|
: null;
|
|
151
154
|
const viewportShading = new ViewportShadingRenderer();
|
|
152
155
|
let viewportShadingMode = 'solid';
|
|
156
|
+
/** Top-level `renderer.render()` calls the last presentation cost — written
|
|
157
|
+
* by the render system, read by `postFrame`. See both for why the count is
|
|
158
|
+
* latched rather than differenced at report time. */
|
|
159
|
+
let lastPresentationPasses = 0;
|
|
153
160
|
// --- System runner (engine-level systems) ---
|
|
154
161
|
const systems = createSystemRunner(host.game?.profiler.systemObserver, 'three');
|
|
155
162
|
// D15/T-D15.3/.5: pass the LIVE shared game tick (not a locally-counted
|
|
@@ -201,7 +208,8 @@ export class SetupThreeRootAdapter {
|
|
|
201
208
|
}, { name: 'materials-and-particles' });
|
|
202
209
|
if (!headless) {
|
|
203
210
|
systems.add('render', (dt) => {
|
|
204
|
-
|
|
211
|
+
const profiler = host.game?.profiler;
|
|
212
|
+
if (profiler?.enabled)
|
|
205
213
|
gpuTimer?.begin();
|
|
206
214
|
if (sparkRenderer)
|
|
207
215
|
scene.add(sparkRenderer);
|
|
@@ -211,6 +219,21 @@ export class SetupThreeRootAdapter {
|
|
|
211
219
|
// patched context — in the SAME finally as gpuTimer.end(). Spark is
|
|
212
220
|
// added before this so its draws fall inside the captured frame.
|
|
213
221
|
renderDebugWiring?.beforeRender();
|
|
222
|
+
// Issue #1504: CPU render submission, bracketed as its OWN profiler
|
|
223
|
+
// phase nested inside the frame's enclosing `render` phase. This is
|
|
224
|
+
// the frame's only isolated measurement of "how long did it take to
|
|
225
|
+
// hand the draws to the driver" — the `render` phase around it also
|
|
226
|
+
// contains physics-debug redraw and every other world's render work,
|
|
227
|
+
// and a system span would key the reading to this adapter's system
|
|
228
|
+
// NAME. The profiler's phase clock is a stack precisely so this
|
|
229
|
+
// bracket cannot truncate its parent (`dev/performance-profiler.ts`).
|
|
230
|
+
// It is also what makes a frame a PRESENTATION as far as the vitals
|
|
231
|
+
// fold is concerned: no draw, no bracket, no display frame.
|
|
232
|
+
profiler?.beginPhase();
|
|
233
|
+
// `info.render.frame` counts top-level renderer.render() calls and
|
|
234
|
+
// survives `info.reset()`, so its delta across the draw is how many
|
|
235
|
+
// passes the composer chain (plus shadows) actually cost.
|
|
236
|
+
const passesBefore = renderer.info.render.frame;
|
|
214
237
|
try {
|
|
215
238
|
viewportShading.render(scene, viewportShadingMode, () => composer.render(dt),
|
|
216
239
|
// Scene authoring can temporarily attach editor infrastructure
|
|
@@ -219,6 +242,8 @@ export class SetupThreeRootAdapter {
|
|
|
219
242
|
(mesh) => camera.layers.test(mesh.layers));
|
|
220
243
|
}
|
|
221
244
|
finally {
|
|
245
|
+
profiler?.endPhase(RENDER_SUBMIT_PHASE);
|
|
246
|
+
lastPresentationPasses = renderer.info.render.frame - passesBefore;
|
|
222
247
|
renderDebugWiring?.afterRender();
|
|
223
248
|
sparkRenderer?.removeFromParent();
|
|
224
249
|
gpuTimer?.end();
|
|
@@ -239,13 +264,25 @@ export class SetupThreeRootAdapter {
|
|
|
239
264
|
const postFrame = () => {
|
|
240
265
|
if (ownsInput)
|
|
241
266
|
input.endFrame();
|
|
242
|
-
|
|
267
|
+
// `renderer.info` is checked, not assumed: a headless/stub renderer
|
|
268
|
+
// supplies only what the runtime needs to mount, and a mount with no
|
|
269
|
+
// counters must report NOTHING rather than throw once per frame. This
|
|
270
|
+
// used to be unreachable because only the editor ever enabled the
|
|
271
|
+
// profiler; the engine enables it itself under the dev gate now (see the
|
|
272
|
+
// render-vitals seed below), so the assumption has to be paid for.
|
|
273
|
+
if (host.game?.profiler.enabled && renderer.info) {
|
|
243
274
|
host.game.profiler.reportRender({
|
|
244
275
|
gpuMs: gpuTimer?.poll() ?? null,
|
|
245
276
|
drawCalls: renderer.info.render.calls,
|
|
246
277
|
triangles: renderer.info.render.triangles,
|
|
247
278
|
geometries: renderer.info.memory.geometries,
|
|
248
279
|
textures: renderer.info.memory.textures,
|
|
280
|
+
// The LAST presentation's pass count, not a delta taken here: under
|
|
281
|
+
// the display-rate loop this hook runs once per fixed SUBSTEP, so a
|
|
282
|
+
// delta measured at this point would read as 0 on every substep that
|
|
283
|
+
// did not draw. Same one-frame-warm semantics as the renderer.info
|
|
284
|
+
// counters beside it.
|
|
285
|
+
renderPasses: lastPresentationPasses,
|
|
249
286
|
});
|
|
250
287
|
}
|
|
251
288
|
};
|
|
@@ -431,6 +468,59 @@ export class SetupThreeRootAdapter {
|
|
|
431
468
|
ctx.random =
|
|
432
469
|
(host.game ? getSeededRandom(host.game) : null) ??
|
|
433
470
|
createSeededRandom(DEFAULT_SEEDED_RANDOM_SEED);
|
|
471
|
+
// --- Live render vitals (issue #1504) -------------------------------------
|
|
472
|
+
// Engine-owned and first-party, seeded HERE like `renderDebug` above: a
|
|
473
|
+
// running game must be able to explain its own frame cost through the debug
|
|
474
|
+
// registry, with no capability to install and nothing for a game to write.
|
|
475
|
+
//
|
|
476
|
+
// THE GATE, in one place. Three conditions, and the reason for each:
|
|
477
|
+
// 1. `devLayersEnabled()` — the ONE owner of "is this a dev/editor
|
|
478
|
+
// context" (`runtime/dev-layers.ts`). A ship build registers nothing,
|
|
479
|
+
// subscribes to nothing, and never enables the profiler.
|
|
480
|
+
// 2. a `Game` shell exists — the readings are folded out of that game's
|
|
481
|
+
// profiler frames, and a bare mount has no profiler to fold.
|
|
482
|
+
// 3. not headless — a headless mount registers no render system at all,
|
|
483
|
+
// so it never brackets a submission and never presents a frame. Its
|
|
484
|
+
// vitals could only ever read empty, and registering a door that is
|
|
485
|
+
// structurally incapable of answering is worse than not having one.
|
|
486
|
+
//
|
|
487
|
+
// The enabled-gating question, resolved: the profiler is AUTO-ENABLED here
|
|
488
|
+
// rather than given a second, cheaper always-on counter tier. A counter
|
|
489
|
+
// tier would be a parallel measurement path — exactly the side ledger this
|
|
490
|
+
// work exists to avoid — and the profiler already has the one property
|
|
491
|
+
// that made the choice: `enabled` is a plain flag whose cost is per-frame
|
|
492
|
+
// bookkeeping the editor ALREADY pays (it sets `profiler.enabled = true`
|
|
493
|
+
// on play, `CenterDocuments.tsx`), so under the dev gate this is not a new
|
|
494
|
+
// cost, it is the same cost arriving a little earlier. Nothing here ever
|
|
495
|
+
// turns it OFF: the profiler's other owners (the Performance panel,
|
|
496
|
+
// `render-control.ts`'s perfSample) save and restore the prior state, so a
|
|
497
|
+
// dev session that enabled it at mount keeps it enabled.
|
|
498
|
+
let renderVitals = null;
|
|
499
|
+
const seedRenderVitals = () => {
|
|
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))
|
|
512
|
+
return;
|
|
513
|
+
renderVitals?.dispose();
|
|
514
|
+
host.game.profiler.enabled = true;
|
|
515
|
+
renderVitals = registerRenderVitals({
|
|
516
|
+
registry: debugRegistry,
|
|
517
|
+
worldId: this.id,
|
|
518
|
+
profiler: host.game.profiler,
|
|
519
|
+
scene,
|
|
520
|
+
renderer,
|
|
521
|
+
});
|
|
522
|
+
};
|
|
523
|
+
seedRenderVitals();
|
|
434
524
|
// Snapshot of the engine-owned adapter kinds, taken right after seeding
|
|
435
525
|
// and before any setup() has had a chance to run. `hotReload`/`disposeGame`
|
|
436
526
|
// use this to strip every GAME-registered kind (including an override of a
|
|
@@ -623,6 +713,13 @@ export class SetupThreeRootAdapter {
|
|
|
623
713
|
// roots remain alive during root replacement. A bare standalone adapter
|
|
624
714
|
// owns the registry and can clear it globally.
|
|
625
715
|
safeStep('debugRegistry.strip', () => host.game ? debugRegistry.strip(this.id) : debugRegistry.strip());
|
|
716
|
+
// The strip above removes the vitals REGISTRATIONS (their provenance is
|
|
717
|
+
// this world). This ends the fold's profiler subscription — the one
|
|
718
|
+
// resource `registerRenderVitals` allocates that a strip cannot reach.
|
|
719
|
+
safeStep('renderVitals.dispose', () => {
|
|
720
|
+
renderVitals?.dispose();
|
|
721
|
+
renderVitals = null;
|
|
722
|
+
});
|
|
626
723
|
// Release analyser taps BEFORE the audio teardown below. A consumer
|
|
627
724
|
// (the editor's meter poll) should dispose its own handle, but Stop must
|
|
628
725
|
// never leak taps regardless.
|
|
@@ -735,6 +832,12 @@ export class SetupThreeRootAdapter {
|
|
|
735
832
|
// every react-door registration (whose `useEffect` cleanup never
|
|
736
833
|
// re-fires to restore them after someone else's strip).
|
|
737
834
|
safeStep('debugRegistry.strip', () => debugRegistry.strip(this.id));
|
|
835
|
+
// The strip above takes the ENGINE's own vitals registrations with it
|
|
836
|
+
// (they carry this world's provenance, which is what makes the game's
|
|
837
|
+
// own re-seed silent). Re-seed them before the incoming setup runs, so a
|
|
838
|
+
// warm restart does not silently cost a game its render vitals — and so
|
|
839
|
+
// the fold starts clean rather than carrying the outgoing game's frames.
|
|
840
|
+
safeStep('renderVitals re-seed', seedRenderVitals);
|
|
738
841
|
currentCleanup = await newSetup(ctx, newEditorPreview);
|
|
739
842
|
};
|
|
740
843
|
return {
|
|
@@ -33,6 +33,15 @@ export interface PerformanceRenderStats {
|
|
|
33
33
|
readonly triangles: number;
|
|
34
34
|
readonly geometries: number;
|
|
35
35
|
readonly textures: number;
|
|
36
|
+
/**
|
|
37
|
+
* How many top-level `renderer.render()` submissions the last presentation
|
|
38
|
+
* took (`renderer.info.render.frame`'s delta across one draw) — a composer
|
|
39
|
+
* chain, a shadow pass and a post stack each cost one. `null` (never a
|
|
40
|
+
* fabricated 0, same rule as `gpuMs`) when no reporter counted them: a
|
|
41
|
+
* reporter that omits `renderPasses` is saying it does not know, which is
|
|
42
|
+
* different from saying nothing was drawn.
|
|
43
|
+
*/
|
|
44
|
+
readonly renderPasses: number | null;
|
|
36
45
|
}
|
|
37
46
|
export interface PerformanceFrame {
|
|
38
47
|
readonly id: number;
|
|
@@ -71,13 +80,7 @@ export interface PerformanceSnapshot {
|
|
|
71
80
|
readonly phases: readonly PerformanceTiming[];
|
|
72
81
|
readonly systems: readonly PerformanceTiming[];
|
|
73
82
|
readonly components: readonly PerformanceTiming[];
|
|
74
|
-
readonly render:
|
|
75
|
-
readonly gpuMs: number | null;
|
|
76
|
-
readonly drawCalls: number;
|
|
77
|
-
readonly triangles: number;
|
|
78
|
-
readonly geometries: number;
|
|
79
|
-
readonly textures: number;
|
|
80
|
-
};
|
|
83
|
+
readonly render: PerformanceRenderStats;
|
|
81
84
|
}
|
|
82
85
|
/** Game-owned, bounded external store for browser-native performance diagnostics. */
|
|
83
86
|
export declare function createPerformanceProfiler(initiallyEnabled?: boolean): {
|
|
@@ -101,6 +104,9 @@ export declare function createPerformanceProfiler(initiallyEnabled?: boolean): {
|
|
|
101
104
|
triangles: number;
|
|
102
105
|
geometries: number;
|
|
103
106
|
textures: number;
|
|
107
|
+
/** Omit when this reporter does not count passes — see
|
|
108
|
+
* {@link PerformanceRenderStats.renderPasses}. */
|
|
109
|
+
renderPasses?: number;
|
|
104
110
|
}): void;
|
|
105
111
|
endFrame(): void;
|
|
106
112
|
subscribe(listener: () => void): () => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"performance-profiler.d.ts","sourceRoot":"","sources":["../../src/dev/performance-profiler.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB;;;;;;;;OAQG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AAED,yEAAyE;AACzE,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"performance-profiler.d.ts","sourceRoot":"","sources":["../../src/dev/performance-profiler.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB;;;;;;;;OAQG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AAED,yEAAyE;AACzE,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;;;;OAOG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC9C;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,CAAC;IAC7C;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,SAAS,eAAe,EAAE,CAAC;IAChD;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,sBAAsB,CAAC;CACzC;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC7C,QAAQ,CAAC,MAAM,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC9C,QAAQ,CAAC,OAAO,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAC/C,QAAQ,CAAC,UAAU,EAAE,SAAS,iBAAiB,EAAE,CAAC;IAClD,QAAQ,CAAC,MAAM,EAAE,sBAAsB,CAAC;CACzC;AAUD,qFAAqF;AACrF,wBAAgB,yBAAyB,CAAC,gBAAgB,UAAQ;aA8E3C,OAAO;;;;;;;mBA8CX,MAAM;;;yBAqBF,MAAM,SAAS,MAAM,QAAQ,MAAM;;;uBAiBnC,MAAM;wBAWL;QAClB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;QACjB;2DACmD;QACnD,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB;;wBAqCmB,MAAM,IAAI;;;EAejC;AAED,MAAM,MAAM,mBAAmB,GAAG,UAAU,CAAC,OAAO,yBAAyB,CAAC,CAAC"}
|
|
@@ -23,7 +23,18 @@ export function createPerformanceProfiler(initiallyEnabled = false) {
|
|
|
23
23
|
let frameId = 0;
|
|
24
24
|
let frameStart = 0;
|
|
25
25
|
let lastFrameStart = 0;
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Phase start times, innermost last. A STACK rather than the single slot
|
|
28
|
+
* this used to be, because phases NEST: the three adapter brackets its CPU
|
|
29
|
+
* render submission as its own phase (`render.submit`,
|
|
30
|
+
* `dev/render-vitals.ts`) from inside the frame's enclosing `render` phase.
|
|
31
|
+
* With one slot the inner `beginPhase()` overwrote the outer's start, so the
|
|
32
|
+
* outer `endPhase('render')` measured only the tail after the inner phase —
|
|
33
|
+
* silently under-reporting the very phase the inner one was decomposing.
|
|
34
|
+
* Balanced callers are unaffected; `endPhase` with an empty stack falls back
|
|
35
|
+
* to `frameStart` rather than a stale start from a previous frame.
|
|
36
|
+
*/
|
|
37
|
+
const phaseStack = [];
|
|
27
38
|
let systemStart = 0;
|
|
28
39
|
let componentStart = 0;
|
|
29
40
|
let render = {
|
|
@@ -32,6 +43,7 @@ export function createPerformanceProfiler(initiallyEnabled = false) {
|
|
|
32
43
|
triangles: 0,
|
|
33
44
|
geometries: 0,
|
|
34
45
|
textures: 0,
|
|
46
|
+
renderPasses: null,
|
|
35
47
|
};
|
|
36
48
|
let snapshot = {
|
|
37
49
|
enabled,
|
|
@@ -102,17 +114,30 @@ export function createPerformanceProfiler(initiallyEnabled = false) {
|
|
|
102
114
|
currentComponents.clear();
|
|
103
115
|
frameSystems = [];
|
|
104
116
|
frameComponents = [];
|
|
105
|
-
|
|
117
|
+
// A frame that threw mid-phase leaves entries on the stack; a fresh
|
|
118
|
+
// frame starts from nothing rather than inheriting them.
|
|
119
|
+
phaseStack.length = 0;
|
|
120
|
+
render = {
|
|
121
|
+
gpuMs: null,
|
|
122
|
+
drawCalls: 0,
|
|
123
|
+
triangles: 0,
|
|
124
|
+
geometries: 0,
|
|
125
|
+
textures: 0,
|
|
126
|
+
renderPasses: null,
|
|
127
|
+
};
|
|
106
128
|
frameStart = now();
|
|
107
129
|
},
|
|
108
130
|
beginPhase() {
|
|
109
131
|
if (!enabled)
|
|
110
132
|
return;
|
|
111
|
-
|
|
133
|
+
phaseStack.push(now());
|
|
112
134
|
},
|
|
113
135
|
endPhase(name) {
|
|
114
136
|
if (!enabled)
|
|
115
137
|
return;
|
|
138
|
+
// See `phaseStack`: pop restores the ENCLOSING phase's own start, so a
|
|
139
|
+
// nested bracket measures itself without truncating its parent.
|
|
140
|
+
const phaseStart = phaseStack.pop() ?? frameStart;
|
|
116
141
|
const elapsed = now() - phaseStart;
|
|
117
142
|
const existing = currentPhases.get(name);
|
|
118
143
|
// Accumulate ms across re-entries of the same phase, but keep the FIRST
|
|
@@ -170,6 +195,9 @@ export function createPerformanceProfiler(initiallyEnabled = false) {
|
|
|
170
195
|
triangles: render.triangles + stats.triangles,
|
|
171
196
|
geometries: render.geometries + stats.geometries,
|
|
172
197
|
textures: render.textures + stats.textures,
|
|
198
|
+
renderPasses: stats.renderPasses === undefined
|
|
199
|
+
? render.renderPasses
|
|
200
|
+
: (render.renderPasses ?? 0) + stats.renderPasses,
|
|
173
201
|
};
|
|
174
202
|
},
|
|
175
203
|
endFrame() {
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE DOOR for live render vitals: one provider and four commands on the
|
|
3
|
+
* game's own debug registry (`runtime/debug-registry.ts`), so a running game
|
|
4
|
+
* explains its own frame cost through the SAME seam every other reading and
|
|
5
|
+
* verb uses — `game.state('render.vitals')` / `game.command('render.census')`
|
|
6
|
+
* through `vgai eval`, and `game.providers()`/`game.commands()` to discover
|
|
7
|
+
* them without being told they exist.
|
|
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`
|
|
13
|
+
* (derived entirely from profiler frames); the address-book half is
|
|
14
|
+
* `dev/render-census.ts` (pure walks over the live scene). This module is only
|
|
15
|
+
* the wiring between them and the registry.
|
|
16
|
+
*
|
|
17
|
+
* ── THE READING AND THE EXPERIMENTS ─────────────────────────────────────────
|
|
18
|
+
* A number alone does not route an investigation, so each command is the
|
|
19
|
+
* experiment that turns one reading into an address:
|
|
20
|
+
* - `render.census` — which subtree owns the meshes and triangles, with a
|
|
21
|
+
* drill-down argument;
|
|
22
|
+
* - `render.families` — which meshes are identical enough to instance;
|
|
23
|
+
* - `render.toggle` — hide a subtree and watch the stat move (the only way
|
|
24
|
+
* to ATTRIBUTE cost rather than infer it);
|
|
25
|
+
* - `render.dpr` — change the pixel count, which separates GPU fill cost
|
|
26
|
+
* from vsync idle: render CPU that does not move when
|
|
27
|
+
* the drawing buffer halves was never fill-bound.
|
|
28
|
+
*
|
|
29
|
+
* ── 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.
|
|
33
|
+
*
|
|
34
|
+
* ── RESOURCE OWNERSHIP ──────────────────────────────────────────────────────
|
|
35
|
+
* OWNER: {@link registerRenderVitals}'s caller. It allocates one
|
|
36
|
+
* {@link RenderVitals} fold (which owns one `profiler.subscribe`), a SECOND,
|
|
37
|
+
* self-ending `profiler.subscribe` for the static-batch advisory, and the
|
|
38
|
+
* registrations, whose provenance is the caller's `worldId`. SHARERS: none.
|
|
39
|
+
* TEARDOWN: the returned `dispose()` — the ONE path that ends both
|
|
40
|
+
* subscriptions. The REGISTRATIONS are ended by the registry's own
|
|
41
|
+
* `strip(worldId)`, which the adapter already runs on stop and warm restart;
|
|
42
|
+
* this module never strips, because a blind strip here would take the game's
|
|
43
|
+
* registrations with it.
|
|
44
|
+
*/
|
|
45
|
+
import * as THREE from 'three';
|
|
46
|
+
import type { DebugRegistry } from '../runtime/debug-registry';
|
|
47
|
+
import type { PerformanceProfiler } from './performance-profiler';
|
|
48
|
+
import { type RenderVitals } from './render-vitals';
|
|
49
|
+
/** The one provider name — spelled here, referenced everywhere else. */
|
|
50
|
+
export declare const RENDER_VITALS_PROVIDER = "render.vitals";
|
|
51
|
+
export declare const RENDER_CENSUS_COMMAND = "render.census";
|
|
52
|
+
export declare const RENDER_FAMILIES_COMMAND = "render.families";
|
|
53
|
+
export declare const RENDER_TOGGLE_COMMAND = "render.toggle";
|
|
54
|
+
export declare const RENDER_DPR_COMMAND = "render.dpr";
|
|
55
|
+
/** The slice of `THREE.WebGLRenderer` `render.dpr` actuates. Narrow on
|
|
56
|
+
* purpose: a headless mount has no renderer at all and passes `null`. */
|
|
57
|
+
export interface RenderVitalsRenderer {
|
|
58
|
+
getSize(target: THREE.Vector2): THREE.Vector2;
|
|
59
|
+
setPixelRatio(value: number): void;
|
|
60
|
+
setSize(width: number, height: number, updateStyle?: boolean): void;
|
|
61
|
+
getPixelRatio(): number;
|
|
62
|
+
readonly domElement: {
|
|
63
|
+
readonly width: number;
|
|
64
|
+
readonly height: number;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export interface RenderVitalsRegistration {
|
|
68
|
+
/** The live fold, exposed so a test can drive it without a registry. */
|
|
69
|
+
readonly vitals: RenderVitals;
|
|
70
|
+
/** See the ownership note — the ONE teardown path. Idempotent. */
|
|
71
|
+
dispose(): void;
|
|
72
|
+
}
|
|
73
|
+
export interface RegisterRenderVitalsDeps {
|
|
74
|
+
readonly registry: DebugRegistry;
|
|
75
|
+
/** Registration provenance — the mount's adapter id, so the adapter's own
|
|
76
|
+
* `strip(worldId)` on stop/warm-restart removes exactly these. */
|
|
77
|
+
readonly worldId: string;
|
|
78
|
+
readonly profiler: PerformanceProfiler;
|
|
79
|
+
/** The live scene the census walks. */
|
|
80
|
+
readonly scene: THREE.Object3D;
|
|
81
|
+
/** `null` under a headless mount — `render.dpr` then refuses by name rather
|
|
82
|
+
* than silently doing nothing. */
|
|
83
|
+
readonly renderer: RenderVitalsRenderer | null;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Seed the vitals door for one world. Returns `null` when another live world
|
|
87
|
+
* already owns the door: the names are fixed and discoverable on purpose, and
|
|
88
|
+
* the registry (correctly) throws on a cross-world name collision, so the
|
|
89
|
+
* FIRST three root to mount owns them. A second three root's draws still show
|
|
90
|
+
* up in the readings — the profiler is game-scoped, so every world's
|
|
91
|
+
* submission folds into the same frame — but the census commands address the
|
|
92
|
+
* owning root's scene.
|
|
93
|
+
*/
|
|
94
|
+
export declare function registerRenderVitals(deps: RegisterRenderVitalsDeps): RenderVitalsRegistration | null;
|
|
95
|
+
//# sourceMappingURL=register-render-vitals.d.ts.map
|
|
@@ -0,0 +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"}
|