using System;
using System.Collections.Generic;
using UnityEditor.UIElements;
using UnityEditorInternal;
using UnityEngine;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.UIElements;
namespace UnityEditor.Rendering
{
public partial class RenderGraphViewer
{
static readonly string[] k_PassTypeNames =
{
"Legacy Render Pass",
"Unsafe Render Pass",
"Raster Render Pass",
"Compute Pass"
};
static partial class Names
{
public const string kPanelContainer = "panel-container";
public const string kResourceListFoldout = "panel-resource-list";
public const string kPassListFoldout = "panel-pass-list";
public const string kResourceSearchField = "resource-search-field";
public const string kPassSearchField = "pass-search-field";
}
static partial class Classes
{
public const string kPanelListLineBreak = "panel-list__line-break";
public const string kPanelListItem = "panel-list__item";
public const string kPanelListItemSelectionAnimation = "panel-list__item--selection-animation";
public const string kPanelResourceListItem = "panel-resource-list__item";
public const string kPanelPassListItem = "panel-pass-list__item";
public const string kSubHeaderText = "sub-header-text";
public const string kAttachmentInfoItem = "attachment-info__item";
public const string kCustomFoldoutArrow = "custom-foldout-arrow";
}
static readonly System.Text.RegularExpressions.Regex k_TagRegex = new ("<[^>]*>");
const string k_SelectionColorBeginTag = "";
const string k_SelectionColorEndTag = "";
TwoPaneSplitView m_SidePanelSplitView;
bool m_ResourceListExpanded = true;
bool m_PassListExpanded = true;
float m_SidePanelVerticalAspectRatio = 0.5f;
float m_SidePanelFixedPaneHeight = 0;
Dictionary> m_ResourceDescendantCache = new ();
Dictionary> m_PassDescendantCache = new ();
void InitializeSidePanel()
{
m_SidePanelSplitView = rootVisualElement.Q(Names.kPanelContainer);
rootVisualElement.RegisterCallback(_ =>
{
SaveSplitViewFixedPaneHeight(); // Window resized - save the current pane height
UpdatePanelHeights();
});
// Callbacks for dynamic height allocation between resource & pass lists
HeaderFoldout resourceListFoldout = rootVisualElement.Q(Names.kResourceListFoldout);
resourceListFoldout.RegisterValueChangedCallback(evt =>
{
if (m_ResourceListExpanded)
SaveSplitViewFixedPaneHeight(); // Closing the foldout - save the current pane height
m_ResourceListExpanded = resourceListFoldout.value;
UpdatePanelHeights();
});
resourceListFoldout.icon = m_ResourceListIcon;
resourceListFoldout.contextMenuGenerator = () => CreateContextMenu(resourceListFoldout.Q());
HeaderFoldout passListFoldout = rootVisualElement.Q(Names.kPassListFoldout);
passListFoldout.RegisterValueChangedCallback(evt =>
{
if (m_PassListExpanded)
SaveSplitViewFixedPaneHeight(); // Closing the foldout - save the current pane height
m_PassListExpanded = passListFoldout.value;
UpdatePanelHeights();
});
passListFoldout.icon = m_PassListIcon;
passListFoldout.contextMenuGenerator = () => CreateContextMenu(passListFoldout.Q());
// Search fields
var resourceSearchField = rootVisualElement.Q(Names.kResourceSearchField);
resourceSearchField.placeholderText = "Search";
resourceSearchField.RegisterValueChangedCallback(evt => OnSearchFilterChanged(m_ResourceDescendantCache, evt.newValue));
var passSearchField = rootVisualElement.Q(Names.kPassSearchField);
passSearchField.placeholderText = "Search";
passSearchField.RegisterValueChangedCallback(evt => OnSearchFilterChanged(m_PassDescendantCache, evt.newValue));
}
bool IsSearchFilterMatch(string str, string searchString, out int startIndex, out int endIndex)
{
startIndex = -1;
endIndex = -1;
startIndex = str.IndexOf(searchString, 0, StringComparison.CurrentCultureIgnoreCase);
if (startIndex == -1)
return false;
endIndex = startIndex + searchString.Length - 1;
return true;
}
void OnSearchFilterChanged(Dictionary> elementCache, string searchString)
{
// Display filter
foreach (var (foldout, descendants) in elementCache)
{
bool anyDescendantMatchesSearch = false;
foreach (var elem in descendants)
{
// Remove any existing highlight
var text = elem.text;
var hasHighlight = k_TagRegex.IsMatch(text);
text = k_TagRegex.Replace(text, string.Empty);
if (!IsSearchFilterMatch(text, searchString, out int startHighlight, out int endHighlight))
{
if (hasHighlight)
elem.text = text;
continue;
}
text = text.Insert(startHighlight, k_SelectionColorBeginTag);
text = text.Insert(endHighlight + k_SelectionColorBeginTag.Length + 1, k_SelectionColorEndTag);
elem.text = text;
anyDescendantMatchesSearch = true;
}
foldout.style.display = anyDescendantMatchesSearch ? DisplayStyle.Flex : DisplayStyle.None;
}
}
void SetChildFoldoutsExpanded(VisualElement elem, bool expanded)
{
elem.Query().ForEach(f => f.value = expanded);
}
GenericMenu CreateContextMenu(VisualElement content)
{
var menu = new GenericMenu();
menu.AddItem(new GUIContent("Collapse All"), false, () => SetChildFoldoutsExpanded(content, false));
menu.AddItem(new GUIContent("Expand All"), false, () => SetChildFoldoutsExpanded(content, true));
return menu;
}
void PopulateResourceList()
{
ScrollView content = rootVisualElement.Q(Names.kResourceListFoldout).Q();
content.Clear();
UpdatePanelHeights();
m_ResourceDescendantCache.Clear();
int visibleResourceIndex = 0;
foreach (var visibleResourceElement in m_ResourceElementsInfo)
{
var resourceData = m_CurrentDebugData.resourceLists[(int)visibleResourceElement.type][visibleResourceElement.index];
var resourceItem = new Foldout();
resourceItem.text = resourceData.name;
resourceItem.value = false;
resourceItem.userData = visibleResourceIndex;
resourceItem.AddToClassList(Classes.kPanelListItem);
resourceItem.AddToClassList(Classes.kPanelResourceListItem);
resourceItem.AddToClassList(Classes.kCustomFoldoutArrow);
visibleResourceIndex++;
var iconContainer = new VisualElement();
iconContainer.AddToClassList(Classes.kResourceIconContainer);
var importedIcon = new VisualElement();
importedIcon.AddToClassList(Classes.kResourceIconImported);
importedIcon.tooltip = "Imported resource";
importedIcon.style.display = resourceData.imported ? DisplayStyle.Flex : DisplayStyle.None;
iconContainer.Add(importedIcon);
var foldoutCheckmark = resourceItem.Q("unity-checkmark");
// Add resource type icon before the label
foldoutCheckmark.parent.Insert(1, CreateResourceTypeIcon(visibleResourceElement.type));
foldoutCheckmark.BringToFront(); // Move foldout checkmark to the right
// Add imported icon to the right of the foldout checkmark
var toggleContainer = resourceItem.Q();
toggleContainer.tooltip = resourceData.name;
toggleContainer.Add(iconContainer);
RenderGraphResourceType type = (RenderGraphResourceType)visibleResourceElement.type;
if (type == RenderGraphResourceType.Texture && resourceData.textureData != null)
{
var lineBreak = new VisualElement();
lineBreak.AddToClassList(Classes.kPanelListLineBreak);
resourceItem.Add(lineBreak);
resourceItem.Add(new Label($"Size: {resourceData.textureData.width}x{resourceData.textureData.height}x{resourceData.textureData.depth}"));
resourceItem.Add(new Label($"Format: {resourceData.textureData.format.ToString()}"));
resourceItem.Add(new Label($"Clear: {resourceData.textureData.clearBuffer}"));
resourceItem.Add(new Label($"BindMS: {resourceData.textureData.bindMS}"));
resourceItem.Add(new Label($"Samples: {resourceData.textureData.samples}"));
if (m_CurrentDebugData.isNRPCompiler)
resourceItem.Add(new Label($"Memoryless: {resourceData.memoryless}"));
}
else if (type == RenderGraphResourceType.Buffer && resourceData.bufferData != null)
{
var lineBreak = new VisualElement();
lineBreak.AddToClassList(Classes.kPanelListLineBreak);
resourceItem.Add(lineBreak);
resourceItem.Add(new Label($"Count: {resourceData.bufferData.count}"));
resourceItem.Add(new Label($"Stride: {resourceData.bufferData.stride}"));
resourceItem.Add(new Label($"Target: {resourceData.bufferData.target.ToString()}"));
resourceItem.Add(new Label($"Usage: {resourceData.bufferData.usage.ToString()}"));
}
content.Add(resourceItem);
m_ResourceDescendantCache[resourceItem] = resourceItem.Query().Descendents().ToList();
}
}
void PopulatePassList()
{
HeaderFoldout headerFoldout = rootVisualElement.Q(Names.kPassListFoldout);
if (!m_CurrentDebugData.isNRPCompiler)
{
headerFoldout.style.display = DisplayStyle.None;
return;
}
headerFoldout.style.display = DisplayStyle.Flex;
ScrollView content = headerFoldout.Q();
content.Clear();
UpdatePanelHeights();
m_PassDescendantCache.Clear();
void CreateTextElement(VisualElement parent, string text, string className = null)
{
var textElement = new TextElement();
textElement.text = text;
if (className != null)
textElement.AddToClassList(className);
parent.Add(textElement);
}
HashSet addedPasses = new HashSet();
foreach (var visiblePassElement in m_PassElementsInfo)
{
if (addedPasses.Contains(visiblePassElement.passId))
continue; // Add only one item per merged pass group
List passDatas = new();
List passNames = new();
var groupedPassIds = GetGroupedPassIds(visiblePassElement.passId);
foreach (int groupedId in groupedPassIds) {
addedPasses.Add(groupedId);
passDatas.Add(m_CurrentDebugData.passList[groupedId]);
passNames.Add(m_CurrentDebugData.passList[groupedId].name);
}
var passItem = new Foldout();
var passesText = string.Join(", ", passNames);
passItem.text = $"{passesText}";
passItem.Q().tooltip = passesText;
passItem.value = false;
passItem.userData = m_PassIdToVisiblePassIndex[visiblePassElement.passId];
passItem.AddToClassList(Classes.kPanelListItem);
passItem.AddToClassList(Classes.kPanelPassListItem);
//Native pass info (duplicated for each pass group so just look at the first)
var firstPassData = passDatas[0];
var nativePassInfo = firstPassData.nrpInfo?.nativePassInfo;
if (nativePassInfo != null)
{
if (nativePassInfo.mergedPassIds.Count == 1)
CreateTextElement(passItem, "Native Pass was created from Raster Render Pass.");
else if (nativePassInfo.mergedPassIds.Count > 1)
CreateTextElement(passItem, $"Native Pass was created by merging {nativePassInfo.mergedPassIds.Count} Raster Render Passes.");
CreateTextElement(passItem, "Pass break reasoning", Classes.kSubHeaderText);
CreateTextElement(passItem, nativePassInfo.passBreakReasoning);
}
else
{
CreateTextElement(passItem, "Pass break reasoning", Classes.kSubHeaderText);
var msg = $"This is a {k_PassTypeNames[(int) firstPassData.type]}. Only Raster Render Passes can be merged.";
msg = msg.Replace("a Unsafe", "an Unsafe");
CreateTextElement(passItem, msg);
}
if (nativePassInfo != null)
{
CreateTextElement(passItem, "Render Graph Pass Info", Classes.kSubHeaderText);
foreach (int passId in groupedPassIds)
{
var pass = m_CurrentDebugData.passList[passId];
Debug.Assert(pass.nrpInfo != null); // This overlay currently assumes NRP compiler
var passFoldout = new Foldout();
passFoldout.text = $"{pass.name} ({k_PassTypeNames[(int) pass.type]})";
passFoldout.AddToClassList(Classes.kAttachmentInfoItem);
passFoldout.AddToClassList(Classes.kCustomFoldoutArrow);
passFoldout.Q().tooltip = passFoldout.text;
var foldoutCheckmark = passFoldout.Q("unity-checkmark");
foldoutCheckmark.BringToFront(); // Move foldout checkmark to the right
var lineBreak = new VisualElement();
lineBreak.AddToClassList(Classes.kPanelListLineBreak);
passFoldout.Add(lineBreak);
CreateTextElement(passFoldout,
$"Attachment dimensions: {pass.nrpInfo.width}x{pass.nrpInfo.height}x{pass.nrpInfo.volumeDepth}");
CreateTextElement(passFoldout, $"Has depth attachment: {pass.nrpInfo.hasDepth}");
CreateTextElement(passFoldout, $"MSAA samples: {pass.nrpInfo.samples}");
CreateTextElement(passFoldout, $"Async compute: {pass.async}");
passItem.Add(passFoldout);
}
CreateTextElement(passItem, "Attachment Load/Store Actions", Classes.kSubHeaderText);
if (nativePassInfo != null && nativePassInfo.attachmentInfos.Count > 0)
{
foreach (var attachmentInfo in nativePassInfo.attachmentInfos)
{
var attachmentFoldout = new Foldout();
attachmentFoldout.text = attachmentInfo.resourceName;
attachmentFoldout.AddToClassList(Classes.kAttachmentInfoItem);
attachmentFoldout.AddToClassList(Classes.kCustomFoldoutArrow);
attachmentFoldout.Q().tooltip = attachmentFoldout.text;
var foldoutCheckmark = attachmentFoldout.Q("unity-checkmark");
foldoutCheckmark.BringToFront(); // Move foldout checkmark to the right
var lineBreak = new VisualElement();
lineBreak.AddToClassList(Classes.kPanelListLineBreak);
attachmentFoldout.Add(lineBreak);
attachmentFoldout.Add(new TextElement
{
text = $"Load action: {attachmentInfo.loadAction}\n- {attachmentInfo.loadReason}"
});
bool addMsaaInfo = !string.IsNullOrEmpty(attachmentInfo.storeMsaaReason);
string resolvedTexturePrefix = addMsaaInfo ? "Resolved surface: " : "";
string storeActionText = $"Store action: {attachmentInfo.storeAction}" +
$"\n - {resolvedTexturePrefix}{attachmentInfo.storeReason}";
if (addMsaaInfo)
{
string msaaTexturePrefix = "MSAA surface: ";
storeActionText += $"\n - {msaaTexturePrefix}{attachmentInfo.storeMsaaReason}";
}
attachmentFoldout.Add(new TextElement { text = storeActionText });
passItem.Add(attachmentFoldout);
}
}
else
{
CreateTextElement(passItem, "No attachments.");
}
}
content.Add(passItem);
m_PassDescendantCache[passItem] = passItem.Query().Descendents().ToList();
}
}
void SaveSplitViewFixedPaneHeight()
{
m_SidePanelFixedPaneHeight = m_SidePanelSplitView.fixedPane?.resolvedStyle?.height ?? 0;
}
void UpdatePanelHeights()
{
bool passListExpanded = m_PassListExpanded && (m_CurrentDebugData != null && m_CurrentDebugData.isNRPCompiler);
const int kFoldoutHeaderHeightPx = 18;
const int kWindowExtraMarginPx = 6;
float panelHeightPx = focusedWindow.position.height - kHeaderContainerHeightPx - kWindowExtraMarginPx;
if (!m_ResourceListExpanded)
{
m_SidePanelSplitView.fixedPaneInitialDimension = kFoldoutHeaderHeightPx;
}
else if (!passListExpanded)
{
m_SidePanelSplitView.fixedPaneInitialDimension = panelHeightPx - kFoldoutHeaderHeightPx;
}
else
{
// Update aspect ratio in case user has dragged the split view
if (m_SidePanelFixedPaneHeight > kFoldoutHeaderHeightPx && m_SidePanelFixedPaneHeight < panelHeightPx - kFoldoutHeaderHeightPx)
{
m_SidePanelVerticalAspectRatio = m_SidePanelFixedPaneHeight / panelHeightPx;
}
m_SidePanelSplitView.fixedPaneInitialDimension = panelHeightPx * m_SidePanelVerticalAspectRatio;
}
// Disable drag line when one of the foldouts is collapsed
var dragLine = m_SidePanelSplitView.Q("unity-dragline");
var dragLineAnchor = m_SidePanelSplitView.Q("unity-dragline-anchor");
if (!m_ResourceListExpanded || !passListExpanded)
{
dragLine.pickingMode = PickingMode.Ignore;
dragLineAnchor.pickingMode = PickingMode.Ignore;
}
else
{
dragLine.pickingMode = PickingMode.Position;
dragLineAnchor.pickingMode = PickingMode.Position;
}
}
void ScrollToPass(int visiblePassIndex)
{
var passFoldout = rootVisualElement.Q(Names.kPassListFoldout);
ScrollToFoldout(passFoldout, visiblePassIndex);
}
void ScrollToResource(int visibleResourceIndex)
{
var resourceFoldout = rootVisualElement.Q(Names.kResourceListFoldout);
ScrollToFoldout(resourceFoldout, visibleResourceIndex);
}
void ScrollToFoldout(VisualElement parent, int index)
{
ScrollView scrollView = parent.Q();
scrollView.Query(classes: Classes.kPanelListItem).ForEach(foldout =>
{
if (index == (int) foldout.userData)
{
// Trigger animation
foldout.AddToClassList(Classes.kPanelListItemSelectionAnimation);
// This repaint hack is needed because transition animations have poor framerate. So we are hooking to editor update
// loop for the duration of the animation to force repaints and have a smooth highlight animation.
// See https://jira.unity3d.com/browse/UIE-1326
EditorApplication.update += Repaint;
foldout.RegisterCallbackOnce(_ =>
{
// "Highlight in" animation finished
foldout.RemoveFromClassList(Classes.kPanelListItemSelectionAnimation);
foldout.RegisterCallbackOnce(_ =>
{
// "Highlight out" animation finished
EditorApplication.update -= Repaint;
});
});
// Open foldout
foldout.value = true;
// Defer scrolling to allow foldout to be expanded first
scrollView.schedule.Execute(() => scrollView.ScrollTo(foldout)).StartingIn(50);
}
});
}
}
}