@volter/editor-game 0.5.65

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 (302) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +23 -0
  3. package/contributions/asset-budget-asset.menu.ts +26 -0
  4. package/contributions/asset-budget.action.ts +18 -0
  5. package/contributions/asset-budget.document.tsx +21 -0
  6. package/contributions/asset-budget.menu.ts +22 -0
  7. package/contributions/audio-unlock.service.ts +17 -0
  8. package/contributions/audio.utility.tsx +15 -0
  9. package/contributions/autoplay.service.ts +48 -0
  10. package/contributions/bridge.command.ts +172 -0
  11. package/contributions/build-profiles.document.tsx +21 -0
  12. package/contributions/build-progress.status.tsx +45 -0
  13. package/contributions/build.action.ts +26 -0
  14. package/contributions/build.command.ts +27 -0
  15. package/contributions/build.header.tsx +37 -0
  16. package/contributions/build.menu.ts +31 -0
  17. package/contributions/build.service.ts +31 -0
  18. package/contributions/connection.status.tsx +43 -0
  19. package/contributions/coverage.service.ts +118 -0
  20. package/contributions/edit-mode-audio.service.ts +23 -0
  21. package/contributions/edit-mode-networking.service.ts +30 -0
  22. package/contributions/game-document.service.ts +25 -0
  23. package/contributions/game-eval.command.ts +210 -0
  24. package/contributions/game.layout.ts +19 -0
  25. package/contributions/gameplay.command.ts +255 -0
  26. package/contributions/generation.service.ts +96 -0
  27. package/contributions/generations.status.tsx +53 -0
  28. package/contributions/ingest.service.ts +80 -0
  29. package/contributions/instances.command.ts +75 -0
  30. package/contributions/navmesh.menu.ts +39 -0
  31. package/contributions/navmesh.service.ts +19 -0
  32. package/contributions/network.utility.tsx +17 -0
  33. package/contributions/play.command.ts +355 -0
  34. package/contributions/profiler.action.ts +16 -0
  35. package/contributions/profiler.menu.ts +22 -0
  36. package/contributions/profiler.utility.tsx +17 -0
  37. package/contributions/react/component-board.service.ts +21 -0
  38. package/contributions/react/design-time-mount.service.ts +60 -0
  39. package/contributions/react/pasteboard.action.ts +41 -0
  40. package/contributions/react/react-inspector.service.ts +85 -0
  41. package/contributions/react/story-documents.service.ts +44 -0
  42. package/contributions/scene-document.service.ts +32 -0
  43. package/contributions/state-watch.action.ts +17 -0
  44. package/contributions/state-watch.menu.ts +23 -0
  45. package/contributions/state-watch.utility.tsx +20 -0
  46. package/contributions/team-playtest.service.ts +124 -0
  47. package/contributions/three/camera-runtime.inspector.tsx +34 -0
  48. package/contributions/three/component-board.service.ts +24 -0
  49. package/contributions/three/component-verbs.command.ts +110 -0
  50. package/contributions/three/component-verbs.service.ts +92 -0
  51. package/contributions/three/constraints.inspector.tsx +34 -0
  52. package/contributions/three/model-asset-sections.service.ts +26 -0
  53. package/contributions/three/reflection-probe-capture.inspector.tsx +32 -0
  54. package/contributions/three/story-documents.service.ts +31 -0
  55. package/contributions/three/three-authoring.service.ts +66 -0
  56. package/contributions/transport.header.tsx +19 -0
  57. package/contributions/xstate-behavior.action.ts +42 -0
  58. package/contributions/xstate-behavior.document.tsx +73 -0
  59. package/contributions/xstate-behavior.inspector.tsx +28 -0
  60. package/contributions/xstate-behavior.menu.ts +23 -0
  61. package/package.json +144 -0
  62. package/src/asset-budget/AssetBudgetPanel.tsx +1172 -0
  63. package/src/asset-budget/asset-budget-model.ts +799 -0
  64. package/src/asset-budget/basis-encoder.ts +165 -0
  65. package/src/asset-budget/gltf-io.ts +154 -0
  66. package/src/asset-budget/gltf-optimize.ts +384 -0
  67. package/src/asset-budget/image-dims.ts +78 -0
  68. package/src/asset-budget/optimize-apply.ts +221 -0
  69. package/src/audio/AudioDebuggerPanel.tsx +457 -0
  70. package/src/audio/audio-debugger-model.ts +87 -0
  71. package/src/bridge/call.ts +60 -0
  72. package/src/bridge/dispatch.ts +459 -0
  73. package/src/bridge/live-frames.ts +25 -0
  74. package/src/bridge/screenshot.ts +316 -0
  75. package/src/build/BuildProfilesPanel.tsx +476 -0
  76. package/src/build/build-session.ts +247 -0
  77. package/src/build/format-bytes.ts +14 -0
  78. package/src/command-results.ts +36 -0
  79. package/src/coverage/live-authoring-surface.ts +30 -0
  80. package/src/coverage/live-project-verbs.ts +162 -0
  81. package/src/coverage/native-system-coverage.ts +143 -0
  82. package/src/coverage/root-coverage.ts +79 -0
  83. package/src/coverage/session-coverage.ts +193 -0
  84. package/src/design-system-stories/ApplicationChrome.stories.tsx +100 -0
  85. package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +406 -0
  86. package/src/edit-mode/edit-mode-audio.ts +56 -0
  87. package/src/edit-mode/edit-mode-networking.ts +112 -0
  88. package/src/game-document/DevicePresetPicker.tsx +84 -0
  89. package/src/game-document/GameCaptureFrameButton.tsx +45 -0
  90. package/src/game-document/GameDocument.tsx +215 -0
  91. package/src/game-document/GamePanel.tsx +662 -0
  92. package/src/game-document/InstanceInspectorPicker.tsx +183 -0
  93. package/src/game-document/crowd-debug.ts +183 -0
  94. package/src/game-document/device-preview.ts +336 -0
  95. package/src/game-document/game-view-store.ts +107 -0
  96. package/src/game-document/physics-debug.ts +187 -0
  97. package/src/generation/GenerationActivity.tsx +421 -0
  98. package/src/generation/GenerationGallery.css +231 -0
  99. package/src/generation/generation-documents.tsx +338 -0
  100. package/src/generation/generation-jobs.ts +128 -0
  101. package/src/generation/generation-presentation.ts +257 -0
  102. package/src/host/adapter-reach.ts +445 -0
  103. package/src/host/adapter-runtime-bindings.ts +303 -0
  104. package/src/host/after-paint.ts +112 -0
  105. package/src/host/api/configurations.ts +66 -0
  106. package/src/host/authoring/babylon-authoring-adapter.ts +703 -0
  107. package/src/host/authoring/canvas-runtime-recognition.ts +50 -0
  108. package/src/host/authoring/contract-hierarchy-authoring.ts +144 -0
  109. package/src/host/authoring/contract-scenes-stories.ts +204 -0
  110. package/src/host/authoring/creation-site-related.ts +55 -0
  111. package/src/host/authoring/ephemeral-persistence.ts +29 -0
  112. package/src/host/authoring/gesture-persist.ts +84 -0
  113. package/src/host/authoring/ingest-data-writer.ts +232 -0
  114. package/src/host/authoring/ingest-source-persistence.ts +826 -0
  115. package/src/host/authoring/mount-isolated-pixi-screen.ts +250 -0
  116. package/src/host/authoring/mounted-authoring.ts +41 -0
  117. package/src/host/authoring/owned-pixi-ticker-listeners.ts +96 -0
  118. package/src/host/authoring/phaser-live-authoring-adapter.ts +265 -0
  119. package/src/host/authoring/pixi-authoring-adapter.ts +1554 -0
  120. package/src/host/authoring/pixi-creation-site-write-target.ts +60 -0
  121. package/src/host/authoring/pixi-isolation-assets.ts +25 -0
  122. package/src/host/authoring/pixi-live-write-target.ts +979 -0
  123. package/src/host/authoring/pixi-source-identity.ts +141 -0
  124. package/src/host/authoring/pixi-still-presentation.ts +71 -0
  125. package/src/host/authoring/pixi-structure-history.ts +237 -0
  126. package/src/host/authoring/pixi-transform-channels.ts +205 -0
  127. package/src/host/authoring/selection-remount-handoff.ts +23 -0
  128. package/src/host/authoring/source-persistence-backend.ts +373 -0
  129. package/src/host/authoring/source-refresh-revisions.ts +81 -0
  130. package/src/host/authoring/struct-write-pipe.ts +143 -0
  131. package/src/host/auto-frame-window.ts +89 -0
  132. package/src/host/binding-resolver.ts +393 -0
  133. package/src/host/browser-transpile.ts +631 -0
  134. package/src/host/canvas-entry-runtime.ts +95 -0
  135. package/src/host/components/CameraAuthoringOverlay.tsx +216 -0
  136. package/src/host/components/HeaderTelemetry.tsx +341 -0
  137. package/src/host/components/PixiIsolationSceneContent.tsx +280 -0
  138. package/src/host/components/ResolutionPicker.tsx +89 -0
  139. package/src/host/components/ThreeIsolationSceneContent.tsx +180 -0
  140. package/src/host/components/frame-debugger-model.ts +579 -0
  141. package/src/host/components/header-telemetry-model.ts +74 -0
  142. package/src/host/components/scene-document.tsx +447 -0
  143. package/src/host/components/utility-view-state.ts +87 -0
  144. package/src/host/components/world-root-stage-binding.tsx +133 -0
  145. package/src/host/components/world-root-stage.ts +1046 -0
  146. package/src/host/coverage/authoring-read-probe.ts +583 -0
  147. package/src/host/coverage/capability-coverage.ts +1587 -0
  148. package/src/host/coverage/coverage-accounting.ts +261 -0
  149. package/src/host/coverage/game-contract-seam-evidence.ts +82 -0
  150. package/src/host/coverage/project-verb-coverage.ts +148 -0
  151. package/src/host/coverage/system-adapter-coverage.ts +373 -0
  152. package/src/host/design-system-stories/StoryLayout.tsx +104 -0
  153. package/src/host/design-system-stories/fixtures/authoring.ts +247 -0
  154. package/src/host/design-system-stories/fixtures/editor-runtime.tsx +115 -0
  155. package/src/host/document-preview-three.ts +110 -0
  156. package/src/host/entry-adjudication.ts +89 -0
  157. package/src/host/game-location-guard.ts +150 -0
  158. package/src/host/game-module-access.ts +196 -0
  159. package/src/host/game-realm-page.ts +358 -0
  160. package/src/host/game-realm-reclaim.ts +55 -0
  161. package/src/host/game-realm-storage.ts +104 -0
  162. package/src/host/gameplay-export.ts +288 -0
  163. package/src/host/gameplay-recording.ts +717 -0
  164. package/src/host/gated-globals.ts +1511 -0
  165. package/src/host/history/json-history-resource.ts +254 -0
  166. package/src/host/ingest/registry.ts +261 -0
  167. package/src/host/instance-extract-actions.ts +120 -0
  168. package/src/host/instance-fork-actions.ts +120 -0
  169. package/src/host/play-control-hook.ts +26 -0
  170. package/src/host/playwright-shim.ts +479 -0
  171. package/src/host/projection/dom.ts +295 -0
  172. package/src/host/projection/pixi.ts +288 -0
  173. package/src/host/r3f-entry-runtime.ts +77 -0
  174. package/src/host/react-mount-runtime.ts +162 -0
  175. package/src/host/realm-services.ts +148 -0
  176. package/src/host/recording-preview.ts +106 -0
  177. package/src/host/roots/module-root.ts +203 -0
  178. package/src/host/roots/react-root.ts +181 -0
  179. package/src/host/same-realm-loop-gate.ts +544 -0
  180. package/src/host/scene-view-drawability.ts +69 -0
  181. package/src/host/sdk/tools.ts +31 -0
  182. package/src/host/served-bundle-runtime-modules.ts +331 -0
  183. package/src/host/server-log-bridge.ts +60 -0
  184. package/src/host/staged-projects.ts +24 -0
  185. package/src/host/stories/mounted-story-viewport-source.ts +64 -0
  186. package/src/host/stories/story-arg-descriptors.ts +50 -0
  187. package/src/host/stories/story-media-presence.ts +152 -0
  188. package/src/host/surface-content.ts +87 -0
  189. package/src/host/take-named-export.ts +23 -0
  190. package/src/host/three-ingest-runtime.ts +76 -0
  191. package/src/host/types-fastnoise-lite.d.ts +7 -0
  192. package/src/host/types-mikktspace.d.ts +20 -0
  193. package/src/host/types-troika-three-text.d.ts +7 -0
  194. package/src/host/use-active-performance-source.ts +53 -0
  195. package/src/host/viewport-pose-memory.ts +48 -0
  196. package/src/host/viewport-root-presentation.ts +40 -0
  197. package/src/ingest/active-ingest.ts +251 -0
  198. package/src/ingest/active-scene-navigation.ts +43 -0
  199. package/src/ingest/authoring/ingest-dom-surface-authoring.ts +191 -0
  200. package/src/ingest/authoring/ingest-root-adapter.ts +897 -0
  201. package/src/ingest/capture-wait-report.ts +122 -0
  202. package/src/ingest/deferred-ingest-play.ts +216 -0
  203. package/src/ingest/deferred-ingest-session.ts +23 -0
  204. package/src/ingest/discovery-public-ingest.ts +242 -0
  205. package/src/ingest/dom-stub-mark.ts +7 -0
  206. package/src/ingest/entry-load.ts +56 -0
  207. package/src/ingest/game-contract-realm.ts +34 -0
  208. package/src/ingest/game-pointer-lock.ts +89 -0
  209. package/src/ingest/held-scene-repaint.ts +73 -0
  210. package/src/ingest/host-surface-box.ts +174 -0
  211. package/src/ingest/ingest-boot-viewport.ts +54 -0
  212. package/src/ingest/ingest-canvas-scene-document.tsx +177 -0
  213. package/src/ingest/ingest-canvas-scene.ts +50 -0
  214. package/src/ingest/ingest-evidence-hook.ts +131 -0
  215. package/src/ingest/ingest-frame-snapshot.ts +183 -0
  216. package/src/ingest/ingest-play-commands.ts +134 -0
  217. package/src/ingest/ingest-play-control.ts +299 -0
  218. package/src/ingest/ingest-render-debug.ts +320 -0
  219. package/src/ingest/ingest-siblings.ts +487 -0
  220. package/src/ingest/ingest-status.ts +62 -0
  221. package/src/ingest/live-ingest-facet.ts +46 -0
  222. package/src/ingest/module-mode.ts +229 -0
  223. package/src/ingest/mount-canvas-ingest-root.ts +899 -0
  224. package/src/ingest/mount-coverage.ts +296 -0
  225. package/src/ingest/mount-dom-ingest-root.ts +282 -0
  226. package/src/ingest/mount-ingest-root.ts +744 -0
  227. package/src/ingest/mount-three-ingest-root.ts +366 -0
  228. package/src/ingest/resolve-canvas.ts +47 -0
  229. package/src/ingest/resolve-three.ts +123 -0
  230. package/src/ingest/served-bundle.ts +109 -0
  231. package/src/ingest/served-html-boot.ts +292 -0
  232. package/src/ingest/surface-canvas.ts +84 -0
  233. package/src/ingest/surface-dom.ts +84 -0
  234. package/src/ingest/surface-three.ts +128 -0
  235. package/src/ingest/types.ts +76 -0
  236. package/src/ingest/unmount-ingest-root.ts +224 -0
  237. package/src/navmesh/navmesh-actions.ts +27 -0
  238. package/src/navmesh/navmesh-handler.ts +237 -0
  239. package/src/navmesh/navmesh-workflow-store.ts +78 -0
  240. package/src/network/NetworkInspectorPanel.tsx +644 -0
  241. package/src/network/network-inspector-model.ts +225 -0
  242. package/src/play/play-boot-stall.ts +118 -0
  243. package/src/play/play-log-events.ts +26 -0
  244. package/src/play/play-mode.ts +2553 -0
  245. package/src/play/play-recording.ts +335 -0
  246. package/src/play/react-play-live-authoring.ts +165 -0
  247. package/src/play-bar/PlayBar.tsx +488 -0
  248. package/src/play-bar/PlayerCountPicker.tsx +100 -0
  249. package/src/profiler/FrameDebuggerPanel.tsx +458 -0
  250. package/src/profiler/PerformancePanel.tsx +1068 -0
  251. package/src/profiler/ProfilerPanel.tsx +53 -0
  252. package/src/profiler/frame-debugger-store.ts +97 -0
  253. package/src/profiler/main-thread-busy.ts +90 -0
  254. package/src/react/design-time-react-mount.ts +782 -0
  255. package/src/react/dom-authoring-adapter.ts +764 -0
  256. package/src/react/pasteboard-materialize.ts +154 -0
  257. package/src/react/react-inspector-section.tsx +2496 -0
  258. package/src/react/react-world-authoring-adapter.ts +3795 -0
  259. package/src/react/story-documents/story-args-section.ts +19 -0
  260. package/src/react/story-documents/story-documents.tsx +1380 -0
  261. package/src/react/story-paint-bounds.ts +94 -0
  262. package/src/react/ui-board-document.tsx +101 -0
  263. package/src/react/ui-board-title.ts +9 -0
  264. package/src/react/ui-component-board.ts +72 -0
  265. package/src/services/audio-pose-guard.ts +81 -0
  266. package/src/services/game-audio-unlock.ts +48 -0
  267. package/src/state-watch/StateWatchPanel.tsx +535 -0
  268. package/src/three/authoring/camera-runtime-inspector-section.tsx +135 -0
  269. package/src/three/authoring/constraint-inspector-section.tsx +189 -0
  270. package/src/three/authoring/design-time-renderer.ts +189 -0
  271. package/src/three/authoring/model-asset-inspector-section.css +41 -0
  272. package/src/three/authoring/model-asset-inspector-section.tsx +869 -0
  273. package/src/three/authoring/oid-source-persistence.ts +557 -0
  274. package/src/three/authoring/r3f-design-session.ts +1174 -0
  275. package/src/three/authoring/r3f-source-authoring-adapter.ts +5949 -0
  276. package/src/three/authoring/reflection-probe-inspector-section.tsx +76 -0
  277. package/src/three/authoring/spatial-audio-handles.ts +215 -0
  278. package/src/three/authoring/spatial-collider-handles.ts +229 -0
  279. package/src/three/authoring/spatial-joint-handles.ts +114 -0
  280. package/src/three/authoring/spatial-light-handles.ts +178 -0
  281. package/src/three/authoring/spatial-lod-handles.ts +93 -0
  282. package/src/three/authoring/spatial-particle-handles.ts +368 -0
  283. package/src/three/authoring/three-authoring-adapter.ts +1953 -0
  284. package/src/three/authoring/three-scene-identity.ts +19 -0
  285. package/src/three/authoring/three-spatial-handles.ts +161 -0
  286. package/src/three/authoring/typed-three-inspector.ts +528 -0
  287. package/src/three/component-verbs/extract-menu.ts +71 -0
  288. package/src/three/component-verbs/fork-menu.ts +78 -0
  289. package/src/three/component-verbs/internals-menu.ts +91 -0
  290. package/src/three/story-documents/three-story-documents.tsx +607 -0
  291. package/src/three/three-board/ThreeBoardDocument.tsx +888 -0
  292. package/src/three/three-board/board-framing.ts +412 -0
  293. package/src/three/three-board/board-layout.ts +401 -0
  294. package/src/three/three-board/board-scene.ts +901 -0
  295. package/src/three/three-board/three-component-board.ts +62 -0
  296. package/src/xstate/XStateBehaviorSection.tsx +130 -0
  297. package/src/xstate/XStateMachineInspector.tsx +566 -0
  298. package/src/xstate/character-animation-machine.fixture.ts +74 -0
  299. package/src/xstate/live-behaviors.ts +106 -0
  300. package/src/xstate/use-live-actor-state.ts +44 -0
  301. package/src/xstate/xstate-graph.ts +235 -0
  302. package/src/xstate/xstate-layout.ts +76 -0
