@zenfg/inspector 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -0
  3. package/dist/FrameGraphInspector.d.ts +153 -0
  4. package/dist/FrameGraphInspector.d.ts.map +1 -0
  5. package/dist/FrameGraphInspector.js +580 -0
  6. package/dist/FrameGraphInspector.js.map +1 -0
  7. package/dist/debugCaptureModel.d.ts +166 -0
  8. package/dist/debugCaptureModel.d.ts.map +1 -0
  9. package/dist/debugCaptureModel.js +364 -0
  10. package/dist/debugCaptureModel.js.map +1 -0
  11. package/dist/index.d.ts +17 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +15 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/panelAliasAnalysis.d.ts +20 -0
  16. package/dist/panelAliasAnalysis.d.ts.map +1 -0
  17. package/dist/panelAliasAnalysis.js +113 -0
  18. package/dist/panelAliasAnalysis.js.map +1 -0
  19. package/dist/panelCytoscapeGraphRenderer.d.ts +71 -0
  20. package/dist/panelCytoscapeGraphRenderer.d.ts.map +1 -0
  21. package/dist/panelCytoscapeGraphRenderer.js +559 -0
  22. package/dist/panelCytoscapeGraphRenderer.js.map +1 -0
  23. package/dist/panelDiagnosticsView.d.ts +22 -0
  24. package/dist/panelDiagnosticsView.d.ts.map +1 -0
  25. package/dist/panelDiagnosticsView.js +123 -0
  26. package/dist/panelDiagnosticsView.js.map +1 -0
  27. package/dist/panelDomHelpers.d.ts +26 -0
  28. package/dist/panelDomHelpers.d.ts.map +1 -0
  29. package/dist/panelDomHelpers.js +129 -0
  30. package/dist/panelDomHelpers.js.map +1 -0
  31. package/dist/panelGraphLayout.d.ts +28 -0
  32. package/dist/panelGraphLayout.d.ts.map +1 -0
  33. package/dist/panelGraphLayout.js +223 -0
  34. package/dist/panelGraphLayout.js.map +1 -0
  35. package/dist/panelGraphRenderer.d.ts +20 -0
  36. package/dist/panelGraphRenderer.d.ts.map +1 -0
  37. package/dist/panelGraphRenderer.js +2 -0
  38. package/dist/panelGraphRenderer.js.map +1 -0
  39. package/dist/panelGraphScene.d.ts +98 -0
  40. package/dist/panelGraphScene.d.ts.map +1 -0
  41. package/dist/panelGraphScene.js +559 -0
  42. package/dist/panelGraphScene.js.map +1 -0
  43. package/dist/panelGraphView.d.ts +9 -0
  44. package/dist/panelGraphView.d.ts.map +1 -0
  45. package/dist/panelGraphView.js +88 -0
  46. package/dist/panelGraphView.js.map +1 -0
  47. package/dist/panelGraphVisuals.d.ts +38 -0
  48. package/dist/panelGraphVisuals.d.ts.map +1 -0
  49. package/dist/panelGraphVisuals.js +330 -0
  50. package/dist/panelGraphVisuals.js.map +1 -0
  51. package/dist/panelIcons.d.ts +4 -0
  52. package/dist/panelIcons.d.ts.map +1 -0
  53. package/dist/panelIcons.js +43 -0
  54. package/dist/panelIcons.js.map +1 -0
  55. package/dist/panelInspectorView.d.ts +33 -0
  56. package/dist/panelInspectorView.d.ts.map +1 -0
  57. package/dist/panelInspectorView.js +357 -0
  58. package/dist/panelInspectorView.js.map +1 -0
  59. package/dist/panelMemoryView.d.ts +21 -0
  60. package/dist/panelMemoryView.d.ts.map +1 -0
  61. package/dist/panelMemoryView.js +134 -0
  62. package/dist/panelMemoryView.js.map +1 -0
  63. package/dist/panelPassesView.d.ts +31 -0
  64. package/dist/panelPassesView.d.ts.map +1 -0
  65. package/dist/panelPassesView.js +183 -0
  66. package/dist/panelPassesView.js.map +1 -0
  67. package/dist/panelResourcesView.d.ts +19 -0
  68. package/dist/panelResourcesView.d.ts.map +1 -0
  69. package/dist/panelResourcesView.js +89 -0
  70. package/dist/panelResourcesView.js.map +1 -0
  71. package/dist/panelSelection.d.ts +5 -0
  72. package/dist/panelSelection.d.ts.map +1 -0
  73. package/dist/panelSelection.js +88 -0
  74. package/dist/panelSelection.js.map +1 -0
  75. package/dist/panelTypes.d.ts +40 -0
  76. package/dist/panelTypes.d.ts.map +1 -0
  77. package/dist/panelTypes.js +2 -0
  78. package/dist/panelTypes.js.map +1 -0
  79. package/dist/panelVisualTheme.d.ts +76 -0
  80. package/dist/panelVisualTheme.d.ts.map +1 -0
  81. package/dist/panelVisualTheme.js +39 -0
  82. package/dist/panelVisualTheme.js.map +1 -0
  83. package/dist/panelWorkbenchHelpers.d.ts +34 -0
  84. package/dist/panelWorkbenchHelpers.d.ts.map +1 -0
  85. package/dist/panelWorkbenchHelpers.js +162 -0
  86. package/dist/panelWorkbenchHelpers.js.map +1 -0
  87. package/dist/panelWorkbenchView.d.ts +77 -0
  88. package/dist/panelWorkbenchView.d.ts.map +1 -0
  89. package/dist/panelWorkbenchView.js +409 -0
  90. package/dist/panelWorkbenchView.js.map +1 -0
  91. package/dist/query.d.ts +11 -0
  92. package/dist/query.d.ts.map +1 -0
  93. package/dist/query.js +21 -0
  94. package/dist/query.js.map +1 -0
  95. package/dist/styles.d.ts +8 -0
  96. package/dist/styles.d.ts.map +1 -0
  97. package/dist/styles.js +1073 -0
  98. package/dist/styles.js.map +1 -0
  99. package/package.json +29 -0
  100. package/src/FrameGraphInspector.ts +632 -0
  101. package/src/debugCaptureModel.ts +583 -0
  102. package/src/index.ts +21 -0
  103. package/src/panelAliasAnalysis.ts +164 -0
  104. package/src/panelCytoscapeGraphRenderer.ts +617 -0
  105. package/src/panelDiagnosticsView.ts +160 -0
  106. package/src/panelDomHelpers.ts +151 -0
  107. package/src/panelGraphLayout.ts +281 -0
  108. package/src/panelGraphRenderer.ts +21 -0
  109. package/src/panelGraphScene.ts +787 -0
  110. package/src/panelGraphView.ts +108 -0
  111. package/src/panelGraphVisuals.ts +364 -0
  112. package/src/panelIcons.ts +56 -0
  113. package/src/panelInspectorView.ts +377 -0
  114. package/src/panelMemoryView.ts +154 -0
  115. package/src/panelPassesView.ts +222 -0
  116. package/src/panelResourcesView.ts +115 -0
  117. package/src/panelSelection.ts +99 -0
  118. package/src/panelTypes.ts +28 -0
  119. package/src/panelVisualTheme.ts +39 -0
  120. package/src/panelWorkbenchHelpers.ts +216 -0
  121. package/src/panelWorkbenchView.ts +465 -0
  122. package/src/query.ts +22 -0
  123. package/src/styles.ts +1076 -0
