A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using UnityEditor.UIElements;
4using UnityEditorInternal;
5using UnityEngine;
6using UnityEngine.Rendering.RenderGraphModule;
7using UnityEngine.UIElements;
8
9namespace UnityEditor.Rendering
10{
11 public partial class RenderGraphViewer
12 {
13 static readonly string[] k_PassTypeNames =
14 {
15 "Legacy Render Pass",
16 "Unsafe Render Pass",
17 "Raster Render Pass",
18 "Compute Pass"
19 };
20
21 static partial class Names
22 {
23 public const string kPanelContainer = "panel-container";
24 public const string kResourceListFoldout = "panel-resource-list";
25 public const string kPassListFoldout = "panel-pass-list";
26 public const string kResourceSearchField = "resource-search-field";
27 public const string kPassSearchField = "pass-search-field";
28 }
29 static partial class Classes
30 {
31 public const string kPanelListLineBreak = "panel-list__line-break";
32 public const string kPanelListItem = "panel-list__item";
33 public const string kPanelListItemSelectionAnimation = "panel-list__item--selection-animation";
34 public const string kPanelResourceListItem = "panel-resource-list__item";
35 public const string kPanelPassListItem = "panel-pass-list__item";
36 public const string kSubHeaderText = "sub-header-text";
37 public const string kInfoFoldout = "info-foldout";
38 public const string kInfoFoldoutSecondaryText = "info-foldout__secondary-text";
39 public const string kCustomFoldoutArrow = "custom-foldout-arrow";
40 }
41
42 static readonly System.Text.RegularExpressions.Regex k_TagRegex = new ("<[^>]*>");
43 const string k_SelectionColorBeginTag = "<mark=#3169ACAB>";
44 const string k_SelectionColorEndTag = "</mark>";
45
46 TwoPaneSplitView m_SidePanelSplitView;
47 bool m_ResourceListExpanded = true;
48 bool m_PassListExpanded = true;
49 float m_SidePanelVerticalAspectRatio = 0.5f;
50 float m_SidePanelFixedPaneHeight = 0;
51 float m_ContentSplitViewFixedPaneWidth = 280;
52
53 Dictionary<VisualElement, List<TextElement>> m_ResourceDescendantCache = new ();
54 Dictionary<VisualElement, List<TextElement>> m_PassDescendantCache = new ();
55
56 void InitializeSidePanel()
57 {
58 m_SidePanelSplitView = rootVisualElement.Q<TwoPaneSplitView>(Names.kPanelContainer);
59 rootVisualElement.RegisterCallback<GeometryChangedEvent>(_ =>
60 {
61 SaveSplitViewFixedPaneHeight(); // Window resized - save the current pane height
62 UpdatePanelHeights();
63 });
64
65 var contentSplitView = rootVisualElement.Q<TwoPaneSplitView>(Names.kContentContainer);
66 contentSplitView.fixedPaneInitialDimension = m_ContentSplitViewFixedPaneWidth;
67 contentSplitView.fixedPaneIndex = 1;
68 contentSplitView.fixedPane?.RegisterCallback<GeometryChangedEvent>(_ =>
69 {
70 float? w = contentSplitView.fixedPane?.resolvedStyle?.width;
71 if (w.HasValue)
72 m_ContentSplitViewFixedPaneWidth = w.Value;
73 });
74
75 // Callbacks for dynamic height allocation between resource & pass lists
76 HeaderFoldout resourceListFoldout = rootVisualElement.Q<HeaderFoldout>(Names.kResourceListFoldout);
77 resourceListFoldout.value = m_ResourceListExpanded;
78 resourceListFoldout.RegisterValueChangedCallback(evt =>
79 {
80 if (m_ResourceListExpanded)
81 SaveSplitViewFixedPaneHeight(); // Closing the foldout - save the current pane height
82
83 m_ResourceListExpanded = resourceListFoldout.value;
84 UpdatePanelHeights();
85 });
86 resourceListFoldout.icon = m_ResourceListIcon;
87 resourceListFoldout.contextMenuGenerator = () => CreateContextMenu(resourceListFoldout.Q<ScrollView>());
88
89 HeaderFoldout passListFoldout = rootVisualElement.Q<HeaderFoldout>(Names.kPassListFoldout);
90 passListFoldout.value = m_PassListExpanded;
91 passListFoldout.RegisterValueChangedCallback(evt =>
92 {
93 if (m_PassListExpanded)
94 SaveSplitViewFixedPaneHeight(); // Closing the foldout - save the current pane height
95
96 m_PassListExpanded = passListFoldout.value;
97 UpdatePanelHeights();
98 });
99 passListFoldout.icon = m_PassListIcon;
100 passListFoldout.contextMenuGenerator = () => CreateContextMenu(passListFoldout.Q<ScrollView>());
101
102 // Search fields
103 var resourceSearchField = rootVisualElement.Q<ToolbarSearchField>(Names.kResourceSearchField);
104 resourceSearchField.placeholderText = "Search";
105 resourceSearchField.RegisterValueChangedCallback(evt => OnSearchFilterChanged(m_ResourceDescendantCache, evt.newValue));
106
107 var passSearchField = rootVisualElement.Q<ToolbarSearchField>(Names.kPassSearchField);
108 passSearchField.placeholderText = "Search";
109 passSearchField.RegisterValueChangedCallback(evt => OnSearchFilterChanged(m_PassDescendantCache, evt.newValue));
110 }
111
112 bool IsSearchFilterMatch(string str, string searchString, out int startIndex, out int endIndex)
113 {
114 startIndex = -1;
115 endIndex = -1;
116
117 startIndex = str.IndexOf(searchString, 0, StringComparison.CurrentCultureIgnoreCase);
118 if (startIndex == -1)
119 return false;
120
121 endIndex = startIndex + searchString.Length - 1;
122 return true;
123 }
124
125 void OnSearchFilterChanged(Dictionary<VisualElement, List<TextElement>> elementCache, string searchString)
126 {
127 // Display filter
128 foreach (var (foldout, descendants) in elementCache)
129 {
130 bool anyDescendantMatchesSearch = false;
131 foreach (var elem in descendants)
132 {
133 // Remove any existing highlight
134 var text = elem.text;
135 var hasHighlight = k_TagRegex.IsMatch(text);
136 text = k_TagRegex.Replace(text, string.Empty);
137 if (!IsSearchFilterMatch(text, searchString, out int startHighlight, out int endHighlight))
138 {
139 if (hasHighlight)
140 elem.text = text;
141 continue;
142 }
143
144
145 text = text.Insert(startHighlight, k_SelectionColorBeginTag);
146 text = text.Insert(endHighlight + k_SelectionColorBeginTag.Length + 1, k_SelectionColorEndTag);
147 elem.text = text;
148 anyDescendantMatchesSearch = true;
149 }
150 foldout.style.display = anyDescendantMatchesSearch ? DisplayStyle.Flex : DisplayStyle.None;
151 }
152 }
153
154 void SetChildFoldoutsExpanded(VisualElement elem, bool expanded)
155 {
156 elem.Query<Foldout>().ForEach(f => f.value = expanded);
157 }
158
159 GenericMenu CreateContextMenu(VisualElement content)
160 {
161 var menu = new GenericMenu();
162 menu.AddItem(new GUIContent("Collapse All"), false, () => SetChildFoldoutsExpanded(content, false));
163 menu.AddItem(new GUIContent("Expand All"), false, () => SetChildFoldoutsExpanded(content, true));
164 return menu;
165 }
166
167 void PopulateResourceList()
168 {
169 ScrollView content = rootVisualElement.Q<HeaderFoldout>(Names.kResourceListFoldout).Q<ScrollView>();
170 content.Clear();
171
172 UpdatePanelHeights();
173
174 m_ResourceDescendantCache.Clear();
175
176 int visibleResourceIndex = 0;
177 foreach (var visibleResourceElement in m_ResourceElementsInfo)
178 {
179 var resourceData = m_CurrentDebugData.resourceLists[(int)visibleResourceElement.type][visibleResourceElement.index];
180
181 var resourceItem = new Foldout();
182 resourceItem.text = resourceData.name;
183 resourceItem.value = false;
184 resourceItem.userData = visibleResourceIndex;
185 resourceItem.AddToClassList(Classes.kPanelListItem);
186 resourceItem.AddToClassList(Classes.kPanelResourceListItem);
187 resourceItem.AddToClassList(Classes.kCustomFoldoutArrow);
188 visibleResourceIndex++;
189
190 var iconContainer = new VisualElement();
191 iconContainer.AddToClassList(Classes.kResourceIconContainer);
192
193 var importedIcon = new VisualElement();
194 importedIcon.AddToClassList(Classes.kResourceIconImported);
195 importedIcon.tooltip = "Imported resource";
196 importedIcon.style.display = resourceData.imported ? DisplayStyle.Flex : DisplayStyle.None;
197 iconContainer.Add(importedIcon);
198
199 var foldoutCheckmark = resourceItem.Q("unity-checkmark");
200 // Add resource type icon before the label
201 foldoutCheckmark.parent.Insert(1, CreateResourceTypeIcon(visibleResourceElement.type));
202 foldoutCheckmark.parent.Add(iconContainer);
203 foldoutCheckmark.BringToFront(); // Move foldout checkmark to the right
204
205 // Add imported icon to the right of the foldout checkmark
206 var toggleContainer = resourceItem.Q<Toggle>();
207 toggleContainer.tooltip = resourceData.name;
208
209 RenderGraphResourceType type = visibleResourceElement.type;
210 if (type == RenderGraphResourceType.Texture && resourceData.textureData != null)
211 {
212 var lineBreak = new VisualElement();
213 lineBreak.AddToClassList(Classes.kPanelListLineBreak);
214 resourceItem.Add(lineBreak);
215 resourceItem.Add(new Label($"Size: {resourceData.textureData.width}x{resourceData.textureData.height}x{resourceData.textureData.depth}"));
216 resourceItem.Add(new Label($"Format: {resourceData.textureData.format.ToString()}"));
217 resourceItem.Add(new Label($"Clear: {resourceData.textureData.clearBuffer}"));
218 resourceItem.Add(new Label($"BindMS: {resourceData.textureData.bindMS}"));
219 resourceItem.Add(new Label($"Samples: {resourceData.textureData.samples}"));
220 if (m_CurrentDebugData.isNRPCompiler)
221 resourceItem.Add(new Label($"Memoryless: {resourceData.memoryless}"));
222 }
223 else if (type == RenderGraphResourceType.Buffer && resourceData.bufferData != null)
224 {
225 var lineBreak = new VisualElement();
226 lineBreak.AddToClassList(Classes.kPanelListLineBreak);
227 resourceItem.Add(lineBreak);
228 resourceItem.Add(new Label($"Count: {resourceData.bufferData.count}"));
229 resourceItem.Add(new Label($"Stride: {resourceData.bufferData.stride}"));
230 resourceItem.Add(new Label($"Target: {resourceData.bufferData.target.ToString()}"));
231 resourceItem.Add(new Label($"Usage: {resourceData.bufferData.usage.ToString()}"));
232 }
233
234 content.Add(resourceItem);
235
236 m_ResourceDescendantCache[resourceItem] = resourceItem.Query().Descendents<TextElement>().ToList();
237 }
238 }
239
240 void PopulatePassList()
241 {
242 HeaderFoldout headerFoldout = rootVisualElement.Q<HeaderFoldout>(Names.kPassListFoldout);
243 if (!m_CurrentDebugData.isNRPCompiler)
244 {
245 headerFoldout.style.display = DisplayStyle.None;
246 return;
247 }
248 headerFoldout.style.display = DisplayStyle.Flex;
249
250 ScrollView content = headerFoldout.Q<ScrollView>();
251 content.Clear();
252
253 UpdatePanelHeights();
254
255 m_PassDescendantCache.Clear();
256
257 void CreateTextElement(VisualElement parent, string text, string className = null)
258 {
259 var textElement = new TextElement();
260 textElement.text = text;
261 if (className != null)
262 textElement.AddToClassList(className);
263 parent.Add(textElement);
264 }
265
266 HashSet<int> addedPasses = new HashSet<int>();
267
268 foreach (var visiblePassElement in m_PassElementsInfo)
269 {
270 if (addedPasses.Contains(visiblePassElement.passId))
271 continue; // Add only one item per merged pass group
272
273 List<RenderGraph.DebugData.PassData> passDatas = new();
274 List<string> passNames = new();
275 var groupedPassIds = GetGroupedPassIds(visiblePassElement.passId);
276 foreach (int groupedId in groupedPassIds) {
277 addedPasses.Add(groupedId);
278 passDatas.Add(m_CurrentDebugData.passList[groupedId]);
279 passNames.Add(m_CurrentDebugData.passList[groupedId].name);
280 }
281
282 var passItem = new Foldout();
283 var passesText = string.Join(", ", passNames);
284 passItem.text = $"<b>{passesText}</b>";
285 passItem.Q<Toggle>().tooltip = passesText;
286 passItem.value = false;
287 passItem.userData = m_PassIdToVisiblePassIndex[visiblePassElement.passId];
288 passItem.AddToClassList(Classes.kPanelListItem);
289 passItem.AddToClassList(Classes.kPanelPassListItem);
290
291 //Native pass info (duplicated for each pass group so just look at the first)
292 var firstPassData = passDatas[0];
293 var nativePassInfo = firstPassData.nrpInfo?.nativePassInfo;
294
295 if (nativePassInfo != null)
296 {
297 if (nativePassInfo.mergedPassIds.Count == 1)
298 CreateTextElement(passItem, "Native Pass was created from Raster Render Pass.");
299 else if (nativePassInfo.mergedPassIds.Count > 1)
300 CreateTextElement(passItem, $"Native Pass was created by merging {nativePassInfo.mergedPassIds.Count} Raster Render Passes.");
301
302 CreateTextElement(passItem, "Pass break reasoning", Classes.kSubHeaderText);
303 CreateTextElement(passItem, nativePassInfo.passBreakReasoning);
304 }
305 else
306 {
307 CreateTextElement(passItem, "Pass break reasoning", Classes.kSubHeaderText);
308 var msg = $"This is a {k_PassTypeNames[(int) firstPassData.type]}. Only Raster Render Passes can be merged.";
309 msg = msg.Replace("a Unsafe", "an Unsafe");
310 CreateTextElement(passItem, msg);
311 }
312
313 if (nativePassInfo != null)
314 {
315 CreateTextElement(passItem, "Render Graph Pass Info", Classes.kSubHeaderText);
316 foreach (int passId in groupedPassIds)
317 {
318 var pass = m_CurrentDebugData.passList[passId];
319 Debug.Assert(pass.nrpInfo != null); // This overlay currently assumes NRP compiler
320
321 var passFoldout = new Foldout();
322 passFoldout.text = $"<b>{pass.name}</b> ({k_PassTypeNames[(int) pass.type]})";
323
324 var foldoutTextElement = passFoldout.Q<TextElement>(className: Foldout.textUssClassName);
325 foldoutTextElement.displayTooltipWhenElided = false; // no tooltip override when ellipsis is active
326
327 bool hasSubpassIndex = pass.nativeSubPassIndex != -1;
328 if (hasSubpassIndex)
329 {
330 // Abuse Foldout to allow two-line header: add line break <br> at the end of the actual foldout text to increase height,
331 // then inject a second label into the hierarchy starting with a line break to offset it to the second line.
332 passFoldout.text += "<br>";
333 Label subpassIndexLabel = new Label($"<br>Subpass #{pass.nativeSubPassIndex}");
334 subpassIndexLabel.AddToClassList(Classes.kInfoFoldoutSecondaryText);
335 foldoutTextElement.Add(subpassIndexLabel);
336 }
337
338 passFoldout.AddToClassList(Classes.kInfoFoldout);
339 passFoldout.AddToClassList(Classes.kCustomFoldoutArrow);
340 passFoldout.Q<Toggle>().tooltip = $"The {k_PassTypeNames[(int) pass.type]} <b>{pass.name}</b> belongs to native subpass {pass.nativeSubPassIndex}.";
341
342 var foldoutCheckmark = passFoldout.Q("unity-checkmark");
343 foldoutCheckmark.BringToFront(); // Move foldout checkmark to the right
344
345 var lineBreak = new VisualElement();
346 lineBreak.AddToClassList(Classes.kPanelListLineBreak);
347 passFoldout.Add(lineBreak);
348
349 CreateTextElement(passFoldout,
350 $"Attachment dimensions: {pass.nrpInfo.width}x{pass.nrpInfo.height}x{pass.nrpInfo.volumeDepth}");
351 CreateTextElement(passFoldout, $"Has depth attachment: {pass.nrpInfo.hasDepth}");
352 CreateTextElement(passFoldout, $"MSAA samples: {pass.nrpInfo.samples}");
353 CreateTextElement(passFoldout, $"Async compute: {pass.async}");
354
355 passItem.Add(passFoldout);
356 }
357
358 CreateTextElement(passItem, "Attachment Load/Store Actions", Classes.kSubHeaderText);
359 if (nativePassInfo != null && nativePassInfo.attachmentInfos.Count > 0)
360 {
361 foreach (var attachmentInfo in nativePassInfo.attachmentInfos)
362 {
363 var attachmentFoldout = new Foldout();
364
365 // Abuse Foldout to allow two-line header (same as above)
366 attachmentFoldout.text = $"<b>{attachmentInfo.resourceName}</b><br>";
367 Label attachmentIndexLabel = new Label($"<br>Attachment #{attachmentInfo.attachmentIndex}");
368 attachmentIndexLabel.AddToClassList(Classes.kInfoFoldoutSecondaryText);
369
370 var foldoutTextElement = attachmentFoldout.Q<TextElement>(className: Foldout.textUssClassName);
371 foldoutTextElement.displayTooltipWhenElided = false; // no tooltip override when ellipsis is active
372 foldoutTextElement.Add(attachmentIndexLabel);
373
374 attachmentFoldout.AddToClassList(Classes.kInfoFoldout);
375 attachmentFoldout.AddToClassList(Classes.kCustomFoldoutArrow);
376 attachmentFoldout.Q<Toggle>().tooltip = $"Texture <b>{attachmentInfo.resourceName}</b> is bound at attachment index {attachmentInfo.attachmentIndex}.";
377
378 var foldoutCheckmark = attachmentFoldout.Q("unity-checkmark");
379 foldoutCheckmark.BringToFront(); // Move foldout checkmark to the right
380
381 var lineBreak = new VisualElement();
382 lineBreak.AddToClassList(Classes.kPanelListLineBreak);
383 attachmentFoldout.Add(lineBreak);
384
385 attachmentFoldout.Add(new TextElement
386 {
387 text = $"<b>Load action:</b> {attachmentInfo.loadAction}\n- {attachmentInfo.loadReason}"
388 });
389
390 bool addMsaaInfo = !string.IsNullOrEmpty(attachmentInfo.storeMsaaReason);
391 string resolvedTexturePrefix = addMsaaInfo ? "Resolved surface: " : "";
392
393 string storeActionText = $"<b>Store action:</b> {attachmentInfo.storeAction}" +
394 $"\n - {resolvedTexturePrefix}{attachmentInfo.storeReason}";
395
396 if (addMsaaInfo)
397 {
398 string msaaTexturePrefix = "MSAA surface: ";
399 storeActionText += $"\n - {msaaTexturePrefix}{attachmentInfo.storeMsaaReason}";
400 }
401
402 attachmentFoldout.Add(new TextElement { text = storeActionText });
403
404 passItem.Add(attachmentFoldout);
405 }
406 }
407 else
408 {
409 CreateTextElement(passItem, "No attachments.");
410 }
411 }
412
413 content.Add(passItem);
414
415 m_PassDescendantCache[passItem] = passItem.Query().Descendents<TextElement>().ToList();
416 }
417 }
418
419 void SaveSplitViewFixedPaneHeight()
420 {
421 m_SidePanelFixedPaneHeight = m_SidePanelSplitView.fixedPane?.resolvedStyle?.height ?? 0;
422 }
423
424 void UpdatePanelHeights()
425 {
426 bool passListExpanded = m_PassListExpanded && (m_CurrentDebugData != null && m_CurrentDebugData.isNRPCompiler);
427 const int kFoldoutHeaderHeightPx = 18;
428 const int kFoldoutHeaderExpandedMinHeightPx = 50;
429 const int kWindowExtraMarginPx = 6;
430
431 var resourceList = rootVisualElement.Q<HeaderFoldout>(Names.kResourceListFoldout);
432 var passList = rootVisualElement.Q<HeaderFoldout>(Names.kPassListFoldout);
433
434 resourceList.style.minHeight = kFoldoutHeaderHeightPx;
435 passList.style.minHeight = kFoldoutHeaderHeightPx;
436
437 float panelHeightPx = position.height - kHeaderContainerHeightPx - kWindowExtraMarginPx;
438 if (!m_ResourceListExpanded)
439 {
440 m_SidePanelSplitView.fixedPaneInitialDimension = kFoldoutHeaderHeightPx;
441 }
442 else if (!passListExpanded)
443 {
444 m_SidePanelSplitView.fixedPaneInitialDimension = panelHeightPx - kFoldoutHeaderHeightPx;
445 }
446 else
447 {
448 // Update aspect ratio in case user has dragged the split view
449 if (m_SidePanelFixedPaneHeight > kFoldoutHeaderHeightPx && m_SidePanelFixedPaneHeight < panelHeightPx - kFoldoutHeaderHeightPx)
450 {
451 m_SidePanelVerticalAspectRatio = m_SidePanelFixedPaneHeight / panelHeightPx;
452 }
453 m_SidePanelSplitView.fixedPaneInitialDimension = panelHeightPx * m_SidePanelVerticalAspectRatio;
454
455 resourceList.style.minHeight = kFoldoutHeaderExpandedMinHeightPx;
456 passList.style.minHeight = kFoldoutHeaderExpandedMinHeightPx;
457 }
458
459 // Ensure fixed pane initial dimension gets applied in case it has already been set
460 m_SidePanelSplitView.fixedPane.style.height = m_SidePanelSplitView.fixedPaneInitialDimension;
461
462 // Disable drag line when one of the foldouts is collapsed
463 var dragLine = m_SidePanelSplitView.Q("unity-dragline");
464 var dragLineAnchor = m_SidePanelSplitView.Q("unity-dragline-anchor");
465 if (!m_ResourceListExpanded || !passListExpanded)
466 {
467 dragLine.pickingMode = PickingMode.Ignore;
468 dragLineAnchor.pickingMode = PickingMode.Ignore;
469 }
470 else
471 {
472 dragLine.pickingMode = PickingMode.Position;
473 dragLineAnchor.pickingMode = PickingMode.Position;
474 }
475 }
476
477 void ScrollToPass(int visiblePassIndex)
478 {
479 var passFoldout = rootVisualElement.Q<HeaderFoldout>(Names.kPassListFoldout);
480 ScrollToFoldout(passFoldout, visiblePassIndex);
481 }
482
483 void ScrollToResource(int visibleResourceIndex)
484 {
485 var resourceFoldout = rootVisualElement.Q<HeaderFoldout>(Names.kResourceListFoldout);
486 ScrollToFoldout(resourceFoldout, visibleResourceIndex);
487 }
488
489 void ScrollToFoldout(VisualElement parent, int index)
490 {
491 ScrollView scrollView = parent.Q<ScrollView>();
492 scrollView.Query<Foldout>(classes: Classes.kPanelListItem).ForEach(foldout =>
493 {
494 if (index == (int) foldout.userData)
495 {
496 // Trigger animation
497 foldout.AddToClassList(Classes.kPanelListItemSelectionAnimation);
498
499 // This repaint hack is needed because transition animations have poor framerate. So we are hooking to editor update
500 // loop for the duration of the animation to force repaints and have a smooth highlight animation.
501 // See https://jira.unity3d.com/browse/UIE-1326
502 EditorApplication.update += Repaint;
503
504 foldout.RegisterCallbackOnce<TransitionEndEvent>(_ =>
505 {
506 // "Highlight in" animation finished
507 foldout.RemoveFromClassList(Classes.kPanelListItemSelectionAnimation);
508 foldout.RegisterCallbackOnce<TransitionEndEvent>(_ =>
509 {
510 // "Highlight out" animation finished
511 EditorApplication.update -= Repaint;
512 });
513 });
514
515 // Open foldout
516 foldout.value = true;
517 // Defer scrolling to allow foldout to be expanded first
518 scrollView.schedule.Execute(() => scrollView.ScrollTo(foldout)).StartingIn(50);
519 }
520 });
521 }
522 }
523}