@@ -0,0 +1,458 @@
1
+ /**
2
+ * Frame debugger tab (W4b, F11) — the read-only single-frame draw-call
3
+ * introspection surface over `SystemAdapters.RenderDebugAdapter`:
4
+ *
5
+ * - a CAPTURE button (`frame-capture-button`) arms the first-party WebGL2
6
+ * capture and appends the next rendered frame's draw list to a bounded
7
+ * ring (shared with the Game-tab `game-capture-frame` button via
8
+ * `frame-debugger-store.ts`);
9
+ * - a capture HISTORY strip (last 10) to switch between captures;
10
+ * - the selected capture's DRAW CALLS grouped under render-target headers,
11
+ * with prev/next step buttons walking a per-draw selection;
12
+ * - a selected-draw DETAIL panel: entry point, mode/count/instances, program
13
+ * label, geometry attributes, blend/depth/cull/scissor flags, viewport, and
14
+ * the draw's attribution — or an honest "not attributable" for a
15
+ * shadow/composer-pass draw the instrument could not attribute.
16
+ *
17
+ * This is a capture LIST with per-draw STATE, NOT a GPU replayer — the header
18
+ * says so. Degradation ladder rendered honestly (W3a contract): no adapter →
19
+ * the register-an-adapter notice; a capture rejection (paused/stopped world,
20
+ * no frame rendered) → the reason printed, never a fabricated draw list.
21
+ * Editor code speaks only the `RenderDebugAdapter` interface and the plain
22
+ * capture shapes — never the WebGL2 instrument.
23
+ */
24
+
25
+ import {
26
+ deriveRenderDebugCapabilities,
27
+ groupDrawCallsByTarget,
28
+ summarizeCapture,
29
+ } from '../host/components/frame-debugger-model';
30
+ import { editorHost, useHostAvailabilitySelector } from '@volter/editor-sdk/host';
31
+ import { Button, themeVars } from '@volter/editor-sdk/widgets';
32
+ import type {
33
+ FrameCapture,
34
+ FrameCaptureDrawCall,
35
+ } from '@volter/game-runtime/dev/webgl-frame-capture';
36
+ import { useEffect, useState, useSyncExternalStore } from 'react';
37
+ import {
38
+ captureFrame,
39
+ clearCaptures,
40
+ frameDebuggerVersion,
41
+ getCaptureHistory,
42
+ isCapturing,
43
+ lastCaptureError,
44
+ selectCapture,
45
+ subscribeFrameDebugger,
46
+ syncFrameDebuggerAdapter,
47
+ } from './frame-debugger-store';
48
+
49
+ const MONO: React.CSSProperties = {
50
+ fontFamily: themeVars.typography.mono,
51
+ fontSize: 11,
52
+ };
53
+
54
+ // --- Draw-call rows ---------------------------------------------------------
55
+
56
+ function DrawRow({
57
+ draw,
58
+ selected,
59
+ onSelect,
60
+ }: {
61
+ draw: FrameCaptureDrawCall;
62
+ selected: boolean;
63
+ onSelect: () => void;
64
+ }) {
65
+ return (
66
+ <Button
67
+ type="button"
68
+ variant="ghost"
69
+ data-testid="frame-draw-row"
70
+ data-draw-index={draw.index}
71
+ data-selected={selected || undefined}
72
+ className="vgai-frame-draw-row"
73
+ onClick={onSelect}
74
+ >
75
+ <span style={{ ...MONO, color: themeVars.content.dim, width: 34, display: 'inline-block' }}>
76
+ #{draw.index}
77
+ </span>
78
+ <span style={{ ...MONO, color: themeVars.content.primary }}>{draw.mode}</span>
79
+ <span style={{ ...MONO, color: themeVars.content.muted }}> {draw.count}</span>
80
+ {draw.instanceCount !== null && (
81
+ <span style={{ ...MONO, color: themeVars.content.muted }}> ×{draw.instanceCount}</span>
82
+ )}
83
+ <span
84
+ style={{
85
+ ...MONO,
86
+ color: draw.annotation ? themeVars.content.primary : themeVars.content.dim,
87
+ fontStyle: draw.annotation ? 'normal' : 'italic',
88
+ }}
89
+ >
90
+ {' '}
91
+ {draw.annotation
92
+ ? draw.annotation.object.name || draw.annotation.object.type
93
+ : 'unattributed'}
94
+ </span>
95
+ </Button>
96
+ );
97
+ }
98
+
99
+ function DetailField({ label, value }: { label: string; value: React.ReactNode }) {
100
+ return (
101
+ <div style={{ display: 'flex', gap: 8, padding: '1px 0' }}>
102
+ <span style={{ fontSize: 10, color: themeVars.content.muted, width: 92, flexShrink: 0 }}>
103
+ {label}
104
+ </span>
105
+ <span style={{ ...MONO, color: themeVars.content.primary }}>{value}</span>
106
+ </div>
107
+ );
108
+ }
109
+
110
+ const onOff = (b: boolean): string => (b ? 'on' : 'off');
111
+
112
+ function formatDrawTarget(target: FrameCaptureDrawCall['target']): string {
113
+ if (target.kind === 'canvas') return 'canvas (default framebuffer)';
114
+ const size =
115
+ target.width !== null && target.height !== null
116
+ ? `${target.width}×${target.height}`
117
+ : '(size not queryable)';
118
+ return `${target.label ?? 'framebuffer'} ${size}`;
119
+ }
120
+
121
+ function formatDrawState(state: FrameCaptureDrawCall['state']): string {
122
+ return `blend ${onOff(state.blend)} · depth test ${onOff(state.depthTest)} · depth write ${onOff(
123
+ state.depthWrite,
124
+ )} · cull ${state.cull} · scissor ${onOff(state.scissor)}`;
125
+ }
126
+
127
+ function formatCountLine(draw: FrameCaptureDrawCall): string {
128
+ const instances =
129
+ draw.instanceCount !== null ? ` · ${draw.instanceCount} instances` : ' · not instanced';
130
+ return `${draw.mode} · ${draw.count}${instances}`;
131
+ }
132
+
133
+ function DrawDetail({ draw }: { draw: FrameCaptureDrawCall }) {
134
+ return (
135
+ <div data-testid="frame-draw-detail" style={{ padding: 8 }}>
136
+ <DetailField label="entry point" value={draw.entryPoint} />
137
+ <DetailField label="mode / count" value={formatCountLine(draw)} />
138
+ <DetailField label="program" value={draw.programLabel ?? 'none bound'} />
139
+ <DetailField label="target" value={formatDrawTarget(draw.target)} />
140
+ <DetailField label="viewport" value={`[${draw.viewport.join(', ')}]`} />
141
+ <DetailField label="state" value={formatDrawState(draw.state)} />
142
+ {draw.annotation ? (
143
+ <>
144
+ <DetailField
145
+ label="object"
146
+ value={`${draw.annotation.object.name || '(unnamed)'} · ${draw.annotation.object.type}`}
147
+ />
148
+ <DetailField
149
+ label="geometry"
150
+ value={`${draw.annotation.geometry.name || draw.annotation.geometry.type}${
151
+ draw.annotation.geometry.indexed ? ' · indexed' : ''
152
+ }`}
153
+ />
154
+ <DetailField
155
+ label="attributes"
156
+ value={draw.annotation.geometry.attributes.join(', ') || '—'}
157
+ />
158
+ <DetailField
159
+ label="material"
160
+ value={`${draw.annotation.material.name || '(unnamed)'} · ${draw.annotation.material.type}`}
161
+ />
162
+ </>
163
+ ) : (
164
+ <div
165
+ data-testid="frame-draw-unattributed"
166
+ style={{
167
+ marginTop: 6,
168
+ fontSize: 11,
169
+ fontStyle: 'italic',
170
+ color: themeVars.content.muted,
171
+ }}
172
+ >
173
+ Not attributable (shadow / post-processing pass — the instrument attributes only main-pass
174
+ draws through each object's onBeforeRender, and reports the gap honestly rather than
175
+ guessing).
176
+ </div>
177
+ )}
178
+ </div>
179
+ );
180
+ }
181
+
182
+ // --- Selected-capture body --------------------------------------------------
183
+
184
+ /** The two-column view of the selected capture: draw list grouped by target
185
+ * with prev/next stepping, and the selected-draw detail. Owns the per-draw
186
+ * selection index (reset by its `capture.id` key at the call site). */
187
+ function CaptureDetailView({
188
+ capture,
189
+ drawIndex,
190
+ setDrawIndex,
191
+ }: {
192
+ capture: FrameCapture;
193
+ drawIndex: number;
194
+ setDrawIndex: (index: number) => void;
195
+ }) {
196
+ const drawCount = capture.drawCalls.length;
197
+ const boundedIndex = Math.min(drawIndex, Math.max(0, drawCount - 1));
198
+ const selectedDraw = capture.drawCalls[boundedIndex] ?? null;
199
+ const groups = groupDrawCallsByTarget(capture);
200
+
201
+ return (
202
+ <div style={{ flex: 1, minHeight: 0, display: 'flex' }}>
203
+ {/* Draw list, grouped by render target */}
204
+ <div
205
+ style={{
206
+ flex: 1,
207
+ minWidth: 0,
208
+ overflow: 'auto',
209
+ borderRight: `1px solid ${themeVars.boundary.default}`,
210
+ }}
211
+ >
212
+ <div
213
+ style={{
214
+ display: 'flex',
215
+ gap: 6,
216
+ alignItems: 'center',
217
+ padding: 4,
218
+ borderBottom: `1px solid ${themeVars.boundary.default}`,
219
+ }}
220
+ >
221
+ <Button
222
+ type="button"
223
+ size="compact"
224
+ variant="ghost"
225
+ data-testid="frame-draw-prev"
226
+ disabled={boundedIndex <= 0}
227
+ onClick={() => setDrawIndex(Math.max(0, boundedIndex - 1))}
228
+ >
229
+ ◀ Prev
230
+ </Button>
231
+ <Button
232
+ type="button"
233
+ size="compact"
234
+ variant="ghost"
235
+ data-testid="frame-draw-next"
236
+ disabled={boundedIndex >= drawCount - 1}
237
+ onClick={() => setDrawIndex(Math.min(drawCount - 1, boundedIndex + 1))}
238
+ >
239
+ Next ▶
240
+ </Button>
241
+ <span style={{ fontSize: 10, color: themeVars.content.muted }}>
242
+ {drawCount === 0 ? '0' : boundedIndex + 1}/{drawCount}
243
+ </span>
244
+ </div>
245
+ {groups.map((group) => (
246
+ <div key={group.target} data-testid="frame-target-group" data-target={group.target}>
247
+ <div
248
+ style={{
249
+ ...MONO,
250
+ // §2.31: hairline group separator instead of a fill.
251
+ padding: '2px 8px',
252
+ color: themeVars.content.muted,
253
+ borderTop: `1px solid ${themeVars.boundary.default}`,
254
+ borderBottom: `1px solid ${themeVars.boundary.default}`,
255
+ }}
256
+ >
257
+ {group.target} · {group.draws.length} draws
258
+ </div>
259
+ {group.draws.map((draw) => (
260
+ <DrawRow
261
+ key={draw.index}
262
+ draw={draw}
263
+ selected={draw.index === boundedIndex}
264
+ onSelect={() => setDrawIndex(draw.index)}
265
+ />
266
+ ))}
267
+ </div>
268
+ ))}
269
+ </div>
270
+
271
+ {/* Selected-draw detail */}
272
+ <div style={{ flex: 1, minWidth: 0, overflow: 'auto' }}>
273
+ {selectedDraw ? (
274
+ <DrawDetail draw={selectedDraw} />
275
+ ) : (
276
+ <div style={{ padding: 12, color: themeVars.content.muted, fontSize: 12 }}>
277
+ This capture recorded no draws.
278
+ </div>
279
+ )}
280
+ {capture.notes.length > 0 && (
281
+ <div
282
+ style={{ padding: '4px 8px', fontSize: 10, color: themeVars.content.dim }}
283
+ data-testid="frame-capture-notes"
284
+ >
285
+ {capture.notes.join(' ')}
286
+ </div>
287
+ )}
288
+ </div>
289
+ </div>
290
+ );
291
+ }
292
+
293
+ /** The capture-history strip (last N captures). */
294
+ function CaptureHistoryStrip({
295
+ captures,
296
+ selectedId,
297
+ }: {
298
+ captures: readonly FrameCapture[];
299
+ selectedId: number | null;
300
+ }) {
301
+ return (
302
+ <div
303
+ data-testid="frame-capture-history"
304
+ style={{
305
+ display: 'flex',
306
+ gap: 6,
307
+ flexWrap: 'wrap',
308
+ padding: '4px 8px',
309
+ borderBottom: `1px solid ${themeVars.boundary.default}`,
310
+ }}
311
+ >
312
+ {captures.map((capture) => {
313
+ const summary = summarizeCapture(capture);
314
+ return (
315
+ <Button
316
+ key={capture.id}
317
+ type="button"
318
+ size="compact"
319
+ variant="ghost"
320
+ data-testid="frame-capture-item"
321
+ data-capture-id={capture.id}
322
+ data-selected={capture.id === selectedId || undefined}
323
+ onClick={() => selectCapture(capture.id)}
324
+ >
325
+ #{capture.id} · {summary.drawCalls} draws
326
+ {summary.unattributed > 0 ? ` · ${summary.unattributed} unattr` : ''}
327
+ {summary.truncated > 0 ? ` · +${summary.truncated} capped` : ''}
328
+ </Button>
329
+ );
330
+ })}
331
+ </div>
332
+ );
333
+ }
334
+
335
+ // --- The panel --------------------------------------------------------------
336
+
337
+ export function FrameDebuggerPanel() {
338
+ useSyncExternalStore(subscribeFrameDebugger, frameDebuggerVersion, frameDebuggerVersion);
339
+ const [selectedDrawIndex, setSelectedDrawIndex] = useState(0);
340
+
341
+ const adapter = useHostAvailabilitySelector(
342
+ () => editorHost().systems.inspected().renderDebug ?? null,
343
+ );
344
+ // Reset the shared ring when the adapter identity changes (new mount / stop).
345
+ useEffect(() => {
346
+ syncFrameDebuggerAdapter(adapter);
347
+ }, [adapter]);
348
+
349
+ const caps = deriveRenderDebugCapabilities(adapter);
350
+ const history = getCaptureHistory();
351
+ const selected = history.selected;
352
+
353
+ // Reset the per-draw selection when the selected capture changes.
354
+ useEffect(() => {
355
+ setSelectedDrawIndex(0);
356
+ }, [selected?.id]);
357
+
358
+ if (!adapter || !caps) {
359
+ return (
360
+ <div
361
+ data-testid="frame-debugger-empty"
362
+ style={{ padding: 16, color: themeVars.content.muted, fontSize: 12, lineHeight: '18px' }}
363
+ >
364
+ <div style={{ color: themeVars.content.primary, marginBottom: 4 }}>
365
+ No render-debug adapter — no game session is running.
366
+ </div>
367
+ Frame capture exists only while a game session runs. Press Play: first-party worlds register
368
+ a <code style={MONO}>RenderDebugAdapter</code> automatically when they hold a real WebGL2
369
+ context; a custom world exposes its own via{' '}
370
+ <code style={MONO}>{"ctx.registerSystemAdapter?.('renderDebug', adapter)"}</code>. A
371
+ headless mount registers none — there is nothing to capture.
372
+ </div>
373
+ );
374
+ }
375
+
376
+ const captureError = lastCaptureError();
377
+ const capturing = isCapturing();
378
+
379
+ return (
380
+ <div
381
+ data-testid="frame-debugger"
382
+ // §2.31 P2 amendment: text-dense output region reads over a local
383
+ // frosted layer (inert for non-frost themes).
384
+ className="vgai-content-frost"
385
+ style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', fontSize: 12 }}
386
+ >
387
+ {/* Header: capture control + honest scope statement + history strip */}
388
+ <div
389
+ style={{
390
+ display: 'flex',
391
+ flexWrap: 'wrap',
392
+ gap: 10,
393
+ alignItems: 'center',
394
+ padding: '4px 8px',
395
+ borderBottom: `1px solid ${themeVars.boundary.default}`,
396
+ }}
397
+ >
398
+ <Button
399
+ type="button"
400
+ size="compact"
401
+ data-testid="frame-capture-button"
402
+ disabled={capturing}
403
+ onClick={() => void captureFrame(adapter)}
404
+ >
405
+ {capturing ? 'Capturing…' : 'Capture frame'}
406
+ </Button>
407
+ <span style={{ fontSize: 10, color: themeVars.content.dim }}>
408
+ Single-frame draw-call list with per-draw state — not a GPU replayer.
409
+ </span>
410
+ {history.all.length > 0 && (
411
+ <Button
412
+ type="button"
413
+ size="compact"
414
+ variant="ghost"
415
+ data-testid="frame-capture-clear"
416
+ onClick={() => clearCaptures()}
417
+ >
418
+ Clear
419
+ </Button>
420
+ )}
421
+ </div>
422
+
423
+ {captureError && (
424
+ <div
425
+ data-testid="frame-capture-error"
426
+ style={{
427
+ padding: '4px 8px',
428
+ fontSize: 11,
429
+ color: themeVars.semantic.danger,
430
+ borderBottom: `1px solid ${themeVars.boundary.default}`,
431
+ }}
432
+ >
433
+ Capture failed: {captureError}
434
+ </div>
435
+ )}
436
+
437
+ {history.all.length > 0 && (
438
+ <CaptureHistoryStrip captures={history.all} selectedId={selected?.id ?? null} />
439
+ )}
440
+
441
+ {selected ? (
442
+ <CaptureDetailView
443
+ key={selected.id}
444
+ capture={selected}
445
+ drawIndex={selectedDrawIndex}
446
+ setDrawIndex={setSelectedDrawIndex}
447
+ />
448
+ ) : (
449
+ <div
450
+ data-testid="frame-debugger-no-capture"
451
+ style={{ padding: 12, color: themeVars.content.muted, fontSize: 12 }}
452
+ >
453
+ No frame captured yet. Press <strong>Capture frame</strong> while the game renders.
454
+ </div>
455
+ )}
456
+ </div>
457
+ );
458
+ }