351 lines
14 KiB
C#
351 lines
14 KiB
C#
|
using System;
|
||
|
using System.IO;
|
||
|
using UnityEngine.Assertions;
|
||
|
#if UNITY_EDITOR
|
||
|
using UnityEditor;
|
||
|
using UnityEditorInternal;
|
||
|
using System.Reflection;
|
||
|
#endif
|
||
|
|
||
|
namespace UnityEngine.Rendering
|
||
|
{
|
||
|
#if UNITY_EDITOR
|
||
|
/// <summary>
|
||
|
/// The resources that need to be reloaded in Editor can live in Runtime.
|
||
|
/// The reload call should only be done in Editor context though but it
|
||
|
/// could be called from runtime entities.
|
||
|
/// </summary>
|
||
|
public static class ResourceReloader
|
||
|
{
|
||
|
/// <summary>
|
||
|
/// Looks for resources in the given <paramref name="container"/> object and reload the ones
|
||
|
/// that are missing or broken.
|
||
|
/// This version will still return null value without throwing error if the issue is due to
|
||
|
/// AssetDatabase being not ready. But in this case the assetDatabaseNotReady result will be true.
|
||
|
/// </summary>
|
||
|
/// <param name="container">The object containing reload-able resources</param>
|
||
|
/// <param name="basePath">The base path for the package</param>
|
||
|
/// <returns>
|
||
|
/// - 1 hasChange: True if something have been reloaded.
|
||
|
/// - 2 assetDatabaseNotReady: True if the issue preventing loading is due to state of AssetDatabase
|
||
|
/// </returns>
|
||
|
public static (bool hasChange, bool assetDatabaseNotReady) TryReloadAllNullIn(System.Object container, string basePath)
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
return (ReloadAllNullIn(container, basePath), false);
|
||
|
}
|
||
|
catch (InvalidImportException)
|
||
|
{
|
||
|
return (false, true);
|
||
|
}
|
||
|
catch (Exception e)
|
||
|
{
|
||
|
throw e;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Looks for resources in the given <paramref name="container"/> object and reload the ones
|
||
|
/// that are missing or broken.
|
||
|
/// </summary>
|
||
|
/// <param name="container">The object containing reload-able resources</param>
|
||
|
/// <param name="basePath">The base path for the package</param>
|
||
|
/// <returns>True if something have been reloaded.</returns>
|
||
|
public static bool ReloadAllNullIn(System.Object container, string basePath)
|
||
|
{
|
||
|
if (IsNull(container))
|
||
|
return false;
|
||
|
|
||
|
var changed = false;
|
||
|
foreach (var fieldInfo in container.GetType()
|
||
|
.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static))
|
||
|
{
|
||
|
//Recurse on sub-containers
|
||
|
if (IsReloadGroup(fieldInfo))
|
||
|
{
|
||
|
changed |= FixGroupIfNeeded(container, fieldInfo);
|
||
|
changed |= ReloadAllNullIn(fieldInfo.GetValue(container), basePath);
|
||
|
}
|
||
|
|
||
|
//Find null field and reload them
|
||
|
var attribute = GetReloadAttribute(fieldInfo);
|
||
|
if (attribute != null)
|
||
|
{
|
||
|
if (attribute.paths.Length == 1)
|
||
|
{
|
||
|
changed |= SetAndLoadIfNull(container, fieldInfo, GetFullPath(basePath, attribute),
|
||
|
attribute.package);
|
||
|
}
|
||
|
else if (attribute.paths.Length > 1)
|
||
|
{
|
||
|
changed |= FixArrayIfNeeded(container, fieldInfo, attribute.paths.Length);
|
||
|
|
||
|
var array = (Array)fieldInfo.GetValue(container);
|
||
|
if (IsReloadGroup(array))
|
||
|
{
|
||
|
//Recurse on each sub-containers
|
||
|
for (int index = 0; index < attribute.paths.Length; ++index)
|
||
|
{
|
||
|
changed |= FixGroupIfNeeded(array, index);
|
||
|
changed |= ReloadAllNullIn(array.GetValue(index), basePath);
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
//Find each null element and reload them
|
||
|
for (int index = 0; index < attribute.paths.Length; ++index)
|
||
|
changed |= SetAndLoadIfNull(array, index, GetFullPath(basePath, attribute, index),
|
||
|
attribute.package);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (changed && container is UnityEngine.Object c)
|
||
|
EditorUtility.SetDirty(c);
|
||
|
return changed;
|
||
|
}
|
||
|
|
||
|
static void CheckReloadGroupSupportedType(Type type)
|
||
|
{
|
||
|
if (type.IsSubclassOf(typeof(ScriptableObject)))
|
||
|
throw new Exception(@$"ReloadGroup attribute must not be used on {nameof(ScriptableObject)}.
|
||
|
If {nameof(ResourceReloader)} create an instance of it, it will be not saved as a file, resulting in corrupted ID when building.");
|
||
|
}
|
||
|
|
||
|
static bool FixGroupIfNeeded(System.Object container, FieldInfo info)
|
||
|
{
|
||
|
var type = info.FieldType;
|
||
|
CheckReloadGroupSupportedType(type);
|
||
|
|
||
|
if (IsNull(container, info))
|
||
|
{
|
||
|
var value = Activator.CreateInstance(type);
|
||
|
|
||
|
info.SetValue(
|
||
|
container,
|
||
|
value
|
||
|
);
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
static bool FixGroupIfNeeded(Array array, int index)
|
||
|
{
|
||
|
Assert.IsNotNull(array);
|
||
|
|
||
|
var type = array.GetType().GetElementType();
|
||
|
CheckReloadGroupSupportedType(type);
|
||
|
|
||
|
if (IsNull(array.GetValue(index)))
|
||
|
{
|
||
|
var value = type.IsSubclassOf(typeof(ScriptableObject))
|
||
|
? ScriptableObject.CreateInstance(type)
|
||
|
: Activator.CreateInstance(type);
|
||
|
|
||
|
array.SetValue(value, index);
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
static bool FixArrayIfNeeded(System.Object container, FieldInfo info, int length)
|
||
|
{
|
||
|
if (IsNull(container, info) || ((Array)info.GetValue(container)).Length < length)
|
||
|
{
|
||
|
info.SetValue(container, Activator.CreateInstance(info.FieldType, length));
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
static ReloadAttribute GetReloadAttribute(FieldInfo fieldInfo)
|
||
|
{
|
||
|
var attributes = (ReloadAttribute[])fieldInfo
|
||
|
.GetCustomAttributes(typeof(ReloadAttribute), false);
|
||
|
if (attributes.Length == 0)
|
||
|
return null;
|
||
|
return attributes[0];
|
||
|
}
|
||
|
|
||
|
static bool IsReloadGroup(FieldInfo info)
|
||
|
=> info.FieldType
|
||
|
.GetCustomAttributes(typeof(ReloadGroupAttribute), false).Length > 0;
|
||
|
|
||
|
static bool IsReloadGroup(Array field)
|
||
|
=> field.GetType().GetElementType()
|
||
|
.GetCustomAttributes(typeof(ReloadGroupAttribute), false).Length > 0;
|
||
|
|
||
|
static bool IsNull(System.Object container, FieldInfo info)
|
||
|
=> IsNull(info.GetValue(container));
|
||
|
|
||
|
static bool IsNull(System.Object field)
|
||
|
=> field == null || field.Equals(null);
|
||
|
|
||
|
static UnityEngine.Object Load(string path, Type type, ReloadAttribute.Package location)
|
||
|
{
|
||
|
// Check if asset exist.
|
||
|
// Direct loading can be prevented by AssetDatabase being reloading.
|
||
|
var guid = AssetDatabase.AssetPathToGUID(path);
|
||
|
if (location == ReloadAttribute.Package.Root && String.IsNullOrEmpty(guid))
|
||
|
throw new Exception($"Cannot load. Incorrect path: {path}");
|
||
|
|
||
|
// Else the path is good. Attempt loading resource if AssetDatabase available.
|
||
|
UnityEngine.Object result;
|
||
|
switch (location)
|
||
|
{
|
||
|
case ReloadAttribute.Package.Builtin:
|
||
|
if (type == typeof(Shader))
|
||
|
result = Shader.Find(path);
|
||
|
else
|
||
|
result = Resources.GetBuiltinResource(type, path); //handle wrong path error
|
||
|
break;
|
||
|
case ReloadAttribute.Package.BuiltinExtra:
|
||
|
if (type == typeof(Shader))
|
||
|
result = Shader.Find(path);
|
||
|
else
|
||
|
result = AssetDatabase.GetBuiltinExtraResource(type, path); //handle wrong path error
|
||
|
break;
|
||
|
case ReloadAttribute.Package.Root:
|
||
|
result = AssetDatabase.LoadAssetAtPath(path, type);
|
||
|
break;
|
||
|
default:
|
||
|
throw new NotImplementedException($"Unknown {location}");
|
||
|
}
|
||
|
|
||
|
if (IsNull(result))
|
||
|
{
|
||
|
throw new InvalidImportException($"Cannot load. Path {path} is correct but AssetDatabase cannot load now.");
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
static bool SetAndLoadIfNull(System.Object container, FieldInfo info,
|
||
|
string path, ReloadAttribute.Package location)
|
||
|
{
|
||
|
if (IsNull(container, info))
|
||
|
{
|
||
|
info.SetValue(container, Load(path, info.FieldType, location));
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
static bool SetAndLoadIfNull(Array array, int index, string path, ReloadAttribute.Package location)
|
||
|
{
|
||
|
var element = array.GetValue(index);
|
||
|
if (IsNull(element))
|
||
|
{
|
||
|
array.SetValue(Load(path, array.GetType().GetElementType(), location), index);
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
static string GetFullPath(string basePath, ReloadAttribute attribute, int index = 0)
|
||
|
{
|
||
|
string path;
|
||
|
switch (attribute.package)
|
||
|
{
|
||
|
case ReloadAttribute.Package.Builtin:
|
||
|
path = attribute.paths[index];
|
||
|
break;
|
||
|
case ReloadAttribute.Package.Root:
|
||
|
path = basePath + "/" + attribute.paths[index];
|
||
|
break;
|
||
|
default:
|
||
|
throw new ArgumentException("Unknown Package Path!");
|
||
|
}
|
||
|
return path;
|
||
|
}
|
||
|
|
||
|
// It's not perfect retrying right away but making it called in EditorApplication.delayCall
|
||
|
// from EnsureResources creates GC which we want to avoid
|
||
|
static void DelayedNullReload<T>(string resourcePath)
|
||
|
where T : RenderPipelineResources
|
||
|
{
|
||
|
T resourcesDelayed = AssetDatabase.LoadAssetAtPath<T>(resourcePath);
|
||
|
if (resourcesDelayed == null)
|
||
|
EditorApplication.delayCall += () => DelayedNullReload<T>(resourcePath);
|
||
|
else
|
||
|
ResourceReloader.ReloadAllNullIn(resourcesDelayed, resourcesDelayed.packagePath_Internal);
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Ensures that all resources in a container has been loaded
|
||
|
/// </summary>
|
||
|
/// <param name="forceReload">Set to true to force all resources to be reloaded even if they are loaded already</param>
|
||
|
/// <param name="resources">The resource container with the resulting loaded resources</param>
|
||
|
/// <param name="resourcePath">The asset path to load the resource container from</param>
|
||
|
/// <param name="checker">Function to test if the resource container is present in a RenderPipelineGlobalSettings</param>
|
||
|
/// <param name="settings">RenderPipelineGlobalSettings to be passed to checker to test of the resource container is already loaded</param>
|
||
|
public static void EnsureResources<T, S>(bool forceReload, ref T resources, string resourcePath, Func<S, bool> checker, S settings)
|
||
|
where T : RenderPipelineResources where S : RenderPipelineGlobalSettings
|
||
|
{
|
||
|
T resourceChecked = null;
|
||
|
|
||
|
if (checker(settings))
|
||
|
{
|
||
|
if (!EditorUtility.IsPersistent(resources)) // if not loaded from the Asset database
|
||
|
{
|
||
|
// try to load from AssetDatabase if it is ready
|
||
|
resourceChecked = AssetDatabase.LoadAssetAtPath<T>(resourcePath);
|
||
|
if (resourceChecked && !resourceChecked.Equals(null))
|
||
|
resources = resourceChecked;
|
||
|
}
|
||
|
|
||
|
if (forceReload)
|
||
|
ResourceReloader.ReloadAllNullIn(resources, resources.packagePath_Internal);
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
resourceChecked = AssetDatabase.LoadAssetAtPath<T>(resourcePath);
|
||
|
if (resourceChecked != null && !resourceChecked.Equals(null))
|
||
|
{
|
||
|
resources = resourceChecked;
|
||
|
if (forceReload)
|
||
|
ResourceReloader.ReloadAllNullIn(resources, resources.packagePath_Internal);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// Asset database may not be ready
|
||
|
var objs = InternalEditorUtility.LoadSerializedFileAndForget(resourcePath);
|
||
|
resources = (objs != null && objs.Length > 0) ? objs[0] as T : null;
|
||
|
if (forceReload)
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
if (ResourceReloader.ReloadAllNullIn(resources, resources.packagePath_Internal))
|
||
|
{
|
||
|
InternalEditorUtility.SaveToSerializedFileAndForget(
|
||
|
new Object[] { resources },
|
||
|
resourcePath,
|
||
|
true);
|
||
|
}
|
||
|
}
|
||
|
catch (System.Exception e)
|
||
|
{
|
||
|
// This can be called at a time where AssetDatabase is not available for loading.
|
||
|
// When this happens, the GUID can be get but the resource loaded will be null.
|
||
|
// Using the ResourceReloader mechanism in CoreRP, it checks this and add InvalidImport data when this occurs.
|
||
|
if (!(e.Data.Contains("InvalidImport") && e.Data["InvalidImport"] is int dii && dii == 1))
|
||
|
Debug.LogException(e);
|
||
|
else
|
||
|
DelayedNullReload<T>(resourcePath);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
Debug.Assert(checker(settings), $"Could not load {typeof(T).Name}.");
|
||
|
}
|
||
|
}
|
||
|
#endif
|
||
|
}
|