@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,8 +1,8 @@
1
1
  import { formatGpuDuration, labelNode, labelResource } from "./panelDomHelpers.js";
2
+ import { declarationEntrances } from "./debugCaptureModel.js";
3
+ import { formatMeasuredGpuWork } from "./panelWorkbenchHelpers.js";
2
4
  export function createGraphScene(snapshot, options) {
3
- return options.mode === 'resources'
4
- ? createResourceGraphScene(snapshot)
5
- : createPassGraphScene(snapshot, options.groupsEnabled, options.expandedGroupPaths);
5
+ return createFrameFlowScene(snapshot, options.groupsEnabled, options.expandedGroupPaths, options.showResourceDeclarations ?? true);
6
6
  }
7
7
  export function graphGroupElementId(pathKey) {
8
8
  return `group:${pathKey}`;
@@ -18,14 +18,14 @@ export function selectionKey(selection) {
18
18
  case 'allocation':
19
19
  return `allocation:${selection.id}`;
20
20
  case 'root':
21
- return `root:${selection.index}`;
21
+ return `root:${selection.key}`;
22
22
  case 'culled':
23
- return `culled:${selection.index}`;
23
+ return `culled:${selection.id}`;
24
24
  case 'segment':
25
25
  return `segment:${selection.index}`;
26
26
  }
27
27
  }
