fix: improve build performance by ~10x

CreateAsset, as it turns out, can be extremely slow, particularly when used on Mesh objects.
By adding our generated objects as sub-objects of a container AnimatorController, we can
minimize this overhead.
This commit is contained in:
bd_ 2023-01-05 21:30:01 +09:00
parent b13f60e80f
commit 250e8be54c
10 changed files with 133 additions and 100 deletions

View File

@ -52,9 +52,9 @@ namespace nadena.dev.modular_avatar.core.editor
private int controllerBaseLayer = 0;
public AnimatorCombiner()
public AnimatorCombiner(BuildContext context)
{
_combined = Util.CreateAnimator();
_combined = context.CreateAnimator();
}
public AnimatorController Finish()

View File

@ -156,16 +156,16 @@ namespace nadena.dev.modular_avatar.core.editor
var context = new BuildContext(vrcAvatarDescriptor);
new RenameParametersHook().OnPreprocessAvatar(avatarGameObject);
new MergeAnimatorProcessor().OnPreprocessAvatar(avatarGameObject);
new RenameParametersHook().OnPreprocessAvatar(avatarGameObject, context);
new MergeAnimatorProcessor().OnPreprocessAvatar(avatarGameObject, context);
context.AnimationDatabase.Bootstrap(vrcAvatarDescriptor);
new MenuInstallHook().OnPreprocessAvatar(avatarGameObject);
new MenuInstallHook().OnPreprocessAvatar(avatarGameObject, context);
new MergeArmatureHook().OnPreprocessAvatar(context, avatarGameObject);
new BoneProxyProcessor().OnPreprocessAvatar(avatarGameObject);
new VisibleHeadAccessoryProcessor(vrcAvatarDescriptor).Process();
new RemapAnimationPass(vrcAvatarDescriptor).Process(context.AnimationDatabase);
new BlendshapeSyncAnimationProcessor().OnPreprocessAvatar(avatarGameObject, context.AnimationDatabase);
new BlendshapeSyncAnimationProcessor().OnPreprocessAvatar(avatarGameObject, context);
PhysboneBlockerPass.Process(avatarGameObject);
context.AnimationDatabase.Commit();

View File

