A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using UnityEditor.Rendering.Analytics;
4using UnityEditor.UIElements;
5using UnityEngine;
6using UnityEngine.Rendering.RenderGraphModule;
7using UnityEngine.Scripting.APIUpdating;
8using UnityEngine.UIElements;
9
10namespace UnityEditor.Rendering
11{
12 /// <summary>
13 /// Editor window class for the Render Graph Viewer
14 /// </summary>
15 [MovedFrom("")]
16 public partial class RenderGraphViewer : EditorWindow
17 {
18 static partial class Names
19 {
20 public const string kCaptureButton = "capture-button";
21 public const string kCurrentGraphDropdown = "current-graph-dropdown";
22 public const string kCurrentExecutionDropdown = "current-execution-dropdown";
23 public const string kPassFilterField = "pass-filter-field";
24 public const string kResourceFilterField = "resource-filter-field";
25 public const string kContentContainer = "content-container";
26 public const string kMainContainer = "main-container";
27 public const string kPassList = "pass-list";
28 public const string kPassListScrollView = "pass-list-scroll-view";
29 public const string kResourceListScrollView = "resource-list-scroll-view";
30 public const string kResourceGridScrollView = "resource-grid-scroll-view";
31 public const string kResourceGrid = "resource-grid";
32 public const string kGridlineContainer = "grid-line-container";
33 public const string kHoverOverlay = "hover-overlay";
34 public const string kEmptyStateMessage = "empty-state-message";
35 }
36
37 static partial class Classes
38 {
39 public const string kPassListItem = "pass-list__item";
40 public const string kPassTitle = "pass-title";
41 public const string kPassBlock = "pass-block";
42 public const string kPassBlockScriptLink = "pass-block-script-link";
43 public const string kPassBlockCulledPass = "pass-block--culled";
44 public const string kPassBlockAsyncPass = "pass-block--async";
45 public const string kPassHighlight = "pass--highlight";
46 public const string kPassHoverHighlight = "pass--hover-highlight";
47 public const string kPassHighlightBorder = "pass--highlight-border";
48 public const string kPassMergeIndicator = "pass-merge-indicator";
49 public const string kPassCompatibilityMessageIndicator = "pass-compatibility-message-indicator";
50 public const string kPassCompatibilityMessageIndicatorAnimation = "pass-compatibility-message-indicator--anim";
51 public const string kPassCompatibilityMessageIndicatorCompatible = "pass-compatibility-message-indicator--compatible";
52 public const string kPassSynchronizationMessageIndicator = "pass-synchronization-message-indicator";
53 public const string kResourceListItem = "resource-list__item";
54 public const string kResourceListItemHighlight = "resource-list__item--highlight";
55 public const string kResourceListPaddingItem = "resource-list-padding-item";
56 public const string kResourceIconContainer = "resource-icon-container";
57 public const string kResourceIcon = "resource-icon";
58 public const string kResourceIconImported = "resource-icon--imported";
59 public const string kResourceIconMultipleUsage = "resource-icon--multiple-usage";
60 public const string kResourceIconGlobalDark = "resource-icon--global-dark";
61 public const string kResourceIconGlobalLight = "resource-icon--global-light";
62 public const string kResourceIconFbfetch = "resource-icon--fbfetch";
63 public const string kResourceIconTexture = "resource-icon--texture";
64 public const string kResourceIconBuffer = "resource-icon--buffer";
65 public const string kResourceIconAccelerationStructure = "resource-icon--acceleration-structure";
66 public const string kResourceGridRow = "resource-grid__row";
67 public const string kResourceGridFocusOverlay = "resource-grid-focus-overlay";
68 public const string kResourceHelperLine = "resource-helper-line";
69 public const string kResourceHelperLineHighlight = "resource-helper-line--highlight";
70 public const string kResourceUsageRangeBlock = "usage-range-block";
71 public const string kResourceUsageRangeBlockHighlight = "usage-range-block--highlight";
72 public const string kResourceDependencyBlock = "dependency-block";
73 public const string kResourceDependencyBlockRead = "dependency-block-read";
74 public const string kResourceDependencyBlockWrite = "dependency-block-write";
75 public const string kResourceDependencyBlockReadWrite = "dependency-block-readwrite";
76 public const string kGridLine = "grid-line";
77 public const string kGridLineHighlight = "grid-line--highlight";
78 }
79
80 const string k_TemplatePath = "Packages/com.unity.render-pipelines.core/Editor/UXML/RenderGraphViewer.uxml";
81 const string k_DarkStylePath = "Packages/com.unity.render-pipelines.core/Editor/StyleSheets/RenderGraphViewerDark.uss";
82 const string k_LightStylePath = "Packages/com.unity.render-pipelines.core/Editor/StyleSheets/RenderGraphViewerLight.uss";
83 const string k_ResourceListIconPath = "Packages/com.unity.render-pipelines.core/Editor/Icons/RenderGraphViewer/{0}Resources@2x.png";
84 const string k_PassListIconPath = "Packages/com.unity.render-pipelines.core/Editor/Icons/RenderGraphViewer/{0}PassInspector@2x.png";
85
86 // keep in sync with .uss
87 const int kPassWidthPx = 26;
88 const int kResourceRowHeightPx = 30;
89 const int kResourceColumnWidth = 220;
90 const int kResourceIconSize = 16;
91 const int kResourceGridMarginTopPx = 6;
92 const int kDependencyBlockHeightPx = 26;
93 const int kDependencyBlockWidthPx = kPassWidthPx;
94 const int kPassTitleAllowanceMargin = 120;
95 const int kHeaderContainerHeightPx = 24;
96
97 static readonly Color kReadWriteBlockFillColorDark = new Color32(0xA9, 0xD1, 0x36, 255);
98 static readonly Color kReadWriteBlockFillColorLight = new Color32(0x67, 0x9C, 0x33, 255);
99
100 readonly Dictionary<RenderGraph, HashSet<string>> m_RegisteredGraphs = new();
101 RenderGraph.DebugData m_CurrentDebugData;
102
103 Foldout m_ResourcesList;
104 Foldout m_PassList;
105 PanManipulator m_PanManipulator;
106 Texture2D m_ResourceListIcon;
107 Texture2D m_PassListIcon;
108
109 readonly List<PassElementInfo> m_PassElementsInfo = new(); // Indexed using visiblePassIndex
110 readonly List<ResourceElementInfo> m_ResourceElementsInfo = new(); // Indexed using visibleResourceIndex
111 readonly Dictionary<int, int> m_PassIdToVisiblePassIndex = new();
112 readonly Dictionary<int, int> m_VisiblePassIndexToPassId = new();
113
114 int m_CurrentHoveredVisiblePassIndex = -1;
115 int m_CurrentHoveredVisibleResourceIndex = -1;
116 int m_CurrentSelectedVisiblePassIndex = -1;
117
118 const string kPassFilterLegacyEditorPrefsKey = "RenderGraphViewer.PassFilterLegacy";
119 const string kPassFilterEditorPrefsKey = "RenderGraphViewer.PassFilter";
120 const string kResourceFilterEditorPrefsKey = "RenderGraphViewer.ResourceFilter";
121 const string kSelectedExecutionEditorPrefsKey = "RenderGraphViewer.SelectedExecution";
122
123 PassFilter m_PassFilter = PassFilter.CulledPasses | PassFilter.RasterPasses | PassFilter.UnsafePasses | PassFilter.ComputePasses;
124 PassFilterLegacy m_PassFilterLegacy = PassFilterLegacy.CulledPasses;
125
126 ResourceFilter m_ResourceFilter =
127 ResourceFilter.ImportedResources | ResourceFilter.Textures |
128 ResourceFilter.Buffers | ResourceFilter.AccelerationStructures;
129
130 enum EmptyStateReason
131 {
132 None = 0,
133 NoGraphRegistered,
134 NoExecutionRegistered,
135 NoDataAvailable,
136 WaitingForCameraRender,
137 EmptyPassFilterResult,
138 EmptyResourceFilterResult
139 };
140
141 static readonly string[] kEmptyStateMessages =
142 {
143 "",
144 L10n.Tr("No Render Graph has been registered. The Render Graph Viewer is only functional when Render Graph API is in use."),
145 L10n.Tr("The selected camera has not rendered anything yet using a Render Graph API. Interact with the selected camera to display data in the Render Graph Viewer. Make sure your current SRP is using the Render Graph API."),
146 L10n.Tr("No data to display. Click refresh to capture data."),
147 L10n.Tr("Waiting for the selected camera to render. Depending on the camera, you may need to trigger rendering by selecting the Scene or Game view."),
148 L10n.Tr("No passes to display. Select a different Pass Filter to display contents."),
149 L10n.Tr("No resources to display. Select a different Resource Filter to display contents.")
150 };
151
152 static readonly string kOpenInCSharpEditorTooltip = L10n.Tr("Click to open <b>{0}</b> definition in C# editor.");
153
154 [MenuItem("Window/Analysis/Render Graph Viewer", false, 10006)]
155 static void Init()
156 {
157 var window = GetWindow<RenderGraphViewer>();
158 window.titleContent = new GUIContent("Render Graph Viewer");
159 }
160
161 [Flags]
162 enum PassFilterLegacy
163 {
164 CulledPasses = 1 << 0,
165 }
166
167 [Flags]
168 enum PassFilter
169 {
170 CulledPasses = 1 << 0,
171 RasterPasses = 1 << 1,
172 UnsafePasses = 1 << 2,
173 ComputePasses = 1 << 3,
174 }
175
176 [Flags]
177 enum ResourceFilter
178 {
179 ImportedResources = 1 << 0,
180 Textures = 1 << 1,
181 Buffers = 1 << 2,
182 AccelerationStructures = 1 << 3,
183 }
184
185 class ResourceElementInfo
186 {
187 public RenderGraphResourceType type;
188 public int index;
189 public VisualElement usageRangeBlock;
190 public VisualElement resourceListItem;
191 public VisualElement resourceHelperLine;
192 public int firstPassId;
193 public int lastPassId;
194 }
195
196 class ResourceRWBlock
197 {
198 [Flags]
199 public enum UsageFlags
200 {
201 None = 0,
202 UpdatesGlobalResource = 1 << 0,
203 FramebufferFetch = 1 << 1,
204 }
205
206 public VisualElement element;
207 public string tooltip;
208 public int visibleResourceIndex;
209 public bool read;
210 public bool write;
211 public UsageFlags usage;
212
213 public bool HasMultipleUsageFlags()
214 {
215 // Check if usage is a power of 2, meaning only one bit is set
216 return usage != 0 && (usage & (usage - 1)) != 0;
217 }
218 }
219
220 class PassElementInfo
221 {
222 public int passId;
223 public VisualElement passBlock;
224 public PassTitleLabel passTitle;
225 public bool isCulled;
226 public bool isAsync;
227
228 // List of resource blocks read/written to by this pass
229 public readonly List<ResourceRWBlock> resourceBlocks = new();
230
231 public VisualElement leftGridLine;
232 public VisualElement rightGridLine;
233
234 public bool hasPassCompatibilityTooltip;
235 public bool isPassCompatibleToMerge;
236 public bool hasAsyncDependencyTooltip;
237 }
238
239 void AppendToTooltipIfNotEmpty(VisualElement elem, string append)
240 {
241 if (elem.tooltip.Length > 0)
242 elem.tooltip += append;
243 }
244
245 void ResetPassBlockState()
246 {
247 foreach (var info in m_PassElementsInfo)
248 {
249 info.hasPassCompatibilityTooltip = false;
250 info.isPassCompatibleToMerge = false;
251 info.hasAsyncDependencyTooltip = false;
252
253 info.passBlock.RemoveFromClassList(Classes.kPassBlockCulledPass);
254 info.passBlock.tooltip = string.Empty;
255 if (info.isCulled)
256 {
257 info.passBlock.AddToClassList(Classes.kPassBlockCulledPass);
258 info.passBlock.tooltip = "Culled pass";
259 }
260
261 info.passBlock.RemoveFromClassList(Classes.kPassBlockAsyncPass);
262 if (info.isAsync)
263 {
264 info.passBlock.AddToClassList(Classes.kPassBlockAsyncPass);
265 AppendToTooltipIfNotEmpty(info.passBlock, $"{Environment.NewLine}");
266 info.passBlock.tooltip += "Async Compute Pass";
267 }
268
269 var groupedIds = GetGroupedPassIds(info.passId);
270 if (groupedIds.Count > 1)
271 {
272 AppendToTooltipIfNotEmpty(info.passBlock, $"{Environment.NewLine}");
273 info.passBlock.tooltip += $"{groupedIds.Count} Raster Render Passes merged into a single Native Render Pass";
274 }
275
276 AppendToTooltipIfNotEmpty(info.passBlock, $"{Environment.NewLine}{Environment.NewLine}");
277 info.passBlock.tooltip += string.Format(kOpenInCSharpEditorTooltip, info.passTitle.text);
278 }
279 }
280
281 void UpdatePassBlocksToSelectedState(List<int> selectedPassIds)
282 {
283 // Hide culled/async pass indicators when a block is selected
284 foreach (var info in m_PassElementsInfo)
285 {
286 info.passBlock.RemoveFromClassList(Classes.kPassBlockCulledPass);
287 info.passBlock.RemoveFromClassList(Classes.kPassBlockAsyncPass);
288 info.passBlock.tooltip = string.Empty;
289 }
290
291 foreach (var passIdInGroup in selectedPassIds)
292 {
293 var pass = m_CurrentDebugData.passList[passIdInGroup];
294
295 // Native pass compatibility
296 if (m_CurrentDebugData.isNRPCompiler)
297 {
298 if (pass.nrpInfo.nativePassInfo != null && pass.nrpInfo.nativePassInfo.passCompatibility.Count > 0)
299 {
300 foreach (var msg in pass.nrpInfo.nativePassInfo.passCompatibility)
301 {
302 int linkedPassId = msg.Key;
303 string compatibilityMessage = msg.Value.message;
304 var linkedPassGroup = GetGroupedPassIds(linkedPassId);
305 foreach (var passIdInLinkedPassGroup in linkedPassGroup)
306 {
307 if (selectedPassIds.Contains(passIdInLinkedPassGroup))
308 continue; // Don't show compatibility info among passes that are merged
309
310 if (m_PassIdToVisiblePassIndex.TryGetValue(passIdInLinkedPassGroup,
311 out int visiblePassIndexInLinkedPassGroup))
312 {
313 var info = m_PassElementsInfo[visiblePassIndexInLinkedPassGroup];
314 info.hasPassCompatibilityTooltip = true;
315 info.isPassCompatibleToMerge = msg.Value.isCompatible;
316 info.passBlock.tooltip = compatibilityMessage;
317 }
318 }
319 }
320
321 // Each native pass has compatibility messages, it's enough to process the first one
322 break;
323 }
324 }
325
326 // Async compute dependencies
327 if (m_PassIdToVisiblePassIndex.TryGetValue(pass.syncFromPassIndex, out int visibleSyncFromPassIndex))
328 {
329 var syncFromPassInfo = m_PassElementsInfo[visibleSyncFromPassIndex];
330 syncFromPassInfo.hasAsyncDependencyTooltip = true;
331 AppendToTooltipIfNotEmpty(syncFromPassInfo.passBlock, $"{Environment.NewLine}");
332 syncFromPassInfo.passBlock.tooltip +=
333 "Currently selected Async Compute Pass inserts a GraphicsFence, which this pass waits on.";
334 }
335
336 if (m_PassIdToVisiblePassIndex.TryGetValue(pass.syncToPassIndex, out int visibleSyncToPassIndex))
337 {
338 var syncToPassInfo = m_PassElementsInfo[visibleSyncToPassIndex];
339 syncToPassInfo.hasAsyncDependencyTooltip = true;
340 AppendToTooltipIfNotEmpty(syncToPassInfo.passBlock, $"{Environment.NewLine}");
341 syncToPassInfo.passBlock.tooltip +=
342 "Currently selected Async Compute Pass waits on a GraphicsFence inserted after this pass.";
343 }
344 }
345
346 foreach (var info in m_PassElementsInfo)
347 {
348 AppendToTooltipIfNotEmpty(info.passBlock, $"{Environment.NewLine}{Environment.NewLine}");
349 info.passBlock.tooltip += string.Format(kOpenInCSharpEditorTooltip, info.passTitle.text);
350 }
351 }
352
353 [Flags]
354 enum ResourceHighlightOptions
355 {
356 None = 1 << 0,
357 ResourceListItem = 1 << 1,
358 ResourceUsageRangeBorder = 1 << 2,
359 ResourceHelperLine = 1 << 3,
360
361 All = ResourceListItem | ResourceUsageRangeBorder | ResourceHelperLine
362 }
363
364 void ClearResourceHighlight(ResourceHighlightOptions highlightOptions = ResourceHighlightOptions.All)
365 {
366 if (highlightOptions.HasFlag(ResourceHighlightOptions.ResourceListItem))
367 {
368 rootVisualElement.Query(classes: Classes.kResourceListItem).ForEach(elem =>
369 {
370 elem.RemoveFromClassList(Classes.kResourceListItemHighlight);
371 });
372 }
373
374 if (highlightOptions.HasFlag(ResourceHighlightOptions.ResourceUsageRangeBorder))
375 {
376 rootVisualElement.Query(classes: Classes.kResourceUsageRangeBlockHighlight).ForEach(elem =>
377 {
378 elem.RemoveFromHierarchy();
379 });
380 }
381
382 if (highlightOptions.HasFlag(ResourceHighlightOptions.ResourceHelperLine))
383 {
384 rootVisualElement.Query(classes: Classes.kResourceHelperLineHighlight).ForEach(elem =>
385 {
386 elem.RemoveFromClassList(Classes.kResourceHelperLineHighlight);
387 });
388 }
389 }
390
391 void SetResourceHighlight(ResourceElementInfo info, int visibleResourceIndex, ResourceHighlightOptions highlightOptions)
392 {
393 if (highlightOptions.HasFlag(ResourceHighlightOptions.ResourceListItem))
394 {
395 info.resourceListItem.AddToClassList(Classes.kResourceListItemHighlight);
396 }
397
398 if (highlightOptions.HasFlag(ResourceHighlightOptions.ResourceUsageRangeBorder))
399 {
400 var usageRangeHighlightBlock = new VisualElement();
401 usageRangeHighlightBlock.style.left = info.usageRangeBlock.style.left.value.value;
402 usageRangeHighlightBlock.style.width = info.usageRangeBlock.style.width.value.value + 1.0f;
403 usageRangeHighlightBlock.style.top = visibleResourceIndex * kResourceRowHeightPx;
404 usageRangeHighlightBlock.pickingMode = PickingMode.Ignore;
405 usageRangeHighlightBlock.AddToClassList(Classes.kResourceUsageRangeBlockHighlight);
406
407 rootVisualElement.Q<VisualElement>(Names.kResourceGrid).parent.Add(usageRangeHighlightBlock);
408 usageRangeHighlightBlock.PlaceInFront(rootVisualElement.Q<VisualElement>(Names.kGridlineContainer));
409 }
410
411 if (highlightOptions.HasFlag(ResourceHighlightOptions.ResourceHelperLine))
412 {
413 info.resourceHelperLine.AddToClassList(Classes.kResourceHelperLineHighlight);
414 }
415 }
416
417 [Flags]
418 enum PassHighlightOptions
419 {
420 None = 1 << 0,
421 PassBlockBorder = 1 << 1,
422 PassBlockFill = 1 << 2,
423 PassTitle = 1 << 3,
424 PassGridLines = 1 << 4,
425 ResourceRWBlocks = 1 << 5,
426 PassesWithCompatibilityMessage = 1 << 6,
427 PassesWithSynchronizationMessage = 1 << 7,
428 ResourceGridFocusOverlay = 1 << 8,
429 PassTitleHover = 1 << 9,
430
431 All = PassBlockBorder | PassBlockFill | PassTitle | PassGridLines | ResourceRWBlocks |
432 PassesWithCompatibilityMessage | PassesWithSynchronizationMessage | ResourceGridFocusOverlay | PassTitleHover
433 }
434
435 void ClearPassHighlight(PassHighlightOptions highlightOptions = PassHighlightOptions.All)
436 {
437 // Remove pass block & title highlight
438 foreach (var info in m_PassElementsInfo)
439 {
440 if (highlightOptions.HasFlag(PassHighlightOptions.PassTitle))
441 info.passTitle.RemoveFromClassList(Classes.kPassHighlight);
442 if (highlightOptions.HasFlag(PassHighlightOptions.PassTitleHover))
443 info.passTitle.RemoveFromClassList(Classes.kPassHoverHighlight);
444 if (highlightOptions.HasFlag(PassHighlightOptions.PassBlockBorder))
445 info.passBlock.RemoveFromClassList(Classes.kPassHighlightBorder);
446 if (highlightOptions.HasFlag(PassHighlightOptions.PassBlockFill))
447 info.passBlock.RemoveFromClassList(Classes.kPassHighlight);
448 if (highlightOptions.HasFlag(PassHighlightOptions.PassesWithCompatibilityMessage))
449 {
450 info.passBlock.RemoveFromClassList(Classes.kPassCompatibilityMessageIndicator);
451 info.passBlock.RemoveFromClassList(Classes.kPassCompatibilityMessageIndicatorAnimation);
452 info.passBlock.RemoveFromClassList(Classes.kPassCompatibilityMessageIndicatorCompatible);
453 info.passBlock.UnregisterCallback<TransitionEndEvent, VisualElement>(ToggleCompatiblePassAnimation);
454 }
455
456 if (highlightOptions.HasFlag(PassHighlightOptions.PassesWithSynchronizationMessage))
457 info.passBlock.RemoveFromClassList(Classes.kPassSynchronizationMessageIndicator);
458 }
459
460 // Remove grid line highlight
461 if (highlightOptions.HasFlag(PassHighlightOptions.PassGridLines))
462 {
463 rootVisualElement.Query(classes: Classes.kGridLine).ForEach(elem =>
464 {
465 elem.RemoveFromClassList(Classes.kGridLineHighlight);
466 });
467 }
468
469 // Remove focus overlay
470 if (highlightOptions.HasFlag(PassHighlightOptions.ResourceGridFocusOverlay))
471 {
472 rootVisualElement.Query(classes: Classes.kResourceGridFocusOverlay).ForEach(elem =>
473 {
474 elem.RemoveFromHierarchy();
475 });
476 }
477 }
478
479 void SetPassHighlight(int visiblePassIndex, PassHighlightOptions highlightOptions)
480 {
481 if (!m_VisiblePassIndexToPassId.TryGetValue(visiblePassIndex, out int passId))
482 return;
483
484 var groupedPassIds = GetGroupedPassIds(passId);
485
486 // Add pass block & title highlight
487 List<PassElementInfo> visiblePassInfos = new();
488 foreach (int groupedPassId in groupedPassIds)
489 {
490 if (m_PassIdToVisiblePassIndex.TryGetValue(groupedPassId, out int groupedVisiblePassIndex))
491 {
492 var info = m_PassElementsInfo[groupedVisiblePassIndex];
493 if (highlightOptions.HasFlag(PassHighlightOptions.PassTitle))
494 info.passTitle.AddToClassList(Classes.kPassHighlight);
495 if (highlightOptions.HasFlag(PassHighlightOptions.PassTitleHover))
496 info.passTitle.AddToClassList(Classes.kPassHoverHighlight);
497 if (highlightOptions.HasFlag(PassHighlightOptions.PassBlockBorder))
498 info.passBlock.AddToClassList(Classes.kPassHighlightBorder);
499 if (highlightOptions.HasFlag(PassHighlightOptions.PassBlockFill))
500 info.passBlock.AddToClassList(Classes.kPassHighlight);
501 visiblePassInfos.Add(info);
502 }
503 }
504
505 foreach (var info in m_PassElementsInfo)
506 {
507 if (groupedPassIds.Contains(info.passId))
508 continue;
509
510 if (highlightOptions.HasFlag(PassHighlightOptions.PassesWithCompatibilityMessage) &&
511 info.hasPassCompatibilityTooltip)
512 {
513 info.passBlock.AddToClassList(Classes.kPassCompatibilityMessageIndicator);
514 if (info.isPassCompatibleToMerge)
515 {
516 info.passBlock.schedule.Execute(() =>
517 {
518 info.passBlock.AddToClassList(Classes.kPassCompatibilityMessageIndicatorAnimation);
519 info.passBlock.AddToClassList(Classes.kPassCompatibilityMessageIndicatorCompatible);
520 }).StartingIn(100);
521 info.passBlock.RegisterCallback<TransitionEndEvent, VisualElement>(
522 ToggleCompatiblePassAnimation, info.passBlock);
523 }
524 }
525
526 if (highlightOptions.HasFlag(PassHighlightOptions.PassesWithSynchronizationMessage) &&
527 info.hasAsyncDependencyTooltip)
528 info.passBlock.AddToClassList(Classes.kPassSynchronizationMessageIndicator);
529 }
530
531 // Add grid line highlight
532 if (highlightOptions.HasFlag(PassHighlightOptions.PassGridLines))
533 {
534 var firstVisiblePassInfo = visiblePassInfos[0];
535 firstVisiblePassInfo.leftGridLine.AddToClassList(Classes.kGridLineHighlight);
536
537 int nextVisiblePassIndex = FindNextVisiblePassIndex(visiblePassInfos[^1].passId + 1);
538 if (nextVisiblePassIndex != -1)
539 m_PassElementsInfo[nextVisiblePassIndex].leftGridLine.AddToClassList(Classes.kGridLineHighlight);
540 else
541 rootVisualElement.Query(classes: Classes.kGridLine).Last()
542 .AddToClassList(Classes.kGridLineHighlight);
543 }
544
545 if (highlightOptions.HasFlag(PassHighlightOptions.ResourceGridFocusOverlay))
546 {
547 int firstPassIndex = FindNextVisiblePassIndex(groupedPassIds[0]);
548 int afterLastPassIndex = FindNextVisiblePassIndex(groupedPassIds[^1] + 1);
549 int focusOverlayHeightPx = m_ResourceElementsInfo.Count * kResourceRowHeightPx + kResourceGridMarginTopPx;
550 int leftWidth = firstPassIndex * kPassWidthPx;
551 int rightWidth = (m_PassElementsInfo.Count - afterLastPassIndex) * kPassWidthPx;
552
553 VisualElement left = new VisualElement();
554 left.AddToClassList(Classes.kResourceGridFocusOverlay);
555 left.style.marginTop = kResourceGridMarginTopPx;
556 left.style.width = leftWidth;
557 left.style.height = focusOverlayHeightPx;
558 left.pickingMode = PickingMode.Ignore;
559
560 VisualElement right = new VisualElement();
561 right.AddToClassList(Classes.kResourceGridFocusOverlay);
562 right.style.marginTop = kResourceGridMarginTopPx;
563 right.style.marginLeft = afterLastPassIndex * kPassWidthPx;
564 right.style.width = rightWidth;
565 right.style.height = focusOverlayHeightPx;
566 right.pickingMode = PickingMode.Ignore;
567
568 var resourceGridScrollView = rootVisualElement.Q<ScrollView>(Names.kResourceGridScrollView);
569 resourceGridScrollView.Add(left);
570 resourceGridScrollView.Add(right);
571 }
572 }
573
574 void ToggleCompatiblePassAnimation(TransitionEndEvent _, VisualElement element)
575 {
576 element.ToggleInClassList(Classes.kPassCompatibilityMessageIndicatorCompatible);
577 }
578
579 // Returns a list of passes containing the pass itself and potentially others (if merging happened)
580 List<int> GetGroupedPassIds(int passId)
581 {
582 var pass = m_CurrentDebugData.passList[passId];
583 return pass.nrpInfo?.nativePassInfo?.mergedPassIds ?? new List<int> { passId };
584 }
585
586 bool SelectResource(int visibleResourceIndex, int visiblePassIndex)
587 {
588 bool validResourceIndex = visibleResourceIndex >= 0 && visibleResourceIndex < m_ResourceElementsInfo.Count;
589 if (!validResourceIndex)
590 return false;
591
592 var resInfo = m_ResourceElementsInfo[visibleResourceIndex];
593 if (m_VisiblePassIndexToPassId.TryGetValue(visiblePassIndex, out int passId))
594 {
595 if (passId >= resInfo.firstPassId && passId <= resInfo.lastPassId)
596 {
597 ScrollToResource(visibleResourceIndex);
598 return true;
599 }
600 }
601
602 return false;
603 }
604
605 void SelectPass(int visiblePassIndex)
606 {
607 m_CurrentSelectedVisiblePassIndex = visiblePassIndex;
608
609 const PassHighlightOptions opts = PassHighlightOptions.PassTitle |
610 PassHighlightOptions.PassBlockFill |
611 PassHighlightOptions.PassGridLines |
612 PassHighlightOptions.PassBlockBorder |
613 PassHighlightOptions.ResourceRWBlocks |
614 PassHighlightOptions.PassesWithCompatibilityMessage |
615 PassHighlightOptions.PassesWithSynchronizationMessage |
616 PassHighlightOptions.ResourceGridFocusOverlay;
617
618 ClearPassHighlight(opts);
619 ResetPassBlockState();
620
621 if (m_VisiblePassIndexToPassId.TryGetValue(visiblePassIndex, out int passId))
622 {
623 var selectedPassIds = GetGroupedPassIds(passId);
624 UpdatePassBlocksToSelectedState(selectedPassIds);
625 SetPassHighlight(visiblePassIndex, opts);
626 ScrollToPass(m_PassIdToVisiblePassIndex[selectedPassIds[0]]);
627 }
628 }
629
630 void HoverPass(int visiblePassIndex, int visibleResourceIndex)
631 {
632 if (m_CurrentHoveredVisiblePassIndex != visiblePassIndex ||
633 m_CurrentHoveredVisibleResourceIndex != visibleResourceIndex)
634 {
635 var highlight = PassHighlightOptions.PassBlockBorder |
636 PassHighlightOptions.PassGridLines |
637 PassHighlightOptions.PassBlockFill |
638 PassHighlightOptions.PassTitleHover;
639
640 if (m_CurrentSelectedVisiblePassIndex != -1)
641 {
642 // Don't highlight or clear these when a pass is selected
643 highlight &= ~(PassHighlightOptions.PassTitle | PassHighlightOptions.PassBlockFill |
644 PassHighlightOptions.PassBlockBorder | PassHighlightOptions.PassGridLines);
645 }
646
647 if (m_CurrentHoveredVisiblePassIndex != -1)
648 ClearPassHighlight(highlight);
649
650 if (visibleResourceIndex != -1) // Don't highlight these while mouse is on resource grid
651 highlight &= ~(PassHighlightOptions.PassBlockFill | PassHighlightOptions.PassTitleHover);
652
653 if (visiblePassIndex != -1)
654 SetPassHighlight(visiblePassIndex, highlight);
655 }
656 }
657
658 void HoverResourceByIndex(int visibleResourceIndex, int visiblePassIndex)
659 {
660 if (m_CurrentHoveredVisibleResourceIndex != visibleResourceIndex ||
661 m_CurrentHoveredVisiblePassIndex != visiblePassIndex)
662 {
663 var highlight = ResourceHighlightOptions.ResourceUsageRangeBorder | ResourceHighlightOptions.ResourceHelperLine;
664 if (m_CurrentHoveredVisibleResourceIndex >= 0 &&
665 m_CurrentHoveredVisibleResourceIndex < m_ResourceElementsInfo.Count)
666 {
667 ClearResourceHighlight(highlight);
668 rootVisualElement.Q(Names.kHoverOverlay).AddToClassList(PanManipulator.k_ContentPanClassName);
669 m_PanManipulator.canStartDragging = true;
670 }
671
672 if (visibleResourceIndex != -1)
673 {
674 var info = m_ResourceElementsInfo[visibleResourceIndex];
675 if (m_VisiblePassIndexToPassId.TryGetValue(visiblePassIndex, out int passId))
676 {
677 bool disablePanning = false;
678 var passInfo = m_PassElementsInfo[visiblePassIndex];
679 foreach (var res in passInfo.resourceBlocks)
680 {
681 if (res.visibleResourceIndex == visibleResourceIndex &&
682 res.usage.HasFlag(ResourceRWBlock.UsageFlags.UpdatesGlobalResource))
683 {
684 disablePanning = true;
685 }
686 }
687
688 if (passId >= info.firstPassId && passId <= info.lastPassId)
689 {
690 SetResourceHighlight(info, visibleResourceIndex, highlight);
691 disablePanning = true;
692 }
693
694 if (disablePanning)
695 {
696 rootVisualElement.Q(Names.kHoverOverlay)
697 .RemoveFromClassList(PanManipulator.k_ContentPanClassName);
698 m_PanManipulator.canStartDragging = false;
699 }
700 }
701 }
702 }
703 }
704
705 void HoverResourceGrid(int visiblePassIndex, int visibleResourceIndex)
706 {
707 if (m_PanManipulator is { dragActive: true })
708 {
709 visiblePassIndex = -1;
710 visibleResourceIndex = -1;
711 }
712
713 HoverPass(visiblePassIndex, visibleResourceIndex);
714 HoverResourceByIndex(visibleResourceIndex, visiblePassIndex);
715 m_CurrentHoveredVisiblePassIndex = visiblePassIndex;
716 m_CurrentHoveredVisibleResourceIndex = visibleResourceIndex;
717 }
718
719 void GetVisiblePassAndResourceIndex(Vector2 pos, out int visiblePassIndex, out int visibleResourceIndex)
720 {
721 visiblePassIndex = Math.Min(Mathf.FloorToInt(pos.x / kPassWidthPx), m_PassElementsInfo.Count - 1);
722 visibleResourceIndex = Math.Min(Mathf.FloorToInt(pos.y / kResourceRowHeightPx),
723 m_ResourceElementsInfo.Count - 1);
724 }
725
726 void ResourceGridHovered(MouseMoveEvent evt)
727 {
728 GetVisiblePassAndResourceIndex(evt.localMousePosition, out int visiblePassIndex,
729 out int visibleResourceIndex);
730 HoverResourceGrid(visiblePassIndex, visibleResourceIndex);
731 }
732
733 void ResourceGridClicked(ClickEvent evt)
734 {
735 GetVisiblePassAndResourceIndex(evt.localPosition, out int visiblePassIndex, out int visibleResourceIndex);
736
737 bool selectedResource = SelectResource(visibleResourceIndex, visiblePassIndex);
738
739 // Also select pass when clicking on resource
740 if (selectedResource)
741 SelectPass(visiblePassIndex);
742
743 // Clicked grid background, clear selection
744 if (!selectedResource)
745 DeselectPass();
746
747 evt.StopImmediatePropagation(); // Required because to root element click deselects
748 }
749
750 void PassBlockClicked(ClickEvent evt, int visiblePassIndex)
751 {
752 SelectPass(visiblePassIndex);
753 evt.StopImmediatePropagation(); // Required because to root element click deselects
754 }
755
756 void ResourceGridTooltipDisplayed(TooltipEvent evt)
757 {
758 evt.tooltip = string.Empty;
759 if (m_CurrentHoveredVisibleResourceIndex != -1 && m_CurrentHoveredVisiblePassIndex != -1)
760 {
761 var passInfo = m_PassElementsInfo[m_CurrentHoveredVisiblePassIndex];
762 var resourceInfo = m_ResourceElementsInfo[m_CurrentHoveredVisibleResourceIndex];
763 var passId = m_VisiblePassIndexToPassId[m_CurrentHoveredVisiblePassIndex];
764
765 foreach (var rwBlock in passInfo.resourceBlocks)
766 {
767 if (rwBlock.visibleResourceIndex == m_CurrentHoveredVisibleResourceIndex)
768 {
769 evt.tooltip = rwBlock.tooltip;
770 evt.rect = rwBlock.element.worldBound;
771 break;
772 }
773 }
774
775 if (evt.tooltip == string.Empty &&
776 passId >= resourceInfo.firstPassId && passId <= resourceInfo.lastPassId)
777 {
778 evt.tooltip = "Resource is alive but not used by this pass.";
779 evt.rect = resourceInfo.usageRangeBlock.worldBound;
780 }
781 }
782
783 evt.StopPropagation();
784 }
785
786 void DeselectPass()
787 {
788 SelectPass(-1);
789 m_CurrentHoveredVisiblePassIndex = -1;
790 m_CurrentHoveredVisibleResourceIndex = -1;
791 }
792
793 void KeyPressed(KeyUpEvent evt)
794 {
795 if (evt.keyCode == KeyCode.Escape)
796 DeselectPass();
797 }
798
799 void RequestCaptureSelectedExecution()
800 {
801 if (!CaptureEnabled())
802 return;
803
804 selectedRenderGraph.RequestCaptureDebugData(selectedExecutionName);
805
806 ClearGraphViewerUI();
807 SetEmptyStateMessage(EmptyStateReason.WaitingForCameraRender);
808 }
809
810 void SelectedRenderGraphChanged(string newRenderGraphName)
811 {
812 foreach (var rg in m_RegisteredGraphs.Keys)
813 {
814 if (rg.name == newRenderGraphName)
815 {
816 selectedRenderGraph = rg;
817 return;
818 }
819 }
820 selectedRenderGraph = null;
821
822 if (m_CurrentDebugData != null)
823 RequestCaptureSelectedExecution();
824 }
825
826 void SelectedExecutionChanged(string newExecutionName)
827 {
828 if (newExecutionName == selectedExecutionName)
829 return;
830
831 selectedExecutionName = newExecutionName;
832
833 if (m_CurrentDebugData != null)
834 RequestCaptureSelectedExecution();
835 }
836
837 void ClearEmptyStateMessage()
838 {
839 rootVisualElement.Q<VisualElement>(Names.kContentContainer).style.display = DisplayStyle.Flex;
840 rootVisualElement.Q<VisualElement>(Names.kEmptyStateMessage).style.display = DisplayStyle.None;
841 }
842
843 void SetEmptyStateMessage(EmptyStateReason reason)
844 {
845 rootVisualElement.Q<VisualElement>(Names.kContentContainer).style.display = DisplayStyle.None;
846
847 var emptyStateElement = rootVisualElement.Q<VisualElement>(Names.kEmptyStateMessage);
848 emptyStateElement.style.display = DisplayStyle.Flex;
849 if (emptyStateElement[0] is TextElement emptyStateText)
850 emptyStateText.text = $"{kEmptyStateMessages[(int) reason]}";
851 }
852
853 void RebuildRenderGraphPopup()
854 {
855 var renderGraphDropdownField = rootVisualElement.Q<DropdownField>(Names.kCurrentGraphDropdown);
856 if (m_RegisteredGraphs.Count == 0 || renderGraphDropdownField == null)
857 {
858 selectedRenderGraph = null;
859 return;
860 }
861
862 var choices = new List<string>();
863 foreach (var rg in m_RegisteredGraphs.Keys)
864 choices.Add(rg.name);
865
866 renderGraphDropdownField.choices = choices;
867 renderGraphDropdownField.style.display = DisplayStyle.Flex;
868 renderGraphDropdownField.value = choices[0];
869 SelectedRenderGraphChanged(choices[0]);
870 }
871
872 void RebuildExecutionPopup()
873 {
874 var executionDropdownField = rootVisualElement.Q<DropdownField>(Names.kCurrentExecutionDropdown);
875 List<string> choices = new List<string>();
876 if (selectedRenderGraph != null)
877 {
878 m_RegisteredGraphs.TryGetValue(selectedRenderGraph, out var executionSet);
879 choices.AddRange(executionSet);
880 }
881
882 if (choices.Count == 0 || executionDropdownField == null)
883 {
884 selectedExecutionName = null;
885 return;
886 }
887
888 executionDropdownField.choices = choices;
889 executionDropdownField.RegisterValueChangedCallback(evt => selectedExecutionName = evt.newValue);
890
891 int selectedIndex = 0;
892 if (EditorPrefs.HasKey(kSelectedExecutionEditorPrefsKey))
893 {
894 string previousSelectedExecution = EditorPrefs.GetString(kSelectedExecutionEditorPrefsKey);
895 int previousSelectedIndex = choices.IndexOf(previousSelectedExecution);
896 if (previousSelectedIndex != -1)
897 selectedIndex = previousSelectedIndex;
898 }
899
900 // Set value without triggering serialization of the editorpref
901 executionDropdownField.SetValueWithoutNotify(choices[selectedIndex]);
902 SelectedExecutionChanged(choices[selectedIndex]);
903 }
904
905 void OnPassFilterChanged(ChangeEvent<Enum> evt)
906 {
907 m_PassFilter = (PassFilter) evt.newValue;
908 EditorPrefs.SetInt(kPassFilterEditorPrefsKey, (int)m_PassFilter);
909 RebuildGraphViewerUI();
910 }
911
912 void OnPassFilterLegacyChanged(ChangeEvent<Enum> evt)
913 {
914 m_PassFilterLegacy = (PassFilterLegacy) evt.newValue;
915 EditorPrefs.SetInt(kPassFilterLegacyEditorPrefsKey, (int)m_PassFilterLegacy);
916 RebuildGraphViewerUI();
917 }
918
919 void OnResourceFilterChanged(ChangeEvent<Enum> evt)
920 {
921 m_ResourceFilter = (ResourceFilter) evt.newValue;
922 EditorPrefs.SetInt(kResourceFilterEditorPrefsKey, (int)m_ResourceFilter);
923 RebuildGraphViewerUI();
924 }
925
926 void RebuildPassFilterUI()
927 {
928 var passFilter = rootVisualElement.Q<EnumFlagsField>(Names.kPassFilterField);
929 passFilter.style.display = DisplayStyle.Flex;
930 // We don't know which callback was registered before, so unregister both.
931 passFilter.UnregisterCallback<ChangeEvent<Enum>>(OnPassFilterChanged);
932 passFilter.UnregisterCallback<ChangeEvent<Enum>>(OnPassFilterLegacyChanged);
933 if (m_CurrentDebugData.isNRPCompiler)
934 {
935 passFilter.Init(m_PassFilter);
936 passFilter.RegisterCallback<ChangeEvent<Enum>>(OnPassFilterChanged);
937 }
938 else
939 {
940 passFilter.Init(m_PassFilterLegacy);
941 passFilter.RegisterCallback<ChangeEvent<Enum>>(OnPassFilterLegacyChanged);
942 }
943 }
944
945 void RebuildResourceFilterUI()
946 {
947 var resourceFilter = rootVisualElement.Q<EnumFlagsField>(Names.kResourceFilterField);
948 resourceFilter.style.display = DisplayStyle.Flex;
949 resourceFilter.UnregisterCallback<ChangeEvent<Enum>>(OnResourceFilterChanged);
950 resourceFilter.Init(m_ResourceFilter);
951 resourceFilter.RegisterCallback<ChangeEvent<Enum>>(OnResourceFilterChanged);
952 }
953
954 void RebuildHeaderUI()
955 {
956 RebuildRenderGraphPopup();
957 RebuildExecutionPopup();
958 }
959
960 RenderGraph m_SelectedRenderGraph;
961
962 RenderGraph selectedRenderGraph
963 {
964 get => m_SelectedRenderGraph;
965 set
966 {
967 m_SelectedRenderGraph = value;
968 UpdateCaptureEnabledUIState();
969 }
970 }
971
972 string m_SelectedExecutionName;
973
974 string selectedExecutionName
975 {
976 get => m_SelectedExecutionName;
977 set
978 {
979 m_SelectedExecutionName = value;
980 UpdateCaptureEnabledUIState();
981 }
982 }
983
984 bool CaptureEnabled() => selectedExecutionName != null && selectedRenderGraph != null;
985
986 void UpdateCaptureEnabledUIState()
987 {
988 if (rootVisualElement?.childCount == 0)
989 return;
990
991 bool enabled = CaptureEnabled();
992 var captureButton = rootVisualElement.Q<Button>(Names.kCaptureButton);
993 captureButton.SetEnabled(enabled);
994
995 var renderGraphDropdownField = rootVisualElement.Q<DropdownField>(Names.kCurrentGraphDropdown);
996 renderGraphDropdownField.style.display = enabled ? DisplayStyle.Flex : DisplayStyle.None;
997
998 var executionDropdownField = rootVisualElement.Q<DropdownField>(Names.kCurrentExecutionDropdown);
999 executionDropdownField.style.display = enabled ? DisplayStyle.Flex : DisplayStyle.None;
1000 }
1001
1002 bool IsResourceVisible(RenderGraph.DebugData.ResourceData resource, RenderGraphResourceType type)
1003 {
1004 // Unused resources are always hidden
1005 if (resource.releasePassIndex == -1 && resource.creationPassIndex == -1)
1006 return false;
1007
1008 if (resource.imported && !m_ResourceFilter.HasFlag(ResourceFilter.ImportedResources))
1009 return false;
1010 if (type == RenderGraphResourceType.Texture && !m_ResourceFilter.HasFlag(ResourceFilter.Textures))
1011 return false;
1012 if (type == RenderGraphResourceType.Buffer && !m_ResourceFilter.HasFlag(ResourceFilter.Buffers))
1013 return false;
1014 if (type == RenderGraphResourceType.AccelerationStructure &&
1015 !m_ResourceFilter.HasFlag(ResourceFilter.AccelerationStructures))
1016 return false;
1017
1018 return true;
1019 }
1020
1021 bool IsPassVisible(RenderGraph.DebugData.PassData pass)
1022 {
1023 if (!pass.generateDebugData)
1024 return false;
1025
1026 if (m_CurrentDebugData.isNRPCompiler)
1027 {
1028 if (pass.culled && !m_PassFilter.HasFlag(PassFilter.CulledPasses))
1029 return false;
1030 if (pass.type == RenderGraphPassType.Compute && !m_PassFilter.HasFlag(PassFilter.ComputePasses))
1031 return false;
1032 if (pass.type == RenderGraphPassType.Raster && !m_PassFilter.HasFlag(PassFilter.RasterPasses))
1033 return false;
1034 if (pass.type == RenderGraphPassType.Unsafe && !m_PassFilter.HasFlag(PassFilter.UnsafePasses))
1035 return false;
1036 }
1037 else
1038 {
1039 if (pass.culled && !m_PassFilterLegacy.HasFlag(PassFilterLegacy.CulledPasses))
1040 return false;
1041 }
1042
1043 return true;
1044 }
1045
1046 static readonly string[] k_ResourceNames =
1047 { "Texture Resource", "Buffer Resource", "Acceleration Structure Resource" };
1048
1049
1050 // Pass title ellipsis must be generated manually because it can't be done right for rotated text using uss.
1051 void TruncatePassTitle(GeometryChangedEvent evt)
1052 {
1053 const int MaxPassTitleLenghtPx = 180;
1054
1055 if (evt.target is Label label)
1056 {
1057 bool wasTruncated = false;
1058 while (true)
1059 {
1060 var rect = label.MeasureTextSize(label.text, 0, VisualElement.MeasureMode.Undefined, 0,
1061 VisualElement.MeasureMode.Undefined);
1062
1063 if (float.IsNaN(rect.x))
1064 return; // layout not ready yet
1065
1066 var needsTruncate = rect.x > MaxPassTitleLenghtPx;
1067 if (!needsTruncate)
1068 break;
1069
1070 label.text = label.text.Remove(label.text.Length - 1, 1);
1071 wasTruncated = true;
1072 }
1073
1074 if (wasTruncated)
1075 label.text += "...";
1076 }
1077 }
1078
1079 /// <summary>
1080 /// Converts a physical file path (absolute or relative) to an AssetDatabase-compatible asset path.
1081 /// If the path does not point to a script under Assets or in a package, the path is returned as-is.
1082 /// </summary>
1083 /// <param name="physicalPath">The physical file path (absolute or relative) to convert.</param>
1084 /// <returns>An AssetDatabase-compatible asset path, or the untransformed string if the physical path
1085 /// could not be resolved to an asset path.</returns>
1086 /// <remarks>This method is internal for testing purposes only.</remarks>
1087 internal static string ScriptPathToAssetPath(string physicalPath)
1088 {
1089 // Make sure we have an absolute path with Unity separators, this is necessary for the following checks
1090 var absolutePath = System.IO.Path.GetFullPath(physicalPath).Replace(@"\", "/");
1091
1092 // Project assets start with data path (in Editor: <project-path>/Assets)
1093 if (absolutePath.StartsWith(Application.dataPath, StringComparison.OrdinalIgnoreCase))
1094 {
1095 var assetPath = absolutePath.Replace(Application.dataPath, "Assets");
1096 return assetPath;
1097 }
1098
1099 // Try to get the logical path mapped to this physical absolute path (for assets in a package)
1100 var logicalPath = FileUtil.GetLogicalPath(absolutePath);
1101 if (logicalPath != absolutePath)
1102 {
1103 return logicalPath;
1104 }
1105
1106 // Asset path cannot be found - invalid script path
1107 return physicalPath;
1108 }
1109
1110 UnityEngine.Object FindScriptAssetByScriptPath(string scriptPath)
1111 {
1112 var assetPath = ScriptPathToAssetPath(scriptPath);
1113 var asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(assetPath);
1114 if (asset == null)
1115 Debug.LogWarning($"Could not find a script asset to open for file path {scriptPath}");
1116 return asset;
1117 }
1118
1119 VisualElement CreatePassListItem(int passId, RenderGraph.DebugData.PassData pass, int visiblePassIndex)
1120 {
1121 var passListItem = new VisualElement();
1122 passListItem.AddToClassList(Classes.kPassListItem);
1123 passListItem.pickingMode = PickingMode.Ignore;
1124 passListItem.style.left = visiblePassIndex * kPassWidthPx;
1125
1126 var passTitle = new PassTitleLabel(pass.name);
1127 passTitle.tooltip = pass.name;
1128 passTitle.AddToClassList(Classes.kPassTitle);
1129 passTitle.RegisterCallback<GeometryChangedEvent>(TruncatePassTitle);
1130 passTitle.RegisterCallback<MouseOverEvent>(_ => HoverResourceGrid(visiblePassIndex, -1));
1131 passTitle.RegisterCallback<MouseOutEvent>(_ => HoverResourceGrid(-1, -1));
1132 passTitle.RegisterCallback<MouseMoveEvent>(_ => HoverResourceGrid(visiblePassIndex, -1));
1133 passTitle.RegisterCallback<ClickEvent>(evt => PassBlockClicked(evt, visiblePassIndex));
1134 passListItem.Add(passTitle);
1135
1136 var passMergeIndicator = new VisualElement();
1137 passMergeIndicator.AddToClassList(Classes.kPassMergeIndicator);
1138 if (pass.nrpInfo?.nativePassInfo?.mergedPassIds.Count > 1)
1139 {
1140 // Blue line do denote merged render passes
1141 passMergeIndicator.style.visibility = Visibility.Visible;
1142
1143 bool firstMergedPass = pass.nrpInfo.nativePassInfo.mergedPassIds[0] == passId;
1144 bool lastMergedPass = pass.nrpInfo.nativePassInfo.mergedPassIds[^1] == passId;
1145
1146 const int kBorderRadius = 2;
1147 const int kEdgeMargin = 2;
1148
1149 // Use margins to create a break between consecutive merged passes
1150 int width = kPassWidthPx;
1151 if (firstMergedPass)
1152 {
1153 passMergeIndicator.style.marginLeft = kEdgeMargin;
1154 passMergeIndicator.style.borderTopLeftRadius = kBorderRadius;
1155 passMergeIndicator.style.borderBottomLeftRadius = kBorderRadius;
1156 width -= kEdgeMargin;
1157 }
1158
1159 if (lastMergedPass)
1160 {
1161 passMergeIndicator.style.marginRight = kEdgeMargin;
1162 passMergeIndicator.style.borderTopRightRadius = kBorderRadius;
1163 passMergeIndicator.style.borderBottomRightRadius = kBorderRadius;
1164 width -= kEdgeMargin;
1165 }
1166
1167 passMergeIndicator.style.width = width;
1168 }
1169
1170 passListItem.Add(passMergeIndicator);
1171
1172 var passBlock = new VisualElement();
1173 passBlock.AddToClassList(Classes.kPassBlock);
1174 passBlock.RegisterCallback<MouseEnterEvent>(_ =>
1175 {
1176 var scriptLinkBlock = new VisualElement();
1177 scriptLinkBlock.pickingMode = PickingMode.Ignore;
1178 scriptLinkBlock.AddToClassList(Classes.kPassBlock);
1179 scriptLinkBlock.AddToClassList(Classes.kPassBlockScriptLink);
1180 passBlock.Add(scriptLinkBlock);
1181 });
1182 passBlock.RegisterCallback<MouseLeaveEvent>(_ => passBlock.Clear());
1183 passBlock.RegisterCallback<MouseUpEvent>(evt =>
1184 {
1185 if (evt.button == 0)
1186 {
1187 var scriptAsset = FindScriptAssetByScriptPath(pass.scriptInfo.filePath);
1188 AssetDatabase.OpenAsset(scriptAsset, pass.scriptInfo.line);
1189 }
1190 evt.StopImmediatePropagation();
1191 });
1192
1193 var passInfo = new PassElementInfo
1194 {
1195 passBlock = passBlock,
1196 passTitle = passTitle,
1197 passId = passId,
1198 isCulled = pass.culled,
1199 isAsync = pass.async
1200 };
1201
1202 m_PassElementsInfo.Add(passInfo);
1203 passListItem.Add(passBlock);
1204 return passListItem;
1205 }
1206
1207 VisualElement CreatePassGridLine(int gridLineHeightPx, int offsetPx)
1208 {
1209 var gridline = new VisualElement();
1210 gridline.AddToClassList(Classes.kGridLine);
1211 gridline.style.left = offsetPx;
1212 gridline.style.height = gridLineHeightPx;
1213 gridline.pickingMode = PickingMode.Ignore;
1214 return gridline;
1215 }
1216
1217 VisualElement CreateResourceTypeIcon(RenderGraphResourceType type)
1218 {
1219 var resourceTypeIcon = new VisualElement();
1220 resourceTypeIcon.AddToClassList(Classes.kResourceIcon);
1221 string className = type switch
1222 {
1223 RenderGraphResourceType.Texture => Classes.kResourceIconTexture,
1224 RenderGraphResourceType.Buffer => Classes.kResourceIconBuffer,
1225 RenderGraphResourceType.AccelerationStructure => Classes.kResourceIconAccelerationStructure,
1226 _ => throw new ArgumentOutOfRangeException(nameof(type))
1227 };
1228 resourceTypeIcon.AddToClassList(className);
1229 resourceTypeIcon.tooltip = k_ResourceNames[(int)type];
1230 return resourceTypeIcon;
1231 }
1232
1233 VisualElement CreateResourceListItem(RenderGraph.DebugData.ResourceData res, RenderGraphResourceType type)
1234 {
1235 var resourceListItem = new VisualElement();
1236 resourceListItem.AddToClassList(Classes.kResourceListItem);
1237
1238 var resourceTitleContainer = new VisualElement();
1239 resourceTitleContainer.Add(CreateResourceTypeIcon(type));
1240
1241 var resourceLabel = new Label();
1242 resourceLabel.text = res.name;
1243 resourceLabel.tooltip = res.name;
1244 resourceTitleContainer.Add(resourceLabel);
1245
1246 var iconContainer = new VisualElement();
1247 iconContainer.AddToClassList(Classes.kResourceIconContainer);
1248
1249 var importedIcon = new VisualElement();
1250 importedIcon.AddToClassList(Classes.kResourceIcon);
1251 importedIcon.AddToClassList(Classes.kResourceIconImported);
1252 importedIcon.tooltip = "Imported resource";
1253 importedIcon.style.visibility = res.imported ? Visibility.Visible : Visibility.Hidden;
1254 iconContainer.Add(importedIcon);
1255
1256 int numIcons = 1 + iconContainer.childCount;
1257 resourceLabel.style.maxWidth = kResourceColumnWidth - numIcons * kResourceIconSize - 4;
1258
1259 resourceListItem.Add(resourceTitleContainer);
1260 resourceListItem.Add(iconContainer);
1261
1262 return resourceListItem;
1263 }
1264
1265 int FindNextVisiblePassIndex(int passId)
1266 {
1267 while (passId < m_CurrentDebugData.passList.Count)
1268 {
1269 if (m_PassIdToVisiblePassIndex.TryGetValue(passId, out int visiblePassIndex))
1270 return visiblePassIndex;
1271 passId++;
1272 }
1273
1274 return -1;
1275 }
1276
1277 void CreateRWResourceBlockElement(int offsetPx, ResourceRWBlock block)
1278 {
1279 string accessType = null;
1280 if (block.read && block.write)
1281 {
1282 var triangle = new TriangleElement();
1283 triangle.width = kDependencyBlockWidthPx;
1284 triangle.height = kDependencyBlockHeightPx;
1285 triangle.color = EditorGUIUtility.isProSkin ? kReadWriteBlockFillColorDark : kReadWriteBlockFillColorLight;
1286 block.element = triangle;
1287 block.element.AddToClassList(Classes.kResourceDependencyBlockReadWrite);
1288 accessType = "Read/write";
1289 }
1290 else
1291 {
1292 block.element = new VisualElement();
1293 if (block.read)
1294 {
1295 block.element.AddToClassList(Classes.kResourceDependencyBlockRead);
1296 accessType = "Read";
1297 }
1298 else if (block.write)
1299 {
1300 block.element.AddToClassList(Classes.kResourceDependencyBlockWrite);
1301 accessType = "Write";
1302 }
1303 }
1304
1305 string tooltip = string.Empty;
1306 if (!string.IsNullOrEmpty(accessType))
1307 tooltip += $"<b>{accessType}</b> access to this resource.";
1308
1309 if (block.usage != ResourceRWBlock.UsageFlags.None)
1310 {
1311 string resourceIconClassName = string.Empty;
1312
1313 if (block.HasMultipleUsageFlags())
1314 resourceIconClassName = Classes.kResourceIconMultipleUsage;
1315 else if (block.usage.HasFlag(ResourceRWBlock.UsageFlags.FramebufferFetch))
1316 resourceIconClassName = Classes.kResourceIconFbfetch;
1317 else if (block.usage.HasFlag(ResourceRWBlock.UsageFlags.UpdatesGlobalResource))
1318 resourceIconClassName = block.read || block.write ? Classes.kResourceIconGlobalLight : Classes.kResourceIconGlobalDark;
1319
1320 if (!string.IsNullOrEmpty(resourceIconClassName))
1321 {
1322 var usageIcon = new VisualElement();
1323 usageIcon.AddToClassList(Classes.kResourceIcon);
1324 usageIcon.AddToClassList(resourceIconClassName);
1325 block.element.Add(usageIcon);
1326 }
1327
1328 if (tooltip.Length > 0)
1329 tooltip += "<br><br>";
1330 tooltip += "Usage details:";
1331 if (block.usage.HasFlag(ResourceRWBlock.UsageFlags.FramebufferFetch))
1332 tooltip += "<br>- Read is using <b>framebuffer fetch</b>.";
1333 if (block.usage.HasFlag(ResourceRWBlock.UsageFlags.UpdatesGlobalResource))
1334 tooltip += "<br>- Updates a global resource slot.";
1335 }
1336
1337 block.tooltip = tooltip;
1338 block.element.style.left = offsetPx;
1339 block.element.AddToClassList(Classes.kResourceDependencyBlock);
1340 }
1341
1342 VisualElement CreateResourceGridRow(
1343 RenderGraph.DebugData.ResourceData res,
1344 RenderGraphResourceType resourceType,
1345 int resourceIndex,
1346 ResourceElementInfo elementInfo,
1347 int visibleResourceIndex)
1348 {
1349 var row = new VisualElement();
1350 row.pickingMode = PickingMode.Ignore;
1351 row.AddToClassList(Classes.kResourceGridRow);
1352
1353 // Dashed line connecting resource to first usage
1354 int firstVisiblePassUsingResource = FindNextVisiblePassIndex(res.creationPassIndex);
1355 if (firstVisiblePassUsingResource == -1)
1356 {
1357 return row; // Early out - no pass uses the resource
1358 }
1359
1360 int firstUsePassOffsetPx = firstVisiblePassUsingResource * kPassWidthPx;
1361 const int resourceHelperLineMargin = 4;
1362 var resourceHelperLine = new VisualElement();
1363 resourceHelperLine.AddToClassList(Classes.kResourceHelperLine);
1364 resourceHelperLine.style.marginLeft = resourceHelperLineMargin;
1365 resourceHelperLine.style.width = firstUsePassOffsetPx - 2 * resourceHelperLineMargin;
1366 resourceHelperLine.pickingMode = PickingMode.Ignore;
1367 row.Add(resourceHelperLine);
1368 elementInfo.resourceHelperLine = resourceHelperLine;
1369
1370 // Wide gray block indicating first <-> last use range
1371 var usageRangeBlock = new VisualElement();
1372 usageRangeBlock.AddToClassList(Classes.kResourceUsageRangeBlock);
1373
1374 int passIdAfterLastUse = res.releasePassIndex + 1;
1375 int visiblePassAfterLastUse = FindNextVisiblePassIndex(passIdAfterLastUse);
1376 if (visiblePassAfterLastUse == -1)
1377 visiblePassAfterLastUse = m_PassElementsInfo.Count; // last pass uses resource
1378
1379 int numVisiblePassesUsed = visiblePassAfterLastUse - firstVisiblePassUsingResource;
1380
1381 usageRangeBlock.style.position = Position.Absolute;
1382 usageRangeBlock.style.left = firstUsePassOffsetPx;
1383 usageRangeBlock.style.width = numVisiblePassesUsed * kPassWidthPx;
1384 row.Add(usageRangeBlock);
1385 elementInfo.usageRangeBlock = usageRangeBlock;
1386
1387 // Read/write/read-write blocks
1388 List<ResourceRWBlock> blocks = new(m_CurrentDebugData.passList.Count);
1389 for (int passId = 0; passId < m_CurrentDebugData.passList.Count; passId++)
1390 blocks.Add(new ResourceRWBlock());
1391
1392 foreach (int readPassId in res.consumerList)
1393 blocks[readPassId].read = true;
1394
1395 foreach (var writePassId in res.producerList)
1396 blocks[writePassId].write = true;
1397
1398 for (int passId = 0; passId < blocks.Count; passId++)
1399 {
1400 ResourceRWBlock block = blocks[passId];
1401 if (m_PassIdToVisiblePassIndex.TryGetValue(passId, out int visiblePassIndex))
1402 {
1403 var pass = m_CurrentDebugData.passList[passId];
1404 if (resourceType == RenderGraphResourceType.Texture && pass.nrpInfo != null)
1405 {
1406 if (pass.nrpInfo.textureFBFetchList.Contains(resourceIndex))
1407 block.usage |= ResourceRWBlock.UsageFlags.FramebufferFetch;
1408 if (pass.nrpInfo.setGlobals.Contains(resourceIndex))
1409 block.usage |= ResourceRWBlock.UsageFlags.UpdatesGlobalResource;
1410 }
1411
1412 if (!block.read && !block.write && block.usage == ResourceRWBlock.UsageFlags.None)
1413 continue; // No need to create a visual element
1414
1415 int offsetPx = visiblePassIndex * kPassWidthPx;
1416 CreateRWResourceBlockElement(offsetPx, block);
1417 block.visibleResourceIndex = visibleResourceIndex;
1418 row.Add(block.element);
1419 m_PassElementsInfo[visiblePassIndex].resourceBlocks.Add(block);
1420 }
1421 }
1422
1423 return row;
1424 }
1425
1426 void ClearGraphViewerUI()
1427 {
1428 rootVisualElement.Q<VisualElement>(Names.kGridlineContainer)?.Clear();
1429 rootVisualElement.Q<VisualElement>(Names.kPassList)?.Clear();
1430 rootVisualElement.Q<VisualElement>(Names.kResourceListScrollView)?.Clear();
1431 rootVisualElement.Q<VisualElement>(Names.kResourceGrid)?.Clear();
1432
1433 m_PassElementsInfo.Clear();
1434 m_ResourceElementsInfo.Clear();
1435 m_PassIdToVisiblePassIndex.Clear();
1436 m_VisiblePassIndexToPassId.Clear();
1437 m_CurrentSelectedVisiblePassIndex = -1;
1438 m_CurrentHoveredVisibleResourceIndex = -1;
1439 m_CurrentHoveredVisiblePassIndex = -1;
1440
1441 ClearPassHighlight();
1442 ClearResourceHighlight();
1443 }
1444
1445 void RebuildGraphViewerUI()
1446 {
1447 if (rootVisualElement?.childCount == 0)
1448 return;
1449
1450 ClearGraphViewerUI();
1451 ClearEmptyStateMessage();
1452
1453 if (m_RegisteredGraphs.Count == 0)
1454 {
1455 SetEmptyStateMessage(EmptyStateReason.NoGraphRegistered);
1456 return;
1457 }
1458
1459 if (!CaptureEnabled())
1460 {
1461 SetEmptyStateMessage(EmptyStateReason.NoExecutionRegistered);
1462 return;
1463 }
1464
1465 if (m_CurrentDebugData == null)
1466 {
1467 SetEmptyStateMessage(EmptyStateReason.NoDataAvailable);
1468 return;
1469 }
1470
1471 // Pass list
1472 var passList = rootVisualElement.Q<VisualElement>(Names.kPassList);
1473 int visiblePassIndex = 0;
1474 for (int passId = 0; passId < m_CurrentDebugData.passList.Count; passId++)
1475 {
1476 var pass = m_CurrentDebugData.passList[passId];
1477 if (!IsPassVisible(pass))
1478 continue;
1479
1480 passList.Add(CreatePassListItem(passId, pass, visiblePassIndex));
1481 m_PassIdToVisiblePassIndex.Add(passId, visiblePassIndex);
1482 m_VisiblePassIndexToPassId.Add(visiblePassIndex, passId);
1483 visiblePassIndex++;
1484 }
1485
1486 int numVisiblePasses = visiblePassIndex;
1487 if (numVisiblePasses == 0)
1488 {
1489 SaveSplitViewFixedPaneHeight();
1490 ClearGraphViewerUI();
1491 SetEmptyStateMessage(EmptyStateReason.EmptyPassFilterResult);
1492 return;
1493 }
1494
1495 ResetPassBlockState();
1496
1497 // Resource list & grid
1498 var resourceListScrollView = rootVisualElement.Q<ScrollView>(Names.kResourceListScrollView);
1499 var resourceGrid = rootVisualElement.Q<VisualElement>(Names.kResourceGrid);
1500 resourceGrid.style.width = numVisiblePasses * kPassWidthPx + kPassTitleAllowanceMargin;
1501
1502 int visibleResourceIndex = 0;
1503 for (int t = 0; t < (int) RenderGraphResourceType.Count; t++)
1504 {
1505 var resourceType = (RenderGraphResourceType) t;
1506 var resourceList = m_CurrentDebugData.resourceLists[t];
1507 for (int resourceIndex = 0; resourceIndex < resourceList.Count; resourceIndex++)
1508 {
1509 var res = resourceList[resourceIndex];
1510 if (!IsResourceVisible(res, resourceType))
1511 continue;
1512
1513 var elementInfo = new ResourceElementInfo
1514 {
1515 type = resourceType,
1516 index = resourceIndex,
1517 firstPassId = res.creationPassIndex,
1518 lastPassId = res.releasePassIndex
1519 };
1520 elementInfo.resourceListItem = CreateResourceListItem(res, resourceType);
1521 resourceListScrollView.Add(elementInfo.resourceListItem);
1522 resourceGrid.Add(CreateResourceGridRow(res, resourceType, resourceIndex,
1523 elementInfo, visibleResourceIndex));
1524 m_ResourceElementsInfo.Add(elementInfo);
1525
1526 visibleResourceIndex++;
1527 }
1528 }
1529
1530 int numVisibleResources = visibleResourceIndex;
1531 if (numVisibleResources == 0)
1532 {
1533 SaveSplitViewFixedPaneHeight();
1534 ClearGraphViewerUI();
1535 SetEmptyStateMessage(EmptyStateReason.EmptyResourceFilterResult);
1536 return;
1537 }
1538
1539 // Add a padding item to ensure horizontal scrollbar doesn't cause the listviews to get out of sync
1540 var resourcePaddingItem = new VisualElement();
1541 resourcePaddingItem.AddToClassList(Classes.kResourceListPaddingItem);
1542 resourceListScrollView.Add(resourcePaddingItem);
1543
1544 // Grid lines
1545 int gridLineHeightPx = numVisibleResources * kResourceRowHeightPx + kResourceGridMarginTopPx;
1546 int gridLineOffsetPx = 0;
1547 var gridlineContainer = rootVisualElement.Q<VisualElement>(Names.kGridlineContainer);
1548 for (int passIndex = 0; passIndex < numVisiblePasses; passIndex++)
1549 {
1550 var gridLine = CreatePassGridLine(gridLineHeightPx, gridLineOffsetPx);
1551 gridlineContainer.Add(gridLine);
1552 gridLineOffsetPx += kPassWidthPx;
1553 m_PassElementsInfo[passIndex].leftGridLine = gridLine;
1554 }
1555
1556 gridlineContainer.Add(CreatePassGridLine(gridLineHeightPx, gridLineOffsetPx));
1557
1558 // Hover overlay element
1559 var hoverOverlay = rootVisualElement.Q(Names.kHoverOverlay);
1560 hoverOverlay.style.marginTop = kResourceGridMarginTopPx;
1561 hoverOverlay.style.width = gridLineOffsetPx;
1562 hoverOverlay.style.height = gridLineHeightPx;
1563 hoverOverlay.focusable = true;
1564
1565 // Side panel
1566 PopulateResourceList();
1567 PopulatePassList();
1568 }
1569
1570 void RebuildUI()
1571 {
1572 rootVisualElement.Clear();
1573
1574 var visualTreeAsset = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(k_TemplatePath);
1575 visualTreeAsset.CloneTree(rootVisualElement);
1576
1577 var themeStyleSheet =
1578 AssetDatabase.LoadAssetAtPath<StyleSheet>(EditorGUIUtility.isProSkin
1579 ? k_DarkStylePath
1580 : k_LightStylePath);
1581 rootVisualElement.styleSheets.Add(themeStyleSheet);
1582
1583 RebuildHeaderUI();
1584 RebuildGraphViewerUI();
1585 }
1586
1587 // Initialize, register callbacks & manipulators etc. once
1588 void InitializePersistentElements()
1589 {
1590 // Header elements
1591 var captureButton = rootVisualElement.Q<Button>(Names.kCaptureButton);
1592 captureButton.SetEnabled(CaptureEnabled());
1593 captureButton.RegisterCallback<ClickEvent>(_ => RequestCaptureSelectedExecution());
1594
1595 var renderGraphDropdownField = rootVisualElement.Q<DropdownField>(Names.kCurrentGraphDropdown);
1596 renderGraphDropdownField.RegisterValueChangedCallback(evt => SelectedRenderGraphChanged(evt.newValue));
1597
1598 var executionDropdownField = rootVisualElement.Q<DropdownField>(Names.kCurrentExecutionDropdown);
1599 executionDropdownField.RegisterValueChangedCallback(evt =>
1600 {
1601 EditorPrefs.SetString(kSelectedExecutionEditorPrefsKey, evt.newValue);
1602 SelectedExecutionChanged(evt.newValue);
1603 });
1604
1605 // After delay, serialize currently selected execution. This avoids an issue where activating a new camera
1606 // causes RG Viewer to change the execution just because it was serialized some time in the past.
1607 executionDropdownField.schedule.Execute(() =>
1608 {
1609 EditorPrefs.SetString(kSelectedExecutionEditorPrefsKey, selectedExecutionName);
1610 }).ExecuteLater(500);
1611
1612 var passFilter = rootVisualElement.Q<EnumFlagsField>(Names.kPassFilterField);
1613 passFilter.style.display = DisplayStyle.None; // Hidden until the compiler is known
1614
1615 var resourceFilter = rootVisualElement.Q<EnumFlagsField>(Names.kResourceFilterField);
1616 resourceFilter.style.display = DisplayStyle.None; // Hidden until the compiler is known
1617
1618 // Hover overlay
1619 var hoverOverlay = rootVisualElement.Q(Names.kHoverOverlay);
1620 hoverOverlay.RegisterCallback<MouseOverEvent>(_ => HoverResourceGrid(-1, -1));
1621 hoverOverlay.RegisterCallback<MouseOutEvent>(_ => HoverResourceGrid(-1, -1));
1622 hoverOverlay.RegisterCallback<MouseMoveEvent>(ResourceGridHovered);
1623 hoverOverlay.RegisterCallback<ClickEvent>(ResourceGridClicked);
1624 hoverOverlay.RegisterCallback<TooltipEvent>(ResourceGridTooltipDisplayed, TrickleDown.TrickleDown);
1625 hoverOverlay.RegisterCallback<KeyUpEvent>(KeyPressed);
1626
1627 rootVisualElement.Q(Names.kMainContainer).RegisterCallback<MouseUpEvent>(_ => DeselectPass());
1628
1629 // Resource grid manipulation
1630 var resourceListScrollView = rootVisualElement.Q<ScrollView>(Names.kResourceListScrollView);
1631 var passListScrollView = rootVisualElement.Q<ScrollView>(Names.kPassListScrollView);
1632 var resourceGridScrollView = rootVisualElement.Q<ScrollView>(Names.kResourceGridScrollView);
1633 m_PanManipulator = new PanManipulator(this);
1634 resourceGridScrollView.AddManipulator(m_PanManipulator);
1635 resourceGridScrollView.mode = ScrollViewMode.VerticalAndHorizontal;
1636
1637 // Sync resource grid scrollbar state to resource list and pass list scrollbars
1638 resourceListScrollView.verticalScrollerVisibility = ScrollerVisibility.Hidden;
1639 resourceListScrollView.horizontalScrollerVisibility = ScrollerVisibility.Hidden;
1640 resourceGridScrollView.verticalScroller.valueChanged += value =>
1641 resourceListScrollView.scrollOffset = new Vector2(resourceGridScrollView.scrollOffset.x, value);
1642
1643 passListScrollView.verticalScrollerVisibility = ScrollerVisibility.Hidden;
1644 passListScrollView.horizontalScrollerVisibility = ScrollerVisibility.Hidden;
1645 resourceGridScrollView.horizontalScroller.valueChanged += value =>
1646 passListScrollView.scrollOffset = new Vector2(value, passListScrollView.scrollOffset.y);
1647
1648 // Disable mouse wheel on the scroll views that are synced to the resource grid
1649 resourceListScrollView.RegisterCallback<WheelEvent>(evt => evt.StopImmediatePropagation(), TrickleDown.TrickleDown);
1650 passListScrollView.RegisterCallback<WheelEvent>(evt => evt.StopImmediatePropagation(), TrickleDown.TrickleDown);
1651
1652 InitializeSidePanel();
1653 }
1654
1655 void OnGraphRegistered(RenderGraph graph)
1656 {
1657 m_RegisteredGraphs.Add(graph, new HashSet<string>());
1658 RebuildHeaderUI();
1659 }
1660
1661 void OnGraphUnregistered(RenderGraph graph)
1662 {
1663 m_RegisteredGraphs.Remove(graph);
1664 RebuildHeaderUI();
1665 if (m_RegisteredGraphs.Count == 0)
1666 RebuildGraphViewerUI();
1667 }
1668
1669 void OnExecutionRegistered(RenderGraph graph, string name)
1670 {
1671 m_RegisteredGraphs.TryGetValue(graph, out var executionList);
1672 Debug.Assert(executionList != null,
1673 $"RenderGraph {graph.name} should be registered before registering its executions.");
1674 executionList.Add(name);
1675
1676 RebuildHeaderUI();
1677
1678 // Automatically capture data when window is opened if not available yet.
1679 if (m_CurrentDebugData == null)
1680 RequestCaptureSelectedExecution();
1681 }
1682
1683 void OnExecutionUnregistered(RenderGraph graph, string name)
1684 {
1685 m_RegisteredGraphs.TryGetValue(graph, out var executionList);
1686 Debug.Assert(executionList != null,
1687 $"RenderGraph {graph.name} should be registered before unregistering its executions.");
1688 executionList.Remove(name);
1689
1690 RebuildHeaderUI();
1691 }
1692
1693 void OnDebugDataCaptured()
1694 {
1695 // Refresh delayed. That way we don't break rendering if something goes wrong on the UI layer.
1696 EditorApplication.delayCall += () =>
1697 {
1698 if (selectedRenderGraph != null)
1699 {
1700 var debugData = selectedRenderGraph.GetDebugData(selectedExecutionName);
1701 if (debugData != null)
1702 {
1703 m_CurrentDebugData = debugData;
1704
1705 RebuildPassFilterUI();
1706 RebuildResourceFilterUI();
1707 RebuildGraphViewerUI();
1708 }
1709 }
1710 };
1711 }
1712
1713 void OnEnable()
1714 {
1715 var registeredGraph = RenderGraph.GetRegisteredRenderGraphs();
1716 foreach (var graph in registeredGraph)
1717 m_RegisteredGraphs.Add(graph, new HashSet<string>());
1718
1719 SubscribeToRenderGraphEvents();
1720
1721 if (EditorPrefs.HasKey(kPassFilterLegacyEditorPrefsKey))
1722 m_PassFilterLegacy = (PassFilterLegacy)EditorPrefs.GetInt(kPassFilterLegacyEditorPrefsKey);
1723 if (EditorPrefs.HasKey(kPassFilterEditorPrefsKey))
1724 m_PassFilter = (PassFilter)EditorPrefs.GetInt(kPassFilterEditorPrefsKey);
1725 if (EditorPrefs.HasKey(kResourceFilterEditorPrefsKey))
1726 m_ResourceFilter = (ResourceFilter)EditorPrefs.GetInt(kResourceFilterEditorPrefsKey);
1727
1728 GraphicsToolLifetimeAnalytic.WindowOpened<RenderGraphViewer>();
1729 }
1730
1731 void CreateGUI()
1732 {
1733 m_ResourceListIcon = AssetDatabase.LoadAssetAtPath<Texture2D>(string.Format(k_ResourceListIconPath, EditorGUIUtility.isProSkin ? "d_" : ""));
1734 m_PassListIcon = AssetDatabase.LoadAssetAtPath<Texture2D>(string.Format(k_PassListIconPath, EditorGUIUtility.isProSkin ? "d_" : ""));
1735
1736 RebuildUI();
1737 InitializePersistentElements();
1738
1739 // Automatically capture data when window is opened if not available yet.
1740 if (m_CurrentDebugData == null)
1741 RequestCaptureSelectedExecution();
1742 }
1743
1744 void OnDisable()
1745 {
1746 UnsubscribeToRenderGraphEvents();
1747 GraphicsToolLifetimeAnalytic.WindowClosed<RenderGraphViewer>();
1748 }
1749
1750 void SubscribeToRenderGraphEvents()
1751 {
1752 if (RenderGraph.isRenderGraphViewerActive)
1753 return;
1754
1755 RenderGraph.isRenderGraphViewerActive = true;
1756 RenderGraph.onGraphRegistered += OnGraphRegistered;
1757 RenderGraph.onGraphUnregistered += OnGraphUnregistered;
1758 RenderGraph.onExecutionRegistered += OnExecutionRegistered;
1759 RenderGraph.onExecutionUnregistered += OnExecutionUnregistered;
1760 RenderGraph.onDebugDataCaptured += OnDebugDataCaptured;
1761 }
1762
1763 void UnsubscribeToRenderGraphEvents()
1764 {
1765 if (!RenderGraph.isRenderGraphViewerActive)
1766 return;
1767
1768 RenderGraph.isRenderGraphViewerActive = false;
1769 RenderGraph.onGraphRegistered -= OnGraphRegistered;
1770 RenderGraph.onGraphUnregistered -= OnGraphUnregistered;
1771 RenderGraph.onExecutionRegistered -= OnExecutionRegistered;
1772 RenderGraph.onExecutionUnregistered -= OnExecutionUnregistered;
1773 RenderGraph.onDebugDataCaptured -= OnDebugDataCaptured;
1774 }
1775
1776 void Update()
1777 {
1778 // UUM-70378: In case the OnDisable Unsubscribes to Render Graph events when coming back from a Maximized state
1779 SubscribeToRenderGraphEvents();
1780 }
1781 }
1782
1783 [UxmlElement]
1784 internal partial class TriangleElement : VisualElement
1785 {
1786 public int width { get; set; }
1787
1788 public int height { get; set; }
1789
1790 public Color color { get; set; }
1791
1792 public TriangleElement()
1793 {
1794 generateVisualContent += ctx =>
1795 {
1796 var painter = ctx.painter2D;
1797 painter.fillColor = color;
1798 painter.BeginPath();
1799 painter.MoveTo(new Vector2(0, height));
1800 painter.LineTo(new Vector2(0, 0));
1801 painter.LineTo(new Vector2(width, 0));
1802 painter.ClosePath();
1803 painter.Fill();
1804 };
1805 }
1806 }
1807}