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 kInfoFoldout = "info-foldout"; public const string kInfoFoldoutSecondaryText = "info-foldout__secondary-text"; 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; float m_ContentSplitViewFixedPaneWidth = 280; 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(); }); var contentSplitView = rootVisualElement.Q(Names.kContentContainer); contentSplitView.fixedPaneInitialDimension = m_ContentSplitViewFixedPaneWidth; contentSplitView.fixedPaneIndex = 1; contentSplitView.fixedPane?.RegisterCallback(_ => { float? w = contentSplitView.fixedPane?.resolvedStyle?.width; if (w.HasValue) m_ContentSplitViewFixedPaneWidth = w.Value; }); // Callbacks for dynamic height allocation between resource & pass lists HeaderFoldout resourceListFoldout = rootVisualElement.Q(Names.kResourceListFoldout); resourceListFoldout.value = m_ResourceListExpanded; 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.value = m_PassListExpanded; 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; } private IVisualElementScheduledItem m_PreviousSearch; private string m_PendingSearchString = string.Empty; private const int k_SearchStringLimit = 15; void OnSearchFilterChanged(Dictionary> elementCache, string searchString) { // Ensure the search string is within the allowed length limit (15 chars max) if (searchString.Length > k_SearchStringLimit) { searchString = searchString[..k_SearchStringLimit]; // Trim to max 15 chars Debug.LogWarning("[Render Graph Viewer] Search string limit exceeded: " + k_SearchStringLimit); } // If the search string hasn't changed, avoid repeating the same search if (m_PendingSearchString == searchString) return; m_PendingSearchString = searchString; if (m_PreviousSearch != null && m_PreviousSearch.isActive) m_PreviousSearch.Pause(); m_PreviousSearch = rootVisualElement .schedule .Execute(() => { PerformSearchAsync(elementCache, searchString); }) .StartingIn(5); // Avoid spamming multiple search if the user types really fast } private void PerformSearchAsync(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.parent.Add(iconContainer); 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; RenderGraphResourceType type = 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]})"; var foldoutTextElement = passFoldout.Q(className: Foldout.textUssClassName); foldoutTextElement.displayTooltipWhenElided = false; // no tooltip override when ellipsis is active bool hasSubpassIndex = pass.nativeSubPassIndex != -1; if (hasSubpassIndex) { // Abuse Foldout to allow two-line header: add line break
at the end of the actual foldout text to increase height, // then inject a second label into the hierarchy starting with a line break to offset it to the second line. passFoldout.text += "
"; Label subpassIndexLabel = new Label($"
Subpass #{pass.nativeSubPassIndex}"); subpassIndexLabel.AddToClassList(Classes.kInfoFoldoutSecondaryText); foldoutTextElement.Add(subpassIndexLabel); } passFoldout.AddToClassList(Classes.kInfoFoldout); passFoldout.AddToClassList(Classes.kCustomFoldoutArrow); passFoldout.Q().tooltip = $"The {k_PassTypeNames[(int) pass.type]} {pass.name} belongs to native subpass {pass.nativeSubPassIndex}."; 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(); string subResourceText = string.Empty; if (attachmentInfo.attachment.mipLevel > 0) subResourceText += $" Mip:{attachmentInfo.attachment.mipLevel}"; if (attachmentInfo.attachment.depthSlice > 0) subResourceText += $" Slice:{attachmentInfo.attachment.depthSlice}"; // Abuse Foldout to allow two-line header (same as above) attachmentFoldout.text = $"{attachmentInfo.resourceName + subResourceText}
"; Label attachmentIndexLabel = new Label($"
Attachment #{attachmentInfo.attachmentIndex}"); attachmentIndexLabel.AddToClassList(Classes.kInfoFoldoutSecondaryText); var foldoutTextElement = attachmentFoldout.Q(className: Foldout.textUssClassName); foldoutTextElement.displayTooltipWhenElided = false; // no tooltip override when ellipsis is active foldoutTextElement.Add(attachmentIndexLabel); attachmentFoldout.AddToClassList(Classes.kInfoFoldout); attachmentFoldout.AddToClassList(Classes.kCustomFoldoutArrow); attachmentFoldout.Q().tooltip = $"Texture {attachmentInfo.resourceName} is bound at attachment index {attachmentInfo.attachmentIndex}."; 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.attachment.loadAction}\n- {attachmentInfo.loadReason}" }); bool addMsaaInfo = !string.IsNullOrEmpty(attachmentInfo.storeMsaaReason); string resolvedTexturePrefix = addMsaaInfo ? "Resolved surface: " : ""; string storeActionText = $"Store action: {attachmentInfo.attachment.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 kFoldoutHeaderExpandedMinHeightPx = 50; const int kWindowExtraMarginPx = 6; var resourceList = rootVisualElement.Q(Names.kResourceListFoldout); var passList = rootVisualElement.Q(Names.kPassListFoldout); resourceList.style.minHeight = kFoldoutHeaderHeightPx; passList.style.minHeight = kFoldoutHeaderHeightPx; float panelHeightPx = 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; resourceList.style.minHeight = kFoldoutHeaderExpandedMinHeightPx; passList.style.minHeight = kFoldoutHeaderExpandedMinHeightPx; } // Ensure fixed pane initial dimension gets applied in case it has already been set m_SidePanelSplitView.fixedPane.style.height = m_SidePanelSplitView.fixedPaneInitialDimension; // 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); } }); } } }