@ -16,7 +16,7 @@ namespace nadena.dev.modular_avatar.core.editor
*/
internal class BlendshapeSyncAnimationProcessor
{
private Object _container;
private BuildContext _context;
private Dictionary<Motion, Motion> _motionCache;
private Dictionary<SummaryBinding, List<SummaryBinding>> _bindingMappings;
@ -43,8 +43,11 @@ namespace nadena.dev.modular_avatar.core.editor
}
}
public void OnPreprocessAvatar(GameObject avatar, AnimationDatabase animDb)
public void OnPreprocessAvatar(GameObject avatar, BuildContext context)
{
_context = context;
var animDb = _context.AnimationDatabase;
var avatarDescriptor = avatar.GetComponent<VRCAvatarDescriptor>();
_bindingMappings = new Dictionary<SummaryBinding, List<SummaryBinding>>();
_motionCache = new Dictionary<Motion, Motion>();
@ -145,15 +148,7 @@ namespace nadena.dev.modular_avatar.core.editor
{
var newTree = new BlendTree();
EditorUtility.CopySerialized(tree, newTree);
if (_container == null)
{
_container = newTree;
AssetDatabase.CreateAsset(_container, Util.GenerateAssetPath());
}
else
{
AssetDatabase.AddObjectToAsset(newTree, _container);
}
_context.SaveAsset(newTree);
newTree.children = children;
motion = newTree;

View File

@ -1,4 +1,9 @@
using VRC.SDK3.Avatars.Components;
using System;
using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
using Object = UnityEngine.Object;
namespace nadena.dev.modular_avatar.core.editor
{
@ -6,10 +11,66 @@ namespace nadena.dev.modular_avatar.core.editor
{
internal readonly VRCAvatarDescriptor AvatarDescriptor;
internal readonly AnimationDatabase AnimationDatabase = new AnimationDatabase();
internal readonly AnimatorController AssetContainer;
public BuildContext(VRCAvatarDescriptor avatarDescriptor)
{
AvatarDescriptor = avatarDescriptor;
// AssetDatabase.CreateAsset is super slow - so only do it once, and add everything else as sub-assets.
// This animator controller exists for the sole purpose of providing a placeholder to dump everything we
// generate into.
AssetContainer = new AnimatorController();
AssetDatabase.CreateAsset(AssetContainer, Util.GenerateAssetPath());
}
public void SaveAsset(Object obj)
{
if (AssetDatabase.IsMainAsset(obj) || AssetDatabase.IsSubAsset(obj)) return;
AssetDatabase.AddObjectToAsset(obj, AssetContainer);
}
public AnimatorController CreateAnimator(AnimatorController toClone = null)
{
AnimatorController controller;
if (toClone != null)
{
controller = Object.Instantiate(toClone);
}
else
{
controller = new AnimatorController();
}
SaveAsset(controller);
return controller;
}
public AnimatorController DeepCloneAnimator(RuntimeAnimatorController controller)
{
var merger = new AnimatorCombiner(this);
switch (controller)
{
case AnimatorController ac:
merger.AddController("", ac, null);
break;
case AnimatorOverrideController oac:
merger.AddOverrideController("", oac, null);
break;
default:
throw new Exception("Unknown RuntimeAnimatorContoller type " + controller.GetType());
}
return merger.Finish();
}
public AnimatorController ConvertAnimatorController(AnimatorOverrideController overrideController)
{
var merger = new AnimatorCombiner(this);
merger.AddOverrideController("", overrideController, null);
return merger.Finish();
}
}
}

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using UnityEditor;
using UnityEngine;
using VRC.SDK3.Avatars.Components;
@ -16,31 +17,36 @@ namespace nadena.dev.modular_avatar.core.editor
"Packages/nadena.dev.modular-avatar/Runtime/Icons/Icon_More_A.png"
);
private BuildContext _context;
private Dictionary<VRCExpressionsMenu, VRCExpressionsMenu> _clonedMenus;
private VRCExpressionsMenu _rootMenu;
private MenuTree _menuTree;
private Stack<ModularAvatarMenuInstaller> _visitedInstallerStack;
public void OnPreprocessAvatar(GameObject avatarRoot)
public void OnPreprocessAvatar(GameObject avatarRoot, BuildContext context)
{
ModularAvatarMenuInstaller[] menuInstallers = avatarRoot.GetComponentsInChildren<ModularAvatarMenuInstaller>(true)
_context = context;
ModularAvatarMenuInstaller[] menuInstallers = avatarRoot
.GetComponentsInChildren<ModularAvatarMenuInstaller>(true)
.Where(menuInstaller => menuInstaller.enabled)
.ToArray();
if (menuInstallers.Length == 0) return;
_clonedMenus = new Dictionary<VRCExpressionsMenu, VRCExpressionsMenu>();
_visitedInstallerStack = new Stack<ModularAvatarMenuInstaller>();
VRCAvatarDescriptor avatar = avatarRoot.GetComponent<VRCAvatarDescriptor>();
if (avatar.expressionsMenu == null)
if (avatar.expressionsMenu == null)
{
var menu = ScriptableObject.CreateInstance<VRCExpressionsMenu>();
AssetDatabase.CreateAsset(menu, Util.GenerateAssetPath());
_context.SaveAsset(menu);
avatar.expressionsMenu = menu;
_clonedMenus[menu] = menu;
}
@ -48,20 +54,20 @@ namespace nadena.dev.modular_avatar.core.editor
_rootMenu = avatar.expressionsMenu;
_menuTree = new MenuTree(avatar);
_menuTree.TraverseAvatarMenu();
avatar.expressionsMenu = CloneMenu(avatar.expressionsMenu);
foreach (ModularAvatarMenuInstaller installer in menuInstallers)
foreach (ModularAvatarMenuInstaller installer in menuInstallers)
{
_menuTree.TraverseMenuInstaller(installer);
}
foreach (MenuTree.ChildElement childElement in _menuTree.GetChildInstallers(null))
foreach (MenuTree.ChildElement childElement in _menuTree.GetChildInstallers(null))
{
InstallMenu(childElement.installer);
}
}
private void InstallMenu(ModularAvatarMenuInstaller installer, VRCExpressionsMenu installTarget = null)
{
if (!installer.enabled) return;
@ -71,22 +77,22 @@ namespace nadena.dev.modular_avatar.core.editor
installer.installTargetMenu = _rootMenu;
}
if (installTarget == null)
if (installTarget == null)
{
installTarget = installer.installTargetMenu;
}
if (installer.installTargetMenu == null || installer.menuToAppend == null) return;
if (!_clonedMenus.TryGetValue(installTarget, out var targetMenu)) return;
// Clone before appending to sanitize menu icons
targetMenu.controls.AddRange(CloneMenu(installer.menuToAppend).controls);
SplitMenu(installer, targetMenu);
if (_visitedInstallerStack.Contains(installer)) return;
_visitedInstallerStack.Push(installer);
foreach (MenuTree.ChildElement childElement in _menuTree.GetChildInstallers(installer))
foreach (MenuTree.ChildElement childElement in _menuTree.GetChildInstallers(installer))
{
InstallMenu(childElement.installer, childElement.parent);
}
@ -94,13 +100,13 @@ namespace nadena.dev.modular_avatar.core.editor
_visitedInstallerStack.Pop();
}
private void SplitMenu(ModularAvatarMenuInstaller installer, VRCExpressionsMenu targetMenu)
private void SplitMenu(ModularAvatarMenuInstaller installer, VRCExpressionsMenu targetMenu)
{
while (targetMenu.controls.Count > VRCExpressionsMenu.MAX_CONTROLS)
while (targetMenu.controls.Count > VRCExpressionsMenu.MAX_CONTROLS)
{
// Split target menu
var newMenu = ScriptableObject.CreateInstance<VRCExpressionsMenu>();
AssetDatabase.CreateAsset(newMenu, Util.GenerateAssetPath());
_context.SaveAsset(newMenu);
const int keepCount = VRCExpressionsMenu.MAX_CONTROLS - 1;
newMenu.controls.AddRange(targetMenu.controls.Skip(keepCount));
targetMenu.controls.RemoveRange(keepCount,
@ -125,20 +131,20 @@ namespace nadena.dev.modular_avatar.core.editor
targetMenu = newMenu;
}
}
private VRCExpressionsMenu CloneMenu(VRCExpressionsMenu menu)
{
if (menu == null) return null;
if (_clonedMenus.TryGetValue(menu, out var newMenu)) return newMenu;
newMenu = Object.Instantiate(menu);
AssetDatabase.CreateAsset(newMenu, Util.GenerateAssetPath());
_context.SaveAsset(newMenu);
_clonedMenus[menu] = newMenu;
foreach (var control in newMenu.controls)
{
if (Util.ValidateExpressionMenuIcon(control.icon) != Util.ValidateExpressionMenuIconResult.Success)
control.icon = null;
for (int i = 0; i < control.labels.Length; i++)
{
var label = control.labels[i];
@ -149,7 +155,7 @@ namespace nadena.dev.modular_avatar.core.editor
control.labels[i] = label;
}
}
if (control.type == VRCExpressionsMenu.Control.ControlType.SubMenu)
{
control.subMenu = CloneMenu(control.subMenu);

View File

@ -39,6 +39,8 @@ namespace nadena.dev.modular_avatar.core.editor
private const string SAMPLE_PATH_LEGACY = "Assets/VRCSDK/Examples3/Animation/Controllers";
private BuildContext _context;
private Dictionary<VRCAvatarDescriptor.AnimLayerType, AnimatorController> defaultControllers_ =
new Dictionary<VRCAvatarDescriptor.AnimLayerType, AnimatorController>();
@ -48,8 +50,10 @@ namespace nadena.dev.modular_avatar.core.editor
Dictionary<VRCAvatarDescriptor.AnimLayerType, AnimatorCombiner> mergeSessions =
new Dictionary<VRCAvatarDescriptor.AnimLayerType, AnimatorCombiner>();
internal void OnPreprocessAvatar(GameObject avatarGameObject)
internal void OnPreprocessAvatar(GameObject avatarGameObject, BuildContext context)
{
_context = context;
defaultControllers_.Clear();
mergeSessions.Clear();
@ -77,7 +81,7 @@ namespace nadena.dev.modular_avatar.core.editor
if (!mergeSessions.TryGetValue(merge.layerType, out var session))
{
session = new AnimatorCombiner();
session = new AnimatorCombiner(context);
mergeSessions[merge.layerType] = session;
if (defaultControllers_.ContainsKey(merge.layerType))
{
@ -131,7 +135,7 @@ namespace nadena.dev.modular_avatar.core.editor
{
// For non-default layers, ensure we always clone the controller for the benefit of subsequent
// processing phases
mergeSessions[layer.type] = new AnimatorCombiner();
mergeSessions[layer.type] = new AnimatorCombiner(_context);
mergeSessions[layer.type].AddController("", controller, null);
}
}

View File

@ -80,7 +80,7 @@ namespace nadena.dev.modular_avatar.core.editor
RetainBoneReferences(c as Component);
}
new RetargetMeshes().OnPreprocessAvatar(avatarGameObject);
new RetargetMeshes().OnPreprocessAvatar(avatarGameObject, context);
}
private void RetainBoneReferences(Component c)

View File

@ -77,8 +77,12 @@ namespace nadena.dev.modular_avatar.core.editor
internal class RetargetMeshes
{
internal void OnPreprocessAvatar(GameObject avatarGameObject)
private BuildContext _context;
internal void OnPreprocessAvatar(GameObject avatarGameObject, BuildContext context)
{
_context = context;
foreach (var renderer in avatarGameObject.GetComponentsInChildren<SkinnedMeshRenderer>(true))
{
bool isRetargetable = false;
@ -93,7 +97,8 @@ namespace nadena.dev.modular_avatar.core.editor
if (isRetargetable)
{
new MeshRetargeter(renderer).Retarget();
var newMesh = new MeshRetargeter(renderer).Retarget();
_context.SaveAsset(newMesh);
}
}
@ -139,7 +144,7 @@ namespace nadena.dev.modular_avatar.core.editor
this.renderer = renderer;
}
public void Retarget()
public Mesh Retarget()
{
var avatar = RuntimeUtil.FindAvatarInParents(renderer.transform);
if (avatar == null) throw new System.Exception("Could not find avatar in parents of " + renderer.name);
@ -164,7 +169,7 @@ namespace nadena.dev.modular_avatar.core.editor
avatarTransform.rotation = avRot;
avatarTransform.localScale = avScale;
AssetDatabase.CreateAsset(dst, Util.GenerateAssetPath());
return dst;
}
private void AdjustShapeKeys()

View File

@ -17,13 +17,17 @@ namespace nadena.dev.modular_avatar.core.editor
{
private const string DEFAULT_EXP_PARAMS_ASSET_GUID = "03a6d797deb62f0429471c4e17ea99a7";
private BuildContext _context;
private int internalParamIndex = 0;
private Dictionary<string, VRCExpressionParameters.Parameter> _syncedParams =
new Dictionary<string, VRCExpressionParameters.Parameter>();
public void OnPreprocessAvatar(GameObject avatar)
public void OnPreprocessAvatar(GameObject avatar, BuildContext context)
{
_context = context;
_syncedParams.Clear();
WalkTree(avatar, ImmutableDictionary<string, string>.Empty, ImmutableDictionary<string, string>.Empty);
@ -49,7 +53,7 @@ namespace nadena.dev.modular_avatar.core.editor
}
expParams = Object.Instantiate(expParams);
AssetDatabase.CreateAsset(expParams, Util.GenerateAssetPath());
_context.SaveAsset(expParams);
var knownParams = expParams.parameters.Select(p => p.name).ToImmutableHashSet();
var parameters = expParams.parameters.ToList();
@ -129,7 +133,7 @@ namespace nadena.dev.modular_avatar.core.editor
// RuntimeAnimatorController may be AnimatorOverrideController, convert in case of AnimatorOverrideController
if (anim.runtimeAnimatorController is AnimatorOverrideController overrideController)
{
anim.runtimeAnimatorController = Util.ConvertAnimatorController(overrideController);
anim.runtimeAnimatorController = _context.ConvertAnimatorController(overrideController);
}
var controller = anim.runtimeAnimatorController as AnimatorController;
@ -147,7 +151,7 @@ namespace nadena.dev.modular_avatar.core.editor
// RuntimeAnimatorController may be AnimatorOverrideController, convert in case of AnimatorOverrideController
if (merger.animator is AnimatorOverrideController overrideController)
{
merger.animator = Util.ConvertAnimatorController(overrideController);
merger.animator = _context.ConvertAnimatorController(overrideController);
}
var controller = merger.animator as AnimatorController;
@ -192,7 +196,7 @@ namespace nadena.dev.modular_avatar.core.editor
if (remapped.TryGetValue(menu, out var newMenu)) return newMenu;
newMenu = Object.Instantiate(menu);
AssetDatabase.CreateAsset(newMenu, Util.GenerateAssetPath());
_context.SaveAsset(newMenu);
remapped[menu] = newMenu;
ClonedMenuMappings.Add(menu, newMenu);
@ -222,7 +226,7 @@ namespace nadena.dev.modular_avatar.core.editor
// Deep clone the animator
if (!Util.IsTemporaryAsset(controller))
{
controller = Util.DeepCloneAnimator(controller);
controller = _context.DeepCloneAnimator(controller);
}
var parameters = controller.parameters;

View File

@ -60,23 +60,6 @@ namespace nadena.dev.modular_avatar.core.editor
EditorApplication.hierarchyChanged += () => { RuntimeUtil.InvokeHierarchyChanged(); };
}
public static AnimatorController CreateAnimator(AnimatorController toClone = null)
{
AnimatorController controller;
if (toClone != null)
{
controller = Object.Instantiate(toClone);
}
else
{
controller = new AnimatorController();
}
AssetDatabase.CreateAsset(controller, GenerateAssetPath());
return controller;
}
public static string GenerateAssetPath()
{
return GetGeneratedAssetsFolder() + "/" + GUID.Generate() + ".asset";
@ -113,31 +96,6 @@ namespace nadena.dev.modular_avatar.core.editor
};
}
public static AnimatorController DeepCloneAnimator(RuntimeAnimatorController controller)
{
var merger = new AnimatorCombiner();
switch (controller)
{
case AnimatorController ac:
merger.AddController("", ac, null);
break;
case AnimatorOverrideController oac:
merger.AddOverrideController("", oac, null);
break;
default:
throw new Exception("Unknown RuntimeAnimatorContoller type " + controller.GetType());
}
return merger.Finish();
}
public static AnimatorController ConvertAnimatorController(AnimatorOverrideController overrideController)
{
var merger = new AnimatorCombiner();
merger.AddOverrideController("", overrideController, null);
return merger.Finish();
}
public static bool IsTemporaryAsset(Object obj)
{
var path = AssetDatabase.GetAssetPath(obj);