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 3a13a224f72..5cf8c7d8a20 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
@@ -1,7 +1,10 @@
using System;
using System.Collections.Generic;
using System.Text;
+using UnityEditor.SceneManagement;
+using UnityEditor.Search;
using UnityEngine;
+using UnityEngine.SceneManagement;
namespace UnityEditor.Rendering.Converter
{
@@ -57,22 +60,11 @@ void OnSearchFinish()
searchQueries,
(item, description) =>
{
- var unityObject = item.ToObject();
-
- if (unityObject == null)
- return;
-
- GameObject go = null;
-
- if (unityObject is GameObject gameObject)
- go = gameObject;
- else if (unityObject is Component component)
- go = component.gameObject;
- else
+ var newAssetItem = CreateAssetItem(item);
+ if (newAssetItem == null)
return;
- var gid = GlobalObjectId.GetGlobalObjectIdSlow(go);
- var gidString = gid.ToString();
+ var gidString = newAssetItem.GlobalObjectId;
string groupName = GetGroupNameFromDescription(description);
@@ -89,14 +81,7 @@ void OnSearchFinish()
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,
- };
-
+ assetItem = newAssetItem;
assets.Add(assetItem);
assetLookup[gidString] = assetItem;
}
@@ -125,6 +110,77 @@ void OnSearchFinish()
}
}
+ internal static RenderPipelineConverterAssetItem CreateAssetItem(SearchItem item)
+ {
+ var unityObject = item.ToObject();
+
+ if (unityObject == null)
+ return CreateUnloadedSceneObjectItem(item);
+
+ GameObject go = null;
+
+ if (unityObject is GameObject gameObject)
+ go = gameObject;
+ else if (unityObject is Component component)
+ go = component.gameObject;
+ else
+ return null;
+
+ var gid = GlobalObjectId.GetGlobalObjectIdSlow(go);
+
+ if (gid.identifierType == GlobalObjectIdentifierType.ImportedAsset)
+ {
+ return new RenderPipelineConverterAssetItem(gid.ToString())
+ {
+ name = $"{unityObject.name} (Prefab)",
+ info = AssetDatabase.GetAssetPath(unityObject),
+ };
+ }
+
+ return new RenderPipelineConverterAssetItem(gid.ToString())
+ {
+ name = GetSceneObjectName(UnityEditor.Search.SearchUtils.GetTransformPath(go.transform)),
+ info = go.scene.path,
+ };
+ }
+
+ // Named with the path in the hierarchy, the only name Search has for objects in closed scenes. GameObject
+ // names can contain '/', so the object's own name can't be taken from it.
+ internal static string GetSceneObjectName(string transformPath)
+ {
+ var path = transformPath.StartsWith('/') ? transformPath.Substring(1) : transformPath;
+ return $"{path} (SceneObject)";
+ }
+
+ // Returns null unless the id is of an object in a scene that is not loaded
+ internal static string GetUnloadedScenePath(string globalObjectId)
+ {
+ if (!GlobalObjectId.TryParse(globalObjectId, out var gid) || gid.identifierType != GlobalObjectIdentifierType.SceneObject)
+ return null;
+
+ var scenePath = AssetDatabase.GUIDToAssetPath(gid.assetGUID);
+ if (string.IsNullOrEmpty(scenePath) || SceneManager.GetSceneByPath(scenePath).isLoaded)
+ return null;
+
+ return scenePath;
+ }
+
+ // A scene object only loads while its scene is open, so build the item from the search result and let
+ // LoadObject open the scene at Convert time
+ static RenderPipelineConverterAssetItem CreateUnloadedSceneObjectItem(SearchItem item)
+ {
+ var scenePath = GetUnloadedScenePath(item.id);
+ if (scenePath == null)
+ return null;
+
+ // Search labels scene objects with their path in the hierarchy
+ return new RenderPipelineConverterAssetItem(item.id)
+ {
+ name = GetSceneObjectName(item.GetLabel(item.context, true) ?? string.Empty),
+ info = scenePath,
+ };
+ }
+
///
/// Categorizes scan results into a tree structure.
/// Override this method to customize how items are organized.
@@ -159,7 +215,10 @@ protected virtual string GetGroupNameFromDescription(string description)
return description.Replace(" is being referenced", "");
}
- public virtual void BeforeConvert() { }
+ public virtual void BeforeConvert()
+ {
+ m_ScenesOpenedForConversion?.Clear();
+ }
protected abstract Status ConvertObject(UnityEngine.Object obj, StringBuilder message);
@@ -173,6 +232,10 @@ public Status Convert(IRenderPipelineConverterItem item, out string message)
return Status.Error;
}
+ var scenePath = GetUnloadedScenePath(assetItem.GlobalObjectId);
+ if (scenePath != null)
+ (m_ScenesOpenedForConversion ??= new HashSet()).Add(scenePath);
+
var obj = assetItem.LoadObject();
if (obj == null)
@@ -184,10 +247,33 @@ public Status Convert(IRenderPipelineConverterItem item, out string message)
var errorString = new StringBuilder();
var status = ConvertObject(obj, errorString);
+ SaveSceneOpenedForConversion(obj);
message = errorString.ToString();
return status;
}
- public virtual void AfterConvert() { }
+ // Scenes opened to convert their objects are closed again without saving when the next one opens, or when
+ // the conversion returns to the scene it started from. Only tracked for the current conversion, so that a
+ // later one leaves saving them to the user.
+ [NonSerialized]
+ HashSet m_ScenesOpenedForConversion;
+
+ void SaveSceneOpenedForConversion(UnityEngine.Object obj)
+ {
+ var scene = obj switch
+ {
+ GameObject go => go.scene,
+ Component component => component.gameObject.scene,
+ _ => default
+ };
+
+ if (scene.IsValid() && scene.isDirty && m_ScenesOpenedForConversion != null && m_ScenesOpenedForConversion.Contains(scene.path))
+ EditorSceneManager.SaveScene(scene);
+ }
+
+ public virtual void AfterConvert()
+ {
+ m_ScenesOpenedForConversion?.Clear();
+ }
}
}
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 75c648a7fba..d0f717d5a2d 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
@@ -16,29 +16,34 @@ internal static class CheckboxStateHelper
///
public static (bool isChecked, bool isIndeterminate) CalculateCheckboxState(IEnumerable childItems)
{
- int selectedCount = 0;
- int enabledCount = 0;
+ bool hasSelected = false;
+ bool hasUnselected = false;
foreach (var child in childItems)
{
if (child.item.isEnabled)
{
- enabledCount++;
if (child.isSelected == true)
- selectedCount++;
+ hasSelected = true;
+ else
+ hasUnselected = true;
+
+ // Early exit: mixed state found
+ if (hasSelected && hasUnselected)
+ return (false, true);
}
}
- if (enabledCount == 0)
- return (false, false); // No enabled children
-
- if (selectedCount == 0)
- return (false, false); // None selected
+ // No enabled children
+ if (!hasSelected && !hasUnselected)
+ return (false, false);
- if (selectedCount == enabledCount)
- return (true, false); // All selected
+ // All selected (hasSelected && !hasUnselected)
+ if (hasSelected)
+ return (true, false);
- return (false, true); // Mixed state (some selected)
+ // None selected (!hasSelected && hasUnselected)
+ return (false, false);
}
///
@@ -59,6 +64,7 @@ public static (bool isChecked, bool isIndeterminate) CalculateCheckboxState(IEnu
interface IConverterItemState
{
bool? isSelected { get; set; }
+ bool isExpanded { get; set; }
IRenderPipelineConverterItem item { get; }
(Status Status, string Message) conversionResult { get; }
bool hasConverted { get; }
@@ -70,6 +76,7 @@ interface IConverterItemState
void SetSelectionChangedHandler(Action handler);
void Clear();
int CountItemsWithStatus(Status status);
+ int CountSelectedItemsWithStatus(Status status);
int GetTotalItemCount();
int GetSelectedItemCount();
bool IsFolder();
@@ -89,6 +96,7 @@ public Action onIsSelectedChanged
set => m_OnIsSelectedChanged = value;
}
+ [SerializeField]
private bool m_IsSelected;
public bool? isSelected
{
@@ -103,16 +111,27 @@ public bool? isSelected
}
}
+ // Leaf items are never expanded
+ public bool isExpanded { get => false; set { } }
+
+ [field:SerializeReference]
public IRenderPipelineConverterItem item { get; set; }
- [NonSerialized]
- private (Status Status, string Message) m_ConversionResult = (Status.Pending, string.Empty);
+ [SerializeField]
+ private Status m_ConversionStatus = Status.Pending;
+ [SerializeField]
+ private string m_ConversionMessage = string.Empty;
+
public (Status Status, string Message) conversionResult
{
- get => m_ConversionResult;
- set => m_ConversionResult = value;
+ get => (m_ConversionStatus, m_ConversionMessage);
+ set
+ {
+ m_ConversionStatus = value.Status;
+ m_ConversionMessage = value.Message;
+ }
}
- public bool hasConverted => m_ConversionResult.Status != Status.Pending;
+ public bool hasConverted => m_ConversionStatus != Status.Pending;
public void OnSelectionChanged(ClickEvent _)
{
@@ -136,7 +155,12 @@ public void SetSelectionChangedHandler(Action handler)
public int CountItemsWithStatus(Status status)
{
- return m_ConversionResult.Status == status ? 1 : 0;
+ return m_ConversionStatus == status ? 1 : 0;
+ }
+
+ public int CountSelectedItemsWithStatus(Status status)
+ {
+ return (m_IsSelected && m_ConversionStatus == status) ? 1 : 0;
}
public int GetTotalItemCount()
@@ -160,15 +184,26 @@ public bool IsFolder()
public int CompareTo(IConverterItemState other)
{
+ if (other == null)
+ return 1;
+
if (other.IsFolder())
return 1;
+ // Handle null items gracefully
+ if (item == null && other.item == null)
+ return 0;
+ if (item == null)
+ return -1;
+ if (other.item == null)
+ return 1;
+
return string.Compare(item.name, other.item.name, StringComparison.Ordinal);
}
public DisplayFilter GetDisplayFilter()
{
- return m_ConversionResult.Status switch
+ return m_ConversionStatus switch
{
Status.Pending => DisplayFilter.Pending,
Status.Warning => DisplayFilter.Warnings,
@@ -209,14 +244,30 @@ public bool? isSelected
}
}
+ [SerializeField]
+ private bool m_IsExpanded = true; // Default to true so folders are expanded by default
+ public bool isExpanded
+ {
+ get => m_IsExpanded;
+ set => m_IsExpanded = value;
+ }
+
+ [field:SerializeReference]
public IRenderPipelineConverterItem item { get; set; }
- [NonSerialized]
- private (Status Status, string Message) m_ConversionResult = (Status.Pending, string.Empty);
+ [SerializeField]
+ private Status m_ConversionStatus = Status.Pending;
+ [SerializeField]
+ private string m_ConversionMessage = string.Empty;
+
public (Status Status, string Message) conversionResult
{
- get => m_ConversionResult;
- set => m_ConversionResult = value;
+ get => (m_ConversionStatus, m_ConversionMessage);
+ set
+ {
+ m_ConversionStatus = value.Status;
+ m_ConversionMessage = value.Message;
+ }
}
public bool hasConverted => false;
@@ -265,6 +316,30 @@ public void SubscribeToChildren()
}
}
+ ///
+ /// Recalculates folder selection state from children.
+ /// Called after deserialization since nullable bool can't be serialized.
+ ///
+ public void RecalculateSelectionFromChildren()
+ {
+ // Recursively recalculate for nested folders first, so this folder
+ // aggregates up-to-date child states
+ foreach (var child in children)
+ {
+ if (child is FolderItemState childFolder)
+ childFolder.RecalculateSelectionFromChildren();
+ }
+
+ m_IsSelected = CheckboxStateHelper.CalculateSelectionState(children);
+
+ // Recursively recalculate for nested folders
+ foreach (var child in children)
+ {
+ if (child is FolderItemState childFolder)
+ childFolder.RecalculateSelectionFromChildren();
+ }
+ }
+
public void UnsubscribeFromChildren()
{
foreach (var child in children)
@@ -315,6 +390,16 @@ public int CountItemsWithStatus(Status status)
return count;
}
+ public int CountSelectedItemsWithStatus(Status status)
+ {
+ int count = 0;
+ foreach (var child in children)
+ {
+ count += child.CountSelectedItemsWithStatus(status);
+ }
+ return count;
+ }
+
public int GetTotalItemCount()
{
int count = 0;
@@ -347,9 +432,20 @@ public bool IsFolder()
public int CompareTo(IConverterItemState other)
{
+ if (other == null)
+ return 1;
+
if (!other.IsFolder())
return -1;
+ // Handle null items gracefully
+ if (item == null && other.item == null)
+ return 0;
+ if (item == null)
+ return -1;
+ if (other.item == null)
+ return 1;
+
return string.Compare(item.name, other.item.name, StringComparison.Ordinal);
}
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 16f592ede19..a64b85439d0 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
@@ -89,7 +89,19 @@ private int CountItemWithFlag(Status status)
}
return count;
}
+
+ private int CountSelectedItemWithFlag(Status status)
+ {
+ int count = 0;
+ foreach (var itemState in items)
+ {
+ count += itemState.CountSelectedItemsWithStatus(status);
+ }
+ return count;
+ }
+
public int pending => CountItemWithFlag(Status.Pending);
+ public int selectedPending => CountSelectedItemWithFlag(Status.Pending);
public int warnings => CountItemWithFlag(Status.Warning);
public int errors => CountItemWithFlag(Status.Error);
public int success => CountItemWithFlag(Status.Success);
@@ -111,6 +123,10 @@ public void Clear()
m_FilteredItemsTree.Clear();
m_CachedFullTree.Clear();
m_TreeCacheDirty = true;
+
+ // Reset filter to show all items when re-scanning
+ // (prevents blank tree when filter doesn't match new scan results)
+ currentFilter = DisplayFilter.All;
}
private bool IsVisible(DisplayFilter filter)
@@ -133,6 +149,76 @@ internal void SetupRootEventHandlers(Action onRootSelectionChanged)
}
}
+ ///
+ /// Cleans up invalid items after deserialization.
+ /// Called automatically after assembly reload.
+ /// Note: Root event handlers are re-established by the window during CreateGUI.
+ /// Note: Tree cache rebuild is deferred - window should call ApplyFilter() before using filteredItemsTree.
+ ///
+ internal void OnAfterDeserialize()
+ {
+ // Recursively validate and clean up items
+ bool hasValidItems = ValidateAndCleanItems(items);
+
+ if (!hasValidItems)
+ {
+ isInitialized = false;
+ return;
+ }
+
+ // Re-subscribe folder items to their children's selection changes
+ // and recalculate folder selection states from children
+ foreach (var itemState in items)
+ {
+ if (itemState is FolderItemState folderState)
+ {
+ folderState.SubscribeToChildren();
+ folderState.RecalculateSelectionFromChildren();
+ }
+ }
+
+ // Mark tree cache as dirty - window will call ApplyFilter() when needed
+ m_TreeCacheDirty = true;
+ }
+
+ ///
+ /// Recursively validates items and removes invalid ones.
+ /// Returns true if any valid items remain.
+ ///
+ private bool ValidateAndCleanItems(List itemList)
+ {
+ bool hasValidItems = false;
+
+ for (int i = itemList.Count - 1; i >= 0; i--)
+ {
+ var itemState = itemList[i];
+ if (itemState == null || itemState.item == null)
+ {
+ itemList.RemoveAt(i);
+ }
+ else
+ {
+ hasValidItems = true;
+
+ // Recursively validate children of folders
+ if (itemState is FolderItemState folderState && folderState.children != null && folderState.children.Count > 0)
+ {
+ ValidateAndCleanItems(folderState.children);
+ }
+ }
+ }
+
+ return hasValidItems;
+ }
+
+ ///
+ /// No-op - event handlers are [NonSerialized] and don't need cleanup.
+ /// Kept for ISerializationCallbackReceiver contract.
+ ///
+ internal void OnBeforeSerialize()
+ {
+ }
+
///
/// Recursively creates the appropriate state type (FolderItemState or ConverterItemState) for an item
///
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 55568b8f016..52ddb2f0fd7 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
@@ -7,12 +7,19 @@ namespace UnityEditor.Rendering.Converter
{
internal class ReadonlyMaterialMap
{
+ const string k_BuiltInExtraResourcesPath = "Resources/unity_builtin_extra";
+
public bool TryGetMappingMaterial(Material material, out Material mappingMaterial)
{
mappingMaterial = material;
- if (m_BuiltInMaterialsMappings.TryGetValue(material.name, out var mapping))
+ if (material != null &&
+ sourceMaterials.TryGetValue(material.name, out var sourceMaterial) &&
+ sourceMaterial == material &&
+ m_BuiltInMaterialsMappings.TryGetValue(material.name, out var mapping))
+ {
mappingMaterial = mapping();
+ }
return mappingMaterial != null;
}
@@ -22,6 +29,10 @@ public bool TryGetMappingMaterial(Material material, out Material mappingMateria
public IEnumerable Keys => m_BuiltInMaterialsMappings.Keys;
Dictionary> m_BuiltInMaterialsMappings = new();
+ Func> m_SourceMaterialsProvider;
+ Dictionary m_SourceMaterials;
+
+ Dictionary sourceMaterials => m_SourceMaterials ??= CollectSourceMaterials();
public List<(string materialName, string searchQuery)> GetMaterialSearchList()
{
@@ -34,9 +45,30 @@ public bool TryGetMappingMaterial(Material material, out Material mappingMateria
return list;
}
- public ReadonlyMaterialMap(Dictionary> mappings)
+ public ReadonlyMaterialMap(Dictionary> mappings, Func> sourceMaterialsProvider = null)
{
m_BuiltInMaterialsMappings = mappings;
+ m_SourceMaterialsProvider = sourceMaterialsProvider ?? LoadBuiltInExtraMaterials;
+ }
+
+ public static IEnumerable LoadBuiltInExtraMaterials()
+ {
+ foreach (var asset in AssetDatabase.LoadAllAssetsAtPath(k_BuiltInExtraResourcesPath))
+ {
+ if (asset is Material material)
+ yield return material;
+ }
+ }
+
+ Dictionary CollectSourceMaterials()
+ {
+ var materials = new Dictionary();
+ foreach (var material in m_SourceMaterialsProvider())
+ {
+ if (material != null && m_BuiltInMaterialsMappings.ContainsKey(material.name))
+ materials.TryAdd(material.name, material);
+ }
+ return materials;
}
public Material[] GetBuiltInMaterials()
@@ -45,34 +77,9 @@ public Material[] GetBuiltInMaterials()
{
foreach (var materialName in Keys)
{
- var name = materialName + ".mat";
-
- Material mat = null;
- foreach (var material in AssetDatabaseHelper.FindAssets())
+ if (!sourceMaterials.TryGetValue(materialName, out var mat) || mat == null)
{
- if (material.name == materialName)
- {
- mat = material;
- break;
- }
- }
-
- if (mat == null)
- {
- mat = AssetDatabase.GetBuiltinExtraResource(name);
- if (mat == null)
- {
- mat = Resources.GetBuiltinResource(name);
- if (mat == null)
- {
- mat = Resources.Load(name);
- }
- }
- }
-
- if (mat == null)
- {
- Debug.LogError($"Material '{materialName}' not found in built-in resources or project assets.");
+ Debug.LogError($"Material '{materialName}' not found in built-in resources.");
continue;
}
@@ -88,6 +95,8 @@ internal abstract class ReadonlyMaterialConverter : AssetsConverter
{
protected virtual Dictionary> materialMappings { get; }
+ protected virtual IEnumerable GetSourceMaterials() => ReadonlyMaterialMap.LoadBuiltInExtraMaterials();
+
protected override List<(string query, string description)> contextSearchQueriesAndIds
{
get => mappings.GetMaterialSearchList();
@@ -140,13 +149,14 @@ internal ReadonlyMaterialMap mappings
{
get
{
- m_Mappings ??= new ReadonlyMaterialMap(materialMappings);
+ m_Mappings ??= new ReadonlyMaterialMap(materialMappings, GetSourceMaterials);
return m_Mappings;
}
}
public override void BeforeConvert()
{
+ base.BeforeConvert();
m_MaterialReferenceChanger = new MaterialReferenceChanger(mappings);
}
@@ -154,6 +164,7 @@ public override void AfterConvert()
{
m_MaterialReferenceChanger?.Dispose();
m_MaterialReferenceChanger = null;
+ base.AfterConvert();
}
protected override Status ConvertObject(UnityEngine.Object obj, StringBuilder message)
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 bb4626e60bd..e12f2d34b3f 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
@@ -79,8 +79,21 @@ public string info
set => m_Info = value;
}
- public bool isEnabled { get; set; } = true;
- public string isDisabledMessage { get; set; } = string.Empty;
+ [SerializeField]
+ private bool m_IsEnabled = true;
+ public bool isEnabled
+ {
+ get => m_IsEnabled;
+ set => m_IsEnabled = value;
+ }
+
+ [SerializeField]
+ private string m_IsDisabledMessage = string.Empty;
+ public string isDisabledMessage
+ {
+ get => m_IsDisabledMessage;
+ set => m_IsDisabledMessage = value;
+ }
public Texture2D icon
{
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 7dcfc84eea9..7756030fa7f 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
@@ -71,15 +71,17 @@ internal void Reset()
public void OnBeforeSerialize()
{
- // TODO: As the converters have data stored inside during initialization we need to clear the states
- // Once we keep the items to be classes and converters can inherit them to store any kind of data this can be removed
+ // Clear event handlers before serialization
+ // They will be re-established by the window during CreateGUI after reload
foreach (var state in converterStates)
- state.Clear();
+ {
+ state.OnBeforeSerialize();
+ }
}
public void OnAfterDeserialize()
{
- // Something null, remove it
+ // Remove null states
for (int i = converterStates.Count - 1; i >= 0; i--)
{
if (converterStates[i] == null || converterStates[i].converter == null)
@@ -87,6 +89,12 @@ public void OnAfterDeserialize()
converterStates.RemoveAt(i);
}
}
+
+ // Clean up invalid items and rebuild trees
+ foreach (var state in converterStates)
+ {
+ state.OnAfterDeserialize();
+ }
}
}
}
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 5c9bb00b331..265638d5077 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
@@ -8,15 +8,18 @@ namespace UnityEditor.Rendering.Converter
[Serializable]
internal class RenderPipelineConverterMaterialUpgraderItem : IRenderPipelineConverterItem
{
- public string assetPath { get; }
- public List variantsPaths { get; }
-
- public string name { get; }
-
- public string info { get; }
-
- public bool isEnabled { get; set; } = true;
- public string isDisabledMessage { get; set; } = string.Empty;
+ [SerializeField]
+ private string m_AssetPath;
+ public string assetPath => m_AssetPath;
+
+ [SerializeField]
+ private List m_VariantsPaths;
+ public List variantsPaths => m_VariantsPaths;
+
+ [field:SerializeField] public string name { get; set; }
+ [field:SerializeField] public string info { get; set; }
+ [field:SerializeField] public bool isEnabled { get; set; } = true;
+ [field:SerializeField] public string isDisabledMessage { get; set; }
public Texture2D icon
{
@@ -42,8 +45,8 @@ public RenderPipelineConverterMaterialUpgraderItem(string shaderPath, string mat
if (string.IsNullOrEmpty(materialPath))
throw new ArgumentException(nameof(materialPath));
- assetPath = materialPath;
- this.variantsPaths = variantsPaths;
+ m_AssetPath = materialPath;
+ m_VariantsPaths = variantsPaths;
if (material == null)
throw new ArgumentException($"Unable to load material at path {materialPath}");
@@ -170,6 +173,9 @@ public void Scan(Action> onScanFinish)
public Status Convert(IRenderPipelineConverterItem item, out string message)
{
+ // Lazy initialization of upgraders cache (needed after assembly reload since it's not serialized)
+ m_UpgradersCache ??= upgraders;
+
if (item is not RenderPipelineConverterMaterialUpgraderItem materialUpgraderItem)
{
message = $"Item is not a {nameof(RenderPipelineConverterMaterialUpgraderItem)}.";
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
index c089b1f8c6f..6bdd92e38e9 100644
--- 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
@@ -15,13 +15,40 @@ internal static class RenderPipelineConverterUtility
///
/// Helper class to represent a folder item with children
///
+ [Serializable]
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();
+ [field:SerializeField] public string name { get; set; }
+ [field:SerializeField] public string info { get; set; }
+
+ ///
+ /// Folder is enabled if at least one child is enabled.
+ /// This follows Unity Editor Design System guidelines: disable controls that aren't available right now.
+ ///
+ public bool isEnabled
+ {
+ get
+ {
+ // A folder is enabled if any of its children are enabled
+ foreach (var child in m_Children)
+ {
+ if (child.isEnabled)
+ return true;
+ }
+ return false;
+ }
+ set { /* Computed property, setter is no-op */ }
+ }
+
+ [field:SerializeField] public string isDisabledMessage { get; set; }
+
+ [SerializeReference]
+ private List m_Children = new List();
+ public IList children
+ {
+ get => m_Children;
+ set => m_Children = value == null ? new List() : (value as List ?? new List(value));
+ }
public virtual Texture2D icon => s_FolderIconCache ??= EditorGUIUtility.FindTexture("Folder Icon");
@@ -32,21 +59,64 @@ 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
+ [Serializable]
+ internal class AssetGroupItem : IFolderRenderPipelineConverterItem, ISerializationCallbackReceiver
{
- 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();
+ [field:SerializeField] public string name { get; set; }
+ [field:SerializeField] public string info { get; set; }
+
+ ///
+ /// Asset group is enabled if at least one child is enabled.
+ /// This follows Unity Editor Design System guidelines: disable controls that aren't available right now.
+ ///
+ public bool isEnabled
+ {
+ get
+ {
+ // An asset group is enabled if any of its children are enabled
+ foreach (var child in m_Children)
+ {
+ if (child.isEnabled)
+ return true;
+ }
+ return false;
+ }
+ set { /* Computed property, setter is no-op */ }
+ }
+
+ [field:SerializeField] public string isDisabledMessage { get; set; }
+
+ [SerializeReference]
+ private List m_Children = new List();
+ public IList children
+ {
+ get => m_Children;
+ set => m_Children = value == null ? new List() : (value as List ?? new List(value));
+ }
///
/// The asset type to use for the icon (e.g., typeof(Material), typeof(Shader))
///
public System.Type assetType { get; set; }
+ [SerializeField]
+ private string m_AssetTypeAssemblyQualifiedName;
+
private Texture2D m_CachedIcon;
+ public void OnBeforeSerialize()
+ {
+ m_AssetTypeAssemblyQualifiedName = assetType?.AssemblyQualifiedName;
+ }
+
+ public void OnAfterDeserialize()
+ {
+ if (!string.IsNullOrEmpty(m_AssetTypeAssemblyQualifiedName))
+ {
+ assetType = System.Type.GetType(m_AssetTypeAssemblyQualifiedName);
+ }
+ }
+
public virtual Texture2D icon
{
get
@@ -66,11 +136,12 @@ public void Reset()
{
name = null;
info = null;
- isEnabled = true;
+ // isEnabled is now computed from children, no need to reset it
isDisabledMessage = null;
assetType = null;
+ m_AssetTypeAssemblyQualifiedName = null;
m_CachedIcon = null;
- children.Clear();
+ m_Children.Clear();
}
}
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 e67b25f6499..86ea0d80000 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
@@ -20,19 +20,12 @@ internal static class RenderPipelineTarget
[Serializable]
internal class RenderSettingsConverterItem : IRenderPipelineConverterItem
{
- public int qualityLevelIndex { get; set; }
-
- public string name { get; set; }
-
- public string info { get; set; }
-
- 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";
+ [field:SerializeField] public int qualityLevelIndex { get; set; }
+ [field:SerializeField] public string name { get; set; }
+ [field:SerializeField] public string info { get; set; }
+ [field:SerializeField] public bool isEnabled { get; set; } = true;
+ [field:SerializeField] public string isDisabledMessage { get; set; }
+ [field:SerializeField] public string settingsPath { get; set; } = "Project/Quality"; // Settings path to open when clicked (e.g., "Project/Quality" or "Project/Graphics")
private static Texture2D s_CachedIcon;
@@ -135,89 +128,74 @@ public Status Convert(IRenderPipelineConverterItem item, out string message)
{
message = string.Empty;
- if (item is RenderSettingsConverterItem qualityLevelItem)
+ if (item is not RenderSettingsConverterItem qualityLevelItem)
{
- if (CreateRPAssetForQualityLevel(qualityLevelItem.qualityLevelIndex, out message))
- {
- 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;
- }
+ message = $"Item is not a {nameof(RenderSettingsConverterItem)}.";
+ return Status.Error;
}
- return Status.Error;
+ return CreateRPAssetForQualityLevel(qualityLevelItem.qualityLevelIndex, out message);
}
- private bool CreateRPAssetForQualityLevel(int qualityIndex, out string message)
+ private Status 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}.";
+ message = $"Graphics Settings already references a Render Pipeline Asset: {rpAsset.name}.";
+ return Status.Warning;
}
- else
+
+ var asset = CreateAsset("DefaultRenderPipeline");
+ if (asset == null)
{
- 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.";
- }
+ message = "Failed to create Render Pipeline Asset for Graphics Settings.";
+ return Status.Error;
}
- return ok;
+ SetPipelineSettings(asset);
+ EditorUtility.SetDirty(asset);
+ AssetDatabase.SaveAssetIfDirty(asset);
+ GraphicsSettings.defaultRenderPipeline = asset;
+
+ message = "Default Render Pipeline Asset created and assigned to Graphics Settings.";
+ return Status.Warning;
}
var currentQualityLevel = QualitySettings.GetQualityLevel();
-
QualitySettings.SetQualityLevel(qualityIndex);
- if (QualitySettings.renderPipeline is RenderPipelineAsset qualityRPAsset)
+ try
{
- message = $"Quality Level {qualityIndex} already references a Render Pipeline Asset: {qualityRPAsset.name}.";
- }
- else
- {
- var asset = CreateAsset($"{QualitySettings.names[qualityIndex]}");
-
- if (asset != null)
+ if (QualitySettings.renderPipeline is RenderPipelineAsset qualityRPAsset)
{
- SetPipelineSettings(asset);
- EditorUtility.SetDirty(asset);
- AssetDatabase.SaveAssetIfDirty(asset);
-
- QualitySettings.renderPipeline = asset;
- ok = true;
+ message = $"Quality Level {qualityIndex} already references a Render Pipeline Asset: {qualityRPAsset.name}.";
+ return Status.Warning;
}
- else
+
+ var asset = CreateAsset($"{QualitySettings.names[qualityIndex]}");
+ if (asset == null)
{
message = "Failed to create Render Pipeline Asset.";
+ return Status.Error;
}
- }
- // Restore back the quality level
- QualitySettings.SetQualityLevel(currentQualityLevel);
+ SetPipelineSettings(asset);
+ EditorUtility.SetDirty(asset);
+ AssetDatabase.SaveAssetIfDirty(asset);
+ QualitySettings.renderPipeline = asset;
- return ok;
+ message = "Render Pipeline Asset created. Modify each asset to restore your performance/quality tiers.";
+ return Status.Warning;
+ }
+ finally
+ {
+ // Restore back the quality level
+ QualitySettings.SetQualityLevel(currentQualityLevel);
+ }
}
protected abstract RenderPipelineAsset CreateAsset(string name);
diff --git a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/SearchServiceUtils.cs b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/SearchServiceUtils.cs
index 157fe36abb4..462c266c5fd 100644
--- a/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/SearchServiceUtils.cs
+++ b/Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/SearchServiceUtils.cs
@@ -14,15 +14,18 @@ public enum IndexingOptions
PackageIndexing = 1 << 1
}
+ // Set while a caller blocks the Editor, so searches have to finish before RunQueuedSearch returns
+ public static bool synchronous { get; set; }
+
public static void RunQueuedSearch(
IndexingOptions neededOptions,
List<(string query, string description)> contextSearchQueriesAndIds,
Action onAssetGUIDFound,
- Action onSearchsFinished)
+ Action onSearchesFinished)
{
if (contextSearchQueriesAndIds == null || contextSearchQueriesAndIds.Count == 0)
{
- onSearchsFinished?.Invoke();
+ onSearchesFinished?.Invoke();
return;
}
@@ -32,6 +35,41 @@ public static void RunQueuedSearch(
bool isDeepSearchEnabled = Search.SearchService.IsDeepIndexingEnabled();
bool isPackageIndexingEnabled = Search.SearchService.IsPackageIndexingEnabled();
+ if (synchronous)
+ {
+ // Changing the indexing settings rebuilds the index over several Editor updates, which a blocked caller can't wait for
+ if (isDeepSearchEnabled != needsDeepSearch || isPackageIndexingEnabled != needsPackageIndexing)
+ {
+ throw new InvalidOperationException(
+ "A synchronous scan can't change the Search indexing settings. In Preferences > Search > Indexing, " +
+ $"set 'Deep scene and prefab indexing' to {(needsDeepSearch ? "enabled" : "disabled")} and 'Packages indexing' to {(needsPackageIndexing ? "enabled" : "disabled")}, then scan again.");
+ }
+
+ foreach (var (query, id) in contextSearchQueriesAndIds)
+ {
+ using (var context = Search.SearchService.CreateContext(query))
+ {
+ Search.SearchService.Request(context, (searchContext, searchItems) =>
+ {
+ foreach (var item in searchItems)
+ {
+ try
+ {
+ onAssetGUIDFound.Invoke(item, id);
+ }
+ catch (Exception e)
+ {
+ UnityEngine.Debug.LogException(e);
+ }
+ }
+ }, Search.SearchFlags.Synchronous);
+ }
+ }
+
+ onSearchesFinished?.Invoke();
+ return;
+ }
+
int index = 0;
void ProcessNextSearch()
{
@@ -44,12 +82,12 @@ void ProcessNextSearch()
Search.SearchService.ChangeIndexingSettings(
deepIndexing: isDeepSearchEnabled,
packageIndexing: isPackageIndexingEnabled,
- () => onSearchsFinished?.Invoke());
+ () => onSearchesFinished?.Invoke());
}
else
{
// No need to rollback the index info, the initialization is done now
- onSearchsFinished?.Invoke();
+ onSearchesFinished?.Invoke();
}
return;
}
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 0bc67d2d24a..5b5513cd966 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
@@ -7,6 +7,16 @@
namespace UnityEditor.Rendering.Converter
{
+ ///
+ /// User data stored in toggle elements for folder items in the tree view.
+ /// Contains the node data and optional callback for event cleanup.
+ ///
+ internal struct FolderToggleUserData
+ {
+ public TreeNodeData nodeData;
+ public EventCallback> callback;
+ }
+
internal class RenderPipelineConverterVisualElement : VisualElement
{
const string k_Uxml = "Packages/com.unity.render-pipelines.core/Editor-PrivateShared/Tools/Converter/Window/RenderPipelineConverterVisualElement.uxml";
@@ -95,12 +105,14 @@ public RenderPipelineConverterVisualElement(Node converterInfo)
{
SetItemsActive(true);
Refresh();
+ converterSelected?.Invoke();
});
var noneLabel = m_RootVisualElement.Q