@volter/editor-game 0.5.66 → 0.5.67
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/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 +20 -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 +3 -3
- package/src/canvas/canvas-board/CanvasBoardDocument.tsx +676 -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 +40 -15
- 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 +1614 -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 +10 -10
- 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 +361 -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 +1 -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 +8 -8
- 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 +449 -14
- package/src/network/network-inspector-model.ts +14 -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 +169 -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 +657 -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,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE ROUTING from a draw-call reading to the one-line fix.
|
|
3
|
+
*
|
|
4
|
+
* `dev/render-vitals.ts` can already tell a game it is submitting 4,000 draws
|
|
5
|
+
* and `dev/render-census.ts` can already say which subtree they are in. Both
|
|
6
|
+
* require someone to ASK, and both require that someone to already know that
|
|
7
|
+
* static batching exists, is possible here, and is spelled `<Frozen>`. An
|
|
8
|
+
* agent building a game does not know any of that, so the measurement has to
|
|
9
|
+
* do the routing itself — the same idiom as dev-tools' unconfigured-section
|
|
10
|
+
* warning: the reading names the exact edit.
|
|
11
|
+
*
|
|
12
|
+
* ── THE DECISION IS PURE, THE SCHEDULE IS NOT ───────────────────────────────
|
|
13
|
+
* {@link decideStaticBatchAdvisory} takes a draw-call count and two scan
|
|
14
|
+
* reports and answers with an advisory or `null`. It reads no clock, no
|
|
15
|
+
* scene, no console. `editor-game/src/runtime/dev/register-render-vitals.ts` owns the impure half —
|
|
16
|
+
* when to scan, and warning once — because that is where the profiler
|
|
17
|
+
* subscription already lives.
|
|
18
|
+
*
|
|
19
|
+
* ── COST ────────────────────────────────────────────────────────────────────
|
|
20
|
+
* The scan is TWO walks of the scene graph, once, after the frame rate has
|
|
21
|
+
* settled ({@link ADVISOR_SETTLE_FRAMES}). Never per frame: a walk of 4,000
|
|
22
|
+
* nodes every frame is itself the kind of cost this advisory exists to
|
|
23
|
+
* remove, and the answer does not change from one frame to the next in a world
|
|
24
|
+
* whose scenery is mount-static — which is the only world the advice applies
|
|
25
|
+
* to anyway.
|
|
26
|
+
*
|
|
27
|
+
* ── WHY IT ASKS INSTEAD OF ACTING ───────────────────────────────────────────
|
|
28
|
+
* "These 2,600 meshes are the same draw" is measurable. "These 2,600 meshes
|
|
29
|
+
* never move" is NOT — nothing in a scene graph distinguishes scenery from a
|
|
30
|
+
* thing that will move on the next input. Inferring it and batching anyway is
|
|
31
|
+
* how a batcher freezes a door half-open. So the advisory names the subtree
|
|
32
|
+
* and the wrapper, and the author (who knows) places it.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import type { CensusReport, StructuralBatchReport } from './render-census';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Draw calls below which no advisory fires, however batchable the scene.
|
|
39
|
+
*
|
|
40
|
+
* The field measurement this capability came out of: ~4,000 draws cost ~11 ms
|
|
41
|
+
* of CPU submission per frame — about 2.75 µs each on a desktop browser. At
|
|
42
|
+
* 500 draws that is ~1.4 ms, roughly 8% of a 60 Hz frame: the first point
|
|
43
|
+
* where halving it is a visible win rather than noise a profiler cannot
|
|
44
|
+
* separate from jitter. Below it, an advisory would be a nag pointing at
|
|
45
|
+
* something that is not costing anything, and a nag that is usually wrong is
|
|
46
|
+
* one nobody reads when it is right.
|
|
47
|
+
*/
|
|
48
|
+
export const ADVISOR_DRAW_CALL_THRESHOLD = 500;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The share of the draw calls that must be collapsible before the advice is
|
|
52
|
+
* worth an edit. Half: below that, the wrapper leaves most of the cost exactly
|
|
53
|
+
* where it was, and "you could remove a third of a third" is not a payoff
|
|
54
|
+
* anyone should restructure a scene for.
|
|
55
|
+
*/
|
|
56
|
+
export const ADVISOR_COLLAPSIBLE_SHARE = 0.5;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A subtree must hold at least this share of the collapsible meshes to be
|
|
60
|
+
* named as THE address. Under it the advisory says "across the scene" — an
|
|
61
|
+
* invented address is worse than none, because the reader wraps the wrong
|
|
62
|
+
* group and measures no change.
|
|
63
|
+
*/
|
|
64
|
+
export const ADVISOR_SUBTREE_SHARE = 0.4;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Presented frames to wait before scanning — ~2 s at 60 Hz. Long enough for
|
|
68
|
+
* asset loads and the first setup pass to finish populating the graph (a scan
|
|
69
|
+
* at frame one measures an empty world and stays silent forever), short enough
|
|
70
|
+
* that the line lands while the author is still looking at the boot.
|
|
71
|
+
*/
|
|
72
|
+
export const ADVISOR_SETTLE_FRAMES = 120;
|
|
73
|
+
|
|
74
|
+
/** The finding, as a caller can present it however it likes. */
|
|
75
|
+
export interface StaticBatchAdvisory {
|
|
76
|
+
/** The reading that triggered it. */
|
|
77
|
+
readonly drawCalls: number;
|
|
78
|
+
/** Meshes sitting in a structural family of two or more. */
|
|
79
|
+
readonly collapsible: number;
|
|
80
|
+
/** The subtree holding most of them, or `null` when they are spread out. */
|
|
81
|
+
readonly subtree: string | null;
|
|
82
|
+
/** The exact edit, ready to paste — `<Frozen name="Terminal">`. */
|
|
83
|
+
readonly fix: string;
|
|
84
|
+
/** The one-line console message: payoff, address, edit, install. */
|
|
85
|
+
readonly message: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface StaticBatchAdvisoryInput {
|
|
89
|
+
/** `render.vitals`' own reading. `null` before the first presented frame. */
|
|
90
|
+
readonly drawCalls: number | null;
|
|
91
|
+
/** The one-level census, used only to sanity-check the address against a
|
|
92
|
+
* subtree that genuinely exists in the graph. */
|
|
93
|
+
readonly census: CensusReport;
|
|
94
|
+
/** The STRUCTURAL scan — see `render-census.ts` for why the identity scan
|
|
95
|
+
* cannot answer this. */
|
|
96
|
+
readonly structural: StructuralBatchReport;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** `2600` → `2,600`. Digits a person reads at a glance, in the one place the
|
|
100
|
+
* message is built, so every number in it is grouped the same way. */
|
|
101
|
+
function grouped(value: number): string {
|
|
102
|
+
return value.toLocaleString('en-US');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Decide whether this frame's cost is worth an advisory, and what it should
|
|
107
|
+
* say. Pure — every input is an argument, and the same arguments always
|
|
108
|
+
* produce the same message.
|
|
109
|
+
*/
|
|
110
|
+
export function decideStaticBatchAdvisory(
|
|
111
|
+
input: StaticBatchAdvisoryInput,
|
|
112
|
+
): StaticBatchAdvisory | null {
|
|
113
|
+
const { drawCalls, census, structural } = input;
|
|
114
|
+
if (drawCalls === null || drawCalls < ADVISOR_DRAW_CALL_THRESHOLD) return null;
|
|
115
|
+
|
|
116
|
+
const { collapsible } = structural;
|
|
117
|
+
if (collapsible < drawCalls * ADVISOR_COLLAPSIBLE_SHARE) return null;
|
|
118
|
+
|
|
119
|
+
// The address, when one subtree genuinely dominates. `bySubtree` is already
|
|
120
|
+
// sorted, and a name that no census row confirms is not offered: it would
|
|
121
|
+
// send the reader looking for a node the other render.* commands cannot
|
|
122
|
+
// find either.
|
|
123
|
+
const addressable = new Set(census.subtrees.map((row) => row.name));
|
|
124
|
+
const leader = structural.bySubtree[0];
|
|
125
|
+
const subtree =
|
|
126
|
+
leader &&
|
|
127
|
+
leader.collapsible >= collapsible * ADVISOR_SUBTREE_SHARE &&
|
|
128
|
+
addressable.has(leader.name)
|
|
129
|
+
? leader.name
|
|
130
|
+
: null;
|
|
131
|
+
|
|
132
|
+
const fix = subtree === null ? '<Frozen>' : `<Frozen name="${subtree}">`;
|
|
133
|
+
const where = subtree === null ? 'spread across the scene' : `mostly under "${subtree}"`;
|
|
134
|
+
const message =
|
|
135
|
+
// `collapsible` counts meshes in the graph (culled ones included) and `drawCalls` this
|
|
136
|
+
// frame's submissions, so each number is stated in its own unit, never as a share.
|
|
137
|
+
`[static-batch] ${grouped(drawCalls)} draw calls this frame; ~${grouped(collapsible)} meshes ` +
|
|
138
|
+
`are repeats of ${grouped(structural.familyCount)} structural families, ` +
|
|
139
|
+
`${where}. If that scenery is mount-static — nothing under it moves, re-colours or ` +
|
|
140
|
+
`unmounts after mount — one wrapper collapses it to a few draws: wrap it in ` +
|
|
141
|
+
`${fix}…</Frozen> (vgai add static-batch). Reactive scenery goes outside the wrapper, ` +
|
|
142
|
+
`and a subtree that must stay unbatched declares it: userData={{ staticBatch: false }}.`;
|
|
143
|
+
|
|
144
|
+
return { drawCalls, collapsible, subtree, fix, message };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Once per PAGE, not once per module evaluation — a hot reload re-runs this
|
|
149
|
+
* module, and an advisory that reappears on every save is one that gets muted
|
|
150
|
+
* along with everything else on the console. Same mechanism, and the same
|
|
151
|
+
* reason, as dev-tools' unconfigured-section warning.
|
|
152
|
+
*/
|
|
153
|
+
const WARNED_KEY = '__vgaiStaticBatchAdvised';
|
|
154
|
+
|
|
155
|
+
function alreadyWarned(): boolean {
|
|
156
|
+
return (globalThis as unknown as Record<string, boolean | undefined>)[WARNED_KEY] === true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The console IS this advisory's channel: `vgai status` reports console
|
|
161
|
+
* warnings, which is where a building agent already looks. An in-editor
|
|
162
|
+
* banner would be one nobody opens, and a provider would be one nobody reads
|
|
163
|
+
* without already knowing to ask.
|
|
164
|
+
*/
|
|
165
|
+
function warnOnConsole(message: string): void {
|
|
166
|
+
// biome-ignore lint/suspicious/noConsole: this function's entire job — see above.
|
|
167
|
+
console.warn(message);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Emit `advisory` on the console, at most once per page. Answers whether it
|
|
172
|
+
* warned, so a caller can stop scanning.
|
|
173
|
+
*/
|
|
174
|
+
export function warnStaticBatchAdvisory(
|
|
175
|
+
advisory: StaticBatchAdvisory,
|
|
176
|
+
/** Injectable so the decision is testable without a console. */
|
|
177
|
+
warn: (message: string) => void = warnOnConsole,
|
|
178
|
+
): boolean {
|
|
179
|
+
if (alreadyWarned()) return false;
|
|
180
|
+
(globalThis as unknown as Record<string, boolean>)[WARNED_KEY] = true;
|
|
181
|
+
warn(advisory.message);
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Test-only: forget that the advisory was ever emitted. */
|
|
186
|
+
export function __resetStaticBatchAdvisoryForTest(): void {
|
|
187
|
+
(globalThis as unknown as Record<string, boolean | undefined>)[WARNED_KEY] = undefined;
|
|
188
|
+
}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-party WebGL2 single-frame draw-call capture (W4b, F11 frame debugger).
|
|
3
|
+
*
|
|
4
|
+
* WHY FIRST-PARTY, NOT spectorjs (recorded per the "use libraries directly, no
|
|
5
|
+
* wrappers" rule): the capture seam we need is the WebGL2 context this engine
|
|
6
|
+
* ALREADY owns end-to-end (`renderer.getContext()` in
|
|
7
|
+
* `editor-game/src/host/roots/r3f-root.tsx`). spectorjs is absent from node_modules, and its
|
|
8
|
+
* actual value is a bundled inspector UI we would discard — adopting it imports
|
|
9
|
+
* ~2MB of library to keep ~10% of it, and it wraps the context with its own
|
|
10
|
+
* global patching model rather than the instance-shadow-and-restore discipline
|
|
11
|
+
* the rest of dev/ uses (see `webgl-gpu-timer.ts`, the raw-context precedent
|
|
12
|
+
* this mirrors: instance shadowing, restore-in-finally, mock-GL unit test). So
|
|
13
|
+
* we instrument the real context directly, at the one seam we control.
|
|
14
|
+
*
|
|
15
|
+
* DISCIPLINE (mirrors webgl-gpu-timer.ts):
|
|
16
|
+
* - Patching happens ONLY inside `beginPass()` and ONLY while `armed`. Every
|
|
17
|
+
* patch is an OWN-property shadow on the context instance; the WebGL2
|
|
18
|
+
* prototype is NEVER touched. `endPass()` restores every original in a
|
|
19
|
+
* `finally` (own props reassigned, prototype-inherited methods `delete`d so
|
|
20
|
+
* the real method shows through again), so a throwing draw cannot leave the
|
|
21
|
+
* context wrapped.
|
|
22
|
+
* - HONESTY (adapters never fabricate): a value we cannot measure at this seam
|
|
23
|
+
* is recorded as `null` with the reason implied by the field, never zeroed
|
|
24
|
+
* or guessed. Program/framebuffer LABELS are capture-local identity tags
|
|
25
|
+
* (`program#N`), not the object's real GL debug name (raw WebGL2 exposes
|
|
26
|
+
* none). Framebuffer dimensions are only reported when cheaply knowable
|
|
27
|
+
* (renderbuffer color attachment); a texture-attachment FBO's size is not
|
|
28
|
+
* queryable in raw WebGL2, so it is `null`, not invented (see below —
|
|
29
|
+
* a documented deviation from the plan's non-nullable width/height).
|
|
30
|
+
* - Draw attribution is a single pending-annotation slot consumed by the NEXT
|
|
31
|
+
* draw and then cleared: a second draw with no fresh annotation records
|
|
32
|
+
* `annotation: null` (counted as unattributed), never the previous draw's.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import type {
|
|
36
|
+
DrawAnnotation,
|
|
37
|
+
DrawTarget,
|
|
38
|
+
FrameCapture,
|
|
39
|
+
FrameCaptureDrawCall,
|
|
40
|
+
FrameCaptureEntryPoint,
|
|
41
|
+
} from '@volter/editor-project/adapter/frame-capture';
|
|
42
|
+
|
|
43
|
+
export type {
|
|
44
|
+
DrawAnnotation,
|
|
45
|
+
DrawTarget,
|
|
46
|
+
FrameCapture,
|
|
47
|
+
FrameCaptureDrawCall,
|
|
48
|
+
FrameCaptureEntryPoint,
|
|
49
|
+
} from '@volter/editor-project/adapter/frame-capture';
|
|
50
|
+
|
|
51
|
+
export interface WebGLFrameCaptureOptions {
|
|
52
|
+
/** Injected clock for `capturedAt`; defaults to `Date.now`. Injectable so a
|
|
53
|
+
* test gets deterministic timestamps (mirrors the profiler stamping time
|
|
54
|
+
* through a single `now()` indirection rather than a module-level call). */
|
|
55
|
+
now?: () => number;
|
|
56
|
+
/** Hard cap on recorded draws (default 5000). Draws past it are dropped and
|
|
57
|
+
* counted in `totals.truncated`, with a note. */
|
|
58
|
+
maxDrawCalls?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface WebGLFrameCapture {
|
|
62
|
+
/** Arm a single capture; the NEXT `beginPass()` patches the context. */
|
|
63
|
+
arm(): void;
|
|
64
|
+
readonly armed: boolean;
|
|
65
|
+
/** If armed, shadow-patch the context. No-op otherwise (zero patching when
|
|
66
|
+
* not armed). Must be paired with `endPass()`. */
|
|
67
|
+
beginPass(): void;
|
|
68
|
+
/** Restore all patches (in `finally`) and, if a pass was patched, return the
|
|
69
|
+
* assembled {@link FrameCapture}. Returns `null` if `beginPass` did not
|
|
70
|
+
* patch (was not armed). One-shot: disarms after. */
|
|
71
|
+
endPass(): FrameCapture | null;
|
|
72
|
+
/** Set the annotation the next draw will consume (then cleared). */
|
|
73
|
+
annotateNextDraw(annotation: DrawAnnotation): void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const DEFAULT_MAX_DRAW_CALLS = 5000;
|
|
77
|
+
|
|
78
|
+
/** GL primitive-mode enum → name (decoded once, from the live context). */
|
|
79
|
+
function buildModeTable(gl: WebGL2RenderingContext): Map<number, string> {
|
|
80
|
+
return new Map<number, string>([
|
|
81
|
+
[gl.POINTS, 'POINTS'],
|
|
82
|
+
[gl.LINES, 'LINES'],
|
|
83
|
+
[gl.LINE_LOOP, 'LINE_LOOP'],
|
|
84
|
+
[gl.LINE_STRIP, 'LINE_STRIP'],
|
|
85
|
+
[gl.TRIANGLES, 'TRIANGLES'],
|
|
86
|
+
[gl.TRIANGLE_STRIP, 'TRIANGLE_STRIP'],
|
|
87
|
+
[gl.TRIANGLE_FAN, 'TRIANGLE_FAN'],
|
|
88
|
+
]);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function createWebGLFrameCapture(
|
|
92
|
+
gl: WebGL2RenderingContext,
|
|
93
|
+
options: WebGLFrameCaptureOptions = {},
|
|
94
|
+
): WebGLFrameCapture {
|
|
95
|
+
const nowFn = options.now ?? (() => Date.now());
|
|
96
|
+
const maxDrawCalls = options.maxDrawCalls ?? DEFAULT_MAX_DRAW_CALLS;
|
|
97
|
+
const modeTable = buildModeTable(gl);
|
|
98
|
+
|
|
99
|
+
// Persistent identity maps (stable labels across passes).
|
|
100
|
+
const programLabels = new Map<WebGLProgram, string>();
|
|
101
|
+
const framebufferLabels = new Map<WebGLFramebuffer, string>();
|
|
102
|
+
let nextProgramId = 0;
|
|
103
|
+
let nextFramebufferId = 0;
|
|
104
|
+
let captureId = 0;
|
|
105
|
+
|
|
106
|
+
// Shadow-tracked GL state (updated by the wrapped setters during a pass).
|
|
107
|
+
let currentProgram: WebGLProgram | null = null;
|
|
108
|
+
let currentFramebuffer: WebGLFramebuffer | null = null;
|
|
109
|
+
let currentViewport: [number, number, number, number] = [0, 0, 0, 0];
|
|
110
|
+
|
|
111
|
+
// Per-pass accumulators.
|
|
112
|
+
let armed = false;
|
|
113
|
+
let patched = false;
|
|
114
|
+
let drawCalls: FrameCaptureDrawCall[] = [];
|
|
115
|
+
let truncated = 0;
|
|
116
|
+
let notes: string[] = [];
|
|
117
|
+
let pendingAnnotation: DrawAnnotation | null = null;
|
|
118
|
+
|
|
119
|
+
// Saved originals for restore. Value is the original fn; a key present in
|
|
120
|
+
// `wasOwn` means it was an own property (restore by assignment), otherwise
|
|
121
|
+
// it was prototype-inherited (restore by delete).
|
|
122
|
+
const originals = new Map<string, unknown>();
|
|
123
|
+
const wasOwn = new Set<string>();
|
|
124
|
+
|
|
125
|
+
function decodeMode(mode: number): string {
|
|
126
|
+
return modeTable.get(mode) ?? `0x${mode.toString(16)}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function labelForProgram(program: WebGLProgram | null): string | null {
|
|
130
|
+
if (!program) return null;
|
|
131
|
+
let label = programLabels.get(program);
|
|
132
|
+
if (label === undefined) {
|
|
133
|
+
label = `program#${nextProgramId++}`;
|
|
134
|
+
programLabels.set(program, label);
|
|
135
|
+
}
|
|
136
|
+
return label;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Best-effort color-attachment dimensions for a bound draw FBO. Only a
|
|
140
|
+
* RENDERBUFFER color attachment is cheaply queryable in raw WebGL2; a
|
|
141
|
+
* texture attachment is not, so this returns nulls (honest absence). All
|
|
142
|
+
* reads are guarded — a mock/foreign context returns nulls, never throws. */
|
|
143
|
+
function measureFramebufferSize(): { width: number | null; height: number | null } {
|
|
144
|
+
try {
|
|
145
|
+
const type = gl.getFramebufferAttachmentParameter(
|
|
146
|
+
gl.DRAW_FRAMEBUFFER,
|
|
147
|
+
gl.COLOR_ATTACHMENT0,
|
|
148
|
+
gl.FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE,
|
|
149
|
+
);
|
|
150
|
+
if (type === gl.RENDERBUFFER) {
|
|
151
|
+
const rb = gl.getFramebufferAttachmentParameter(
|
|
152
|
+
gl.DRAW_FRAMEBUFFER,
|
|
153
|
+
gl.COLOR_ATTACHMENT0,
|
|
154
|
+
gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME,
|
|
155
|
+
) as WebGLRenderbuffer | null;
|
|
156
|
+
if (!rb) return { width: null, height: null };
|
|
157
|
+
const prev = gl.getParameter(gl.RENDERBUFFER_BINDING) as WebGLRenderbuffer | null;
|
|
158
|
+
gl.bindRenderbuffer(gl.RENDERBUFFER, rb);
|
|
159
|
+
const width = gl.getRenderbufferParameter(gl.RENDERBUFFER, gl.RENDERBUFFER_WIDTH) as number;
|
|
160
|
+
const height = gl.getRenderbufferParameter(
|
|
161
|
+
gl.RENDERBUFFER,
|
|
162
|
+
gl.RENDERBUFFER_HEIGHT,
|
|
163
|
+
) as number;
|
|
164
|
+
gl.bindRenderbuffer(gl.RENDERBUFFER, prev);
|
|
165
|
+
return { width, height };
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
// Foreign/mock context or an attachment we can't introspect — degrade
|
|
169
|
+
// to unknown dimensions rather than throwing mid-capture.
|
|
170
|
+
}
|
|
171
|
+
return { width: null, height: null };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function currentTarget(): DrawTarget {
|
|
175
|
+
if (!currentFramebuffer) return { kind: 'canvas' };
|
|
176
|
+
let label = framebufferLabels.get(currentFramebuffer);
|
|
177
|
+
if (label === undefined) {
|
|
178
|
+
label = `framebuffer#${nextFramebufferId++}`;
|
|
179
|
+
framebufferLabels.set(currentFramebuffer, label);
|
|
180
|
+
}
|
|
181
|
+
const { width, height } = measureFramebufferSize();
|
|
182
|
+
return { kind: 'framebuffer', width, height, label };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function readCull(): 'front' | 'back' | 'none' {
|
|
186
|
+
if (!gl.getParameter(gl.CULL_FACE)) return 'none';
|
|
187
|
+
return gl.getParameter(gl.CULL_FACE_MODE) === gl.FRONT ? 'front' : 'back';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function recordDraw(
|
|
191
|
+
entryPoint: FrameCaptureEntryPoint,
|
|
192
|
+
mode: number,
|
|
193
|
+
count: number,
|
|
194
|
+
instanceCount: number | null,
|
|
195
|
+
): void {
|
|
196
|
+
if (drawCalls.length >= maxDrawCalls) {
|
|
197
|
+
truncated++;
|
|
198
|
+
pendingAnnotation = null;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const annotation = pendingAnnotation;
|
|
202
|
+
pendingAnnotation = null;
|
|
203
|
+
drawCalls.push({
|
|
204
|
+
index: drawCalls.length,
|
|
205
|
+
entryPoint,
|
|
206
|
+
mode: decodeMode(mode),
|
|
207
|
+
count,
|
|
208
|
+
instanceCount,
|
|
209
|
+
programLabel: labelForProgram(currentProgram),
|
|
210
|
+
target: currentTarget(),
|
|
211
|
+
viewport: [...currentViewport] as [number, number, number, number],
|
|
212
|
+
state: {
|
|
213
|
+
blend: Boolean(gl.getParameter(gl.BLEND)),
|
|
214
|
+
depthTest: Boolean(gl.getParameter(gl.DEPTH_TEST)),
|
|
215
|
+
depthWrite: Boolean(gl.getParameter(gl.DEPTH_WRITEMASK)),
|
|
216
|
+
cull: readCull(),
|
|
217
|
+
scissor: Boolean(gl.getParameter(gl.SCISSOR_TEST)),
|
|
218
|
+
},
|
|
219
|
+
annotation: annotation ?? null,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Cast to an index signature to shadow methods without fighting the DOM lib
|
|
224
|
+
// types on every wrapped name.
|
|
225
|
+
const target = gl as unknown as Record<string, unknown>;
|
|
226
|
+
|
|
227
|
+
function patch(name: string, wrapper: (original: (...a: unknown[]) => unknown) => unknown): void {
|
|
228
|
+
const original = target[name] as (...a: unknown[]) => unknown;
|
|
229
|
+
originals.set(name, original);
|
|
230
|
+
if (Object.hasOwn(gl, name)) wasOwn.add(name);
|
|
231
|
+
target[name] = wrapper(original.bind(gl));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function restoreAll(): void {
|
|
235
|
+
for (const [name, original] of originals) {
|
|
236
|
+
if (wasOwn.has(name)) target[name] = original;
|
|
237
|
+
else delete target[name];
|
|
238
|
+
}
|
|
239
|
+
originals.clear();
|
|
240
|
+
wasOwn.clear();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function installPatches(): void {
|
|
244
|
+
// Seed shadow state from the live context so the first draw's viewport /
|
|
245
|
+
// framebuffer / program reflect reality even before any setter fires.
|
|
246
|
+
try {
|
|
247
|
+
const vp = gl.getParameter(gl.VIEWPORT) as ArrayLike<number> | null;
|
|
248
|
+
if (vp && vp.length >= 4) currentViewport = [vp[0]!, vp[1]!, vp[2]!, vp[3]!];
|
|
249
|
+
currentFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING) as WebGLFramebuffer | null;
|
|
250
|
+
currentProgram = gl.getParameter(gl.CURRENT_PROGRAM) as WebGLProgram | null;
|
|
251
|
+
} catch {
|
|
252
|
+
// Mock context without full getParameter coverage — start from defaults.
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
patch('useProgram', (original) => (program: unknown) => {
|
|
256
|
+
currentProgram = (program as WebGLProgram) ?? null;
|
|
257
|
+
return original(program);
|
|
258
|
+
});
|
|
259
|
+
patch('bindFramebuffer', (original) => (bindTarget: unknown, framebuffer: unknown) => {
|
|
260
|
+
// Track the DRAW framebuffer binding (FRAMEBUFFER and DRAW_FRAMEBUFFER
|
|
261
|
+
// both affect it; READ_FRAMEBUFFER does not).
|
|
262
|
+
if (bindTarget === gl.FRAMEBUFFER || bindTarget === gl.DRAW_FRAMEBUFFER) {
|
|
263
|
+
currentFramebuffer = (framebuffer as WebGLFramebuffer) ?? null;
|
|
264
|
+
}
|
|
265
|
+
return original(bindTarget, framebuffer);
|
|
266
|
+
});
|
|
267
|
+
patch('viewport', (original) => (x: unknown, y: unknown, w: unknown, h: unknown) => {
|
|
268
|
+
currentViewport = [x as number, y as number, w as number, h as number];
|
|
269
|
+
return original(x, y, w, h);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
patch('drawArrays', (original) => (mode: unknown, first: unknown, count: unknown) => {
|
|
273
|
+
recordDraw('drawArrays', mode as number, count as number, null);
|
|
274
|
+
return original(mode, first, count);
|
|
275
|
+
});
|
|
276
|
+
patch(
|
|
277
|
+
'drawElements',
|
|
278
|
+
(original) => (mode: unknown, count: unknown, type: unknown, offset: unknown) => {
|
|
279
|
+
recordDraw('drawElements', mode as number, count as number, null);
|
|
280
|
+
return original(mode, count, type, offset);
|
|
281
|
+
},
|
|
282
|
+
);
|
|
283
|
+
patch(
|
|
284
|
+
'drawArraysInstanced',
|
|
285
|
+
(original) => (mode: unknown, first: unknown, count: unknown, instanceCount: unknown) => {
|
|
286
|
+
recordDraw('drawArraysInstanced', mode as number, count as number, instanceCount as number);
|
|
287
|
+
return original(mode, first, count, instanceCount);
|
|
288
|
+
},
|
|
289
|
+
);
|
|
290
|
+
patch(
|
|
291
|
+
'drawElementsInstanced',
|
|
292
|
+
(original) =>
|
|
293
|
+
(mode: unknown, count: unknown, type: unknown, offset: unknown, instanceCount: unknown) => {
|
|
294
|
+
recordDraw(
|
|
295
|
+
'drawElementsInstanced',
|
|
296
|
+
mode as number,
|
|
297
|
+
count as number,
|
|
298
|
+
instanceCount as number,
|
|
299
|
+
);
|
|
300
|
+
return original(mode, count, type, offset, instanceCount);
|
|
301
|
+
},
|
|
302
|
+
);
|
|
303
|
+
patch(
|
|
304
|
+
'drawRangeElements',
|
|
305
|
+
(original) =>
|
|
306
|
+
(
|
|
307
|
+
mode: unknown,
|
|
308
|
+
start: unknown,
|
|
309
|
+
end: unknown,
|
|
310
|
+
count: unknown,
|
|
311
|
+
type: unknown,
|
|
312
|
+
offset: unknown,
|
|
313
|
+
) => {
|
|
314
|
+
recordDraw('drawRangeElements', mode as number, count as number, null);
|
|
315
|
+
return original(mode, start, end, count, type, offset);
|
|
316
|
+
},
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
arm() {
|
|
322
|
+
armed = true;
|
|
323
|
+
},
|
|
324
|
+
get armed() {
|
|
325
|
+
return armed;
|
|
326
|
+
},
|
|
327
|
+
beginPass() {
|
|
328
|
+
if (!armed || patched) return;
|
|
329
|
+
patched = true;
|
|
330
|
+
drawCalls = [];
|
|
331
|
+
truncated = 0;
|
|
332
|
+
notes = [];
|
|
333
|
+
pendingAnnotation = null;
|
|
334
|
+
installPatches();
|
|
335
|
+
},
|
|
336
|
+
endPass(): FrameCapture | null {
|
|
337
|
+
if (!patched) {
|
|
338
|
+
armed = false;
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
try {
|
|
342
|
+
if (truncated > 0) {
|
|
343
|
+
notes.push(
|
|
344
|
+
`Draw list capped at ${maxDrawCalls}; ${truncated} later draw(s) were not recorded.`,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
const unattributed = drawCalls.reduce((n, d) => n + (d.annotation === null ? 1 : 0), 0);
|
|
348
|
+
return {
|
|
349
|
+
id: ++captureId,
|
|
350
|
+
capturedAt: nowFn(),
|
|
351
|
+
drawCalls: drawCalls.slice(),
|
|
352
|
+
totals: { drawCalls: drawCalls.length, unattributed, truncated },
|
|
353
|
+
notes: notes.slice(),
|
|
354
|
+
};
|
|
355
|
+
} finally {
|
|
356
|
+
restoreAll();
|
|
357
|
+
patched = false;
|
|
358
|
+
armed = false;
|
|
359
|
+
pendingAnnotation = null;
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
annotateNextDraw(annotation: DrawAnnotation) {
|
|
363
|
+
pendingAnnotation = annotation;
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
interface DisjointTimerQueryExtension {
|
|
2
|
+
readonly TIME_ELAPSED_EXT: number;
|
|
3
|
+
readonly GPU_DISJOINT_EXT: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/** Non-blocking WebGL2 timer query; results are consumed on later frames. */
|
|
7
|
+
export function createWebGLGpuTimer(context: WebGLRenderingContext | WebGL2RenderingContext) {
|
|
8
|
+
const gl = context as WebGL2RenderingContext;
|
|
9
|
+
const extension = gl.getExtension(
|
|
10
|
+
'EXT_disjoint_timer_query_webgl2',
|
|
11
|
+
) as DisjointTimerQueryExtension | null;
|
|
12
|
+
const supported = extension !== null && typeof gl.createQuery === 'function';
|
|
13
|
+
const pending: WebGLQuery[] = [];
|
|
14
|
+
let active: WebGLQuery | null = null;
|
|
15
|
+
let latestMs: number | null = null;
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
begin() {
|
|
19
|
+
if (!supported || active) return;
|
|
20
|
+
active = gl.createQuery();
|
|
21
|
+
if (active) gl.beginQuery(extension!.TIME_ELAPSED_EXT, active);
|
|
22
|
+
},
|
|
23
|
+
end() {
|
|
24
|
+
if (!supported || !active) return;
|
|
25
|
+
gl.endQuery(extension!.TIME_ELAPSED_EXT);
|
|
26
|
+
pending.push(active);
|
|
27
|
+
active = null;
|
|
28
|
+
},
|
|
29
|
+
poll(): number | null {
|
|
30
|
+
if (!supported) return null;
|
|
31
|
+
if (gl.getParameter(extension!.GPU_DISJOINT_EXT)) {
|
|
32
|
+
for (const query of pending.splice(0)) gl.deleteQuery(query);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
while (pending.length > 0) {
|
|
36
|
+
const query = pending[0]!;
|
|
37
|
+
if (!gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE)) break;
|
|
38
|
+
pending.shift();
|
|
39
|
+
latestMs = (gl.getQueryParameter(query, gl.QUERY_RESULT) as number) / 1_000_000;
|
|
40
|
+
gl.deleteQuery(query);
|
|
41
|
+
}
|
|
42
|
+
return latestMs;
|
|
43
|
+
},
|
|
44
|
+
dispose() {
|
|
45
|
+
if (active) {
|
|
46
|
+
gl.endQuery(extension!.TIME_ELAPSED_EXT);
|
|
47
|
+
gl.deleteQuery(active);
|
|
48
|
+
active = null;
|
|
49
|
+
}
|
|
50
|
+
for (const query of pending.splice(0)) gl.deleteQuery(query);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE one owner of "is this a development context". Every reader that has to
|
|
3
|
+
* answer that question calls {@link devBuildEnabled}; there is deliberately NO
|
|
4
|
+
* second source of truth — not a module-level cached boolean, not a
|
|
5
|
+
* `globalThis` flag, not a per-host copy of the `import.meta.env` read below.
|
|
6
|
+
* Instrumentation that ships to players because two places disagreed about
|
|
7
|
+
* what "dev" means is exactly the failure this single owner exists to make
|
|
8
|
+
* impossible.
|
|
9
|
+
*
|
|
10
|
+
* Ownership, stated in one place (the build rule):
|
|
11
|
+
* - OWNER: this function. It resolves the answer; nothing else derives it.
|
|
12
|
+
* - SHARERS: the three-root adapter (`editor-game/src/host/roots/r3f-root.tsx`),
|
|
13
|
+
* which seeds live render vitals only under it. Any future dev-only
|
|
14
|
+
* instrument calls this too, with its own `override`.
|
|
15
|
+
* - TEARDOWN: none. This is a pure predicate over build config and one
|
|
16
|
+
* caller-supplied argument — it owns no resource, allocates nothing, and
|
|
17
|
+
* has no lifecycle to end.
|
|
18
|
+
*
|
|
19
|
+
* The three inputs, highest precedence first:
|
|
20
|
+
* 1. `override` — the explicit per-call answer. A headless test, a capture
|
|
21
|
+
* harness, or a host that knows better passes `true`/`false` and gets
|
|
22
|
+
* exactly that. Passing `undefined` (or omitting it) means "decide for
|
|
23
|
+
* me" and falls through. The editor's own preview mount is the worked
|
|
24
|
+
* case: it is a dev session by definition even when the editor SPA it
|
|
25
|
+
* runs inside is a production build.
|
|
26
|
+
* 2. A dev build — `import.meta.env.DEV`. The ordinary local/editor case.
|
|
27
|
+
* 3. A production build's EXPLICIT opt-in — `VITE_VGAI_DEV_BUILD=true`.
|
|
28
|
+
* Instrumenting a production bundle is a real, legitimate choice (an
|
|
29
|
+
* internal playtest build, a QA build), and it must be an opt-in someone
|
|
30
|
+
* had to type, never something a default drifts into.
|
|
31
|
+
*
|
|
32
|
+
* Anything else — a production build with no opt-in — is `false`.
|
|
33
|
+
*/
|
|
34
|
+
export function devBuildEnabled(override?: boolean | undefined): boolean {
|
|
35
|
+
if (override !== undefined) return override;
|
|
36
|
+
// `import.meta` is cast whole, not just its `.env`: this module is reachable
|
|
37
|
+
// from programs whose tsconfig does not pull in `vite/client` (the session
|
|
38
|
+
// client's, for one, which reaches the three adapter transitively), and there
|
|
39
|
+
// `ImportMeta` has no declared `env` at all. The cast keeps the single owner
|
|
40
|
+
// of the dev answer importable from ANY program rather than forcing every
|
|
41
|
+
// downstream tsconfig to adopt Vite's ambient types.
|
|
42
|
+
const env = (import.meta as unknown as { env?: unknown }).env as
|
|
43
|
+
| { DEV?: boolean | undefined; VITE_VGAI_DEV_BUILD?: string | undefined }
|
|
44
|
+
| undefined;
|
|
45
|
+
if (env?.DEV === true) return true;
|
|
46
|
+
return env?.VITE_VGAI_DEV_BUILD === 'true';
|
|
47
|
+
}
|