No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ResourceReloader.cs 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. using System;
  2. using System.IO;
  3. using UnityEngine.Assertions;
  4. #if UNITY_EDITOR
  5. using UnityEditor;
  6. using UnityEditorInternal;
  7. using System.Reflection;
  8. #endif
  9. namespace UnityEngine.Rendering
  10. {
  11. #if UNITY_EDITOR
  12. /// <summary>
  13. /// The resources that need to be reloaded in Editor can live in Runtime.
  14. /// The reload call should only be done in Editor context though but it
  15. /// could be called from runtime entities.
  16. /// </summary>
  17. public static class ResourceReloader
  18. {
  19. /// <summary>
  20. /// Looks for resources in the given <paramref name="container"/> object and reload the ones
  21. /// that are missing or broken.
  22. /// This version will still return null value without throwing error if the issue is due to
  23. /// AssetDatabase being not ready. But in this case the assetDatabaseNotReady result will be true.
  24. /// </summary>
  25. /// <param name="container">The object containing reload-able resources</param>
  26. /// <param name="basePath">The base path for the package</param>
  27. /// <returns>
  28. /// - 1 hasChange: True if something have been reloaded.
  29. /// - 2 assetDatabaseNotReady: True if the issue preventing loading is due to state of AssetDatabase
  30. /// </returns>
  31. public static (bool hasChange, bool assetDatabaseNotReady) TryReloadAllNullIn(System.Object container, string basePath)
  32. {
  33. try
  34. {
  35. return (ReloadAllNullIn(container, basePath), false);
  36. }
  37. catch (InvalidImportException)
  38. {
  39. return (false, true);
  40. }
  41. catch (Exception e)
  42. {
  43. throw e;
  44. }
  45. }
  46. /// <summary>
  47. /// Looks for resources in the given <paramref name="container"/> object and reload the ones
  48. /// that are missing or broken.
  49. /// </summary>
  50. /// <param name="container">The object containing reload-able resources</param>
  51. /// <param name="basePath">The base path for the package</param>
  52. /// <returns>True if something have been reloaded.</returns>
  53. public static bool ReloadAllNullIn(System.Object container, string basePath)
  54. {
  55. if (IsNull(container))
  56. return false;
  57. var changed = false;
  58. foreach (var fieldInfo in container.GetType()
  59. .GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static))
  60. {
  61. //Recurse on sub-containers
  62. if (IsReloadGroup(fieldInfo))
  63. {
  64. changed |= FixGroupIfNeeded(container, fieldInfo);
  65. changed |= ReloadAllNullIn(fieldInfo.GetValue(container), basePath);
  66. }
  67. //Find null field and reload them
  68. var attribute = GetReloadAttribute(fieldInfo);
  69. if (attribute != null)
  70. {
  71. if (attribute.paths.Length == 1)
  72. {
  73. changed |= SetAndLoadIfNull(container, fieldInfo, GetFullPath(basePath, attribute),
  74. attribute.package);
  75. }
  76. else if (attribute.paths.Length > 1)
  77. {
  78. changed |= FixArrayIfNeeded(container, fieldInfo, attribute.paths.Length);
  79. var array = (Array)fieldInfo.GetValue(container);
  80. if (IsReloadGroup(array))
  81. {
  82. //Recurse on each sub-containers
  83. for (int index = 0; index < attribute.paths.Length; ++index)
  84. {
  85. changed |= FixGroupIfNeeded(array, index);
  86. changed |= ReloadAllNullIn(array.GetValue(index), basePath);
  87. }
  88. }
  89. else
  90. {
  91. //Find each null element and reload them
  92. for (int index = 0; index < attribute.paths.Length; ++index)
  93. changed |= SetAndLoadIfNull(array, index, GetFullPath(basePath, attribute, index),
  94. attribute.package);
  95. }
  96. }
  97. }
  98. }
  99. if (changed && container is UnityEngine.Object c)
  100. EditorUtility.SetDirty(c);
  101. return changed;
  102. }
  103. static void CheckReloadGroupSupportedType(Type type)
  104. {
  105. if (type.IsSubclassOf(typeof(ScriptableObject)))
  106. throw new Exception(@$"ReloadGroup attribute must not be used on {nameof(ScriptableObject)}.
  107. If {nameof(ResourceReloader)} create an instance of it, it will be not saved as a file, resulting in corrupted ID when building.");
  108. }
  109. static bool FixGroupIfNeeded(System.Object container, FieldInfo info)
  110. {
  111. var type = info.FieldType;
  112. CheckReloadGroupSupportedType(type);
  113. if (IsNull(container, info))
  114. {
  115. var value = Activator.CreateInstance(type);
  116. info.SetValue(
  117. container,
  118. value
  119. );
  120. return true;
  121. }
  122. return false;
  123. }
  124. static bool FixGroupIfNeeded(Array array, int index)
  125. {
  126. Assert.IsNotNull(array);
  127. var type = array.GetType().GetElementType();
  128. CheckReloadGroupSupportedType(type);
  129. if (IsNull(array.GetValue(index)))
  130. {
  131. var value = type.IsSubclassOf(typeof(ScriptableObject))
  132. ? ScriptableObject.CreateInstance(type)
  133. : Activator.CreateInstance(type);
  134. array.SetValue(value, index);
  135. return true;
  136. }
  137. return false;
  138. }
  139. static bool FixArrayIfNeeded(System.Object container, FieldInfo info, int length)
  140. {
  141. if (IsNull(container, info) || ((Array)info.GetValue(container)).Length < length)
  142. {
  143. info.SetValue(container, Activator.CreateInstance(info.FieldType, length));
  144. return true;
  145. }
  146. return false;
  147. }
  148. static ReloadAttribute GetReloadAttribute(FieldInfo fieldInfo)
  149. {
  150. var attributes = (ReloadAttribute[])fieldInfo
  151. .GetCustomAttributes(typeof(ReloadAttribute), false);
  152. if (attributes.Length == 0)
  153. return null;
  154. return attributes[0];
  155. }
  156. static bool IsReloadGroup(FieldInfo info)
  157. => info.FieldType
  158. .GetCustomAttributes(typeof(ReloadGroupAttribute), false).Length > 0;
  159. static bool IsReloadGroup(Array field)
  160. => field.GetType().GetElementType()
  161. .GetCustomAttributes(typeof(ReloadGroupAttribute), false).Length > 0;
  162. static bool IsNull(System.Object container, FieldInfo info)
  163. => IsNull(info.GetValue(container));
  164. static bool IsNull(System.Object field)
  165. => field == null || field.Equals(null);
  166. static UnityEngine.Object Load(string path, Type type, ReloadAttribute.Package location)
  167. {
  168. // Check if asset exist.
  169. // Direct loading can be prevented by AssetDatabase being reloading.
  170. var guid = AssetDatabase.AssetPathToGUID(path);
  171. if (location == ReloadAttribute.Package.Root && String.IsNullOrEmpty(guid))
  172. throw new Exception($"Cannot load. Incorrect path: {path}");
  173. // Else the path is good. Attempt loading resource if AssetDatabase available.
  174. UnityEngine.Object result;
  175. switch (location)
  176. {
  177. case ReloadAttribute.Package.Builtin:
  178. if (type == typeof(Shader))
  179. result = Shader.Find(path);
  180. else
  181. result = Resources.GetBuiltinResource(type, path); //handle wrong path error
  182. break;
  183. case ReloadAttribute.Package.BuiltinExtra:
  184. if (type == typeof(Shader))
  185. result = Shader.Find(path);
  186. else
  187. result = AssetDatabase.GetBuiltinExtraResource(type, path); //handle wrong path error
  188. break;
  189. case ReloadAttribute.Package.Root:
  190. result = AssetDatabase.LoadAssetAtPath(path, type);
  191. break;
  192. default:
  193. throw new NotImplementedException($"Unknown {location}");
  194. }
  195. if (IsNull(result))
  196. {
  197. throw new InvalidImportException($"Cannot load. Path {path} is correct but AssetDatabase cannot load now.");
  198. }
  199. return result;
  200. }
  201. static bool SetAndLoadIfNull(System.Object container, FieldInfo info,
  202. string path, ReloadAttribute.Package location)
  203. {
  204. if (IsNull(container, info))
  205. {
  206. info.SetValue(container, Load(path, info.FieldType, location));
  207. return true;
  208. }
  209. return false;
  210. }
  211. static bool SetAndLoadIfNull(Array array, int index, string path, ReloadAttribute.Package location)
  212. {
  213. var element = array.GetValue(index);
  214. if (IsNull(element))
  215. {
  216. array.SetValue(Load(path, array.GetType().GetElementType(), location), index);
  217. return true;
  218. }
  219. return false;
  220. }
  221. static string GetFullPath(string basePath, ReloadAttribute attribute, int index = 0)
  222. {
  223. string path;
  224. switch (attribute.package)
  225. {
  226. case ReloadAttribute.Package.Builtin:
  227. path = attribute.paths[index];
  228. break;
  229. case ReloadAttribute.Package.Root:
  230. path = basePath + "/" + attribute.paths[index];
  231. break;
  232. default:
  233. throw new ArgumentException("Unknown Package Path!");
  234. }
  235. return path;
  236. }
  237. // It's not perfect retrying right away but making it called in EditorApplication.delayCall
  238. // from EnsureResources creates GC which we want to avoid
  239. static void DelayedNullReload<T>(string resourcePath)
  240. where T : RenderPipelineResources
  241. {
  242. T resourcesDelayed = AssetDatabase.LoadAssetAtPath<T>(resourcePath);
  243. if (resourcesDelayed == null)
  244. EditorApplication.delayCall += () => DelayedNullReload<T>(resourcePath);
  245. else
  246. ResourceReloader.ReloadAllNullIn(resourcesDelayed, resourcesDelayed.packagePath_Internal);
  247. }
  248. /// <summary>
  249. /// Ensures that all resources in a container has been loaded
  250. /// </summary>
  251. /// <param name="forceReload">Set to true to force all resources to be reloaded even if they are loaded already</param>
  252. /// <param name="resources">The resource container with the resulting loaded resources</param>
  253. /// <param name="resourcePath">The asset path to load the resource container from</param>
  254. /// <param name="checker">Function to test if the resource container is present in a RenderPipelineGlobalSettings</param>
  255. /// <param name="settings">RenderPipelineGlobalSettings to be passed to checker to test of the resource container is already loaded</param>
  256. public static void EnsureResources<T, S>(bool forceReload, ref T resources, string resourcePath, Func<S, bool> checker, S settings)
  257. where T : RenderPipelineResources where S : RenderPipelineGlobalSettings
  258. {
  259. T resourceChecked = null;
  260. if (checker(settings))
  261. {
  262. if (!EditorUtility.IsPersistent(resources)) // if not loaded from the Asset database
  263. {
  264. // try to load from AssetDatabase if it is ready
  265. resourceChecked = AssetDatabase.LoadAssetAtPath<T>(resourcePath);
  266. if (resourceChecked && !resourceChecked.Equals(null))
  267. resources = resourceChecked;
  268. }
  269. if (forceReload)
  270. ResourceReloader.ReloadAllNullIn(resources, resources.packagePath_Internal);
  271. return;
  272. }
  273. resourceChecked = AssetDatabase.LoadAssetAtPath<T>(resourcePath);
  274. if (resourceChecked != null && !resourceChecked.Equals(null))
  275. {
  276. resources = resourceChecked;
  277. if (forceReload)
  278. ResourceReloader.ReloadAllNullIn(resources, resources.packagePath_Internal);
  279. }
  280. else
  281. {
  282. // Asset database may not be ready
  283. var objs = InternalEditorUtility.LoadSerializedFileAndForget(resourcePath);
  284. resources = (objs != null && objs.Length > 0) ? objs[0] as T : null;
  285. if (forceReload)
  286. {
  287. try
  288. {
  289. if (ResourceReloader.ReloadAllNullIn(resources, resources.packagePath_Internal))
  290. {
  291. InternalEditorUtility.SaveToSerializedFileAndForget(
  292. new Object[] { resources },
  293. resourcePath,
  294. true);
  295. }
  296. }
  297. catch (System.Exception e)
  298. {
  299. // This can be called at a time where AssetDatabase is not available for loading.
  300. // When this happens, the GUID can be get but the resource loaded will be null.
  301. // Using the ResourceReloader mechanism in CoreRP, it checks this and add InvalidImport data when this occurs.
  302. if (!(e.Data.Contains("InvalidImport") && e.Data["InvalidImport"] is int dii && dii == 1))
  303. Debug.LogException(e);
  304. else
  305. DelayedNullReload<T>(resourcePath);
  306. }
  307. }
  308. }
  309. Debug.Assert(checker(settings), $"Could not load {typeof(T).Name}.");
  310. }
  311. }
  312. #endif
  313. }