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

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 (62) hide show
  1. package/README.md +63 -1
  2. package/dist/FrameGraphInspector.d.ts +2 -5
  3. package/dist/FrameGraphInspector.d.ts.map +1 -1
  4. package/dist/FrameGraphInspector.js +16 -38
  5. package/dist/FrameGraphInspector.js.map +1 -1
  6. package/dist/debugCaptureModel.d.ts +9 -4
  7. package/dist/debugCaptureModel.d.ts.map +1 -1
  8. package/dist/debugCaptureModel.js +36 -14
  9. package/dist/debugCaptureModel.js.map +1 -1
  10. package/dist/panelCytoscapeGraphRenderer.d.ts +3 -1
  11. package/dist/panelCytoscapeGraphRenderer.d.ts.map +1 -1
  12. package/dist/panelCytoscapeGraphRenderer.js +39 -19
  13. package/dist/panelCytoscapeGraphRenderer.js.map +1 -1
  14. package/dist/panelDiagnosticsView.js +2 -2
  15. package/dist/panelDiagnosticsView.js.map +1 -1
  16. package/dist/panelGraphLayout.d.ts.map +1 -1
  17. package/dist/panelGraphLayout.js +15 -1
  18. package/dist/panelGraphLayout.js.map +1 -1
  19. package/dist/panelGraphScene.d.ts +14 -26
  20. package/dist/panelGraphScene.d.ts.map +1 -1
  21. package/dist/panelGraphScene.js +141 -195
  22. package/dist/panelGraphScene.js.map +1 -1
  23. package/dist/panelGraphView.d.ts.map +1 -1
  24. package/dist/panelGraphView.js +31 -9
  25. package/dist/panelGraphView.js.map +1 -1
  26. package/dist/panelGraphVisuals.d.ts +7 -3
  27. package/dist/panelGraphVisuals.d.ts.map +1 -1
  28. package/dist/panelGraphVisuals.js +45 -80
  29. package/dist/panelGraphVisuals.js.map +1 -1
  30. package/dist/panelInspectorView.d.ts +5 -0
  31. package/dist/panelInspectorView.d.ts.map +1 -1
  32. package/dist/panelInspectorView.js +99 -13
  33. package/dist/panelInspectorView.js.map +1 -1
  34. package/dist/panelSelection.js +2 -2
  35. package/dist/panelSelection.js.map +1 -1
  36. package/dist/panelTypes.d.ts +8 -3
  37. package/dist/panelTypes.d.ts.map +1 -1
  38. package/dist/panelTypes.js.map +1 -1
  39. package/dist/panelVisualTheme.d.ts +21 -13
  40. package/dist/panelVisualTheme.d.ts.map +1 -1
  41. package/dist/panelVisualTheme.js +15 -7
  42. package/dist/panelVisualTheme.js.map +1 -1
  43. package/dist/panelWorkbenchHelpers.js +1 -1
  44. package/dist/panelWorkbenchHelpers.js.map +1 -1
  45. package/dist/styles.d.ts.map +1 -1
  46. package/dist/styles.js +16 -12
  47. package/dist/styles.js.map +1 -1
  48. package/package.json +2 -2
  49. package/src/FrameGraphInspector.ts +17 -39
  50. package/src/debugCaptureModel.ts +44 -19
  51. package/src/panelCytoscapeGraphRenderer.ts +38 -18
  52. package/src/panelDiagnosticsView.ts +3 -3
  53. package/src/panelGraphLayout.ts +15 -1
  54. package/src/panelGraphScene.ts +153 -279
  55. package/src/panelGraphView.ts +31 -9
  56. package/src/panelGraphVisuals.ts +46 -82
  57. package/src/panelInspectorView.ts +104 -16
  58. package/src/panelSelection.ts +2 -2
  59. package/src/panelTypes.ts +9 -3
  60. package/src/panelVisualTheme.ts +18 -7
  61. package/src/panelWorkbenchHelpers.ts +2 -2
  62. package/src/styles.ts +16 -12
@@ -4,11 +4,11 @@ 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
12
 
