@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.
Files changed (236) hide show
  1. package/contributions/audio-unlock.service.ts +2 -2
  2. package/contributions/autoplay.service.ts +1 -1
  3. package/contributions/bridge.command.ts +3 -3
  4. package/contributions/canvas/component-board.service.ts +16 -0
  5. package/contributions/canvas/design-time-mount.service.ts +45 -0
  6. package/contributions/canvas-story-capture.service.ts +12 -0
  7. package/contributions/edit-mode-audio.service.ts +2 -2
  8. package/contributions/edit-mode-networking.service.ts +1 -1
  9. package/contributions/gameplay.command.ts +4 -4
  10. package/contributions/generation.service.ts +1 -1
  11. package/contributions/godot.style.ts +26 -6
  12. package/contributions/godot.view.ts +6 -0
  13. package/contributions/ingest.service.ts +2 -2
  14. package/contributions/instances.command.ts +1 -1
  15. package/contributions/navmesh.menu.ts +1 -1
  16. package/contributions/network-observer.service.ts +14 -0
  17. package/contributions/play.command.ts +10 -4
  18. package/contributions/react/component-board.service.ts +1 -1
  19. package/contributions/react/design-time-mount.service.ts +1 -1
  20. package/contributions/scene-document.service.ts +3 -3
  21. package/contributions/state-watch.menu.ts +1 -1
  22. package/contributions/state-watch.utility.tsx +1 -1
  23. package/contributions/team-playtest.service.ts +4 -4
  24. package/contributions/three/component-board.service.ts +1 -1
  25. package/contributions/three/component-verbs.command.ts +6 -6
  26. package/contributions/three/three-authoring.service.ts +8 -5
  27. package/contributions/three-story-capture.service.ts +12 -0
  28. package/contributions/unity.style.ts +17 -7
  29. package/contributions/unity.view.ts +6 -0
  30. package/contributions/unreal.style.ts +10 -2
  31. package/contributions/unreal.view.ts +5 -0
  32. package/package.json +20 -10
  33. package/src/asset-budget/AssetBudgetPanel.tsx +1 -1
  34. package/src/asset-budget/asset-budget-model.ts +1 -1
  35. package/src/audio/AudioDebuggerPanel.tsx +1 -1
  36. package/src/bridge/dispatch.ts +14 -14
  37. package/src/bridge/live-frames.ts +1 -1
  38. package/src/bridge/screenshot.ts +3 -3
  39. package/src/build/BuildProfilesPanel.tsx +3 -3
  40. package/src/canvas/canvas-board/CanvasBoardDocument.tsx +676 -0
  41. package/src/canvas/canvas-board/canvas-board-model.ts +407 -0
  42. package/src/canvas/canvas-board/canvas-component-board.ts +56 -0
  43. package/src/canvas/canvas-design-mount.ts +524 -0
  44. package/src/canvas/design-time-canvas-mount.ts +79 -0
  45. package/src/coverage/live-authoring-surface.ts +5 -5
  46. package/src/coverage/live-project-verbs.ts +1 -1
  47. package/src/coverage/native-system-coverage.ts +5 -5
  48. package/src/coverage/root-coverage.ts +1 -1
  49. package/src/coverage/session-coverage.ts +3 -3
  50. package/src/design-system-stories/ApplicationChrome.stories.tsx +5 -5
  51. package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +7 -7
  52. package/src/edit-mode/edit-mode-audio.ts +4 -4
  53. package/src/edit-mode/edit-mode-networking.ts +2 -2
  54. package/src/game-document/GameCaptureFrameButton.tsx +1 -1
  55. package/src/game-document/GameDocument.tsx +2 -2
  56. package/src/game-document/GamePanel.tsx +4 -4
  57. package/src/game-document/InstanceInspectorPicker.tsx +2 -2
  58. package/src/game-document/crowd-debug.ts +1 -1
  59. package/src/game-document/device-preview.ts +10 -11
  60. package/src/game-document/physics-debug.ts +1 -1
  61. package/src/generation/GenerationActivity.tsx +3 -3
  62. package/src/generation/generation-documents.tsx +5 -5
  63. package/src/generation/generation-jobs.ts +1 -1
  64. package/src/host/adapter-runtime-bindings.ts +96 -5
  65. package/src/host/authoring/babylon-authoring-adapter.ts +6 -6
  66. package/src/host/authoring/gesture-persist.ts +1 -1
  67. package/src/host/authoring/ingest-data-writer.ts +1 -1
  68. package/src/host/authoring/ingest-source-persistence.ts +4 -4
  69. package/src/host/authoring/mounted-authoring.ts +4 -4
  70. package/src/host/authoring/phaser-live-authoring-adapter.ts +4 -4
  71. package/src/host/authoring/pixi-authoring-adapter.ts +40 -15
  72. package/src/host/authoring/pixi-creation-site-write-target.ts +2 -2
  73. package/src/host/authoring/pixi-live-write-target.ts +4 -4
  74. package/src/host/authoring/pixi-source-identity.ts +3 -3
  75. package/src/host/authoring/pixi-source-write-target.ts +1614 -0
  76. package/src/host/authoring/pixi-still-presentation.ts +1 -1
  77. package/src/host/authoring/pixi-structure-history.ts +2 -2
  78. package/src/host/authoring/pixi-transform-channels.ts +16 -14
  79. package/src/host/authoring/source-persistence-backend.ts +3 -3
  80. package/src/host/authoring/struct-write-pipe.ts +1 -1
  81. package/src/host/binding-resolver.ts +8 -9
  82. package/src/host/browser-transpile.ts +1 -1
  83. package/src/host/canvas-entry-runtime.ts +58 -47
  84. package/src/host/canvas-preview-frames.ts +482 -0
  85. package/src/host/components/CameraAuthoringOverlay.tsx +1 -1
  86. package/src/host/components/HeaderTelemetry.tsx +4 -4
  87. package/src/host/components/PixiIsolationSceneContent.tsx +10 -10
  88. package/src/host/components/ThreeIsolationSceneContent.tsx +3 -3
  89. package/src/host/components/frame-debugger-model.ts +2 -2
  90. package/src/host/components/header-telemetry-model.ts +2 -2
  91. package/src/host/components/scene-document.tsx +14 -14
  92. package/src/host/components/utility-view-state.ts +1 -1
  93. package/src/host/components/world-root-stage-binding.tsx +12 -12
  94. package/src/host/components/world-root-stage.ts +70 -49
  95. package/src/host/coverage/system-adapter-coverage.ts +3 -4
  96. package/src/host/design-system-stories/fixtures/editor-runtime.tsx +9 -11
  97. package/src/host/document-preview-three.ts +1 -1
  98. package/src/host/entry-adjudication.ts +6 -6
  99. package/src/host/game-css-scope-transform.ts +4 -0
  100. package/src/host/game-realm-page.ts +1 -1
  101. package/src/host/gameplay-export.ts +25 -14
  102. package/src/host/gameplay-recording.ts +5 -5
  103. package/src/host/gated-globals.ts +2 -2
  104. package/src/host/history/json-history-resource.ts +1 -1
  105. package/src/host/projection/pixi.ts +24 -2
  106. package/src/host/r3f-entry-runtime.ts +65 -34
  107. package/src/host/react-mount-runtime.ts +7 -48
  108. package/src/host/realm-services.ts +1 -1
  109. package/src/host/roots/canvas-root.tsx +361 -0
  110. package/src/host/roots/r3f-root.tsx +473 -0
  111. package/src/host/roots/react-root.ts +9 -43
  112. package/src/host/served-bundle-runtime-modules.ts +1 -19
  113. package/src/host/server-log-bridge.ts +2 -2
  114. package/src/host/stories/mounted-story-viewport-source.ts +1 -1
  115. package/src/host/stories/pixi-story-model.ts +30 -0
  116. package/src/host/stories/story-media-captures.ts +46 -0
  117. package/src/host/stories/story-media-presence.ts +3 -3
  118. package/src/host/stories/story-pixi-preview.ts +408 -0
  119. package/src/host/stories/story-three-preview.ts +806 -0
  120. package/src/host/stories/three-story-captures.ts +35 -0
  121. package/src/host/story-three-preview-runtime.ts +56 -0
  122. package/src/host/use-active-performance-source.ts +2 -2
  123. package/src/host/viewport-pose-memory.ts +1 -1
  124. package/src/host/viewport-root-presentation.ts +6 -5
  125. package/src/ingest/active-ingest.ts +1 -1
  126. package/src/ingest/authoring/ingest-dom-surface-authoring.ts +4 -4
  127. package/src/ingest/authoring/ingest-root-adapter.ts +8 -8
  128. package/src/ingest/deferred-ingest-play.ts +6 -6
  129. package/src/ingest/discovery-public-ingest.ts +2 -2
  130. package/src/ingest/ingest-boot-viewport.ts +2 -2
  131. package/src/ingest/ingest-canvas-scene-document.tsx +8 -8
  132. package/src/ingest/ingest-canvas-scene.ts +3 -3
  133. package/src/ingest/ingest-evidence-hook.ts +1 -1
  134. package/src/ingest/ingest-frame-snapshot.ts +1 -1
  135. package/src/ingest/ingest-play-control.ts +1 -1
  136. package/src/ingest/ingest-render-debug.ts +10 -10
  137. package/src/ingest/ingest-siblings.ts +13 -25
  138. package/src/ingest/module-mode.ts +14 -14
  139. package/src/ingest/mount-canvas-ingest-root.ts +21 -21
  140. package/src/ingest/mount-coverage.ts +2 -2
  141. package/src/ingest/mount-dom-ingest-root.ts +11 -11
  142. package/src/ingest/mount-ingest-root.ts +8 -8
  143. package/src/ingest/mount-three-ingest-root.ts +8 -8
  144. package/src/ingest/resolve-canvas.ts +1 -1
  145. package/src/ingest/served-html-boot.ts +1 -1
  146. package/src/ingest/surface-canvas.ts +1 -1
  147. package/src/ingest/unmount-ingest-root.ts +5 -5
  148. package/src/navmesh/navmesh-handler.ts +24 -16
  149. package/src/network/NetworkInspectorPanel.tsx +449 -14
  150. package/src/network/network-inspector-model.ts +14 -2
  151. package/src/play/play-log-events.ts +1 -1
  152. package/src/play/play-mode.ts +76 -133
  153. package/src/play/play-recording.ts +1 -1
  154. package/src/play/react-play-live-authoring.ts +3 -3
  155. package/src/play/run-selection.ts +93 -0
  156. package/src/play-bar/PlayBar.tsx +20 -39
  157. package/src/profiler/FrameDebuggerPanel.tsx +1 -1
  158. package/src/profiler/PerformancePanel.tsx +3 -3
  159. package/src/react/design-time-react-mount.ts +23 -65
  160. package/src/react/dom-authoring-adapter.ts +9 -9
  161. package/src/react/react-inspector-section.tsx +5 -5
  162. package/src/react/react-world-authoring-adapter.ts +11 -11
  163. package/src/react/story-documents/story-documents.tsx +9 -9
  164. package/src/react/ui-board-document.tsx +7 -7
  165. package/src/react/ui-component-board.ts +2 -2
  166. package/src/runtime/adapter/audio-meter.ts +21 -0
  167. package/src/runtime/adapter/first-party-audio-system.ts +230 -0
  168. package/src/runtime/adapter/ingest/contract-debug-adapter.ts +114 -0
  169. package/src/runtime/adapter/ingest/contract-system-adapters.ts +256 -0
  170. package/src/runtime/adapter/ingest/merge-debug-adapters.ts +197 -0
  171. package/src/runtime/adapter/ingest/observation-debug-adapter.ts +162 -0
  172. package/src/runtime/adapter/ingest/upstream-pin.ts +51 -0
  173. package/src/runtime/adapter/native-debug-module.ts +498 -0
  174. package/src/runtime/audio/bus-mixer.ts +161 -0
  175. package/src/runtime/audio/pose-guard.ts +80 -0
  176. package/src/runtime/core/frame-pacing.ts +126 -0
  177. package/src/runtime/core/game-loop.ts +225 -0
  178. package/src/runtime/core/game-scoped-slot.ts +28 -0
  179. package/src/runtime/core/seeded-random.ts +162 -0
  180. package/src/runtime/core/sim-clock.ts +391 -0
  181. package/src/runtime/core/system-runner.ts +269 -0
  182. package/src/runtime/core/types.ts +104 -0
  183. package/src/runtime/create-runtime.ts +1128 -0
  184. package/src/runtime/debug-bridge.ts +570 -0
  185. package/src/runtime/debug-registry.ts +899 -0
  186. package/src/runtime/dev/chrome-trace.ts +153 -0
  187. package/src/runtime/dev/instruments.ts +403 -0
  188. package/src/runtime/dev/logger.ts +119 -0
  189. package/src/runtime/dev/performance-profiler.ts +367 -0
  190. package/src/runtime/dev/register-render-vitals.ts +276 -0
  191. package/src/runtime/dev/render-census.ts +354 -0
  192. package/src/runtime/dev/render-debug-adapter.ts +218 -0
  193. package/src/runtime/dev/render-memory.ts +226 -0
  194. package/src/runtime/dev/render-vitals.ts +338 -0
  195. package/src/runtime/dev/static-batch-advisor.ts +188 -0
  196. package/src/runtime/dev/webgl-frame-capture.ts +366 -0
  197. package/src/runtime/dev/webgl-gpu-timer.ts +53 -0
  198. package/src/runtime/dev-build.ts +47 -0
  199. package/src/runtime/game.ts +1636 -0
  200. package/src/runtime/gameplay-rng-trap.ts +135 -0
  201. package/src/runtime/host-context.ts +64 -0
  202. package/src/runtime/input-router.ts +169 -0
  203. package/src/runtime/mount-manifest.ts +480 -0
  204. package/src/runtime/pixi/authoring.ts +706 -0
  205. package/src/runtime/pixi/ingest.ts +116 -0
  206. package/src/runtime/pixi/physics-registry.ts +49 -0
  207. package/src/runtime/pixi/render-pass-bracket.ts +117 -0
  208. package/src/runtime/pixi/scene-capture.ts +179 -0
  209. package/src/runtime/pixi/system-adapters.ts +69 -0
  210. package/src/runtime/playtest.ts +22 -0
  211. package/src/runtime/presentation.ts +141 -0
  212. package/src/runtime/render-control.ts +642 -0
  213. package/src/runtime/render-seed.ts +77 -0
  214. package/src/runtime/run-ticks-settled.ts +73 -0
  215. package/src/runtime/setup/setup-audio.ts +72 -0
  216. package/src/services/audio-pose-guard.ts +2 -2
  217. package/src/services/game-audio.ts +152 -0
  218. package/src/services/game-network.ts +657 -0
  219. package/src/services/game-physics.ts +334 -0
  220. package/src/state-watch/StateWatchPanel.tsx +1 -1
  221. package/src/three/authoring/camera-runtime-inspector-section.tsx +3 -2
  222. package/src/three/authoring/constraint-inspector-section.tsx +6 -5
  223. package/src/three/authoring/model-asset-inspector-section.tsx +7 -6
  224. package/src/three/authoring/oid-source-persistence.ts +7 -7
  225. package/src/three/authoring/r3f-design-session.ts +58 -54
  226. package/src/three/authoring/r3f-source-authoring-adapter.ts +95 -91
  227. package/src/three/authoring/reflection-probe-inspector-section.tsx +3 -2
  228. package/src/three/authoring/three-authoring-adapter.ts +29 -29
  229. package/src/three/component-verbs/extract-menu.ts +7 -6
  230. package/src/three/component-verbs/fork-menu.ts +7 -6
  231. package/src/three/component-verbs/internals-menu.ts +2 -2
  232. package/src/three/story-documents/three-story-documents.tsx +13 -13
  233. package/src/three/three-board/ThreeBoardDocument.tsx +13 -12
  234. package/src/three/three-board/board-scene.ts +6 -6
  235. package/src/three/three-board/three-component-board.ts +2 -2
  236. package/src/services/game-audio-unlock.ts +0 -48
