modular-avatar/Runtime/ArmatureAwase/ArmatureLockController.cs
bd_ 37b0f3c036
opti: fix perf regressions in new armature lock system (#729)
* opti: fix perf regressions in new armature lock system

... by avoiding reinitializing everything whenever any target bone moves.

* chore: fixing unity 2019 issues
2024-03-05 17:26:30 +09:00

225 lines
6.0 KiB
C#

#region
using System;
using System.Collections.Generic;
using nadena.dev.modular_avatar.ui;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
#endregion
namespace nadena.dev.modular_avatar.core.armature_lock
{
internal class ArmatureLockConfig
#if UNITY_EDITOR
: ScriptableSingleton<ArmatureLockConfig>
#endif
{
#if !UNITY_EDITOR
internal static ArmatureLockConfig instance { get; } = new ArmatureLockConfig();
#endif
[SerializeField] private bool _globalEnable = true;
internal event Action OnGlobalEnableChange;
internal bool GlobalEnable
{
get => _globalEnable;
set
{
if (value == _globalEnable) return;
#if UNITY_EDITOR
Undo.RecordObject(this, "Toggle Edit Mode Bone Sync");
Menu.SetChecked(UnityMenuItems.TopMenu_EditModeBoneSync, value);
#endif
_globalEnable = value;
OnGlobalEnableChange?.Invoke();
}
}
#if UNITY_EDITOR
[InitializeOnLoadMethod]
static void Init()
{
EditorApplication.delayCall += () =>
{
Menu.SetChecked(UnityMenuItems.TopMenu_EditModeBoneSync, instance._globalEnable);
};
}
[MenuItem(UnityMenuItems.TopMenu_EditModeBoneSync, false, UnityMenuItems.TopMenu_EditModeBoneSyncOrder)]
static void ToggleBoneSync()
{
instance.GlobalEnable = !instance.GlobalEnable;
}
#endif
}
internal class ArmatureLockController : IDisposable
{
public delegate IReadOnlyList<(Transform, Transform)> GetTransformsDelegate();
private static long lastMovedFrame = 0;
// Undo operations can reinitialize the MAMA component, which destroys critical lock controller state.
// Avoid this issue by keeping a static reference to the controller for each MAMA component.
private static Dictionary<ModularAvatarMergeArmature, ArmatureLockController>
_controllers = new Dictionary<ModularAvatarMergeArmature, ArmatureLockController>();
private readonly GetTransformsDelegate _getTransforms;
private readonly ModularAvatarMergeArmature _mama;
private ArmatureLockMode _curMode, _mode;
private bool _enabled;
private ArmatureLockJob _job;
public ArmatureLockController(ModularAvatarMergeArmature mama, GetTransformsDelegate getTransforms)
{
#if UNITY_EDITOR
AssemblyReloadEvents.beforeAssemblyReload += OnDomainUnload;
#endif
_mama = mama;
_getTransforms = getTransforms;
}
public static bool MovedThisFrame => Time.frameCount == lastMovedFrame;
private bool GlobalEnable => ArmatureLockConfig.instance.GlobalEnable;
public ArmatureLockMode Mode
{
get => _mode;
set
{
if (value == _mode && _job != null) return;
_mode = value;
RebuildLock();
}
}
public bool Enabled
{
get => _enabled;
set
{
if (Enabled == value) return;
_enabled = value;
RebuildLock();
}
}
public void Dispose()
{
_job?.Dispose();
_job = null;
#if UNITY_EDITOR
AssemblyReloadEvents.beforeAssemblyReload -= OnDomainUnload;
#endif
_controllers.Remove(_mama);
}
internal event Action WhenUnstable;
public static ArmatureLockController ForMerge(ModularAvatarMergeArmature mama,
GetTransformsDelegate getTransforms)
{
if (!_controllers.TryGetValue(mama, out var controller))
{
controller = new ArmatureLockController(mama, getTransforms);
_controllers[mama] = controller;
}
return controller;
}
internal void CheckLockJob()
{
if (_mama == null || !_mama.gameObject.scene.IsValid() || !Enabled)
{
_job?.Dispose();
return;
}
if (_curMode != _mode || _job == null || !_job.IsValid)
{
if (_job != null && _job.FailedOnStartup)
{
WhenUnstable?.Invoke();
Enabled = false;
_job?.Dispose();
return;
}
RebuildLock();
}
}
private bool RebuildLock()
{
_job?.Dispose();
_job = null;
var xforms = _getTransforms();
if (xforms == null)
{
return false;
}
try
{
switch (Mode)
{
case ArmatureLockMode.BidirectionalExact:
_job = BidirectionalArmatureLockOperator.Instance.RegisterLock(xforms);
break;
case ArmatureLockMode.BaseToMerge:
_job = OnewayArmatureLockOperator.Instance.RegisterLock(xforms);
break;
default:
Enabled = false;
break;
}
}
catch (Exception e)
{
Debug.LogException(e);
_job = null;
return false;
}
if (_job != null)
{
#if UNITY_EDITOR
_job.OnInvalidation += () => { EditorApplication.delayCall += CheckLockJob; };
#endif
}
_curMode = _mode;
return true;
}
private void OnDomainUnload()
{
// Unity 2019 does not call deferred callbacks before domain unload completes,
// so we need to make sure to immediately destroy all our TransformAccessArrays.
DeferDestroy.DestroyImmediate(this);
}
}
}