@zenfg/inspector 0.1.0-beta.2 → 0.1.0-beta.4

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 (172) hide show
  1. package/GUIDE.md +182 -0
  2. package/README.md +60 -26
  3. package/THEMING.md +124 -0
  4. package/dist/FrameGraphInspector.d.ts +24 -8
  5. package/dist/FrameGraphInspector.d.ts.map +1 -1
  6. package/dist/FrameGraphInspector.js +151 -65
  7. package/dist/FrameGraphInspector.js.map +1 -1
  8. package/dist/capture.d.ts +5 -0
  9. package/dist/capture.d.ts.map +1 -0
  10. package/dist/capture.js +2 -0
  11. package/dist/capture.js.map +1 -0
  12. package/dist/debugCaptureModel.d.ts +28 -4
  13. package/dist/debugCaptureModel.d.ts.map +1 -1
  14. package/dist/debugCaptureModel.js +72 -16
  15. package/dist/debugCaptureModel.js.map +1 -1
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/panelAliasAnalysis.d.ts +1 -0
  20. package/dist/panelAliasAnalysis.d.ts.map +1 -1
  21. package/dist/panelAliasAnalysis.js +23 -54
  22. package/dist/panelAliasAnalysis.js.map +1 -1
  23. package/dist/panelCytoscapeGraphRenderer.d.ts +18 -3
  24. package/dist/panelCytoscapeGraphRenderer.d.ts.map +1 -1
  25. package/dist/panelCytoscapeGraphRenderer.js +183 -41
  26. package/dist/panelCytoscapeGraphRenderer.js.map +1 -1
  27. package/dist/panelDetailLayout.d.ts +27 -0
  28. package/dist/panelDetailLayout.d.ts.map +1 -0
  29. package/dist/panelDetailLayout.js +174 -0
  30. package/dist/panelDetailLayout.js.map +1 -0
  31. package/dist/panelDetailStyles.d.ts +2 -0
  32. package/dist/panelDetailStyles.d.ts.map +1 -0
  33. package/dist/panelDetailStyles.js +57 -0
  34. package/dist/panelDetailStyles.js.map +1 -0
  35. package/dist/panelDiagnosticsView.d.ts +10 -1
  36. package/dist/panelDiagnosticsView.d.ts.map +1 -1
  37. package/dist/panelDiagnosticsView.js +167 -58
  38. package/dist/panelDiagnosticsView.js.map +1 -1
  39. package/dist/panelDomHelpers.d.ts.map +1 -1
  40. package/dist/panelDomHelpers.js +6 -5
  41. package/dist/panelDomHelpers.js.map +1 -1
  42. package/dist/panelGraphLayout.d.ts +3 -2
  43. package/dist/panelGraphLayout.d.ts.map +1 -1
  44. package/dist/panelGraphLayout.js +20 -5
  45. package/dist/panelGraphLayout.js.map +1 -1
  46. package/dist/panelGraphRenderer.d.ts +10 -0
  47. package/dist/panelGraphRenderer.d.ts.map +1 -1
  48. package/dist/panelGraphRenderer.js.map +1 -1
  49. package/dist/panelGraphScene.d.ts +15 -26
  50. package/dist/panelGraphScene.d.ts.map +1 -1
  51. package/dist/panelGraphScene.js +191 -208
  52. package/dist/panelGraphScene.js.map +1 -1
  53. package/dist/panelGraphSearch.d.ts +16 -0
  54. package/dist/panelGraphSearch.d.ts.map +1 -0
  55. package/dist/panelGraphSearch.js +93 -0
  56. package/dist/panelGraphSearch.js.map +1 -0
  57. package/dist/panelGraphView.d.ts +82 -0
  58. package/dist/panelGraphView.d.ts.map +1 -1
  59. package/dist/panelGraphView.js +42 -9
  60. package/dist/panelGraphView.js.map +1 -1
  61. package/dist/panelGraphVisuals.d.ts +18 -6
  62. package/dist/panelGraphVisuals.d.ts.map +1 -1
  63. package/dist/panelGraphVisuals.js +156 -116
  64. package/dist/panelGraphVisuals.js.map +1 -1
  65. package/dist/panelInspectorView.d.ts +13 -0
  66. package/dist/panelInspectorView.d.ts.map +1 -1
  67. package/dist/panelInspectorView.js +326 -46
  68. package/dist/panelInspectorView.js.map +1 -1
  69. package/dist/panelListStyles.d.ts +3 -0
  70. package/dist/panelListStyles.d.ts.map +1 -0
  71. package/dist/panelListStyles.js +155 -0
  72. package/dist/panelListStyles.js.map +1 -0
  73. package/dist/panelMemoryStyles.d.ts +3 -0
  74. package/dist/panelMemoryStyles.d.ts.map +1 -0
  75. package/dist/panelMemoryStyles.js +42 -0
  76. package/dist/panelMemoryStyles.js.map +1 -0
  77. package/dist/panelMemoryView.d.ts +13 -1
  78. package/dist/panelMemoryView.d.ts.map +1 -1
  79. package/dist/panelMemoryView.js +161 -41
  80. package/dist/panelMemoryView.js.map +1 -1
  81. package/dist/panelPassesView.d.ts +15 -0
  82. package/dist/panelPassesView.d.ts.map +1 -1
  83. package/dist/panelPassesView.js +202 -72
  84. package/dist/panelPassesView.js.map +1 -1
  85. package/dist/panelRawView.d.ts +15 -0
  86. package/dist/panelRawView.d.ts.map +1 -0
  87. package/dist/panelRawView.js +105 -0
  88. package/dist/panelRawView.js.map +1 -0
  89. package/dist/panelResourcesView.d.ts +8 -0
  90. package/dist/panelResourcesView.d.ts.map +1 -1
  91. package/dist/panelResourcesView.js +62 -31
  92. package/dist/panelResourcesView.js.map +1 -1
  93. package/dist/panelSelection.d.ts +9 -1
  94. package/dist/panelSelection.d.ts.map +1 -1
  95. package/dist/panelSelection.js +36 -6
  96. package/dist/panelSelection.js.map +1 -1
  97. package/dist/panelTheme.d.ts +13 -0
  98. package/dist/panelTheme.d.ts.map +1 -0
  99. package/dist/panelTheme.js +104 -0
  100. package/dist/panelTheme.js.map +1 -0
  101. package/dist/panelTypes.d.ts +18 -4
  102. package/dist/panelTypes.d.ts.map +1 -1
  103. package/dist/panelTypes.js.map +1 -1
  104. package/dist/panelVisualTheme.d.ts +176 -71
  105. package/dist/panelVisualTheme.d.ts.map +1 -1
  106. package/dist/panelVisualTheme.js +34 -36
  107. package/dist/panelVisualTheme.js.map +1 -1
  108. package/dist/panelWorkbenchHelpers.d.ts +12 -1
  109. package/dist/panelWorkbenchHelpers.d.ts.map +1 -1
  110. package/dist/panelWorkbenchHelpers.js +63 -2
  111. package/dist/panelWorkbenchHelpers.js.map +1 -1
  112. package/dist/panelWorkbenchStyles.d.ts +2 -0
  113. package/dist/panelWorkbenchStyles.d.ts.map +1 -0
  114. package/dist/panelWorkbenchStyles.js +101 -0
  115. package/dist/panelWorkbenchStyles.js.map +1 -0
  116. package/dist/panelWorkbenchView.d.ts +16 -0
  117. package/dist/panelWorkbenchView.d.ts.map +1 -1
  118. package/dist/panelWorkbenchView.js +228 -50
  119. package/dist/panelWorkbenchView.js.map +1 -1
  120. package/dist/query.js.map +1 -1
  121. package/dist/styles.d.ts.map +1 -1
  122. package/dist/styles.js +161 -205
  123. package/dist/styles.js.map +1 -1
  124. package/dist/theme.d.ts +22 -0
  125. package/dist/theme.d.ts.map +1 -0
  126. package/dist/theme.js +10 -0
  127. package/dist/theme.js.map +1 -0
  128. package/dist/themeDefinitions.d.ts +251 -0
  129. package/dist/themeDefinitions.d.ts.map +1 -0
  130. package/dist/themeDefinitions.js +96 -0
  131. package/dist/themeDefinitions.js.map +1 -0
  132. package/dist/themePalette.d.ts +66 -0
  133. package/dist/themePalette.d.ts.map +1 -0
  134. package/dist/themePalette.js +37 -0
  135. package/dist/themePalette.js.map +1 -0
  136. package/dist/themes.css +165 -0
  137. package/package.json +44 -10
  138. package/src/FrameGraphInspector.ts +145 -68
  139. package/src/capture.ts +4 -0
  140. package/src/debugCaptureModel.ts +97 -21
  141. package/src/index.ts +6 -2
  142. package/src/panelAliasAnalysis.ts +24 -70
  143. package/src/panelCytoscapeGraphRenderer.ts +177 -40
  144. package/src/panelDetailLayout.ts +155 -0
  145. package/src/panelDetailStyles.ts +56 -0
  146. package/src/panelDiagnosticsView.ts +167 -86
  147. package/src/panelDomHelpers.ts +4 -5
  148. package/src/panelGraphLayout.ts +20 -5
  149. package/src/panelGraphRenderer.ts +7 -0
  150. package/src/panelGraphScene.ts +204 -292
  151. package/src/panelGraphSearch.ts +96 -0
  152. package/src/panelGraphView.ts +41 -9
  153. package/src/panelGraphVisuals.ts +152 -118
  154. package/src/panelInspectorView.ts +315 -48
  155. package/src/panelListStyles.ts +154 -0
  156. package/src/panelMemoryStyles.ts +41 -0
  157. package/src/panelMemoryView.ts +178 -53
  158. package/src/panelPassesView.ts +190 -137
  159. package/src/panelRawView.ts +99 -0
  160. package/src/panelResourcesView.ts +69 -56
  161. package/src/panelSelection.ts +39 -7
  162. package/src/panelTheme.ts +85 -0
  163. package/src/panelTypes.ts +16 -4
  164. package/src/panelVisualTheme.ts +38 -38
  165. package/src/panelWorkbenchHelpers.ts +62 -4
  166. package/src/panelWorkbenchStyles.ts +100 -0
  167. package/src/panelWorkbenchView.ts +201 -52
  168. package/src/query.ts +11 -11
  169. package/src/styles.ts +162 -205
  170. package/src/theme.ts +24 -0
  171. package/src/themeDefinitions.ts +103 -0
  172. package/src/themePalette.ts +44 -0
