using UnityEngine; using UnityEngine.UIElements; using UnityEditor; using UnityEditor.UIElements; using UnityEngine.Rendering; using UnityEngine.Experimental.Rendering.RenderGraphModule; using System.Collections.Generic; /// /// Render graph viewer window /// public class RenderGraphViewer : EditorWindow { static class Style { public static readonly GUIContent title = EditorGUIUtility.TrTextContent("Render Graph Viewer"); } const float kRenderPassWidth = 20.0f; const float kResourceHeight = 15.0f; class CellElement : VisualElement { public CellElement(int idxStart, int idxEnd) { style.borderBottomLeftRadius = style.borderTopLeftRadius = style.borderBottomRightRadius = style.borderTopRightRadius = 5; style.borderBottomWidth = style.borderTopWidth = style.borderLeftWidth = style.borderRightWidth = 1f; style.borderBottomColor = style.borderTopColor = style.borderLeftColor = style.borderRightColor = new Color(0f, 0f, 0f, 1f); style.backgroundColor = (Color)new Color32(88, 88, 88, 255); style.height = kResourceHeight; style.left = idxStart * kRenderPassWidth; style.width = (idxEnd - idxStart + 1) * kRenderPassWidth; } public void SetColor(StyleColor color) { style.backgroundColor = color; } } [MenuItem("Window/Analysis/Render Graph Viewer", false, 10006)] static void Init() { // Get existing open window or if none, make a new one: var window = GetWindow(); window.titleContent = new GUIContent("Render Graph Viewer"); } [System.Flags] enum Filter { ImportedResources = 1 << 0, CulledPasses = 1 << 1, Textures = 1 << 2, ComputeBuffers = 1 << 3, } struct ResourceElementInfo { public VisualElement lifetime; public VisualElement resourceLabel; public void Reset() { lifetime = null; resourceLabel = null; } } struct PassElementInfo { public VisualElement pass; public int remap; public void Reset() { pass = null; remap = -1; } } Dictionary> m_RegisteredGraphs = new Dictionary>(); RenderGraphDebugData m_CurrentDebugData; VisualElement m_Root; VisualElement m_HeaderElement; VisualElement m_GraphViewerElement; readonly StyleColor m_ResourceColorRead = new StyleColor(new Color(0.2f, 1.0f, 0.2f)); readonly StyleColor m_ResourceColorWrite = new StyleColor(new Color(1.0f, 0.2f, 0.2f)); readonly StyleColor m_ImportedResourceColor = new StyleColor(new Color(0.3f, 0.75f, 0.75f)); readonly StyleColor m_CulledPassColor = new StyleColor(Color.black); readonly StyleColor m_ResourceHighlightColor = new StyleColor(Color.white); readonly StyleColor m_ResourceLifeHighLightColor = new StyleColor(new Color32(103, 103, 103, 255)); StyleColor m_OriginalResourceLifeColor; StyleColor m_OriginalPassColor; StyleColor m_OriginalResourceColor; DynamicArray[] m_ResourceElementsInfo = new DynamicArray[(int)RenderGraphResourceType.Count]; DynamicArray m_PassElementsInfo = new DynamicArray(); Filter m_Filter = Filter.Textures | Filter.ComputeBuffers; void RenderPassLabelChanged(GeometryChangedEvent evt) { var label = evt.currentTarget as Label; Vector2 textSize = label.MeasureTextSize(label.text, 0, VisualElement.MeasureMode.Undefined, 10, VisualElement.MeasureMode.Undefined); float textWidth = Mathf.Max(kRenderPassWidth, textSize.x); float desiredHeight = Mathf.Sqrt(textWidth * textWidth - kRenderPassWidth * kRenderPassWidth); // Should be able to do that and rely on the parent layout but for some reason flex-end does not work so I set the parent's parent height instead. //label.parent.style.height = desiredHeight; var passNamesContainerHeight = Mathf.Max(label.parent.parent.style.height.value.value, desiredHeight); label.parent.parent.style.height = passNamesContainerHeight; label.parent.parent.style.minHeight = passNamesContainerHeight; var topRowElement = m_GraphViewerElement.Q("GraphViewer.TopRowElement"); topRowElement.style.minHeight = passNamesContainerHeight; } void LastRenderPassLabelChanged(GeometryChangedEvent evt) { var label = evt.currentTarget as Label; Vector2 textSize = label.MeasureTextSize(label.text, 0, VisualElement.MeasureMode.Undefined, 10, VisualElement.MeasureMode.Undefined); float textWidth = Mathf.Max(kRenderPassWidth, textSize.x); // Keep a margin on the right of the container to avoid label being clipped. var viewerContainer = m_GraphViewerElement.Q("GraphViewer.ViewerContainer"); viewerContainer.style.marginRight = Mathf.Max(viewerContainer.style.marginRight.value.value, (textWidth - kRenderPassWidth)); } void UpdateResourceLifetimeColor(int passIndex, StyleColor colorRead, StyleColor colorWrite) { var pass = m_CurrentDebugData.passList[passIndex]; if (pass.culled) return; for (int type = 0; type < (int)RenderGraphResourceType.Count; ++type) { foreach (int resourceRead in pass.resourceReadLists[type]) { CellElement resourceLifetime = m_ResourceElementsInfo[type][resourceRead].lifetime as CellElement; if (resourceLifetime != null) resourceLifetime.SetColor(colorRead); } foreach (int resourceWrite in pass.resourceWriteLists[type]) { CellElement resourceLifetime = m_ResourceElementsInfo[type][resourceWrite].lifetime as CellElement; if (resourceLifetime != null) resourceLifetime.SetColor(colorWrite); } } } void MouseEnterPassCallback(MouseEnterEvent evt, int index) { UpdateResourceLifetimeColor(index, m_ResourceColorRead, m_ResourceColorWrite); } void MouseLeavePassCallback(MouseLeaveEvent evt, int index) { UpdateResourceLifetimeColor(index, m_OriginalResourceLifeColor, m_OriginalResourceLifeColor); } void UpdatePassColor((int index, int resourceType) resInfo, StyleColor colorRead, StyleColor colorWrite) { var resource = m_CurrentDebugData.resourceLists[resInfo.resourceType][resInfo.index]; foreach (int consumer in resource.consumerList) { var passDebugData = m_CurrentDebugData.passList[consumer]; if (passDebugData.culled) continue; VisualElement passElement = m_PassElementsInfo[consumer].pass; if (passElement != null) { VisualElement passButton = passElement.Q("RenderPass.Cell"); passButton.style.backgroundColor = colorRead; } } foreach (int producer in resource.producerList) { var passDebugData = m_CurrentDebugData.passList[producer]; if (passDebugData.culled) continue; VisualElement passElement = m_PassElementsInfo[producer].pass; if (passElement != null) { VisualElement passButton = passElement.Q("RenderPass.Cell"); passButton.style.backgroundColor = colorWrite; } } } void UpdateResourceLabelColor((int index, int resourceType) resInfo, StyleColor color) { var label = m_ResourceElementsInfo[resInfo.resourceType][resInfo.index].resourceLabel; if (label != null) { label.style.color = color; } } void MouseEnterResourceCallback(MouseEnterEvent evt, (int index, int resourceType) info) { CellElement resourceLifetime = m_ResourceElementsInfo[info.resourceType][info.index].lifetime as CellElement; resourceLifetime.SetColor(m_ResourceLifeHighLightColor); UpdatePassColor(info, m_ResourceColorRead, m_ResourceColorWrite); UpdateResourceLabelColor(info, m_ResourceHighlightColor); } void MouseLeaveResourceCallback(MouseLeaveEvent evt, (int index, int resourceType) info) { CellElement resourceLifetime = m_ResourceElementsInfo[info.resourceType][info.index].lifetime as CellElement; resourceLifetime.SetColor(m_OriginalResourceLifeColor); var resource = m_CurrentDebugData.resourceLists[info.resourceType][info.index]; UpdatePassColor(info, m_OriginalPassColor, m_OriginalPassColor); UpdateResourceLabelColor(info, resource.imported ? m_ImportedResourceColor : m_OriginalResourceColor); ; } VisualElement CreateRenderPass(string name, int index, bool culled) { var container = new VisualElement(); container.name = "RenderPass"; container.style.width = kRenderPassWidth; container.style.overflow = Overflow.Visible; container.style.flexDirection = FlexDirection.ColumnReverse; container.style.minWidth = kRenderPassWidth; var cell = new Button(); cell.name = "RenderPass.Cell"; cell.style.marginBottom = 0.0f; cell.style.marginLeft = 0.0f; cell.style.marginRight = 0.0f; cell.style.marginTop = 0.0f; cell.RegisterCallback(MouseEnterPassCallback, index); cell.RegisterCallback(MouseLeavePassCallback, index); m_OriginalPassColor = cell.style.backgroundColor; if (culled) cell.style.backgroundColor = m_CulledPassColor; container.Add(cell); var label = new Label(name); label.name = "RenderPass.Label"; label.transform.rotation = Quaternion.Euler(new Vector3(0.0f, 0.0f, -45.0f)); container.Add(label); label.RegisterCallback(RenderPassLabelChanged); return container; } void ResourceNamesContainerChanged(GeometryChangedEvent evt) { var label = evt.currentTarget as Label; float textWidth = label.MeasureTextSize(label.text, 0, VisualElement.MeasureMode.Undefined, 10, VisualElement.MeasureMode.Undefined).x; var cornerElement = m_GraphViewerElement.Q("GraphViewer.Corner"); cornerElement.style.width = Mathf.Max(textWidth, cornerElement.style.width.value.value); cornerElement.style.minWidth = Mathf.Max(textWidth, cornerElement.style.minWidth.value.value); // We need to make sure all resource types have the same width m_GraphViewerElement.Query("GraphViewer.Resources.ResourceNames").Build().ForEach((elem) => { elem.style.width = Mathf.Max(textWidth, elem.style.width.value.value); elem.style.minWidth = Mathf.Max(textWidth, elem.style.minWidth.value.value); }); m_GraphViewerElement.Query("GraphViewer.Resources.ResourceTypeName").Build().ForEach((elem) => { elem.style.width = Mathf.Max(textWidth, elem.style.width.value.value); elem.style.minWidth = Mathf.Max(textWidth, elem.style.minWidth.value.value); }); } VisualElement CreateResourceLabel(string name, bool imported) { var label = new Label(name); label.style.height = kResourceHeight; label.style.overflow = Overflow.Hidden; label.style.textOverflow = TextOverflow.Ellipsis; label.style.unityTextOverflowPosition = TextOverflowPosition.End; if (imported) label.style.color = m_ImportedResourceColor; else m_OriginalResourceColor = label.style.color; return label; } VisualElement CreateColorLegend(string name, StyleColor color) { VisualElement legend = new VisualElement(); legend.style.flexDirection = FlexDirection.Row; Button button = new Button(); button.style.width = kRenderPassWidth;// * 2; button.style.backgroundColor = color; legend.Add(button); var label = new Label(name); label.style.unityTextAlign = TextAnchor.MiddleCenter; legend.Add(label); return legend; } string RenderGraphPopupCallback(RenderGraph rg) { var currentRG = GetCurrentRenderGraph(); if (currentRG != null && rg != currentRG) RebuildHeaderExecutionPopup(); return rg.name; } string EmptyRenderGraphPopupCallback(RenderGraph rg) { return "NotAvailable"; } string EmptExecutionListCallback(string name) { return "NotAvailable"; } void OnCaptureGraph() { RebuildGraphViewerUI(); } void RebuildHeaderExecutionPopup() { var controlsElement = m_HeaderElement.Q("Header.Controls"); var existingExecutionPopup = controlsElement.Q("Header.ExecutionPopup"); if (existingExecutionPopup != null) controlsElement.Remove(existingExecutionPopup); var currentRG = GetCurrentRenderGraph(); List executionList = new List(); if (currentRG != null) { m_RegisteredGraphs.TryGetValue(currentRG, out var executionSet); Debug.Assert(executionSet != null); executionList.AddRange(executionSet); } PopupField executionPopup = null; if (executionList.Count != 0) { executionPopup = new PopupField("Current Execution", executionList, 0); } else { executionList.Add(null); executionPopup = new PopupField("Current Execution", executionList, 0, EmptExecutionListCallback, EmptExecutionListCallback); } executionPopup.labelElement.style.minWidth = 0; executionPopup.name = "Header.ExecutionPopup"; controlsElement.Add(executionPopup); } void RebuildHeaderUI() { m_HeaderElement.Clear(); var controlsElement = new VisualElement(); controlsElement.name = "Header.Controls"; controlsElement.style.flexDirection = FlexDirection.Row; m_HeaderElement.Add(controlsElement); var renderGraphList = new List(m_RegisteredGraphs.Keys); PopupField renderGraphPopup = null; if (renderGraphList.Count != 0) { renderGraphPopup = new PopupField("Current Graph", renderGraphList, 0, RenderGraphPopupCallback, RenderGraphPopupCallback); } else { renderGraphList.Add(null); renderGraphPopup = new PopupField("Current Graph", renderGraphList, 0, EmptyRenderGraphPopupCallback, EmptyRenderGraphPopupCallback); } renderGraphPopup.labelElement.style.minWidth = 0; renderGraphPopup.name = "Header.RenderGraphPopup"; controlsElement.Add(renderGraphPopup); RebuildHeaderExecutionPopup(); var captureButton = new Button(OnCaptureGraph); captureButton.text = "Capture Graph"; controlsElement.Add(captureButton); var filters = new EnumFlagsField("Filters", m_Filter); filters.labelElement.style.minWidth = 0; filters.labelElement.style.alignItems = Align.Center; filters.RegisterCallback>((evt) => { m_Filter = (Filter)evt.newValue; RebuildGraphViewerUI(); }); controlsElement.Add(filters); var legendsElement = new VisualElement(); legendsElement.name = "Header.Legends"; legendsElement.style.flexDirection = FlexDirection.Row; legendsElement.style.alignContent = Align.FlexEnd; legendsElement.Add(CreateColorLegend("Resource Read", m_ResourceColorRead)); legendsElement.Add(CreateColorLegend("Resource Write", m_ResourceColorWrite)); legendsElement.Add(CreateColorLegend("Culled Pass", m_CulledPassColor)); legendsElement.Add(CreateColorLegend("Imported Resource", m_ImportedResourceColor)); m_HeaderElement.Add(legendsElement); } RenderGraph GetCurrentRenderGraph() { var popup = m_HeaderElement.Q>("Header.RenderGraphPopup"); if (popup != null) return popup.value; return null; } RenderGraphDebugData GetCurrentDebugData() { var currentRG = GetCurrentRenderGraph(); if (currentRG != null) { var popup = m_HeaderElement.Q>("Header.ExecutionPopup"); if (popup != null && popup.value != null) return currentRG.GetDebugData(popup.value); } return null; } VisualElement CreateTopRowWithPasses(RenderGraphDebugData debugData, out int finalPassCount) { var topRowElement = new VisualElement(); topRowElement.name = "GraphViewer.TopRowElement"; topRowElement.style.flexDirection = FlexDirection.Row; var cornerElement = new VisualElement(); cornerElement.name = "GraphViewer.Corner"; topRowElement.Add(cornerElement); var passNamesElement = new VisualElement(); passNamesElement.name = "GraphViewer.TopRowElement.PassNames"; passNamesElement.style.flexDirection = FlexDirection.Row; int passIndex = 0; finalPassCount = 0; int lastValidPassIndex = -1; foreach (var pass in debugData.passList) { if ((pass.culled && !m_Filter.HasFlag(Filter.CulledPasses)) || !pass.generateDebugData) { m_PassElementsInfo[passIndex].Reset(); } else { var passElement = CreateRenderPass(pass.name, passIndex, pass.culled); m_PassElementsInfo[passIndex].pass = passElement; m_PassElementsInfo[passIndex].remap = finalPassCount; passNamesElement.Add(passElement); finalPassCount++; lastValidPassIndex = passIndex; } passIndex++; } if (lastValidPassIndex > 0) { var label = m_PassElementsInfo[lastValidPassIndex].pass.Q