@@ -0,0 +1,226 @@
1
+ /**
2
+ * First-party GPU-memory estimator (W4b, F11 profiler memory tab).
3
+ *
4
+ * Walks a live three.js scene and reports geometry/texture byte footprints
5
+ * from data this engine ALREADY holds — attribute buffers and texture images —
6
+ * plus the renderer's own resource counts (`renderer.info`). Uses three.js
7
+ * directly, no wrapper.
8
+ *
9
+ * HONESTY (adapters never fabricate; unmeasurable = absent-with-reason):
10
+ * - `estimateGeometryBytes` is EXACT: the summed `byteLength` of every
11
+ * attribute and index array. Not an estimate at all — it is what the
12
+ * buffers occupy in CPU memory (and, uploaded 1:1, on the GPU).
13
+ * - `estimateTextureBytes` returns `null` when the footprint is genuinely
14
+ * unknowable at this seam (no image dimensions, or a pixel format/type we
15
+ * can't size). A null NEVER enters the totals — it goes to `unestimated[]`
16
+ * with a reason, so the panel can say "N textures unmeasured" instead of
17
+ * silently undercounting.
18
+ * - JS heap is deliberately NOT here: `performance.memory` is a browser-only
19
+ * global read editor-side (M2), not an engine-scene property.
20
+ */
21
+
22
+ import type {
23
+ RenderMemoryEntry,
24
+ RenderMemorySnapshot,
25
+ RenderMemoryUnestimated,
26
+ } from '@volter/editor-project/adapter/render-memory';
27
+ import * as THREE from 'three';
28
+
29
+ export type {
30
+ RenderMemoryEntry,
31
+ RenderMemorySnapshot,
32
+ RenderMemoryUnestimated,
33
+ } from '@volter/editor-project/adapter/render-memory';
34
+
35
+ const TOP_N = 10;
36
+
37
+ /** Channel count per pixel format. Packed/compressed formats are handled by
38
+ * the byte-size logic in {@link estimateTextureBytes}, not here. */
39
+ const FORMAT_CHANNELS: Record<number, number> = {
40
+ [THREE.AlphaFormat]: 1,
41
+ [THREE.RedFormat]: 1,
42
+ [THREE.RedIntegerFormat]: 1,
43
+ [THREE.RGFormat]: 2,
44
+ [THREE.RGIntegerFormat]: 2,
45
+ [THREE.RGBAFormat]: 4,
46
+ [THREE.RGBAIntegerFormat]: 4,
47
+ [THREE.DepthFormat]: 1,
48
+ [THREE.DepthStencilFormat]: 2,
49
+ };
50
+
51
+ /** Bytes per channel for a (non-packed) pixel type. */
52
+ const TYPE_BYTES: Record<number, number> = {
53
+ [THREE.UnsignedByteType]: 1,
54
+ [THREE.ByteType]: 1,
55
+ [THREE.ShortType]: 2,
56
+ [THREE.UnsignedShortType]: 2,
57
+ [THREE.IntType]: 4,
58
+ [THREE.UnsignedIntType]: 4,
59
+ [THREE.FloatType]: 4,
60
+ [THREE.HalfFloatType]: 2,
61
+ };
62
+
63
+ /** Packed pixel types occupy a FIXED size per texel regardless of channel
64
+ * count — sized here directly rather than via channels × type-bytes. */
65
+ const PACKED_TYPE_TEXEL_BYTES: Record<number, number> = {
66
+ [THREE.UnsignedShort4444Type]: 2,
67
+ [THREE.UnsignedShort5551Type]: 2,
68
+ [THREE.UnsignedInt248Type]: 4,
69
+ };
70
+
71
+ function bytesPerTexel(format: number, type: number): number | null {
72
+ const packed = PACKED_TYPE_TEXEL_BYTES[type];
73
+ if (packed !== undefined) return packed;
74
+ const channels = FORMAT_CHANNELS[format];
75
+ const typeBytes = TYPE_BYTES[type];
76
+ if (channels === undefined || typeBytes === undefined) return null;
77
+ return channels * typeBytes;
78
+ }
79
+
80
+ /** EXACT CPU/GPU footprint of a geometry's attribute + index buffers. */
81
+ export function estimateGeometryBytes(geometry: THREE.BufferGeometry): number {
82
+ // Interleaved attributes share ONE backing buffer (`attribute.data.array`);
83
+ // a plain attribute owns its own (`attribute.array`). Count each unique
84
+ // backing view exactly once.
85
+ const counted = new Set<ArrayBufferView>();
86
+ let bytes = 0;
87
+ const add = (view: ArrayBufferView | undefined): void => {
88
+ if (view && !counted.has(view)) {
89
+ counted.add(view);
90
+ bytes += view.byteLength;
91
+ }
92
+ };
93
+ for (const attribute of Object.values(geometry.attributes)) {
94
+ const interleaved = (attribute as THREE.InterleavedBufferAttribute).data?.array as
95
+ | ArrayBufferView
96
+ | undefined;
97
+ add(interleaved ?? ((attribute as THREE.BufferAttribute).array as ArrayBufferView | undefined));
98
+ }
99
+ add(geometry.index?.array as ArrayBufferView | undefined);
100
+ return bytes;
101
+ }
102
+
103
+ /** Estimated GPU footprint of a texture, or `null` when unknowable at this
104
+ * seam (the caller routes null into `unestimated[]` with a reason). */
105
+ export function estimateTextureBytes(texture: THREE.Texture): number | null {
106
+ // Compressed textures carry their real per-mip byte arrays — sum them; no
107
+ // format math needed and no estimate involved.
108
+ const compressed = texture as THREE.CompressedTexture;
109
+ if (
110
+ (compressed as { isCompressedTexture?: boolean }).isCompressedTexture &&
111
+ Array.isArray(compressed.mipmaps)
112
+ ) {
113
+ let bytes = 0;
114
+ for (const mip of compressed.mipmaps) {
115
+ const data = (mip as { data?: ArrayBufferView }).data;
116
+ if (data?.byteLength) bytes += data.byteLength;
117
+ }
118
+ return bytes;
119
+ }
120
+
121
+ const image = texture.image as { width?: number; height?: number } | undefined;
122
+ const width = image?.width;
123
+ const height = image?.height;
124
+ if (!width || !height) return null;
125
+
126
+ const bpt = bytesPerTexel(texture.format, texture.type);
127
+ if (bpt === null) return null;
128
+
129
+ const base = width * height * bpt;
130
+ return texture.generateMipmaps ? Math.round(base * 1.333) : base;
131
+ }
132
+
133
+ function textureName(texture: THREE.Texture): string {
134
+ return texture.name || (texture.image as { src?: string })?.src || `texture:${texture.uuid}`;
135
+ }
136
+
137
+ function geometryName(geometry: THREE.BufferGeometry): string {
138
+ return geometry.name || `${geometry.type}:${geometry.uuid}`;
139
+ }
140
+
141
+ function unestimatedReason(texture: THREE.Texture): string {
142
+ const image = texture.image as { width?: number; height?: number } | undefined;
143
+ if (!image?.width || !image?.height) return 'no image dimensions available';
144
+ return `unsupported format/type (format 0x${texture.format.toString(16)}, type 0x${texture.type.toString(16)})`;
145
+ }
146
+
147
+ /** Collect every unique geometry and texture referenced by a scene graph. */
148
+ function collectResources(scene: THREE.Object3D): {
149
+ geometries: Map<string, THREE.BufferGeometry>;
150
+ textures: Map<string, THREE.Texture>;
151
+ } {
152
+ const geometries = new Map<string, THREE.BufferGeometry>();
153
+ const textures = new Map<string, THREE.Texture>();
154
+
155
+ const addMaterialTextures = (material: THREE.Material): void => {
156
+ for (const value of Object.values(material as unknown as Record<string, unknown>)) {
157
+ if (value && (value as THREE.Texture).isTexture) {
158
+ const tex = value as THREE.Texture;
159
+ textures.set(tex.uuid, tex);
160
+ }
161
+ }
162
+ };
163
+
164
+ scene.traverse((object) => {
165
+ const geometry = (object as THREE.Mesh).geometry as THREE.BufferGeometry | undefined;
166
+ if (geometry?.isBufferGeometry) geometries.set(geometry.uuid, geometry);
167
+ const material = (object as THREE.Mesh).material;
168
+ if (Array.isArray(material)) material.forEach(addMaterialTextures);
169
+ else if (material) addMaterialTextures(material);
170
+ });
171
+
172
+ // Scene-level environment/background textures count too.
173
+ for (const maybe of [scene as unknown as THREE.Scene]) {
174
+ for (const value of [maybe.background, maybe.environment]) {
175
+ if (value && (value as THREE.Texture).isTexture) {
176
+ const tex = value as THREE.Texture;
177
+ textures.set(tex.uuid, tex);
178
+ }
179
+ }
180
+ }
181
+
182
+ return { geometries, textures };
183
+ }
184
+
185
+ export function collectRenderMemory(
186
+ scene: THREE.Object3D,
187
+ info: THREE.WebGLInfo,
188
+ ): RenderMemorySnapshot {
189
+ const { geometries, textures } = collectResources(scene);
190
+
191
+ const geometryEntries: RenderMemoryEntry[] = [];
192
+ let estimatedGeometryBytes = 0;
193
+ for (const geometry of geometries.values()) {
194
+ const bytes = estimateGeometryBytes(geometry);
195
+ estimatedGeometryBytes += bytes;
196
+ geometryEntries.push({ name: geometryName(geometry), bytes });
197
+ }
198
+
199
+ const textureEntries: RenderMemoryEntry[] = [];
200
+ const unestimated: RenderMemoryUnestimated[] = [];
201
+ let estimatedTextureBytes = 0;
202
+ for (const texture of textures.values()) {
203
+ const bytes = estimateTextureBytes(texture);
204
+ if (bytes === null) {
205
+ unestimated.push({ name: textureName(texture), reason: unestimatedReason(texture) });
206
+ continue;
207
+ }
208
+ estimatedTextureBytes += bytes;
209
+ textureEntries.push({ name: textureName(texture), bytes });
210
+ }
211
+
212
+ const byBytesDesc = (a: RenderMemoryEntry, b: RenderMemoryEntry): number => b.bytes - a.bytes;
213
+
214
+ return {
215
+ counts: {
216
+ geometries: info.memory.geometries,
217
+ textures: info.memory.textures,
218
+ programs: info.programs ? info.programs.length : null,
219
+ },
220
+ estimatedTextureBytes,
221
+ estimatedGeometryBytes,
222
+ topTextures: textureEntries.sort(byBytesDesc).slice(0, TOP_N),
223
+ topGeometries: geometryEntries.sort(byBytesDesc).slice(0, TOP_N),
224
+ unestimated,
225
+ };
226
+ }
@@ -0,0 +1,338 @@
1
+ /**
2
+ * LIVE RENDER VITALS — the frame explaining its own cost, derived ENTIRELY
3
+ * from `PerformanceProfiler` frames (`dev/performance-profiler.ts`).
4
+ *
5
+ * Nothing in a viewport tells you why it is slow, and a frame-time number on
6
+ * its own tells you only THAT it is. These readings answer the next question:
7
+ * where did the milliseconds go, and which subtree owns them
8
+ * (`dev/render-census.ts` is the address book half).
9
+ *
10
+ * ── THE MEASUREMENT SOURCE, STATED ONCE ─────────────────────────────────────
11
+ * There is no side ledger here. Every number below is folded out of profiler
12
+ * frames — the timing from phase spans, the counters from `reportRender`. The
13
+ * CPU submission cost is the profiler phase {@link RENDER_SUBMIT_PHASE}, which
14
+ * the three adapter brackets around its actual draw; the profiler's phase
15
+ * bookkeeping is a stack precisely so that bracket can sit INSIDE the frame's
16
+ * enclosing `render` phase without truncating it.
17
+ *
18
+ * ── TWO FRAME FLAVOURS, ONE FOLD ────────────────────────────────────────────
19
+ * The real runtime runs sim and presentation on different callbacks
20
+ * (`create-runtime.ts`: `update` → `runFrame({skipRenderPhases})` per fixed
21
+ * substep, `render` → `runRenderFrame` once per display frame), so a profiler
22
+ * "frame" is EITHER a sim substep OR a presentation pass. A capture/offline
23
+ * host drives one `runFrame` that is both. {@link foldProfilerFrame} handles
24
+ * all three the same way, and never has to know which host it is under:
25
+ *
26
+ * - a frame carrying a {@link RENDER_SUBMIT_PHASE} span IS a presentation —
27
+ * that phase only exists when a draw actually happened;
28
+ * - sim CPU accumulates across every frame since the last presentation, so
29
+ * the substeps a display frame consumed are attributed to it;
30
+ * - renderer COUNTERS ride whichever frame `reportRender` landed on (the
31
+ * adapter reports from its `endFrame` hook, which the substep pass drives),
32
+ * so the latest report wins and the counters describe the last completed
33
+ * draw. That is the same one-frame-warm reading `renderer.info` gives any
34
+ * caller — three resets it when the next draw starts.
35
+ *
36
+ * ── WHY THE WORST FRAME IS NOT AN EMA ───────────────────────────────────────
37
+ * A decaying average keeps a restart spike on the readout for ~30 s, long
38
+ * after it stopped being true. The rolling two-window max
39
+ * ({@link WORST_WINDOW_FRAMES}) holds a hitch for one to two windows and then
40
+ * genuinely forgets it, so the number always describes recent history.
41
+ *
42
+ * ── RESOURCE OWNERSHIP ──────────────────────────────────────────────────────
43
+ * OWNER: the caller of {@link createRenderVitals}, which allocates one state
44
+ * object and one `profiler.subscribe` registration. SHARER: none — the state
45
+ * is private to that call. TEARDOWN: the returned `dispose()`, the ONE path
46
+ * that ends the subscription (`editor-game/src/host/roots/r3f-root.tsx` calls it
47
+ * from the mounted root's own `dispose()`).
48
+ */
49
+
50
+ import type { PerformanceFrame, PerformanceProfiler } from './performance-profiler';
51
+
52
+ /**
53
+ * The profiler phase name the three adapter brackets its CPU render
54
+ * submission with — spelled HERE and nowhere else, so the producer
55
+ * (`editor-game/src/host/roots/r3f-root.tsx`) and the consumer ({@link foldProfilerFrame})
56
+ * cannot drift apart. Dotted, so it reads as a decomposition of the enclosing
57
+ * `render` phase rather than a ninth peer of `SystemPhase`.
58
+ */
59
+ export const RENDER_SUBMIT_PHASE = 'render.submit';
60
+
61
+ /**
62
+ * The phases whose cost is SIMULATION. Deliberately not "everything that is
63
+ * not render": `preRender` (LOD selection, particle stepping, culling prep) is
64
+ * neither the simulation nor the submission, so it lands in the hitch's
65
+ * `other` bucket where an investigation can see it. `render` itself is
66
+ * excluded because {@link RENDER_SUBMIT_PHASE} nests inside it — summing both
67
+ * would double-count the draw.
68
+ */
69
+ export const SIM_PHASES: readonly string[] = [
70
+ 'input',
71
+ 'prePhysics',
72
+ 'physics',
73
+ 'postPhysics',
74
+ 'gameLogic',
75
+ 'animation',
76
+ ];
77
+
78
+ /** Display frames per worst-frame window — ~5 s at 60 Hz, so the reported
79
+ * worst (the max of the current and previous window) survives 5–10 s. */
80
+ export const WORST_WINDOW_FRAMES = 300;
81
+
82
+ /** A display frame at or above this is worth decomposing. ~3 dropped frames at
83
+ * 60 Hz: below it, ordinary jitter would rewrite the reading constantly. */
84
+ export const HITCH_THRESHOLD_MS = 50;
85
+
86
+ /** Smoothing for the frame-time reading — a readable number, not a blur. */
87
+ export const FRAME_TIME_EMA_ALPHA = 0.05;
88
+
89
+ /** Smoothing for the render-CPU reading. Faster than frame time: submission
90
+ * cost is what a draw-call diet moves, and it should visibly move. */
91
+ export const RENDER_CPU_EMA_ALPHA = 0.1;
92
+
93
+ /**
94
+ * A hitch, split into the three buckets that route an investigation. `other`
95
+ * is the diagnostic payload, not a rounding remainder: when it dominates, the
96
+ * thief is neither the renderer nor the simulation — it is GC, asset decode,
97
+ * a layout-thrashing DOM overlay, or the browser itself — and every minute
98
+ * spent on draw calls would have been wasted.
99
+ */
100
+ export interface HitchDecomposition {
101
+ /** The display frame's wall-clock cost. */
102
+ readonly totalMs: number;
103
+ /** CPU spent submitting draws ({@link RENDER_SUBMIT_PHASE}). */
104
+ readonly renderMs: number;
105
+ /** CPU spent in {@link SIM_PHASES}, summed over the substeps this frame
106
+ * consumed. */
107
+ readonly simMs: number;
108
+ /** Everything else in the wall clock — see this interface's own note. May be
109
+ * negative if a measurement straddles the frame boundary; reported as
110
+ * measured rather than clamped to a tidier lie. */
111
+ readonly otherMs: number;
112
+ /** The one-line reading: `"NNms = render X + sim Y + other Z"`. */
113
+ readonly text: string;
114
+ }
115
+
116
+ /** Split one display frame's wall clock into render / sim / other. Pure. */
117
+ export function decomposeHitch(
118
+ totalMs: number,
119
+ renderMs: number,
120
+ simMs: number,
121
+ ): HitchDecomposition {
122
+ const otherMs = totalMs - renderMs - simMs;
123
+ return {
124
+ totalMs,
125
+ renderMs,
126
+ simMs,
127
+ otherMs,
128
+ text:
129
+ `${totalMs.toFixed(0)}ms = render ${renderMs.toFixed(0)} + ` +
130
+ `sim ${simMs.toFixed(0)} + other ${otherMs.toFixed(0)}`,
131
+ };
132
+ }
133
+
134
+ /** What `render.vitals` reads. Every field is `null` until the fold has
135
+ * actually measured it — a stopped, headless or never-drawn game reports
136
+ * emptiness rather than a fabricated `0 ms / 0 draws`. */
137
+ export interface RenderVitalsReading {
138
+ /** Smoothed wall-clock ms per display frame. */
139
+ readonly frameTimeMs: number | null;
140
+ /** Frames per second implied by {@link frameTimeMs}. */
141
+ readonly fps: number | null;
142
+ /** Draw calls in the last reported draw. */
143
+ readonly drawCalls: number | null;
144
+ /** Triangles in the last reported draw. */
145
+ readonly triangles: number | null;
146
+ /** Smoothed CPU ms spent submitting draws — invisible in frame time once
147
+ * vsync caps the loop, and exactly what fewer draw calls would improve. */
148
+ readonly renderCpuMs: number | null;
149
+ /** Top-level `renderer.render()` submissions the last presentation took. */
150
+ readonly renderPasses: number | null;
151
+ /** Worst display frame in the last one-to-two windows. */
152
+ readonly worstFrameMs: number | null;
153
+ /** The most recent frame over {@link HITCH_THRESHOLD_MS}, decomposed. */
154
+ readonly lastHitch: HitchDecomposition | null;
155
+ /** How many display frames the fold has seen. `0` reads as "nothing has
156
+ * presented yet", which is why every reading above is `null`. */
157
+ readonly presentedFrames: number;
158
+ }
159
+
160
+ /** The fold's accumulator. Mutable by design (one allocation for the life of a
161
+ * mount, written once per profiler frame); read through
162
+ * {@link readRenderVitals}. */
163
+ export interface RenderVitalsState {
164
+ /** Highest profiler frame id already folded — the dedupe that makes the fold
165
+ * safe to drive from `profiler.subscribe`, which also fires for
166
+ * enabled/recording changes that publish no new frame. */
167
+ lastFrameId: number;
168
+ /** `timestamp` of the last presentation, or `null` before the first (there
169
+ * is no interval to measure from one sample). */
170
+ lastPresentAt: number | null;
171
+ /** Sim CPU accumulated since the last presentation. */
172
+ pendingSimMs: number;
173
+ frameTimeMs: number | null;
174
+ renderCpuMs: number | null;
175
+ drawCalls: number | null;
176
+ triangles: number | null;
177
+ renderPasses: number | null;
178
+ /** Running max of the CURRENT worst-frame window. */
179
+ worstCurrentMs: number;
180
+ /** The PREVIOUS window's max, kept so the reading does not drop to zero the
181
+ * instant a window rolls over. */
182
+ worstPreviousMs: number;
183
+ /** Display frames counted into the current window. */
184
+ worstWindowFrames: number;
185
+ lastHitch: HitchDecomposition | null;
186
+ presentedFrames: number;
187
+ }
188
+
189
+ export function createRenderVitalsState(): RenderVitalsState {
190
+ return {
191
+ lastFrameId: 0,
192
+ lastPresentAt: null,
193
+ pendingSimMs: 0,
194
+ frameTimeMs: null,
195
+ renderCpuMs: null,
196
+ drawCalls: null,
197
+ triangles: null,
198
+ renderPasses: null,
199
+ worstCurrentMs: 0,
200
+ worstPreviousMs: 0,
201
+ worstWindowFrames: 0,
202
+ lastHitch: null,
203
+ presentedFrames: 0,
204
+ };
205
+ }
206
+
207
+ /** Sum of one frame's phase timings whose names are in `names`. */
208
+ function sumPhases(frame: PerformanceFrame, names: readonly string[]): number {
209
+ let total = 0;
210
+ for (const timing of frame.phases) if (names.includes(timing.name)) total += timing.ms;
211
+ return total;
212
+ }
213
+
214
+ /** This frame's CPU render submission, or `null` when it was not a
215
+ * presentation (no draw happened, so no bracket was recorded). */
216
+ function submissionMs(frame: PerformanceFrame): number | null {
217
+ for (const timing of frame.phases) if (timing.name === RENDER_SUBMIT_PHASE) return timing.ms;
218
+ return null;
219
+ }
220
+
221
+ function ema(previous: number | null, sample: number, alpha: number): number {
222
+ return previous === null ? sample : previous * (1 - alpha) + sample * alpha;
223
+ }
224
+
225
+ /**
226
+ * Fold ONE profiler frame into the accumulator. Pure with respect to time and
227
+ * randomness — every input is on the frame — which is what makes the whole
228
+ * derivation testable from synthetic frames with no renderer, no clock and no
229
+ * game.
230
+ *
231
+ * Returns `true` when the frame was a presentation (the readings moved).
232
+ */
233
+ export function foldProfilerFrame(state: RenderVitalsState, frame: PerformanceFrame): boolean {
234
+ // Re-publishes and the ring's own re-reads must not double-count. Frame ids
235
+ // are monotonic for the profiler's whole life, including across `clear()`.
236
+ if (frame.id <= state.lastFrameId) return false;
237
+ state.lastFrameId = frame.id;
238
+
239
+ // Counters ride whichever frame `reportRender` landed on — see the module
240
+ // note. `renderPasses !== null` is the marker that a reporter spoke at all;
241
+ // a frame nobody reported on leaves the previous reading standing rather
242
+ // than blanking it to a zero that would read as "nothing is drawn".
243
+ if (frame.render.renderPasses !== null) {
244
+ state.drawCalls = frame.render.drawCalls;
245
+ state.triangles = frame.render.triangles;
246
+ state.renderPasses = frame.render.renderPasses;
247
+ }
248
+
249
+ state.pendingSimMs += sumPhases(frame, SIM_PHASES);
250
+
251
+ const renderMs = submissionMs(frame);
252
+ if (renderMs === null) return false; // a sim substep: its cost is now pending
253
+
254
+ const simMs = state.pendingSimMs;
255
+ state.pendingSimMs = 0;
256
+ state.presentedFrames += 1;
257
+ state.renderCpuMs = ema(state.renderCpuMs, renderMs, RENDER_CPU_EMA_ALPHA);
258
+
259
+ const previousAt = state.lastPresentAt;
260
+ state.lastPresentAt = frame.timestamp;
261
+ // The FIRST presentation has no interval behind it. Everything that needs a
262
+ // dt (frame time, worst frame, the hitch) waits for the second one; the
263
+ // alternative is to invent an interval from the profiler's own start, which
264
+ // would report the mount cost as a frame time forever.
265
+ if (previousAt === null) return true;
266
+
267
+ const dt = frame.timestamp - previousAt;
268
+ state.frameTimeMs = ema(state.frameTimeMs, dt, FRAME_TIME_EMA_ALPHA);
269
+
270
+ if (dt > state.worstCurrentMs) state.worstCurrentMs = dt;
271
+ state.worstWindowFrames += 1;
272
+ if (state.worstWindowFrames >= WORST_WINDOW_FRAMES) {
273
+ state.worstPreviousMs = state.worstCurrentMs;
274
+ state.worstCurrentMs = 0;
275
+ state.worstWindowFrames = 0;
276
+ }
277
+
278
+ // Blame the hitch with THIS frame's own measurements: `dt` spans the work
279
+ // between the two presentations, which is exactly the submission just
280
+ // measured plus the substeps that ran in between.
281
+ if (dt >= HITCH_THRESHOLD_MS) state.lastHitch = decomposeHitch(dt, renderMs, simMs);
282
+
283
+ return true;
284
+ }
285
+
286
+ function round(value: number | null, places: number): number | null {
287
+ if (value === null) return null;
288
+ const factor = 10 ** places;
289
+ return Math.round(value * factor) / factor;
290
+ }
291
+
292
+ /** The provider body — pure over the accumulator. */
293
+ export function readRenderVitals(state: RenderVitalsState): RenderVitalsReading {
294
+ const frameTimeMs = round(state.frameTimeMs, 2);
295
+ const worst = Math.max(state.worstCurrentMs, state.worstPreviousMs);
296
+ return {
297
+ frameTimeMs,
298
+ fps: frameTimeMs !== null && frameTimeMs > 0 ? round(1000 / frameTimeMs, 1) : null,
299
+ drawCalls: state.drawCalls,
300
+ triangles: state.triangles,
301
+ renderCpuMs: round(state.renderCpuMs, 2),
302
+ renderPasses: state.renderPasses,
303
+ worstFrameMs: state.frameTimeMs === null ? null : round(worst, 2),
304
+ lastHitch: state.lastHitch,
305
+ presentedFrames: state.presentedFrames,
306
+ };
307
+ }
308
+
309
+ /** What {@link createRenderVitals} hands back. */
310
+ export interface RenderVitals {
311
+ /** The `render.vitals` provider body. */
312
+ read(): RenderVitalsReading;
313
+ /** See the module's ownership note — the ONE path that ends the
314
+ * subscription. Idempotent. */
315
+ dispose(): void;
316
+ }
317
+
318
+ /**
319
+ * Attach a fold to a live profiler. `subscribe` fires once per published
320
+ * frame (and on enabled/recording changes, which the id dedupe absorbs), so
321
+ * nothing here polls and nothing here owns a timer.
322
+ */
323
+ export function createRenderVitals(profiler: PerformanceProfiler): RenderVitals {
324
+ const state = createRenderVitalsState();
325
+ const unsubscribe = profiler.subscribe(() => {
326
+ const frame = profiler.getSnapshot().frames.at(-1);
327
+ if (frame) foldProfilerFrame(state, frame);
328
+ });
329
+ let disposed = false;
330
+ return {
331
+ read: () => readRenderVitals(state),
332
+ dispose() {
333
+ if (disposed) return;
334
+ disposed = true;
335
+ unsubscribe();
336
+ },
337
+ };
338
+ }