@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
@@ -1,8 +1,7 @@
1
1
  import { formatGpuDuration, labelNode, labelResource } from "./panelDomHelpers.js";
2
+ import { declarationEntrances } from "./debugCaptureModel.js";
2
3
  export function createGraphScene(snapshot, options) {
3
- return options.mode === 'resources'
4
- ? createResourceGraphScene(snapshot)
5
- : createPassGraphScene(snapshot, options.groupsEnabled, options.expandedGroupPaths);
4
+ return createFrameFlowScene(snapshot, options.groupsEnabled, options.expandedGroupPaths);
6
5
  }
7
6
  export function graphGroupElementId(pathKey) {
8
7
  return `group:${pathKey}`;
@@ -18,14 +17,14 @@ export function selectionKey(selection) {
18
17
  case 'allocation':
19
18
  return `allocation:${selection.id}`;
20
19
  case 'root':
21
- return `root:${selection.index}`;
20
+ return `root:${selection.key}`;
22
21
  case 'culled':
23
22
  return `culled:${selection.index}`;
24
23
  case 'segment':
25
24
  return `segment:${selection.index}`;
26
25
  }
27
26
  }
28
- function createPassGraphScene(snapshot, groupsEnabled, expandedGroupPaths) {
27
+ function createFrameFlowScene(snapshot, groupsEnabled, expandedGroupPaths) {
29
28
  const interaction = createMutableInteractionIndex();
30
29
  const segmentByNodeId = executionSegmentByNodeId(snapshot);
31
30
  const groupsById = new Map(snapshot.debugGroups.map((group) => [group.id, group]));
@@ -34,8 +33,21 @@ function createPassGraphScene(snapshot, groupsEnabled, expandedGroupPaths) {
34
33
  const hasVisibleAncestors = (group) => group.ancestorIds
35
34
  .slice(0, -1)
36
35
  .every((id) => isExpanded(groupsById.get(id)));
36
+ const retainedIds = new Set(snapshot.nodes.map((node) => node.id));
37
+ const usedResourceIds = new Set([
38
+ ...snapshot.accessEdges.filter((access) => retainedIds.has(access.nodeId)).map((access) => access.resource.id),
39
+ ...snapshot.roots.flatMap((root) => root.resource ? [root.resource.id] : []),
40
+ ]);
41
+ const resources = snapshot.resources.filter((resource) => usedResourceIds.has(resource.id));
42
+ const populatedGroupIds = new Set();
43
+ for (const item of [...snapshot.nodes, ...resources]) {
44
+ for (const id of groupsById.get(item.debugGroupId ?? '')?.ancestorIds ?? [])
45
+ populatedGroupIds.add(id);
46
+ }
47
+ const visibleResources = resources.filter((resource) => !useGroups || resource.debugGroupId === undefined
48
+ || groupsById.get(resource.debugGroupId).ancestorIds.every((id) => isExpanded(groupsById.get(id))));
37
49
  const includedGroups = useGroups
38
- ? snapshot.debugGroups.filter((group) => group.summary.retainedNodeCount > 0 && hasVisibleAncestors(group))
50
+ ? snapshot.debugGroups.filter((group) => populatedGroupIds.has(group.id) && hasVisibleAncestors(group))
39
51
  : [];
40
52
  const includedGroupIds = new Set(includedGroups.map((group) => group.id));
41
53
  const representedNodeIdsByGroupId = new Map();
@@ -64,6 +76,7 @@ function createPassGraphScene(snapshot, groupsEnabled, expandedGroupPaths) {
64
76
  .map((child) => graphGroupElementId(child.pathKey)),
65
77
  ...(visiblePassesByGroupId.get(group.id) ?? [])
66
78
  .map((node) => passElementId(node.id)),
79
+ ...visibleResources.filter((resource) => resource.debugGroupId === group.id).map((resource) => resourceElementId(resource.id)),
67
80
  ];
68
81
  const node = {
69
82
  id,
@@ -131,7 +144,7 @@ function createPassGraphScene(snapshot, groupsEnabled, expandedGroupPaths) {
131
144
  }
132
145
  for (const node of snapshot.nodes) {
133
146
  const representative = representativeByNodeId.get(node.id);
134
- setSelectionElements(interaction, { kind: 'node', id: node.id }, [representative], [representative]);
147
+ setSelectionElements(interaction, { kind: 'node', id: node.id }, [representative]);
135
148
  }
136
149
  if (useGroups) {
137
150
  for (const group of snapshot.debugGroups) {
@@ -140,165 +153,121 @@ function createPassGraphScene(snapshot, groupsEnabled, expandedGroupPaths) {
140
153
  const collapsedAncestorId = group.ancestorIds.find((id) => !isExpanded(groupsById.get(id)));
141
154
  if (collapsedAncestorId !== undefined) {
142
155
  const representative = graphGroupElementId(groupsById.get(collapsedAncestorId).pathKey);
143
- setSelectionElements(interaction, { kind: 'group', pathKey: group.pathKey }, [representative], [representative]);
156
+ setSelectionElements(interaction, { kind: 'group', pathKey: group.pathKey }, [representative]);
144
157
  }
145
158
  }
146
159
  }
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
- });
160
+ const representativeByResourceId = new Map();
161
+ for (const resource of resources) {
162
+ const collapsed = useGroups ? groupsById.get(resource.debugGroupId ?? '')?.ancestorIds.find((id) => !isExpanded(groupsById.get(id))) : undefined;
163
+ const representative = collapsed ? graphGroupElementId(groupsById.get(collapsed).pathKey) : resourceElementId(resource.id);
164
+ representativeByResourceId.set(resource.id, representative);
162
165
  }
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),
166
+ const resourceNodes = visibleResources.map((resource) => {
167
+ const origin = resource.origin === 'transient' ? 'Created' : resource.origin === 'imported' ? 'Imported' : 'Surface';
168
+ const node = {
169
+ id: resourceElementId(resource.id), kind: 'resource', resourceId: resource.id, resourceKind: resource.kind,
170
+ label: origin + ' · ' + (resource.kind === 'buffer' ? 'Buffer' : 'Texture') + '\n' + formatGraphResourceLabel(labelResource(resource)),
171
+ overviewLabel: origin + '\n' + formatGraphResourceLabel(shortGraphLabel(labelResource(resource))),
172
+ title: createResourceTitle(resource, snapshot, snapshot.accessesByResourceId.get(resource.id) ?? [])
173
+ + '\nDeclaration entrance, not a complete initial-content provenance graph.',
174
+ parentId: useGroups && resource.debugGroupId ? graphGroupElementId(groupsById.get(resource.debugGroupId).pathKey) : undefined,
232
175
  };
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;
176
+ registerElement(interaction, node.id, { kind: 'resource', id: resource.id }, emptyReferences({ resourceIds: [resource.id] }));
177
+ interaction.resourceElementIdsByResourceId.set(resource.id, [node.id]);
178
+ return node;
244
179
  });
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);
180
+ const rootNodes = [];
181
+ const aggregates = new Map();
182
+ const addRelation = (from, to, resourceId, relation) => {
265
183
  if (from === to)
184
+ return;
185
+ const key = JSON.stringify([from, to, resourceId]);
186
+ const entry = aggregates.get(key) ?? { from, to, resourceId, relations: [] };
187
+ entry.relations.push(relation);
188
+ aggregates.set(key, entry);
189
+ };
190
+ for (const dependency of snapshot.edges) {
191
+ addRelation(representativeByNodeId.get(dependency.fromNodeId), representativeByNodeId.get(dependency.toNodeId), dependency.resource.id, { role: dependency.kind, nodeIds: [dependency.fromNodeId, dependency.toNodeId], dependency });
192
+ }
193
+ for (const access of declarationEntrances(snapshot.nodes, snapshot.accessEdges, snapshot.edges)) {
194
+ addRelation(representativeByResourceId.get(access.resource.id), representativeByNodeId.get(access.nodeId), access.resource.id, { role: 'declaration', nodeIds: [access.nodeId] });
195
+ }
196
+ const rootOccurrences = new Map();
197
+ const rangesByRootFamily = new Map();
198
+ for (const root of snapshot.roots) {
199
+ if (!root.resource)
200
+ continue;
201
+ const family = JSON.stringify([root.resource.id, root.reason]);
202
+ const ranges = rangesByRootFamily.get(family) ?? new Set();
203
+ ranges.add(root.key);
204
+ rangesByRootFamily.set(family, ranges);
205
+ }
206
+ for (const root of snapshot.roots) {
207
+ if (!root.resource) {
208
+ if (root.nodeId) {
209
+ const representative = representativeByNodeId.get(root.nodeId);
210
+ setSelectionElements(interaction, { kind: 'root', key: root.key }, [representative]);
211
+ }
266
212
  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
213
  }
272
- else {
273
- dependenciesByKey.set(key, { from, to, resource: dependency.resource, dependencies: [dependency] });
214
+ const occurrence = rootOccurrences.get(root.key) ?? 0;
215
+ rootOccurrences.set(root.key, occurrence + 1);
216
+ const id = 'root:' + root.key + (occurrence ? ':' + occurrence : '');
217
+ const resource = snapshot.resourceById.get(root.resource.id);
218
+ const rangeLabel = root.range?.kind === 'buffer' ? `bytes ${root.range.offset}–${root.range.offset + root.range.size}`
219
+ : 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';
220
+ const rangeSummary = root.range?.kind === 'texture'
221
+ ? 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('; ')
222
+ : rangeLabel;
223
+ const reasonLabel = root.reason.split('-').map((word) => word[0].toUpperCase() + word.slice(1)).join(' ');
224
+ const disambiguate = rangesByRootFamily.get(JSON.stringify([resource.id, root.reason])).size > 1;
225
+ rootNodes.push({
226
+ id, kind: 'root', rootKey: root.key, resourceId: resource.id, resourceKind: resource.kind,
227
+ label: reasonLabel + '\n' + formatGraphResourceLabel(labelResource(resource))
228
+ + (disambiguate ? '\n' + formatGraphResourceLabel(rangeSummary) : ''),
229
+ overviewLabel: reasonLabel + '\n' + formatGraphResourceLabel(shortGraphLabel(labelResource(resource))),
230
+ title: labelResource(resource) + '\n' + root.reason + '\n' + rangeLabel
231
+ + (root.resolution ? '\nProducers: ' + (root.resolution.producerNodeIds.join(', ') || 'none') + '\nInitial contents: ' + root.resolution.usesInitialContents : '\nOutput sources unavailable in Legacy capture.'),
232
+ });
233
+ registerElement(interaction, id, { kind: 'root', key: root.key }, emptyReferences({ resourceIds: [resource.id], nodeIds: root.resolution?.producerNodeIds ?? [] }));
234
+ for (const nodeId of root.resolution?.producerNodeIds ?? []) {
235
+ addRelation(representativeByNodeId.get(nodeId), id, resource.id, { role: 'output-producer', nodeIds: [nodeId], rootKey: root.key });
274
236
  }
237
+ if (root.resolution?.usesInitialContents)
238
+ addRelation(representativeByResourceId.get(resource.id), id, resource.id, { role: 'output-initial', nodeIds: [], rootKey: root.key });
275
239
  }
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
- };
240
+ const edges = [...aggregates.entries()].map(([key, entry]) => {
241
+ const underlyingDependencies = entry.relations.flatMap((relation) => relation.dependency ? [relation.dependency] : []);
242
+ const edge = {
243
+ id: 'flow:' + key, ...entry,
244
+ kind: entry.relations.every((relation) => relation.role === 'ordering') ? 'ordering' : 'flow',
245
+ title: labelResource(snapshot.resourceById.get(entry.resourceId)) + '\n'
246
+ + unique(entry.relations.map((relation) => relation.role)).join(' · '),
247
+ underlyingDependencies, underlyingDependencyCount: underlyingDependencies.length,
248
+ };
249
+ const selection = { kind: 'resource', id: entry.resourceId };
250
+ registerElement(interaction, edge.id, selection, emptyReferences({
251
+ nodeIds: unique(entry.relations.flatMap((relation) => relation.nodeIds)), resourceIds: [entry.resourceId], dependencies: underlyingDependencies,
252
+ }));
253
+ const resourceElements = interaction.resourceElementIdsByResourceId.get(entry.resourceId) ?? [];
254
+ resourceElements.push(edge.id);
255
+ interaction.resourceElementIdsByResourceId.set(entry.resourceId, resourceElements);
256
+ return edge;
257
+ });
258
+ for (const [resourceId, elements] of interaction.resourceElementIdsByResourceId) {
259
+ const selection = { kind: 'resource', id: resourceId };
260
+ setSelectionElements(interaction, selection, elements);
261
+ interaction.hoverElementIdsBySelection.set(selectionKey(selection), [...elements]);
262
+ }
263
+ return finalizeScene([...groupNodes, ...passNodes, ...resourceNodes, ...rootNodes], edges, interaction);
293
264
  }
