@volter/editor-game 0.5.66 → 0.5.68
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/contributions/audio-unlock.service.ts +2 -2
- package/contributions/autoplay.service.ts +1 -1
- package/contributions/bridge.command.ts +3 -3
- package/contributions/build-player.document.tsx +95 -0
- package/contributions/canvas/component-board.service.ts +16 -0
- package/contributions/canvas/design-time-mount.service.ts +45 -0
- package/contributions/canvas-story-capture.service.ts +12 -0
- package/contributions/edit-mode-audio.service.ts +2 -2
- package/contributions/edit-mode-networking.service.ts +1 -1
- package/contributions/gameplay.command.ts +4 -4
- package/contributions/generation.service.ts +1 -1
- package/contributions/godot.style.ts +26 -6
- package/contributions/godot.view.ts +6 -0
- package/contributions/ingest.service.ts +2 -2
- package/contributions/instances.command.ts +1 -1
- package/contributions/navmesh.menu.ts +1 -1
- package/contributions/network-observer.service.ts +14 -0
- package/contributions/play.command.ts +10 -4
- package/contributions/react/component-board.service.ts +1 -1
- package/contributions/react/design-time-mount.service.ts +1 -1
- package/contributions/scene-document.service.ts +3 -3
- package/contributions/state-watch.menu.ts +1 -1
- package/contributions/state-watch.utility.tsx +1 -1
- package/contributions/team-playtest.service.ts +4 -4
- package/contributions/three/component-board.service.ts +1 -1
- package/contributions/three/component-verbs.command.ts +6 -6
- package/contributions/three/three-authoring.service.ts +8 -5
- package/contributions/three-story-capture.service.ts +12 -0
- package/contributions/unity.style.ts +17 -7
- package/contributions/unity.view.ts +6 -0
- package/contributions/unreal.style.ts +10 -2
- package/contributions/unreal.view.ts +5 -0
- package/package.json +21 -10
- package/src/asset-budget/AssetBudgetPanel.tsx +1 -1
- package/src/asset-budget/asset-budget-model.ts +1 -1
- package/src/audio/AudioDebuggerPanel.tsx +1 -1
- package/src/bridge/dispatch.ts +14 -14
- package/src/bridge/live-frames.ts +1 -1
- package/src/bridge/screenshot.ts +3 -3
- package/src/build/BuildProfilesPanel.tsx +14 -3
- package/src/build/build-session.ts +35 -0
- package/src/canvas/canvas-board/CanvasBoardDocument.tsx +749 -0
- package/src/canvas/canvas-board/canvas-board-model.ts +407 -0
- package/src/canvas/canvas-board/canvas-component-board.ts +56 -0
- package/src/canvas/canvas-design-mount.ts +524 -0
- package/src/canvas/design-time-canvas-mount.ts +79 -0
- package/src/coverage/live-authoring-surface.ts +5 -5
- package/src/coverage/live-project-verbs.ts +1 -1
- package/src/coverage/native-system-coverage.ts +5 -5
- package/src/coverage/root-coverage.ts +1 -1
- package/src/coverage/session-coverage.ts +3 -3
- package/src/design-system-stories/ApplicationChrome.stories.tsx +5 -5
- package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +7 -7
- package/src/edit-mode/edit-mode-audio.ts +4 -4
- package/src/edit-mode/edit-mode-networking.ts +2 -2
- package/src/game-document/GameCaptureFrameButton.tsx +1 -1
- package/src/game-document/GameDocument.tsx +2 -2
- package/src/game-document/GamePanel.tsx +4 -4
- package/src/game-document/InstanceInspectorPicker.tsx +2 -2
- package/src/game-document/crowd-debug.ts +1 -1
- package/src/game-document/device-preview.ts +10 -11
- package/src/game-document/physics-debug.ts +1 -1
- package/src/generation/GenerationActivity.tsx +3 -3
- package/src/generation/generation-documents.tsx +5 -5
- package/src/generation/generation-jobs.ts +1 -1
- package/src/host/adapter-runtime-bindings.ts +96 -5
- package/src/host/authoring/babylon-authoring-adapter.ts +6 -6
- package/src/host/authoring/gesture-persist.ts +1 -1
- package/src/host/authoring/ingest-data-writer.ts +1 -1
- package/src/host/authoring/ingest-source-persistence.ts +4 -4
- package/src/host/authoring/mounted-authoring.ts +4 -4
- package/src/host/authoring/phaser-live-authoring-adapter.ts +4 -4
- package/src/host/authoring/pixi-authoring-adapter.ts +178 -31
- package/src/host/authoring/pixi-creatable-kinds.ts +62 -0
- package/src/host/authoring/pixi-creation-site-write-target.ts +2 -2
- package/src/host/authoring/pixi-live-write-target.ts +4 -4
- package/src/host/authoring/pixi-source-identity.ts +3 -3
- package/src/host/authoring/pixi-source-write-target.ts +1709 -0
- package/src/host/authoring/pixi-still-presentation.ts +1 -1
- package/src/host/authoring/pixi-structure-history.ts +2 -2
- package/src/host/authoring/pixi-transform-channels.ts +16 -14
- package/src/host/authoring/source-persistence-backend.ts +3 -3
- package/src/host/authoring/struct-write-pipe.ts +1 -1
- package/src/host/binding-resolver.ts +8 -9
- package/src/host/browser-transpile.ts +1 -1
- package/src/host/canvas-entry-runtime.ts +58 -47
- package/src/host/canvas-preview-frames.ts +482 -0
- package/src/host/components/CameraAuthoringOverlay.tsx +1 -1
- package/src/host/components/HeaderTelemetry.tsx +4 -4
- package/src/host/components/PixiIsolationSceneContent.tsx +11 -11
- package/src/host/components/ThreeIsolationSceneContent.tsx +3 -3
- package/src/host/components/frame-debugger-model.ts +2 -2
- package/src/host/components/header-telemetry-model.ts +2 -2
- package/src/host/components/scene-document.tsx +14 -14
- package/src/host/components/utility-view-state.ts +1 -1
- package/src/host/components/world-root-stage-binding.tsx +12 -12
- package/src/host/components/world-root-stage.ts +70 -49
- package/src/host/coverage/system-adapter-coverage.ts +3 -4
- package/src/host/design-system-stories/fixtures/editor-runtime.tsx +9 -11
- package/src/host/document-preview-three.ts +1 -1
- package/src/host/entry-adjudication.ts +6 -6
- package/src/host/game-css-scope-transform.ts +4 -0
- package/src/host/game-realm-page.ts +1 -1
- package/src/host/gameplay-export.ts +25 -14
- package/src/host/gameplay-recording.ts +5 -5
- package/src/host/gated-globals.ts +2 -2
- package/src/host/history/json-history-resource.ts +1 -1
- package/src/host/projection/pixi.ts +24 -2
- package/src/host/r3f-entry-runtime.ts +65 -34
- package/src/host/react-mount-runtime.ts +7 -48
- package/src/host/realm-services.ts +1 -1
- package/src/host/roots/canvas-root.tsx +373 -0
- package/src/host/roots/r3f-root.tsx +473 -0
- package/src/host/roots/react-root.ts +9 -43
- package/src/host/served-bundle-runtime-modules.ts +3 -19
- package/src/host/server-log-bridge.ts +2 -2
- package/src/host/stories/mounted-story-viewport-source.ts +1 -1
- package/src/host/stories/pixi-story-model.ts +30 -0
- package/src/host/stories/story-media-captures.ts +46 -0
- package/src/host/stories/story-media-presence.ts +3 -3
- package/src/host/stories/story-pixi-preview.ts +408 -0
- package/src/host/stories/story-three-preview.ts +806 -0
- package/src/host/stories/three-story-captures.ts +35 -0
- package/src/host/story-three-preview-runtime.ts +56 -0
- package/src/host/use-active-performance-source.ts +2 -2
- package/src/host/viewport-pose-memory.ts +1 -1
- package/src/host/viewport-root-presentation.ts +6 -5
- package/src/ingest/active-ingest.ts +1 -1
- package/src/ingest/authoring/ingest-dom-surface-authoring.ts +4 -4
- package/src/ingest/authoring/ingest-root-adapter.ts +8 -8
- package/src/ingest/deferred-ingest-play.ts +6 -6
- package/src/ingest/discovery-public-ingest.ts +2 -2
- package/src/ingest/ingest-boot-viewport.ts +2 -2
- package/src/ingest/ingest-canvas-scene-document.tsx +9 -9
- package/src/ingest/ingest-canvas-scene.ts +3 -3
- package/src/ingest/ingest-evidence-hook.ts +1 -1
- package/src/ingest/ingest-frame-snapshot.ts +1 -1
- package/src/ingest/ingest-play-control.ts +1 -1
- package/src/ingest/ingest-render-debug.ts +10 -10
- package/src/ingest/ingest-siblings.ts +13 -25
- package/src/ingest/module-mode.ts +14 -14
- package/src/ingest/mount-canvas-ingest-root.ts +21 -21
- package/src/ingest/mount-coverage.ts +2 -2
- package/src/ingest/mount-dom-ingest-root.ts +11 -11
- package/src/ingest/mount-ingest-root.ts +8 -8
- package/src/ingest/mount-three-ingest-root.ts +8 -8
- package/src/ingest/resolve-canvas.ts +1 -1
- package/src/ingest/served-html-boot.ts +1 -1
- package/src/ingest/surface-canvas.ts +1 -1
- package/src/ingest/unmount-ingest-root.ts +5 -5
- package/src/navmesh/navmesh-handler.ts +24 -16
- package/src/network/NetworkInspectorPanel.tsx +939 -37
- package/src/network/network-inspector-model.ts +20 -2
- package/src/play/play-log-events.ts +1 -1
- package/src/play/play-mode.ts +76 -133
- package/src/play/play-recording.ts +1 -1
- package/src/play/react-play-live-authoring.ts +3 -3
- package/src/play/run-selection.ts +93 -0
- package/src/play-bar/PlayBar.tsx +20 -39
- package/src/profiler/FrameDebuggerPanel.tsx +1 -1
- package/src/profiler/PerformancePanel.tsx +3 -3
- package/src/react/design-time-react-mount.ts +23 -65
- package/src/react/dom-authoring-adapter.ts +9 -9
- package/src/react/react-inspector-section.tsx +5 -5
- package/src/react/react-world-authoring-adapter.ts +11 -11
- package/src/react/story-documents/story-documents.tsx +9 -9
- package/src/react/ui-board-document.tsx +7 -7
- package/src/react/ui-component-board.ts +2 -2
- package/src/runtime/adapter/audio-meter.ts +21 -0
- package/src/runtime/adapter/first-party-audio-system.ts +230 -0
- package/src/runtime/adapter/ingest/contract-debug-adapter.ts +114 -0
- package/src/runtime/adapter/ingest/contract-system-adapters.ts +256 -0
- package/src/runtime/adapter/ingest/merge-debug-adapters.ts +197 -0
- package/src/runtime/adapter/ingest/observation-debug-adapter.ts +162 -0
- package/src/runtime/adapter/ingest/upstream-pin.ts +51 -0
- package/src/runtime/adapter/native-debug-module.ts +498 -0
- package/src/runtime/audio/bus-mixer.ts +161 -0
- package/src/runtime/audio/pose-guard.ts +80 -0
- package/src/runtime/core/frame-pacing.ts +126 -0
- package/src/runtime/core/game-loop.ts +225 -0
- package/src/runtime/core/game-scoped-slot.ts +28 -0
- package/src/runtime/core/seeded-random.ts +162 -0
- package/src/runtime/core/sim-clock.ts +391 -0
- package/src/runtime/core/system-runner.ts +269 -0
- package/src/runtime/core/types.ts +104 -0
- package/src/runtime/create-runtime.ts +1128 -0
- package/src/runtime/debug-bridge.ts +570 -0
- package/src/runtime/debug-registry.ts +899 -0
- package/src/runtime/dev/chrome-trace.ts +153 -0
- package/src/runtime/dev/instruments.ts +403 -0
- package/src/runtime/dev/logger.ts +119 -0
- package/src/runtime/dev/performance-profiler.ts +367 -0
- package/src/runtime/dev/register-render-vitals.ts +276 -0
- package/src/runtime/dev/render-census.ts +354 -0
- package/src/runtime/dev/render-debug-adapter.ts +218 -0
- package/src/runtime/dev/render-memory.ts +226 -0
- package/src/runtime/dev/render-vitals.ts +338 -0
- package/src/runtime/dev/static-batch-advisor.ts +188 -0
- package/src/runtime/dev/webgl-frame-capture.ts +366 -0
- package/src/runtime/dev/webgl-gpu-timer.ts +53 -0
- package/src/runtime/dev-build.ts +47 -0
- package/src/runtime/game.ts +1636 -0
- package/src/runtime/gameplay-rng-trap.ts +135 -0
- package/src/runtime/host-context.ts +64 -0
- package/src/runtime/input-router.ts +182 -0
- package/src/runtime/mount-manifest.ts +480 -0
- package/src/runtime/pixi/authoring.ts +706 -0
- package/src/runtime/pixi/ingest.ts +116 -0
- package/src/runtime/pixi/physics-registry.ts +49 -0
- package/src/runtime/pixi/render-pass-bracket.ts +117 -0
- package/src/runtime/pixi/scene-capture.ts +179 -0
- package/src/runtime/pixi/system-adapters.ts +69 -0
- package/src/runtime/playtest.ts +22 -0
- package/src/runtime/presentation.ts +141 -0
- package/src/runtime/render-control.ts +642 -0
- package/src/runtime/render-seed.ts +77 -0
- package/src/runtime/run-ticks-settled.ts +73 -0
- package/src/runtime/setup/setup-audio.ts +72 -0
- package/src/services/audio-pose-guard.ts +2 -2
- package/src/services/game-audio.ts +152 -0
- package/src/services/game-network.ts +767 -0
- package/src/services/game-physics.ts +334 -0
- package/src/state-watch/StateWatchPanel.tsx +1 -1
- package/src/three/authoring/camera-runtime-inspector-section.tsx +3 -2
- package/src/three/authoring/constraint-inspector-section.tsx +6 -5
- package/src/three/authoring/model-asset-inspector-section.tsx +7 -6
- package/src/three/authoring/oid-source-persistence.ts +7 -7
- package/src/three/authoring/r3f-design-session.ts +58 -54
- package/src/three/authoring/r3f-source-authoring-adapter.ts +95 -91
- package/src/three/authoring/reflection-probe-inspector-section.tsx +3 -2
- package/src/three/authoring/three-authoring-adapter.ts +29 -29
- package/src/three/component-verbs/extract-menu.ts +7 -6
- package/src/three/component-verbs/fork-menu.ts +7 -6
- package/src/three/component-verbs/internals-menu.ts +2 -2
- package/src/three/story-documents/three-story-documents.tsx +13 -13
- package/src/three/three-board/ThreeBoardDocument.tsx +13 -12
- package/src/three/three-board/board-scene.ts +6 -6
- package/src/three/three-board/three-component-board.ts +2 -2
- package/src/services/game-audio-unlock.ts +0 -48
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
export interface PerformanceTiming {
|
|
2
|
+
readonly name: string;
|
|
3
|
+
readonly ms: number;
|
|
4
|
+
/**
|
|
5
|
+
* Offset (ms) from THIS frame's start (`frameStart`) at which the timing
|
|
6
|
+
* began — a MEASURED value (e.g. a phase's `phaseStart - frameStart`), never
|
|
7
|
+
* synthesized. Present on per-frame phase timings (W4b, so a Chrome trace
|
|
8
|
+
* can place each phase span under its frame at the real offset). OMITTED on
|
|
9
|
+
* cross-frame aggregate rollups (`PerformanceSnapshot.phases`/`systems`/
|
|
10
|
+
* `components`), where no single start point exists — absent, never a
|
|
11
|
+
* fabricated 0.
|
|
12
|
+
*/
|
|
13
|
+
readonly startMs?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* One MEASURED span within a frame (W4b) — a system or component invocation,
|
|
18
|
+
* with the phase it ran in and its measured start offset from `frameStart`.
|
|
19
|
+
* Distinct from {@link PerformanceTiming}: a span is a single timed
|
|
20
|
+
* invocation carrying `phase` + always-present `startMs` (the trace/flame
|
|
21
|
+
* unit), whereas a `PerformanceTiming` may be a cross-frame rollup.
|
|
22
|
+
*/
|
|
23
|
+
export interface PerformanceSpan {
|
|
24
|
+
readonly name: string;
|
|
25
|
+
readonly phase: string;
|
|
26
|
+
/** Measured offset (ms) from `frameStart` at which this span began. */
|
|
27
|
+
readonly startMs: number;
|
|
28
|
+
readonly ms: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Renderer counters accumulated over a frame (from `renderer.info`). */
|
|
32
|
+
export interface PerformanceRenderStats {
|
|
33
|
+
readonly gpuMs: number | null;
|
|
34
|
+
readonly drawCalls: number;
|
|
35
|
+
readonly triangles: number;
|
|
36
|
+
readonly geometries: number;
|
|
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;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface PerformanceFrame {
|
|
50
|
+
readonly id: number;
|
|
51
|
+
readonly timestamp: number;
|
|
52
|
+
readonly intervalMs: number;
|
|
53
|
+
readonly cpuMs: number;
|
|
54
|
+
readonly phases: readonly PerformanceTiming[];
|
|
55
|
+
/**
|
|
56
|
+
* Per-invocation system spans measured this frame (W4b). Ordered by
|
|
57
|
+
* `startMs` (execution order), NOT accumulated — a system that ran twice
|
|
58
|
+
* (two substeps) appears twice. Empty when the profiler saw no named
|
|
59
|
+
* systems this frame.
|
|
60
|
+
*/
|
|
61
|
+
readonly systems: readonly PerformanceSpan[];
|
|
62
|
+
/**
|
|
63
|
+
* Per-invocation component spans measured this frame (W4b) — populated ONLY
|
|
64
|
+
* while `recording` (component timing is gated on recording, matching
|
|
65
|
+
* `beginComponent`/`endComponent`). Empty otherwise.
|
|
66
|
+
*/
|
|
67
|
+
readonly components: readonly PerformanceSpan[];
|
|
68
|
+
/**
|
|
69
|
+
* Renderer counters reported during this frame (W4b) — a snapshot of the
|
|
70
|
+
* accumulated `reportRender` totals taken at `endFrame`. `gpuMs` is `null`
|
|
71
|
+
* when no GPU timer reported (headless/SwiftShader), never a fabricated 0.
|
|
72
|
+
*/
|
|
73
|
+
readonly render: PerformanceRenderStats;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface PerformanceSnapshot {
|
|
77
|
+
readonly enabled: boolean;
|
|
78
|
+
readonly recording: boolean;
|
|
79
|
+
readonly fps: number;
|
|
80
|
+
readonly cpuMs: number;
|
|
81
|
+
readonly p95Ms: number;
|
|
82
|
+
readonly p99Ms: number;
|
|
83
|
+
readonly frames: readonly PerformanceFrame[];
|
|
84
|
+
readonly phases: readonly PerformanceTiming[];
|
|
85
|
+
readonly systems: readonly PerformanceTiming[];
|
|
86
|
+
readonly components: readonly PerformanceTiming[];
|
|
87
|
+
readonly render: PerformanceRenderStats;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const now = () => globalThis.performance?.now() ?? Date.now();
|
|
91
|
+
|
|
92
|
+
function percentile(sorted: readonly number[], fraction: number): number {
|
|
93
|
+
if (sorted.length === 0) return 0;
|
|
94
|
+
return sorted[Math.min(sorted.length - 1, Math.floor((sorted.length - 1) * fraction))] ?? 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Game-owned, bounded external store for browser-native performance diagnostics. */
|
|
98
|
+
export function createPerformanceProfiler(initiallyEnabled = false) {
|
|
99
|
+
const listeners = new Set<() => void>();
|
|
100
|
+
const frames: PerformanceFrame[] = [];
|
|
101
|
+
const currentPhases = new Map<string, { ms: number; startMs: number }>();
|
|
102
|
+
const currentSystems = new Map<string, number>();
|
|
103
|
+
const currentComponents = new Map<string, number>();
|
|
104
|
+
// Per-frame span logs (W4b) — cleared each beginFrame, snapshotted into the
|
|
105
|
+
// frame at endFrame. Distinct from the accumulate-by-name aggregate maps
|
|
106
|
+
// above (which back the cross-frame snapshot rollups): these preserve every
|
|
107
|
+
// individual invocation and its measured start offset, for the trace.
|
|
108
|
+
let frameSystems: PerformanceSpan[] = [];
|
|
109
|
+
let frameComponents: PerformanceSpan[] = [];
|
|
110
|
+
let enabled = initiallyEnabled;
|
|
111
|
+
// The header's always-visible FPS sparkline needs frame INTERVALS without
|
|
112
|
+
// turning on the full profiler. Full profiling measures phases/systems,
|
|
113
|
+
// issues GPU timer queries at render sites, and intentionally warns about
|
|
114
|
+
// overhead; a glanceable heartbeat must not silently opt the user into all
|
|
115
|
+
// of that work. `telemetryEnabled` records only the frame envelope.
|
|
116
|
+
let telemetryEnabled = false;
|
|
117
|
+
let recording = false;
|
|
118
|
+
let frameId = 0;
|
|
119
|
+
let frameStart = 0;
|
|
120
|
+
let lastFrameStart = 0;
|
|
121
|
+
/**
|
|
122
|
+
* Phase start times, innermost last. A STACK rather than the single slot
|
|
123
|
+
* this used to be, because phases NEST: the three adapter brackets its CPU
|
|
124
|
+
* render submission as its own phase (`render.submit`,
|
|
125
|
+
* `dev/render-vitals.ts`) from inside the frame's enclosing `render` phase.
|
|
126
|
+
* With one slot the inner `beginPhase()` overwrote the outer's start, so the
|
|
127
|
+
* outer `endPhase('render')` measured only the tail after the inner phase —
|
|
128
|
+
* silently under-reporting the very phase the inner one was decomposing.
|
|
129
|
+
* Balanced callers are unaffected; `endPhase` with an empty stack falls back
|
|
130
|
+
* to `frameStart` rather than a stale start from a previous frame.
|
|
131
|
+
*/
|
|
132
|
+
const phaseStack: number[] = [];
|
|
133
|
+
let systemStart = 0;
|
|
134
|
+
let componentStart = 0;
|
|
135
|
+
let render: PerformanceRenderStats = {
|
|
136
|
+
gpuMs: null,
|
|
137
|
+
drawCalls: 0,
|
|
138
|
+
triangles: 0,
|
|
139
|
+
geometries: 0,
|
|
140
|
+
textures: 0,
|
|
141
|
+
renderPasses: null,
|
|
142
|
+
};
|
|
143
|
+
let snapshot: PerformanceSnapshot = {
|
|
144
|
+
enabled,
|
|
145
|
+
recording,
|
|
146
|
+
fps: 0,
|
|
147
|
+
cpuMs: 0,
|
|
148
|
+
p95Ms: 0,
|
|
149
|
+
p99Ms: 0,
|
|
150
|
+
frames: [],
|
|
151
|
+
phases: [],
|
|
152
|
+
systems: [],
|
|
153
|
+
components: [],
|
|
154
|
+
render,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
function publish(): void {
|
|
158
|
+
const latest = frames.at(-1);
|
|
159
|
+
// Frame-only subscribers need no percentile work. Keep this window stable
|
|
160
|
+
// so a retained snapshot's first percentile read still describes its frames.
|
|
161
|
+
const snapshotFrames = frames.slice();
|
|
162
|
+
let sortedIntervals: number[] | undefined;
|
|
163
|
+
const readPercentile = (fraction: number): number => {
|
|
164
|
+
sortedIntervals ??= snapshotFrames
|
|
165
|
+
.map((frame) => frame.intervalMs)
|
|
166
|
+
.filter((value) => value > 0)
|
|
167
|
+
.sort((a, b) => a - b);
|
|
168
|
+
return percentile(sortedIntervals, fraction);
|
|
169
|
+
};
|
|
170
|
+
snapshot = {
|
|
171
|
+
enabled,
|
|
172
|
+
recording,
|
|
173
|
+
fps: latest?.intervalMs ? 1000 / latest.intervalMs : 0,
|
|
174
|
+
cpuMs: latest?.cpuMs ?? 0,
|
|
175
|
+
get p95Ms() {
|
|
176
|
+
return readPercentile(0.95);
|
|
177
|
+
},
|
|
178
|
+
get p99Ms() {
|
|
179
|
+
return readPercentile(0.99);
|
|
180
|
+
},
|
|
181
|
+
frames: snapshotFrames,
|
|
182
|
+
phases: latest?.phases ?? [],
|
|
183
|
+
systems: [...currentSystems].map(([name, ms]) => ({ name, ms })).sort((a, b) => b.ms - a.ms),
|
|
184
|
+
components: [...currentComponents]
|
|
185
|
+
.map(([name, ms]) => ({ name, ms }))
|
|
186
|
+
.sort((a, b) => b.ms - a.ms),
|
|
187
|
+
render,
|
|
188
|
+
};
|
|
189
|
+
for (const listener of listeners) listener();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
get enabled() {
|
|
194
|
+
return enabled;
|
|
195
|
+
},
|
|
196
|
+
set enabled(value: boolean) {
|
|
197
|
+
if (enabled === value) return;
|
|
198
|
+
enabled = value;
|
|
199
|
+
if (value && !telemetryEnabled) lastFrameStart = 0;
|
|
200
|
+
publish();
|
|
201
|
+
},
|
|
202
|
+
get telemetryEnabled() {
|
|
203
|
+
return telemetryEnabled;
|
|
204
|
+
},
|
|
205
|
+
set telemetryEnabled(value: boolean) {
|
|
206
|
+
if (telemetryEnabled === value) return;
|
|
207
|
+
const wasInactive = !enabled && !telemetryEnabled;
|
|
208
|
+
telemetryEnabled = value;
|
|
209
|
+
if (value && wasInactive) lastFrameStart = 0;
|
|
210
|
+
publish();
|
|
211
|
+
},
|
|
212
|
+
get recording() {
|
|
213
|
+
return recording;
|
|
214
|
+
},
|
|
215
|
+
startRecording() {
|
|
216
|
+
const wasInactive = !enabled && !telemetryEnabled;
|
|
217
|
+
enabled = true;
|
|
218
|
+
if (wasInactive) lastFrameStart = 0;
|
|
219
|
+
recording = true;
|
|
220
|
+
frames.length = 0;
|
|
221
|
+
publish();
|
|
222
|
+
},
|
|
223
|
+
stopRecording() {
|
|
224
|
+
recording = false;
|
|
225
|
+
publish();
|
|
226
|
+
},
|
|
227
|
+
clear() {
|
|
228
|
+
frames.length = 0;
|
|
229
|
+
publish();
|
|
230
|
+
},
|
|
231
|
+
beginFrame() {
|
|
232
|
+
if (!enabled && !telemetryEnabled) return;
|
|
233
|
+
currentPhases.clear();
|
|
234
|
+
currentSystems.clear();
|
|
235
|
+
currentComponents.clear();
|
|
236
|
+
frameSystems = [];
|
|
237
|
+
frameComponents = [];
|
|
238
|
+
// A frame that threw mid-phase leaves entries on the stack; a fresh
|
|
239
|
+
// frame starts from nothing rather than inheriting them.
|
|
240
|
+
phaseStack.length = 0;
|
|
241
|
+
render = {
|
|
242
|
+
gpuMs: null,
|
|
243
|
+
drawCalls: 0,
|
|
244
|
+
triangles: 0,
|
|
245
|
+
geometries: 0,
|
|
246
|
+
textures: 0,
|
|
247
|
+
renderPasses: null,
|
|
248
|
+
};
|
|
249
|
+
frameStart = now();
|
|
250
|
+
},
|
|
251
|
+
beginPhase() {
|
|
252
|
+
if (!enabled) return;
|
|
253
|
+
phaseStack.push(now());
|
|
254
|
+
},
|
|
255
|
+
endPhase(name: string) {
|
|
256
|
+
if (!enabled) return;
|
|
257
|
+
// See `phaseStack`: pop restores the ENCLOSING phase's own start, so a
|
|
258
|
+
// nested bracket measures itself without truncating its parent.
|
|
259
|
+
const phaseStart = phaseStack.pop() ?? frameStart;
|
|
260
|
+
const elapsed = now() - phaseStart;
|
|
261
|
+
const existing = currentPhases.get(name);
|
|
262
|
+
// Accumulate ms across re-entries of the same phase, but keep the FIRST
|
|
263
|
+
// measured start offset (earliest entry) as the span's startMs.
|
|
264
|
+
currentPhases.set(name, {
|
|
265
|
+
ms: (existing?.ms ?? 0) + elapsed,
|
|
266
|
+
startMs: existing?.startMs ?? phaseStart - frameStart,
|
|
267
|
+
});
|
|
268
|
+
if (recording)
|
|
269
|
+
performance.measure(`vgai.phase.${name}`, { start: phaseStart, duration: elapsed });
|
|
270
|
+
},
|
|
271
|
+
systemObserver: {
|
|
272
|
+
beginSystem() {
|
|
273
|
+
if (!enabled) return;
|
|
274
|
+
systemStart = now();
|
|
275
|
+
},
|
|
276
|
+
endSystem(scope: string, phase: string, name: string) {
|
|
277
|
+
if (!enabled) return;
|
|
278
|
+
const end = now();
|
|
279
|
+
const key = `${scope} / ${phase} / ${name}`;
|
|
280
|
+
currentSystems.set(key, (currentSystems.get(key) ?? 0) + end - systemStart);
|
|
281
|
+
frameSystems.push({
|
|
282
|
+
name: key,
|
|
283
|
+
phase,
|
|
284
|
+
startMs: systemStart - frameStart,
|
|
285
|
+
ms: end - systemStart,
|
|
286
|
+
});
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
beginComponent() {
|
|
290
|
+
if (!recording) return;
|
|
291
|
+
componentStart = now();
|
|
292
|
+
},
|
|
293
|
+
endComponent(name: string, phase = '') {
|
|
294
|
+
if (!recording) return;
|
|
295
|
+
const end = now();
|
|
296
|
+
currentComponents.set(name, (currentComponents.get(name) ?? 0) + end - componentStart);
|
|
297
|
+
frameComponents.push({
|
|
298
|
+
name,
|
|
299
|
+
phase,
|
|
300
|
+
startMs: componentStart - frameStart,
|
|
301
|
+
ms: end - componentStart,
|
|
302
|
+
});
|
|
303
|
+
},
|
|
304
|
+
reportRender(stats: {
|
|
305
|
+
gpuMs: number | null;
|
|
306
|
+
drawCalls: number;
|
|
307
|
+
triangles: number;
|
|
308
|
+
geometries: number;
|
|
309
|
+
textures: number;
|
|
310
|
+
/** Omit when this reporter does not count passes — see
|
|
311
|
+
* {@link PerformanceRenderStats.renderPasses}. */
|
|
312
|
+
renderPasses?: number;
|
|
313
|
+
}) {
|
|
314
|
+
if (!enabled) return;
|
|
315
|
+
render = {
|
|
316
|
+
gpuMs: stats.gpuMs ?? render.gpuMs,
|
|
317
|
+
drawCalls: render.drawCalls + stats.drawCalls,
|
|
318
|
+
triangles: render.triangles + stats.triangles,
|
|
319
|
+
geometries: render.geometries + stats.geometries,
|
|
320
|
+
textures: render.textures + stats.textures,
|
|
321
|
+
renderPasses:
|
|
322
|
+
stats.renderPasses === undefined
|
|
323
|
+
? render.renderPasses
|
|
324
|
+
: (render.renderPasses ?? 0) + stats.renderPasses,
|
|
325
|
+
};
|
|
326
|
+
},
|
|
327
|
+
endFrame() {
|
|
328
|
+
if (!enabled && !telemetryEnabled) return;
|
|
329
|
+
const timestamp = now();
|
|
330
|
+
const intervalMs = lastFrameStart === 0 ? 0 : frameStart - lastFrameStart;
|
|
331
|
+
lastFrameStart = frameStart;
|
|
332
|
+
const frame: PerformanceFrame = {
|
|
333
|
+
id: ++frameId,
|
|
334
|
+
timestamp,
|
|
335
|
+
intervalMs,
|
|
336
|
+
cpuMs: timestamp - frameStart,
|
|
337
|
+
phases: [...currentPhases]
|
|
338
|
+
.map(([name, { ms, startMs }]) => ({ name, ms, startMs }))
|
|
339
|
+
.sort((a, b) => b.ms - a.ms),
|
|
340
|
+
systems: frameSystems.slice(),
|
|
341
|
+
components: frameComponents.slice(),
|
|
342
|
+
render: { ...render },
|
|
343
|
+
};
|
|
344
|
+
frames.push(frame);
|
|
345
|
+
if (frames.length > 600) frames.splice(0, frames.length - 600);
|
|
346
|
+
if (recording)
|
|
347
|
+
performance.measure('vgai.frame', { start: frameStart, duration: frame.cpuMs });
|
|
348
|
+
publish();
|
|
349
|
+
},
|
|
350
|
+
subscribe(listener: () => void) {
|
|
351
|
+
listeners.add(listener);
|
|
352
|
+
return () => {
|
|
353
|
+
listeners.delete(listener);
|
|
354
|
+
};
|
|
355
|
+
},
|
|
356
|
+
getSnapshot() {
|
|
357
|
+
return snapshot;
|
|
358
|
+
},
|
|
359
|
+
exportJSON() {
|
|
360
|
+
// version 2 (W4b): frames now carry per-frame `systems`/`components`
|
|
361
|
+
// spans, per-frame `render` counters, and `startMs` on phase timings.
|
|
362
|
+
return JSON.stringify({ version: 2, capturedAt: new Date().toISOString(), frames }, null, 2);
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export type PerformanceProfiler = ReturnType<typeof createPerformanceProfiler>;
|
|
@@ -0,0 +1,276 @@
|
|
|
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 BOTH three-root mount paths — a game
|
|
10
|
+
* writes nothing to get these, and there is no capability to install:
|
|
11
|
+
* - `editor-game/src/host/roots/r3f-root.tsx` (a TSX/R3F world — the shape EVERY
|
|
12
|
+
* scaffolded project's three root has), right after fiber's first commit
|
|
13
|
+
* resolves the scene.
|
|
14
|
+
* Both is the whole point and not redundancy: they are two independent mounts
|
|
15
|
+
* onto the same `Game`, and a door wired into only one of them is a door half
|
|
16
|
+
* the engine's worlds never get (issue #1518 — the R3F half was dark
|
|
17
|
+
* everywhere, found in the field on a package-native project).
|
|
18
|
+
* The measurement half is `dev/render-vitals.ts`
|
|
19
|
+
* (derived entirely from profiler frames); the address-book half is
|
|
20
|
+
* `dev/render-census.ts` (pure walks over the live scene). This module is only
|
|
21
|
+
* the wiring between them and the registry.
|
|
22
|
+
*
|
|
23
|
+
* ── THE READING AND THE EXPERIMENTS ─────────────────────────────────────────
|
|
24
|
+
* A number alone does not route an investigation, so each command is the
|
|
25
|
+
* experiment that turns one reading into an address:
|
|
26
|
+
* - `render.census` — which subtree owns the meshes and triangles, with a
|
|
27
|
+
* drill-down argument;
|
|
28
|
+
* - `render.families` — which meshes are identical enough to instance;
|
|
29
|
+
* - `render.toggle` — hide a subtree and watch the stat move (the only way
|
|
30
|
+
* to ATTRIBUTE cost rather than infer it);
|
|
31
|
+
* - `render.dpr` — change the pixel count, which separates GPU fill cost
|
|
32
|
+
* from vsync idle: render CPU that does not move when
|
|
33
|
+
* the drawing buffer halves was never fill-bound.
|
|
34
|
+
*
|
|
35
|
+
* ── GATING ──────────────────────────────────────────────────────────────────
|
|
36
|
+
* The caller gates. Both seeders call this only under `devBuildEnabled()`
|
|
37
|
+
* (`runtime/dev-build.ts` — the ONE owner of "is this a dev context"), so a
|
|
38
|
+
* ship build registers nothing and pays nothing. Each also requires a `Game`
|
|
39
|
+
* shell (there is no profiler to fold without one) and a non-headless mount
|
|
40
|
+
* (nothing brackets a submission, so the readings could only ever be empty).
|
|
41
|
+
*
|
|
42
|
+
* ── RESOURCE OWNERSHIP ──────────────────────────────────────────────────────
|
|
43
|
+
* OWNER: {@link registerRenderVitals}'s caller. It allocates one
|
|
44
|
+
* {@link RenderVitals} fold (which owns one `profiler.subscribe`), a SECOND,
|
|
45
|
+
* self-ending `profiler.subscribe` for the static-batch advisory, and the
|
|
46
|
+
* registrations, whose provenance is the caller's `worldId`. SHARERS: none.
|
|
47
|
+
* TEARDOWN: the returned `dispose()` — the ONE path that ends both
|
|
48
|
+
* subscriptions. The REGISTRATIONS are ended by the registry's own
|
|
49
|
+
* `strip(worldId)`, which the adapter already runs on stop and warm restart;
|
|
50
|
+
* this module never strips, because a blind strip here would take the game's
|
|
51
|
+
* registrations with it.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
import * as THREE from 'three';
|
|
55
|
+
import { z } from 'zod';
|
|
56
|
+
import type { DebugRegistry } from '../debug-registry';
|
|
57
|
+
import type { PerformanceProfiler } from './performance-profiler';
|
|
58
|
+
import { findByName, meshFamilies, sceneCensus, structuralBatchScan } from './render-census';
|
|
59
|
+
import { createRenderVitals, type RenderVitals } from './render-vitals';
|
|
60
|
+
import {
|
|
61
|
+
ADVISOR_SETTLE_FRAMES,
|
|
62
|
+
decideStaticBatchAdvisory,
|
|
63
|
+
warnStaticBatchAdvisory,
|
|
64
|
+
} from './static-batch-advisor';
|
|
65
|
+
|
|
66
|
+
/** The one provider name — spelled here, referenced everywhere else. */
|
|
67
|
+
export const RENDER_VITALS_PROVIDER = 'render.vitals';
|
|
68
|
+
export const RENDER_CENSUS_COMMAND = 'render.census';
|
|
69
|
+
export const RENDER_FAMILIES_COMMAND = 'render.families';
|
|
70
|
+
export const RENDER_TOGGLE_COMMAND = 'render.toggle';
|
|
71
|
+
export const RENDER_DPR_COMMAND = 'render.dpr';
|
|
72
|
+
|
|
73
|
+
/** The slice of `THREE.WebGLRenderer` `render.dpr` actuates. Narrow on
|
|
74
|
+
* purpose: a headless mount has no renderer at all and passes `null`. */
|
|
75
|
+
export interface RenderVitalsRenderer {
|
|
76
|
+
getSize(target: THREE.Vector2): THREE.Vector2;
|
|
77
|
+
setPixelRatio(value: number): void;
|
|
78
|
+
setSize(width: number, height: number, updateStyle?: boolean): void;
|
|
79
|
+
getPixelRatio(): number;
|
|
80
|
+
readonly domElement: { readonly width: number; readonly height: number };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface RenderVitalsRegistration {
|
|
84
|
+
/** The live fold, exposed so a test can drive it without a registry. */
|
|
85
|
+
readonly vitals: RenderVitals;
|
|
86
|
+
/** See the ownership note — the ONE teardown path. Idempotent. */
|
|
87
|
+
dispose(): void;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface RegisterRenderVitalsDeps {
|
|
91
|
+
readonly registry: DebugRegistry;
|
|
92
|
+
/** Registration provenance — the mount's adapter id, so the adapter's own
|
|
93
|
+
* `strip(worldId)` on stop/warm-restart removes exactly these. */
|
|
94
|
+
readonly worldId: string;
|
|
95
|
+
readonly profiler: PerformanceProfiler;
|
|
96
|
+
/** The live scene the census walks. */
|
|
97
|
+
readonly scene: THREE.Object3D;
|
|
98
|
+
/** `null` under a headless mount — `render.dpr` then refuses by name rather
|
|
99
|
+
* than silently doing nothing. */
|
|
100
|
+
readonly renderer: RenderVitalsRenderer | null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const NO_RENDERER =
|
|
104
|
+
'render.dpr: this world has no WebGL renderer (headless mount), so there is no pixel ratio ' +
|
|
105
|
+
'to change. The other render.* commands still work — they read the scene graph.';
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Seed the vitals door for one world. Returns `null` when another live world
|
|
109
|
+
* already owns the door: the names are fixed and discoverable on purpose, and
|
|
110
|
+
* the registry (correctly) throws on a cross-world name collision, so the
|
|
111
|
+
* FIRST three root to mount owns them. A second three root's draws still show
|
|
112
|
+
* up in the readings — the profiler is game-scoped, so every world's
|
|
113
|
+
* submission folds into the same frame — but the census commands address the
|
|
114
|
+
* owning root's scene.
|
|
115
|
+
*/
|
|
116
|
+
export function registerRenderVitals(
|
|
117
|
+
deps: RegisterRenderVitalsDeps,
|
|
118
|
+
): RenderVitalsRegistration | null {
|
|
119
|
+
const { registry, worldId, profiler, scene, renderer } = deps;
|
|
120
|
+
const taken = registry.adapter
|
|
121
|
+
.providers()
|
|
122
|
+
.some((provider) => provider.name === RENDER_VITALS_PROVIDER);
|
|
123
|
+
if (taken) {
|
|
124
|
+
// Loud, by name. This branch used to return `null` in silence, and a
|
|
125
|
+
// silent decline is indistinguishable from a door that was never seeded —
|
|
126
|
+
// which is precisely how issue #1518 stayed a mystery through a field
|
|
127
|
+
// session: every hypothesis about WHY the door was closed had to be
|
|
128
|
+
// guessed, because nothing on any path said anything. It costs one line to
|
|
129
|
+
// make the one legitimate decline explain itself, and the same line names
|
|
130
|
+
// the mount/teardown OVERLAP case (a re-mount seeding before the outgoing
|
|
131
|
+
// mount's `strip()` runs), where the second world would otherwise lose the
|
|
132
|
+
// door and no log would mention it.
|
|
133
|
+
console.warn(
|
|
134
|
+
`[render-vitals] world "${worldId}" did not get the ${RENDER_VITALS_PROVIDER} door: ` +
|
|
135
|
+
'another live world already owns it (the names are fixed, so the first three root to ' +
|
|
136
|
+
'mount wins). Its draws still fold into the readings — the profiler is game-scoped — ' +
|
|
137
|
+
'but render.census/families/toggle address the owning root. If this world IS the ' +
|
|
138
|
+
'current one, the previous mount was torn down AFTER this one seeded and its ' +
|
|
139
|
+
'strip() took the door with it.',
|
|
140
|
+
);
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const vitals = createRenderVitals(profiler);
|
|
145
|
+
const debug = registry.forRoot(worldId);
|
|
146
|
+
|
|
147
|
+
debug.registerStateProvider(RENDER_VITALS_PROVIDER, () => vitals.read());
|
|
148
|
+
|
|
149
|
+
debug.registerCommand(
|
|
150
|
+
RENDER_CENSUS_COMMAND,
|
|
151
|
+
{
|
|
152
|
+
description:
|
|
153
|
+
'Visible meshes and triangles per subtree — the address book behind the draw-call ' +
|
|
154
|
+
'reading. Pass a subtree name (as listed by a previous census) to drill into it.',
|
|
155
|
+
args: z.tuple([z.string().optional()]),
|
|
156
|
+
locus: 'client',
|
|
157
|
+
},
|
|
158
|
+
(subtree) => {
|
|
159
|
+
if (subtree === undefined || subtree === '') return sceneCensus(scene);
|
|
160
|
+
const node = findByName(scene, subtree);
|
|
161
|
+
if (!node) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`render.census: no node named "${subtree}" in this world. Run render.census with no ` +
|
|
164
|
+
'argument to list the addressable subtrees.',
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return sceneCensus(node, subtree);
|
|
168
|
+
},
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
debug.registerCommand(
|
|
172
|
+
RENDER_FAMILIES_COMMAND,
|
|
173
|
+
{
|
|
174
|
+
description:
|
|
175
|
+
'Meshes grouped by shared (geometry, material) identity, largest group first — the ' +
|
|
176
|
+
'instancing shortlist for a draw-call diet. `structural` is the same scan keyed by ' +
|
|
177
|
+
'VALUE instead of object identity: what would collapse under a <Frozen> wrapper ' +
|
|
178
|
+
'(vgai add static-batch) even when every mesh carries its own inline material.',
|
|
179
|
+
args: z.tuple([]),
|
|
180
|
+
locus: 'client',
|
|
181
|
+
},
|
|
182
|
+
// Two readings, deliberately: the identity families (what can be instanced
|
|
183
|
+
// as the scene stands) and the STRUCTURAL scan (what would collapse if the
|
|
184
|
+
// same draws were batched by value). A world writing inline materials —
|
|
185
|
+
// every fresh scaffold — has families of one and a large structural win,
|
|
186
|
+
// and reporting only the first would say "nothing to do" about the exact
|
|
187
|
+
// scene the `static-batch` capability exists for.
|
|
188
|
+
() => ({ ...meshFamilies(scene), structural: structuralBatchScan(scene) }),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
debug.registerCommand(
|
|
192
|
+
RENDER_TOGGLE_COMMAND,
|
|
193
|
+
{
|
|
194
|
+
description:
|
|
195
|
+
'Show/hide a named subtree (as listed by render.census) and read render.vitals again — ' +
|
|
196
|
+
'the difference is what that subtree costs.',
|
|
197
|
+
args: z.tuple([z.string()]),
|
|
198
|
+
locus: 'client',
|
|
199
|
+
},
|
|
200
|
+
(name) => {
|
|
201
|
+
const node = findByName(scene, name);
|
|
202
|
+
if (!node) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`render.toggle: no node named "${name}" in this world. Run render.census to list the ` +
|
|
205
|
+
'addressable subtrees.',
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
node.visible = !node.visible;
|
|
209
|
+
return { name, visible: node.visible };
|
|
210
|
+
},
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
debug.registerCommand(
|
|
214
|
+
RENDER_DPR_COMMAND,
|
|
215
|
+
{
|
|
216
|
+
description:
|
|
217
|
+
'Set the renderer pixel ratio (retina default is ~2) to probe GPU fill cost — render CPU ' +
|
|
218
|
+
'that does not move when the drawing buffer shrinks was never fill-bound.',
|
|
219
|
+
args: z.tuple([z.number().positive().max(8)]),
|
|
220
|
+
locus: 'client',
|
|
221
|
+
},
|
|
222
|
+
(ratio) => {
|
|
223
|
+
if (!renderer) throw new Error(NO_RENDERER);
|
|
224
|
+
const size = renderer.getSize(new THREE.Vector2());
|
|
225
|
+
renderer.setPixelRatio(ratio);
|
|
226
|
+
// `updateStyle: false` — the CSS size is the layout's, not ours to
|
|
227
|
+
// change; only the drawing buffer moves, which is the whole experiment.
|
|
228
|
+
renderer.setSize(size.x, size.y, false);
|
|
229
|
+
return {
|
|
230
|
+
pixelRatio: renderer.getPixelRatio(),
|
|
231
|
+
drawingBuffer: { width: renderer.domElement.width, height: renderer.domElement.height },
|
|
232
|
+
};
|
|
233
|
+
},
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// --- The static-batch advisory (issue #1503) -------------------------------
|
|
237
|
+
// A reading nobody asks for is a reading nobody gets, so the measurement
|
|
238
|
+
// routes itself: once the frame rate has settled, scan ONCE, and if this
|
|
239
|
+
// world is paying for draws it does not have to, name the exact wrapper on
|
|
240
|
+
// the console (`dev/static-batch-advisor.ts` owns the decision and the
|
|
241
|
+
// wording; this owns only WHEN).
|
|
242
|
+
//
|
|
243
|
+
// It rides the profiler subscription rather than a timer because the trigger
|
|
244
|
+
// is presented frames, not wall time: a tab that never presents (hidden,
|
|
245
|
+
// headless) should never scan, and a slow boot should not be measured before
|
|
246
|
+
// its assets are in the graph. The subscription ends itself the moment it
|
|
247
|
+
// fires or the advisory turns out to be `null`.
|
|
248
|
+
let stopAdvisor: (() => void) | null = null;
|
|
249
|
+
let advised = false;
|
|
250
|
+
const advisorTick = (): void => {
|
|
251
|
+
if (advised) return;
|
|
252
|
+
if (vitals.read().presentedFrames < ADVISOR_SETTLE_FRAMES) return;
|
|
253
|
+
advised = true;
|
|
254
|
+
stopAdvisor?.();
|
|
255
|
+
stopAdvisor = null;
|
|
256
|
+
const advisory = decideStaticBatchAdvisory({
|
|
257
|
+
drawCalls: vitals.read().drawCalls,
|
|
258
|
+
census: sceneCensus(scene),
|
|
259
|
+
structural: structuralBatchScan(scene),
|
|
260
|
+
});
|
|
261
|
+
if (advisory) warnStaticBatchAdvisory(advisory);
|
|
262
|
+
};
|
|
263
|
+
stopAdvisor = profiler.subscribe(advisorTick);
|
|
264
|
+
|
|
265
|
+
let disposed = false;
|
|
266
|
+
return {
|
|
267
|
+
vitals,
|
|
268
|
+
dispose() {
|
|
269
|
+
if (disposed) return;
|
|
270
|
+
disposed = true;
|
|
271
|
+
stopAdvisor?.();
|
|
272
|
+
stopAdvisor = null;
|
|
273
|
+
vitals.dispose();
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
}
|