From e70e43bbf3086e4e3305ab9a313a0ed969a341e1 Mon Sep 17 00:00:00 2001 From: bd_ Date: Fri, 20 Sep 2024 20:32:37 -0700 Subject: [PATCH] fix: serialize assets after the initial plugin hook (#408) * prof: profile BuildContext.Serialize * perf: improve BuildContext.Serialize performance * fix: serialize assets after the initial plugin hook This ensures that we don't have any references from assets to non-assets when passing control to non-NDMF hooks such as VRCF. --- CHANGELOG.md | 2 + Editor/API/BuildContext.cs | 113 +++++++------ Editor/API/Util/VisitAssets.cs | 148 ++++++++++++++++-- Editor/VRChat/BuildFrameworkPreprocessHook.cs | 3 + 4 files changed, 205 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95d2bd2..5433d1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added ### Fixed +- [#408] Improved performance of `BuildContext.Serialize` ### Changed +- [#408] Unserialized assets will be serialized after the Transforming phase completes (before e.g. VRCFury runs) ### Removed diff --git a/Editor/API/BuildContext.cs b/Editor/API/BuildContext.cs index 093de60..5e53154 100644 --- a/Editor/API/BuildContext.cs +++ b/Editor/API/BuildContext.cs @@ -213,73 +213,88 @@ public void Serialize() return; // unit tests with no serialized assets } - HashSet _savedObjects = - new HashSet(AssetDatabase.LoadAllAssetsAtPath(AssetDatabase.GetAssetPath(AssetContainer))); + Profiler.BeginSample("BuildContext.Serialize"); - _savedObjects.Remove(AssetContainer); - - int index = 0; - foreach (var asset in _avatarRootObject.ReferencedAssets(traverseSaved: true, includeScene: false)) + try { - if (asset is MonoScript) - { - // MonoScripts aren't considered to be a Main or Sub-asset, but they can't be added to asset - // containers either. - continue; - } + AssetDatabase.StartAssetEditing(); - if (_savedObjects.Contains(asset)) - { - _savedObjects.Remove(asset); - continue; - } + HashSet _savedObjects = + new HashSet( + AssetDatabase.LoadAllAssetsAtPath(AssetDatabase.GetAssetPath(AssetContainer))); - if (asset == null) + _savedObjects.Remove(AssetContainer); + + int index = 0; + foreach (var asset in _avatarRootObject.ReferencedAssets(traverseSaved: true, includeScene: false)) { - Debug.Log($"Asset {index} is null"); - } + if (asset is MonoScript) + { + // MonoScripts aren't considered to be a Main or Sub-asset, but they can't be added to asset + // containers either. + continue; + } - index++; + if (_savedObjects.Contains(asset)) + { + _savedObjects.Remove(asset); + continue; + } - if (!EditorUtility.IsPersistent(asset)) - { - try + if (asset == null) { - AssetDatabase.AddObjectToAsset(asset, AssetContainer); + Debug.Log($"Asset {index} is null"); } - catch (UnityException ex) + + index++; + + if (!EditorUtility.IsPersistent(asset)) { - Debug.Log( - $"Error adding asset {asset} p={AssetDatabase.GetAssetOrScenePath(asset)} isMain={AssetDatabase.IsMainAsset(asset)} " + - $"isSub={AssetDatabase.IsSubAsset(asset)} isForeign={AssetDatabase.IsForeignAsset(asset)} isNative={AssetDatabase.IsNativeAsset(asset)}"); - throw ex; + try + { + AssetDatabase.AddObjectToAsset(asset, AssetContainer); + } + catch (UnityException ex) + { + Debug.Log( + $"Error adding asset {asset} p={AssetDatabase.GetAssetOrScenePath(asset)} isMain={AssetDatabase.IsMainAsset(asset)} " + + $"isSub={AssetDatabase.IsSubAsset(asset)} isForeign={AssetDatabase.IsForeignAsset(asset)} isNative={AssetDatabase.IsNativeAsset(asset)}"); + throw ex; + } } } - } - // SaveAssets to make sub-assets visible on the Project window - AssetDatabase.SaveAssets(); - - foreach (var assetToHide in AssetDatabase.LoadAllAssetsAtPath(AssetDatabase.GetAssetPath(AssetContainer))) - { - if (assetToHide != AssetContainer && - GeneratedAssetBundleExtractor.IsAssetTypeHidden(assetToHide.GetType())) + foreach (var assetToHide in AssetDatabase.LoadAllAssetsAtPath( + AssetDatabase.GetAssetPath(AssetContainer))) { - assetToHide.hideFlags = HideFlags.HideInHierarchy; + if (assetToHide != AssetContainer && + GeneratedAssetBundleExtractor.IsAssetTypeHidden(assetToHide.GetType())) + { + assetToHide.hideFlags = HideFlags.HideInHierarchy; + } } - } - - // Remove obsolete temporary assets - foreach (var asset in _savedObjects) - { - if (!(asset is Component || asset is GameObject)) + + // Remove obsolete temporary assets + foreach (var asset in _savedObjects) { - // Traversal can't currently handle prefabs, so this must have been manually added. Avoid purging it. - continue; - } + if (!(asset is Component || asset is GameObject)) + { + // Traversal can't currently handle prefabs, so this must have been manually added. Avoid purging it. + continue; + } - UnityEngine.Object.DestroyImmediate(asset); + UnityEngine.Object.DestroyImmediate(asset); + } + } + finally + { + AssetDatabase.StopAssetEditing(); + + // SaveAssets to make sub-assets visible on the Project window + AssetDatabase.SaveAssets(); } + + Profiler.EndSample(); } public void DeactivateExtensionContext() where T : IExtensionContext diff --git a/Editor/API/Util/VisitAssets.cs b/Editor/API/Util/VisitAssets.cs index 6589d07..f6ea1d4 100644 --- a/Editor/API/Util/VisitAssets.cs +++ b/Editor/API/Util/VisitAssets.cs @@ -1,8 +1,12 @@ #region +using System; using System.Collections.Generic; using UnityEditor; +using UnityEditor.Animations; using UnityEngine; +using UnityEngine.Profiling; +using Object = UnityEngine.Object; #endregion @@ -92,22 +96,142 @@ public static IEnumerable ReferencedAssets( continue; } - foreach (var prop in new SerializedObject(next).ObjectProperties()) + + if (!SamplerCache.TryGetValue(next.GetType(), out var sampler)) { - var value = prop.objectReferenceValue; - if (value == null) continue; + sampler = CustomSampler.Create("ObjectReferences." + next.GetType()); + SamplerCache[next.GetType()] = sampler; + } + + sampler.Begin(next); + foreach (var referenced in ObjectReferences(next)) + { + MaybeEnqueueObject(referenced); + } + sampler.End(); + } + + void MaybeEnqueueObject(Object value) + { + if (value == null) return; + + var objIsScene = value is GameObject || value is Component; + + if (!objIsScene + && (traverseSaved || !EditorUtility.IsPersistent(value)) + && visited.Add(value) + && traversalFilter(value) + ) + { + queue.Enqueue((index++, value)); + } + } + } + + private static Dictionary SamplerCache = new(); + + private static IEnumerable ObjectReferences(Object obj) + { + if (obj == null) yield break; - var objIsScene = value is GameObject || value is Component; + // We have special cases here for a bunch of popular asset types, because SerializedObject traversal is slow. + // For unrecognized stuff (e.g. VRChat MonoBehaviors) we fall back to SerializedObject. + switch (obj) + { + case Mesh: + case Shader: + case Avatar: // Humanoid avatar descriptors have no subassets + break; + case AnimationClip clip: + { + var pptrCurves = AnimationUtility.GetObjectReferenceCurveBindings(clip); + foreach (var curve in pptrCurves) + { + var frames = AnimationUtility.GetObjectReferenceCurve(clip, curve); + foreach (var frame in frames) + { + yield return frame.value; + } + } - if (value != null - && !objIsScene - && (traverseSaved || !EditorUtility.IsPersistent(value)) - && visited.Add(value) - && traversalFilter(value) - ) + break; + } + case BlendTree tree: + { + foreach (var child in tree.children) + { + yield return child.motion; + } + + break; + } + case AnimatorState state: + { + yield return state.motion; + foreach (var b in state.behaviours ?? Array.Empty()) { - queue.Enqueue((index++, value)); + yield return b; } + + foreach (var t in state.transitions ?? Array.Empty()) + { + yield return t; + } + + break; + } + case Material m: + { + /* This approach actually seems slower than using SerializedProperty... + var ids = m.GetTexturePropertyNameIDs(); + foreach (var id in ids) + { + yield return m.GetTexture(id); + }*/ + + // But we can be more efficient and only look at texture props + var so = new SerializedObject(m); + var texEnvs = so.FindProperty("m_SavedProperties") + ?.FindPropertyRelative("m_TexEnvs"); + + if (texEnvs == null || !texEnvs.isArray) + { + break; + } + + var size = texEnvs.arraySize; + + for (var i = 0; i < size; ++i) + { + var texEnv = texEnvs.GetArrayElementAtIndex(i).FindPropertyRelative("second"); + var texture = texEnv.FindPropertyRelative("m_Texture"); + + yield return texture.objectReferenceValue; + } + + break; + } + case AnimatorStateTransition t: + { + yield return t.destinationState; + yield return t.destinationStateMachine; + break; + } + default: + { + if (obj is ParticleSystem ps) + { + Debug.Log("ps"); + } + + foreach (var prop in new SerializedObject(obj).ObjectProperties()) + { + var value = prop.objectReferenceValue; + + yield return value; + } + + break; } } } @@ -152,7 +276,7 @@ public static IEnumerable AllProperties(this SerializedObjec enterChildren = false; } - if (prop.propertyType == SerializedPropertyType.String) + if (prop.propertyType == SerializedPropertyType.String || prop.propertyType == SerializedPropertyType.AnimationCurve) { enterChildren = false; } diff --git a/Editor/VRChat/BuildFrameworkPreprocessHook.cs b/Editor/VRChat/BuildFrameworkPreprocessHook.cs index 8b6268f..576edce 100644 --- a/Editor/VRChat/BuildFrameworkPreprocessHook.cs +++ b/Editor/VRChat/BuildFrameworkPreprocessHook.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; using nadena.dev.ndmf.runtime; +using UnityEditor; using UnityEngine; using VRC.SDKBase.Editor.BuildPipeline; using Debug = UnityEngine.Debug; @@ -41,6 +42,8 @@ public bool OnPreprocessAvatar(GameObject avatarGameObject) holder.context = new BuildContext(avatarGameObject, AvatarProcessor.TemporaryAssetRoot); AvatarProcessor.ProcessAvatar(holder.context, BuildPhase.Resolving, BuildPhase.Transforming); + holder.context.Serialize(); + return true; } catch (Exception e)