@@ -1,21 +1,52 @@
1
1
  import type {
2
2
  FrameGraphDebugAccess,
3
+ FrameGraphDebugRoot,
3
4
  FrameGraphDebugResourceRef,
4
5
  FrameGraphDebugViewModel,
5
6
  } from './debugCaptureModel.ts';
6
7
  import { labelNode, labelResource } from './panelDomHelpers.ts';
7
8
  import { createPanelIcon } from './panelIcons.ts';
8
- import { resolveSelectedDetail } from './panelSelection.ts';
9
- import type { InspectorTab, Selection } from './panelTypes.ts';
9
+ import { RawDetailView } from './panelRawView.ts';
10
+ import { resolveNodeSelection, resolveSelectedCanonicalDetail } from './panelSelection.ts';
11
+ import type { InspectorTab, Selection, WorkbenchTab } from './panelTypes.ts';
10
12
  import {
11
13
  createRelationButton,
12
14
  formatEstimatedBytes,
13
15
  formatResourceDescriptor,
16
+ formatTimingCoverage,
17
+ enableTabKeyboard,
18
+ writeClipboardText,
19
+ selectionKey,
14
20
  groupPath,
15
21
  resourceLabel,
16
22
  type WorkbenchCallbacks,
17
23
  } from './panelWorkbenchHelpers.ts';