@@ -0,0 +1,787 @@
1
+ import type {
2
+ FrameGraphDebugAccessEdge,
3
+ FrameGraphDebugEdge,
4
+ FrameGraphDebugGroup,
5
+ FrameGraphDebugNode,
6
+ FrameGraphDebugResource,
7
+ FrameGraphDebugResourceRef,
8
+ FrameGraphDebugViewModel,
9
+ } from './debugCaptureModel.ts';
10
+ import { formatGpuDuration, labelNode, labelResource } from './panelDomHelpers.ts';
11
+ import type { GraphViewMode, Selection } from './panelTypes.ts';
12
+
13
+ export type GraphSceneElementId = string;
14
+
15
+ type GraphSceneNodeBase = {
16
+ readonly id: GraphSceneElementId;
17
+ readonly label: string;
18
+ readonly overviewLabel: string;
19
+ readonly title: string;
20
+ readonly parentId?: GraphSceneElementId;
21
+ };
22
+
23
+ export type PassSceneNode = GraphSceneNodeBase & {
24
+ readonly kind: 'pass';
25
+ readonly nodeId: string;
26
+ readonly passKind: FrameGraphDebugNode['kind'];
27
+ readonly executionSegmentIndex?: number;
28
+ readonly gpuDurationMicros?: number;
29
+ };
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
+ export type GroupSceneNode = GraphSceneNodeBase & {
40
+ readonly kind: 'group';
41
+ readonly groupId: string;
42
+ readonly groupPathKey: string;
43
+ readonly collapsed: boolean;
44
+ readonly depthBand: 0 | 1;
45
+ readonly childNodeIds: readonly GraphSceneElementId[];
46
+ readonly representedNodeIds: readonly string[];
47
+ readonly retainedNodeCount: number;
48
+ readonly culledNodeCount: number;
49
+ readonly gpuWorkDurationMicros: number;
50
+ readonly timedNodeCount: number;
51
+ readonly timingEligibleNodeCount: number;
52
+ };
53
+
54
+ export type ResourceSceneNode = GraphSceneNodeBase & {
55
+ readonly kind: 'resource';
56
+ readonly resourceId: string;
57
+ readonly resourceKind: FrameGraphDebugResource['kind'];
58
+ };
59
+
60
+ export type GraphSceneNode = PassSceneNode | CulledPassSceneNode | GroupSceneNode | ResourceSceneNode;
61
+
62
+ type GraphSceneEdgeBase = {
63
+ readonly id: GraphSceneElementId;
64
+ readonly from: GraphSceneElementId;
65
+ readonly to: GraphSceneElementId;
66
+ readonly label?: string;
67
+ readonly title: string;
68
+ readonly resourceId: string;
69
+ };
70
+
71
+ export type DependencySceneEdge = GraphSceneEdgeBase & {
72
+ readonly kind: 'dependency';
73
+ readonly dependencyKind: FrameGraphDebugEdge['kind'];
74
+ readonly underlyingDependencies: readonly FrameGraphDebugEdge[];
75
+ readonly underlyingDependencyCount: number;
76
+ };
77
+
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
+ export type GraphSemanticReferences = {
89
+ readonly nodeIds: readonly string[];
90
+ readonly groupPathKeys: readonly string[];
91
+ readonly resourceIds: readonly string[];
92
+ readonly accessIds: readonly string[];
93
+ readonly dependencies: readonly FrameGraphDebugEdge[];
94
+ };
95
+
96
+ export type GraphSceneInteractionIndex = {
97
+ readonly selectionByElementId: ReadonlyMap<GraphSceneElementId, Selection>;
98
+ readonly primaryElementIdsBySelection: ReadonlyMap<string, readonly GraphSceneElementId[]>;
99
+ readonly relatedElementIdsBySelection: ReadonlyMap<string, readonly GraphSceneElementId[]>;
100
+ readonly semanticReferencesByElementId: ReadonlyMap<GraphSceneElementId, GraphSemanticReferences>;
101
+ };
102
+
103
+ export type GraphScene = {
104
+ readonly mode: GraphViewMode;
105
+ readonly nodes: readonly GraphSceneNode[];
106
+ readonly edges: readonly GraphSceneEdge[];
107
+ readonly topologyKey: string;
108
+ readonly contentKey: string;
109
+ readonly interaction: GraphSceneInteractionIndex;
110
+ };
111
+
112
+ export type CreateGraphSceneOptions = {
113
+ readonly mode: GraphViewMode;
114
+ readonly groupsEnabled: boolean;
115
+ readonly expandedGroupPaths: ReadonlySet<string>;
116
+ };
117
+
118
+ type MutableInteractionIndex = {
119
+ readonly selectionByElementId: Map<GraphSceneElementId, Selection>;
120
+ readonly primaryElementIdsBySelection: Map<string, GraphSceneElementId[]>;
121
+ readonly relatedElementIdsBySelection: Map<string, GraphSceneElementId[]>;
122
+ readonly semanticReferencesByElementId: Map<GraphSceneElementId, GraphSemanticReferences>;
123
+ };
124
+
125
+ export function createGraphScene(
126
+ snapshot: FrameGraphDebugViewModel,
127
+ options: CreateGraphSceneOptions,
128
+ ): GraphScene {
129
+ return options.mode === 'resources'
130
+ ? createResourceGraphScene(snapshot)
131
+ : createPassGraphScene(snapshot, options.groupsEnabled, options.expandedGroupPaths);
132
+ }
133
+
134
+ export function graphGroupElementId(pathKey: string): GraphSceneElementId {
135
+ return `group:${pathKey}`;
136
+ }
137
+
138
+ export function selectionKey(selection: Selection): string {
139
+ switch (selection.kind) {
140
+ case 'node':
141
+ return `node:${selection.id}`;
142
+ case 'group':
143
+ return `group:${selection.pathKey}`;
144
+ case 'resource':
145
+ return `resource:${selection.id}`;
146
+ case 'allocation':
147
+ return `allocation:${selection.id}`;
148
+ case 'root':
149
+ return `root:${selection.index}`;
150
+ case 'culled':
151
+ return `culled:${selection.index}`;
152
+ case 'segment':
153
+ return `segment:${selection.index}`;
154
+ }
155
+ }
156
+
157
+ function createPassGraphScene(
158
+ snapshot: FrameGraphDebugViewModel,
159
+ groupsEnabled: boolean,
160
+ expandedGroupPaths: ReadonlySet<string>,
161
+ ): GraphScene {
162
+ const interaction = createMutableInteractionIndex();
163
+ const segmentByNodeId = executionSegmentByNodeId(snapshot);
164
+ const groupsById = new Map(snapshot.debugGroups.map((group) => [group.id, group]));
165
+ const useGroups = groupsEnabled && snapshot.debugGroups.length > 0;
166
+ const isExpanded = (group: FrameGraphDebugGroup) => expandedGroupPaths.has(group.pathKey);
167
+ const hasVisibleAncestors = (group: FrameGraphDebugGroup) => group.ancestorIds
168
+ .slice(0, -1)
169
+ .every((id) => isExpanded(groupsById.get(id)!));
170
+ const includedGroups = useGroups
171
+ ? snapshot.debugGroups.filter((group) => group.summary.retainedNodeCount > 0 && hasVisibleAncestors(group))
172
+ : [];
173
+ const includedGroupIds = new Set(includedGroups.map((group) => group.id));
174
+ const representedNodeIdsByGroupId = new Map<string, string[]>();
175
+ for (const node of snapshot.nodes) {
176
+ const group = node.debugGroupId === undefined ? undefined : groupsById.get(node.debugGroupId);
177
+ for (const groupId of group?.ancestorIds ?? []) {
178
+ const represented = representedNodeIdsByGroupId.get(groupId) ?? [];
179
+ represented.push(node.id);
180
+ representedNodeIdsByGroupId.set(groupId, represented);
181
+ }
182
+ }
183
+ const visiblePasses = snapshot.nodes.filter((node) => {
184
+ if (!useGroups || node.debugGroupId === undefined) return true;
185
+ return groupsById.get(node.debugGroupId)!.ancestorIds.every((id) => isExpanded(groupsById.get(id)!));
186
+ });
187
+ const visiblePassIds = new Set(visiblePasses.map((node) => node.id));
188
+ const includedGroupsByParentId = indexByOptionalId(includedGroups, (group) => group.parentId);
189
+ const visiblePassesByGroupId = indexByOptionalId(visiblePasses, (node) => node.debugGroupId);
190
+
191
+ const groupNodes: GroupSceneNode[] = includedGroups.map((group) => {
192
+ const id = graphGroupElementId(group.pathKey);
193
+ const collapsed = !isExpanded(group);
194
+ const representedNodeIds = representedNodeIdsByGroupId.get(group.id) ?? [];
195
+ const childNodeIds = collapsed ? [] : [
196
+ ...(includedGroupsByParentId.get(group.id) ?? [])
197
+ .map((child) => graphGroupElementId(child.pathKey)),
198
+ ...(visiblePassesByGroupId.get(group.id) ?? [])
199
+ .map((node) => passElementId(node.id)),
200
+ ];
201
+ const node: GroupSceneNode = {
202
+ id,
203
+ kind: 'group',
204
+ groupId: group.id,
205
+ groupPathKey: group.pathKey,
206
+ label: createGroupLabel(group, collapsed),
207
+ overviewLabel: `${collapsed ? '▸' : '▾'} ${group.label}`,
208
+ title: createGroupTitle(group),
209
+ parentId: group.parentId !== undefined && includedGroupIds.has(group.parentId)
210
+ ? graphGroupElementId(groupsById.get(group.parentId)!.pathKey)
211
+ : undefined,
212
+ collapsed,
213
+ depthBand: group.ancestorIds.length % 2 as 0 | 1,
214
+ childNodeIds,
215
+ representedNodeIds,
216
+ retainedNodeCount: group.summary.retainedNodeCount,
217
+ culledNodeCount: group.summary.culledNodeCount,
218
+ gpuWorkDurationMicros: group.summary.gpuWorkDurationMicros,
219
+ timedNodeCount: group.summary.timedNodeCount,
220
+ timingEligibleNodeCount: group.summary.timingEligibleNodeCount,
221
+ };
222
+ const selection: Selection = { kind: 'group', pathKey: group.pathKey };
223
+ registerElement(interaction, node.id, selection, {
224
+ nodeIds: representedNodeIds,
225
+ groupPathKeys: [group.pathKey],
226
+ resourceIds: [
227
+ ...group.summary.inputResources.map((resource) => resource.id),
228
+ ...group.summary.outputResources.map((resource) => resource.id),
229
+ ],
230
+ accessIds: [],
231
+ dependencies: [],
232
+ });
233
+ return node;
234
+ });
235
+
236
+ const passNodes: PassSceneNode[] = visiblePasses.map((node) => {
237
+ const segment = segmentByNodeId.get(node.id);
238
+ const sceneNode: PassSceneNode = {
239
+ id: passElementId(node.id),
240
+ kind: 'pass',
241
+ nodeId: node.id,
242
+ passKind: node.kind,
243
+ executionSegmentIndex: segment?.index,
244
+ gpuDurationMicros: node.gpuDurationMicros,
245
+ label: formatGraphNodeLabel(`${segmentLabel(segment)} ${labelNode(node)}`),
246
+ overviewLabel: shortGraphLabel(labelNode(node)),
247
+ title: createNodeTitle(node, segment, snapshot),
248
+ parentId: useGroups && node.debugGroupId !== undefined
249
+ ? graphGroupElementId(groupsById.get(node.debugGroupId)!.pathKey)
250
+ : undefined,
251
+ };
252
+ const selection: Selection = { kind: 'node', id: node.id };
253
+ registerElement(interaction, sceneNode.id, selection, emptyReferences({ nodeIds: [node.id] }));
254
+ return sceneNode;
255
+ });
256
+
257
+ const representativeByNodeId = new Map<string, GraphSceneElementId>();
258
+ for (const node of snapshot.nodes) {
259
+ if (visiblePassIds.has(node.id)) {
260
+ representativeByNodeId.set(node.id, passElementId(node.id));
261
+ continue;
262
+ }
263
+ const group = groupsById.get(node.debugGroupId!)!;
264
+ const collapsedAncestorId = group.ancestorIds.find((id) => !isExpanded(groupsById.get(id)!));
265
+ representativeByNodeId.set(node.id, graphGroupElementId(groupsById.get(collapsedAncestorId!)!.pathKey));
266
+ }
267
+
268
+ for (const node of snapshot.nodes) {
269
+ const representative = representativeByNodeId.get(node.id)!;
270
+ setSelectionElements(interaction, { kind: 'node', id: node.id }, [representative], [representative]);
271
+ }
272
+ if (useGroups) {
273
+ for (const group of snapshot.debugGroups) {
274
+ if (includedGroupIds.has(group.id)) continue;
275
+ const collapsedAncestorId = group.ancestorIds.find((id) => !isExpanded(groupsById.get(id)!));
276
+ if (collapsedAncestorId !== undefined) {
277
+ const representative = graphGroupElementId(groupsById.get(collapsedAncestorId)!.pathKey);
278
+ setSelectionElements(interaction, { kind: 'group', pathKey: group.pathKey }, [representative], [representative]);
279
+ }
280
+ }
281
+ }
282
+
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
+ });
304
+ }
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) ?? []),
363
+ };
364
+ registerElement(
365
+ interaction,
366
+ sceneNode.id,
367
+ { kind: 'resource', id: resource.id },
368
+ emptyReferences({ resourceIds: [resource.id] }),
369
+ );
370
+ return sceneNode;
371
+ });
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: [],
401
+ });
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]);
418
+ }
419
+ }
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
+ }
444
+ }
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
+ };
475
+ }
476
+
477
+ function finalizeScene(
478
+ mode: GraphViewMode,
479
+ nodes: readonly GraphSceneNode[],
480
+ edges: readonly GraphSceneEdge[],
481
+ interaction: MutableInteractionIndex,
482
+ ): GraphScene {
483
+ const topologyKey = JSON.stringify({
484
+ mode,
485
+ nodes: nodes.map((node) => [node.id, node.kind, node.parentId, node.kind === 'group' ? node.collapsed : undefined]),
486
+ edges: edges.map((edge) => [edge.id, edge.kind, edge.from, edge.to]),
487
+ });
488
+ const contentKey = JSON.stringify({
489
+ mode,
490
+ nodes: nodes.map((node) => [
491
+ node.id,
492
+ node.kind,
493
+ node.parentId,
494
+ node.label,
495
+ node.overviewLabel,
496
+ node.title,
497
+ node.kind === 'pass' || node.kind === 'culled-pass' ? node.passKind : undefined,
498
+ node.kind === 'resource' ? node.resourceKind : undefined,
499
+ node.kind === 'group' ? node.collapsed : undefined,
500
+ node.kind === 'group' ? node.depthBand : undefined,
501
+ node.kind === 'group' ? node.culledNodeCount > 0 : undefined,
502
+ ]),
503
+ edges: edges.map((edge) => [
504
+ edge.id,
505
+ edge.kind,
506
+ edge.from,
507
+ edge.to,
508
+ edge.label,
509
+ edge.title,
510
+ edge.kind === 'dependency' ? edge.dependencyKind : undefined,
511
+ edge.kind === 'access' ? edge.accessMode : undefined,
512
+ edge.kind === 'access' ? edge.dashed : undefined,
513
+ ]),
514
+ });
515
+ return {
516
+ mode,
517
+ nodes,
518
+ edges,
519
+ topologyKey,
520
+ contentKey,
521
+ interaction,
522
+ };
523
+ }
524
+
525
+ function createMutableInteractionIndex(): MutableInteractionIndex {
526
+ return {
527
+ selectionByElementId: new Map(),
528
+ primaryElementIdsBySelection: new Map(),
529
+ relatedElementIdsBySelection: new Map(),
530
+ semanticReferencesByElementId: new Map(),
531
+ };
532
+ }
533
+
534
+ function registerElement(
535
+ interaction: MutableInteractionIndex,
536
+ elementId: GraphSceneElementId,
537
+ selection: Selection,
538
+ references: GraphSemanticReferences,
539
+ ): void {
540
+ interaction.selectionByElementId.set(elementId, selection);
541
+ setSelectionElements(interaction, selection, [elementId], [elementId]);
542
+ interaction.semanticReferencesByElementId.set(elementId, references);
543
+ }
544
+
545
+ function setSelectionElements(
546
+ interaction: MutableInteractionIndex,
547
+ selection: Selection,
548
+ primary: readonly GraphSceneElementId[],
549
+ related: readonly GraphSceneElementId[],
550
+ ): void {
551
+ 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
+ }
565
+
566
+ function emptyReferences(overrides: Partial<GraphSemanticReferences>): GraphSemanticReferences {
567
+ return {
568
+ nodeIds: [],
569
+ groupPathKeys: [],
570
+ resourceIds: [],
571
+ accessIds: [],
572
+ dependencies: [],
573
+ ...overrides,
574
+ };
575
+ }
576
+
577
+ function passElementId(nodeId: string): GraphSceneElementId {
578
+ return `pass:${nodeId}`;
579
+ }
580
+
581
+ function culledPassElementId(index: number, nodeId: string): GraphSceneElementId {
582
+ return `culled-pass:${index}:${nodeId}`;
583
+ }
584
+
585
+ function resourceElementId(resourceId: string): GraphSceneElementId {
586
+ return `resource:${resourceId}`;
587
+ }
588
+
589
+ function accessElementId(accessId: string, resourceId: string): GraphSceneElementId {
590
+ return `access:${accessId}:${resourceId}`;
591
+ }
592
+
593
+ function formatGraphNodeLabel(label: string): string {
594
+ return formatGraphLabel(label, 20, 3);
595
+ }
596
+
597
+ function formatGraphResourceLabel(label: string): string {
598
+ return formatGraphLabel(label, 18, 3);
599
+ }
600
+
601
+ function formatGraphLabel(label: string, maxLineLength: number, maxLines: number): string {
602
+ if (label.length <= maxLineLength) return label;
603
+ const chunks = label.match(/[^.]+\.?/g) ?? [label];
604
+ const lines: string[] = [];
605
+ let current = '';
606
+ let truncated = false;
607
+ for (const chunk of chunks) {
608
+ const next = current ? `${current}${chunk}` : chunk;
609
+ if (current && next.length > maxLineLength) {
610
+ lines.push(current);
611
+ current = chunk;
612
+ if (lines.length === maxLines) {
613
+ truncated = true;
614
+ break;
615
+ }
616
+ continue;
617
+ }
618
+ if (!current && chunk.length > maxLineLength) {
619
+ const wrapped = splitLongGraphLabelChunk(chunk, maxLineLength);
620
+ for (const line of wrapped) {
621
+ if (lines.length === maxLines) {
622
+ truncated = true;
623
+ break;
624
+ }
625
+ lines.push(line);
626
+ }
627
+ current = '';
628
+ if (truncated) break;
629
+ continue;
630
+ }
631
+ current = next;
632
+ }
633
+ if (current && lines.length < maxLines) lines.push(current);
634
+ else if (current) truncated = true;
635
+ if (lines.length > maxLines) {
636
+ lines.length = maxLines;
637
+ truncated = true;
638
+ }
639
+ if (truncated && lines.length > 0) {
640
+ const line = lines[lines.length - 1]!;
641
+ lines[lines.length - 1] = line.length < maxLineLength
642
+ ? `${line}…`
643
+ : `${line.slice(0, Math.max(1, maxLineLength - 1))}…`;
644
+ }
645
+ return lines.join('\n');
646
+ }
647
+
648
+ function splitLongGraphLabelChunk(label: string, maxLineLength: number): string[] {
649
+ const chunks = label.match(/[^\s_\-/]+[\s_\-/]*/g) ?? [label];
650
+ const lines: string[] = [];
651
+ let current = '';
652
+ for (const chunk of chunks) {
653
+ const next = current ? `${current}${chunk}` : chunk;
654
+ if (current && next.length > maxLineLength) {
655
+ lines.push(current);
656
+ current = chunk;
657
+ } else if (!current && chunk.length > maxLineLength) {
658
+ for (let index = 0; index < chunk.length; index += maxLineLength) {
659
+ lines.push(chunk.slice(index, index + maxLineLength));
660
+ }
661
+ } else {
662
+ current = next;
663
+ }
664
+ }
665
+ if (current) lines.push(current);
666
+ return lines;
667
+ }
668
+
669
+ function shortGraphLabel(label: string): string {
670
+ const parts = label.split(/[./]/).filter(Boolean);
671
+ const last = parts[parts.length - 1] ?? label;
672
+ const short = parts.length > 1 && (/^\d+$/.test(last) || /^view#\d+$/.test(last))
673
+ ? `${parts[parts.length - 2]}.${last}`
674
+ : last;
675
+ return short.length <= 18 ? short : `${short.slice(0, 17)}…`;
676
+ }
677
+
678
+ function createGroupLabel(group: FrameGraphDebugGroup, collapsed: boolean): string {
679
+ 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}`;
684
+ }
685
+
686
+ function createNodeTitle(
687
+ node: FrameGraphDebugNode,
688
+ segment: FrameGraphDebugViewModel['executionSegments'][number] | undefined,
689
+ snapshot: FrameGraphDebugViewModel,
690
+ ): string {
691
+ return [
692
+ labelNode(node),
693
+ `kind: ${node.kind}`,
694
+ `group: ${debugGroupPathForId(node.debugGroupId, snapshot)}`,
695
+ `segment: ${segment ? `${segment.index}:${segment.kind}` : '-'}`,
696
+ `gpu: ${node.kind === 'external-submission' ? 'opaque' : `${formatGpuDuration(node)} ms`}`,
697
+ `reads: ${node.reads.map((access) => labelResource(access.resource)).join(', ') || '-'}`,
698
+ `writes: ${node.writes.map((access) => labelResource(access.resource)).join(', ') || '-'}`,
699
+ ].join('\n');
700
+ }
701
+
702
+ function createGroupTitle(group: FrameGraphDebugGroup): string {
703
+ const summary = group.summary;
704
+ return [
705
+ group.path.join(' / '),
706
+ `retained: ${summary.retainedNodeCount}`,
707
+ `culled: ${summary.culledNodeCount}`,
708
+ `inputs: ${summary.inputResources.map(labelResource).join(', ') || '-'}`,
709
+ `outputs: ${summary.outputResources.map(labelResource).join(', ') || '-'}`,
710
+ `transient registered/accessed: ${summary.registeredTransientResourceCount}/${summary.accessedTransientResourceCount}`,
711
+ `physical allocations: ${summary.physicalAllocationCount}`,
712
+ `segments: ${summary.executionSegmentCount}`,
713
+ `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(', ') || '-'}`,
725
+ ].join('\n');
726
+ }
727
+
728
+ function createResourceTitle(
729
+ resource: FrameGraphDebugResource,
730
+ snapshot: FrameGraphDebugViewModel,
731
+ accesses: readonly FrameGraphDebugAccessEdge[],
732
+ ): string {
733
+ return [
734
+ labelResource(resource),
735
+ `kind: ${resource.kind}`,
736
+ `origin: ${resource.origin}`,
737
+ `registered in: ${debugGroupPathForId(resource.debugGroupId, snapshot)}`,
738
+ `lifetime: ${resource.lifetime ? `${resource.lifetime.firstUse}-${resource.lifetime.lastUse}` : '-'}`,
739
+ `physical: ${resource.physicalResourceId ?? '-'}`,
740
+ `reads: ${accesses.filter((access) => access.mode === 'read').length}`,
741
+ `writes: ${accesses.filter((access) => access.mode === 'write').length}`,
742
+ ].join('\n');
743
+ }
744
+
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
+ function debugGroupPathForId(groupId: string | undefined, snapshot: FrameGraphDebugViewModel): string {
759
+ if (groupId === undefined) return '-';
760
+ return snapshot.debugGroups.find((group) => group.id === groupId)?.path.join(' / ') ?? `#${groupId}`;
761
+ }
762
+
763
+ function executionSegmentByNodeId(snapshot: FrameGraphDebugViewModel) {
764
+ return new Map(snapshot.executionSegments.flatMap((segment) => segment.nodeIds.map((nodeId) => [nodeId, segment] as const)));
765
+ }
766
+
767
+ function segmentLabel(segment: FrameGraphDebugViewModel['executionSegments'][number] | undefined): string {
768
+ return segment ? `[S${segment.index}]` : '[S-]';
769
+ }
770
+
771
+ function unique<T>(values: readonly T[]): T[] {
772
+ return [...new Set(values)];
773
+ }
774
+
775
+ function indexByOptionalId<T>(
776
+ values: readonly T[],
777
+ getId: (value: T) => string | undefined,
778
+ ): ReadonlyMap<string | undefined, readonly T[]> {
779
+ const result = new Map<string | undefined, T[]>();
780
+ for (const value of values) {
781
+ const id = getId(value);
782
+ const indexed = result.get(id) ?? [];
783
+ indexed.push(value);
784
+ result.set(id, indexed);
785
+ }
786
+ return result;
787
+ }