using System; using System.Collections.Generic; using UnityEditor.Rendering.Analytics; using UnityEditor.UIElements; using UnityEngine; using UnityEngine.Rendering.RenderGraphModule; using UnityEngine.Scripting.APIUpdating; using UnityEngine.UIElements; namespace UnityEditor.Rendering { /// /// Editor window class for the Render Graph Viewer /// [MovedFrom("")] public partial class RenderGraphViewer : EditorWindow { static partial class Names { public const string kCaptureButton = "capture-button"; public const string kCurrentGraphDropdown = "current-graph-dropdown"; public const string kCurrentExecutionDropdown = "current-execution-dropdown"; public const string kPassFilterField = "pass-filter-field"; public const string kResourceFilterField = "resource-filter-field"; public const string kContentContainer = "content-container"; public const string kMainContainer = "main-container"; public const string kPassList = "pass-list"; public const string kPassListScrollView = "pass-list-scroll-view"; public const string kResourceListScrollView = "resource-list-scroll-view"; public const string kResourceGridScrollView = "resource-grid-scroll-view"; public const string kResourceGrid = "resource-grid"; public const string kGridlineContainer = "grid-line-container"; public const string kHoverOverlay = "hover-overlay"; public const string kEmptyStateMessage = "empty-state-message"; } static partial class Classes { public const string kPassListItem = "pass-list__item"; public const string kPassTitle = "pass-title"; public const string kPassBlock = "pass-block"; public const string kPassBlockScriptLink = "pass-block-script-link"; public const string kPassBlockCulledPass = "pass-block--culled"; public const string kPassBlockAsyncPass = "pass-block--async"; public const string kPassHighlight = "pass--highlight"; public const string kPassHoverHighlight = "pass--hover-highlight"; public const string kPassHighlightBorder = "pass--highlight-border"; public const string kPassMergeIndicator = "pass-merge-indicator"; public const string kPassCompatibilityMessageIndicator = "pass-compatibility-message-indicator"; public const string kPassCompatibilityMessageIndicatorAnimation = "pass-compatibility-message-indicator--anim"; public const string kPassCompatibilityMessageIndicatorCompatible = "pass-compatibility-message-indicator--compatible"; public const string kPassSynchronizationMessageIndicator = "pass-synchronization-message-indicator"; public const string kResourceListItem = "resource-list__item"; public const string kResourceListItemHighlight = "resource-list__item--highlight"; public const string kResourceListPaddingItem = "resource-list-padding-item"; public const string kResourceIconContainer = "resource-icon-container"; public const string kResourceIcon = "resource-icon"; public const string kResourceIconImported = "resource-icon--imported"; public const string kResourceIconMultipleUsage = "resource-icon--multiple-usage"; public const string kResourceIconGlobalDark = "resource-icon--global-dark"; public const string kResourceIconGlobalLight = "resource-icon--global-light"; public const string kResourceIconFbfetch = "resource-icon--fbfetch"; public const string kResourceIconTexture = "resource-icon--texture"; public const string kResourceIconBuffer = "resource-icon--buffer"; public const string kResourceIconAccelerationStructure = "resource-icon--acceleration-structure"; public const string kResourceGridRow = "resource-grid__row"; public const string kResourceGridFocusOverlay = "resource-grid-focus-overlay"; public const string kResourceHelperLine = "resource-helper-line"; public const string kResourceHelperLineHighlight = "resource-helper-line--highlight"; public const string kResourceUsageRangeBlock = "usage-range-block"; public const string kResourceUsageRangeBlockHighlight = "usage-range-block--highlight"; public const string kResourceDependencyBlock = "dependency-block"; public const string kResourceDependencyBlockRead = "dependency-block-read"; public const string kResourceDependencyBlockWrite = "dependency-block-write"; public const string kResourceDependencyBlockReadWrite = "dependency-block-readwrite"; public const string kGridLine = "grid-line"; public const string kGridLineHighlight = "grid-line--highlight"; } const string k_TemplatePath = "Packages/com.unity.render-pipelines.core/Editor/UXML/RenderGraphViewer.uxml"; const string k_DarkStylePath = "Packages/com.unity.render-pipelines.core/Editor/StyleSheets/RenderGraphViewerDark.uss"; const string k_LightStylePath = "Packages/com.unity.render-pipelines.core/Editor/StyleSheets/RenderGraphViewerLight.uss"; const string k_ResourceListIconPath = "Packages/com.unity.render-pipelines.core/Editor/Icons/RenderGraphViewer/{0}Resources@2x.png"; const string k_PassListIconPath = "Packages/com.unity.render-pipelines.core/Editor/Icons/RenderGraphViewer/{0}PassInspector@2x.png"; // keep in sync with .uss const int kPassWidthPx = 26; const int kResourceRowHeightPx = 30; const int kResourceColumnWidth = 220; const int kResourceIconSize = 16; const int kResourceGridMarginTopPx = 6; const int kDependencyBlockHeightPx = 26; const int kDependencyBlockWidthPx = kPassWidthPx; const int kPassTitleAllowanceMargin = 120; const int kHeaderContainerHeightPx = 24; static readonly Color kReadWriteBlockFillColorDark = new Color32(0xA9, 0xD1, 0x36, 255); static readonly Color kReadWriteBlockFillColorLight = new Color32(0x67, 0x9C, 0x33, 255); readonly Dictionary> m_RegisteredGraphs = new(); RenderGraph.DebugData m_CurrentDebugData; Foldout m_ResourcesList; Foldout m_PassList; PanManipulator m_PanManipulator; Texture2D m_ResourceListIcon; Texture2D m_PassListIcon; readonly List m_PassElementsInfo = new(); // Indexed using visiblePassIndex readonly List m_ResourceElementsInfo = new(); // Indexed using visibleResourceIndex readonly Dictionary m_PassIdToVisiblePassIndex = new(); readonly Dictionary m_VisiblePassIndexToPassId = new(); int m_CurrentHoveredVisiblePassIndex = -1; int m_CurrentHoveredVisibleResourceIndex = -1; int m_CurrentSelectedVisiblePassIndex = -1; const string kPassFilterLegacyEditorPrefsKey = "RenderGraphViewer.PassFilterLegacy"; const string kPassFilterEditorPrefsKey = "RenderGraphViewer.PassFilter"; const string kResourceFilterEditorPrefsKey = "RenderGraphViewer.ResourceFilter"; const string kSelectedExecutionEditorPrefsKey = "RenderGraphViewer.SelectedExecution"; PassFilter m_PassFilter = PassFilter.CulledPasses | PassFilter.RasterPasses | PassFilter.UnsafePasses | PassFilter.ComputePasses; PassFilterLegacy m_PassFilterLegacy = PassFilterLegacy.CulledPasses; ResourceFilter m_ResourceFilter = ResourceFilter.ImportedResources | ResourceFilter.Textures | ResourceFilter.Buffers | ResourceFilter.AccelerationStructures; enum EmptyStateReason { None = 0, NoGraphRegistered, NoExecutionRegistered, NoDataAvailable, WaitingForCameraRender, EmptyPassFilterResult, EmptyResourceFilterResult }; static readonly string[] kEmptyStateMessages = { "", L10n.Tr("No Render Graph has been registered. The Render Graph Viewer is only functional when Render Graph API is in use."), 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."), L10n.Tr("No data to display. Click refresh to capture data."), 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."), L10n.Tr("No passes to display. Select a different Pass Filter to display contents."), L10n.Tr("No resources to display. Select a different Resource Filter to display contents.") }; static readonly string kOpenInCSharpEditorTooltip = L10n.Tr("Click to open {0} definition in C# editor."); [MenuItem("Window/Analysis/Render Graph Viewer", false, 10006)] static void Init() { var window = GetWindow(); window.titleContent = new GUIContent("Render Graph Viewer"); } [Flags] enum PassFilterLegacy { CulledPasses = 1 << 0, } [Flags] enum PassFilter { CulledPasses = 1 << 0, RasterPasses = 1 << 1, UnsafePasses = 1 << 2, ComputePasses = 1 << 3, } [Flags] enum ResourceFilter { ImportedResources = 1 << 0, Textures = 1 << 1, Buffers = 1 << 2, AccelerationStructures = 1 << 3, } class ResourceElementInfo { public RenderGraphResourceType type; public int index; public VisualElement usageRangeBlock; public VisualElement resourceListItem; public VisualElement resourceHelperLine; public int firstPassId; public int lastPassId; } class ResourceRWBlock { [Flags] public enum UsageFlags { None = 0, UpdatesGlobalResource = 1 << 0, FramebufferFetch = 1 << 1, } public VisualElement element; public string tooltip; public int visibleResourceIndex; public bool read; public bool write; public UsageFlags usage; public bool HasMultipleUsageFlags() { // Check if usage is a power of 2, meaning only one bit is set return usage != 0 && (usage & (usage - 1)) != 0; } } class PassElementInfo { public int passId; public VisualElement passBlock; public PassTitleLabel passTitle; public bool isCulled; public bool isAsync; // List of resource blocks read/written to by this pass public readonly List resourceBlocks = new(); public VisualElement leftGridLine; public VisualElement rightGridLine; public bool hasPassCompatibilityTooltip; public bool isPassCompatibleToMerge; public bool hasAsyncDependencyTooltip; } void AppendToTooltipIfNotEmpty(VisualElement elem, string append) { if (elem.tooltip.Length > 0) elem.tooltip += append; } void ResetPassBlockState() { foreach (var info in m_PassElementsInfo) { info.hasPassCompatibilityTooltip = false; info.isPassCompatibleToMerge = false; info.hasAsyncDependencyTooltip = false; info.passBlock.RemoveFromClassList(Classes.kPassBlockCulledPass); info.passBlock.tooltip = string.Empty; if (info.isCulled) { info.passBlock.AddToClassList(Classes.kPassBlockCulledPass); info.passBlock.tooltip = "Culled pass"; } info.passBlock.RemoveFromClassList(Classes.kPassBlockAsyncPass); if (info.isAsync) { info.passBlock.AddToClassList(Classes.kPassBlockAsyncPass); AppendToTooltipIfNotEmpty(info.passBlock, $"{Environment.NewLine}"); info.passBlock.tooltip += "Async Compute Pass"; } var groupedIds = GetGroupedPassIds(info.passId); if (groupedIds.Count > 1) { AppendToTooltipIfNotEmpty(info.passBlock, $"{Environment.NewLine}"); info.passBlock.tooltip += $"{groupedIds.Count} Raster Render Passes merged into a single Native Render Pass"; } AppendToTooltipIfNotEmpty(info.passBlock, $"{Environment.NewLine}{Environment.NewLine}"); info.passBlock.tooltip += string.Format(kOpenInCSharpEditorTooltip, info.passTitle.text); } } void UpdatePassBlocksToSelectedState(List selectedPassIds) { // Hide culled/async pass indicators when a block is selected foreach (var info in m_PassElementsInfo) { info.passBlock.RemoveFromClassList(Classes.kPassBlockCulledPass); info.passBlock.RemoveFromClassList(Classes.kPassBlockAsyncPass); info.passBlock.tooltip = string.Empty; } foreach (var passIdInGroup in selectedPassIds) { var pass = m_CurrentDebugData.passList[passIdInGroup]; // Native pass compatibility if (m_CurrentDebugData.isNRPCompiler) { if (pass.nrpInfo.nativePassInfo != null && pass.nrpInfo.nativePassInfo.passCompatibility.Count > 0) { foreach (var msg in pass.nrpInfo.nativePassInfo.passCompatibility) { int linkedPassId = msg.Key; string compatibilityMessage = msg.Value.message; var linkedPassGroup = GetGroupedPassIds(linkedPassId); foreach (var passIdInLinkedPassGroup in linkedPassGroup) { if (selectedPassIds.Contains(passIdInLinkedPassGroup)) continue; // Don't show compatibility info among passes that are merged if (m_PassIdToVisiblePassIndex.TryGetValue(passIdInLinkedPassGroup, out int visiblePassIndexInLinkedPassGroup)) { var info = m_PassElementsInfo[visiblePassIndexInLinkedPassGroup]; info.hasPassCompatibilityTooltip = true; info.isPassCompatibleToMerge = msg.Value.isCompatible; info.passBlock.tooltip = compatibilityMessage; } } } // Each native pass has compatibility messages, it's enough to process the first one break; } } // Async compute dependencies if (m_PassIdToVisiblePassIndex.TryGetValue(pass.syncFromPassIndex, out int visibleSyncFromPassIndex)) { var syncFromPassInfo = m_PassElementsInfo[visibleSyncFromPassIndex]; syncFromPassInfo.hasAsyncDependencyTooltip = true; AppendToTooltipIfNotEmpty(syncFromPassInfo.passBlock, $"{Environment.NewLine}"); syncFromPassInfo.passBlock.tooltip += "Currently selected Async Compute Pass inserts a GraphicsFence, which this pass waits on."; } if (m_PassIdToVisiblePassIndex.TryGetValue(pass.syncToPassIndex, out int visibleSyncToPassIndex)) { var syncToPassInfo = m_PassElementsInfo[visibleSyncToPassIndex]; syncToPassInfo.hasAsyncDependencyTooltip = true; AppendToTooltipIfNotEmpty(syncToPassInfo.passBlock, $"{Environment.NewLine}"); syncToPassInfo.passBlock.tooltip += "Currently selected Async Compute Pass waits on a GraphicsFence inserted after this pass."; } } foreach (var info in m_PassElementsInfo) { AppendToTooltipIfNotEmpty(info.passBlock, $"{Environment.NewLine}{Environment.NewLine}"); info.passBlock.tooltip += string.Format(kOpenInCSharpEditorTooltip, info.passTitle.text); } } [Flags] enum ResourceHighlightOptions { None = 1 << 0, ResourceListItem = 1 << 1, ResourceUsageRangeBorder = 1 << 2, ResourceHelperLine = 1 << 3, All = ResourceListItem | ResourceUsageRangeBorder | ResourceHelperLine } void ClearResourceHighlight(ResourceHighlightOptions highlightOptions = ResourceHighlightOptions.All) { if (highlightOptions.HasFlag(ResourceHighlightOptions.ResourceListItem)) { rootVisualElement.Query(classes: Classes.kResourceListItem).ForEach(elem => { elem.RemoveFromClassList(Classes.kResourceListItemHighlight); }); } if (highlightOptions.HasFlag(ResourceHighlightOptions.ResourceUsageRangeBorder)) { rootVisualElement.Query(classes: Classes.kResourceUsageRangeBlockHighlight).ForEach(elem => { elem.RemoveFromHierarchy(); }); } if (highlightOptions.HasFlag(ResourceHighlightOptions.ResourceHelperLine)) { rootVisualElement.Query(classes: Classes.kResourceHelperLineHighlight).ForEach(elem => { elem.RemoveFromClassList(Classes.kResourceHelperLineHighlight); }); } } void SetResourceHighlight(ResourceElementInfo info, int visibleResourceIndex, ResourceHighlightOptions highlightOptions) { if (highlightOptions.HasFlag(ResourceHighlightOptions.ResourceListItem)) { info.resourceListItem.AddToClassList(Classes.kResourceListItemHighlight); } if (highlightOptions.HasFlag(ResourceHighlightOptions.ResourceUsageRangeBorder)) { var usageRangeHighlightBlock = new VisualElement(); usageRangeHighlightBlock.style.left = info.usageRangeBlock.style.left.value.value; usageRangeHighlightBlock.style.width = info.usageRangeBlock.style.width.value.value + 1.0f; usageRangeHighlightBlock.style.top = visibleResourceIndex * kResourceRowHeightPx; usageRangeHighlightBlock.pickingMode = PickingMode.Ignore; usageRangeHighlightBlock.AddToClassList(Classes.kResourceUsageRangeBlockHighlight); rootVisualElement.Q(Names.kResourceGrid).parent.Add(usageRangeHighlightBlock); usageRangeHighlightBlock.PlaceInFront(rootVisualElement.Q(Names.kGridlineContainer)); } if (highlightOptions.HasFlag(ResourceHighlightOptions.ResourceHelperLine)) { info.resourceHelperLine.AddToClassList(Classes.kResourceHelperLineHighlight); } } [Flags] enum PassHighlightOptions { None = 1 << 0, PassBlockBorder = 1 << 1, PassBlockFill = 1 << 2, PassTitle = 1 << 3, PassGridLines = 1 << 4, ResourceRWBlocks = 1 << 5, PassesWithCompatibilityMessage = 1 << 6, PassesWithSynchronizationMessage = 1 << 7, ResourceGridFocusOverlay = 1 << 8, PassTitleHover = 1 << 9, All = PassBlockBorder | PassBlockFill | PassTitle | PassGridLines | ResourceRWBlocks | PassesWithCompatibilityMessage | PassesWithSynchronizationMessage | ResourceGridFocusOverlay | PassTitleHover } void ClearPassHighlight(PassHighlightOptions highlightOptions = PassHighlightOptions.All) { // Remove pass block & title highlight foreach (var info in m_PassElementsInfo) { if (highlightOptions.HasFlag(PassHighlightOptions.PassTitle)) info.passTitle.RemoveFromClassList(Classes.kPassHighlight); if (highlightOptions.HasFlag(PassHighlightOptions.PassTitleHover)) info.passTitle.RemoveFromClassList(Classes.kPassHoverHighlight); if (highlightOptions.HasFlag(PassHighlightOptions.PassBlockBorder)) info.passBlock.RemoveFromClassList(Classes.kPassHighlightBorder); if (highlightOptions.HasFlag(PassHighlightOptions.PassBlockFill)) info.passBlock.RemoveFromClassList(Classes.kPassHighlight); if (highlightOptions.HasFlag(PassHighlightOptions.PassesWithCompatibilityMessage)) { info.passBlock.RemoveFromClassList(Classes.kPassCompatibilityMessageIndicator); info.passBlock.RemoveFromClassList(Classes.kPassCompatibilityMessageIndicatorAnimation); info.passBlock.RemoveFromClassList(Classes.kPassCompatibilityMessageIndicatorCompatible); info.passBlock.UnregisterCallback(ToggleCompatiblePassAnimation); } if (highlightOptions.HasFlag(PassHighlightOptions.PassesWithSynchronizationMessage)) info.passBlock.RemoveFromClassList(Classes.kPassSynchronizationMessageIndicator); } // Remove grid line highlight if (highlightOptions.HasFlag(PassHighlightOptions.PassGridLines)) { rootVisualElement.Query(classes: Classes.kGridLine).ForEach(elem => { elem.RemoveFromClassList(Classes.kGridLineHighlight); }); } // Remove focus overlay if (highlightOptions.HasFlag(PassHighlightOptions.ResourceGridFocusOverlay)) { rootVisualElement.Query(classes: Classes.kResourceGridFocusOverlay).ForEach(elem => { elem.RemoveFromHierarchy(); }); } } void SetPassHighlight(int visiblePassIndex, PassHighlightOptions highlightOptions) { if (!m_VisiblePassIndexToPassId.TryGetValue(visiblePassIndex, out int passId)) return; var groupedPassIds = GetGroupedPassIds(passId); // Add pass block & title highlight List visiblePassInfos = new(); foreach (int groupedPassId in groupedPassIds) { if (m_PassIdToVisiblePassIndex.TryGetValue(groupedPassId, out int groupedVisiblePassIndex)) { var info = m_PassElementsInfo[groupedVisiblePassIndex]; if (highlightOptions.HasFlag(PassHighlightOptions.PassTitle)) info.passTitle.AddToClassList(Classes.kPassHighlight); if (highlightOptions.HasFlag(PassHighlightOptions.PassTitleHover)) info.passTitle.AddToClassList(Classes.kPassHoverHighlight); if (highlightOptions.HasFlag(PassHighlightOptions.PassBlockBorder)) info.passBlock.AddToClassList(Classes.kPassHighlightBorder); if (highlightOptions.HasFlag(PassHighlightOptions.PassBlockFill)) info.passBlock.AddToClassList(Classes.kPassHighlight); visiblePassInfos.Add(info); } } foreach (var info in m_PassElementsInfo) { if (groupedPassIds.Contains(info.passId)) continue; if (highlightOptions.HasFlag(PassHighlightOptions.PassesWithCompatibilityMessage) && info.hasPassCompatibilityTooltip) { info.passBlock.AddToClassList(Classes.kPassCompatibilityMessageIndicator); if (info.isPassCompatibleToMerge) { info.passBlock.schedule.Execute(() => { info.passBlock.AddToClassList(Classes.kPassCompatibilityMessageIndicatorAnimation); info.passBlock.AddToClassList(Classes.kPassCompatibilityMessageIndicatorCompatible); }).StartingIn(100); info.passBlock.RegisterCallback( ToggleCompatiblePassAnimation, info.passBlock); } } if (highlightOptions.HasFlag(PassHighlightOptions.PassesWithSynchronizationMessage) && info.hasAsyncDependencyTooltip) info.passBlock.AddToClassList(Classes.kPassSynchronizationMessageIndicator); } // Add grid line highlight if (highlightOptions.HasFlag(PassHighlightOptions.PassGridLines)) { var firstVisiblePassInfo = visiblePassInfos[0]; firstVisiblePassInfo.leftGridLine.AddToClassList(Classes.kGridLineHighlight); int nextVisiblePassIndex = FindNextVisiblePassIndex(visiblePassInfos[^1].passId + 1); if (nextVisiblePassIndex != -1) m_PassElementsInfo[nextVisiblePassIndex].leftGridLine.AddToClassList(Classes.kGridLineHighlight); else rootVisualElement.Query(classes: Classes.kGridLine).Last() .AddToClassList(Classes.kGridLineHighlight); } if (highlightOptions.HasFlag(PassHighlightOptions.ResourceGridFocusOverlay)) { int firstPassIndex = FindNextVisiblePassIndex(groupedPassIds[0]); int afterLastPassIndex = FindNextVisiblePassIndex(groupedPassIds[^1] + 1); int focusOverlayHeightPx = m_ResourceElementsInfo.Count * kResourceRowHeightPx + kResourceGridMarginTopPx; int leftWidth = firstPassIndex * kPassWidthPx; int rightWidth = (m_PassElementsInfo.Count - afterLastPassIndex) * kPassWidthPx; VisualElement left = new VisualElement(); left.AddToClassList(Classes.kResourceGridFocusOverlay); left.style.marginTop = kResourceGridMarginTopPx; left.style.width = leftWidth; left.style.height = focusOverlayHeightPx; left.pickingMode = PickingMode.Ignore; VisualElement right = new VisualElement(); right.AddToClassList(Classes.kResourceGridFocusOverlay); right.style.marginTop = kResourceGridMarginTopPx; right.style.marginLeft = afterLastPassIndex * kPassWidthPx; right.style.width = rightWidth; right.style.height = focusOverlayHeightPx; right.pickingMode = PickingMode.Ignore; var resourceGridScrollView = rootVisualElement.Q(Names.kResourceGridScrollView); resourceGridScrollView.Add(left); resourceGridScrollView.Add(right); } } void ToggleCompatiblePassAnimation(TransitionEndEvent _, VisualElement element) { element.ToggleInClassList(Classes.kPassCompatibilityMessageIndicatorCompatible); } // Returns a list of passes containing the pass itself and potentially others (if merging happened) List GetGroupedPassIds(int passId) { var pass = m_CurrentDebugData.passList[passId]; return pass.nrpInfo?.nativePassInfo?.mergedPassIds ?? new List { passId }; } bool SelectResource(int visibleResourceIndex, int visiblePassIndex) { bool validResourceIndex = visibleResourceIndex >= 0 && visibleResourceIndex < m_ResourceElementsInfo.Count; if (!validResourceIndex) return false; var resInfo = m_ResourceElementsInfo[visibleResourceIndex]; if (m_VisiblePassIndexToPassId.TryGetValue(visiblePassIndex, out int passId)) { if (passId >= resInfo.firstPassId && passId <= resInfo.lastPassId) { ScrollToResource(visibleResourceIndex); return true; } } return false; } void SelectPass(int visiblePassIndex) { m_CurrentSelectedVisiblePassIndex = visiblePassIndex; const PassHighlightOptions opts = PassHighlightOptions.PassTitle | PassHighlightOptions.PassBlockFill | PassHighlightOptions.PassGridLines | PassHighlightOptions.PassBlockBorder | PassHighlightOptions.ResourceRWBlocks | PassHighlightOptions.PassesWithCompatibilityMessage | PassHighlightOptions.PassesWithSynchronizationMessage | PassHighlightOptions.ResourceGridFocusOverlay; ClearPassHighlight(opts); ResetPassBlockState(); if (m_VisiblePassIndexToPassId.TryGetValue(visiblePassIndex, out int passId)) { var selectedPassIds = GetGroupedPassIds(passId); UpdatePassBlocksToSelectedState(selectedPassIds); SetPassHighlight(visiblePassIndex, opts); ScrollToPass(m_PassIdToVisiblePassIndex[selectedPassIds[0]]); } } void HoverPass(int visiblePassIndex, int visibleResourceIndex) { if (m_CurrentHoveredVisiblePassIndex != visiblePassIndex || m_CurrentHoveredVisibleResourceIndex != visibleResourceIndex) { var highlight = PassHighlightOptions.PassBlockBorder | PassHighlightOptions.PassGridLines | PassHighlightOptions.PassBlockFill | PassHighlightOptions.PassTitleHover; if (m_CurrentSelectedVisiblePassIndex != -1) { // Don't highlight or clear these when a pass is selected highlight &= ~(PassHighlightOptions.PassTitle | PassHighlightOptions.PassBlockFill | PassHighlightOptions.PassBlockBorder | PassHighlightOptions.PassGridLines); } if (m_CurrentHoveredVisiblePassIndex != -1) ClearPassHighlight(highlight); if (visibleResourceIndex != -1) // Don't highlight these while mouse is on resource grid highlight &= ~(PassHighlightOptions.PassBlockFill | PassHighlightOptions.PassTitleHover); if (visiblePassIndex != -1) SetPassHighlight(visiblePassIndex, highlight); } } void HoverResourceByIndex(int visibleResourceIndex, int visiblePassIndex) { if (m_CurrentHoveredVisibleResourceIndex != visibleResourceIndex || m_CurrentHoveredVisiblePassIndex != visiblePassIndex) { var highlight = ResourceHighlightOptions.ResourceUsageRangeBorder | ResourceHighlightOptions.ResourceHelperLine; if (m_CurrentHoveredVisibleResourceIndex >= 0 && m_CurrentHoveredVisibleResourceIndex < m_ResourceElementsInfo.Count) { ClearResourceHighlight(highlight); rootVisualElement.Q(Names.kHoverOverlay).AddToClassList(PanManipulator.k_ContentPanClassName); m_PanManipulator.canStartDragging = true; } if (visibleResourceIndex != -1) { var info = m_ResourceElementsInfo[visibleResourceIndex]; if (m_VisiblePassIndexToPassId.TryGetValue(visiblePassIndex, out int passId)) { bool disablePanning = false; var passInfo = m_PassElementsInfo[visiblePassIndex]; foreach (var res in passInfo.resourceBlocks) { if (res.visibleResourceIndex == visibleResourceIndex && res.usage.HasFlag(ResourceRWBlock.UsageFlags.UpdatesGlobalResource)) { disablePanning = true; } } if (passId >= info.firstPassId && passId <= info.lastPassId) { SetResourceHighlight(info, visibleResourceIndex, highlight); disablePanning = true; } if (disablePanning) { rootVisualElement.Q(Names.kHoverOverlay) .RemoveFromClassList(PanManipulator.k_ContentPanClassName); m_PanManipulator.canStartDragging = false; } } } } } void HoverResourceGrid(int visiblePassIndex, int visibleResourceIndex) { if (m_PanManipulator is { dragActive: true }) { visiblePassIndex = -1; visibleResourceIndex = -1; } HoverPass(visiblePassIndex, visibleResourceIndex); HoverResourceByIndex(visibleResourceIndex, visiblePassIndex); m_CurrentHoveredVisiblePassIndex = visiblePassIndex; m_CurrentHoveredVisibleResourceIndex = visibleResourceIndex; } void GetVisiblePassAndResourceIndex(Vector2 pos, out int visiblePassIndex, out int visibleResourceIndex) { visiblePassIndex = Math.Min(Mathf.FloorToInt(pos.x / kPassWidthPx), m_PassElementsInfo.Count - 1); visibleResourceIndex = Math.Min(Mathf.FloorToInt(pos.y / kResourceRowHeightPx), m_ResourceElementsInfo.Count - 1); } void ResourceGridHovered(MouseMoveEvent evt) { GetVisiblePassAndResourceIndex(evt.localMousePosition, out int visiblePassIndex, out int visibleResourceIndex); HoverResourceGrid(visiblePassIndex, visibleResourceIndex); } void ResourceGridClicked(ClickEvent evt) { GetVisiblePassAndResourceIndex(evt.localPosition, out int visiblePassIndex, out int visibleResourceIndex); bool selectedResource = SelectResource(visibleResourceIndex, visiblePassIndex); // Also select pass when clicking on resource if (selectedResource) SelectPass(visiblePassIndex); // Clicked grid background, clear selection if (!selectedResource) DeselectPass(); evt.StopImmediatePropagation(); // Required because to root element click deselects } void PassBlockClicked(ClickEvent evt, int visiblePassIndex) { SelectPass(visiblePassIndex); evt.StopImmediatePropagation(); // Required because to root element click deselects } void ResourceGridTooltipDisplayed(TooltipEvent evt) { evt.tooltip = string.Empty; if (m_CurrentHoveredVisibleResourceIndex != -1 && m_CurrentHoveredVisiblePassIndex != -1) { var passInfo = m_PassElementsInfo[m_CurrentHoveredVisiblePassIndex]; var resourceInfo = m_ResourceElementsInfo[m_CurrentHoveredVisibleResourceIndex]; var passId = m_VisiblePassIndexToPassId[m_CurrentHoveredVisiblePassIndex]; foreach (var rwBlock in passInfo.resourceBlocks) { if (rwBlock.visibleResourceIndex == m_CurrentHoveredVisibleResourceIndex) { evt.tooltip = rwBlock.tooltip; evt.rect = rwBlock.element.worldBound; break; } } if (evt.tooltip == string.Empty && passId >= resourceInfo.firstPassId && passId <= resourceInfo.lastPassId) { evt.tooltip = "Resource is alive but not used by this pass."; evt.rect = resourceInfo.usageRangeBlock.worldBound; } } evt.StopPropagation(); } void DeselectPass() { SelectPass(-1); m_CurrentHoveredVisiblePassIndex = -1; m_CurrentHoveredVisibleResourceIndex = -1; } void KeyPressed(KeyUpEvent evt) { if (evt.keyCode == KeyCode.Escape) DeselectPass(); } void RequestCaptureSelectedExecution() { if (!CaptureEnabled()) return; selectedRenderGraph.RequestCaptureDebugData(selectedExecutionName); ClearGraphViewerUI(); SetEmptyStateMessage(EmptyStateReason.WaitingForCameraRender); } void SelectedRenderGraphChanged(string newRenderGraphName) { foreach (var rg in m_RegisteredGraphs.Keys) { if (rg.name == newRenderGraphName) { selectedRenderGraph = rg; return; } } selectedRenderGraph = null; if (m_CurrentDebugData != null) RequestCaptureSelectedExecution(); } void SelectedExecutionChanged(string newExecutionName) { if (newExecutionName == selectedExecutionName) return; selectedExecutionName = newExecutionName; if (m_CurrentDebugData != null) RequestCaptureSelectedExecution(); } void ClearEmptyStateMessage() { rootVisualElement.Q(Names.kContentContainer).style.display = DisplayStyle.Flex; rootVisualElement.Q(Names.kEmptyStateMessage).style.display = DisplayStyle.None; } void SetEmptyStateMessage(EmptyStateReason reason) { rootVisualElement.Q(Names.kContentContainer).style.display = DisplayStyle.None; var emptyStateElement = rootVisualElement.Q(Names.kEmptyStateMessage); emptyStateElement.style.display = DisplayStyle.Flex; if (emptyStateElement[0] is TextElement emptyStateText) emptyStateText.text = $"{kEmptyStateMessages[(int) reason]}"; } void RebuildRenderGraphPopup() { var renderGraphDropdownField = rootVisualElement.Q(Names.kCurrentGraphDropdown); if (m_RegisteredGraphs.Count == 0 || renderGraphDropdownField == null) { selectedRenderGraph = null; return; } var choices = new List(); foreach (var rg in m_RegisteredGraphs.Keys) choices.Add(rg.name); renderGraphDropdownField.choices = choices; renderGraphDropdownField.style.display = DisplayStyle.Flex; renderGraphDropdownField.value = choices[0]; SelectedRenderGraphChanged(choices[0]); } void RebuildExecutionPopup() { var executionDropdownField = rootVisualElement.Q(Names.kCurrentExecutionDropdown); List choices = new List(); if (selectedRenderGraph != null) { m_RegisteredGraphs.TryGetValue(selectedRenderGraph, out var executionSet); choices.AddRange(executionSet); } if (choices.Count == 0 || executionDropdownField == null) { selectedExecutionName = null; return; } executionDropdownField.choices = choices; executionDropdownField.RegisterValueChangedCallback(evt => selectedExecutionName = evt.newValue); int selectedIndex = 0; if (EditorPrefs.HasKey(kSelectedExecutionEditorPrefsKey)) { string previousSelectedExecution = EditorPrefs.GetString(kSelectedExecutionEditorPrefsKey); int previousSelectedIndex = choices.IndexOf(previousSelectedExecution); if (previousSelectedIndex != -1) selectedIndex = previousSelectedIndex; } // Set value without triggering serialization of the editorpref executionDropdownField.SetValueWithoutNotify(choices[selectedIndex]); SelectedExecutionChanged(choices[selectedIndex]); } void OnPassFilterChanged(ChangeEvent evt) { m_PassFilter = (PassFilter) evt.newValue; EditorPrefs.SetInt(kPassFilterEditorPrefsKey, (int)m_PassFilter); RebuildGraphViewerUI(); } void OnPassFilterLegacyChanged(ChangeEvent evt) { m_PassFilterLegacy = (PassFilterLegacy) evt.newValue; EditorPrefs.SetInt(kPassFilterLegacyEditorPrefsKey, (int)m_PassFilterLegacy); RebuildGraphViewerUI(); } void OnResourceFilterChanged(ChangeEvent evt) { m_ResourceFilter = (ResourceFilter) evt.newValue; EditorPrefs.SetInt(kResourceFilterEditorPrefsKey, (int)m_ResourceFilter); RebuildGraphViewerUI(); } void RebuildPassFilterUI() { var passFilter = rootVisualElement.Q(Names.kPassFilterField); passFilter.style.display = DisplayStyle.Flex; // We don't know which callback was registered before, so unregister both. passFilter.UnregisterCallback>(OnPassFilterChanged); passFilter.UnregisterCallback>(OnPassFilterLegacyChanged); if (m_CurrentDebugData.isNRPCompiler) { passFilter.Init(m_PassFilter); passFilter.RegisterCallback>(OnPassFilterChanged); } else { passFilter.Init(m_PassFilterLegacy); passFilter.RegisterCallback>(OnPassFilterLegacyChanged); } } void RebuildResourceFilterUI() { var resourceFilter = rootVisualElement.Q(Names.kResourceFilterField); resourceFilter.style.display = DisplayStyle.Flex; resourceFilter.UnregisterCallback>(OnResourceFilterChanged); resourceFilter.Init(m_ResourceFilter); resourceFilter.RegisterCallback>(OnResourceFilterChanged); } void RebuildHeaderUI() { RebuildRenderGraphPopup(); RebuildExecutionPopup(); } RenderGraph m_SelectedRenderGraph; RenderGraph selectedRenderGraph { get => m_SelectedRenderGraph; set { m_SelectedRenderGraph = value; UpdateCaptureEnabledUIState(); } } string m_SelectedExecutionName; string selectedExecutionName { get => m_SelectedExecutionName; set { m_SelectedExecutionName = value; UpdateCaptureEnabledUIState(); } } bool CaptureEnabled() => selectedExecutionName != null && selectedRenderGraph != null; void UpdateCaptureEnabledUIState() { if (rootVisualElement?.childCount == 0) return; bool enabled = CaptureEnabled(); var captureButton = rootVisualElement.Q