294
- function finalizeScene(mode, nodes, edges, interaction) {
265
+ function finalizeScene(nodes, edges, interaction) {
295
266
  const topologyKey = JSON.stringify({
296
- mode,
297
267
  nodes: nodes.map((node) => [node.id, node.kind, node.parentId, node.kind === 'group' ? node.collapsed : undefined]),
298
268
  edges: edges.map((edge) => [edge.id, edge.kind, edge.from, edge.to]),
299
269
  });
300
270
  const contentKey = JSON.stringify({
301
- mode,
302
271
  nodes: nodes.map((node) => [
303
272
  node.id,
304
273
  node.kind,
@@ -306,8 +275,8 @@ function finalizeScene(mode, nodes, edges, interaction) {
306
275
  node.label,
307
276
  node.overviewLabel,
308
277
  node.title,
309
- node.kind === 'pass' || node.kind === 'culled-pass' ? node.passKind : undefined,
310
- node.kind === 'resource' ? node.resourceKind : undefined,
278
+ node.kind === 'pass' ? node.passKind : undefined,
279
+ (node.kind === 'resource' || node.kind === 'root') ? node.resourceKind : undefined,
311
280
  node.kind === 'group' ? node.collapsed : undefined,
312
281
  node.kind === 'group' ? node.depthBand : undefined,
313
282
  node.kind === 'group' ? node.culledNodeCount > 0 : undefined,
@@ -317,15 +286,11 @@ function finalizeScene(mode, nodes, edges, interaction) {
317
286
  edge.kind,
318
287
  edge.from,
319
288
  edge.to,
320
- edge.label,
321
289
  edge.title,
322
- edge.kind === 'dependency' ? edge.dependencyKind : undefined,
323
- edge.kind === 'access' ? edge.accessMode : undefined,
324
- edge.kind === 'access' ? edge.dashed : undefined,
290
+ edge.relations,
325
291
  ]),
326
292
  });
327
293
  return {
328
- mode,
329
294
  nodes,
330
295
  edges,
331
296
  topologyKey,
@@ -337,25 +302,19 @@ function createMutableInteractionIndex() {
337
302
  return {
338
303
  selectionByElementId: new Map(),
339
304
  primaryElementIdsBySelection: new Map(),
340
- relatedElementIdsBySelection: new Map(),
305
+ hoverElementIdsBySelection: new Map(),
306
+ resourceElementIdsByResourceId: new Map(),
341
307
  semanticReferencesByElementId: new Map(),
342
308
  };
343
309
  }
344
310
  function registerElement(interaction, elementId, selection, references) {
345
311
  interaction.selectionByElementId.set(elementId, selection);
346
- setSelectionElements(interaction, selection, [elementId], [elementId]);
312
+ setSelectionElements(interaction, selection, [elementId]);
313
+ interaction.hoverElementIdsBySelection.set(selectionKey(selection), [elementId]);
347
314
  interaction.semanticReferencesByElementId.set(elementId, references);
348
315
  }
349
- function setSelectionElements(interaction, selection, primary, related) {
316
+ function setSelectionElements(interaction, selection, primary) {
350
317
  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
318
  }
360
319
  function emptyReferences(overrides) {
361
320
  return {
@@ -370,20 +329,28 @@ function emptyReferences(overrides) {
370
329
  function passElementId(nodeId) {
371
330
  return `pass:${nodeId}`;
372
331
  }
373
- function culledPassElementId(index, nodeId) {
374
- return `culled-pass:${index}:${nodeId}`;
375
- }
376
332
  function resourceElementId(resourceId) {
377
333
  return `resource:${resourceId}`;
378
334
  }
379
- function accessElementId(accessId, resourceId) {
380
- return `access:${accessId}:${resourceId}`;
381
- }
382
335
  function formatGraphNodeLabel(label) {
383
336
  return formatGraphLabel(label, 20, 3);
384
337
  }
385
338
  function formatGraphResourceLabel(label) {
386
- return formatGraphLabel(label, 18, 3);
339
+ const singleLine = label.replace(/\s+/g, ' ').trim();
340
+ // Reserve conservative monospace cells for CJK/emoji fallback glyphs as well.
341
+ const glyphs = [...singleLine];
342
+ const cells = (glyph) => /\p{Mark}/u.test(glyph) ? 0 : glyph.codePointAt(0) > 255 ? 2 : 1;
343
+ if (glyphs.reduce((sum, glyph) => sum + cells(glyph), 0) <= 18)
344
+ return singleLine;
345
+ let result = '';
346
+ let width = 0;
347
+ for (const glyph of glyphs) {
348
+ if (width + cells(glyph) > 17)
349
+ break;
350
+ result += glyph;
351
+ width += cells(glyph);
352
+ }
353
+ return result + '…';
387
354
  }
388
355
  function formatGraphLabel(label, maxLineLength, maxLines) {
389
356
  if (label.length <= maxLineLength)
@@ -499,15 +466,6 @@ function createGroupTitle(group) {
499
466
  `Σ GPU work: ${(summary.gpuWorkDurationMicros / 1000).toFixed(3)} ms (${summary.timedNodeCount}/${summary.timingEligibleNodeCount})`,
500
467
  ].join('\n');
501
468
  }
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(', ') || '-'}`,
509
- ].join('\n');
510
- }
511
469
  function createResourceTitle(resource, snapshot, accesses) {
512
470
  return [
513
471
  labelResource(resource),
@@ -520,18 +478,6 @@ function createResourceTitle(resource, snapshot, accesses) {
520
478
  `writes: ${accesses.filter((access) => access.mode === 'write').length}`,
521
479
  ].join('\n');
522
480
  }
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
481
  function debugGroupPathForId(groupId, snapshot) {
536
482
  if (groupId === undefined)
537
483
  return '-';