diff --git a/Packages/com.unity.render-pipelines.core/CHANGELOG.md b/Packages/com.unity.render-pipelines.core/CHANGELOG.md index 28ddd3eb859..2354bbba65a 100644 --- a/Packages/com.unity.render-pipelines.core/CHANGELOG.md +++ b/Packages/com.unity.render-pipelines.core/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Fixed +- Fixed a `variant DISABLE_TEXTURE2D_X_ARRAY not found` shader error when copying MSAA textures on OpenGL ES 3.1 drivers that lack per-sample shading (`gl_SampleID`); these devices now fall back to a regular blit instead. +- Fixed a "Shader 'Hidden/Core/ProbeVolumeDebug': All SubShaders were stripped" warning when building an HDRP project, caused by a leftover URP-specific RenderPipeline tag on the shared probe volume debug shader. + Version Updated The version number for this package has increased due to a version update of a related graphics package. diff --git a/Packages/com.unity.render-pipelines.core/Documentation~/UnifiedRayTracing/create-ray-tracing-context.md b/Packages/com.unity.render-pipelines.core/Documentation~/UnifiedRayTracing/create-ray-tracing-context.md index ca17c94cb8c..0f2d779654e 100644 --- a/Packages/com.unity.render-pipelines.core/Documentation~/UnifiedRayTracing/create-ray-tracing-context.md +++ b/Packages/com.unity.render-pipelines.core/Documentation~/UnifiedRayTracing/create-ray-tracing-context.md @@ -36,19 +36,19 @@ rtResources.Load(); **Note:** Since the Player doesn't give access to the Asset Database, this method only works in the Editor. To load the [`RayTracingResources`](xref:UnityEngine.Rendering.UnifiedRayTracing.RayTracingResources) in the Player, you can also build an AssetBundle that includes the following SRP Core files: -- Runtime/UnifiedRayTracing/Common/GeometryPool/GeometryPoolKernels.compute -- Runtime/UnifiedRayTracing/Common/Utilities/CopyBuffer.compute -- Runtime/UnifiedRayTracing/Compute/RadeonRays/kernels/copyPositions.compute -- Runtime/UnifiedRayTracing/Compute/RadeonRays/kernels/bit_histogram.compute -- Runtime/UnifiedRayTracing/Compute/RadeonRays/kernels/block_reduce_part.compute -- Runtime/UnifiedRayTracing/Compute/RadeonRays/kernels/block_scan.compute -- Runtime/UnifiedRayTracing/Compute/RadeonRays/kernels/build_hlbvh.compute -- Runtime/UnifiedRayTracing/Compute/RadeonRays/kernels/restructure_bvh.compute -- Runtime/UnifiedRayTracing/Compute/RadeonRays/kernels/scatter.compute +- `Runtime/UnifiedRayTracing/Common/GeometryPool/GeometryPoolKernels.compute` +- `Runtime/UnifiedRayTracing/Common/Utilities/CopyBuffer.compute` +- `Runtime/UnifiedRayTracing/Compute/RadeonRays/kernels/copyPositions.compute` +- `Runtime/UnifiedRayTracing/Compute/RadeonRays/kernels/bit_histogram.compute` +- `Runtime/UnifiedRayTracing/Compute/RadeonRays/kernels/block_reduce_part.compute` +- `Runtime/UnifiedRayTracing/Compute/RadeonRays/kernels/block_scan.compute` +- `Runtime/UnifiedRayTracing/Compute/RadeonRays/kernels/build_hlbvh.compute` +- `Runtime/UnifiedRayTracing/Compute/RadeonRays/kernels/restructure_bvh.compute` +- `Runtime/UnifiedRayTracing/Compute/RadeonRays/kernels/scatter.compute` -For more information about how to build and load AssetBundles, refer to [AssetBundles](xref:AssetBundlesIntro). +For more information about building and loading AssetBundles, refer to [AssetBundles](xref:AssetBundlesIntro). -Once the AssetBundle is built, you can load it using the following: +After you build the AssetBundle, use the following code to load it: ```C# var rtResources = new RayTracingResources(); diff --git a/Packages/com.unity.render-pipelines.core/Documentation~/render-graph-writing-a-render-pipeline.md b/Packages/com.unity.render-pipelines.core/Documentation~/render-graph-writing-a-render-pipeline.md index 8578643b1d8..2901db57ee5 100644 --- a/Packages/com.unity.render-pipelines.core/Documentation~/render-graph-writing-a-render-pipeline.md +++ b/Packages/com.unity.render-pipelines.core/Documentation~/render-graph-writing-a-render-pipeline.md @@ -124,7 +124,7 @@ public class ExampleRenderPipeline : RenderPipeline } catch (Exception e) { - if (renderGraph.ResetGraphAndLogException(e)) + if (myRenderGraph.ResetGraphAndLogException(e)) throw; } diff --git a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/AssemblyInfo.cs b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/AssemblyInfo.cs index a37f6f47f9c..289c730998e 100644 --- a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/AssemblyInfo.cs +++ b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/AssemblyInfo.cs @@ -1,5 +1,8 @@ using System.Runtime.CompilerServices; +// Core +[assembly: InternalsVisibleTo("Unity.RenderPipelines.Core.Editor.Tests")] + // HDRP [assembly: InternalsVisibleTo("Unity.RenderPipelines.HighDefinition.Editor")] [assembly: InternalsVisibleTo("Unity.RenderPipelines.HighDefinition.Editor.Tests")] diff --git a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/AssetsConverter.cs b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/AssetsConverter.cs index 9f1b51c9221..ff2308f6d81 100644 --- a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/AssetsConverter.cs +++ b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/AssetsConverter.cs @@ -17,54 +17,139 @@ internal abstract class AssetsConverter : IRenderPipelineConverter public void Scan(Action> onScanFinish) { assets.Clear(); - void OnSearchFinish() - { - var returnList = new List(assets.Count); - foreach (var asset in assets) - returnList.Add(asset); - onScanFinish?.Invoke(returnList); - } - var processedIds = new HashSet(); + var pooledDict = UnityEngine.Pool.DictionaryPool>.Get(out var assetsByDescription); + bool dictionaryReleased = false; - SearchServiceUtils.RunQueuedSearch - ( - SearchServiceUtils.IndexingOptions.DeepSearch, - contextSearchQueriesAndIds, - (item, description) => + try + { + void OnSearchFinish() { - // Direct conversion - works for both assets and scene objects - var unityObject = item.ToObject(); + try + { + var organizedList = CategorizeResults(assets, assetsByDescription); + onScanFinish?.Invoke(organizedList); + } + finally + { + if (!dictionaryReleased) + { + UnityEngine.Pool.DictionaryPool>.Release(assetsByDescription); + dictionaryReleased = true; + } + } + } + + var processedIds = new HashSet(); + var perGroupProcessedIds = new Dictionary>(); + var assetLookup = new Dictionary(); + + SearchServiceUtils.RunQueuedSearch + ( + SearchServiceUtils.IndexingOptions.DeepSearch, + contextSearchQueriesAndIds, + (item, description) => + { + var unityObject = item.ToObject(); + + if (unityObject == null) + return; + + GameObject go = null; - if (unityObject == null) + if (unityObject is GameObject gameObject) + go = gameObject; + else if (unityObject is Component component) + go = component.gameObject; + else return; - // Ensure we're always working with GameObjects - GameObject go = null; + var gid = GlobalObjectId.GetGlobalObjectIdSlow(go); + var gidString = gid.ToString(); - if (unityObject is GameObject gameObject) - go = gameObject; - else if (unityObject is Component component) - go = component.gameObject; - else - return; // Not a GameObject or Component + string groupName = GetGroupNameFromDescription(description); - var gid = GlobalObjectId.GetGlobalObjectIdSlow(go); - if (!processedIds.Add(gid.ToString())) - return; + // Check if this group has already processed this object + if (!perGroupProcessedIds.ContainsKey(groupName)) + perGroupProcessedIds[groupName] = new HashSet(); - int type = gid.identifierType; // 1=Asset, 2=SceneObject + if (!perGroupProcessedIds[groupName].Add(gidString)) + return; - var assetItem = new RenderPipelineConverterAssetItem(gid.ToString()) - { - name = $"{unityObject.name} ({(type == 1 ? "Prefab" : "SceneObject")})", - info = type == 1 ? AssetDatabase.GetAssetPath(unityObject) : go.scene.path, - }; - - assets.Add(assetItem); - }, - OnSearchFinish - ); + RenderPipelineConverterAssetItem assetItem; + + // Add to the global assets list only once + bool isNewAsset = processedIds.Add(gidString); + if (isNewAsset) + { + int type = gid.identifierType; + + assetItem = new RenderPipelineConverterAssetItem(gidString) + { + name = $"{unityObject.name} ({(type == GlobalObjectIdentifierType.ImportedAsset ? "Prefab" : "SceneObject")})", + info = type == GlobalObjectIdentifierType.ImportedAsset ? AssetDatabase.GetAssetPath(unityObject) : go.scene.path, + }; + + assets.Add(assetItem); + assetLookup[gidString] = assetItem; + } + else + { + assetItem = assetLookup[gidString]; + } + + // Add the asset item to the group + if (!assetsByDescription.ContainsKey(groupName)) + assetsByDescription[groupName] = new List(); + + assetsByDescription[groupName].Add(assetItem); + }, + OnSearchFinish + ); + } + catch + { + if (!dictionaryReleased) + { + UnityEngine.Pool.DictionaryPool>.Release(assetsByDescription); + dictionaryReleased = true; + } + throw; + } + } + + /// + /// Categorizes scan results into a tree structure. + /// Override this method to customize how items are organized. + /// Default implementation returns a flat list. + /// + /// All assets found during scan + /// Assets grouped by their search description + /// Organized list of converter items + protected virtual List CategorizeResults( + List assets, + Dictionary> assetsByDescription) + { + if (assets == null || assets.Count == 0) + return new List(); + + // Default: Return flat list + var flatList = new List(assets.Count); + foreach (var asset in assets) + flatList.Add(asset); + + return flatList; + } + + /// + /// Extracts the group name from the search description. + /// Override this to customize how group names are extracted. + /// Default implementation removes " is being referenced" suffix. + /// Note: This string is coupled to the format used in ReadonlyMaterialConverter.GetMaterialSearchList() + /// + protected virtual string GetGroupNameFromDescription(string description) + { + return description.Replace(" is being referenced", ""); } public virtual void BeforeConvert() { } @@ -75,6 +160,12 @@ public Status Convert(IRenderPipelineConverterItem item, out string message) { var assetItem = item as RenderPipelineConverterAssetItem; + if (assetItem == null) + { + message = "Item is not a valid asset for conversion."; + return Status.Error; + } + var obj = assetItem.LoadObject(); if (obj == null) diff --git a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/ConverterItemState.cs b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/ConverterItemState.cs index dcd5e8b30a9..75c648a7fba 100644 --- a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/ConverterItemState.cs +++ b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/ConverterItemState.cs @@ -1,30 +1,118 @@ using System; +using System.Collections.Generic; +using UnityEngine; using UnityEngine.UIElements; namespace UnityEditor.Rendering.Converter { - // This is the serialized class that stores the state of each item in the list of items to convert + /// + /// Helper for calculating checkbox states from child items + /// + internal static class CheckboxStateHelper + { + /// + /// Calculate checkbox state from a collection of child items + /// Returns: (isChecked, isIndeterminate) + /// + public static (bool isChecked, bool isIndeterminate) CalculateCheckboxState(IEnumerable childItems) + { + int selectedCount = 0; + int enabledCount = 0; + + foreach (var child in childItems) + { + if (child.item.isEnabled) + { + enabledCount++; + if (child.isSelected == true) + selectedCount++; + } + } + + if (enabledCount == 0) + return (false, false); // No enabled children + + if (selectedCount == 0) + return (false, false); // None selected + + if (selectedCount == enabledCount) + return (true, false); // All selected + + return (false, true); // Mixed state (some selected) + } + + /// + /// Calculate selection state as nullable bool (used internally for folder state) + /// Returns: true (all selected), false (none selected), null (indeterminate) + /// + public static bool? CalculateSelectionState(IEnumerable childItems) + { + var (isChecked, isIndeterminate) = CalculateCheckboxState(childItems); + + if (isIndeterminate) + return null; // Mixed state + + return isChecked; + } + } + + interface IConverterItemState + { + bool? isSelected { get; set; } + IRenderPipelineConverterItem item { get; } + (Status Status, string Message) conversionResult { get; } + bool hasConverted { get; } + Action onIsSelectedChanged { get; set; } + + void OnSelectionChanged(ClickEvent _); + void OnClicked(ClickEvent _); + void SetSelectedWithoutNotify(bool value); + void SetSelectionChangedHandler(Action handler); + void Clear(); + int CountItemsWithStatus(Status status); + int GetTotalItemCount(); + int GetSelectedItemCount(); + bool IsFolder(); + int CompareTo(IConverterItemState other); + DisplayFilter GetDisplayFilter(); + IEnumerable GetLeafItems(); + } + [Serializable] - class ConverterItemState + class ConverterItemState : IConverterItemState { - public Action onIsSelectedChanged; + [NonSerialized] + private Action m_OnIsSelectedChanged; + public Action onIsSelectedChanged + { + get => m_OnIsSelectedChanged; + set => m_OnIsSelectedChanged = value; + } + private bool m_IsSelected; - public bool isSelected + public bool? isSelected { get => m_IsSelected; set { if (m_IsSelected != value) { - m_IsSelected = value; - onIsSelectedChanged?.Invoke(m_IsSelected); + m_IsSelected = value ?? false; + m_OnIsSelectedChanged?.Invoke(m_IsSelected); } } } - public IRenderPipelineConverterItem item; + + public IRenderPipelineConverterItem item { get; set; } + [NonSerialized] - public (Status Status, string Message) conversionResult = (Status.Pending, string.Empty); - internal bool hasConverted => conversionResult.Status != Status.Pending; + private (Status Status, string Message) m_ConversionResult = (Status.Pending, string.Empty); + public (Status Status, string Message) conversionResult + { + get => m_ConversionResult; + set => m_ConversionResult = value; + } + public bool hasConverted => m_ConversionResult.Status != Status.Pending; public void OnSelectionChanged(ClickEvent _) { @@ -35,5 +123,248 @@ public void OnClicked(ClickEvent _) { item.OnClicked(); } + + public void SetSelectedWithoutNotify(bool value) + { + m_IsSelected = value; + } + + public void SetSelectionChangedHandler(Action handler) + { + m_OnIsSelectedChanged = handler; + } + + public int CountItemsWithStatus(Status status) + { + return m_ConversionResult.Status == status ? 1 : 0; + } + + public int GetTotalItemCount() + { + return 1; + } + + public int GetSelectedItemCount() + { + return m_IsSelected ? 1 : 0; + } + + public void Clear() + { + } + + public bool IsFolder() + { + return false; + } + + public int CompareTo(IConverterItemState other) + { + if (other.IsFolder()) + return 1; + + return string.Compare(item.name, other.item.name, StringComparison.Ordinal); + } + + public DisplayFilter GetDisplayFilter() + { + return m_ConversionResult.Status switch + { + Status.Pending => DisplayFilter.Pending, + Status.Warning => DisplayFilter.Warnings, + Status.Error => DisplayFilter.Errors, + Status.Success => DisplayFilter.Success, + _ => DisplayFilter.All + }; + } + + public IEnumerable GetLeafItems() + { + yield return this; + } + } + + [Serializable] + class FolderItemState : IConverterItemState + { + [NonSerialized] + private Action m_OnIsSelectedChanged; + public Action onIsSelectedChanged + { + get => m_OnIsSelectedChanged; + set => m_OnIsSelectedChanged = value; + } + + private bool? m_IsSelected; + public bool? isSelected + { + get => m_IsSelected; + set + { + if (m_IsSelected != value && value.HasValue) + { + SetSelectedWithoutNotify(value.Value); + m_OnIsSelectedChanged?.Invoke(value.Value); + } + } + } + + public IRenderPipelineConverterItem item { get; set; } + + [NonSerialized] + private (Status Status, string Message) m_ConversionResult = (Status.Pending, string.Empty); + public (Status Status, string Message) conversionResult + { + get => m_ConversionResult; + set => m_ConversionResult = value; + } + public bool hasConverted => false; + + [SerializeReference] + public List children = new List(); + + public void OnSelectionChanged(ClickEvent _) + { + isSelected = !(m_IsSelected ?? false); + } + + public void OnClicked(ClickEvent _) + { + item.OnClicked(); + } + + public void SetSelectedWithoutNotify(bool selected) + { + m_IsSelected = selected; + SetAllChildrenSelectWithoutNotify(selected); + } + + public void SetSelectionChangedHandler(Action handler) + { + m_OnIsSelectedChanged = handler; + } + + private void SetAllChildrenSelectWithoutNotify(bool selected) + { + foreach (var child in children) + { + if (child.item.isEnabled) + child.SetSelectedWithoutNotify(selected); + } + } + + public void SubscribeToChildren() + { + foreach (var child in children) + { + child.onIsSelectedChanged -= OnChildSelectionChanged; + child.onIsSelectedChanged += OnChildSelectionChanged; + + if (child is FolderItemState childFolder) + childFolder.SubscribeToChildren(); + } + } + + public void UnsubscribeFromChildren() + { + foreach (var child in children) + { + child.onIsSelectedChanged -= OnChildSelectionChanged; + + if (child is FolderItemState childFolder) + childFolder.UnsubscribeFromChildren(); + } + } + + private void OnChildSelectionChanged(bool childSelected) + { + m_IsSelected = CheckboxStateHelper.CalculateSelectionState(children); + m_OnIsSelectedChanged?.Invoke(m_IsSelected ?? false); + } + + public (bool isChecked, bool isIndeterminate) GetCheckboxState() + { + return CheckboxStateHelper.CalculateCheckboxState(children); + } + + public List GetAllLeafItems() + { + var leafItems = new List(); + CollectLeafItemsRecursive(children, leafItems); + return leafItems; + } + + private void CollectLeafItemsRecursive(List items, List targetList) + { + foreach (var child in items) + { + if (child is FolderItemState childFolder) + CollectLeafItemsRecursive(childFolder.children, targetList); + else if (child is ConverterItemState leafItem) + targetList.Add(leafItem); + } + } + + public int CountItemsWithStatus(Status status) + { + int count = 0; + foreach (var child in children) + { + count += child.CountItemsWithStatus(status); + } + return count; + } + + public int GetTotalItemCount() + { + int count = 0; + foreach (var child in children) + { + count += child.GetTotalItemCount(); + } + return count; + } + + public int GetSelectedItemCount() + { + int count = 0; + foreach (var child in children) + { + count += child.GetSelectedItemCount(); + } + return count; + } + + public void Clear() + { + UnsubscribeFromChildren(); + } + + public bool IsFolder() + { + return true; + } + + public int CompareTo(IConverterItemState other) + { + if (!other.IsFolder()) + return -1; + + return string.Compare(item.name, other.item.name, StringComparison.Ordinal); + } + + public DisplayFilter GetDisplayFilter() + { + return DisplayFilter.All; + } + + public IEnumerable GetLeafItems() + { + foreach (var child in children) + { + foreach (var leafItem in child.GetLeafItems()) + yield return leafItem; + } + } } } diff --git a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/ConverterState.cs b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/ConverterState.cs index baa1a17c9c4..16f592ede19 100644 --- a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/ConverterState.cs +++ b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/ConverterState.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using UnityEngine; +using UnityEngine.Pool; using UnityEngine.UIElements; namespace UnityEditor.Rendering.Converter @@ -16,6 +17,46 @@ internal enum DisplayFilter All = Pending | Warnings | Errors | Success } + /// + /// Represents a node in the tree view - can be either a folder or a leaf item + /// + [Serializable] + internal class TreeNodeData + { + public string displayName; + public string fullPath; + + [SerializeReference] + public IConverterItemState itemState; + + public bool isFolder; + + // For folder nodes - track children for checkbox state calculation + [SerializeReference] + public List childItems; + + public TreeNodeData(string displayName, string fullPath, bool isFolder, IConverterItemState itemState = null) + { + this.displayName = displayName; + this.fullPath = fullPath; + this.isFolder = isFolder; + this.itemState = itemState; + this.childItems = new List(); + } + + /// + /// For folder nodes: Calculate checkbox state based on children + /// Returns: (isChecked, isIndeterminate) + /// + public (bool isChecked, bool isIndeterminate) GetFolderCheckboxState() + { + if (!isFolder || childItems.Count == 0) + return (false, false); + + return CheckboxStateHelper.CalculateCheckboxState(childItems); + } + } + // Each converter uses the active bool // Each converter has a list of active items/assets // We do this so that we can use the binding system of the UI Elements @@ -26,22 +67,25 @@ class ConverterState public bool isSelected; public bool isLoading; // to name public bool isInitialized; - public List items = new List(); + [SerializeReference] + private List items = new List(); [SerializeReference] public IRenderPipelineConverter converter; public DisplayFilter currentFilter = DisplayFilter.All; - public IList> filteredItems {get; private set; } = new List>(); + + private readonly List> m_FilteredItemsTree = new List>(); + private readonly List> m_CachedFullTree = new List>(); + private bool m_TreeCacheDirty = true; + + public IList> filteredItemsTree => m_FilteredItemsTree; private int CountItemWithFlag(Status status) { int count = 0; - foreach (ConverterItemState itemState in items) + foreach (var itemState in items) { - if (itemState.conversionResult.Status == status) - { - count++; - } + count += itemState.CountItemsWithStatus(status); } return count; } @@ -52,14 +96,21 @@ private int CountItemWithFlag(Status status) public override string ToString() { - return $"Warnings: {warnings} - Errors: {errors} - Ok: {success} - Total: {items?.Count ?? 0}"; + return $"Warnings: {warnings} - Errors: {errors} - Ok: {success} - Total: {totalItemsCount}"; } public void Clear() { + foreach (var itemState in items) + { + itemState.Clear(); + } + isInitialized = false; items.Clear(); - filteredItems.Clear(); + m_FilteredItemsTree.Clear(); + m_CachedFullTree.Clear(); + m_TreeCacheDirty = true; } private bool IsVisible(DisplayFilter filter) @@ -67,35 +118,204 @@ private bool IsVisible(DisplayFilter filter) return (currentFilter & filter) == filter; } - internal bool ShouldInclude(ConverterItemState converterItemState) + internal void AddItem(IRenderPipelineConverterItem item) + { + var itemState = CreateItemState(item); + items.Add(itemState); + m_TreeCacheDirty = true; + } + + internal void SetupRootEventHandlers(Action onRootSelectionChanged) { - return converterItemState.conversionResult.Status switch + foreach (var itemState in items) { - Status.Pending => IsVisible(DisplayFilter.Pending), - Status.Warning => IsVisible(DisplayFilter.Warnings), - Status.Error => IsVisible(DisplayFilter.Errors), - Status.Success => IsVisible(DisplayFilter.Success), - _ => false - }; + itemState.SetSelectionChangedHandler(onRootSelectionChanged); + } } - internal void AddItem(ConverterItemState converterItemState) + /// + /// Recursively creates the appropriate state type (FolderItemState or ConverterItemState) for an item + /// + private IConverterItemState CreateItemState(IRenderPipelineConverterItem item) { - items.Add(converterItemState); - if (ShouldInclude(converterItemState)) + if (item is IFolderRenderPipelineConverterItem folder) { - filteredItems.Add(new TreeViewItemData(filteredItems.Count, converterItemState)); + // Create folder state with children + var folderState = new FolderItemState + { + item = folder + }; + + // Recursively create states for children + if (folder.children != null) + { + foreach (var child in folder.children) + { + var childState = CreateItemState(child); + folderState.children.Add(childState); + } + } + + // Subscribe to children's events AFTER all children are added + folderState.SubscribeToChildren(); + + // Calculate initial state from children + folderState.SetSelectedWithoutNotify(folder.isEnabled); + + return folderState; + } + else + { + // Create regular item state + var leafState = new ConverterItemState + { + item = item + }; + + // Set selection silently during initialization + leafState.SetSelectedWithoutNotify(item.isEnabled); + + return leafState; } } internal void ApplyFilter() { - filteredItems.Clear(); + if (m_TreeCacheDirty) + { + m_CachedFullTree.Clear(); + BuildFullTree(items, m_CachedFullTree); + m_TreeCacheDirty = false; + } + + m_FilteredItemsTree.Clear(); + FilterTreeView(m_CachedFullTree, m_FilteredItemsTree); + } + + private void BuildFullTree(List itemStates, List> target) + { + int nodeId = 0; + var sortedItems = ListPool.Get(); + sortedItems.AddRange(itemStates); + + sortedItems.Sort((a, b) => a.CompareTo(b)); + + foreach (var itemState in sortedItems) + { + var treeNode = ConvertToTreeViewItemUncached(itemState, ref nodeId); + if (treeNode.HasValue) + target.Add(treeNode.Value); + } + + ListPool.Release(sortedItems); + } + + private TreeViewItemData? ConvertToTreeViewItemUncached(IConverterItemState itemState, ref int nodeId) + { + if (itemState is FolderItemState folderState) + { + using (var sortedChildren = ListPool.Get(out var sortedChildrenList)) + { + foreach (var child in folderState.children) + sortedChildrenList.Add(child); + + sortedChildrenList.Sort((a, b) => a.CompareTo(b)); + + using (var children = ListPool>.Get(out var childrenList)) + { + foreach (var child in sortedChildrenList) + { + var childNode = ConvertToTreeViewItemUncached(child, ref nodeId); + if (childNode.HasValue) + childrenList.Add(childNode.Value); + } + + var folderNodeData = new TreeNodeData( + folderState.item.name, + "", + isFolder: true, + itemState: folderState + ); + + var leafItems = folderState.GetAllLeafItems(); + folderNodeData.childItems.AddRange(leafItems); + + var childrenListCopy = new List>(childrenList); + + return new TreeViewItemData(nodeId++, folderNodeData, childrenListCopy); + } + } + } + else if (itemState is ConverterItemState leafItem) + { + var leafNodeData = new TreeNodeData( + leafItem.item.name, + "", + isFolder: false, + itemState: leafItem + ); + + return new TreeViewItemData(nodeId++, leafNodeData); + } + + return null; + } + + private void FilterTreeView(List> sourceTree, List> target) + { + foreach (var node in sourceTree) + { + var filteredNode = FilterTreeNode(node); + if (filteredNode.HasValue) + target.Add(filteredNode.Value); + } + } + + private TreeViewItemData? FilterTreeNode(TreeViewItemData node) + { + if (node.data.isFolder) + { + var filteredChildren = new List>(); + foreach (var child in node.children) + { + var filteredChild = FilterTreeNode(child); + if (filteredChild.HasValue) + filteredChildren.Add(filteredChild.Value); + } + + if (filteredChildren.Count == 0) + return null; + + return new TreeViewItemData(node.id, node.data, filteredChildren); + } + else + { + if (!IsVisible(node.data.itemState.GetDisplayFilter())) + return null; + + return node; + } + } + + internal IEnumerable GetAllLeafItems() + { + foreach (var itemState in items) + { + foreach (var leafItem in itemState.GetLeafItems()) + yield return leafItem; + } + } - foreach (var item in items) + public int totalItemsCount + { + get { - if (IsVisible((DisplayFilter)(1 << (int)item.conversionResult.Status))) - filteredItems.Add(new TreeViewItemData(filteredItems.Count, item)); + int count = 0; + foreach (var itemState in items) + { + count += itemState.GetTotalItemCount(); + } + return count; } } @@ -104,15 +324,21 @@ public int selectedItemsCount get { int count = 0; - foreach (ConverterItemState itemState in items) + foreach (var itemState in items) { - if (itemState.isSelected) - { - count++; - } + count += itemState.GetSelectedItemCount(); } return count; } } + + public void SetAllItemsSelected(bool selected) + { + foreach (var itemState in items) + { + if (itemState.item.isEnabled) + itemState.SetSelectedWithoutNotify(selected); + } + } } } diff --git a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/IRenderPipelineConverterItem.cs b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/IRenderPipelineConverterItem.cs index 67dabc0a8e6..4f4e76d79cd 100644 --- a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/IRenderPipelineConverterItem.cs +++ b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/IRenderPipelineConverterItem.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using UnityEngine; namespace UnityEditor.Rendering.Converter @@ -35,4 +36,18 @@ interface IRenderPipelineConverterItem void OnClicked(); } + /// + /// Represents a folder item that can contain other converter items in a custom hierarchy. + /// Converters can return folder items to provide their own organizational structure + /// instead of using the default path-based organization. + /// + interface IFolderRenderPipelineConverterItem : IRenderPipelineConverterItem + { + /// + /// Gets the child items contained in this folder. + /// Children can be regular items or other folder items for nested hierarchies. + /// + IList children { get; } + } + } diff --git a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/ReadonlyMaterialConverter/ReadonlyMaterialConverter.cs b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/ReadonlyMaterialConverter/ReadonlyMaterialConverter.cs index 50ac547a280..55568b8f016 100644 --- a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/ReadonlyMaterialConverter/ReadonlyMaterialConverter.cs +++ b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/ReadonlyMaterialConverter/ReadonlyMaterialConverter.cs @@ -93,6 +93,46 @@ internal abstract class ReadonlyMaterialConverter : AssetsConverter get => mappings.GetMaterialSearchList(); } + /// + /// Categorize results by material name instead of asset path + /// + protected override List CategorizeResults( + List assets, + Dictionary> assetsByDescription) + { + if (assetsByDescription == null || assetsByDescription.Count == 0) + return new List(); + + var result = new List(); + + foreach (var kvp in assetsByDescription) + { + var materialName = kvp.Key; + var materialAssets = kvp.Value; + + if (materialAssets.Count == 0) + continue; + + // Create asset group for this material (shows material icon, not folder icon) + var materialGroup = new RenderPipelineConverterUtility.AssetGroupItem + { + name = materialName, + info = $"{materialAssets.Count} object(s) referencing {materialName}", + assetType = typeof(Material) + }; + + // Add all assets to this group + foreach (var asset in materialAssets) + { + materialGroup.children.Add(asset); + } + + result.Add(materialGroup); + } + + return result; + } + internal MaterialReferenceChanger m_MaterialReferenceChanger; private ReadonlyMaterialMap m_Mappings; diff --git a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterAssetItem.cs b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterAssetItem.cs index 6ea909ec366..bb4626e60bd 100644 --- a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterAssetItem.cs +++ b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterAssetItem.cs @@ -4,6 +4,38 @@ namespace UnityEditor.Rendering.Converter { + /// + /// Constants for GlobalObjectId.identifierType values + /// + internal static class GlobalObjectIdentifierType + { + /// + /// Null object identifier + /// + public const int Null = 0; + + /// + /// Imported asset (prefab asset in project) + /// + public const int ImportedAsset = 1; + + /// + /// Scene object (object instance in scene or prefab instance) + /// + public const int SceneObject = 2; + + /// + /// Source asset identifier + /// + public const int SourceAsset = 3; + + /// + /// Built-in asset + /// + public const int BuiltInAsset = 4; + } + + [Serializable] internal class RenderPipelineConverterAssetItem : IRenderPipelineConverterItem { @@ -100,7 +132,7 @@ public UnityEngine.Object LoadObject() // If the object was not loaded, it is probably part of an unopened scene or prefab; // if so, then the solution is to first load the scene here. - var objIsInSceneOrPrefab = globalId.identifierType == 2; // 2 is IdentifierType.kSceneObject + var objIsInSceneOrPrefab = globalId.identifierType == GlobalObjectIdentifierType.SceneObject; if (!obj && objIsInSceneOrPrefab) { diff --git a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterManager.cs b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterManager.cs index a484518b371..7dcfc84eea9 100644 --- a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterManager.cs +++ b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterManager.cs @@ -47,7 +47,6 @@ private void ReloadConverters() { isSelected = false, isInitialized = false, - items = new List(), converter = renderPipelineConverter }; converterStates.Add(converterState); diff --git a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterMaterialUpgrader.cs b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterMaterialUpgrader.cs index d8abea0461c..9375bfe03d9 100644 --- a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterMaterialUpgrader.cs +++ b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterMaterialUpgrader.cs @@ -14,7 +14,7 @@ internal class RenderPipelineConverterMaterialUpgraderItem : IRenderPipelineConv public string name { get; } - public string info => assetPath; + public string info { get; } public bool isEnabled { get; set; } = true; public string isDisabledMessage { get; set; } = string.Empty; @@ -49,7 +49,8 @@ public RenderPipelineConverterMaterialUpgraderItem(string shaderPath, string mat if (material == null) throw new ArgumentException($"Unable to load material at path {materialPath}"); - name = material.name + " - " + shaderPath; + name = material.name; + info = materialPath; } public Material material => AssetDatabase.LoadAssetAtPath(assetPath); @@ -107,12 +108,16 @@ public void Scan(Action> onScanFinish) } assets.Clear(); + + using var pooledDict = UnityEngine.Pool.DictionaryPool>.Get(out var materialsByShader); + foreach (var kvp in materialsGroupByShader) { - // This material shader is already on the target pipeline, skip it. if (destinationShaders.Contains(kvp.Key)) continue; + var shaderPath = kvp.Key; + foreach (var (parent, variants) in kvp.Value) { List variantsPaths = new(); @@ -121,13 +126,46 @@ public void Scan(Action> onScanFinish) variantsPaths.Add(AssetDatabase.GetAssetPath(variant)); } - assets.Add(new RenderPipelineConverterMaterialUpgraderItem(kvp.Key, + var item = new RenderPipelineConverterMaterialUpgraderItem(shaderPath, AssetDatabase.GetAssetPath(parent), - variantsPaths)); + variantsPaths); + + assets.Add(item); + + if (!materialsByShader.ContainsKey(shaderPath)) + materialsByShader[shaderPath] = new List(); + + materialsByShader[shaderPath].Add(item); + } + } + + var organizedList = new List(); + + foreach (var kvp in materialsByShader) + { + var shaderPath = kvp.Key; + var materials = kvp.Value; + + if (materials.Count == 0) + continue; + + var shaderGroup = new RenderPipelineConverterUtility.AssetGroupItem + { + name = shaderPath, + info = $"{materials.Count} material(s) using {shaderPath}", + assetType = typeof(Shader) + }; + + foreach (var material in materials) + { + shaderGroup.children.Add(material); } + + organizedList.Add(shaderGroup); } - onScanFinish?.Invoke(assets); - } + + onScanFinish?.Invoke(organizedList); + } } public Status Convert(IRenderPipelineConverterItem item, out string message) diff --git a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterUtility.cs b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterUtility.cs new file mode 100644 index 00000000000..c089b1f8c6f --- /dev/null +++ b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterUtility.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Pool; + +namespace UnityEditor.Rendering.Converter +{ + /// + /// Utility methods for render pipeline converters + /// + internal static class RenderPipelineConverterUtility + { + private static Texture2D s_FolderIconCache; + + /// + /// Helper class to represent a folder item with children + /// + internal class FolderItem : IFolderRenderPipelineConverterItem + { + public string name { get; set; } + public string info { get; set; } + public bool isEnabled { get; set; } = true; + public string isDisabledMessage { get; set; } + public IList children { get; set; } = new List(); + + public virtual Texture2D icon => s_FolderIconCache ??= EditorGUIUtility.FindTexture("Folder Icon"); + + public void OnClicked() { } + } + + /// + /// Helper class to represent an asset group (not a file system folder) with children + /// Shows an asset type icon instead of a folder icon + /// + internal class AssetGroupItem : IFolderRenderPipelineConverterItem + { + public string name { get; set; } + public string info { get; set; } + public bool isEnabled { get; set; } = true; + public string isDisabledMessage { get; set; } + public IList children { get; set; } = new List(); + + /// + /// The asset type to use for the icon (e.g., typeof(Material), typeof(Shader)) + /// + public System.Type assetType { get; set; } + + private Texture2D m_CachedIcon; + + public virtual Texture2D icon + { + get + { + if (m_CachedIcon == null && assetType != null) + { + var content = EditorGUIUtility.ObjectContent(null, assetType); + m_CachedIcon = content?.image as Texture2D; + } + return m_CachedIcon; + } + } + + public void OnClicked() { } + + public void Reset() + { + name = null; + info = null; + isEnabled = true; + isDisabledMessage = null; + assetType = null; + m_CachedIcon = null; + children.Clear(); + } + } + + /// + /// Recursively collects all leaf items from a collection, flattening any folder hierarchies. + /// + internal static void CollectLeafItems(IEnumerable items, List targetList) + { + foreach (var item in items) + { + if (item is IFolderRenderPipelineConverterItem folder) + CollectLeafItems(folder.children, targetList); + else + targetList.Add(item); + } + } + } +} diff --git a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterUtility.cs.meta b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterUtility.cs.meta new file mode 100644 index 00000000000..5fd0606d752 --- /dev/null +++ b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderPipelineConverterUtility.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ce26739aa6355fb4498cde81884b9d90 \ No newline at end of file diff --git a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderSettingsConverter.cs b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderSettingsConverter.cs index 8b3195ad280..e67b25f6499 100644 --- a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderSettingsConverter.cs +++ b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/RenderSettingsConverter.cs @@ -6,6 +6,17 @@ namespace UnityEditor.Rendering.Converter { + /// + /// Represents the target for render pipeline configuration + /// + internal static class RenderPipelineTarget + { + /// + /// Indicates the default render pipeline in Graphics Settings (not tied to a specific quality level) + /// + public const int DefaultGraphicsSettings = -1; + } + [Serializable] internal class RenderSettingsConverterItem : IRenderPipelineConverterItem { @@ -18,19 +29,29 @@ internal class RenderSettingsConverterItem : IRenderPipelineConverterItem public bool isEnabled { get; set; } public string isDisabledMessage { get; set; } + /// + /// Settings path to open when clicked (e.g., "Project/Quality" or "Project/Graphics") + /// + public string settingsPath { get; set; } = "Project/Quality"; + + private static Texture2D s_CachedIcon; + public Texture2D icon { get { - var iconAttribute = typeof(RenderPipelineAsset).GetCustomAttribute(); - if (iconAttribute == null || string.IsNullOrEmpty(iconAttribute.path)) - return null; - return EditorGUIUtility.IconContent(iconAttribute.path)?.image as Texture2D; + if (s_CachedIcon == null) + { + var iconAttribute = typeof(RenderPipelineAsset).GetCustomAttribute(); + if (iconAttribute != null && !string.IsNullOrEmpty(iconAttribute.path)) + s_CachedIcon = EditorGUIUtility.IconContent(iconAttribute.path)?.image as Texture2D; + } + return s_CachedIcon; } } public void OnClicked() { - SettingsService.OpenProjectSettings("Project/Quality"); + SettingsService.OpenProjectSettings(settingsPath); } } @@ -39,7 +60,30 @@ abstract class RenderSettingsConverter : IRenderPipelineConverter { public void Scan(Action> onScanFinish) { - List renderPipelineConverterItems = new (); + List renderPipelineConverterItems = new(); + + var graphicsItems = new List(); + var defaultRPItem = new RenderSettingsConverterItem + { + qualityLevelIndex = RenderPipelineTarget.DefaultGraphicsSettings, + name = "Default Render Pipeline", + settingsPath = "Project/Graphics" + }; + + if (GraphicsSettings.defaultRenderPipeline is not RenderPipelineAsset) + { + defaultRPItem.isEnabled = true; + defaultRPItem.info = "Create a default Render Pipeline Asset for Graphics Settings"; + } + else + { + defaultRPItem.info = "Graphics Settings already reference a default Render Pipeline Asset."; + defaultRPItem.isEnabled = false; + defaultRPItem.isDisabledMessage = defaultRPItem.info; + } + graphicsItems.Add(defaultRPItem); + + var qualityItems = new List(); QualitySettings.ForEach((index, name) => { var item = new RenderSettingsConverterItem @@ -59,9 +103,28 @@ public void Scan(Action> onScanFinish) item.isEnabled = false; item.isDisabledMessage = item.info; } - renderPipelineConverterItems.Add(item); + qualityItems.Add(item); }); + var graphicsFolder = new RenderPipelineConverterUtility.AssetGroupItem + { + name = "Graphics", + info = "Default Render Pipeline in Graphics Settings", + assetType = typeof(RenderPipelineAsset), + children = graphicsItems + }; + + var qualityFolder = new RenderPipelineConverterUtility.AssetGroupItem + { + name = "Quality", + info = "Render Pipeline Assets per Quality Level", + assetType = typeof(RenderPipelineAsset), + children = qualityItems + }; + + renderPipelineConverterItems.Add(graphicsFolder); + renderPipelineConverterItems.Add(qualityFolder); + onScanFinish?.Invoke(renderPipelineConverterItems); } public abstract bool isEnabled { get; } @@ -76,7 +139,14 @@ public Status Convert(IRenderPipelineConverterItem item, out string message) { if (CreateRPAssetForQualityLevel(qualityLevelItem.qualityLevelIndex, out message)) { - message = "Each Quality Level now has a new, unique RP asset, but all share identical settings. Modify each asset to restore your performance/quality tiers."; + if (qualityLevelItem.qualityLevelIndex == RenderPipelineTarget.DefaultGraphicsSettings) + { + message = "Default Render Pipeline Asset created and assigned to Graphics Settings."; + } + else + { + message = "Each Quality Level now has a new, unique Render Pipeline Asset, but all share identical settings. Modify each asset to restore your performance/quality tiers."; + } return Status.Warning; } } @@ -89,13 +159,41 @@ private bool CreateRPAssetForQualityLevel(int qualityIndex, out string message) bool ok = false; message = string.Empty; + if (qualityIndex == RenderPipelineTarget.DefaultGraphicsSettings) + { + if (GraphicsSettings.defaultRenderPipeline is RenderPipelineAsset rpAsset) + { + message = $"Graphics Settings already reference a default Render Pipeline Asset: {rpAsset.name}."; + } + else + { + var asset = CreateAsset("DefaultRenderPipeline"); + + if (asset != null) + { + SetPipelineSettings(asset); + EditorUtility.SetDirty(asset); + AssetDatabase.SaveAssetIfDirty(asset); + + GraphicsSettings.defaultRenderPipeline = asset; + ok = true; + } + else + { + message = "Failed to create Render Pipeline Asset for Graphics Settings."; + } + } + + return ok; + } + var currentQualityLevel = QualitySettings.GetQualityLevel(); QualitySettings.SetQualityLevel(qualityIndex); - if (QualitySettings.renderPipeline is RenderPipelineAsset rpAsset) + if (QualitySettings.renderPipeline is RenderPipelineAsset qualityRPAsset) { - message = $"Quality Level {qualityIndex} already references a Render Pipeline Asset: {rpAsset.name}."; + message = $"Quality Level {qualityIndex} already references a Render Pipeline Asset: {qualityRPAsset.name}."; } else { @@ -103,10 +201,7 @@ private bool CreateRPAssetForQualityLevel(int qualityIndex, out string message) if (asset != null) { - // Map built-in data to the URP asset data SetPipelineSettings(asset); - - // Set the asset dirty to make sure that the renderer data is saved EditorUtility.SetDirty(asset); AssetDatabase.SaveAssetIfDirty(asset); @@ -115,7 +210,7 @@ private bool CreateRPAssetForQualityLevel(int qualityIndex, out string message) } else { - message = "Failed to create Universal Render Pipeline Asset."; + message = "Failed to create Render Pipeline Asset."; } } diff --git a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/Window/RenderPipelineConverterVisualElement.cs b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/Window/RenderPipelineConverterVisualElement.cs index 144174199d2..8d35cf0be06 100644 --- a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/Window/RenderPipelineConverterVisualElement.cs +++ b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/Window/RenderPipelineConverterVisualElement.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; using System.Text; -using Mono.Cecil; using UnityEditor.Categorization; -using UnityEditor.IMGUI.Controls; using UnityEngine; using UnityEngine.UIElements; @@ -14,6 +12,10 @@ internal class RenderPipelineConverterVisualElement : VisualElement const string k_Uxml = "Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/Window/RenderPipelineConverterVisualElement.uxml"; const string k_Uss = "Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/Window/RenderPipelineConverterVisualElement.uss"; + const string k_ColumnName = "name"; + const string k_ColumnInfo = "info"; + const string k_ColumnState = "state"; + static Lazy s_VisualTreeAsset = new Lazy(() => AssetDatabase.LoadAssetAtPath(k_Uxml)); static Lazy s_StyleSheet = new Lazy(() => AssetDatabase.LoadAssetAtPath(k_Uss)); @@ -71,6 +73,15 @@ public RenderPipelineConverterVisualElement(Node converterInfo) m_HeaderFoldout.showEnableCheckbox = true; m_HeaderFoldout.documentationURL = converterInfo.helpUrl; + // Add context menu for expand/collapse all + m_HeaderFoldout.contextMenuGenerator = () => + { + var menu = new GenericMenu(); + menu.AddItem(new GUIContent("Expand All"), false, () => ExpandCollapseAll(true)); + menu.AddItem(new GUIContent("Collapse All"), false, () => ExpandCollapseAll(false)); + return menu; + }; + m_HeaderFoldout.enableToggle.SetValueWithoutNotify(state.isSelected); m_HeaderFoldout.enableToggle.RegisterCallback((evt) => { @@ -84,12 +95,14 @@ public RenderPipelineConverterVisualElement(Node converterInfo) { SetItemsActive(true); Refresh(); + converterSelected?.Invoke(); }); var noneLabel = m_RootVisualElement.Q