18
24
 
25
+ type DetailRelation = readonly [label: string, selection: Selection, description?: string];
26
+
27
+ function formatAccessFacts(access: FrameGraphDebugAccess): string {
28
+ return [
29
+ access.mode, access.access,
30
+ ...(access.mode === 'write' ? [access.contents, access.producesValue ? 'produces a value' : 'does not produce a value'] : []),
31
+ ...(access.bufferRange ? [`bytes ${access.bufferRange.offset}–${access.bufferRange.size === undefined ? 'end' : access.bufferRange.offset + access.bufferRange.size}`] : []),
32
+ ...(access.textureRegion ? [formatTextureRegion(access.textureRegion)] : []),
33
+ ...(access.textureViewId ? [`view ${access.textureViewId}`] : []),
34
+ ].join(' · ');
35
+ }
36
+
37
+ function formatTextureRegion(region: NonNullable<FrameGraphDebugAccess['textureRegion']>): string {
38
+ const end = (start: number | undefined, count: number | undefined) => start === undefined ? 'unknown' : count === undefined ? `${start}–end` : `${start}–${start + count - 1}`;
39
+ return `mip ${end(region.baseMipLevel, region.mipLevelCount)} · ${region.baseDepthSlice === undefined
40
+ ? `layers ${end(region.baseArrayLayer, region.arrayLayerCount)}`
41
+ : `depth ${end(region.baseDepthSlice, region.depthSliceCount)}`} · aspect ${region.aspect}`;
42
+ }
43
+
44
+ function formatRootRange(root: FrameGraphDebugRoot): string {
45
+ if (!root.range) return 'Range unavailable';
46
+ if (root.range.kind === 'buffer') return `bytes ${root.range.offset}–${root.range.offset + root.range.size}`;
47
+ return root.range.regions.map(formatTextureRegion).join('; ');
48
+ }
49
+
19
50
  export class InspectorView {
20
51
  readonly root = document.createElement('aside');
21
52
  private readonly title = document.createElement('strong');
@@ -25,7 +56,9 @@ export class InspectorView {
25
56
  private snapshot: FrameGraphDebugViewModel | undefined;
26
57
  private selected: Selection | undefined;
27
58
  private activeTab: InspectorTab = 'summary';
28
- private open = true;
59
+ private open = false;
60
+ private hoveredLink: HTMLButtonElement | undefined;
61
+ private rawCache: { readonly snapshot: FrameGraphDebugViewModel; readonly key: string; readonly view: RawDetailView } | undefined;
29
62
 
30
63
  constructor(
31
64
  private readonly callbacks: WorkbenchCallbacks,
@@ -39,7 +72,7 @@ export class InspectorView {
39
72
  const close = document.createElement('button');
40
73
  close.type = 'button';
41
74
  close.className = 'zenfg-inspector-inspector-close';
42
- close.appendChild(createPanelIcon('close'));
75
+ close.appendChild(createPanelIcon('close'));
43
76
  close.title = 'Close inspector';
44
77
  close.setAttribute('aria-label', 'Close inspector');
45
78
  close.addEventListener('click', () => this.setOpen(false));
@@ -62,6 +95,7 @@ export class InspectorView {
62
95
  this.tabs.set(tab, button);
63
96
  this.tabList.appendChild(button);
64
97
  }
98
+ enableTabKeyboard(this.tabList);
65
99
  this.content.id = `${idPrefix}-inspector-panel`;
66
100
  this.content.className = 'zenfg-inspector-inspector-content';
67
101
  this.content.setAttribute('role', 'tabpanel');
@@ -71,20 +105,30 @@ export class InspectorView {
71
105
  }
72
106
 
73
107
  setSnapshot(snapshot: FrameGraphDebugViewModel): void {
108
+ if (this.snapshot !== snapshot) this.rawCache = undefined;
74
109
  this.snapshot = snapshot;
75
110
  this.render();
76
111
  }
77
112
 
78
113
  setSelection(selected: Selection | undefined, reveal = true): void {
114
+ if (!selected || !this.selected || selectionKey(selected) !== selectionKey(this.selected)) this.rawCache = undefined;
115
+ if (selected?.kind === 'resource'
116
+ && (this.selected?.kind !== 'resource' || this.selected.id !== selected.id)) this.activeTab = 'summary';
79
117
  this.selected = selected;
80
- if (selected && reveal) this.setOpen(true);
118
+ if (!selected) this.setOpen(false);
119
+ else if (reveal && !this.open) {
120
+ this.setOpen(true);
121
+ return;
122
+ }
81
123
  this.render();
82
124
  }
83
125
 
84
126
  setOpen(open: boolean): void {
85
127
  if (this.open === open) return;
128
+ if (!open) this.clearLinkHover();
86
129
  this.open = open;
87
130
  this.updateOpenState();
131
+ if (open) this.render();
88
132
  this.onOpenChange(open);
89
133
  }
90
134
 
@@ -98,10 +142,13 @@ export class InspectorView {
98
142
  }
99
143
 
100
144
  private render(): void {
145
+ const restoreFocus = this.content.contains(document.activeElement);
146
+ this.clearLinkHover();
101
147
  for (const [tab, button] of this.tabs) {
102
148
  const active = tab === this.activeTab;
103
149
  button.classList.toggle('active', active);
104
150
  button.setAttribute('aria-selected', active ? 'true' : 'false');
151
+ button.tabIndex = active ? 0 : -1;
105
152
  if (active) this.content.setAttribute('aria-labelledby', button.id);
106
153
  }
107
154
  const snapshot = this.snapshot;
@@ -116,32 +163,52 @@ export class InspectorView {
116
163
  return;
117
164
  }
118
165
  this.title.textContent = this.selectionTitle(snapshot, selected);
166
+ if (!this.open) return;
119
167
  if (this.activeTab === 'raw') {
120
- const raw = document.createElement('pre');
121
- raw.className = 'zenfg-inspector-raw-detail';
122
- raw.textContent = JSON.stringify(resolveSelectedDetail(snapshot, selected), null, 2);
123
- this.content.appendChild(raw);
168
+ const key = selectionKey(selected);
169
+ if (this.rawCache?.snapshot !== snapshot || this.rawCache.key !== key) {
170
+ const detail = resolveSelectedCanonicalDetail(snapshot, selected);
171
+ this.rawCache = detail ? { snapshot, key, view: new RawDetailView(detail, Boolean(snapshot.protocol.capture.migration)) } : undefined;
172
+ }
173
+ if (this.rawCache) this.content.appendChild(this.rawCache.view.root);
174
+ else this.content.textContent = 'Canonical object unavailable in this capture.';
124
175
  } else if (this.activeTab === 'relations') {
125
176
  this.content.appendChild(this.createRelations(snapshot, selected));
126
177
  } else {
127
- this.content.appendChild(this.createSummary(snapshot, selected));
178
+ this.content.append(this.createLocationActions(snapshot, selected), this.createSummary(snapshot, selected));
179
+ const identity = this.selectionId(snapshot, selected);
180
+ if (identity) this.content.appendChild(this.summary([['ID', this.copyId(identity)]]));
181
+ const diagnostics = this.createDiagnostics(snapshot, selected);
182
+ if (diagnostics) this.content.appendChild(diagnostics);
183
+ if (selected.kind === 'node' || selected.kind === 'culled') {
184
+ const node = snapshot.nodeById.get(selected.id) ?? snapshot.culledById.get(selected.id)?.node;
185
+ if (node) {
186
+ const accesses = document.createElement('details');
187
+ const heading = document.createElement('summary');
188
+ heading.textContent = `Access facts · ${node.reads.length + node.writes.length}`;
189
+ accesses.append(heading, this.accessRelations('Reads', node.reads), this.accessRelations('Writes', node.writes));
190
+ this.content.appendChild(accesses);
191
+ }
192
+ }
128
193
  }
194
+ // Selecting a relation replaces its button. Keep keyboard focus in the pane.
195
+ if (restoreFocus) this.tabs.get(this.activeTab)?.focus();
129
196
  }
130
197
 
131
198
  private selectionTitle(snapshot: FrameGraphDebugViewModel, selection: Selection): string {
132
199
  switch (selection.kind) {
133
200
  case 'node': {
134
201
  const node = snapshot.nodeById.get(selection.id);
135
- return node ? labelNode(node) : `Pass #${selection.id}`;
202
+ return node ? labelNode(node) : selection.id;
136
203
  }
137
204
  case 'group': return snapshot.groupByPathKey.get(selection.pathKey)?.label ?? 'Group';
138
205
  case 'resource': {
139
206
  const resource = snapshot.resourceById.get(selection.id);
140
- return resource ? labelResource(resource) : `Resource #${selection.id}`;
207
+ return resource ? labelResource(resource) : selection.id;
141
208
  }
142
- case 'allocation': return `Allocation #${selection.id}`;
143
- case 'root': return `Retention root #${selection.index}`;
144
- case 'culled': return `Culled node #${selection.index}`;
209
+ case 'allocation': return /^allocation[:#-]/i.test(selection.id) ? selection.id : `Allocation ${selection.id}`;
210
+ case 'root': return 'Output root';
211
+ case 'culled': return `${snapshot.culledById.get(selection.id)?.node.label ?? selection.id} (culled)`;
145
212
  case 'segment': return `Segment #${selection.index}`;
146
213
  }
147
214
  }
@@ -152,12 +219,21 @@ export class InspectorView {
152
219
  const node = snapshot.nodeById.get(selection.id);
153
220
  if (!node) return this.summary([]);
154
221
  const segment = snapshot.segmentByNodeId.get(node.id);
222
+ const roots = snapshot.roots.filter((root) => root.nodeId === node.id || root.resolution?.producerNodeIds.includes(node.id));
155
223
  return this.summary([
224
+ ['Compile state', 'Retained · compiler included this pass; execution success is not implied'],
225
+ ['Retention', roots.length ? roots.map((root) => root.reason).join(', ') : 'No direct output root recorded; inspect dependencies in Relations'],
156
226
  ['Kind', node.kind],
157
- ['Order', String(node.order)],
227
+ ['Execution slot', String(node.order)],
158
228
  ['Group', groupPath(snapshot, node.debugGroupId)],
159
229
  ['Segment', segment ? `#${segment.index} ${segment.kind}` : '-'],
160
- ['GPU', node.kind === 'external-submission' ? 'Opaque' : node.gpuDurationMicros === undefined ? 'Not timed' : `${(node.gpuDurationMicros / 1000).toFixed(3)} ms`],
230
+ ['CPU duration', node.cpuDurationMicros === undefined ? 'Not collected' : (node.cpuDurationMicros / 1000).toFixed(3) + ' ms'],
231
+ ['CPU scope', 'Synchronous elapsed time; zero may reflect clock precision.' + (node.kind === 'external-submission' ? ' Includes synchronous external submission.' : '')],
232
+ ['GPU duration', node.kind === 'external-submission' ? 'Opaque · external work is not measured'
233
+ : node.kind !== 'render' && node.kind !== 'compute' ? 'Not applicable · this pass kind is not timing eligible'
234
+ : node.gpuDurationMicros === undefined ? `Not collected${snapshot.profiling.status === 'unavailable' ? ` · ${snapshot.profiling.reason}` : ''}`
235
+ : `${(node.gpuDurationMicros / 1000).toFixed(3)} ms`],
236
+ ['GPU scope', 'GPU timestamp interval for render/compute passes; excludes external work. Zero may reflect timestamp precision.'],
161
237
  ['Accesses', `${node.reads.length} reads · ${node.writes.length} writes`],
162
238
  ['Side effect', node.sideEffect ? 'yes' : 'no'],
163
239
  ]);
@@ -169,9 +245,12 @@ export class InspectorView {
169
245
  ['Path', group.path.join(' / ')],
170
246
  ['Retained', String(group.summary.retainedNodeCount)],
171
247
  ['Culled', String(group.summary.culledNodeCount)],
172
- ['GPU coverage', `${group.summary.timedNodeCount}/${group.summary.timingEligibleNodeCount}`],
173
- ['GPU work', `${(group.summary.gpuWorkDurationMicros / 1000).toFixed(3)} ms`],
174
- ['Allocations', String(group.summary.physicalAllocationCount)],
248
+ ['CPU coverage', formatTimingCoverage(group.summary.cpuTimedNodeCount, group.summary.retainedNodeCount)],
249
+ ['CPU pass sum', group.summary.cpuTimedNodeCount ? (group.summary.cpuWorkDurationMicros / 1000).toFixed(3) + ' ms' : 'Not collected'],
250
+ ['GPU coverage', formatTimingCoverage(group.summary.timedNodeCount, group.summary.timingEligibleNodeCount)],
251
+ ['GPU pass sum', group.summary.timingEligibleNodeCount === 0 ? 'Not applicable' : group.summary.timedNodeCount === 0 ? 'Not collected' : (group.summary.gpuWorkDurationMicros / 1000).toFixed(3) + ' ms'],
252
+ ['Opaque passes', `${group.summary.externalSubmissionCount} · excluded from GPU pass sum`],
253
+ ['Allocations', snapshot.protocol.memory.allocationReport.status === 'available' ? String(group.summary.physicalAllocationCount) : 'Unknown · allocation report unavailable'],
175
254
  ['Segments', String(group.summary.executionSegmentCount)],
176
255
  ]);
177
256
  }
@@ -182,9 +261,14 @@ export class InspectorView {
182
261
  ['Kind / origin', `${resource.kind} · ${resource.origin}`],
183
262
  ['Group', groupPath(snapshot, resource.debugGroupId)],
184
263
  ['Descriptor', formatResourceDescriptor(resource)],
185
- ['Estimated', formatEstimatedBytes(resource.estimatedByteSize)],
186
- ['Lifetime', resource.lifetime ? `${resource.lifetime.firstUse}–${resource.lifetime.lastUse}` : '-'],
187
- ['Allocation', resource.physicalResourceId === undefined ? '-' : `#${resource.physicalResourceId}`],
264
+ ['Resource estimate', `${formatEstimatedBytes(resource.estimatedByteSize)} · logical descriptor estimate`],
265
+ ['Lifetime', resource.lifetime ? `execution slots ${resource.lifetime.firstUse}–${resource.lifetime.lastUse} (inclusive)` : 'Unavailable'],
266
+ ['Allocation', resource.physicalResourceId !== undefined
267
+ ? this.createDetailLink(resource.physicalResourceId, { kind: 'allocation', id: resource.physicalResourceId })
268
+ : resource.origin !== 'transient' ? 'Not applicable · externally owned resource'
269
+ : snapshot.protocol.memory.allocationReport.status === 'unavailable' ? 'Unavailable · allocation report not captured'
270
+ : 'Unallocated'],
271
+ ['Initial contents', resource.initialContents ?? 'Unknown'],
188
272
  ['Usage', resource.usageFlags.join(', ') || '-'],
189
273
  ]);
190
274
  }
@@ -194,26 +278,33 @@ export class InspectorView {
194
278
  return this.summary([
195
279
  ['Kind', allocation.kind],
196
280
  ['Compatibility class', String(allocation.compatibilityClassId)],
197
- ['Estimated', formatEstimatedBytes(allocation.estimatedByteSize)],
281
+ ['Physical estimate', `${formatEstimatedBytes(allocation.estimatedByteSize)} · allocation capacity estimate`],
198
282
  ['Logical resources', String(allocation.resourceIds.length)],
199
283
  ['Alias', allocation.resourceIds.length > 1 ? `yes · ×${allocation.resourceIds.length}` : 'single'],
200
284
  ]);
201
285
  }
202
286
  case 'root': {
203
- const root = snapshot.roots[selection.index];
287
+ const root = snapshot.roots.find((root) => root.key === selection.key);
204
288
  return this.summary(root ? [
205
289
  ['Reason', root.reason],
206
- ['Node', root.nodeId === undefined ? '-' : `#${root.nodeId}`],
207
- ['Resource', root.resource ? labelResource(root.resource) : '-'],
290
+ ['Node', root.nodeId === undefined ? 'Not applicable' : this.nodeLink(snapshot, root.nodeId)],
291
+ ['Resource', root.resource ? this.viewResourceButton(snapshot, root.resource.id) : '-'],
292
+ ...(root.reason === 'side-effect' ? [] : [
293
+ ['Range', root.range ? formatRootRange(root) : 'Unavailable in Legacy capture'],
294
+ ['Initial contents', root.resolution ? String(root.resolution.usesInitialContents) : 'Unavailable in Legacy capture'],
295
+ ['Producers', root.resolution ? root.resolution.producerNodeIds.join(', ') || 'None' : 'Unavailable in Legacy capture'],
296
+ ] as [string, string][]),
208
297
  ] : []);
209
298
  }
210
299
  case 'culled': {
211
- const culled = snapshot.culledNodes[selection.index];
300
+ const culled = snapshot.culledById.get(selection.id);
212
301
  return this.summary(culled ? [
302
+ ['Compile state', 'Culled · excluded by the compiler'],
213
303
  ['Node', labelNode(culled.node)],
214
304
  ['Kind', culled.node.kind],
215
305
  ['Group', groupPath(snapshot, culled.node.debugGroupId)],
216
306
  ['Reason', culled.reason],
307
+ ['Execution / segment / CPU / GPU', 'Not applicable · culled passes do not execute'],
217
308
  ['Accesses', `${culled.node.reads.length} reads · ${culled.node.writes.length} writes`],
218
309
  ] : []);
219
310
  }
@@ -233,6 +324,7 @@ export class InspectorView {
233
324
  private createRelations(snapshot: FrameGraphDebugViewModel, selection: Selection): HTMLElement {
234
325
  const host = document.createElement('div');
235
326
  host.className = 'zenfg-inspector-inspector-relations';
327
+ host.appendChild(this.createLocationActions(snapshot, selection));
236
328
  switch (selection.kind) {
237
329
  case 'node': {
238
330
  const node = snapshot.nodeById.get(selection.id);
@@ -245,6 +337,18 @@ export class InspectorView {
245
337
  if (segment) host.appendChild(this.relationGroup('Segment', [
246
338
  [`#${segment.index} ${segment.kind}`, { kind: 'segment', index: segment.index }],
247
339
  ]));
340
+ for (const kind of ['value', 'ordering'] as const) {
341
+ const relations: DetailRelation[] = [];
342
+ for (const edge of snapshot.edges) {
343
+ if (edge.kind !== kind || (edge.fromNodeId !== node.id && edge.toNodeId !== node.id)) continue;
344
+ const upstream = edge.toNodeId === node.id;
345
+ const otherId = upstream ? edge.fromNodeId : edge.toNodeId;
346
+ const other = resolveNodeSelection(snapshot, otherId);
347
+ if (other) relations.push([this.accessNodeLabel(snapshot, otherId), other,
348
+ `${upstream ? 'Upstream' : 'Downstream'} · ${resourceLabel(snapshot, edge.resource.id)} · ${kind} dependency`]);
349
+ }
350
+ if (relations.length) host.appendChild(this.relationGroup(kind === 'value' ? 'Value dependencies' : 'Ordering dependencies', relations));
351
+ }
248
352
  }
249
353
  break;
250
354
  }
@@ -254,6 +358,10 @@ export class InspectorView {
254
358
  host.append(
255
359
  this.resourceRelations('Inputs', group.summary.inputResources),
256
360
  this.resourceRelations('Outputs', group.summary.outputResources),
361
+ this.relationGroup('Output roots', group.summary.outputRoots.map((root) => [
362
+ `${root.resource ? labelResource(root.resource) : '-'} · ${root.reason} · ${formatRootRange(root)}`,
363
+ { kind: 'root', key: root.key },
364
+ ])),
257
365
  );
258
366
  }
259
367
  break;
@@ -261,17 +369,22 @@ export class InspectorView {
261
369
  case 'resource': {
262
370
  const resource = snapshot.resourceById.get(selection.id);
263
371
  const accesses = snapshot.accessesByResourceId.get(selection.id) ?? [];
264
- const passRelations: Array<readonly [string, Selection]> = [];
372
+ const passRelations: DetailRelation[] = [];
265
373
  for (const access of accesses) {
266
374
  const nodeSelection = this.accessNodeSelection(snapshot, access.nodeId);
267
375
  if (nodeSelection) passRelations.push([
268
- `${access.mode} · ${this.accessNodeLabel(snapshot, access.nodeId)} · ${access.access}`,
376
+ this.accessNodeLabel(snapshot, access.nodeId),
269
377
  nodeSelection,
378
+ formatAccessFacts(access),
270
379
  ]);
271
380
  }
272
381
  host.appendChild(this.relationGroup('Pass accesses', passRelations));
382
+ const roots = snapshot.roots.filter((root) => root.resource?.id === selection.id);
383
+ if (roots.length) host.appendChild(this.relationGroup('Output roots', roots.map((root) => [
384
+ `${root.reason} · ${formatRootRange(root)}`, { kind: 'root', key: root.key },
385
+ ])));
273
386
  if (resource?.physicalResourceId !== undefined) host.appendChild(this.relationGroup('Allocation', [
274
- [`#${resource.physicalResourceId}`, { kind: 'allocation', id: resource.physicalResourceId }],
387
+ [resource.physicalResourceId, { kind: 'allocation', id: resource.physicalResourceId }],
275
388
  ]));
276
389
  break;
277
390
  }
@@ -283,17 +396,19 @@ export class InspectorView {
283
396
  break;
284
397
  }
285
398
  case 'root': {
286
- const root = snapshot.roots[selection.index];
399
+ const root = snapshot.roots.find((root) => root.key === selection.key);
287
400
  const relations: Array<readonly [string, Selection]> = [];
288
401
  if (root?.nodeId !== undefined && snapshot.nodeById.has(root.nodeId)) relations.push([
289
402
  this.accessNodeLabel(snapshot, root.nodeId), { kind: 'node', id: root.nodeId },
290
403
  ]);
291
- if (root?.resource) relations.push([labelResource(root.resource), { kind: 'resource', id: root.resource.id }]);
292
- host.appendChild(this.relationGroup('Retained object', relations));
404
+ if (root?.resource) host.appendChild(this.relationGroup('Resource', [[labelResource(root.resource), { kind: 'resource', id: root.resource.id }]]));
405
+ if (root?.resource && root.resolution?.usesInitialContents) relations.push([`Initial contents · ${labelResource(root.resource)}`, { kind: 'resource', id: root.resource.id }]);
406
+ for (const id of root?.resolution?.producerNodeIds ?? []) relations.push([this.accessNodeLabel(snapshot, id), { kind: 'node', id }]);
407
+ host.appendChild(this.relationGroup('Output sources', relations));
293
408
  break;
294
409
  }
295
410
  case 'culled': {
296
- const culled = snapshot.culledNodes[selection.index];
411
+ const culled = snapshot.culledById.get(selection.id);
297
412
  if (culled) host.append(
298
413
  this.accessRelations('Reads', culled.node.reads),
299
414
  this.accessRelations('Writes', culled.node.writes),
@@ -309,7 +424,7 @@ export class InspectorView {
309
424
  break;
310
425
  }
311
426
  }
312
- if (host.childElementCount === 0) {
427
+ if (host.childElementCount === 1) {
313
428
  const empty = document.createElement('p');
314
429
  empty.className = 'zenfg-inspector-muted';
315
430
  empty.textContent = 'No related objects.';
@@ -320,8 +435,9 @@ export class InspectorView {
320
435
 
321
436
  private accessRelations(title: string, accesses: readonly FrameGraphDebugAccess[]): HTMLElement {
322
437
  return this.relationGroup(title, accesses.map((access) => [
323
- `${labelResource(access.resource)} · ${access.access}${access.mode === 'write' ? ` · ${access.contents}` : ''}`,
438
+ labelResource(access.resource),
324
439
  { kind: 'resource', id: access.resource.id },
440
+ formatAccessFacts(access),
325
441
  ]));
326
442
  }
327
443
 
@@ -331,13 +447,27 @@ export class InspectorView {
331
447
  ]));
332
448
  }
333
449
 
334
- private relationGroup(title: string, relations: readonly (readonly [string, Selection])[]): HTMLElement {
450
+ private relationGroup(title: string, relations: readonly DetailRelation[]): HTMLElement {
335
451
  const section = document.createElement('section');
336
452
  const heading = document.createElement('h3');
337
453
  heading.textContent = `${title} ${relations.length}`;
338
454
  section.appendChild(heading);
339
- for (const [label, selection] of relations) {
340
- section.appendChild(createRelationButton(label, selection, this.callbacks.onSelect));
455
+ for (const [label, selection, description] of relations) {
456
+ const link = this.createDetailLink(label, selection);
457
+ const entry = document.createElement('div');
458
+ entry.className = 'zenfg-inspector-relation-entry';
459
+ const links = document.createElement('div');
460
+ links.className = 'zenfg-inspector-relation-links';
461
+ links.appendChild(link);
462
+ if (this.callbacks.onReveal) links.appendChild(this.revealButton(selection, this.primaryPage(selection)));
463
+ entry.appendChild(links);
464
+ if (description !== undefined) {
465
+ const metadata = document.createElement('span');
466
+ metadata.className = 'zenfg-inspector-muted';
467
+ metadata.textContent = description;
468
+ entry.appendChild(metadata);
469
+ }
470
+ section.appendChild(entry);
341
471
  }
342
472
  if (relations.length === 0) {
343
473
  const empty = document.createElement('span');
@@ -348,15 +478,47 @@ export class InspectorView {
348
478
  return section;
349
479
  }
350
480
 
351
- private summary(rows: readonly (readonly [string, string])[]): HTMLElement {
481
+ private selectRelated(selection: Selection): void {
482
+ this.clearLinkHover();
483
+ this.callbacks.onSelect(selection);
484
+ }
485
+
486
+ private viewResourceButton(snapshot: FrameGraphDebugViewModel, resourceId: string): HTMLButtonElement {
487
+ return this.createDetailLink(`View resource · ${resourceLabel(snapshot, resourceId)}`,
488
+ { kind: 'resource', id: resourceId });
489
+ }
490
+
491
+ private createDetailLink(label: string, selection: Selection): HTMLButtonElement {
492
+ const link = createRelationButton(label, selection, (target) => this.selectRelated(target));
493
+ link.addEventListener('mouseenter', () => {
494
+ this.hoveredLink = link;
495
+ this.callbacks.onHover(selection);
496
+ });
497
+ link.addEventListener('mouseleave', () => {
498
+ if (this.hoveredLink === link) this.clearLinkHover();
499
+ });
500
+ return link;
501
+ }
502
+
503
+ private clearLinkHover(): void {
504
+ if (!this.hoveredLink) return;
505
+ this.hoveredLink = undefined;
506
+ this.callbacks.onHover(undefined);
507
+ }
508
+
509
+ private summary(rows: readonly (readonly [string, string | HTMLElement])[]): HTMLElement {
352
510
  const summary = document.createElement('dl');
353
511
  summary.className = 'zenfg-inspector-inspector-summary';
354
512
  for (const [label, value] of rows) {
355
513
  const term = document.createElement('dt');
356
514
  term.textContent = label;
357
515
  const description = document.createElement('dd');
358
- description.textContent = value;
359
- description.title = value;
516
+ if (typeof value === 'string') {
517
+ description.textContent = value;
518
+ description.title = value;
519
+ } else {
520
+ description.appendChild(value);
521
+ }
360
522
  summary.append(term, description);
361
523
  }
362
524
  return summary;
@@ -365,13 +527,118 @@ export class InspectorView {
365
527
  private accessNodeLabel(snapshot: FrameGraphDebugViewModel, nodeId: string): string {
366
528
  const node = snapshot.nodeById.get(nodeId);
367
529
  if (node) return labelNode(node);
368
- const culled = snapshot.culledNodes.find((candidate) => candidate.node.id === nodeId);
369
- return culled ? `${labelNode(culled.node)} (culled)` : `node-${nodeId}`;
530
+ const culled = snapshot.culledById.get(nodeId);
531
+ return culled ? `${labelNode(culled.node)} (culled)` : nodeId;
370
532
  }
371
533
 
372
534
  private accessNodeSelection(snapshot: FrameGraphDebugViewModel, nodeId: string): Selection | undefined {
373
- if (snapshot.nodeById.has(nodeId)) return { kind: 'node', id: nodeId };
374
- const index = snapshot.culledNodes.findIndex((candidate) => candidate.node.id === nodeId);
375
- return index < 0 ? undefined : { kind: 'culled', index };
535
+ return resolveNodeSelection(snapshot, nodeId);
536
+ }
537
+
538
+ private nodeLink(snapshot: FrameGraphDebugViewModel, id: string): string | HTMLButtonElement {
539
+ const selected = resolveNodeSelection(snapshot, id);
540
+ return selected ? this.createDetailLink(this.accessNodeLabel(snapshot, id), selected) : id;
541
+ }
542
+
543
+ private primaryPage(selection: Selection): WorkbenchTab {
544
+ switch (selection.kind) {
545
+ case 'node': case 'culled': case 'group': return 'passes';
546
+ case 'resource': return 'resources';
547
+ case 'allocation': return 'memory';
548
+ case 'root': case 'segment': return 'diagnostics';
549
+ }
550
+ }
551
+
552
+ private revealButton(selection: Selection, page: WorkbenchTab): HTMLButtonElement {
553
+ const button = document.createElement('button');
554
+ button.type = 'button';
555
+ button.className = 'zenfg-inspector-inline-action';
556
+ button.textContent = `Locate in ${page[0].toUpperCase()}${page.slice(1)}`;
557
+ button.addEventListener('click', () => {
558
+ this.clearLinkHover();
559
+ this.callbacks.onReveal?.(selection, page);
560
+ });
561
+ return button;
562
+ }
563
+
564
+ private createLocationActions(snapshot: FrameGraphDebugViewModel, selection: Selection): HTMLElement {
565
+ const actions = document.createElement('div');
566
+ actions.className = 'zenfg-inspector-detail-actions';
567
+ if (!this.callbacks.onReveal) return actions;
568
+ actions.appendChild(this.revealButton(selection, this.primaryPage(selection)));
569
+ if (selection.kind === 'node' || selection.kind === 'group' || selection.kind === 'resource' || selection.kind === 'root') {
570
+ actions.appendChild(this.revealButton(selection, 'graph'));
571
+ }
572
+ if (selection.kind === 'resource' && snapshot.resourceById.get(selection.id)?.origin === 'transient') actions.appendChild(this.revealButton(selection, 'memory'));
573
+ if (selection.kind === 'culled') {
574
+ const note = document.createElement('span');
575
+ note.className = 'zenfg-inspector-muted';
576
+ note.textContent = 'Culled passes are absent from Graph. Locate them in Passes.';
577
+ actions.appendChild(note);
578
+ }
579
+ return actions;
580
+ }
581
+
582
+ private selectionId(snapshot: FrameGraphDebugViewModel, selection: Selection): string | undefined {
583
+ if ('id' in selection) return selection.id;
584
+ if (selection.kind === 'group') return snapshot.groupByPathKey.get(selection.pathKey)?.id;
585
+ if (selection.kind === 'segment') return snapshot.segmentByIndex.get(selection.index)?.id;
586
+ return undefined;
587
+ }
588
+
589
+ private copyId(id: string): HTMLElement {
590
+ const host = document.createElement('span');
591
+ host.className = 'zenfg-inspector-copy-id';
592
+ const code = document.createElement('code');
593
+ code.textContent = id;
594
+ const button = document.createElement('button');
595
+ button.type = 'button';
596
+ button.textContent = 'Copy ID';
597
+ button.setAttribute('aria-label', 'Copy full object ID');
598
+ button.addEventListener('click', () => {
599
+ void writeClipboardText(id).then(() => { button.textContent = 'Copied'; }, () => { button.textContent = 'Copy failed'; });
600
+ });
601
+ host.append(code, button);
602
+ return host;
603
+ }
604
+
605
+ private createDiagnostics(snapshot: FrameGraphDebugViewModel, selection: Selection): HTMLElement | undefined {
606
+ let diagnostics: FrameGraphDebugViewModel['protocol']['diagnostics'] = [];
607
+ if (selection.kind === 'node' || selection.kind === 'culled') diagnostics = snapshot.diagnosticsByNodeId.get(selection.id) ?? [];
608
+ else if (selection.kind === 'resource') diagnostics = snapshot.diagnosticsByResourceId.get(selection.id) ?? [];
609
+ else if (selection.kind === 'allocation') {
610
+ const resourceIds = new Set(snapshot.allocationById.get(selection.id)?.resourceIds);
611
+ diagnostics = snapshot.protocol.diagnostics.filter((diagnostic) => diagnostic.resourceId !== undefined && resourceIds.has(diagnostic.resourceId));
612
+ } else if (selection.kind === 'group') {
613
+ const groupId = snapshot.groupByPathKey.get(selection.pathKey)?.id;
614
+ const groups = new Set(snapshot.debugGroups.filter((group) => groupId !== undefined && group.ancestorIds.includes(groupId)).map((group) => group.id));
615
+ diagnostics = snapshot.protocol.diagnostics.filter((diagnostic) => {
616
+ const nodeGroup = diagnostic.nodeId ? snapshot.canonicalNodeById.get(diagnostic.nodeId)?.groupId : undefined;
617
+ const resourceGroup = diagnostic.resourceId ? snapshot.resourceById.get(diagnostic.resourceId)?.debugGroupId : undefined;
618
+ return (nodeGroup !== undefined && groups.has(nodeGroup)) || (resourceGroup !== undefined && groups.has(resourceGroup));
619
+ });
620
+ }
621
+ if (!diagnostics.length) return undefined;
622
+ const section = document.createElement('section');
623
+ section.className = 'zenfg-inspector-detail-diagnostics';
624
+ const heading = document.createElement('h3');
625
+ heading.textContent = `Related diagnostics ${diagnostics.length}`;
626
+ section.appendChild(heading);
627
+ for (const diagnostic of diagnostics) {
628
+ const entry = document.createElement('article');
629
+ entry.dataset.severity = diagnostic.severity;
630
+ const label = document.createElement('strong');
631
+ label.textContent = `${diagnostic.severity} · ${diagnostic.code}`;
632
+ const message = document.createElement('p');
633
+ message.textContent = diagnostic.message;
634
+ entry.append(label, message);
635
+ if (diagnostic.nodeId) {
636
+ const node = resolveNodeSelection(snapshot, diagnostic.nodeId);
637
+ if (node) entry.appendChild(this.createDetailLink(this.accessNodeLabel(snapshot, diagnostic.nodeId), node));
638
+ }
639
+ if (diagnostic.resourceId) entry.appendChild(this.viewResourceButton(snapshot, diagnostic.resourceId));
640
+ section.appendChild(entry);
641
+ }
642
+ return section;
376
643
  }
377
644
  }