13
13
  export type GraphSceneElementId = string;
14
14
 
@@ -28,14 +28,6 @@ export type PassSceneNode = GraphSceneNodeBase & {
28
28
  readonly gpuDurationMicros?: number;
29
29
  };
30
30
 
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
31
  export type GroupSceneNode = GraphSceneNodeBase & {
40
32
  readonly kind: 'group';
41
33
  readonly groupId: string;
@@ -57,34 +49,29 @@ export type ResourceSceneNode = GraphSceneNodeBase & {
57
49
  readonly resourceKind: FrameGraphDebugResource['kind'];
58
50
  };
59
51
 
60
- export type GraphSceneNode = PassSceneNode | CulledPassSceneNode | GroupSceneNode | ResourceSceneNode;
52
+ export type RootSceneNode = GraphSceneNodeBase & {
53
+ readonly kind: 'root';
54
+ readonly rootKey: string;
55
+ readonly resourceId: string;
56
+ readonly resourceKind: FrameGraphDebugResource['kind'];
57
+ };
58
+ export type GraphSceneNode = PassSceneNode | GroupSceneNode | ResourceSceneNode | RootSceneNode;
61
59
 
62
60
  type GraphSceneEdgeBase = {
63
61
  readonly id: GraphSceneElementId;
64
62
  readonly from: GraphSceneElementId;
65
63
  readonly to: GraphSceneElementId;
66
- readonly label?: string;
67
64
  readonly title: string;
68
65
  readonly resourceId: string;
69
66
  };
70
67
 
71
- export type DependencySceneEdge = GraphSceneEdgeBase & {
72
- readonly kind: 'dependency';
73
- readonly dependencyKind: FrameGraphDebugEdge['kind'];
68
+ export type GraphSceneEdge = GraphSceneEdgeBase & {
69
+ readonly kind: 'flow' | 'ordering';
70
+ readonly relations: readonly GraphFlowRelation[];
74
71
  readonly underlyingDependencies: readonly FrameGraphDebugEdge[];
75
72
  readonly underlyingDependencyCount: number;
76
73
  };
77
74
 
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
75
  export type GraphSemanticReferences = {
89
76
  readonly nodeIds: readonly string[];
90
77
  readonly groupPathKeys: readonly string[];
@@ -96,12 +83,12 @@ export type GraphSemanticReferences = {
96
83
  export type GraphSceneInteractionIndex = {
97
84
  readonly selectionByElementId: ReadonlyMap<GraphSceneElementId, Selection>;
98
85
  readonly primaryElementIdsBySelection: ReadonlyMap<string, readonly GraphSceneElementId[]>;
99
- readonly relatedElementIdsBySelection: ReadonlyMap<string, readonly GraphSceneElementId[]>;
86
+ readonly hoverElementIdsBySelection: ReadonlyMap<string, readonly GraphSceneElementId[]>;
87
+ readonly resourceElementIdsByResourceId: ReadonlyMap<string, readonly GraphSceneElementId[]>;
100
88
  readonly semanticReferencesByElementId: ReadonlyMap<GraphSceneElementId, GraphSemanticReferences>;
101
89
  };
102
90
 
103
91
  export type GraphScene = {
104
- readonly mode: GraphViewMode;
105
92
  readonly nodes: readonly GraphSceneNode[];
106
93
  readonly edges: readonly GraphSceneEdge[];
107
94
  readonly topologyKey: string;
@@ -110,7 +97,6 @@ export type GraphScene = {
110
97
  };
111
98
 
112
99
  export type CreateGraphSceneOptions = {
113
- readonly mode: GraphViewMode;
114
100
  readonly groupsEnabled: boolean;
115
101
  readonly expandedGroupPaths: ReadonlySet<string>;
116
102
  };
@@ -118,7 +104,8 @@ export type CreateGraphSceneOptions = {
118
104
  type MutableInteractionIndex = {
119
105
  readonly selectionByElementId: Map<GraphSceneElementId, Selection>;
120
106
  readonly primaryElementIdsBySelection: Map<string, GraphSceneElementId[]>;
121
- readonly relatedElementIdsBySelection: Map<string, GraphSceneElementId[]>;
107
+ readonly hoverElementIdsBySelection: Map<string, GraphSceneElementId[]>;
108
+ readonly resourceElementIdsByResourceId: Map<string, GraphSceneElementId[]>;
122
109
  readonly semanticReferencesByElementId: Map<GraphSceneElementId, GraphSemanticReferences>;
123
110
  };
124
111
 
@@ -126,9 +113,7 @@ export function createGraphScene(
126
113
  snapshot: FrameGraphDebugViewModel,
127
114
  options: CreateGraphSceneOptions,
128
115
  ): GraphScene {
129
- return options.mode === 'resources'
130
- ? createResourceGraphScene(snapshot)
131
- : createPassGraphScene(snapshot, options.groupsEnabled, options.expandedGroupPaths);
116
+ return createFrameFlowScene(snapshot, options.groupsEnabled, options.expandedGroupPaths);
132
117
  }
133
118
 
134
119
  export function graphGroupElementId(pathKey: string): GraphSceneElementId {
@@ -146,7 +131,7 @@ export function selectionKey(selection: Selection): string {
146
131
  case 'allocation':
147
132
  return `allocation:${selection.id}`;
148
133
  case 'root':
149
- return `root:${selection.index}`;
134
+ return `root:${selection.key}`;
150
135
  case 'culled':
151
136
  return `culled:${selection.index}`;
152
137
  case 'segment':
@@ -154,7 +139,7 @@ export function selectionKey(selection: Selection): string {
154
139
  }
155
140
  }
156
141
 
157
- function createPassGraphScene(
142
+ function createFrameFlowScene(
158
143
  snapshot: FrameGraphDebugViewModel,
159
144
  groupsEnabled: boolean,
160
145
  expandedGroupPaths: ReadonlySet<string>,
@@ -167,8 +152,20 @@ function createPassGraphScene(
167
152
  const hasVisibleAncestors = (group: FrameGraphDebugGroup) => group.ancestorIds
168
153
  .slice(0, -1)
169
154
  .every((id) => isExpanded(groupsById.get(id)!));
155
+ const retainedIds = new Set(snapshot.nodes.map((node) => node.id));
156
+ const usedResourceIds = new Set([
157
+ ...snapshot.accessEdges.filter((access) => retainedIds.has(access.nodeId)).map((access) => access.resource.id),
158
+ ...snapshot.roots.flatMap((root) => root.resource ? [root.resource.id] : []),
159
+ ]);
160
+ const resources = snapshot.resources.filter((resource) => usedResourceIds.has(resource.id));
161
+ const populatedGroupIds = new Set<string>();
162
+ for (const item of [...snapshot.nodes, ...resources]) {
163
+ for (const id of groupsById.get(item.debugGroupId ?? '')?.ancestorIds ?? []) populatedGroupIds.add(id);
164
+ }
165
+ const visibleResources = resources.filter((resource) => !useGroups || resource.debugGroupId === undefined
166
+ || groupsById.get(resource.debugGroupId)!.ancestorIds.every((id) => isExpanded(groupsById.get(id)!)));
170
167
  const includedGroups = useGroups
171
- ? snapshot.debugGroups.filter((group) => group.summary.retainedNodeCount > 0 && hasVisibleAncestors(group))
168
+ ? snapshot.debugGroups.filter((group) => populatedGroupIds.has(group.id) && hasVisibleAncestors(group))
172
169
  : [];
173
170
  const includedGroupIds = new Set(includedGroups.map((group) => group.id));
174
171
  const representedNodeIdsByGroupId = new Map<string, string[]>();
@@ -197,6 +194,7 @@ function createPassGraphScene(
197
194
  .map((child) => graphGroupElementId(child.pathKey)),
198
195
  ...(visiblePassesByGroupId.get(group.id) ?? [])
199
196
  .map((node) => passElementId(node.id)),
197
+ ...visibleResources.filter((resource) => resource.debugGroupId === group.id).map((resource) => resourceElementId(resource.id)),
200
198
  ];
201
199
  const node: GroupSceneNode = {
202
200
  id,
@@ -267,7 +265,7 @@ function createPassGraphScene(
267
265
 
268
266
  for (const node of snapshot.nodes) {
269
267
  const representative = representativeByNodeId.get(node.id)!;
270
- setSelectionElements(interaction, { kind: 'node', id: node.id }, [representative], [representative]);
268
+ setSelectionElements(interaction, { kind: 'node', id: node.id }, [representative]);
271
269
  }
272
270
  if (useGroups) {
273
271
  for (const group of snapshot.debugGroups) {
@@ -275,218 +273,128 @@ function createPassGraphScene(
275
273
  const collapsedAncestorId = group.ancestorIds.find((id) => !isExpanded(groupsById.get(id)!));
276
274
  if (collapsedAncestorId !== undefined) {
277
275
  const representative = graphGroupElementId(groupsById.get(collapsedAncestorId)!.pathKey);
278
- setSelectionElements(interaction, { kind: 'group', pathKey: group.pathKey }, [representative], [representative]);
276
+ setSelectionElements(interaction, { kind: 'group', pathKey: group.pathKey }, [representative]);
279
277
  }
280
278
  }
281
279
  }
282
280
 
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
- });
281
+ const representativeByResourceId = new Map<string, string>();
282
+ for (const resource of resources) {
283
+ const collapsed = useGroups ? groupsById.get(resource.debugGroupId ?? '')?.ancestorIds.find((id) => !isExpanded(groupsById.get(id)!)) : undefined;
284
+ const representative = collapsed ? graphGroupElementId(groupsById.get(collapsed)!.pathKey) : resourceElementId(resource.id);
285
+ representativeByResourceId.set(resource.id, representative);
304
286
  }
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),
287
+ const resourceNodes: ResourceSceneNode[] = visibleResources.map((resource) => {
288
+ const origin = resource.origin === 'transient' ? 'Created' : resource.origin === 'imported' ? 'Imported' : 'Surface';
289
+ const node: ResourceSceneNode = {
290
+ id: resourceElementId(resource.id), kind: 'resource', resourceId: resource.id, resourceKind: resource.kind,
291
+ label: origin + ' · ' + (resource.kind === 'buffer' ? 'Buffer' : 'Texture') + '\n' + formatGraphResourceLabel(labelResource(resource)),
292
+ overviewLabel: origin + '\n' + formatGraphResourceLabel(shortGraphLabel(labelResource(resource))),
293
+ title: createResourceTitle(resource, snapshot, snapshot.accessesByResourceId.get(resource.id) ?? [])
294
+ + '\nDeclaration entrance, not a complete initial-content provenance graph.',
295
+ parentId: useGroups && resource.debugGroupId ? graphGroupElementId(groupsById.get(resource.debugGroupId)!.pathKey) : undefined,
345
296
  };
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) ?? []),
363
- };
364
- registerElement(
365
- interaction,
366
- sceneNode.id,
367
- { kind: 'resource', id: resource.id },
368
- emptyReferences({ resourceIds: [resource.id] }),
369
- );
370
- return sceneNode;
297
+ registerElement(interaction, node.id, { kind: 'resource', id: resource.id }, emptyReferences({ resourceIds: [resource.id] }));
298
+ interaction.resourceElementIdsByResourceId.set(resource.id, [node.id]);
299
+ return node;
371
300
  });
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: [],
301
+ const rootNodes: RootSceneNode[] = [];
302
+ const aggregates = new Map<string, { from: string; to: string; resourceId: string; relations: GraphFlowRelation[] }>();
303
+ const addRelation = (from: string, to: string, resourceId: string, relation: GraphFlowRelation) => {
304
+ if (from === to) return;
305
+ const key = JSON.stringify([from, to, resourceId]);
306
+ const entry = aggregates.get(key) ?? { from, to, resourceId, relations: [] };
307
+ entry.relations.push(relation);
308
+ aggregates.set(key, entry);
309
+ };
310
+ for (const dependency of snapshot.edges) {
311
+ addRelation(representativeByNodeId.get(dependency.fromNodeId)!, representativeByNodeId.get(dependency.toNodeId)!, dependency.resource.id,
312
+ { role: dependency.kind, nodeIds: [dependency.fromNodeId, dependency.toNodeId], dependency });
313
+ }
314
+ for (const access of declarationEntrances(snapshot.nodes, snapshot.accessEdges, snapshot.edges)) {
315
+ addRelation(representativeByResourceId.get(access.resource.id)!, representativeByNodeId.get(access.nodeId)!, access.resource.id,
316
+ { role: 'declaration', nodeIds: [access.nodeId] });
317
+ }
318
+ const rootOccurrences = new Map<string, number>();
319
+ const rangesByRootFamily = new Map<string, Set<string>>();
320
+ for (const root of snapshot.roots) {
321
+ if (!root.resource) continue;
322
+ const family = JSON.stringify([root.resource.id, root.reason]);
323
+ const ranges = rangesByRootFamily.get(family) ?? new Set<string>();
324
+ ranges.add(root.key);
325
+ rangesByRootFamily.set(family, ranges);
326
+ }
327
+ for (const root of snapshot.roots) {
328
+ if (!root.resource) {
329
+ if (root.nodeId) {
330
+ const representative = representativeByNodeId.get(root.nodeId)!;
331
+ setSelectionElements(interaction, { kind: 'root', key: root.key }, [representative]);
332
+ }
333
+ continue;
334
+ }
335
+ const occurrence = rootOccurrences.get(root.key) ?? 0;
336
+ rootOccurrences.set(root.key, occurrence + 1);
337
+ const id = 'root:' + root.key + (occurrence ? ':' + occurrence : '');
338
+ const resource = snapshot.resourceById.get(root.resource.id)!;
339
+ const rangeLabel = root.range?.kind === 'buffer' ? `bytes ${root.range.offset}–${root.range.offset + root.range.size}`
340
+ : 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';
341
+ const rangeSummary = root.range?.kind === 'texture'
342
+ ? 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('; ')
343
+ : rangeLabel;
344
+ const reasonLabel = root.reason.split('-').map((word) => word[0]!.toUpperCase() + word.slice(1)).join(' ');
345
+ const disambiguate = rangesByRootFamily.get(JSON.stringify([resource.id, root.reason]))!.size > 1;
346
+ rootNodes.push({
347
+ id, kind: 'root', rootKey: root.key, resourceId: resource.id, resourceKind: resource.kind,
348
+ label: reasonLabel + '\n' + formatGraphResourceLabel(labelResource(resource))
349
+ + (disambiguate ? '\n' + formatGraphResourceLabel(rangeSummary) : ''),
350
+ overviewLabel: reasonLabel + '\n' + formatGraphResourceLabel(shortGraphLabel(labelResource(resource))),
351
+ title: labelResource(resource) + '\n' + root.reason + '\n' + rangeLabel
352
+ + (root.resolution ? '\nProducers: ' + (root.resolution.producerNodeIds.join(', ') || 'none') + '\nInitial contents: ' + root.resolution.usesInitialContents : '\nOutput sources unavailable in Legacy capture.'),
401
353
  });
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]);
354
+ registerElement(interaction, id, { kind: 'root', key: root.key },
355
+ emptyReferences({ resourceIds: [resource.id], nodeIds: root.resolution?.producerNodeIds ?? [] }));
356
+ for (const nodeId of root.resolution?.producerNodeIds ?? []) {
357
+ addRelation(representativeByNodeId.get(nodeId)!, id, resource.id, { role: 'output-producer', nodeIds: [nodeId], rootKey: root.key });
418
358
  }
359
+ if (root.resolution?.usesInitialContents) addRelation(representativeByResourceId.get(resource.id)!, id, resource.id,
360
+ { role: 'output-initial', nodeIds: [], rootKey: root.key });
419
361
  }
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
- }
362
+ const edges: GraphSceneEdge[] = [...aggregates.entries()].map(([key, entry]) => {
363
+ const underlyingDependencies = entry.relations.flatMap((relation) => relation.dependency ? [relation.dependency] : []);
364
+ const edge: GraphSceneEdge = {
365
+ id: 'flow:' + key, ...entry,
366
+ kind: entry.relations.every((relation) => relation.role === 'ordering') ? 'ordering' : 'flow',
367
+ title: labelResource(snapshot.resourceById.get(entry.resourceId)!) + '\n'
368
+ + unique(entry.relations.map((relation) => relation.role)).join(' · '),
369
+ underlyingDependencies, underlyingDependencyCount: underlyingDependencies.length,
370
+ };
371
+ const selection: Selection = { kind: 'resource', id: entry.resourceId };
372
+ registerElement(interaction, edge.id, selection, emptyReferences({
373
+ nodeIds: unique(entry.relations.flatMap((relation) => relation.nodeIds)), resourceIds: [entry.resourceId], dependencies: underlyingDependencies,
374
+ }));
375
+ const resourceElements = interaction.resourceElementIdsByResourceId.get(entry.resourceId) ?? [];
376
+ resourceElements.push(edge.id);
377
+ interaction.resourceElementIdsByResourceId.set(entry.resourceId, resourceElements);
378
+ return edge;
379
+ });
380
+ for (const [resourceId, elements] of interaction.resourceElementIdsByResourceId) {
381
+ const selection: Selection = { kind: 'resource', id: resourceId };
382
+ setSelectionElements(interaction, selection, elements);
383
+ interaction.hoverElementIdsBySelection.set(selectionKey(selection), [...elements]);
444
384
  }
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
- };
385
+ return finalizeScene([...groupNodes, ...passNodes, ...resourceNodes, ...rootNodes], edges, interaction);
475
386
  }
476
387
 
477
388
  function finalizeScene(
478
- mode: GraphViewMode,
479
389
  nodes: readonly GraphSceneNode[],
480
390
  edges: readonly GraphSceneEdge[],
481
391
  interaction: MutableInteractionIndex,
482
392
  ): GraphScene {
483
393
  const topologyKey = JSON.stringify({
484
- mode,
485
394
  nodes: nodes.map((node) => [node.id, node.kind, node.parentId, node.kind === 'group' ? node.collapsed : undefined]),
486
395
  edges: edges.map((edge) => [edge.id, edge.kind, edge.from, edge.to]),
487
396
  });
488
397
  const contentKey = JSON.stringify({
489
- mode,
490
398
  nodes: nodes.map((node) => [
491
399
  node.id,
492
400
  node.kind,
@@ -494,8 +402,8 @@ function finalizeScene(
494
402
  node.label,
495
403
  node.overviewLabel,
496
404
  node.title,
497
- node.kind === 'pass' || node.kind === 'culled-pass' ? node.passKind : undefined,
498
- node.kind === 'resource' ? node.resourceKind : undefined,
405
+ node.kind === 'pass' ? node.passKind : undefined,
406
+ (node.kind === 'resource' || node.kind === 'root') ? node.resourceKind : undefined,
499
407
  node.kind === 'group' ? node.collapsed : undefined,
500
408
  node.kind === 'group' ? node.depthBand : undefined,
501
409
  node.kind === 'group' ? node.culledNodeCount > 0 : undefined,
@@ -505,15 +413,11 @@ function finalizeScene(
505
413
  edge.kind,
506
414
  edge.from,
507
415
  edge.to,
508
- edge.label,
509
416
  edge.title,
510
- edge.kind === 'dependency' ? edge.dependencyKind : undefined,
511
- edge.kind === 'access' ? edge.accessMode : undefined,
512
- edge.kind === 'access' ? edge.dashed : undefined,
417
+ edge.relations,
513
418
  ]),
514
419
  });
515
420
  return {
516
- mode,
517
421
  nodes,
518
422
  edges,
519
423
  topologyKey,
@@ -526,7 +430,8 @@ function createMutableInteractionIndex(): MutableInteractionIndex {
526
430
  return {
527
431
  selectionByElementId: new Map(),
528
432
  primaryElementIdsBySelection: new Map(),
529
- relatedElementIdsBySelection: new Map(),
433
+ hoverElementIdsBySelection: new Map(),
434
+ resourceElementIdsByResourceId: new Map(),
530
435
  semanticReferencesByElementId: new Map(),
531
436
  };
532
437
  }
@@ -538,7 +443,8 @@ function registerElement(
538
443
  references: GraphSemanticReferences,
539
444
  ): void {
540
445
  interaction.selectionByElementId.set(elementId, selection);
541
- setSelectionElements(interaction, selection, [elementId], [elementId]);
446
+ setSelectionElements(interaction, selection, [elementId]);
447
+ interaction.hoverElementIdsBySelection.set(selectionKey(selection), [elementId]);
542
448
  interaction.semanticReferencesByElementId.set(elementId, references);
543
449
  }
544
450
 
@@ -546,21 +452,8 @@ function setSelectionElements(
546
452
  interaction: MutableInteractionIndex,
547
453
  selection: Selection,
548
454
  primary: readonly GraphSceneElementId[],
549
- related: readonly GraphSceneElementId[],
550
455
  ): void {
551
456
  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
457
  }
565
458
 
566
459
  function emptyReferences(overrides: Partial<GraphSemanticReferences>): GraphSemanticReferences {
@@ -578,24 +471,28 @@ function passElementId(nodeId: string): GraphSceneElementId {
578
471
  return `pass:${nodeId}`;
579
472
  }
580
473
 
581
- function culledPassElementId(index: number, nodeId: string): GraphSceneElementId {
582
- return `culled-pass:${index}:${nodeId}`;
583
- }
584
-
585
474
  function resourceElementId(resourceId: string): GraphSceneElementId {
586
475
  return `resource:${resourceId}`;
587
476
  }
588
477
 
589
- function accessElementId(accessId: string, resourceId: string): GraphSceneElementId {
590
- return `access:${accessId}:${resourceId}`;
591
- }
592
-
593
478
  function formatGraphNodeLabel(label: string): string {
594
479
  return formatGraphLabel(label, 20, 3);
595
480
  }
596
481
 
597
482
  function formatGraphResourceLabel(label: string): string {
598
- return formatGraphLabel(label, 18, 3);
483
+ const singleLine = label.replace(/\s+/g, ' ').trim();
484
+ // Reserve conservative monospace cells for CJK/emoji fallback glyphs as well.
485
+ const glyphs = [...singleLine];
486
+ const cells = (glyph: string) => /\p{Mark}/u.test(glyph) ? 0 : glyph.codePointAt(0)! > 255 ? 2 : 1;
487
+ if (glyphs.reduce((sum, glyph) => sum + cells(glyph), 0) <= 18) return singleLine;
488
+ let result = '';
489
+ let width = 0;
490
+ for (const glyph of glyphs) {
491
+ if (width + cells(glyph) > 17) break;
492
+ result += glyph;
493
+ width += cells(glyph);
494
+ }
495
+ return result + '…';
599
496
  }
600
497
 
601
498
  function formatGraphLabel(label: string, maxLineLength: number, maxLines: number): string {
@@ -715,16 +612,6 @@ function createGroupTitle(group: FrameGraphDebugGroup): string {
715
612
  ].join('\n');
716
613
  }
717
614
 
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(', ') || '-'}`,
725
- ].join('\n');
726
- }
727
-
728
615
  function createResourceTitle(
729
616
  resource: FrameGraphDebugResource,
730
617
  snapshot: FrameGraphDebugViewModel,
@@ -742,19 +629,6 @@ function createResourceTitle(
742
629
  ].join('\n');
743
630
  }
744
631
 
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
632
  function debugGroupPathForId(groupId: string | undefined, snapshot: FrameGraphDebugViewModel): string {
759
633
  if (groupId === undefined) return '-';
760
634
  return snapshot.debugGroups.find((group) => group.id === groupId)?.path.join(' / ') ?? `#${groupId}`;