@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
@@ -4,11 +4,12 @@ import type {
4
4
  FrameGraphDebugGroup,
5
5
  FrameGraphDebugNode,
6
6
  FrameGraphDebugResource,
7
- FrameGraphDebugResourceRef,
8
7
  FrameGraphDebugViewModel,
9
8
  } from './debugCaptureModel.ts';
10
9
  import { formatGpuDuration, labelNode, labelResource } from './panelDomHelpers.ts';
11
- import type { GraphViewMode, Selection } from './panelTypes.ts';
10
+ import { declarationEntrances } from './debugCaptureModel.ts';
11
+ import type { GraphFlowRelation, Selection } from './panelTypes.ts';
12
+ import { formatMeasuredGpuWork } from './panelWorkbenchHelpers.ts';
12
13
 
13
14
  export type GraphSceneElementId = string;
14
15
 
@@ -28,14 +29,6 @@ export type PassSceneNode = GraphSceneNodeBase & {
28
29
  readonly gpuDurationMicros?: number;
29
30
  };
30
31
 
31
- export type CulledPassSceneNode = GraphSceneNodeBase & {
32
- readonly kind: 'culled-pass';
33
- readonly culledIndex: number;
34
- readonly nodeId: string;
35
- readonly passKind: FrameGraphDebugNode['kind'];
36
- readonly reason: string;
37
- };
38
-
39
32
  export type GroupSceneNode = GraphSceneNodeBase & {
40
33
  readonly kind: 'group';
41
34
  readonly groupId: string;
@@ -57,34 +50,29 @@ export type ResourceSceneNode = GraphSceneNodeBase & {
57
50
  readonly resourceKind: FrameGraphDebugResource['kind'];
58
51
  };
59
52
 
60
- export type GraphSceneNode = PassSceneNode | CulledPassSceneNode | GroupSceneNode | ResourceSceneNode;
53
+ export type RootSceneNode = GraphSceneNodeBase & {
54
+ readonly kind: 'root';
55
+ readonly rootKey: string;
56
+ readonly resourceId: string;
57
+ readonly resourceKind: FrameGraphDebugResource['kind'];
58
+ };
59
+ export type GraphSceneNode = PassSceneNode | GroupSceneNode | ResourceSceneNode | RootSceneNode;
61
60
 
62
61
  type GraphSceneEdgeBase = {
63
62
  readonly id: GraphSceneElementId;
64
63
  readonly from: GraphSceneElementId;
65
64
  readonly to: GraphSceneElementId;
66
- readonly label?: string;
67
65
  readonly title: string;
68
66
  readonly resourceId: string;
69
67
  };
70
68
 
71
- export type DependencySceneEdge = GraphSceneEdgeBase & {
72
- readonly kind: 'dependency';
73
- readonly dependencyKind: FrameGraphDebugEdge['kind'];
69
+ export type GraphSceneEdge = GraphSceneEdgeBase & {
70
+ readonly kind: 'flow' | 'ordering';
71
+ readonly relations: readonly GraphFlowRelation[];
74
72
  readonly underlyingDependencies: readonly FrameGraphDebugEdge[];
75
73
  readonly underlyingDependencyCount: number;
76
74
  };
77
75
 
78
- export type AccessSceneEdge = GraphSceneEdgeBase & {
79
- readonly kind: 'access';
80
- readonly accessId: string;
81
- readonly accessMode: FrameGraphDebugAccessEdge['mode'];
82
- readonly access: FrameGraphDebugAccessEdge['access'];
83
- readonly dashed: boolean;
84
- };
85
-
86
- export type GraphSceneEdge = DependencySceneEdge | AccessSceneEdge;
87
-
88
76
  export type GraphSemanticReferences = {
89
77
  readonly nodeIds: readonly string[];
90
78
  readonly groupPathKeys: readonly string[];
@@ -96,12 +84,12 @@ export type GraphSemanticReferences = {
96
84
  export type GraphSceneInteractionIndex = {
97
85
  readonly selectionByElementId: ReadonlyMap<GraphSceneElementId, Selection>;
98
86
  readonly primaryElementIdsBySelection: ReadonlyMap<string, readonly GraphSceneElementId[]>;
99
- readonly relatedElementIdsBySelection: ReadonlyMap<string, readonly GraphSceneElementId[]>;
87
+ readonly hoverElementIdsBySelection: ReadonlyMap<string, readonly GraphSceneElementId[]>;
88
+ readonly resourceElementIdsByResourceId: ReadonlyMap<string, readonly GraphSceneElementId[]>;
100
89
  readonly semanticReferencesByElementId: ReadonlyMap<GraphSceneElementId, GraphSemanticReferences>;
101
90
  };
102
91
 
103
92
  export type GraphScene = {
104
- readonly mode: GraphViewMode;
105
93
  readonly nodes: readonly GraphSceneNode[];
106
94
  readonly edges: readonly GraphSceneEdge[];
107
95
  readonly topologyKey: string;
@@ -110,15 +98,16 @@ export type GraphScene = {
110
98
  };
111
99
 
112
100
  export type CreateGraphSceneOptions = {
113
- readonly mode: GraphViewMode;
114
101
  readonly groupsEnabled: boolean;
102
+ readonly showResourceDeclarations?: boolean;
115
103
  readonly expandedGroupPaths: ReadonlySet<string>;
116
104
  };
117
105
 
118
106
  type MutableInteractionIndex = {
119
107
  readonly selectionByElementId: Map<GraphSceneElementId, Selection>;
120
108
  readonly primaryElementIdsBySelection: Map<string, GraphSceneElementId[]>;
121
- readonly relatedElementIdsBySelection: Map<string, GraphSceneElementId[]>;
109
+ readonly hoverElementIdsBySelection: Map<string, GraphSceneElementId[]>;
110
+ readonly resourceElementIdsByResourceId: Map<string, GraphSceneElementId[]>;
122
111
  readonly semanticReferencesByElementId: Map<GraphSceneElementId, GraphSemanticReferences>;
123
112
  };
124
113
 
@@ -126,9 +115,7 @@ export function createGraphScene(
126
115
  snapshot: FrameGraphDebugViewModel,
127
116
  options: CreateGraphSceneOptions,
128
117
  ): GraphScene {
129
- return options.mode === 'resources'
130
- ? createResourceGraphScene(snapshot)
131
- : createPassGraphScene(snapshot, options.groupsEnabled, options.expandedGroupPaths);
118
+ return createFrameFlowScene(snapshot, options.groupsEnabled, options.expandedGroupPaths, options.showResourceDeclarations ?? true);
132
119
  }
133
120
 
134
121
  export function graphGroupElementId(pathKey: string): GraphSceneElementId {
@@ -146,18 +133,19 @@ export function selectionKey(selection: Selection): string {
146
133
  case 'allocation':
147
134
  return `allocation:${selection.id}`;
148
135
  case 'root':
149
- return `root:${selection.index}`;
136
+ return `root:${selection.key}`;
150
137
  case 'culled':
151
- return `culled:${selection.index}`;
138
+ return `culled:${selection.id}`;
152
139
  case 'segment':
153
140
  return `segment:${selection.index}`;
154
141
  }
155
142
  }
156
143
 
157
- function createPassGraphScene(
144
+ function createFrameFlowScene(
158
145
  snapshot: FrameGraphDebugViewModel,
159
146
  groupsEnabled: boolean,
160
147
  expandedGroupPaths: ReadonlySet<string>,
148
+ showResourceDeclarations: boolean,
161
149
  ): GraphScene {
162
150
  const interaction = createMutableInteractionIndex();
163
151
  const segmentByNodeId = executionSegmentByNodeId(snapshot);
@@ -167,8 +155,20 @@ function createPassGraphScene(
167
155
  const hasVisibleAncestors = (group: FrameGraphDebugGroup) => group.ancestorIds
168
156
  .slice(0, -1)
169
157
  .every((id) => isExpanded(groupsById.get(id)!));
158
+ const retainedIds = new Set(snapshot.nodes.map((node) => node.id));
159
+ const usedResourceIds = new Set([
160
+ ...snapshot.accessEdges.filter((access) => retainedIds.has(access.nodeId)).map((access) => access.resource.id),
161
+ ...snapshot.roots.flatMap((root) => root.resource ? [root.resource.id] : []),
162
+ ]);
163
+ const resources = showResourceDeclarations ? snapshot.resources.filter((resource) => usedResourceIds.has(resource.id)) : [];
164
+ const populatedGroupIds = new Set<string>();
165
+ for (const item of [...snapshot.nodes, ...resources]) {
166
+ for (const id of groupsById.get(item.debugGroupId ?? '')?.ancestorIds ?? []) populatedGroupIds.add(id);
167
+ }
168
+ const visibleResources = resources.filter((resource) => !useGroups || resource.debugGroupId === undefined
169
+ || groupsById.get(resource.debugGroupId)!.ancestorIds.every((id) => isExpanded(groupsById.get(id)!)));
170
170
  const includedGroups = useGroups
171
- ? snapshot.debugGroups.filter((group) => group.summary.retainedNodeCount > 0 && hasVisibleAncestors(group))
171
+ ? snapshot.debugGroups.filter((group) => populatedGroupIds.has(group.id) && hasVisibleAncestors(group))
172
172
  : [];
173
173
  const includedGroupIds = new Set(includedGroups.map((group) => group.id));
174
174
  const representedNodeIdsByGroupId = new Map<string, string[]>();
@@ -197,6 +197,7 @@ function createPassGraphScene(
197
197
  .map((child) => graphGroupElementId(child.pathKey)),
198
198
  ...(visiblePassesByGroupId.get(group.id) ?? [])
199
199
  .map((node) => passElementId(node.id)),
200
+ ...visibleResources.filter((resource) => resource.debugGroupId === group.id).map((resource) => resourceElementId(resource.id)),
200
201
  ];
201
202
  const node: GroupSceneNode = {
202
203
  id,
@@ -204,8 +205,8 @@ function createPassGraphScene(
204
205
  groupId: group.id,
205
206
  groupPathKey: group.pathKey,
206
207
  label: createGroupLabel(group, collapsed),
207
- overviewLabel: `${collapsed ? '▸' : '▾'} ${group.label}`,
208
- title: createGroupTitle(group),
208
+ overviewLabel: `${collapsed ? '▸' : '▾'} ${group.label.replace(/\s+/g, ' ').trim()}`,
209
+ title: createGroupTitle(group, snapshot),
209
210
  parentId: group.parentId !== undefined && includedGroupIds.has(group.parentId)
210
211
  ? graphGroupElementId(groupsById.get(group.parentId)!.pathKey)
211
212
  : undefined,
@@ -267,226 +268,138 @@ function createPassGraphScene(
267
268
 
268
269
  for (const node of snapshot.nodes) {
269
270
  const representative = representativeByNodeId.get(node.id)!;
270
- setSelectionElements(interaction, { kind: 'node', id: node.id }, [representative], [representative]);
271
+ setSelectionElements(interaction, { kind: 'node', id: node.id }, [representative]);
271
272
  }
272
273
  if (useGroups) {
273
274
  for (const group of snapshot.debugGroups) {
274
- if (includedGroupIds.has(group.id)) continue;
275
+ if (includedGroupIds.has(group.id) || !populatedGroupIds.has(group.id)) continue;
275
276
  const collapsedAncestorId = group.ancestorIds.find((id) => !isExpanded(groupsById.get(id)!));
276
277
  if (collapsedAncestorId !== undefined) {
277
278
  const representative = graphGroupElementId(groupsById.get(collapsedAncestorId)!.pathKey);
278
- setSelectionElements(interaction, { kind: 'group', pathKey: group.pathKey }, [representative], [representative]);
279
+ setSelectionElements(interaction, { kind: 'group', pathKey: group.pathKey }, [representative]);
279
280
  }
280
281
  }
281
282
  }
282
283
 
283
- const dependencyEdges = useGroups
284
- ? createFoldedDependencyEdges(snapshot.edges, representativeByNodeId)
285
- : snapshot.edges.map((edge, index) => createDependencySceneEdge(
286
- edge,
287
- passElementId(edge.fromNodeId),
288
- passElementId(edge.toNodeId),
289
- [edge],
290
- index,
291
- ));
292
- for (const edge of dependencyEdges) {
293
- const selection: Selection = { kind: 'resource', id: edge.resourceId };
294
- interaction.selectionByElementId.set(edge.id, selection);
295
- appendSelectionElement(interaction.primaryElementIdsBySelection, selection, edge.id);
296
- appendSelectionElement(interaction.relatedElementIdsBySelection, selection, edge.id);
297
- interaction.semanticReferencesByElementId.set(edge.id, {
298
- nodeIds: unique(edge.underlyingDependencies.flatMap((dependency) => [dependency.fromNodeId, dependency.toNodeId])),
299
- groupPathKeys: [],
300
- resourceIds: [edge.resourceId],
301
- accessIds: [],
302
- dependencies: edge.underlyingDependencies,
303
- });
284
+ const representativeByResourceId = new Map<string, string>();
285
+ for (const resource of resources) {
286
+ const collapsed = useGroups ? groupsById.get(resource.debugGroupId ?? '')?.ancestorIds.find((id) => !isExpanded(groupsById.get(id)!)) : undefined;
287
+ const representative = collapsed ? graphGroupElementId(groupsById.get(collapsed)!.pathKey) : resourceElementId(resource.id);
288
+ representativeByResourceId.set(resource.id, representative);
304
289
  }
305
-
306
- return finalizeScene('passes', [...groupNodes, ...passNodes], dependencyEdges, interaction);
307
- }
308
-
309
- function createResourceGraphScene(snapshot: FrameGraphDebugViewModel): GraphScene {
310
- const interaction = createMutableInteractionIndex();
311
- const segmentByNodeId = executionSegmentByNodeId(snapshot);
312
- const accessesByResourceId = indexAccessesByResourceId(snapshot.accessEdges);
313
- const passNodes: PassSceneNode[] = snapshot.nodes.map((node) => {
314
- const segment = segmentByNodeId.get(node.id);
315
- const sceneNode: PassSceneNode = {
316
- id: passElementId(node.id),
317
- kind: 'pass',
318
- nodeId: node.id,
319
- passKind: node.kind,
320
- executionSegmentIndex: segment?.index,
321
- gpuDurationMicros: node.gpuDurationMicros,
322
- label: formatGraphNodeLabel(`${segmentLabel(segment)} ${labelNode(node)}`),
323
- overviewLabel: shortGraphLabel(labelNode(node)),
324
- title: createNodeTitle(node, segment, snapshot),
325
- };
326
- registerElement(
327
- interaction,
328
- sceneNode.id,
329
- { kind: 'node', id: node.id },
330
- emptyReferences({ nodeIds: [node.id] }),
331
- );
332
- return sceneNode;
333
- });
334
- const culledNodes: CulledPassSceneNode[] = snapshot.culledNodes.map((culled, index) => {
335
- const sceneNode: CulledPassSceneNode = {
336
- id: culledPassElementId(index, culled.node.id),
337
- kind: 'culled-pass',
338
- culledIndex: index,
339
- nodeId: culled.node.id,
340
- passKind: culled.node.kind,
341
- reason: culled.reason,
342
- label: `${formatGraphNodeLabel(labelNode(culled.node))}\n(culled)`,
343
- overviewLabel: `${shortGraphLabel(labelNode(culled.node))} (culled)`,
344
- title: createCulledNodeTitle(culled),
345
- };
346
- registerElement(
347
- interaction,
348
- sceneNode.id,
349
- { kind: 'culled', index },
350
- emptyReferences({ nodeIds: [culled.node.id] }),
351
- );
352
- return sceneNode;
353
- });
354
- const resourceNodes: ResourceSceneNode[] = snapshot.resources.map((resource) => {
355
- const sceneNode: ResourceSceneNode = {
356
- id: resourceElementId(resource.id),
357
- kind: 'resource',
358
- resourceId: resource.id,
359
- resourceKind: resource.kind,
360
- label: formatGraphResourceLabel(labelResource(resource)),
361
- overviewLabel: shortGraphLabel(labelResource(resource)),
362
- title: createResourceTitle(resource, snapshot, accessesByResourceId.get(resource.id) ?? []),
290
+ const resourceNodes: ResourceSceneNode[] = visibleResources.map((resource) => {
291
+ const origin = resource.origin === 'transient' ? 'Created' : resource.origin === 'imported' ? 'Imported' : 'Surface';
292
+ const node: ResourceSceneNode = {
293
+ id: resourceElementId(resource.id), kind: 'resource', resourceId: resource.id, resourceKind: resource.kind,
294
+ label: origin + ' · ' + (resource.kind === 'buffer' ? 'Buffer' : 'Texture') + '\n' + formatGraphResourceLabel(labelResource(resource)),
295
+ overviewLabel: origin + '\n' + formatGraphResourceLabel(shortGraphLabel(labelResource(resource)), 1),
296
+ title: createResourceTitle(resource, snapshot, snapshot.accessesByResourceId.get(resource.id) ?? [])
297
+ + '\nDeclaration entrance, not a complete initial-content provenance graph.',
298
+ parentId: useGroups && resource.debugGroupId ? graphGroupElementId(groupsById.get(resource.debugGroupId)!.pathKey) : undefined,
363
299
  };
364
- registerElement(
365
- interaction,
366
- sceneNode.id,
367
- { kind: 'resource', id: resource.id },
368
- emptyReferences({ resourceIds: [resource.id] }),
369
- );
370
- return sceneNode;
300
+ registerElement(interaction, node.id, { kind: 'resource', id: resource.id }, emptyReferences({ resourceIds: [resource.id] }));
301
+ interaction.resourceElementIdsByResourceId.set(resource.id, [node.id]);
302
+ return node;
371
303
  });
372
- const culledNodeIds = new Map(snapshot.culledNodes.map((culled, index) => [
373
- culled.node.id,
374
- culledPassElementId(index, culled.node.id),
375
- ]));
376
- const accessEdges: AccessSceneEdge[] = snapshot.accessEdges.map((access) => {
377
- const passId = culledNodeIds.get(access.nodeId) ?? passElementId(access.nodeId);
378
- const resourceId = resourceElementId(access.resource.id);
379
- const edge: AccessSceneEdge = {
380
- id: accessElementId(access.accessId, access.resource.id),
381
- kind: 'access',
382
- from: access.mode === 'write' ? passId : resourceId,
383
- to: access.mode === 'write' ? resourceId : passId,
384
- label: access.access,
385
- title: createAccessTitle(access),
386
- resourceId: access.resource.id,
387
- accessId: access.accessId,
388
- accessMode: access.mode,
389
- access: access.access,
390
- dashed: culledNodeIds.has(access.nodeId),
391
- };
392
- const selection: Selection = { kind: 'resource', id: access.resource.id };
393
- interaction.selectionByElementId.set(edge.id, selection);
394
- appendSelectionElement(interaction.relatedElementIdsBySelection, selection, edge.id);
395
- interaction.semanticReferencesByElementId.set(edge.id, {
396
- nodeIds: [access.nodeId],
397
- groupPathKeys: [],
398
- resourceIds: [access.resource.id],
399
- accessIds: [access.accessId],
400
- dependencies: [],
304
+ const rootNodes: RootSceneNode[] = [];
305
+ const aggregates = new Map<string, { from: string; to: string; resourceId: string; relations: GraphFlowRelation[] }>();
306
+ const addRelation = (from: string, to: string, resourceId: string, relation: GraphFlowRelation) => {
307
+ if (from === to) return;
308
+ const key = JSON.stringify([from, to, resourceId]);
309
+ const entry = aggregates.get(key) ?? { from, to, resourceId, relations: [] };
310
+ entry.relations.push(relation);
311
+ aggregates.set(key, entry);
312
+ };
313
+ for (const dependency of snapshot.edges) {
314
+ addRelation(representativeByNodeId.get(dependency.fromNodeId)!, representativeByNodeId.get(dependency.toNodeId)!, dependency.resource.id,
315
+ { role: dependency.kind, nodeIds: [dependency.fromNodeId, dependency.toNodeId], dependency });
316
+ }
317
+ for (const access of showResourceDeclarations ? declarationEntrances(snapshot.nodes, snapshot.accessEdges, snapshot.edges) : []) {
318
+ addRelation(representativeByResourceId.get(access.resource.id)!, representativeByNodeId.get(access.nodeId)!, access.resource.id,
319
+ { role: 'declaration', nodeIds: [access.nodeId] });
320
+ }
321
+ const rootOccurrences = new Map<string, number>();
322
+ const rangesByRootFamily = new Map<string, Set<string>>();
323
+ for (const root of snapshot.roots) {
324
+ if (!root.resource) continue;
325
+ const family = JSON.stringify([root.resource.id, root.reason]);
326
+ const ranges = rangesByRootFamily.get(family) ?? new Set<string>();
327
+ ranges.add(root.key);
328
+ rangesByRootFamily.set(family, ranges);
329
+ }
330
+ for (const root of snapshot.roots) {
331
+ if (!root.resource) {
332
+ if (root.nodeId) {
333
+ const representative = representativeByNodeId.get(root.nodeId)!;
334
+ setSelectionElements(interaction, { kind: 'root', key: root.key }, [representative]);
335
+ }
336
+ continue;
337
+ }
338
+ const occurrence = rootOccurrences.get(root.key) ?? 0;
339
+ rootOccurrences.set(root.key, occurrence + 1);
340
+ const id = 'root:' + root.key + (occurrence ? ':' + occurrence : '');
341
+ const resource = snapshot.resourceById.get(root.resource.id)!;
342
+ const rangeLabel = root.range?.kind === 'buffer' ? `bytes ${root.range.offset}–${root.range.offset + root.range.size}`
343
+ : root.range ? root.range.regions.map((r) => `mip ${r.baseMipLevel}+${r.mipLevelCount} · ${r.baseDepthSlice !== undefined ? 'D' + r.baseDepthSlice + '+' + r.depthSliceCount : 'L' + r.baseArrayLayer + '+' + r.arrayLayerCount} · ${r.aspect}`).join('; ') : 'Range unavailable';
344
+ const rangeSummary = root.range?.kind === 'texture'
345
+ ? root.range.regions.map((r) => `m${r.baseMipLevel}+${r.mipLevelCount} ${r.baseDepthSlice !== undefined ? 'D' + r.baseDepthSlice + '+' + r.depthSliceCount : 'L' + r.baseArrayLayer + '+' + r.arrayLayerCount} ${r.aspect === 'depth-only' ? 'depth' : r.aspect === 'stencil-only' ? 'stencil' : 'all'}`).join('; ')
346
+ : rangeLabel;
347
+ const reasonLabel = root.reason.split('-').map((word) => word[0]!.toUpperCase() + word.slice(1)).join(' ');
348
+ const disambiguate = rangesByRootFamily.get(JSON.stringify([resource.id, root.reason]))!.size > 1;
349
+ const initialLabel = !showResourceDeclarations && root.resolution?.usesInitialContents
350
+ ? '\n' + (root.resolution.producerNodeIds.length ? 'With initial contents' : 'Initial contents only') : '';
351
+ rootNodes.push({
352
+ id, kind: 'root', rootKey: root.key, resourceId: resource.id, resourceKind: resource.kind,
353
+ label: reasonLabel + '\n' + formatGraphResourceLabel(labelResource(resource))
354
+ + (disambiguate ? '\n' + formatGraphResourceLabel(rangeSummary, 1) : '') + initialLabel,
355
+ overviewLabel: reasonLabel + '\n' + formatGraphResourceLabel(shortGraphLabel(labelResource(resource)), 1) + initialLabel,
356
+ title: labelResource(resource) + '\n' + root.reason + '\n' + rangeLabel
357
+ + (root.resolution ? '\nProducers: ' + (root.resolution.producerNodeIds.join(', ') || 'none') + '\nInitial contents: ' + root.resolution.usesInitialContents : '\nOutput sources unavailable in Legacy capture.'),
401
358
  });
402
- return edge;
403
- });
404
-
405
- return finalizeScene('resources', [...passNodes, ...culledNodes, ...resourceNodes], accessEdges, interaction);
406
- }
407
-
408
- export function indexAccessesByResourceId(
409
- accesses: readonly FrameGraphDebugAccessEdge[],
410
- ): ReadonlyMap<string, readonly FrameGraphDebugAccessEdge[]> {
411
- const result = new Map<string, FrameGraphDebugAccessEdge[]>();
412
- for (const access of accesses) {
413
- const resourceAccesses = result.get(access.resource.id);
414
- if (resourceAccesses) {
415
- resourceAccesses.push(access);
416
- } else {
417
- result.set(access.resource.id, [access]);
359
+ registerElement(interaction, id, { kind: 'root', key: root.key },
360
+ emptyReferences({ resourceIds: [resource.id], nodeIds: root.resolution?.producerNodeIds ?? [] }));
361
+ for (const nodeId of root.resolution?.producerNodeIds ?? []) {
362
+ addRelation(representativeByNodeId.get(nodeId)!, id, resource.id, { role: 'output-producer', nodeIds: [nodeId], rootKey: root.key });
418
363
  }
364
+ if (showResourceDeclarations && root.resolution?.usesInitialContents) addRelation(representativeByResourceId.get(resource.id)!, id, resource.id,
365
+ { role: 'output-initial', nodeIds: [], rootKey: root.key });
419
366
  }
420
- return result;
421
- }
422
-
423
- function createFoldedDependencyEdges(
424
- dependencies: readonly FrameGraphDebugEdge[],
425
- representativeByNodeId: ReadonlyMap<string, GraphSceneElementId>,
426
- ): DependencySceneEdge[] {
427
- const dependenciesByKey = new Map<string, {
428
- readonly from: GraphSceneElementId;
429
- readonly to: GraphSceneElementId;
430
- readonly resource: FrameGraphDebugResourceRef;
431
- readonly dependencies: FrameGraphDebugEdge[];
432
- }>();
433
- for (const dependency of dependencies) {
434
- const from = representativeByNodeId.get(dependency.fromNodeId)!;
435
- const to = representativeByNodeId.get(dependency.toNodeId)!;
436
- if (from === to) continue;
437
- const key = JSON.stringify([from, to, dependency.resource.id]);
438
- const existing = dependenciesByKey.get(key);
439
- if (existing) {
440
- existing.dependencies.push(dependency);
441
- } else {
442
- dependenciesByKey.set(key, { from, to, resource: dependency.resource, dependencies: [dependency] });
443
- }
367
+ const edges: GraphSceneEdge[] = [...aggregates.entries()].map(([key, entry]) => {
368
+ const underlyingDependencies = entry.relations.flatMap((relation) => relation.dependency ? [relation.dependency] : []);
369
+ const edge: GraphSceneEdge = {
370
+ id: 'flow:' + key, ...entry,
371
+ kind: entry.relations.every((relation) => relation.role === 'ordering') ? 'ordering' : 'flow',
372
+ title: labelResource(snapshot.resourceById.get(entry.resourceId)!) + '\n'
373
+ + unique(entry.relations.map((relation) => relation.role)).join(' · '),
374
+ underlyingDependencies, underlyingDependencyCount: underlyingDependencies.length,
375
+ };
376
+ const selection: Selection = { kind: 'resource', id: entry.resourceId };
377
+ registerElement(interaction, edge.id, selection, emptyReferences({
378
+ nodeIds: unique(entry.relations.flatMap((relation) => relation.nodeIds)), resourceIds: [entry.resourceId], dependencies: underlyingDependencies,
379
+ }));
380
+ const resourceElements = interaction.resourceElementIdsByResourceId.get(entry.resourceId) ?? [];
381
+ resourceElements.push(edge.id);
382
+ interaction.resourceElementIdsByResourceId.set(entry.resourceId, resourceElements);
383
+ return edge;
384
+ });
385
+ for (const [resourceId, elements] of interaction.resourceElementIdsByResourceId) {
386
+ const selection: Selection = { kind: 'resource', id: resourceId };
387
+ setSelectionElements(interaction, selection, elements);
388
+ interaction.hoverElementIdsBySelection.set(selectionKey(selection), [...elements]);
444
389
  }
445
- return [...dependenciesByKey.values()].map((entry, index) => createDependencySceneEdge(
446
- entry.dependencies.find((dependency) => dependency.kind === 'value') ?? entry.dependencies[0]!,
447
- entry.from,
448
- entry.to,
449
- entry.dependencies,
450
- index,
451
- ));
452
- }
453
-
454
- function createDependencySceneEdge(
455
- dependency: FrameGraphDebugEdge,
456
- from: GraphSceneElementId,
457
- to: GraphSceneElementId,
458
- underlyingDependencies: readonly FrameGraphDebugEdge[],
459
- occurrence: number,
460
- ): DependencySceneEdge {
461
- const dependencyKind = underlyingDependencies.some((edge) => edge.kind === 'value') ? 'value' : 'ordering';
462
- return {
463
- id: `dependency:${JSON.stringify([from, to, dependency.resource.id, occurrence])}`,
464
- kind: 'dependency',
465
- from,
466
- to,
467
- title: `${labelResource(dependency.resource)}${underlyingDependencies.length > 1
468
- ? `\n${underlyingDependencies.length} folded dependencies`
469
- : ''}`,
470
- resourceId: dependency.resource.id,
471
- dependencyKind,
472
- underlyingDependencies,
473
- underlyingDependencyCount: underlyingDependencies.length,
474
- };
390
+ return finalizeScene([...groupNodes, ...passNodes, ...resourceNodes, ...rootNodes], edges, interaction);
475
391
  }
476
392
 
477
393
  function finalizeScene(
478
- mode: GraphViewMode,
479
394
  nodes: readonly GraphSceneNode[],
480
395
  edges: readonly GraphSceneEdge[],
481
396
  interaction: MutableInteractionIndex,
482
397
  ): GraphScene {
483
398
  const topologyKey = JSON.stringify({
484
- mode,
485
399
  nodes: nodes.map((node) => [node.id, node.kind, node.parentId, node.kind === 'group' ? node.collapsed : undefined]),
486
400
  edges: edges.map((edge) => [edge.id, edge.kind, edge.from, edge.to]),
487
401
  });
488
402
  const contentKey = JSON.stringify({
489
- mode,
490
403
  nodes: nodes.map((node) => [
491
404
  node.id,
492
405
  node.kind,
@@ -494,8 +407,8 @@ function finalizeScene(
494
407
  node.label,
495
408
  node.overviewLabel,
496
409
  node.title,
497
- node.kind === 'pass' || node.kind === 'culled-pass' ? node.passKind : undefined,
498
- node.kind === 'resource' ? node.resourceKind : undefined,
410
+ node.kind === 'pass' ? node.passKind : undefined,
411
+ (node.kind === 'resource' || node.kind === 'root') ? node.resourceKind : undefined,
499
412
  node.kind === 'group' ? node.collapsed : undefined,
500
413
  node.kind === 'group' ? node.depthBand : undefined,
501
414
  node.kind === 'group' ? node.culledNodeCount > 0 : undefined,
@@ -505,15 +418,11 @@ function finalizeScene(
505
418
  edge.kind,
506
419
  edge.from,
507
420
  edge.to,
508
- edge.label,
509
421
  edge.title,
510
- edge.kind === 'dependency' ? edge.dependencyKind : undefined,
511
- edge.kind === 'access' ? edge.accessMode : undefined,
512
- edge.kind === 'access' ? edge.dashed : undefined,
422
+ edge.relations,
513
423
  ]),
514
424
  });
515
425
  return {
516
- mode,
517
426
  nodes,
518
427
  edges,
519
428
  topologyKey,
@@ -526,7 +435,8 @@ function createMutableInteractionIndex(): MutableInteractionIndex {
526
435
  return {
527
436
  selectionByElementId: new Map(),
528
437
  primaryElementIdsBySelection: new Map(),
529
- relatedElementIdsBySelection: new Map(),
438
+ hoverElementIdsBySelection: new Map(),
439
+ resourceElementIdsByResourceId: new Map(),
530
440
  semanticReferencesByElementId: new Map(),
531
441
  };
532
442
  }
@@ -538,7 +448,8 @@ function registerElement(
538
448
  references: GraphSemanticReferences,
539
449
  ): void {
540
450
  interaction.selectionByElementId.set(elementId, selection);
541
- setSelectionElements(interaction, selection, [elementId], [elementId]);
451
+ setSelectionElements(interaction, selection, [elementId]);
452
+ interaction.hoverElementIdsBySelection.set(selectionKey(selection), [elementId]);
542
453
  interaction.semanticReferencesByElementId.set(elementId, references);
543
454
  }
544
455
 
@@ -546,21 +457,8 @@ function setSelectionElements(
546
457
  interaction: MutableInteractionIndex,
547
458
  selection: Selection,
548
459
  primary: readonly GraphSceneElementId[],
549
- related: readonly GraphSceneElementId[],
550
460
  ): void {
551
461
  interaction.primaryElementIdsBySelection.set(selectionKey(selection), [...primary]);
552
- interaction.relatedElementIdsBySelection.set(selectionKey(selection), [...related]);
553
- }
554
-
555
- function appendSelectionElement(
556
- map: Map<string, GraphSceneElementId[]>,
557
- selection: Selection,
558
- elementId: GraphSceneElementId,
559
- ): void {
560
- const key = selectionKey(selection);
561
- const elementIds = map.get(key) ?? [];
562
- if (!elementIds.includes(elementId)) elementIds.push(elementId);
563
- map.set(key, elementIds);
564
462
  }
565
463
 
566
464
  function emptyReferences(overrides: Partial<GraphSemanticReferences>): GraphSemanticReferences {
@@ -578,24 +476,55 @@ function passElementId(nodeId: string): GraphSceneElementId {
578
476
  return `pass:${nodeId}`;
579
477
  }
580
478
 
581
- function culledPassElementId(index: number, nodeId: string): GraphSceneElementId {
582
- return `culled-pass:${index}:${nodeId}`;
583
- }
584
-
585
479
  function resourceElementId(resourceId: string): GraphSceneElementId {
586
480
  return `resource:${resourceId}`;
587
481
  }
588
482
 
589
- function accessElementId(accessId: string, resourceId: string): GraphSceneElementId {
590
- return `access:${accessId}:${resourceId}`;
591
- }
592
-
593
483
  function formatGraphNodeLabel(label: string): string {
594
484
  return formatGraphLabel(label, 20, 3);
595
485
  }
596
486
 
597
- function formatGraphResourceLabel(label: string): string {
598
- return formatGraphLabel(label, 18, 3);
487
+ function formatGraphResourceLabel(label: string, maxLines = 2): string {
488
+ let remaining = [...label.replace(/\s+/g, ' ').trim()];
489
+ // Reserve conservative monospace cells for CJK/emoji fallback glyphs as well.
490
+ const cells = (glyph: string) => /\p{Mark}/u.test(glyph) ? 0 : glyph.codePointAt(0)! > 255 ? 2 : 1;
491
+ const take = (limit: number) => {
492
+ let width = 0;
493
+ let count = 0;
494
+ for (const glyph of remaining) {
495
+ if (width + cells(glyph) > limit) break;
496
+ width += cells(glyph);
497
+ count++;
498
+ }
499
+ return count;
500
+ };
501
+ const lines: string[] = [];
502
+ while (remaining.length) {
503
+ let count = take(18);
504
+ if (count === remaining.length) {
505
+ lines.push(remaining.join(''));
506
+ break;
507
+ }
508
+ if (lines.length === maxLines - 1) {
509
+ lines.push(remaining.slice(0, take(17)).join('').trimEnd() + '…');
510
+ break;
511
+ }
512
+ // Prefer a readable namespace/path boundary over splitting a word.
513
+ let boundary = 0;
514
+ for (const separators of [/[./]/u, /[_\-\s]/u]) {
515
+ for (let index = count - 1; index > 0; index--) {
516
+ if (separators.test(remaining[index]!)) {
517
+ boundary = index + 1;
518
+ break;
519
+ }
520
+ }
521
+ if (boundary) break;
522
+ }
523
+ if (boundary) count = boundary;
524
+ lines.push(remaining.slice(0, count).join('').trimEnd());
525
+ remaining = remaining.slice(count);
526
+ }
527
+ return lines.join('\n');
599
528
  }
600
529
 
601
530
  function formatGraphLabel(label: string, maxLineLength: number, maxLines: number): string {
@@ -677,10 +606,11 @@ function shortGraphLabel(label: string): string {
677
606
 
678
607
  function createGroupLabel(group: FrameGraphDebugGroup, collapsed: boolean): string {
679
608
  const summary = group.summary;
680
- const gpu = summary.timingEligibleNodeCount === 0
681
- ? 'no timed passes'
682
- : ${(summary.gpuWorkDurationMicros / 1000).toFixed(3)} ms ${summary.timedNodeCount}/${summary.timingEligibleNodeCount}`;
683
- return `${collapsed ? '▸' : '▾'} ${group.label} · ${summary.retainedNodeCount} retained · ${summary.culledNodeCount} culled · ${gpu}`;
609
+ const parts = [
610
+ `${collapsed ? '▸' : '▾'} ${group.label.replace(/\s+/g, ' ').trim()}`,
611
+ `${summary.retainedNodeCount} nodes` + (summary.culledNodeCount ? ` · ${summary.culledNodeCount} culled` : ''),
612
+ ];
613
+ return parts.join(collapsed ? '\n' : ' · ');
684
614
  }
685
615
 
686
616
  function createNodeTitle(
@@ -688,18 +618,22 @@ function createNodeTitle(
688
618
  segment: FrameGraphDebugViewModel['executionSegments'][number] | undefined,
689
619
  snapshot: FrameGraphDebugViewModel,
690
620
  ): string {
621
+ const gpu = node.kind === 'external-submission' ? 'Opaque'
622
+ : node.kind !== 'render' && node.kind !== 'compute' ? 'Not applicable'
623
+ : node.gpuDurationMicros === undefined ? 'Not collected' : `${formatGpuDuration(node)} ms`;
691
624
  return [
692
625
  labelNode(node),
693
626
  `kind: ${node.kind}`,
694
627
  `group: ${debugGroupPathForId(node.debugGroupId, snapshot)}`,
695
628
  `segment: ${segment ? `${segment.index}:${segment.kind}` : '-'}`,
696
- `gpu: ${node.kind === 'external-submission' ? 'opaque' : `${formatGpuDuration(node)} ms`}`,
629
+ `CPU duration: ${node.cpuDurationMicros === undefined ? 'Not collected' : (node.cpuDurationMicros / 1000).toFixed(3) + ' ms'}`,
630
+ `GPU duration: ${gpu}`,
697
631
  `reads: ${node.reads.map((access) => labelResource(access.resource)).join(', ') || '-'}`,
698
632
  `writes: ${node.writes.map((access) => labelResource(access.resource)).join(', ') || '-'}`,
699
633
  ].join('\n');
700
634
  }
701
635
 
702
- function createGroupTitle(group: FrameGraphDebugGroup): string {
636
+ function createGroupTitle(group: FrameGraphDebugGroup, snapshot: FrameGraphDebugViewModel): string {
703
637
  const summary = group.summary;
704
638
  return [
705
639
  group.path.join(' / '),
@@ -708,20 +642,11 @@ function createGroupTitle(group: FrameGraphDebugGroup): string {
708
642
  `inputs: ${summary.inputResources.map(labelResource).join(', ') || '-'}`,
709
643
  `outputs: ${summary.outputResources.map(labelResource).join(', ') || '-'}`,
710
644
  `transient registered/accessed: ${summary.registeredTransientResourceCount}/${summary.accessedTransientResourceCount}`,
711
- `physical allocations: ${summary.physicalAllocationCount}`,
645
+ `physical allocations: ${snapshot.protocol.memory.allocationReport.status === 'available' ? summary.physicalAllocationCount : 'Not collected'}`,
712
646
  `segments: ${summary.executionSegmentCount}`,
713
647
  `opaque: ${summary.externalSubmissionCount}`,
714
- GPU work: ${(summary.gpuWorkDurationMicros / 1000).toFixed(3)} ms (${summary.timedNodeCount}/${summary.timingEligibleNodeCount})`,
715
- ].join('\n');
716
- }
717
-
718
- function createCulledNodeTitle(culled: FrameGraphDebugViewModel['culledNodes'][number]): string {
719
- return [
720
- `${labelNode(culled.node)} (culled)`,
721
- `kind: ${culled.node.kind}`,
722
- `reason: ${culled.reason}`,
723
- `reads: ${culled.node.reads.map((access) => labelResource(access.resource)).join(', ') || '-'}`,
724
- `writes: ${culled.node.writes.map((access) => labelResource(access.resource)).join(', ') || '-'}`,
648
+ `CPU pass sum: ${summary.cpuTimedNodeCount ? (summary.cpuWorkDurationMicros / 1000).toFixed(3) + ' ms' : 'Not collected'} (${summary.cpuTimedNodeCount}/${summary.retainedNodeCount})`,
649
+ `GPU pass sum: ${formatMeasuredGpuWork(summary.gpuWorkDurationMicros, summary.timedNodeCount, summary.timingEligibleNodeCount)}`,
725
650
  ].join('\n');
726
651
  }
727
652
 
@@ -742,19 +667,6 @@ function createResourceTitle(
742
667
  ].join('\n');
743
668
  }
744
669
 
745
- function createAccessTitle(access: FrameGraphDebugAccessEdge): string {
746
- return [
747
- `#${access.accessId} ${access.mode}`,
748
- labelResource(access.resource),
749
- `access: ${access.access}`,
750
- access.mode === 'write' ? `contents: ${access.contents}` : undefined,
751
- access.mode === 'write' ? `result: ${access.producesValue ? 'produced' : 'discarded'}` : undefined,
752
- access.textureViewId === undefined ? undefined : `texture view: #${access.textureViewId}`,
753
- access.textureRegion ? `texture: mip ${access.textureRegion.baseMipLevel}+${access.textureRegion.mipLevelCount}, layer ${access.textureRegion.baseArrayLayer ?? 0}+${access.textureRegion.arrayLayerCount ?? 1}, depth ${access.textureRegion.baseDepthSlice ?? 0}+${access.textureRegion.depthSliceCount ?? 1}, ${access.textureRegion.aspect}` : undefined,
754
- access.bufferRange ? `buffer: ${access.bufferRange.offset}+${access.bufferRange.size ?? 'end'}` : undefined,
755
- ].filter(Boolean).join('\n');
756
- }
757
-
758
670
  function debugGroupPathForId(groupId: string | undefined, snapshot: FrameGraphDebugViewModel): string {
759
671
  if (groupId === undefined) return '-';
760
672
  return snapshot.debugGroups.find((group) => group.id === groupId)?.path.join(' / ') ?? `#${groupId}`;