@zenfg/inspector 0.1.0-beta.1

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 (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -0
  3. package/dist/FrameGraphInspector.d.ts +153 -0
  4. package/dist/FrameGraphInspector.d.ts.map +1 -0
  5. package/dist/FrameGraphInspector.js +580 -0
  6. package/dist/FrameGraphInspector.js.map +1 -0
  7. package/dist/debugCaptureModel.d.ts +166 -0
  8. package/dist/debugCaptureModel.d.ts.map +1 -0
  9. package/dist/debugCaptureModel.js +364 -0
  10. package/dist/debugCaptureModel.js.map +1 -0
  11. package/dist/index.d.ts +17 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +15 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/panelAliasAnalysis.d.ts +20 -0
  16. package/dist/panelAliasAnalysis.d.ts.map +1 -0
  17. package/dist/panelAliasAnalysis.js +113 -0
  18. package/dist/panelAliasAnalysis.js.map +1 -0
  19. package/dist/panelCytoscapeGraphRenderer.d.ts +71 -0
  20. package/dist/panelCytoscapeGraphRenderer.d.ts.map +1 -0
  21. package/dist/panelCytoscapeGraphRenderer.js +559 -0
  22. package/dist/panelCytoscapeGraphRenderer.js.map +1 -0
  23. package/dist/panelDiagnosticsView.d.ts +22 -0
  24. package/dist/panelDiagnosticsView.d.ts.map +1 -0
  25. package/dist/panelDiagnosticsView.js +123 -0
  26. package/dist/panelDiagnosticsView.js.map +1 -0
  27. package/dist/panelDomHelpers.d.ts +26 -0
  28. package/dist/panelDomHelpers.d.ts.map +1 -0
  29. package/dist/panelDomHelpers.js +129 -0
  30. package/dist/panelDomHelpers.js.map +1 -0
  31. package/dist/panelGraphLayout.d.ts +28 -0
  32. package/dist/panelGraphLayout.d.ts.map +1 -0
  33. package/dist/panelGraphLayout.js +223 -0
  34. package/dist/panelGraphLayout.js.map +1 -0
  35. package/dist/panelGraphRenderer.d.ts +20 -0
  36. package/dist/panelGraphRenderer.d.ts.map +1 -0
  37. package/dist/panelGraphRenderer.js +2 -0
  38. package/dist/panelGraphRenderer.js.map +1 -0
  39. package/dist/panelGraphScene.d.ts +98 -0
  40. package/dist/panelGraphScene.d.ts.map +1 -0
  41. package/dist/panelGraphScene.js +559 -0
  42. package/dist/panelGraphScene.js.map +1 -0
  43. package/dist/panelGraphView.d.ts +9 -0
  44. package/dist/panelGraphView.d.ts.map +1 -0
  45. package/dist/panelGraphView.js +88 -0
  46. package/dist/panelGraphView.js.map +1 -0
  47. package/dist/panelGraphVisuals.d.ts +38 -0
  48. package/dist/panelGraphVisuals.d.ts.map +1 -0
  49. package/dist/panelGraphVisuals.js +330 -0
  50. package/dist/panelGraphVisuals.js.map +1 -0
  51. package/dist/panelIcons.d.ts +4 -0
  52. package/dist/panelIcons.d.ts.map +1 -0
  53. package/dist/panelIcons.js +43 -0
  54. package/dist/panelIcons.js.map +1 -0
  55. package/dist/panelInspectorView.d.ts +33 -0
  56. package/dist/panelInspectorView.d.ts.map +1 -0
  57. package/dist/panelInspectorView.js +357 -0
  58. package/dist/panelInspectorView.js.map +1 -0
  59. package/dist/panelMemoryView.d.ts +21 -0
  60. package/dist/panelMemoryView.d.ts.map +1 -0
  61. package/dist/panelMemoryView.js +134 -0
  62. package/dist/panelMemoryView.js.map +1 -0
  63. package/dist/panelPassesView.d.ts +31 -0
  64. package/dist/panelPassesView.d.ts.map +1 -0
  65. package/dist/panelPassesView.js +183 -0
  66. package/dist/panelPassesView.js.map +1 -0
  67. package/dist/panelResourcesView.d.ts +19 -0
  68. package/dist/panelResourcesView.d.ts.map +1 -0
  69. package/dist/panelResourcesView.js +89 -0
  70. package/dist/panelResourcesView.js.map +1 -0
  71. package/dist/panelSelection.d.ts +5 -0
  72. package/dist/panelSelection.d.ts.map +1 -0
  73. package/dist/panelSelection.js +88 -0
  74. package/dist/panelSelection.js.map +1 -0
  75. package/dist/panelTypes.d.ts +40 -0
  76. package/dist/panelTypes.d.ts.map +1 -0
  77. package/dist/panelTypes.js +2 -0
  78. package/dist/panelTypes.js.map +1 -0
  79. package/dist/panelVisualTheme.d.ts +76 -0
  80. package/dist/panelVisualTheme.d.ts.map +1 -0
  81. package/dist/panelVisualTheme.js +39 -0
  82. package/dist/panelVisualTheme.js.map +1 -0
  83. package/dist/panelWorkbenchHelpers.d.ts +34 -0
  84. package/dist/panelWorkbenchHelpers.d.ts.map +1 -0
  85. package/dist/panelWorkbenchHelpers.js +162 -0
  86. package/dist/panelWorkbenchHelpers.js.map +1 -0
  87. package/dist/panelWorkbenchView.d.ts +77 -0
  88. package/dist/panelWorkbenchView.d.ts.map +1 -0
  89. package/dist/panelWorkbenchView.js +409 -0
  90. package/dist/panelWorkbenchView.js.map +1 -0
  91. package/dist/query.d.ts +11 -0
  92. package/dist/query.d.ts.map +1 -0
  93. package/dist/query.js +21 -0
  94. package/dist/query.js.map +1 -0
  95. package/dist/styles.d.ts +8 -0
  96. package/dist/styles.d.ts.map +1 -0
  97. package/dist/styles.js +1073 -0
  98. package/dist/styles.js.map +1 -0
  99. package/package.json +29 -0
  100. package/src/FrameGraphInspector.ts +632 -0
  101. package/src/debugCaptureModel.ts +583 -0
  102. package/src/index.ts +21 -0
  103. package/src/panelAliasAnalysis.ts +164 -0
  104. package/src/panelCytoscapeGraphRenderer.ts +617 -0
  105. package/src/panelDiagnosticsView.ts +160 -0
  106. package/src/panelDomHelpers.ts +151 -0
  107. package/src/panelGraphLayout.ts +281 -0
  108. package/src/panelGraphRenderer.ts +21 -0
  109. package/src/panelGraphScene.ts +787 -0
  110. package/src/panelGraphView.ts +108 -0
  111. package/src/panelGraphVisuals.ts +364 -0
  112. package/src/panelIcons.ts +56 -0
  113. package/src/panelInspectorView.ts +377 -0
  114. package/src/panelMemoryView.ts +154 -0
  115. package/src/panelPassesView.ts +222 -0
  116. package/src/panelResourcesView.ts +115 -0
  117. package/src/panelSelection.ts +99 -0
  118. package/src/panelTypes.ts +28 -0
  119. package/src/panelVisualTheme.ts +39 -0
  120. package/src/panelWorkbenchHelpers.ts +216 -0
  121. package/src/panelWorkbenchView.ts +465 -0
  122. package/src/query.ts +22 -0
  123. package/src/styles.ts +1076 -0
@@ -0,0 +1,216 @@
1
+ import type {
2
+ FrameGraphDebugResource,
3
+ FrameGraphDebugViewModel,
4
+ } from './debugCaptureModel.ts';
5
+ import { createCell, formatBytes, labelResource } from './panelDomHelpers.ts';
6
+ import type { Selection } from './panelTypes.ts';
7
+
8
+ export type WorkbenchCallbacks = {
9
+ readonly onSelect: (selection: Selection) => void;
10
+ readonly onHover: (selection: Selection | undefined) => void;
11
+ readonly onGroupToggle: (pathKey: string) => void;
12
+ readonly isGroupExpanded: (pathKey: string) => boolean;
13
+ };
14
+ export type WorkbenchTableColumn = {
15
+ readonly label: string;
16
+ readonly column?: 'code' | 'kind' | 'numeric';
17
+ };
18
+
19
+
20
+ export function selectionKey(selection: Selection): string {
21
+ switch (selection.kind) {
22
+ case 'node': return `node:${selection.id}`;
23
+ case 'group': return `group:${selection.pathKey}`;
24
+ case 'resource': return `resource:${selection.id}`;
25
+ case 'root': return `root:${selection.index}`;
26
+ case 'culled': return `culled:${selection.index}`;
27
+ case 'allocation': return `allocation:${selection.id}`;
28
+ case 'segment': return `segment:${selection.index}`;
29
+ }
30
+ }
31
+
32
+ export function sameSelection(a: Selection | undefined, b: Selection | undefined): boolean {
33
+ return a === undefined || b === undefined ? a === b : selectionKey(a) === selectionKey(b);
34
+ }
35
+
36
+ export function updateSelectedRows(rows: ReadonlyMap<string, readonly HTMLElement[]>, selected: Selection | undefined): void {
37
+ const key = selected ? selectionKey(selected) : undefined;
38
+ for (const [rowKey, elements] of rows) {
39
+ for (const element of elements) {
40
+ const active = rowKey === key;
41
+ element.classList.toggle('selected', active);
42
+ if (element.getAttribute('role') === 'option') {
43
+ element.setAttribute('aria-selected', active ? 'true' : 'false');
44
+ }
45
+ }
46
+ }
47
+ }
48
+
49
+ export function registerSelectable(
50
+ rows: Map<string, HTMLElement[]>,
51
+ element: HTMLElement,
52
+ selection: Selection,
53
+ callbacks: WorkbenchCallbacks,
54
+ ): void {
55
+ const key = selectionKey(selection);
56
+ const elements = rows.get(key) ?? [];
57
+ elements.push(element);
58
+ rows.set(key, elements);
59
+ element.dataset.selectionKey = key;
60
+ element.addEventListener('mouseenter', () => callbacks.onHover(selection));
61
+ element.addEventListener('mouseleave', () => callbacks.onHover(undefined));
62
+ }
63
+
64
+ export function createViewToolbar(label: string): HTMLDivElement {
65
+ const toolbar = document.createElement('div');
66
+ toolbar.className = 'zenfg-inspector-view-toolbar';
67
+ toolbar.setAttribute('role', 'toolbar');
68
+ toolbar.setAttribute('aria-label', label);
69
+ return toolbar;
70
+ }
71
+
72
+ export function createSearchInput(
73
+ placeholder: string,
74
+ value: string,
75
+ onInput: (value: string) => void,
76
+ ): HTMLInputElement {
77
+ const input = document.createElement('input');
78
+ input.type = 'search';
79
+ input.placeholder = placeholder;
80
+ input.value = value;
81
+ input.setAttribute('aria-label', placeholder);
82
+ input.addEventListener('input', () => onInput(input.value));
83
+ return input;
84
+ }
85
+
86
+ export function createFilterSelect(
87
+ label: string,
88
+ value: string,
89
+ options: readonly (readonly [value: string, label: string])[],
90
+ onChange: (value: string) => void,
91
+ ): HTMLSelectElement {
92
+ const select = document.createElement('select');
93
+ select.setAttribute('aria-label', label);
94
+ for (const [optionValue, optionLabel] of options) {
95
+ const option = document.createElement('option');
96
+ option.value = optionValue;
97
+ option.textContent = optionLabel;
98
+ select.appendChild(option);
99
+ }
100
+ select.value = value;
101
+ select.addEventListener('change', () => onChange(select.value));
102
+ return select;
103
+ }
104
+
105
+ export function createTableScroller(headers: readonly WorkbenchTableColumn[]): {
106
+ readonly scroller: HTMLDivElement;
107
+ readonly table: HTMLTableElement;
108
+ readonly body: HTMLTableSectionElement;
109
+ } {
110
+ const scroller = document.createElement('div');
111
+ scroller.className = 'zenfg-inspector-table-scroller';
112
+ const table = document.createElement('table');
113
+ table.className = 'zenfg-inspector-workbench-table';
114
+ const head = document.createElement('thead');
115
+ const row = document.createElement('tr');
116
+ for (const header of headers) {
117
+ const cell = document.createElement('th');
118
+ cell.textContent = header.label;
119
+ if (header.column) cell.dataset.column = header.column;
120
+ row.appendChild(cell);
121
+ }
122
+ head.appendChild(row);
123
+ const body = document.createElement('tbody');
124
+ table.append(head, body);
125
+ scroller.appendChild(table);
126
+ return { scroller, table, body };
127
+ }
128
+
129
+ export function createSelectionCell(
130
+ text: string,
131
+ selection: Selection,
132
+ callbacks: WorkbenchCallbacks,
133
+ column?: WorkbenchTableColumn['column'],
134
+ ): HTMLTableCellElement {
135
+ const cell = document.createElement('td');
136
+ const button = createRelationButton(text, selection, callbacks.onSelect);
137
+ cell.appendChild(button);
138
+ if (column) cell.dataset.column = column;
139
+ return cell;
140
+ }
141
+
142
+ export function createRelationButton(
143
+ text: string,
144
+ selection: Selection,
145
+ onSelect: (selection: Selection) => void,
146
+ ): HTMLButtonElement {
147
+ const button = document.createElement('button');
148
+ button.type = 'button';
149
+ button.className = 'zenfg-inspector-relation-button';
150
+ button.textContent = text;
151
+ button.addEventListener('click', () => onSelect(selection));
152
+ return button;
153
+ }
154
+
155
+ export function createKindCell(kind: string, label = kind): HTMLTableCellElement {
156
+ const cell = createCell('', { column: 'kind', kind });
157
+ const marker = document.createElement('span');
158
+ marker.className = 'zenfg-inspector-kind-label';
159
+ marker.dataset.kind = kind;
160
+ marker.textContent = label;
161
+ cell.appendChild(marker);
162
+ return cell;
163
+ }
164
+
165
+ export function groupPath(snapshot: FrameGraphDebugViewModel, groupId: string | undefined): string {
166
+ if (groupId === undefined) return '-';
167
+ return snapshot.groupById.get(groupId)?.path.join(' / ') ?? `group-${groupId}`;
168
+ }
169
+
170
+ export function formatEstimatedBytes(bytes: number | undefined): string {
171
+ return bytes === undefined ? 'Unknown' : formatBytes(bytes);
172
+ }
173
+
174
+ export function formatResourceDescriptor(resource: FrameGraphDebugResource): string {
175
+ const descriptor = resource.descriptor;
176
+ if (!descriptor) return 'Unknown';
177
+ if (resource.kind === 'buffer' && 'size' in descriptor && typeof descriptor.size === 'number') {
178
+ return `${formatBytes(descriptor.size)} buffer`;
179
+ }
180
+ if ('format' in descriptor) {
181
+ const size = descriptor.size;
182
+ const extent = `${size.width}×${size.height}×${size.depthOrArrayLayers}`;
183
+ const extras = [
184
+ descriptor.dimension !== '2d' ? descriptor.dimension : undefined,
185
+ descriptor.mipLevelCount > 1 ? `${descriptor.mipLevelCount} mips` : undefined,
186
+ descriptor.sampleCount > 1 ? `${descriptor.sampleCount}× MSAA` : undefined,
187
+ descriptor.viewFormats.length > 0 ? `views ${descriptor.viewFormats.join(', ')}` : undefined,
188
+ ].filter(Boolean);
189
+ return `${descriptor.format} · ${extent}${extras.length ? ` · ${extras.join(' · ')}` : ''}`;
190
+ }
191
+ return 'Unknown';
192
+ }
193
+
194
+ export function resourceAccessCounts(snapshot: FrameGraphDebugViewModel, resourceId: string): readonly [number, number] {
195
+ let reads = 0;
196
+ let writes = 0;
197
+ for (const access of snapshot.accessesByResourceId.get(resourceId) ?? []) {
198
+ if (access.mode === 'read') reads++;
199
+ else writes++;
200
+ }
201
+ return [reads, writes];
202
+ }
203
+
204
+ export function createEmptyTableRow(columnCount: number, text: string): HTMLTableRowElement {
205
+ const row = document.createElement('tr');
206
+ const cell = createCell(text);
207
+ cell.colSpan = columnCount;
208
+ cell.className = 'zenfg-inspector-empty-row';
209
+ row.appendChild(cell);
210
+ return row;
211
+ }
212
+
213
+ export function resourceLabel(snapshot: FrameGraphDebugViewModel, resourceId: string): string {
214
+ const resource = snapshot.resourceById.get(resourceId);
215
+ return resource ? labelResource(resource) : `resource-${resourceId}`;
216
+ }
@@ -0,0 +1,465 @@
1
+ import type { FrameGraphDebugViewModel } from './debugCaptureModel.ts';
2
+ import {
3
+ formatBytes,
4
+ formatGpuFrameDuration,
5
+ formatPoolHitRate,
6
+ labelNode,
7
+ } from './panelDomHelpers.ts';
8
+ import { createPanelIcon, setPanelButtonContent } from './panelIcons.ts';
9
+ import { DiagnosticsView } from './panelDiagnosticsView.ts';
10
+ import { InspectorView } from './panelInspectorView.ts';
11
+ import { MemoryView } from './panelMemoryView.ts';
12
+ import { PassesView } from './panelPassesView.ts';
13
+ import { ResourcesView } from './panelResourcesView.ts';
14
+ import { renderGraphView, resizeGraph } from './panelGraphView.ts';
15
+ import type { GraphViewState, Selection, WorkbenchTab } from './panelTypes.ts';
16
+ import { formatEstimatedBytes, type WorkbenchCallbacks } from './panelWorkbenchHelpers.ts';
17
+
18
+ export type FrameGraphDebugWorkbenchActions = {
19
+ onCapture(): void;
20
+ onImport(file: File): void;
21
+ onDownload(): void;
22
+ onCopyJson(): void;
23
+ };
24
+ export type FrameGraphDebugWorkbenchOptions = {
25
+ branding: string | false;
26
+ idPrefix: string;
27
+ };
28
+ export type FrameGraphDebugEmptyStateKind = 'waiting' | 'capturing' | 'empty' | 'error';
29
+
30
+
31
+ export type FrameGraphDebugSnapshotActionState = {
32
+ providerAvailable: boolean;
33
+ hasCapture: boolean;
34
+ capturing: boolean;
35
+ importing: boolean;
36
+ copying: boolean;
37
+ copied: boolean;
38
+ message?: string;
39
+ messageTone?: 'neutral' | 'error';
40
+ };
41
+
42
+ export class FrameGraphDebugWorkbench {
43
+ readonly root = document.createElement('div');
44
+ private readonly summary = document.createElement('div');
45
+ private readonly commandBar = document.createElement('div');
46
+ private readonly brand = document.createElement('div');
47
+ private readonly tabList = document.createElement('div');
48
+ private readonly commandActions = document.createElement('div');
49
+ private readonly commandStatus = document.createElement('span');
50
+ private readonly workspace = document.createElement('div');
51
+ private readonly main = document.createElement('main');
52
+ private readonly emptyHost = document.createElement('div');
53
+ private readonly overviewRoot = document.createElement('section');
54
+ private readonly graphRoot = document.createElement('section');
55
+ private readonly passes: PassesView;
56
+ private readonly resources: ResourcesView;
57
+ private readonly memory: MemoryView;
58
+ private readonly diagnostics: DiagnosticsView;
59
+ private readonly inspector: InspectorView;
60
+ private readonly inspectorOpenButton = document.createElement('button');
61
+ private readonly captureButton = document.createElement('button');
62
+ private readonly importButton = document.createElement('button');
63
+ private readonly importInput = document.createElement('input');
64
+ private readonly exportButton = document.createElement('button');
65
+ private readonly exportMenu = document.createElement('div');
66
+ private readonly downloadButton = document.createElement('button');
67
+ private readonly copyButton = document.createElement('button');
68
+ private readonly tabButtons = new Map<WorkbenchTab, HTMLButtonElement>();
69
+ private readonly views = new Map<WorkbenchTab, HTMLElement>();
70
+ private activeTab: WorkbenchTab = 'graph';
71
+ private hasSnapshot = false;
72
+ private snapshot: FrameGraphDebugViewModel | undefined;
73
+ private selected: Selection | undefined;
74
+ private hovered: Selection | undefined;
75
+
76
+ constructor(
77
+ private readonly graphView: GraphViewState,
78
+ private readonly callbacks: WorkbenchCallbacks,
79
+ actions: FrameGraphDebugWorkbenchActions,
80
+ options: FrameGraphDebugWorkbenchOptions,
81
+ ) {
82
+ this.root.className = 'zenfg-inspector-workbench';
83
+ this.summary.className = 'zenfg-inspector-capture-summary';
84
+ this.commandBar.className = 'zenfg-inspector-workbench-command-bar';
85
+ this.brand.className = 'zenfg-inspector-brand';
86
+ if (options.branding === false) this.commandBar.classList.add('branding-hidden');
87
+ else this.brand.textContent = options.branding;
88
+ this.tabList.className = 'zenfg-inspector-workbench-tabs';
89
+ this.tabList.setAttribute('role', 'tablist');
90
+ this.tabList.setAttribute('aria-label', 'FrameGraph debug views');
91
+ this.commandActions.className = 'zenfg-inspector-workbench-actions';
92
+ this.commandActions.setAttribute('role', 'toolbar');
93
+ this.commandActions.setAttribute('aria-label', 'FrameGraph commands');
94
+ this.commandStatus.className = 'zenfg-inspector-command-status';
95
+ this.commandStatus.setAttribute('role', 'status');
96
+ this.commandStatus.setAttribute('aria-live', 'polite');
97
+ this.workspace.className = 'zenfg-inspector-workspace inspector-open';
98
+ this.main.className = 'zenfg-inspector-main';
99
+ this.emptyHost.className = 'zenfg-inspector-workbench-empty';
100
+ this.emptyHost.setAttribute('role', 'status');
101
+ this.emptyHost.setAttribute('aria-live', 'polite');
102
+
103
+ this.overviewRoot.className = 'zenfg-inspector-view zenfg-inspector-overview-view';
104
+ this.overviewRoot.id = `${options.idPrefix}-view-overview`;
105
+ this.overviewRoot.setAttribute('role', 'tabpanel');
106
+ this.overviewRoot.append(this.summary);
107
+ this.graphRoot.className = 'zenfg-inspector-view zenfg-inspector-graph-view';
108
+ this.graphRoot.id = `${options.idPrefix}-view-graph`;
109
+ this.graphRoot.setAttribute('role', 'tabpanel');
110
+ this.graphRoot.append(this.graphView.toolbar, this.graphView.host);
111
+ this.passes = new PassesView(callbacks, options.idPrefix);
112
+ this.resources = new ResourcesView(callbacks, options.idPrefix);
113
+ this.memory = new MemoryView(callbacks, options.idPrefix);
114
+ this.diagnostics = new DiagnosticsView(callbacks, options.idPrefix);
115
+ this.inspector = new InspectorView(callbacks, (open) => this.handleInspectorOpenChange(open), options.idPrefix);
116
+
117
+ this.views.set('overview', this.overviewRoot);
118
+ this.views.set('graph', this.graphRoot);
119
+ this.views.set('passes', this.passes.root);
120
+ this.views.set('resources', this.resources.root);
121
+ this.views.set('memory', this.memory.root);
122
+ this.views.set('diagnostics', this.diagnostics.root);
123
+ for (const [tab, label] of [
124
+ ['overview', 'Overview'],
125
+ ['graph', 'Graph'],
126
+ ['passes', 'Passes'],
127
+ ['resources', 'Resources'],
128
+ ['memory', 'Memory'],
129
+ ['diagnostics', 'Diagnostics'],
130
+ ] as const) {
131
+ const button = document.createElement('button');
132
+ button.type = 'button';
133
+ button.id = `${options.idPrefix}-tab-${tab}`;
134
+ button.textContent = label;
135
+ button.setAttribute('role', 'tab');
136
+ button.setAttribute('aria-controls', `${options.idPrefix}-view-${tab}`);
137
+ button.addEventListener('click', () => this.setActiveTab(tab));
138
+ this.tabButtons.set(tab, button);
139
+ this.tabList.appendChild(button);
140
+ }
141
+
142
+ this.inspectorOpenButton.type = 'button';
143
+ this.inspectorOpenButton.hidden = true;
144
+ this.inspectorOpenButton.className = 'zenfg-inspector-open-inspector';
145
+ setPanelButtonContent(this.inspectorOpenButton, 'inspector', 'Inspector');
146
+ this.inspectorOpenButton.title = 'Open selection inspector';
147
+ this.inspectorOpenButton.addEventListener('click', () => this.inspector.setOpen(true));
148
+
149
+ this.captureButton.type = 'button';
150
+ this.captureButton.className = 'zenfg-inspector-capture-action';
151
+ this.captureButton.addEventListener('click', actions.onCapture);
152
+ this.importButton.type = 'button';
153
+ this.importButton.textContent = 'Import';
154
+ this.importButton.addEventListener('click', () => this.importInput.click());
155
+ this.importInput.type = 'file';
156
+ this.importInput.accept = '.fgsnapshot.json,.json,application/json';
157
+ this.importInput.hidden = true;
158
+ this.importInput.addEventListener('change', () => {
159
+ const file = this.importInput.files?.[0];
160
+ this.importInput.value = '';
161
+ if (file) actions.onImport(file);
162
+ });
163
+ this.exportButton.type = 'button';
164
+ this.exportButton.textContent = 'Export';
165
+ this.exportButton.setAttribute('aria-haspopup', 'menu');
166
+ this.exportButton.setAttribute('aria-expanded', 'false');
167
+ this.exportButton.setAttribute('aria-controls', `${options.idPrefix}-export-menu`);
168
+ this.exportButton.addEventListener('click', () => {
169
+ this.setExportMenuOpen(this.exportMenu.hidden !== false);
170
+ });
171
+ this.exportMenu.className = 'zenfg-inspector-export-menu';
172
+ this.exportMenu.id = `${options.idPrefix}-export-menu`;
173
+ this.exportMenu.setAttribute('role', 'menu');
174
+ this.exportMenu.hidden = true;
175
+ this.downloadButton.type = 'button';
176
+ this.downloadButton.textContent = 'Download JSON';
177
+ this.downloadButton.setAttribute('role', 'menuitem');
178
+ this.downloadButton.addEventListener('click', () => {
179
+ this.setExportMenuOpen(false);
180
+ actions.onDownload();
181
+ });
182
+ this.copyButton.type = 'button';
183
+ this.copyButton.className = 'zenfg-inspector-copy-action';
184
+ this.copyButton.textContent = 'Copy JSON';
185
+ this.copyButton.setAttribute('role', 'menuitem');
186
+ this.copyButton.addEventListener('click', () => {
187
+ this.setExportMenuOpen(false);
188
+ actions.onCopyJson();
189
+ });
190
+ this.exportMenu.append(this.downloadButton, this.copyButton);
191
+
192
+ this.commandActions.append(
193
+ this.commandStatus,
194
+ this.inspectorOpenButton,
195
+ this.captureButton,
196
+ this.importButton,
197
+ this.exportButton,
198
+ this.importInput,
199
+ );
200
+ if (options.branding === false) this.commandBar.append(this.tabList, this.commandActions, this.exportMenu);
201
+ else this.commandBar.append(this.brand, this.tabList, this.commandActions, this.exportMenu);
202
+ this.root.addEventListener('click', (event) => {
203
+ const target = event.target;
204
+ if (!target || typeof (target as Node).nodeType !== 'number' || this.exportMenu.hidden) return;
205
+ const targetNode = target as Node;
206
+ if (!this.exportButton.contains(targetNode) && !this.exportMenu.contains(targetNode)) this.setExportMenuOpen(false);
207
+ });
208
+ this.root.addEventListener('keydown', (event) => {
209
+ if (event.key !== 'Escape' || this.exportMenu.hidden) return;
210
+ this.setExportMenuOpen(false);
211
+ this.exportButton.focus();
212
+ });
213
+ this.main.append(this.emptyHost, ...this.views.values());
214
+ this.workspace.append(this.main, this.inspector.root);
215
+ this.root.append(this.commandBar, this.workspace);
216
+ this.showEmptyState('empty', 'Drop a ZenFG Snapshot here or choose Import.', 'Files are processed locally in your browser.');
217
+ this.setSnapshotActionState({
218
+ providerAvailable: false,
219
+ hasCapture: false,
220
+ capturing: false,
221
+ importing: false,
222
+ copying: false,
223
+ copied: false,
224
+ });
225
+ }
226
+
227
+ setSnapshot(snapshot: FrameGraphDebugViewModel, selected: Selection | undefined): void {
228
+ this.hasSnapshot = true;
229
+ this.snapshot = snapshot;
230
+ this.selected = selected;
231
+ this.hovered = undefined;
232
+ this.renderSummary(snapshot);
233
+ this.passes.setSnapshot(snapshot);
234
+ this.resources.setSnapshot(snapshot);
235
+ this.memory.setSnapshot(snapshot);
236
+ this.diagnostics.setSnapshot(snapshot);
237
+ this.inspector.setSnapshot(snapshot);
238
+ this.passes.setSelection(selected);
239
+ this.resources.setSelection(selected);
240
+ this.memory.setSelection(selected);
241
+ this.diagnostics.setSelection(selected);
242
+ this.inspector.setSelection(selected, false);
243
+ this.emptyHost.hidden = true;
244
+ this.updateActiveTab();
245
+ this.updateWorkspaceState();
246
+ if (this.activeTab === 'graph') this.renderGraph();
247
+ }
248
+
249
+ showEmptyState(kind: FrameGraphDebugEmptyStateKind, message: string, detail?: string): void {
250
+ this.hasSnapshot = false;
251
+ this.snapshot = undefined;
252
+ this.selected = undefined;
253
+ this.hovered = undefined;
254
+ this.emptyHost.hidden = false;
255
+ this.emptyHost.dataset.state = kind;
256
+ const icon = createPanelIcon(kind === 'capturing' ? 'spinner' : kind === 'error' ? 'error' : kind === 'waiting' ? 'waiting' : 'empty');
257
+ const label = document.createElement('span');
258
+ label.className = 'zenfg-inspector-empty-message';
259
+ label.textContent = message;
260
+ const detailLabel = document.createElement('span');
261
+ detailLabel.className = 'zenfg-inspector-empty-detail';
262
+ detailLabel.textContent = detail ?? '';
263
+ this.emptyHost.replaceChildren(icon, label, ...(detail ? [detailLabel] : []));
264
+ this.updateActiveTab();
265
+ this.updateWorkspaceState();
266
+ }
267
+
268
+ setSnapshotActionState(state: FrameGraphDebugSnapshotActionState): void {
269
+ const captureLabel = state.capturing ? 'Capturing…' : 'Capture';
270
+ setPanelButtonContent(this.captureButton, state.capturing ? 'spinner' : 'capture', captureLabel);
271
+ this.captureButton.disabled = state.capturing || !state.providerAvailable;
272
+ this.captureButton.hidden = !state.providerAvailable;
273
+ this.captureButton.classList.toggle('active', state.capturing);
274
+ this.captureButton.setAttribute('aria-busy', state.capturing ? 'true' : 'false');
275
+ this.captureButton.dataset.tone = state.capturing ? 'pending' : 'accent';
276
+ this.captureButton.title = !state.providerAvailable
277
+ ? 'Waiting for a FrameGraph capture source.'
278
+ : state.capturing
279
+ ? 'Capturing the next rendered frame.'
280
+ : 'Capture and display the next rendered frame.';
281
+
282
+ this.importButton.textContent = state.importing ? 'Importing…' : 'Import';
283
+ this.importButton.disabled = state.importing;
284
+ this.importButton.setAttribute('aria-busy', state.importing ? 'true' : 'false');
285
+ this.exportButton.disabled = !state.hasCapture;
286
+ this.downloadButton.disabled = !state.hasCapture;
287
+ if (!state.hasCapture) this.setExportMenuOpen(false);
288
+
289
+ const copyLabel = state.copying ? 'Copying…' : state.copied ? 'Copied' : 'Copy JSON';
290
+ const copyIcon = state.copying ? 'spinner' : state.copied ? 'check' : 'copy';
291
+ setPanelButtonContent(this.copyButton, copyIcon, copyLabel);
292
+ this.copyButton.disabled = !state.hasCapture || state.copying;
293
+ this.copyButton.classList.toggle('active', state.copying || state.copied);
294
+ this.copyButton.setAttribute('aria-busy', state.copying ? 'true' : 'false');
295
+ this.copyButton.dataset.tone = state.copied ? 'success' : state.copying ? 'pending' : 'neutral';
296
+ this.copyButton.title = state.hasCapture
297
+ ? 'Copy the current canonical FrameGraph Snapshot JSON.'
298
+ : 'Load a snapshot before exporting.';
299
+
300
+ this.commandStatus.textContent = state.message ?? '';
301
+ this.commandStatus.hidden = !state.message;
302
+ this.commandStatus.dataset.tone = state.message ? state.messageTone ?? 'error' : 'neutral';
303
+ if (state.message) this.commandStatus.title = state.message;
304
+ else this.commandStatus.removeAttribute('title');
305
+ }
306
+
307
+ setSelection(selected: Selection | undefined): void {
308
+ this.selected = selected;
309
+ this.passes.setSelection(selected);
310
+ this.resources.setSelection(selected);
311
+ this.memory.setSelection(selected);
312
+ this.diagnostics.setSelection(selected);
313
+ this.inspector.setSelection(selected);
314
+ if (this.activeTab === 'graph') this.renderGraph();
315
+ }
316
+
317
+ setHovered(hovered: Selection | undefined): void {
318
+ this.hovered = hovered;
319
+ if (this.activeTab === 'graph') this.renderGraph();
320
+ }
321
+
322
+ setActiveTab(tab: WorkbenchTab): void {
323
+ if (this.activeTab === tab) return;
324
+ this.activeTab = tab;
325
+ this.updateActiveTab();
326
+ if (tab === 'graph') {
327
+ window.requestAnimationFrame(() => {
328
+ resizeGraph(this.graphView);
329
+ this.renderGraph();
330
+ });
331
+ }
332
+ }
333
+
334
+ refreshGraphStructure(): void {
335
+ if (!this.snapshot) return;
336
+ this.passes.setSnapshot(this.snapshot);
337
+ if (this.activeTab === 'graph') this.renderGraph();
338
+ }
339
+
340
+ resizeGraph(fit = false): void {
341
+ if (fit) this.graphView.fitOnNextRender = true;
342
+ window.requestAnimationFrame(() => {
343
+ resizeGraph(this.graphView);
344
+ if (this.activeTab === 'graph') this.renderGraph();
345
+ });
346
+ }
347
+
348
+ private updateActiveTab(): void {
349
+ for (const [tab, button] of this.tabButtons) {
350
+ const active = tab === this.activeTab;
351
+ button.disabled = !this.hasSnapshot;
352
+ button.classList.toggle('active', active);
353
+ button.setAttribute('aria-selected', active ? 'true' : 'false');
354
+ const view = this.views.get(tab)!;
355
+ view.hidden = !this.hasSnapshot || !active;
356
+ view.setAttribute('aria-labelledby', button.id);
357
+ }
358
+ }
359
+
360
+ private renderGraph(): void {
361
+ if (!this.snapshot) return;
362
+ renderGraphView(
363
+ this.graphView,
364
+ this.snapshot,
365
+ this.selected,
366
+ this.hovered,
367
+ this.callbacks.onSelect,
368
+ this.callbacks.onHover,
369
+ this.callbacks.onGroupToggle,
370
+ );
371
+ }
372
+
373
+ private renderSummary(snapshot: FrameGraphDebugViewModel): void {
374
+ const frameGraphSegments = snapshot.executionSegments.filter((segment) => segment.kind === 'frame-graph').length;
375
+ const opaqueIntervals = snapshot.executionSegments.length - frameGraphSegments;
376
+ const metrics = snapshot.metrics;
377
+ const coverage = metrics.timingEligibleNodeCount === 0
378
+ ? 'No eligible passes'
379
+ : `${metrics.timedNodeCount}/${metrics.timingEligibleNodeCount} timed`;
380
+ const slowest = metrics.slowestNode
381
+ ? `${labelNode(metrics.slowestNode)} · ${(metrics.slowestNode.gpuDurationMicros! / 1000).toFixed(3)} ms`
382
+ : 'Unknown';
383
+ const protocol = snapshot.protocol;
384
+ const runtime = protocol.producer.runtime;
385
+ const producer = [
386
+ protocol.producer.name,
387
+ protocol.producer.version,
388
+ protocol.producer.language,
389
+ ].filter(Boolean).join(' · ');
390
+ const runtimeLabel = runtime
391
+ ? [runtime.implementation, runtime.graphicsApi, runtime.backend].filter(Boolean).join(' · ') || 'Unknown'
392
+ : 'Unknown';
393
+ const poolRetained = snapshot.resourcePool.status === 'available'
394
+ ? `${snapshot.resourcePool.estimatedRetainedBytes === undefined ? 'Unknown' : formatBytes(snapshot.resourcePool.estimatedRetainedBytes)} · ${snapshot.resourcePool.retainedCount} allocations`
395
+ : 'Unavailable';
396
+ this.summary.replaceChildren(
397
+ this.createSummaryGroup('Capture', [
398
+ ['Source', snapshot.source.label],
399
+ ['Schema', `${protocol.format} v${protocol.version.major}.${protocol.version.minor}`],
400
+ ['Producer', producer],
401
+ ['Runtime', runtimeLabel],
402
+ ['Frame', String(snapshot.frameIndex)],
403
+ ...(snapshot.source.migratedFromLegacy ? [['Migration', 'Legacy V0 → V1'] as const] : []),
404
+ ['Timing', snapshot.profiling.status === 'available' ? 'Available' : snapshot.profiling.reason],
405
+ ]),
406
+ this.createSummaryGroup('GPU', [
407
+ ['GPU Span', snapshot.profiling.status === 'available' ? `${formatGpuFrameDuration(snapshot)} ms` : 'Unknown'],
408
+ ['Coverage', coverage],
409
+ ['Slowest', slowest],
410
+ ]),
411
+ this.createSummaryGroup('Work', [
412
+ ['Nodes', `${snapshot.nodes.length} retained · ${snapshot.culledNodes.length} culled`],
413
+ ['Segments', `${frameGraphSegments} FG · ${opaqueIntervals} opaque`],
414
+ ['Groups', snapshot.availability.groups ? String(snapshot.debugGroups.length) : 'Unknown'],
415
+ ['Recording order', snapshot.availability.recordingOrder ? 'Available' : 'Unknown'],
416
+ ]),
417
+ this.createSummaryGroup('Resources', [
418
+ ['Logical / physical', `${snapshot.resources.length} / ${snapshot.physicalAllocations.length}`],
419
+ ['Texture views', snapshot.availability.textureViews ? String(snapshot.textureViewById.size) : 'Unknown'],
420
+ ['Access regions', snapshot.availability.accessRegions ? 'Available' : 'Unknown'],
421
+ ['Transient estimate', formatEstimatedBytes(metrics.transientEstimatedByteSize)],
422
+ ]),
423
+ this.createSummaryGroup('Pool', [
424
+ ['Retained', poolRetained],
425
+ ['Reuse', formatPoolHitRate(snapshot)],
426
+ ]),
427
+ );
428
+ }
429
+
430
+ private createSummaryGroup(title: string, rows: readonly (readonly [string, string])[]): HTMLElement {
431
+ const group = document.createElement('section');
432
+ const heading = document.createElement('h2');
433
+ heading.textContent = title;
434
+ group.appendChild(heading);
435
+ for (const [label, value] of rows) {
436
+ const row = document.createElement('div');
437
+ const term = document.createElement('span');
438
+ term.textContent = label;
439
+ const description = document.createElement('strong');
440
+ description.textContent = value;
441
+ description.title = value;
442
+ row.append(term, description);
443
+ group.appendChild(row);
444
+ }
445
+ return group;
446
+ }
447
+
448
+ private setExportMenuOpen(open: boolean): void {
449
+ this.exportMenu.hidden = !open;
450
+ this.exportButton.setAttribute('aria-expanded', open ? 'true' : 'false');
451
+ }
452
+
453
+ private handleInspectorOpenChange(_open: boolean): void {
454
+ this.updateWorkspaceState();
455
+ if (this.activeTab === 'graph') this.resizeGraph(false);
456
+ }
457
+
458
+ private updateWorkspaceState(): void {
459
+ const inspectorOpen = this.hasSnapshot && this.inspector.isOpen;
460
+ this.workspace.classList.toggle('has-capture', this.hasSnapshot);
461
+ this.workspace.classList.toggle('inspector-open', inspectorOpen);
462
+ this.inspector.root.classList.toggle('unavailable', !this.hasSnapshot);
463
+ this.inspectorOpenButton.hidden = !this.hasSnapshot || inspectorOpen;
464
+ }
465
+ }