@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,222 @@
1
+ import type { FrameGraphDebugNode, FrameGraphDebugViewModel } from './debugCaptureModel.ts';
2
+ import { createCell, formatGpuDuration, labelNode } from './panelDomHelpers.ts';
3
+ import type { PassesSubview, Selection } from './panelTypes.ts';
4
+ import {
5
+ createEmptyTableRow,
6
+ createKindCell,
7
+ createFilterSelect,
8
+ createSearchInput,
9
+ createSelectionCell,
10
+ createTableScroller,
11
+ createViewToolbar,
12
+ groupPath,
13
+ registerSelectable,
14
+ type WorkbenchCallbacks,
15
+ updateSelectedRows,
16
+ } from './panelWorkbenchHelpers.ts';
17
+
18
+ type PassSort = 'order' | 'label' | 'kind' | 'gpu';
19
+
20
+ export class PassesView {
21
+ readonly root = document.createElement('section');
22
+ private readonly toolbar = createViewToolbar('Pass filters and sorting');
23
+ private readonly tabList = document.createElement('div');
24
+ private readonly listButton = this.createSubviewButton('list', 'Pass List');
25
+ private readonly groupsButton = this.createSubviewButton('groups', 'Group Hierarchy');
26
+ private readonly listPane = document.createElement('div');
27
+ private readonly groupsPane = document.createElement('div');
28
+ private readonly listTable = createTableScroller([
29
+ { label: 'Order', column: 'numeric' },
30
+ { label: 'Segment', column: 'code' },
31
+ { label: 'Label' },
32
+ { label: 'Group' },
33
+ { label: 'Kind', column: 'kind' },
34
+ { label: 'GPU (ms)', column: 'numeric' },
35
+ { label: 'Reads', column: 'numeric' },
36
+ { label: 'Writes', column: 'numeric' },
37
+ ]);
38
+ private readonly groupsTable = createTableScroller([
39
+ { label: '' },
40
+ { label: 'Group' },
41
+ { label: 'Retained', column: 'numeric' },
42
+ { label: 'Culled', column: 'numeric' },
43
+ { label: 'GPU work', column: 'numeric' },
44
+ { label: 'Inputs', column: 'numeric' },
45
+ { label: 'Outputs', column: 'numeric' },
46
+ ]);
47
+ private readonly rows = new Map<string, HTMLElement[]>();
48
+ private snapshot: FrameGraphDebugViewModel | undefined;
49
+ private selected: Selection | undefined;
50
+ private subview: PassesSubview = 'list';
51
+ private search = '';
52
+ private kind = 'all';
53
+ private sort: PassSort = 'order';
54
+
55
+ constructor(private readonly callbacks: WorkbenchCallbacks, idPrefix: string) {
56
+ this.root.className = 'zenfg-inspector-view zenfg-inspector-passes-view';
57
+ this.root.id = `${idPrefix}-view-passes`;
58
+ this.root.setAttribute('role', 'tabpanel');
59
+ this.tabList.className = 'zenfg-inspector-subtabs';
60
+ this.tabList.setAttribute('role', 'tablist');
61
+ this.tabList.setAttribute('aria-label', 'Pass views');
62
+ this.tabList.append(this.listButton, this.groupsButton);
63
+
64
+ const search = createSearchInput('Search pass or group', this.search, (value) => {
65
+ this.search = value.trim().toLocaleLowerCase();
66
+ this.renderRows();
67
+ });
68
+ const kind = createFilterSelect('Pass kind', this.kind, [
69
+ ['all', 'All kinds'],
70
+ ['render', 'Render'],
71
+ ['compute', 'Compute'],
72
+ ['copy', 'Copy'],
73
+ ['clear-buffer', 'Clear Buffer'],
74
+ ['command', 'Command'],
75
+ ['external-submission', 'External'],
76
+ ], (value) => {
77
+ this.kind = value;
78
+ this.renderRows();
79
+ });
80
+ const sort = createFilterSelect('Sort passes', this.sort, [
81
+ ['order', 'Order'],
82
+ ['label', 'Label'],
83
+ ['kind', 'Kind'],
84
+ ['gpu', 'GPU'],
85
+ ], (value) => {
86
+ this.sort = value as PassSort;
87
+ this.renderRows();
88
+ });
89
+ this.toolbar.append(search, kind, sort);
90
+
91
+ this.listButton.id = `${idPrefix}-list-subtab`;
92
+ this.listButton.setAttribute('aria-controls', `${idPrefix}-pass-list-panel`);
93
+ this.groupsButton.id = `${idPrefix}-groups-subtab`;
94
+ this.groupsButton.setAttribute('aria-controls', `${idPrefix}-group-list-panel`);
95
+ this.listPane.id = `${idPrefix}-pass-list-panel`;
96
+ this.listPane.className = 'zenfg-inspector-subview';
97
+ this.listPane.setAttribute('role', 'tabpanel');
98
+ this.listPane.setAttribute('aria-labelledby', this.listButton.id);
99
+ this.listPane.appendChild(this.listTable.scroller);
100
+ this.groupsPane.id = `${idPrefix}-group-list-panel`;
101
+ this.groupsPane.className = 'zenfg-inspector-subview';
102
+ this.groupsPane.setAttribute('role', 'tabpanel');
103
+ this.groupsPane.setAttribute('aria-labelledby', this.groupsButton.id);
104
+ this.groupsPane.appendChild(this.groupsTable.scroller);
105
+ this.root.append(this.tabList, this.toolbar, this.listPane, this.groupsPane);
106
+ this.updateSubview();
107
+ }
108
+
109
+ setSnapshot(snapshot: FrameGraphDebugViewModel): void {
110
+ this.snapshot = snapshot;
111
+ if (snapshot.debugGroups.length === 0 && this.subview === 'groups') this.subview = 'list';
112
+ this.groupsButton.disabled = snapshot.debugGroups.length === 0;
113
+ this.renderRows();
114
+ this.updateSubview();
115
+ }
116
+
117
+ setSelection(selected: Selection | undefined): void {
118
+ this.selected = selected;
119
+ updateSelectedRows(this.rows, selected);
120
+ }
121
+
122
+ private createSubviewButton(subview: PassesSubview, label: string): HTMLButtonElement {
123
+ const button = document.createElement('button');
124
+ button.type = 'button';
125
+ button.textContent = label;
126
+ button.setAttribute('role', 'tab');
127
+ button.addEventListener('click', () => {
128
+ this.subview = subview;
129
+ this.updateSubview();
130
+ });
131
+ return button;
132
+ }
133
+
134
+ private updateSubview(): void {
135
+ const listActive = this.subview === 'list';
136
+ this.listButton.setAttribute('aria-selected', listActive ? 'true' : 'false');
137
+ this.groupsButton.setAttribute('aria-selected', listActive ? 'false' : 'true');
138
+ this.listButton.classList.toggle('active', listActive);
139
+ this.groupsButton.classList.toggle('active', !listActive);
140
+ this.listPane.hidden = !listActive;
141
+ this.groupsPane.hidden = listActive;
142
+ }
143
+
144
+ private renderRows(): void {
145
+ const snapshot = this.snapshot;
146
+ if (!snapshot) return;
147
+ this.rows.clear();
148
+ this.listTable.body.replaceChildren();
149
+ this.groupsTable.body.replaceChildren();
150
+
151
+ const passes = snapshot.nodes.filter((node) => this.matchesNode(snapshot, node));
152
+ passes.sort((a, b) => this.compareNodes(a, b));
153
+ for (const node of passes) {
154
+ const selection: Selection = { kind: 'node', id: node.id };
155
+ const row = document.createElement('tr');
156
+ registerSelectable(this.rows, row, selection, this.callbacks);
157
+ const segment = snapshot.segmentByNodeId.get(node.id);
158
+ row.append(
159
+ createCell(String(node.order), { column: 'numeric' }),
160
+ createCell(segment ? `${segment.index}:${segment.kind === 'frame-graph' ? 'FG' : 'opaque'}` : '-', { column: 'code' }),
161
+ createSelectionCell(labelNode(node), selection, this.callbacks),
162
+ createCell(groupPath(snapshot, node.debugGroupId), { column: 'code' }),
163
+ createKindCell(node.kind),
164
+ createCell(node.kind === 'external-submission' ? 'opaque' : formatGpuDuration(node), { column: 'numeric' }),
165
+ createCell(String(node.reads.length), { column: 'numeric' }),
166
+ createCell(String(node.writes.length), { column: 'numeric' }),
167
+ );
168
+ this.listTable.body.appendChild(row);
169
+ }
170
+ if (passes.length === 0) this.listTable.body.appendChild(createEmptyTableRow(8, 'No passes match the current filters.'));
171
+
172
+ const query = this.search;
173
+ for (const group of snapshot.debugGroups.filter((candidate) => (
174
+ query.length === 0 || candidate.path.join(' / ').toLocaleLowerCase().includes(query)
175
+ ))) {
176
+ const selection: Selection = { kind: 'group', pathKey: group.pathKey };
177
+ const row = document.createElement('tr');
178
+ registerSelectable(this.rows, row, selection, this.callbacks);
179
+ const toggleCell = document.createElement('td');
180
+ const toggle = document.createElement('button');
181
+ toggle.type = 'button';
182
+ toggle.className = 'zenfg-inspector-group-toggle';
183
+ const expanded = this.callbacks.isGroupExpanded(group.pathKey);
184
+ toggle.textContent = expanded ? '▾' : '▸';
185
+ toggle.title = 'Toggle group in Graph view';
186
+ toggle.setAttribute('aria-label', `${expanded ? 'Collapse' : 'Expand'} group ${group.path.join(' / ')}`);
187
+ toggle.addEventListener('click', () => this.callbacks.onGroupToggle(group.pathKey));
188
+ toggleCell.appendChild(toggle);
189
+ const labelCell = createSelectionCell(group.label, selection, this.callbacks);
190
+ labelCell.style.paddingLeft = `${8 + group.depth * 18}px`;
191
+ const summary = group.summary;
192
+ row.append(
193
+ toggleCell,
194
+ labelCell,
195
+ createCell(String(summary.retainedNodeCount), { column: 'numeric' }),
196
+ createCell(String(summary.culledNodeCount), { column: 'numeric' }),
197
+ createCell(summary.timedNodeCount === 0 ? '-' : (summary.gpuWorkDurationMicros / 1000).toFixed(3), { column: 'numeric' }),
198
+ createCell(String(summary.inputResources.length), { column: 'numeric' }),
199
+ createCell(String(summary.outputResources.length), { column: 'numeric' }),
200
+ );
201
+ this.groupsTable.body.appendChild(row);
202
+ }
203
+ if (this.groupsTable.body.childElementCount === 0) this.groupsTable.body.appendChild(createEmptyTableRow(7, 'No diagnostic groups.'));
204
+ updateSelectedRows(this.rows, this.selected);
205
+ }
206
+
207
+ private matchesNode(snapshot: FrameGraphDebugViewModel, node: FrameGraphDebugNode): boolean {
208
+ if (this.kind !== 'all' && node.kind !== this.kind) return false;
209
+ if (this.search.length === 0) return true;
210
+ const haystack = `${labelNode(node)} ${groupPath(snapshot, node.debugGroupId)}`.toLocaleLowerCase();
211
+ return haystack.includes(this.search);
212
+ }
213
+
214
+ private compareNodes(a: FrameGraphDebugNode, b: FrameGraphDebugNode): number {
215
+ switch (this.sort) {
216
+ case 'label': return labelNode(a).localeCompare(labelNode(b)) || a.order - b.order;
217
+ case 'kind': return a.kind.localeCompare(b.kind) || a.order - b.order;
218
+ case 'gpu': return (b.gpuDurationMicros ?? -1) - (a.gpuDurationMicros ?? -1) || a.order - b.order;
219
+ case 'order': return a.order - b.order;
220
+ }
221
+ }
222
+ }
@@ -0,0 +1,115 @@
1
+ import type { FrameGraphDebugViewModel } from './debugCaptureModel.ts';
2
+ import { createCell, labelResource } from './panelDomHelpers.ts';
3
+ import type { Selection } from './panelTypes.ts';
4
+ import {
5
+ createEmptyTableRow,
6
+ createKindCell,
7
+ createFilterSelect,
8
+ createSearchInput,
9
+ createSelectionCell,
10
+ createTableScroller,
11
+ createViewToolbar,
12
+ formatEstimatedBytes,
13
+ formatResourceDescriptor,
14
+ groupPath,
15
+ registerSelectable,
16
+ resourceAccessCounts,
17
+ type WorkbenchCallbacks,
18
+ updateSelectedRows,
19
+ } from './panelWorkbenchHelpers.ts';
20
+
21
+ export class ResourcesView {
22
+ readonly root = document.createElement('section');
23
+ private readonly table = createTableScroller([
24
+ { label: 'Resource / Group' },
25
+ { label: 'Type / Origin', column: 'kind' },
26
+ { label: 'Descriptor', column: 'code' },
27
+ { label: 'Estimated', column: 'numeric' },
28
+ { label: 'Lifetime', column: 'code' },
29
+ { label: 'Physical', column: 'numeric' },
30
+ { label: 'R / W', column: 'numeric' },
31
+ ]);
32
+ private readonly rows = new Map<string, HTMLElement[]>();
33
+ private snapshot: FrameGraphDebugViewModel | undefined;
34
+ private selected: Selection | undefined;
35
+ private search = '';
36
+ private kind = 'all';
37
+ private origin = 'all';
38
+
39
+ constructor(private readonly callbacks: WorkbenchCallbacks, idPrefix: string) {
40
+ this.root.className = 'zenfg-inspector-view zenfg-inspector-resources-view';
41
+ this.root.id = `${idPrefix}-view-resources`;
42
+ this.root.setAttribute('role', 'tabpanel');
43
+ const toolbar = createViewToolbar('Resource filters');
44
+ toolbar.append(
45
+ createSearchInput('Search resource or group', this.search, (value) => {
46
+ this.search = value.trim().toLocaleLowerCase();
47
+ this.renderRows();
48
+ }),
49
+ createFilterSelect('Resource kind', this.kind, [
50
+ ['all', 'All kinds'],
51
+ ['texture', 'Texture'],
52
+ ['buffer', 'Buffer'],
53
+ ], (value) => {
54
+ this.kind = value;
55
+ this.renderRows();
56
+ }),
57
+ createFilterSelect('Resource origin', this.origin, [
58
+ ['all', 'All origins'],
59
+ ['transient', 'Transient'],
60
+ ['imported', 'Imported'],
61
+ ['swapchain', 'Swapchain'],
62
+ ], (value) => {
63
+ this.origin = value;
64
+ this.renderRows();
65
+ }),
66
+ );
67
+ this.root.append(toolbar, this.table.scroller);
68
+ }
69
+
70
+ setSnapshot(snapshot: FrameGraphDebugViewModel): void {
71
+ this.snapshot = snapshot;
72
+ this.renderRows();
73
+ }
74
+
75
+ setSelection(selected: Selection | undefined): void {
76
+ this.selected = selected;
77
+ updateSelectedRows(this.rows, selected);
78
+ }
79
+
80
+ private renderRows(): void {
81
+ const snapshot = this.snapshot;
82
+ if (!snapshot) return;
83
+ this.rows.clear();
84
+ this.table.body.replaceChildren();
85
+ const resources = snapshot.resources.filter((resource) => {
86
+ if (this.kind !== 'all' && resource.kind !== this.kind) return false;
87
+ if (this.origin !== 'all' && resource.origin !== this.origin) return false;
88
+ if (this.search.length === 0) return true;
89
+ return `${labelResource(resource)} ${groupPath(snapshot, resource.debugGroupId)}`
90
+ .toLocaleLowerCase().includes(this.search);
91
+ });
92
+ for (const resource of resources) {
93
+ const selection: Selection = { kind: 'resource', id: resource.id };
94
+ const row = document.createElement('tr');
95
+ registerSelectable(this.rows, row, selection, this.callbacks);
96
+ const [reads, writes] = resourceAccessCounts(snapshot, resource.id);
97
+ const resourceCell = createSelectionCell(labelResource(resource), selection, this.callbacks);
98
+ const group = document.createElement('small');
99
+ group.textContent = groupPath(snapshot, resource.debugGroupId);
100
+ resourceCell.appendChild(group);
101
+ row.append(
102
+ resourceCell,
103
+ createKindCell(resource.kind, `${resource.kind} · ${resource.origin}`),
104
+ createCell(formatResourceDescriptor(resource), { column: 'code' }),
105
+ createCell(formatEstimatedBytes(resource.estimatedByteSize), { column: 'numeric' }),
106
+ createCell(resource.lifetime ? `${resource.lifetime.firstUse}–${resource.lifetime.lastUse}` : '-', { column: 'code' }),
107
+ createCell(resource.physicalResourceId === undefined ? '-' : `#${resource.physicalResourceId}`, { column: 'numeric' }),
108
+ createCell(`${reads} / ${writes}`, { column: 'numeric' }),
109
+ );
110
+ this.table.body.appendChild(row);
111
+ }
112
+ if (resources.length === 0) this.table.body.appendChild(createEmptyTableRow(7, 'No resources match the current filters.'));
113
+ updateSelectedRows(this.rows, this.selected);
114
+ }
115
+ }
@@ -0,0 +1,99 @@
1
+ import type { FrameGraphDebugViewModel } from './debugCaptureModel.ts';
2
+ import type { Selection } from './panelTypes.ts';
3
+
4
+ export function resolveSelectedDetail(
5
+ snapshot: FrameGraphDebugViewModel,
6
+ selected: Selection | undefined,
7
+ ): unknown {
8
+ if (!selected) return snapshot;
9
+
10
+ switch (selected.kind) {
11
+ case 'node': {
12
+ const node = snapshot.nodeById.get(selected.id);
13
+ const executionSegment = snapshot.segmentByNodeId.get(selected.id);
14
+ return node
15
+ ? {
16
+ ...node,
17
+ debugGroupPath: debugGroupPath(snapshot, node.debugGroupId),
18
+ executionSegment,
19
+ reads: node.reads.map((access) => resolveAccessDetail(snapshot, access)),
20
+ writes: node.writes.map((access) => resolveAccessDetail(snapshot, access)),
21
+ }
22
+ : undefined;
23
+ }
24
+ case 'resource':
25
+ return resolveResourceDetail(snapshot, selected.id);
26
+ case 'group':
27
+ return snapshot.groupByPathKey.get(selected.pathKey);
28
+ case 'root':
29
+ return snapshot.roots[selected.index];
30
+ case 'culled': {
31
+ const culled = snapshot.culledNodes[selected.index];
32
+ return culled ? {
33
+ ...culled,
34
+ node: {
35
+ ...culled.node,
36
+ reads: culled.node.reads.map((access) => resolveAccessDetail(snapshot, access)),
37
+ writes: culled.node.writes.map((access) => resolveAccessDetail(snapshot, access)),
38
+ },
39
+ } : undefined;
40
+ }
41
+ case 'allocation':
42
+ return snapshot.allocationById.get(selected.id);
43
+ case 'segment':
44
+ return snapshot.segmentByIndex.get(selected.index);
45
+ }
46
+ }
47
+
48
+ export function selectionExists(snapshot: FrameGraphDebugViewModel, selected: Selection): boolean {
49
+ switch (selected.kind) {
50
+ case 'node':
51
+ return snapshot.nodeById.has(selected.id);
52
+ case 'group':
53
+ return snapshot.groupByPathKey.has(selected.pathKey);
54
+ case 'resource':
55
+ return snapshot.resourceById.has(selected.id);
56
+ case 'root':
57
+ return isValidIndex(selected.index, snapshot.roots.length);
58
+ case 'culled':
59
+ return isValidIndex(selected.index, snapshot.culledNodes.length);
60
+ case 'allocation':
61
+ return snapshot.allocationById.has(selected.id);
62
+ case 'segment':
63
+ return snapshot.segmentByIndex.has(selected.index);
64
+ }
65
+ }
66
+
67
+ function resolveResourceDetail(snapshot: FrameGraphDebugViewModel, resourceId: string): unknown {
68
+ const resource = snapshot.resourceById.get(resourceId);
69
+ if (!resource) return undefined;
70
+ const accesses = (snapshot.accessesByResourceId.get(resourceId) ?? [])
71
+ .map((access) => resolveAccessDetail(snapshot, access));
72
+ return {
73
+ ...resource,
74
+ debugGroupPath: debugGroupPath(snapshot, resource.debugGroupId),
75
+ writes: accesses.filter((access) => access.mode === 'write'),
76
+ reads: accesses.filter((access) => access.mode === 'read'),
77
+ };
78
+ }
79
+
80
+ function resolveAccessDetail<TAccess extends { readonly textureViewId?: string }>(
81
+ snapshot: FrameGraphDebugViewModel,
82
+ access: TAccess,
83
+ ): TAccess & { readonly textureView?: FrameGraphDebugViewModel['protocol']['graph']['textureViews'][number] } {
84
+ return access.textureViewId === undefined
85
+ ? access
86
+ : {
87
+ ...access,
88
+ textureView: snapshot.textureViewById.get(access.textureViewId),
89
+ };
90
+ }
91
+
92
+ function debugGroupPath(snapshot: FrameGraphDebugViewModel, groupId: string | undefined): string {
93
+ if (groupId === undefined) return '-';
94
+ return snapshot.groupById.get(groupId)?.path.join(' / ') ?? `#${groupId}`;
95
+ }
96
+
97
+ function isValidIndex(index: number, length: number): boolean {
98
+ return Number.isInteger(index) && index >= 0 && index < length;
99
+ }
@@ -0,0 +1,28 @@
1
+ import type { GraphRenderer } from './panelGraphRenderer.ts';
2
+
3
+ export type Selection =
4
+ | { kind: 'node'; id: string }
5
+ | { kind: 'group'; pathKey: string }
6
+ | { kind: 'resource'; id: string }
7
+ | { kind: 'root'; index: number }
8
+ | { kind: 'culled'; index: number }
9
+ | { kind: 'allocation'; id: string }
10
+ | { kind: 'segment'; index: number };
11
+
12
+ export type WorkbenchTab = 'overview' | 'graph' | 'passes' | 'resources' | 'memory' | 'diagnostics';
13
+ export type PassesSubview = 'list' | 'groups';
14
+ export type InspectorTab = 'summary' | 'relations' | 'raw';
15
+ export type GraphViewMode = 'passes' | 'resources';
16
+
17
+ export type GraphViewState = {
18
+ readonly host: HTMLElement;
19
+ readonly toolbar: HTMLElement;
20
+ readonly legend?: HTMLElement;
21
+ readonly layoutElementBudget?: number;
22
+ graphMode: GraphViewMode;
23
+ groupsEnabled: boolean;
24
+ readonly expandedGroupPaths: Set<string>;
25
+ renderer?: GraphRenderer;
26
+ fitOnNextRender: boolean;
27
+ anchorElementIdOnNextRender?: string;
28
+ };
@@ -0,0 +1,39 @@
1
+ export const FRAME_GRAPH_DEBUG_VISUAL_THEME = {
2
+ canvas: '#0b0f14',
3
+ panel: '#0f151d',
4
+ surface: '#131b24',
5
+ surfaceRaised: '#18222d',
6
+ surfaceHover: '#1d2935',
7
+ border: '#263341',
8
+ text: '#e6edf3',
9
+ textSecondary: '#b4beca',
10
+ muted: '#8b98a5',
11
+ accent: '#38bdf8',
12
+ success: '#34d399',
13
+ warning: '#fbbf24',
14
+ danger: '#fb7185',
15
+ fontUi: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
16
+ fontMono: 'ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace',
17
+ } as const;
18
+
19
+ export const GRAPH_VISUAL_THEME = {
20
+ canvas: FRAME_GRAPH_DEBUG_VISUAL_THEME.canvas,
21
+ surface: FRAME_GRAPH_DEBUG_VISUAL_THEME.surface,
22
+ surfaceRaised: FRAME_GRAPH_DEBUG_VISUAL_THEME.surfaceRaised,
23
+ text: FRAME_GRAPH_DEBUG_VISUAL_THEME.text,
24
+ muted: FRAME_GRAPH_DEBUG_VISUAL_THEME.muted,
25
+ render: { stroke: '#34d399', fill: '#12342b' },
26
+ compute: { stroke: '#60a5fa', fill: '#162c46' },
27
+ copy: { stroke: '#fbbf24', fill: '#3a2d12' },
28
+ clear: { stroke: '#94a3b8', fill: '#25303a' },
29
+ command: { stroke: '#c084fc', fill: '#322044' },
30
+ external: { stroke: '#fb923c', fill: '#402318' },
31
+ texture: { stroke: '#f472b6', fill: '#3c1f32' },
32
+ buffer: { stroke: '#2dd4bf', fill: '#123632' },
33
+ group: { stroke: '#64748b', fill: '#141c29', alternateFill: '#172033' },
34
+ dependency: { value: '#94a3b8', ordering: '#64748b' },
35
+ access: { read: '#2dd4bf', write: '#f59e0b' },
36
+ selected: FRAME_GRAPH_DEBUG_VISUAL_THEME.accent,
37
+ hover: '#cbd5e1',
38
+ culled: { stroke: '#64748b', fill: '#171d24' },
39
+ } as const;