28
- function createPassGraphScene(snapshot, groupsEnabled, expandedGroupPaths) {
28
+ function createFrameFlowScene(snapshot, groupsEnabled, expandedGroupPaths, showResourceDeclarations) {
29
29
  const interaction = createMutableInteractionIndex();
30
30
  const segmentByNodeId = executionSegmentByNodeId(snapshot);
31
31
  const groupsById = new Map(snapshot.debugGroups.map((group) => [group.id, group]));
@@ -34,8 +34,21 @@ function createPassGraphScene(snapshot, groupsEnabled, expandedGroupPaths) {
34
34
  const hasVisibleAncestors = (group) => group.ancestorIds
35
35
  .slice(0, -1)
36
36
  .every((id) => isExpanded(groupsById.get(id)));
37
+ const retainedIds = new Set(snapshot.nodes.map((node) => node.id));
38
+ const usedResourceIds = new Set([
39
+ ...snapshot.accessEdges.filter((access) => retainedIds.has(access.nodeId)).map((access) => access.resource.id),
40
+ ...snapshot.roots.flatMap((root) => root.resource ? [root.resource.id] : []),
41
+ ]);
42
+ const resources = showResourceDeclarations ? snapshot.resources.filter((resource) => usedResourceIds.has(resource.id)) : [];
43
+ const populatedGroupIds = new Set();
44
+ for (const item of [...snapshot.nodes, ...resources]) {
45
+ for (const id of groupsById.get(item.debugGroupId ?? '')?.ancestorIds ?? [])
46
+ populatedGroupIds.add(id);
47
+ }
48
+ const visibleResources = resources.filter((resource) => !useGroups || resource.debugGroupId === undefined
49
+ || groupsById.get(resource.debugGroupId).ancestorIds.every((id) => isExpanded(groupsById.get(id))));
37
50
  const includedGroups = useGroups
38
- ? snapshot.debugGroups.filter((group) => group.summary.retainedNodeCount > 0 && hasVisibleAncestors(group))
51
+ ? snapshot.debugGroups.filter((group) => populatedGroupIds.has(group.id) && hasVisibleAncestors(group))
39
52
  : [];
40
53
  const includedGroupIds = new Set(includedGroups.map((group) => group.id));
41
54
  const representedNodeIdsByGroupId = new Map();
@@ -64,6 +77,7 @@ function createPassGraphScene(snapshot, groupsEnabled, expandedGroupPaths) {
64
77
  .map((child) => graphGroupElementId(child.pathKey)),
65
78
  ...(visiblePassesByGroupId.get(group.id) ?? [])
66
79
  .map((node) => passElementId(node.id)),
80
+ ...visibleResources.filter((resource) => resource.debugGroupId === group.id).map((resource) => resourceElementId(resource.id)),
67
81
  ];
68
82
  const node = {
69
83
  id,
@@ -71,8 +85,8 @@ function createPassGraphScene(snapshot, groupsEnabled, expandedGroupPaths) {
71
85
  groupId: group.id,
72
86
  groupPathKey: group.pathKey,
73
87
  label: createGroupLabel(group, collapsed),
74
- overviewLabel: `${collapsed ? '▸' : '▾'} ${group.label}`,
75
- title: createGroupTitle(group),
88
+ overviewLabel: `${collapsed ? '▸' : '▾'} ${group.label.replace(/\s+/g, ' ').trim()}`,
89
+ title: createGroupTitle(group, snapshot),
76
90
  parentId: group.parentId !== undefined && includedGroupIds.has(group.parentId)
77
91
  ? graphGroupElementId(groupsById.get(group.parentId).pathKey)
78
92
  : undefined,
@@ -131,174 +145,132 @@ function createPassGraphScene(snapshot, groupsEnabled, expandedGroupPaths) {
131
145
  }
132
146
  for (const node of snapshot.nodes) {
133
147
  const representative = representativeByNodeId.get(node.id);
134
- setSelectionElements(interaction, { kind: 'node', id: node.id }, [representative], [representative]);
148
+ setSelectionElements(interaction, { kind: 'node', id: node.id }, [representative]);
135
149
  }
136
150
  if (useGroups) {
137
151
  for (const group of snapshot.debugGroups) {
138
- if (includedGroupIds.has(group.id))
152
+ if (includedGroupIds.has(group.id) || !populatedGroupIds.has(group.id))
139
153
  continue;
140
154
  const collapsedAncestorId = group.ancestorIds.find((id) => !isExpanded(groupsById.get(id)));
141
155
  if (collapsedAncestorId !== undefined) {
142
156
  const representative = graphGroupElementId(groupsById.get(collapsedAncestorId).pathKey);
143
- setSelectionElements(interaction, { kind: 'group', pathKey: group.pathKey }, [representative], [representative]);
157
+ setSelectionElements(interaction, { kind: 'group', pathKey: group.pathKey }, [representative]);
144
158
  }
145
159
  }
146
160
  }
147
- const dependencyEdges = useGroups
148
- ? createFoldedDependencyEdges(snapshot.edges, representativeByNodeId)
149
- : snapshot.edges.map((edge, index) => createDependencySceneEdge(edge, passElementId(edge.fromNodeId), passElementId(edge.toNodeId), [edge], index));
150
- for (const edge of dependencyEdges) {
151
- const selection = { kind: 'resource', id: edge.resourceId };
152
- interaction.selectionByElementId.set(edge.id, selection);
153
- appendSelectionElement(interaction.primaryElementIdsBySelection, selection, edge.id);
154
- appendSelectionElement(interaction.relatedElementIdsBySelection, selection, edge.id);
155
- interaction.semanticReferencesByElementId.set(edge.id, {
156
- nodeIds: unique(edge.underlyingDependencies.flatMap((dependency) => [dependency.fromNodeId, dependency.toNodeId])),
157
- groupPathKeys: [],
158
- resourceIds: [edge.resourceId],
159
- accessIds: [],
160
- dependencies: edge.underlyingDependencies,
161
- });
161
+ const representativeByResourceId = new Map();
162
+ for (const resource of resources) {
163
+ const collapsed = useGroups ? groupsById.get(resource.debugGroupId ?? '')?.ancestorIds.find((id) => !isExpanded(groupsById.get(id))) : undefined;
164
+ const representative = collapsed ? graphGroupElementId(groupsById.get(collapsed).pathKey) : resourceElementId(resource.id);
165
+ representativeByResourceId.set(resource.id, representative);
162
166
  }
163
- return finalizeScene('passes', [...groupNodes, ...passNodes], dependencyEdges, interaction);
164
- }
165
- function createResourceGraphScene(snapshot) {
166
- const interaction = createMutableInteractionIndex();
167
- const segmentByNodeId = executionSegmentByNodeId(snapshot);
168
- const accessesByResourceId = indexAccessesByResourceId(snapshot.accessEdges);
169
- const passNodes = snapshot.nodes.map((node) => {
170
- const segment = segmentByNodeId.get(node.id);
171
- const sceneNode = {
172
- id: passElementId(node.id),
173
- kind: 'pass',
174
- nodeId: node.id,
175
- passKind: node.kind,
176
- executionSegmentIndex: segment?.index,
177
- gpuDurationMicros: node.gpuDurationMicros,
178
- label: formatGraphNodeLabel(`${segmentLabel(segment)} ${labelNode(node)}`),
179
- overviewLabel: shortGraphLabel(labelNode(node)),
180
- title: createNodeTitle(node, segment, snapshot),
181
- };
182
- registerElement(interaction, sceneNode.id, { kind: 'node', id: node.id }, emptyReferences({ nodeIds: [node.id] }));
183
- return sceneNode;
184
- });
185
- const culledNodes = snapshot.culledNodes.map((culled, index) => {
186
- const sceneNode = {
187
- id: culledPassElementId(index, culled.node.id),
188
- kind: 'culled-pass',
189
- culledIndex: index,
190
- nodeId: culled.node.id,
191
- passKind: culled.node.kind,
192
- reason: culled.reason,
193
- label: `${formatGraphNodeLabel(labelNode(culled.node))}\n(culled)`,
194
- overviewLabel: `${shortGraphLabel(labelNode(culled.node))} (culled)`,
195
- title: createCulledNodeTitle(culled),
196
- };
197
- registerElement(interaction, sceneNode.id, { kind: 'culled', index }, emptyReferences({ nodeIds: [culled.node.id] }));
198
- return sceneNode;
199
- });
200
- const resourceNodes = snapshot.resources.map((resource) => {
201
- const sceneNode = {
202
- id: resourceElementId(resource.id),
203
- kind: 'resource',
204
- resourceId: resource.id,
205
- resourceKind: resource.kind,
206
- label: formatGraphResourceLabel(labelResource(resource)),
207
- overviewLabel: shortGraphLabel(labelResource(resource)),
208
- title: createResourceTitle(resource, snapshot, accessesByResourceId.get(resource.id) ?? []),
209
- };
210
- registerElement(interaction, sceneNode.id, { kind: 'resource', id: resource.id }, emptyReferences({ resourceIds: [resource.id] }));
211
- return sceneNode;
212
- });
213
- const culledNodeIds = new Map(snapshot.culledNodes.map((culled, index) => [
214
- culled.node.id,
215
- culledPassElementId(index, culled.node.id),
216
- ]));
217
- const accessEdges = snapshot.accessEdges.map((access) => {
218
- const passId = culledNodeIds.get(access.nodeId) ?? passElementId(access.nodeId);
219
- const resourceId = resourceElementId(access.resource.id);
220
- const edge = {
221
- id: accessElementId(access.accessId, access.resource.id),
222
- kind: 'access',
223
- from: access.mode === 'write' ? passId : resourceId,
224
- to: access.mode === 'write' ? resourceId : passId,
225
- label: access.access,
226
- title: createAccessTitle(access),
227
- resourceId: access.resource.id,
228
- accessId: access.accessId,
229
- accessMode: access.mode,
230
- access: access.access,
231
- dashed: culledNodeIds.has(access.nodeId),
167
+ const resourceNodes = visibleResources.map((resource) => {
168
+ const origin = resource.origin === 'transient' ? 'Created' : resource.origin === 'imported' ? 'Imported' : 'Surface';
169
+ const node = {
170
+ id: resourceElementId(resource.id), kind: 'resource', resourceId: resource.id, resourceKind: resource.kind,
171
+ label: origin + ' · ' + (resource.kind === 'buffer' ? 'Buffer' : 'Texture') + '\n' + formatGraphResourceLabel(labelResource(resource)),
172
+ overviewLabel: origin + '\n' + formatGraphResourceLabel(shortGraphLabel(labelResource(resource)), 1),
173
+ title: createResourceTitle(resource, snapshot, snapshot.accessesByResourceId.get(resource.id) ?? [])
174
+ + '\nDeclaration entrance, not a complete initial-content provenance graph.',
175
+ parentId: useGroups && resource.debugGroupId ? graphGroupElementId(groupsById.get(resource.debugGroupId).pathKey) : undefined,
232
176
  };
233
- const selection = { kind: 'resource', id: access.resource.id };
234
- interaction.selectionByElementId.set(edge.id, selection);
235
- appendSelectionElement(interaction.relatedElementIdsBySelection, selection, edge.id);
236
- interaction.semanticReferencesByElementId.set(edge.id, {
237
- nodeIds: [access.nodeId],
238
- groupPathKeys: [],
239
- resourceIds: [access.resource.id],
240
- accessIds: [access.accessId],
241
- dependencies: [],
242
- });
243
- return edge;
177
+ registerElement(interaction, node.id, { kind: 'resource', id: resource.id }, emptyReferences({ resourceIds: [resource.id] }));
178
+ interaction.resourceElementIdsByResourceId.set(resource.id, [node.id]);
179
+ return node;
244
180
  });
245
- return finalizeScene('resources', [...passNodes, ...culledNodes, ...resourceNodes], accessEdges, interaction);
246
- }
247
- export function indexAccessesByResourceId(accesses) {
248
- const result = new Map();
249
- for (const access of accesses) {
250
- const resourceAccesses = result.get(access.resource.id);
251
- if (resourceAccesses) {
252
- resourceAccesses.push(access);
253
- }
254
- else {
255
- result.set(access.resource.id, [access]);
256
- }
257
- }
258
- return result;
259
- }
260
- function createFoldedDependencyEdges(dependencies, representativeByNodeId) {
261
- const dependenciesByKey = new Map();
262
- for (const dependency of dependencies) {
263
- const from = representativeByNodeId.get(dependency.fromNodeId);
264
- const to = representativeByNodeId.get(dependency.toNodeId);
181
+ const rootNodes = [];
182
+ const aggregates = new Map();
183
+ const addRelation = (from, to, resourceId, relation) => {
265
184
  if (from === to)
185
+ return;
186
+ const key = JSON.stringify([from, to, resourceId]);
187
+ const entry = aggregates.get(key) ?? { from, to, resourceId, relations: [] };
188
+ entry.relations.push(relation);
189
+ aggregates.set(key, entry);
190
+ };
191
+ for (const dependency of snapshot.edges) {
192
+ addRelation(representativeByNodeId.get(dependency.fromNodeId), representativeByNodeId.get(dependency.toNodeId), dependency.resource.id, { role: dependency.kind, nodeIds: [dependency.fromNodeId, dependency.toNodeId], dependency });
193
+ }
194
+ for (const access of showResourceDeclarations ? declarationEntrances(snapshot.nodes, snapshot.accessEdges, snapshot.edges) : []) {
195
+ addRelation(representativeByResourceId.get(access.resource.id), representativeByNodeId.get(access.nodeId), access.resource.id, { role: 'declaration', nodeIds: [access.nodeId] });
196
+ }
197
+ const rootOccurrences = new Map();
198
+ const rangesByRootFamily = new Map();
199
+ for (const root of snapshot.roots) {
200
+ if (!root.resource)
201
+ continue;
202
+ const family = JSON.stringify([root.resource.id, root.reason]);
203
+ const ranges = rangesByRootFamily.get(family) ?? new Set();
204
+ ranges.add(root.key);
205
+ rangesByRootFamily.set(family, ranges);
206
+ }
207
+ for (const root of snapshot.roots) {
208
+ if (!root.resource) {
209
+ if (root.nodeId) {
210
+ const representative = representativeByNodeId.get(root.nodeId);
211
+ setSelectionElements(interaction, { kind: 'root', key: root.key }, [representative]);
212
+ }
266
213
  continue;
267
- const key = JSON.stringify([from, to, dependency.resource.id]);
268
- const existing = dependenciesByKey.get(key);
269
- if (existing) {
270
- existing.dependencies.push(dependency);
271
214
  }
272
- else {
273
- dependenciesByKey.set(key, { from, to, resource: dependency.resource, dependencies: [dependency] });
215
+ const occurrence = rootOccurrences.get(root.key) ?? 0;
216
+ rootOccurrences.set(root.key, occurrence + 1);
217
+ const id = 'root:' + root.key + (occurrence ? ':' + occurrence : '');
218
+ const resource = snapshot.resourceById.get(root.resource.id);
219
+ const rangeLabel = root.range?.kind === 'buffer' ? `bytes ${root.range.offset}–${root.range.offset + root.range.size}`
220
+ : 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';
221
+ const rangeSummary = root.range?.kind === 'texture'
222
+ ? 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('; ')
223
+ : rangeLabel;
224
+ const reasonLabel = root.reason.split('-').map((word) => word[0].toUpperCase() + word.slice(1)).join(' ');
225
+ const disambiguate = rangesByRootFamily.get(JSON.stringify([resource.id, root.reason])).size > 1;
226
+ const initialLabel = !showResourceDeclarations && root.resolution?.usesInitialContents
227
+ ? '\n' + (root.resolution.producerNodeIds.length ? 'With initial contents' : 'Initial contents only') : '';
228
+ rootNodes.push({
229
+ id, kind: 'root', rootKey: root.key, resourceId: resource.id, resourceKind: resource.kind,
230
+ label: reasonLabel + '\n' + formatGraphResourceLabel(labelResource(resource))
231
+ + (disambiguate ? '\n' + formatGraphResourceLabel(rangeSummary, 1) : '') + initialLabel,
232
+ overviewLabel: reasonLabel + '\n' + formatGraphResourceLabel(shortGraphLabel(labelResource(resource)), 1) + initialLabel,
233
+ title: labelResource(resource) + '\n' + root.reason + '\n' + rangeLabel
234
+ + (root.resolution ? '\nProducers: ' + (root.resolution.producerNodeIds.join(', ') || 'none') + '\nInitial contents: ' + root.resolution.usesInitialContents : '\nOutput sources unavailable in Legacy capture.'),
235
+ });
236
+ registerElement(interaction, id, { kind: 'root', key: root.key }, emptyReferences({ resourceIds: [resource.id], nodeIds: root.resolution?.producerNodeIds ?? [] }));
237
+ for (const nodeId of root.resolution?.producerNodeIds ?? []) {
238
+ addRelation(representativeByNodeId.get(nodeId), id, resource.id, { role: 'output-producer', nodeIds: [nodeId], rootKey: root.key });
274
239
  }
240
+ if (showResourceDeclarations && root.resolution?.usesInitialContents)
241
+ addRelation(representativeByResourceId.get(resource.id), id, resource.id, { role: 'output-initial', nodeIds: [], rootKey: root.key });
275
242
  }
276
- return [...dependenciesByKey.values()].map((entry, index) => createDependencySceneEdge(entry.dependencies.find((dependency) => dependency.kind === 'value') ?? entry.dependencies[0], entry.from, entry.to, entry.dependencies, index));
277
- }
278
- function createDependencySceneEdge(dependency, from, to, underlyingDependencies, occurrence) {
279
- const dependencyKind = underlyingDependencies.some((edge) => edge.kind === 'value') ? 'value' : 'ordering';
280
- return {
281
- id: `dependency:${JSON.stringify([from, to, dependency.resource.id, occurrence])}`,
282
- kind: 'dependency',
283
- from,
284
- to,
285
- title: `${labelResource(dependency.resource)}${underlyingDependencies.length > 1
286
- ? `\n${underlyingDependencies.length} folded dependencies`
287
- : ''}`,
288
- resourceId: dependency.resource.id,
289
- dependencyKind,
290
- underlyingDependencies,
291
- underlyingDependencyCount: underlyingDependencies.length,
292
- };
243
+ const edges = [...aggregates.entries()].map(([key, entry]) => {
244
+ const underlyingDependencies = entry.relations.flatMap((relation) => relation.dependency ? [relation.dependency] : []);
245
+ const edge = {
246
+ id: 'flow:' + key, ...entry,
247
+ kind: entry.relations.every((relation) => relation.role === 'ordering') ? 'ordering' : 'flow',
248
+ title: labelResource(snapshot.resourceById.get(entry.resourceId)) + '\n'
249
+ + unique(entry.relations.map((relation) => relation.role)).join(' · '),
250
+ underlyingDependencies, underlyingDependencyCount: underlyingDependencies.length,
251
+ };
252
+ const selection = { kind: 'resource', id: entry.resourceId };
253
+ registerElement(interaction, edge.id, selection, emptyReferences({
254
+ nodeIds: unique(entry.relations.flatMap((relation) => relation.nodeIds)), resourceIds: [entry.resourceId], dependencies: underlyingDependencies,
255
+ }));
256
+ const resourceElements = interaction.resourceElementIdsByResourceId.get(entry.resourceId) ?? [];
257
+ resourceElements.push(edge.id);
258
+ interaction.resourceElementIdsByResourceId.set(entry.resourceId, resourceElements);
259
+ return edge;
260
+ });
261
+ for (const [resourceId, elements] of interaction.resourceElementIdsByResourceId) {
262
+ const selection = { kind: 'resource', id: resourceId };
263
+ setSelectionElements(interaction, selection, elements);
264
+ interaction.hoverElementIdsBySelection.set(selectionKey(selection), [...elements]);
265
+ }
266
+ return finalizeScene([...groupNodes, ...passNodes, ...resourceNodes, ...rootNodes], edges, interaction);
293
267
  }
294
- function finalizeScene(mode, nodes, edges, interaction) {
268
+ function finalizeScene(nodes, edges, interaction) {
295
269
  const topologyKey = JSON.stringify({
296
- mode,
297
270
  nodes: nodes.map((node) => [node.id, node.kind, node.parentId, node.kind === 'group' ? node.collapsed : undefined]),
298
271
  edges: edges.map((edge) => [edge.id, edge.kind, edge.from, edge.to]),
299
272
  });
300
273
  const contentKey = JSON.stringify({
301
- mode,
302
274
  nodes: nodes.map((node) => [
303
275
  node.id,
304
276
  node.kind,
@@ -306,8 +278,8 @@ function finalizeScene(mode, nodes, edges, interaction) {
306
278
  node.label,
307
279
  node.overviewLabel,
308
280
  node.title,
309
- node.kind === 'pass' || node.kind === 'culled-pass' ? node.passKind : undefined,
310
- node.kind === 'resource' ? node.resourceKind : undefined,
281
+ node.kind === 'pass' ? node.passKind : undefined,
282
+ (node.kind === 'resource' || node.kind === 'root') ? node.resourceKind : undefined,
311
283
  node.kind === 'group' ? node.collapsed : undefined,
312
284
  node.kind === 'group' ? node.depthBand : undefined,
313
285
  node.kind === 'group' ? node.culledNodeCount > 0 : undefined,
@@ -317,15 +289,11 @@ function finalizeScene(mode, nodes, edges, interaction) {
317
289
  edge.kind,
318
290
  edge.from,
319
291
  edge.to,
320
- edge.label,
321
292
  edge.title,
322
- edge.kind === 'dependency' ? edge.dependencyKind : undefined,
323
- edge.kind === 'access' ? edge.accessMode : undefined,
324
- edge.kind === 'access' ? edge.dashed : undefined,
293
+ edge.relations,
325
294
  ]),
326
295
  });
327
296
  return {
328
- mode,
329
297
  nodes,
330
298
  edges,
331
299
  topologyKey,
@@ -337,25 +305,19 @@ function createMutableInteractionIndex() {
337
305
  return {
338
306
  selectionByElementId: new Map(),
339
307
  primaryElementIdsBySelection: new Map(),
340
- relatedElementIdsBySelection: new Map(),
308
+ hoverElementIdsBySelection: new Map(),
309
+ resourceElementIdsByResourceId: new Map(),
341
310
  semanticReferencesByElementId: new Map(),
342
311
  };
343
312
  }
344
313
  function registerElement(interaction, elementId, selection, references) {
345
314
  interaction.selectionByElementId.set(elementId, selection);
346
- setSelectionElements(interaction, selection, [elementId], [elementId]);
315
+ setSelectionElements(interaction, selection, [elementId]);
316
+ interaction.hoverElementIdsBySelection.set(selectionKey(selection), [elementId]);
347
317
  interaction.semanticReferencesByElementId.set(elementId, references);
348
318
  }
349
- function setSelectionElements(interaction, selection, primary, related) {
319
+ function setSelectionElements(interaction, selection, primary) {
350
320
  interaction.primaryElementIdsBySelection.set(selectionKey(selection), [...primary]);
351
- interaction.relatedElementIdsBySelection.set(selectionKey(selection), [...related]);
352
- }
353
- function appendSelectionElement(map, selection, elementId) {
354
- const key = selectionKey(selection);
355
- const elementIds = map.get(key) ?? [];
356
- if (!elementIds.includes(elementId))
357
- elementIds.push(elementId);
358
- map.set(key, elementIds);
359
321
  }
360
322
  function emptyReferences(overrides) {
361
323
  return {
@@ -370,20 +332,56 @@ function emptyReferences(overrides) {
370
332
  function passElementId(nodeId) {
371
333
  return `pass:${nodeId}`;
372
334
  }
373
- function culledPassElementId(index, nodeId) {
374
- return `culled-pass:${index}:${nodeId}`;
375
- }
376
335
  function resourceElementId(resourceId) {
377
336
  return `resource:${resourceId}`;
378
337
  }
379
- function accessElementId(accessId, resourceId) {
380
- return `access:${accessId}:${resourceId}`;
381
- }
382
338
  function formatGraphNodeLabel(label) {
383
339
  return formatGraphLabel(label, 20, 3);
384
340
  }
385
- function formatGraphResourceLabel(label) {
386
- return formatGraphLabel(label, 18, 3);
341
+ function formatGraphResourceLabel(label, maxLines = 2) {
342
+ let remaining = [...label.replace(/\s+/g, ' ').trim()];
343
+ // Reserve conservative monospace cells for CJK/emoji fallback glyphs as well.
344
+ const cells = (glyph) => /\p{Mark}/u.test(glyph) ? 0 : glyph.codePointAt(0) > 255 ? 2 : 1;
345
+ const take = (limit) => {
346
+ let width = 0;
347
+ let count = 0;
348
+ for (const glyph of remaining) {
349
+ if (width + cells(glyph) > limit)
350
+ break;
351
+ width += cells(glyph);
352
+ count++;
353
+ }
354
+ return count;
355
+ };
356
+ const lines = [];
357
+ while (remaining.length) {
358
+ let count = take(18);
359
+ if (count === remaining.length) {
360
+ lines.push(remaining.join(''));
361
+ break;
362
+ }
363
+ if (lines.length === maxLines - 1) {
364
+ lines.push(remaining.slice(0, take(17)).join('').trimEnd() + '…');
365
+ break;
366
+ }
367
+ // Prefer a readable namespace/path boundary over splitting a word.
368
+ let boundary = 0;
369
+ for (const separators of [/[./]/u, /[_\-\s]/u]) {
370
+ for (let index = count - 1; index > 0; index--) {
371
+ if (separators.test(remaining[index])) {
372
+ boundary = index + 1;
373
+ break;
374
+ }
375
+ }
376
+ if (boundary)
377
+ break;
378
+ }
379
+ if (boundary)
380
+ count = boundary;
381
+ lines.push(remaining.slice(0, count).join('').trimEnd());
382
+ remaining = remaining.slice(count);
383
+ }
384
+ return lines.join('\n');
387
385
  }
388
386
  function formatGraphLabel(label, maxLineLength, maxLines) {
389
387
  if (label.length <= maxLineLength)
@@ -468,23 +466,28 @@ function shortGraphLabel(label) {
468
466
  }
469
467
  function createGroupLabel(group, collapsed) {
470
468
  const summary = group.summary;
471
- const gpu = summary.timingEligibleNodeCount === 0
472
- ? 'no timed passes'
473
- : ${(summary.gpuWorkDurationMicros / 1000).toFixed(3)} ms ${summary.timedNodeCount}/${summary.timingEligibleNodeCount}`;
474
- return `${collapsed ? '▸' : '▾'} ${group.label} · ${summary.retainedNodeCount} retained · ${summary.culledNodeCount} culled · ${gpu}`;
469
+ const parts = [
470
+ `${collapsed ? '▸' : '▾'} ${group.label.replace(/\s+/g, ' ').trim()}`,
471
+ `${summary.retainedNodeCount} nodes` + (summary.culledNodeCount ? ` · ${summary.culledNodeCount} culled` : ''),
472
+ ];
473
+ return parts.join(collapsed ? '\n' : ' · ');
475
474
  }
476
475
  function createNodeTitle(node, segment, snapshot) {
476
+ const gpu = node.kind === 'external-submission' ? 'Opaque'
477
+ : node.kind !== 'render' && node.kind !== 'compute' ? 'Not applicable'
478
+ : node.gpuDurationMicros === undefined ? 'Not collected' : `${formatGpuDuration(node)} ms`;
477
479
  return [
478
480
  labelNode(node),
479
481
  `kind: ${node.kind}`,
480
482
  `group: ${debugGroupPathForId(node.debugGroupId, snapshot)}`,
481
483
  `segment: ${segment ? `${segment.index}:${segment.kind}` : '-'}`,
482
- `gpu: ${node.kind === 'external-submission' ? 'opaque' : `${formatGpuDuration(node)} ms`}`,
484
+ `CPU duration: ${node.cpuDurationMicros === undefined ? 'Not collected' : (node.cpuDurationMicros / 1000).toFixed(3) + ' ms'}`,
485
+ `GPU duration: ${gpu}`,
483
486
  `reads: ${node.reads.map((access) => labelResource(access.resource)).join(', ') || '-'}`,
484
487
  `writes: ${node.writes.map((access) => labelResource(access.resource)).join(', ') || '-'}`,
485
488
  ].join('\n');
486
489
  }
487
- function createGroupTitle(group) {
490
+ function createGroupTitle(group, snapshot) {
488
491
  const summary = group.summary;
489
492
  return [
490
493
  group.path.join(' / '),
@@ -493,19 +496,11 @@ function createGroupTitle(group) {
493
496
  `inputs: ${summary.inputResources.map(labelResource).join(', ') || '-'}`,
494
497
  `outputs: ${summary.outputResources.map(labelResource).join(', ') || '-'}`,
495
498
  `transient registered/accessed: ${summary.registeredTransientResourceCount}/${summary.accessedTransientResourceCount}`,
496
- `physical allocations: ${summary.physicalAllocationCount}`,
499
+ `physical allocations: ${snapshot.protocol.memory.allocationReport.status === 'available' ? summary.physicalAllocationCount : 'Not collected'}`,
497
500
  `segments: ${summary.executionSegmentCount}`,
498
501
  `opaque: ${summary.externalSubmissionCount}`,
499
- GPU work: ${(summary.gpuWorkDurationMicros / 1000).toFixed(3)} ms (${summary.timedNodeCount}/${summary.timingEligibleNodeCount})`,
500
- ].join('\n');
501
- }
502
- function createCulledNodeTitle(culled) {
503
- return [
504
- `${labelNode(culled.node)} (culled)`,
505
- `kind: ${culled.node.kind}`,
506
- `reason: ${culled.reason}`,
507
- `reads: ${culled.node.reads.map((access) => labelResource(access.resource)).join(', ') || '-'}`,
508
- `writes: ${culled.node.writes.map((access) => labelResource(access.resource)).join(', ') || '-'}`,
502
+ `CPU pass sum: ${summary.cpuTimedNodeCount ? (summary.cpuWorkDurationMicros / 1000).toFixed(3) + ' ms' : 'Not collected'} (${summary.cpuTimedNodeCount}/${summary.retainedNodeCount})`,
503
+ `GPU pass sum: ${formatMeasuredGpuWork(summary.gpuWorkDurationMicros, summary.timedNodeCount, summary.timingEligibleNodeCount)}`,
509
504
  ].join('\n');
510
505
  }
511
506
  function createResourceTitle(resource, snapshot, accesses) {
@@ -520,18 +515,6 @@ function createResourceTitle(resource, snapshot, accesses) {
520
515
  `writes: ${accesses.filter((access) => access.mode === 'write').length}`,
521
516
  ].join('\n');
522
517
  }
523
- function createAccessTitle(access) {
524
- return [
525
- `#${access.accessId} ${access.mode}`,
526
- labelResource(access.resource),
527
- `access: ${access.access}`,
528
- access.mode === 'write' ? `contents: ${access.contents}` : undefined,
529
- access.mode === 'write' ? `result: ${access.producesValue ? 'produced' : 'discarded'}` : undefined,
530
- access.textureViewId === undefined ? undefined : `texture view: #${access.textureViewId}`,
531
- 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,
532
- access.bufferRange ? `buffer: ${access.bufferRange.offset}+${access.bufferRange.size ?? 'end'}` : undefined,
533
- ].filter(Boolean).join('\n');
534
- }
535
518
  function debugGroupPathForId(groupId, snapshot) {
536
519
  if (groupId === undefined)
537
520
  return '-';