@vgai/engine 0.5.7 → 0.5.9
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 +94 -2
- package/dist/core/game-scoped-slot.d.ts +14 -0
- package/dist/core/game-scoped-slot.d.ts.map +1 -0
- package/dist/core/game-scoped-slot.js +24 -0
- package/dist/core/seeded-random.d.ts.map +1 -1
- package/dist/core/seeded-random.js +3 -2
- package/dist/core/sim-clock.d.ts.map +1 -1
- package/dist/core/sim-clock.js +3 -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/debug-registry.d.ts +2 -2
- package/dist/runtime/debug-registry.d.ts.map +1 -1
- package/dist/runtime/debug-registry.js +4 -3
- package/dist/runtime/dev-layers.d.ts.map +1 -1
- package/dist/runtime/dev-layers.js +6 -0
- package/dist/runtime/game.js +2 -2
- package/dist/runtime/gameplay-rng-trap.d.ts.map +1 -1
- package/dist/runtime/gameplay-rng-trap.js +2 -1
- 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 +94 -2
- package/src/core/game-scoped-slot.ts +28 -0
- package/src/core/seeded-random.ts +3 -2
- package/src/core/sim-clock.ts +3 -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/debug-registry.ts +4 -3
- package/src/runtime/dev-layers.ts +7 -1
- package/src/runtime/game.ts +2 -2
- package/src/runtime/gameplay-rng-trap.ts +4 -2
|
@@ -35,6 +35,15 @@ export interface PerformanceRenderStats {
|
|
|
35
35
|
readonly triangles: number;
|
|
36
36
|
readonly geometries: number;
|
|
37
37
|
readonly textures: number;
|
|
38
|
+
/**
|
|
39
|
+
* How many top-level `renderer.render()` submissions the last presentation
|
|
40
|
+
* took (`renderer.info.render.frame`'s delta across one draw) — a composer
|
|
41
|
+
* chain, a shadow pass and a post stack each cost one. `null` (never a
|
|
42
|
+
* fabricated 0, same rule as `gpuMs`) when no reporter counted them: a
|
|
43
|
+
* reporter that omits `renderPasses` is saying it does not know, which is
|
|
44
|
+
* different from saying nothing was drawn.
|
|
45
|
+
*/
|
|
46
|
+
readonly renderPasses: number | null;
|
|
38
47
|
}
|
|
39
48
|
|
|
40
49
|
export interface PerformanceFrame {
|
|
@@ -75,13 +84,7 @@ export interface PerformanceSnapshot {
|
|
|
75
84
|
readonly phases: readonly PerformanceTiming[];
|
|
76
85
|
readonly systems: readonly PerformanceTiming[];
|
|
77
86
|
readonly components: readonly PerformanceTiming[];
|
|
78
|
-
readonly render:
|
|
79
|
-
readonly gpuMs: number | null;
|
|
80
|
-
readonly drawCalls: number;
|
|
81
|
-
readonly triangles: number;
|
|
82
|
-
readonly geometries: number;
|
|
83
|
-
readonly textures: number;
|
|
84
|
-
};
|
|
87
|
+
readonly render: PerformanceRenderStats;
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
const now = () => globalThis.performance?.now() ?? Date.now();
|
|
@@ -110,15 +113,27 @@ export function createPerformanceProfiler(initiallyEnabled = false) {
|
|
|
110
113
|
let frameId = 0;
|
|
111
114
|
let frameStart = 0;
|
|
112
115
|
let lastFrameStart = 0;
|
|
113
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Phase start times, innermost last. A STACK rather than the single slot
|
|
118
|
+
* this used to be, because phases NEST: the three adapter brackets its CPU
|
|
119
|
+
* render submission as its own phase (`render.submit`,
|
|
120
|
+
* `dev/render-vitals.ts`) from inside the frame's enclosing `render` phase.
|
|
121
|
+
* With one slot the inner `beginPhase()` overwrote the outer's start, so the
|
|
122
|
+
* outer `endPhase('render')` measured only the tail after the inner phase —
|
|
123
|
+
* silently under-reporting the very phase the inner one was decomposing.
|
|
124
|
+
* Balanced callers are unaffected; `endPhase` with an empty stack falls back
|
|
125
|
+
* to `frameStart` rather than a stale start from a previous frame.
|
|
126
|
+
*/
|
|
127
|
+
const phaseStack: number[] = [];
|
|
114
128
|
let systemStart = 0;
|
|
115
129
|
let componentStart = 0;
|
|
116
|
-
let render = {
|
|
117
|
-
gpuMs: null
|
|
130
|
+
let render: PerformanceRenderStats = {
|
|
131
|
+
gpuMs: null,
|
|
118
132
|
drawCalls: 0,
|
|
119
133
|
triangles: 0,
|
|
120
134
|
geometries: 0,
|
|
121
135
|
textures: 0,
|
|
136
|
+
renderPasses: null,
|
|
122
137
|
};
|
|
123
138
|
let snapshot: PerformanceSnapshot = {
|
|
124
139
|
enabled,
|
|
@@ -188,15 +203,28 @@ export function createPerformanceProfiler(initiallyEnabled = false) {
|
|
|
188
203
|
currentComponents.clear();
|
|
189
204
|
frameSystems = [];
|
|
190
205
|
frameComponents = [];
|
|
191
|
-
|
|
206
|
+
// A frame that threw mid-phase leaves entries on the stack; a fresh
|
|
207
|
+
// frame starts from nothing rather than inheriting them.
|
|
208
|
+
phaseStack.length = 0;
|
|
209
|
+
render = {
|
|
210
|
+
gpuMs: null,
|
|
211
|
+
drawCalls: 0,
|
|
212
|
+
triangles: 0,
|
|
213
|
+
geometries: 0,
|
|
214
|
+
textures: 0,
|
|
215
|
+
renderPasses: null,
|
|
216
|
+
};
|
|
192
217
|
frameStart = now();
|
|
193
218
|
},
|
|
194
219
|
beginPhase() {
|
|
195
220
|
if (!enabled) return;
|
|
196
|
-
|
|
221
|
+
phaseStack.push(now());
|
|
197
222
|
},
|
|
198
223
|
endPhase(name: string) {
|
|
199
224
|
if (!enabled) return;
|
|
225
|
+
// See `phaseStack`: pop restores the ENCLOSING phase's own start, so a
|
|
226
|
+
// nested bracket measures itself without truncating its parent.
|
|
227
|
+
const phaseStart = phaseStack.pop() ?? frameStart;
|
|
200
228
|
const elapsed = now() - phaseStart;
|
|
201
229
|
const existing = currentPhases.get(name);
|
|
202
230
|
// Accumulate ms across re-entries of the same phase, but keep the FIRST
|
|
@@ -247,6 +275,9 @@ export function createPerformanceProfiler(initiallyEnabled = false) {
|
|
|
247
275
|
triangles: number;
|
|
248
276
|
geometries: number;
|
|
249
277
|
textures: number;
|
|
278
|
+
/** Omit when this reporter does not count passes — see
|
|
279
|
+
* {@link PerformanceRenderStats.renderPasses}. */
|
|
280
|
+
renderPasses?: number;
|
|
250
281
|
}) {
|
|
251
282
|
if (!enabled) return;
|
|
252
283
|
render = {
|
|
@@ -255,6 +286,10 @@ export function createPerformanceProfiler(initiallyEnabled = false) {
|
|
|
255
286
|
triangles: render.triangles + stats.triangles,
|
|
256
287
|
geometries: render.geometries + stats.geometries,
|
|
257
288
|
textures: render.textures + stats.textures,
|
|
289
|
+
renderPasses:
|
|
290
|
+
stats.renderPasses === undefined
|
|
291
|
+
? render.renderPasses
|
|
292
|
+
: (render.renderPasses ?? 0) + stats.renderPasses,
|
|
258
293
|
};
|
|
259
294
|
},
|
|
260
295
|
endFrame() {
|
|
@@ -0,0 +1,249 @@
|
|
|
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
|
+
|
|
46
|
+
import * as THREE from 'three';
|
|
47
|
+
import { z } from 'zod';
|
|
48
|
+
import type { DebugRegistry } from '../runtime/debug-registry';
|
|
49
|
+
import type { PerformanceProfiler } from './performance-profiler';
|
|
50
|
+
import { findByName, meshFamilies, sceneCensus, structuralBatchScan } from './render-census';
|
|
51
|
+
import { createRenderVitals, type RenderVitals } from './render-vitals';
|
|
52
|
+
import {
|
|
53
|
+
ADVISOR_SETTLE_FRAMES,
|
|
54
|
+
decideStaticBatchAdvisory,
|
|
55
|
+
warnStaticBatchAdvisory,
|
|
56
|
+
} from './static-batch-advisor';
|
|
57
|
+
|
|
58
|
+
/** The one provider name — spelled here, referenced everywhere else. */
|
|
59
|
+
export const RENDER_VITALS_PROVIDER = 'render.vitals';
|
|
60
|
+
export const RENDER_CENSUS_COMMAND = 'render.census';
|
|
61
|
+
export const RENDER_FAMILIES_COMMAND = 'render.families';
|
|
62
|
+
export const RENDER_TOGGLE_COMMAND = 'render.toggle';
|
|
63
|
+
export const RENDER_DPR_COMMAND = 'render.dpr';
|
|
64
|
+
|
|
65
|
+
/** The slice of `THREE.WebGLRenderer` `render.dpr` actuates. Narrow on
|
|
66
|
+
* purpose: a headless mount has no renderer at all and passes `null`. */
|
|
67
|
+
export interface RenderVitalsRenderer {
|
|
68
|
+
getSize(target: THREE.Vector2): THREE.Vector2;
|
|
69
|
+
setPixelRatio(value: number): void;
|
|
70
|
+
setSize(width: number, height: number, updateStyle?: boolean): void;
|
|
71
|
+
getPixelRatio(): number;
|
|
72
|
+
readonly domElement: { readonly width: number; readonly height: number };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface RenderVitalsRegistration {
|
|
76
|
+
/** The live fold, exposed so a test can drive it without a registry. */
|
|
77
|
+
readonly vitals: RenderVitals;
|
|
78
|
+
/** See the ownership note — the ONE teardown path. Idempotent. */
|
|
79
|
+
dispose(): void;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface RegisterRenderVitalsDeps {
|
|
83
|
+
readonly registry: DebugRegistry;
|
|
84
|
+
/** Registration provenance — the mount's adapter id, so the adapter's own
|
|
85
|
+
* `strip(worldId)` on stop/warm-restart removes exactly these. */
|
|
86
|
+
readonly worldId: string;
|
|
87
|
+
readonly profiler: PerformanceProfiler;
|
|
88
|
+
/** The live scene the census walks. */
|
|
89
|
+
readonly scene: THREE.Object3D;
|
|
90
|
+
/** `null` under a headless mount — `render.dpr` then refuses by name rather
|
|
91
|
+
* than silently doing nothing. */
|
|
92
|
+
readonly renderer: RenderVitalsRenderer | null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const NO_RENDERER =
|
|
96
|
+
'render.dpr: this world has no WebGL renderer (headless mount), so there is no pixel ratio ' +
|
|
97
|
+
'to change. The other render.* commands still work — they read the scene graph.';
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Seed the vitals door for one world. Returns `null` when another live world
|
|
101
|
+
* already owns the door: the names are fixed and discoverable on purpose, and
|
|
102
|
+
* the registry (correctly) throws on a cross-world name collision, so the
|
|
103
|
+
* FIRST three root to mount owns them. A second three root's draws still show
|
|
104
|
+
* up in the readings — the profiler is game-scoped, so every world's
|
|
105
|
+
* submission folds into the same frame — but the census commands address the
|
|
106
|
+
* owning root's scene.
|
|
107
|
+
*/
|
|
108
|
+
export function registerRenderVitals(
|
|
109
|
+
deps: RegisterRenderVitalsDeps,
|
|
110
|
+
): RenderVitalsRegistration | null {
|
|
111
|
+
const { registry, worldId, profiler, scene, renderer } = deps;
|
|
112
|
+
const taken = registry.adapter
|
|
113
|
+
.providers()
|
|
114
|
+
.some((provider) => provider.name === RENDER_VITALS_PROVIDER);
|
|
115
|
+
if (taken) return null;
|
|
116
|
+
|
|
117
|
+
const vitals = createRenderVitals(profiler);
|
|
118
|
+
const debug = registry.forRoot(worldId);
|
|
119
|
+
|
|
120
|
+
debug.registerStateProvider(RENDER_VITALS_PROVIDER, () => vitals.read());
|
|
121
|
+
|
|
122
|
+
debug.registerCommand(
|
|
123
|
+
RENDER_CENSUS_COMMAND,
|
|
124
|
+
{
|
|
125
|
+
description:
|
|
126
|
+
'Visible meshes and triangles per subtree — the address book behind the draw-call ' +
|
|
127
|
+
'reading. Pass a subtree name (as listed by a previous census) to drill into it.',
|
|
128
|
+
args: z.tuple([z.string().optional()]),
|
|
129
|
+
locus: 'client',
|
|
130
|
+
},
|
|
131
|
+
(subtree) => {
|
|
132
|
+
if (subtree === undefined || subtree === '') return sceneCensus(scene);
|
|
133
|
+
const node = findByName(scene, subtree);
|
|
134
|
+
if (!node) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`render.census: no node named "${subtree}" in this world. Run render.census with no ` +
|
|
137
|
+
'argument to list the addressable subtrees.',
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return sceneCensus(node, subtree);
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
debug.registerCommand(
|
|
145
|
+
RENDER_FAMILIES_COMMAND,
|
|
146
|
+
{
|
|
147
|
+
description:
|
|
148
|
+
'Meshes grouped by shared (geometry, material) identity, largest group first — the ' +
|
|
149
|
+
'instancing shortlist for a draw-call diet. `structural` is the same scan keyed by ' +
|
|
150
|
+
'VALUE instead of object identity: what would collapse under a <Frozen> wrapper ' +
|
|
151
|
+
'(vgai add static-batch) even when every mesh carries its own inline material.',
|
|
152
|
+
args: z.tuple([]),
|
|
153
|
+
locus: 'client',
|
|
154
|
+
},
|
|
155
|
+
// Two readings, deliberately: the identity families (what can be instanced
|
|
156
|
+
// as the scene stands) and the STRUCTURAL scan (what would collapse if the
|
|
157
|
+
// same draws were batched by value). A world writing inline materials —
|
|
158
|
+
// every fresh scaffold — has families of one and a large structural win,
|
|
159
|
+
// and reporting only the first would say "nothing to do" about the exact
|
|
160
|
+
// scene the `static-batch` capability exists for.
|
|
161
|
+
() => ({ ...meshFamilies(scene), structural: structuralBatchScan(scene) }),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
debug.registerCommand(
|
|
165
|
+
RENDER_TOGGLE_COMMAND,
|
|
166
|
+
{
|
|
167
|
+
description:
|
|
168
|
+
'Show/hide a named subtree (as listed by render.census) and read render.vitals again — ' +
|
|
169
|
+
'the difference is what that subtree costs.',
|
|
170
|
+
args: z.tuple([z.string()]),
|
|
171
|
+
locus: 'client',
|
|
172
|
+
},
|
|
173
|
+
(name) => {
|
|
174
|
+
const node = findByName(scene, name);
|
|
175
|
+
if (!node) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`render.toggle: no node named "${name}" in this world. Run render.census to list the ` +
|
|
178
|
+
'addressable subtrees.',
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
node.visible = !node.visible;
|
|
182
|
+
return { name, visible: node.visible };
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
debug.registerCommand(
|
|
187
|
+
RENDER_DPR_COMMAND,
|
|
188
|
+
{
|
|
189
|
+
description:
|
|
190
|
+
'Set the renderer pixel ratio (retina default is ~2) to probe GPU fill cost — render CPU ' +
|
|
191
|
+
'that does not move when the drawing buffer shrinks was never fill-bound.',
|
|
192
|
+
args: z.tuple([z.number().positive().max(8)]),
|
|
193
|
+
locus: 'client',
|
|
194
|
+
},
|
|
195
|
+
(ratio) => {
|
|
196
|
+
if (!renderer) throw new Error(NO_RENDERER);
|
|
197
|
+
const size = renderer.getSize(new THREE.Vector2());
|
|
198
|
+
renderer.setPixelRatio(ratio);
|
|
199
|
+
// `updateStyle: false` — the CSS size is the layout's, not ours to
|
|
200
|
+
// change; only the drawing buffer moves, which is the whole experiment.
|
|
201
|
+
renderer.setSize(size.x, size.y, false);
|
|
202
|
+
return {
|
|
203
|
+
pixelRatio: renderer.getPixelRatio(),
|
|
204
|
+
drawingBuffer: { width: renderer.domElement.width, height: renderer.domElement.height },
|
|
205
|
+
};
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// --- The static-batch advisory (issue #1503) -------------------------------
|
|
210
|
+
// A reading nobody asks for is a reading nobody gets, so the measurement
|
|
211
|
+
// routes itself: once the frame rate has settled, scan ONCE, and if this
|
|
212
|
+
// world is paying for draws it does not have to, name the exact wrapper on
|
|
213
|
+
// the console (`dev/static-batch-advisor.ts` owns the decision and the
|
|
214
|
+
// wording; this owns only WHEN).
|
|
215
|
+
//
|
|
216
|
+
// It rides the profiler subscription rather than a timer because the trigger
|
|
217
|
+
// is presented frames, not wall time: a tab that never presents (hidden,
|
|
218
|
+
// headless) should never scan, and a slow boot should not be measured before
|
|
219
|
+
// its assets are in the graph. The subscription ends itself the moment it
|
|
220
|
+
// fires or the advisory turns out to be `null`.
|
|
221
|
+
let stopAdvisor: (() => void) | null = null;
|
|
222
|
+
let advised = false;
|
|
223
|
+
const advisorTick = (): void => {
|
|
224
|
+
if (advised) return;
|
|
225
|
+
if (vitals.read().presentedFrames < ADVISOR_SETTLE_FRAMES) return;
|
|
226
|
+
advised = true;
|
|
227
|
+
stopAdvisor?.();
|
|
228
|
+
stopAdvisor = null;
|
|
229
|
+
const advisory = decideStaticBatchAdvisory({
|
|
230
|
+
drawCalls: vitals.read().drawCalls,
|
|
231
|
+
census: sceneCensus(scene),
|
|
232
|
+
structural: structuralBatchScan(scene),
|
|
233
|
+
});
|
|
234
|
+
if (advisory) warnStaticBatchAdvisory(advisory);
|
|
235
|
+
};
|
|
236
|
+
stopAdvisor = profiler.subscribe(advisorTick);
|
|
237
|
+
|
|
238
|
+
let disposed = false;
|
|
239
|
+
return {
|
|
240
|
+
vitals,
|
|
241
|
+
dispose() {
|
|
242
|
+
if (disposed) return;
|
|
243
|
+
disposed = true;
|
|
244
|
+
stopAdvisor?.();
|
|
245
|
+
stopAdvisor = null;
|
|
246
|
+
vitals.dispose();
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|