From f077d24d4871b7c13cddd60d00beea9d3c71f425 Mon Sep 17 00:00:00 2001 From: bd_ Date: Tue, 5 Mar 2024 00:19:54 -0800 Subject: [PATCH 1/4] fix: improved Scale Adjuster implementation (#730) This new implementation avoids creating any internal objects within the avatar itself, instead creating them as hidden scene root objects. We also move all update processing to be driven by camera callbacks as well. These changes help improve compatibility with tools such as FaceEmo, as well as being less likely to break when other tools manipulate the avatar's hierarchy directly. --- Editor/HarmonyPatches/HandleUtilityPatches.cs | 33 +- ...HideScaleAdjusterFromPrefabOverrideView.cs | 56 --- ...caleAdjusterFromPrefabOverrideView.cs.meta | 3 - Editor/HarmonyPatches/HierarchyViewPatches.cs | 4 +- Editor/HarmonyPatches/PatchLoader.cs | 1 - Editor/HarmonyPatches/PickingObjectPatch.cs | 4 +- Editor/ScaleAdjusterPass.cs | 43 ++- Runtime/ScaleAdjuster/CameraHooks.cs | 86 ----- .../ModularAvatarScaleAdjuster.cs | 117 +++--- Runtime/ScaleAdjuster/ProxyManager.cs | 338 ++++++++++++++++++ ...meraHooks.cs.meta => ProxyManager.cs.meta} | 0 .../ScaleAdjuster/ScaleAdjusterRenderer.cs | 281 +-------------- Runtime/ScaleAdjuster/ScaleProxy.cs | 48 +-- 13 files changed, 469 insertions(+), 545 deletions(-) delete mode 100644 Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs delete mode 100644 Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs.meta delete mode 100644 Runtime/ScaleAdjuster/CameraHooks.cs create mode 100644 Runtime/ScaleAdjuster/ProxyManager.cs rename Runtime/ScaleAdjuster/{CameraHooks.cs.meta => ProxyManager.cs.meta} (100%) diff --git a/Editor/HarmonyPatches/HandleUtilityPatches.cs b/Editor/HarmonyPatches/HandleUtilityPatches.cs index 6d735313..7560cf37 100644 --- a/Editor/HarmonyPatches/HandleUtilityPatches.cs +++ b/Editor/HarmonyPatches/HandleUtilityPatches.cs @@ -18,8 +18,9 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches var m_orig = AccessTools.Method(t_HandleUtility, "FilterInstanceIDs"); var m_prefix = AccessTools.Method(typeof(HandleUtilityPatches), "Prefix_FilterInstanceIDs"); + var m_postfix = AccessTools.Method(typeof(HandleUtilityPatches), "Postfix_FilterInstanceIDs"); - h.Patch(original: m_orig, prefix: new HarmonyMethod(m_prefix)); + h.Patch(original: m_orig, prefix: new HarmonyMethod(m_prefix), postfix: new HarmonyMethod(m_postfix)); } [UsedImplicitly] @@ -34,13 +35,41 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches return true; } + private static void Postfix_FilterInstanceIDs( + ref IEnumerable gameObjects, + ref int[] parentInstanceIDs, + ref int[] childInstanceIDs + ) + { + HashSet newChildInstanceIDs = null; + + foreach (var parent in gameObjects) + { + foreach (var renderer in parent.GetComponentsInChildren()) + { + if (renderer is SkinnedMeshRenderer smr && + ProxyManager.OriginalToProxyRenderer.TryGetValue(smr, out var proxy) && + proxy != null) + { + if (newChildInstanceIDs == null) newChildInstanceIDs = new HashSet(childInstanceIDs); + newChildInstanceIDs.Add(proxy.GetInstanceID()); + } + } + } + + if (newChildInstanceIDs != null) + { + childInstanceIDs = newChildInstanceIDs.ToArray(); + } + } + private static IEnumerable RemapObjects(IEnumerable objs) { return objs.Select( obj => { if (obj == null) return obj; - if (ScaleAdjusterRenderer.originalObjects.TryGetValue(obj, out var proxy) && proxy != null) + if (ProxyManager.OriginalToProxyObject.TryGetValue(obj, out var proxy) && proxy != null) { return proxy.gameObject; } diff --git a/Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs b/Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs deleted file mode 100644 index 3069107c..00000000 --- a/Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs +++ /dev/null @@ -1,56 +0,0 @@ -#region - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Reflection; -using HarmonyLib; -using JetBrains.Annotations; -using UnityEditor.SceneManagement; -using UnityEngine; - -#endregion - -namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches -{ - /// - /// Try to prevent various internal objects from showing up in the Prefab Overrides window... - /// - internal class HideScaleAdjusterFromPrefabOverrideView - { - internal static Type t_PrefabOverrides; - internal static PropertyInfo p_AddedGameObjects, p_ObjectOverrides; - - internal static void Patch(Harmony harmony) - { - var t_PrefabOverridesTreeView = AccessTools.TypeByName("UnityEditor.PrefabOverridesTreeView"); - var m_GetPrefabOverrides = AccessTools.Method(t_PrefabOverridesTreeView, "GetPrefabOverrides"); - - var m_postfix = AccessTools.Method(typeof(HideScaleAdjusterFromPrefabOverrideView), "Postfix"); - - t_PrefabOverrides = AccessTools.TypeByName("UnityEditor.PrefabOverridesTreeView+PrefabOverrides"); - p_AddedGameObjects = AccessTools.Property(t_PrefabOverrides, "addedGameObjects"); - p_ObjectOverrides = AccessTools.Property(t_PrefabOverrides, "objectOverrides"); - - harmony.Patch(original: m_GetPrefabOverrides, postfix: new HarmonyMethod(m_postfix)); - } - - [UsedImplicitly] - private static void Postfix(GameObject prefabInstance, object __result) - { - List added = p_AddedGameObjects.GetValue(__result) as List; - - if (added == null) return; - added.RemoveAll(obj => ScaleAdjusterRenderer.proxyObjects.ContainsKey(obj.instanceGameObject)); - - List objectOverrides = p_ObjectOverrides.GetValue(__result) as List; - if (objectOverrides == null) return; - objectOverrides.RemoveAll(oo => - { - var c = oo.instanceObject as Component; - return c != null && ScaleAdjusterRenderer.proxyObjects.ContainsKey(c.gameObject); - }); - } - } -} \ No newline at end of file diff --git a/Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs.meta b/Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs.meta deleted file mode 100644 index 79b92b42..00000000 --- a/Editor/HarmonyPatches/HideScaleAdjusterFromPrefabOverrideView.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 0893522c012a46358e5ecf1df6628b2e -timeCreated: 1708237029 \ No newline at end of file diff --git a/Editor/HarmonyPatches/HierarchyViewPatches.cs b/Editor/HarmonyPatches/HierarchyViewPatches.cs index c769d016..bdb4b01f 100644 --- a/Editor/HarmonyPatches/HierarchyViewPatches.cs +++ b/Editor/HarmonyPatches/HierarchyViewPatches.cs @@ -143,7 +143,7 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches var pptrValue = p_pptrValue.GetValue(hierarchyProperty); if (pptrValue == null) return false; - var skip = ScaleAdjusterRenderer.proxyObjects.ContainsKey((GameObject)pptrValue); + var skip = ProxyManager.ProxyToOriginalObject.ContainsKey((GameObject)pptrValue); if (skip) skipped++; return skip; @@ -163,7 +163,7 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches { if (pptrObject == null || isSceneHeader) return true; - if (hasChildren && ScaleAdjusterRenderer.originalObjects.ContainsKey((GameObject)pptrObject)) + if (hasChildren && ProxyManager.ProxyToOriginalObject.ContainsKey((GameObject)pptrObject)) { // See if there are any other children... hasChildren = ((GameObject)pptrObject).transform.childCount > 1; diff --git a/Editor/HarmonyPatches/PatchLoader.cs b/Editor/HarmonyPatches/PatchLoader.cs index 0e290761..a2c1bf60 100644 --- a/Editor/HarmonyPatches/PatchLoader.cs +++ b/Editor/HarmonyPatches/PatchLoader.cs @@ -13,7 +13,6 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches { private static readonly Action[] patches = new Action[] { - HideScaleAdjusterFromPrefabOverrideView.Patch, HierarchyViewPatches.Patch, #if UNITY_2022_3_OR_NEWER HandleUtilityPatches.Patch_FilterInstanceIDs, diff --git a/Editor/HarmonyPatches/PickingObjectPatch.cs b/Editor/HarmonyPatches/PickingObjectPatch.cs index 3b64a2c3..a7e5f029 100644 --- a/Editor/HarmonyPatches/PickingObjectPatch.cs +++ b/Editor/HarmonyPatches/PickingObjectPatch.cs @@ -58,11 +58,11 @@ namespace nadena.dev.modular_avatar.core.editor.HarmonyPatches if ((bool)m_TryGetGameObject.Invoke(obj, args)) { var go = args[0] as GameObject; - if (go != null && ScaleAdjusterRenderer.proxyObjects.ContainsKey(go)) + if (go != null && ProxyManager.ProxyToOriginalObject.TryGetValue(go, out var original)) { list.Add(ctor_PickingObject.Invoke(new[] { - go.transform.parent.gameObject, + original, p_materialIndex.GetValue(obj) })); continue; diff --git a/Editor/ScaleAdjusterPass.cs b/Editor/ScaleAdjusterPass.cs index 5b1fa799..239eebb5 100644 --- a/Editor/ScaleAdjusterPass.cs +++ b/Editor/ScaleAdjusterPass.cs @@ -1,9 +1,11 @@ -using System.Collections.Generic; -using System.Linq; +#region + +using System.Collections.Generic; using nadena.dev.ndmf; -using UnityEditor.EditorTools; using UnityEngine; +#endregion + namespace nadena.dev.modular_avatar.core.editor { internal class ScaleAdjusterPass : Pass @@ -11,25 +13,30 @@ namespace nadena.dev.modular_avatar.core.editor protected override void Execute(ndmf.BuildContext context) { Dictionary boneMappings = new Dictionary(); - foreach (var component in context.AvatarRootObject.GetComponentsInChildren()) + + foreach (var adjuster in context.AvatarRootObject.GetComponentsInChildren(true)) { - var proxyTransform = component.transform; - var parentAdjuster = component.transform.parent?.GetComponent(); - if (parentAdjuster != null) - { - UnityEngine.Object.DestroyImmediate(component); - - proxyTransform.localScale = parentAdjuster.Scale; - parentAdjuster.scaleProxy = null; // prevent destruction of the ScaleProxy itself - UnityEngine.Object.DestroyImmediate(parentAdjuster); - - boneMappings.Add(proxyTransform.parent, proxyTransform); - } + var proxyObject = new GameObject("ScaleProxy"); + var proxyTransform = proxyObject.transform; + + proxyTransform.SetParent(adjuster.transform, false); + proxyTransform.localPosition = Vector3.zero; + proxyTransform.localRotation = Quaternion.identity; + proxyTransform.localScale = adjuster.Scale; + + boneMappings.Add(adjuster.transform, proxyTransform); + + Object.DestroyImmediate(adjuster); } - + + // Legacy cleanup foreach (var sar in context.AvatarRootObject.GetComponentsInChildren()) { - UnityEngine.Object.DestroyImmediate(sar.gameObject); + Object.DestroyImmediate(sar.gameObject); + } + foreach (var sar in context.AvatarRootObject.GetComponentsInChildren()) + { + Object.DestroyImmediate(sar.gameObject); } if (boneMappings.Count == 0) diff --git a/Runtime/ScaleAdjuster/CameraHooks.cs b/Runtime/ScaleAdjuster/CameraHooks.cs deleted file mode 100644 index e9da8699..00000000 --- a/Runtime/ScaleAdjuster/CameraHooks.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System.Collections.Generic; -using nadena.dev.modular_avatar.JacksonDunstan.NativeCollections; -using UnityEngine; - -namespace nadena.dev.modular_avatar.core -{ - internal static class CameraHooks - { - private static Dictionary originalToProxy - = new Dictionary( - new ObjectIdentityComparer() - ); - - #if UNITY_EDITOR - [UnityEditor.InitializeOnLoadMethod] - private static void Init() - { - Camera.onPreCull += OnPreCull; - Camera.onPostRender += OnPostRender; - UnityEditor.AssemblyReloadEvents.beforeAssemblyReload += ClearStates; - UnityEditor.SceneManagement.EditorSceneManager.sceneSaving += (scene, path) => ClearStates(); - } - #endif - - internal static void RegisterProxy(SkinnedMeshRenderer original, SkinnedMeshRenderer proxy) - { - originalToProxy[original] = proxy; - } - - internal static void UnregisterProxy(SkinnedMeshRenderer original) - { - originalToProxy.Remove(original); - } - - private static List<(SkinnedMeshRenderer, bool)> statesToRestore = new List<(SkinnedMeshRenderer, bool)>(); - - private static List toDeregister = new List(); - - - private static void OnPreCull(Camera camera) - { - ClearStates(); - toDeregister.Clear(); - - foreach (var kvp in originalToProxy) - { - var original = kvp.Key; - var proxy = kvp.Value; - - if (original == null || proxy == null) - { - toDeregister.Add(original); - continue; - } - - proxy.enabled = original.enabled; - if (original.enabled && original.gameObject.activeInHierarchy) - { - statesToRestore.Add((original, original.enabled)); - original.enabled = false; - } - } - - foreach (var original in toDeregister) - { - originalToProxy.Remove(original); - } - } - - private static void OnPostRender(Camera camera) - { - ClearStates(); - } - - - private static void ClearStates() - { - foreach (var (original, state) in statesToRestore) - { - original.enabled = state; - } - - statesToRestore.Clear(); - } - } -} \ No newline at end of file diff --git a/Runtime/ScaleAdjuster/ModularAvatarScaleAdjuster.cs b/Runtime/ScaleAdjuster/ModularAvatarScaleAdjuster.cs index 235cb938..33f90a5b 100644 --- a/Runtime/ScaleAdjuster/ModularAvatarScaleAdjuster.cs +++ b/Runtime/ScaleAdjuster/ModularAvatarScaleAdjuster.cs @@ -1,6 +1,8 @@ #region +using System; using UnityEngine; +using UnityEngine.SceneManagement; using UnityEngine.Serialization; #if UNITY_EDITOR using UnityEditor; @@ -25,105 +27,100 @@ namespace nadena.dev.modular_avatar.core set { m_Scale = value; - Update(); + PreCull(); } } [SerializeField] [FormerlySerializedAs("scaleProxy")] internal Transform legacyScaleProxy; - internal Transform scaleProxy; + internal Transform scaleProxyParent, scaleProxyChild; + [NonSerialized] private bool initialized = false; #if UNITY_EDITOR - void OnValidate() + void Awake() { - base.OnValidate(); + ProxyManager.RegisterAdjuster(this); initialized = false; } - - private void Update() + + void OnValidate() { - if (scaleProxy == null || initialized == false) + ProxyManager.RegisterAdjuster(this); + initialized = false; + } + + internal void PreCull() + { + if (PrefabUtility.IsPartOfPrefabAsset(this)) return; + + if (scaleProxyParent == null || initialized == false) { InitializeProxy(); } + var xform = transform; + scaleProxyParent.position = transform.position; + scaleProxyParent.rotation = transform.rotation; + scaleProxyParent.localScale = transform.localScale; + scaleProxyChild.localScale = m_Scale; + + ProxyManager.RegisterBone(xform, scaleProxyChild); + if (legacyScaleProxy != null && !PrefabUtility.IsPartOfPrefabAsset(legacyScaleProxy)) { DestroyImmediate(legacyScaleProxy.gameObject); legacyScaleProxy = null; } - - scaleProxy.localScale = m_Scale; } private void InitializeProxy() { - if (scaleProxy == null) + if (scaleProxyParent == null) { - scaleProxy = new GameObject(gameObject.name + " (Scale Proxy)").transform; - scaleProxy.SetParent(transform, false); - scaleProxy.localPosition = Vector3.zero; - scaleProxy.localRotation = Quaternion.identity; - scaleProxy.localScale = m_Scale; - scaleProxy.gameObject.AddComponent(); - PrefabUtility.RecordPrefabInstancePropertyModifications(this); + scaleProxyParent = new GameObject(gameObject.name + " (Scale Proxy)").transform; + scaleProxyChild = new GameObject("Child").transform; + + scaleProxyChild.transform.SetParent(scaleProxyParent, false); + +#if MODULAR_AVATAR_DEBUG_HIDDEN + scaleProxyParent.gameObject.hideFlags = HideFlags.DontSave; + scaleProxyChild.gameObject.hideFlags = HideFlags.DontSave; +#else + scaleProxyParent.gameObject.hideFlags = HideFlags.HideAndDontSave; + scaleProxyChild.gameObject.hideFlags = HideFlags.HideAndDontSave; +#endif + + if (scaleProxyParent.gameObject.scene != gameObject.scene && gameObject.scene.IsValid()) + { + SceneManager.MoveGameObjectToScene(scaleProxyParent.gameObject, gameObject.scene); + } } - - ConfigureRenderers(); - + initialized = true; } private void OnDestroy() { - if (scaleProxy != null) + ProxyManager.UnregisterAdjuster(this); + + if (scaleProxyParent != null) { - DestroyImmediate(scaleProxy.gameObject); + DestroyImmediate(scaleProxyParent.gameObject); + } + + if (transform != null) + { + ProxyManager.UnregisterBone(transform); } - ScaleAdjusterRenderer.InvalidateAll(); base.OnDestroy(); } - - - private void ConfigureRenderers() - { - var avatar = RuntimeUtil.FindAvatarInParents(transform); - if (avatar == null) return; - foreach (var smr in avatar.GetComponentsInChildren(true)) - { - if (smr.GetComponent() != null) continue; - - var child = smr.transform.Find(ADJUSTER_OBJECT)?.GetComponent(); - if (child == null) - { - var childObj = new GameObject(ADJUSTER_OBJECT); - Undo.RegisterCreatedObjectUndo(childObj, ""); - - var childSmr = childObj.AddComponent(); - EditorUtility.CopySerialized(smr, childSmr); - - childObj.transform.SetParent(smr.transform, false); - childObj.transform.localPosition = Vector3.zero; - childObj.transform.localRotation = Quaternion.identity; - childObj.transform.localScale = Vector3.one; - - child = childObj.AddComponent(); - } - - child.BoneMappings[transform] = scaleProxy; - child.ClearBoneCache(); - } - } -#endif - -#if !UNITY_EDITOR - private void Update() - { - // placeholder to make builds work +#else + internal void PreCull() { + // build time stub } #endif } diff --git a/Runtime/ScaleAdjuster/ProxyManager.cs b/Runtime/ScaleAdjuster/ProxyManager.cs new file mode 100644 index 00000000..6c22f55e --- /dev/null +++ b/Runtime/ScaleAdjuster/ProxyManager.cs @@ -0,0 +1,338 @@ +#region + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using nadena.dev.modular_avatar.JacksonDunstan.NativeCollections; +#if UNITY_EDITOR +using UnityEditor; +using UnityEditor.SceneManagement; +#endif +using UnityEngine; +using UnityEngine.SceneManagement; + +#endregion + +namespace nadena.dev.modular_avatar.core +{ + internal static class ProxyManager + { + #region Accessible from multiple threads + + private static bool _dirty = false; + private static readonly object _lock = new object(); + + private static ImmutableHashSet _adjusters + = ImmutableHashSet.Empty; + + private static ImmutableDictionary _originalToReplacementBone + = ImmutableDictionary.Empty.WithComparers(new ObjectIdentityComparer()); + + internal static void RegisterAdjuster(ModularAvatarScaleAdjuster adjuster) + { + lock (_lock) + { + _adjusters = _adjusters.Add(adjuster); + _dirty = true; + } + } + + internal static void UnregisterAdjuster(ModularAvatarScaleAdjuster adjuster) + { + lock (_lock) + { + _adjusters = _adjusters.Remove(adjuster); + _dirty = true; + } + } + + internal static void RegisterBone(Transform original, Transform replacement) + { + lock (_lock) + { + if (_originalToReplacementBone.TryGetValue(original, out var val) && val == replacement) + { + return; + } + + _originalToReplacementBone = _originalToReplacementBone.Add(original, replacement); + _dirty = true; + } + } + + internal static void UnregisterBone(Transform original) + { + lock (_lock) + { + _originalToReplacementBone = _originalToReplacementBone.Remove(original); + _dirty = true; + } + } + + #endregion + + private static ImmutableHashSet _capturedAdjusters = + ImmutableHashSet.Empty; + + private static ImmutableDictionary _capturedBones = + ImmutableDictionary.Empty; + + private static ImmutableDictionary _originalToReplacementRenderer + = ImmutableDictionary.Empty.WithComparers( + new ObjectIdentityComparer()); + + internal static ImmutableDictionary ProxyToOriginalObject { get; private set; } = + ImmutableDictionary.Empty; + + internal static ImmutableDictionary OriginalToProxyObject { get; private set; } = + ImmutableDictionary.Empty; + + internal static ImmutableDictionary OriginalToProxyRenderer => + _originalToReplacementRenderer; + + internal static ImmutableHashSet RetainedObjects; + + internal static bool ShouldRetain(GameObject obj) => RetainedObjects.Contains(obj); + + private static void BuildRenderers() + { + lock (_lock) + { + _capturedAdjusters = _adjusters; + + // Give each adjuster a chance to initialize the bone mappings first + foreach (var adj in _capturedAdjusters) + { + adj.PreCull(); + } + + foreach (var kvp in _originalToReplacementBone) + { + if (kvp.Key == null || kvp.Value == null) + { + _originalToReplacementBone = _originalToReplacementBone.Remove(kvp.Key); + } + } + + _capturedBones = _originalToReplacementBone; + _dirty = false; + } + + var avatarRoots = _capturedBones.Keys.Select(RuntimeUtil.FindAvatarTransformInParents).ToImmutableHashSet(); + var potentialRenderers = avatarRoots.SelectMany(r => r.GetComponentsInChildren(true)) + .ToList(); + + ImmutableDictionary.Builder renderers = + ImmutableDictionary.CreateBuilder( + new ObjectIdentityComparer() + ); + + foreach (var originalRenderer in potentialRenderers) + { + SkinnedMeshRenderer replacement; + + if (!NeedsReplacement(originalRenderer)) + { + if (_originalToReplacementRenderer.TryGetValue(originalRenderer, out replacement) && + replacement != null) + { + Object.DestroyImmediate(replacement.gameObject); + } + + continue; + } + + if (!_originalToReplacementRenderer.TryGetValue(originalRenderer, out replacement) || + replacement == null) + { + replacement = CreateReplacement(originalRenderer); + } + + SetupBoneMappings(originalRenderer, replacement); + + renderers.Add(originalRenderer, replacement); + } + + foreach (var kvp in _originalToReplacementRenderer) + { + if (!renderers.ContainsKey(kvp.Key)) + { + if (kvp.Value != null) + { + Object.DestroyImmediate(kvp.Value.gameObject); + } + } + } + + _originalToReplacementRenderer = renderers.ToImmutable(); + ProxyToOriginalObject = _originalToReplacementRenderer.ToImmutableDictionary( + kvp => kvp.Value.gameObject, + kvp => kvp.Key.gameObject + ); + + OriginalToProxyObject = _originalToReplacementRenderer.ToImmutableDictionary( + kvp => kvp.Key.gameObject, + kvp => kvp.Value.gameObject + ); + + RetainedObjects = ProxyToOriginalObject.Keys.Concat( + _capturedBones.Values.Where(b => b != null).Select(b => b.gameObject) + ).ToImmutableHashSet(new ObjectIdentityComparer()); + } + + private static void SetupBoneMappings(SkinnedMeshRenderer originalRenderer, SkinnedMeshRenderer replacement) + { + replacement.sharedMesh = originalRenderer.sharedMesh; + replacement.bones = originalRenderer.bones.Select(MapBone).ToArray(); + } + + private static Transform MapBone(Transform srcBone) + { + if (_capturedBones.TryGetValue(srcBone, out var newBone) && newBone != null) + { + return newBone; + } + else + { + return srcBone; + } + } + + private static SkinnedMeshRenderer CreateReplacement(SkinnedMeshRenderer originalRenderer) + { + var obj = new GameObject("MA Proxy Renderer for " + originalRenderer.gameObject.name); + // We can't use HideAndDontSave as this would break scene view click-to-pick handling + // (so instead this is hidden via the HierarchyViewPatches harmony hack) + obj.hideFlags = HideFlags.DontSave; + + var renderer = obj.AddComponent(); + + return renderer; + } + + private static bool NeedsReplacement(SkinnedMeshRenderer originalRenderer) + { + if (originalRenderer.sharedMesh == null) return false; + + var bones = originalRenderer.bones; + var weights = originalRenderer.sharedMesh.GetAllBoneWeights(); + + for (var i = 0; i < weights.Length; i++) + { + var bone = bones[weights[i].boneIndex]; + if (_capturedBones.ContainsKey(bone)) return true; + } + + return false; + } + +#if UNITY_EDITOR + [InitializeOnLoadMethod] + private static void Init() + { + Camera.onPreCull += OnPreCull; + Camera.onPostRender += OnPostRender; + AssemblyReloadEvents.beforeAssemblyReload += () => + { + ClearStates(); + foreach (var renderer in _originalToReplacementRenderer.Values) + { + Object.DestroyImmediate(renderer.gameObject); + } + }; + EditorSceneManager.sceneSaving += (scene, path) => ClearStates(); + } + + private static List<(SkinnedMeshRenderer, bool)> statesToRestore = new List<(SkinnedMeshRenderer, bool)>(); + + private static void OnPreCull(Camera camera) + { + if (_dirty) + { + BuildRenderers(); + } + + ClearStates(); + + foreach (var adj in _capturedAdjusters) + { + adj.PreCull(); // update scale + } + + foreach (var kvp in _originalToReplacementRenderer) + { + var original = kvp.Key; + var proxy = kvp.Value; + + if (original == null || proxy == null) + { + _dirty = true; + continue; + } + + var originalGameObject = original.gameObject; + var proxyActive = original.enabled && originalGameObject.activeInHierarchy && + !SceneVisibilityManager.instance.IsHidden(originalGameObject, false); + + proxy.enabled = proxyActive; + if (original.enabled && originalGameObject.activeInHierarchy) + { + CopyRendererStates(original, proxy); + + statesToRestore.Add((original, original.enabled)); + original.enabled = false; + } + } + } + + private static void CopyRendererStates(SkinnedMeshRenderer parentRenderer, SkinnedMeshRenderer myRenderer) + { + myRenderer.transform.position = parentRenderer.transform.position; + myRenderer.transform.rotation = parentRenderer.transform.rotation; + + myRenderer.sharedMaterials = parentRenderer.sharedMaterials; + myRenderer.localBounds = parentRenderer.localBounds; + myRenderer.rootBone = MapBone(parentRenderer.rootBone); + myRenderer.quality = parentRenderer.quality; + myRenderer.shadowCastingMode = parentRenderer.shadowCastingMode; + myRenderer.receiveShadows = parentRenderer.receiveShadows; + myRenderer.lightProbeUsage = parentRenderer.lightProbeUsage; + myRenderer.reflectionProbeUsage = parentRenderer.reflectionProbeUsage; + myRenderer.probeAnchor = parentRenderer.probeAnchor; + myRenderer.motionVectorGenerationMode = parentRenderer.motionVectorGenerationMode; + myRenderer.allowOcclusionWhenDynamic = parentRenderer.allowOcclusionWhenDynamic; + + if (myRenderer.gameObject.scene != parentRenderer.gameObject.scene && + parentRenderer.gameObject.scene.IsValid()) + { + SceneManager.MoveGameObjectToScene(myRenderer.gameObject, parentRenderer.gameObject.scene); + } + + if (myRenderer.sharedMesh != null) + { + var blendShapeCount = myRenderer.sharedMesh.blendShapeCount; + + for (int i = 0; i < blendShapeCount; i++) + { + myRenderer.SetBlendShapeWeight(i, parentRenderer.GetBlendShapeWeight(i)); + } + } + } + + private static void OnPostRender(Camera camera) + { + ClearStates(); + } + + private static void ClearStates() + { + foreach (var (original, state) in statesToRestore) + { + original.enabled = state; + } + + statesToRestore.Clear(); + } +#endif + } +} \ No newline at end of file diff --git a/Runtime/ScaleAdjuster/CameraHooks.cs.meta b/Runtime/ScaleAdjuster/ProxyManager.cs.meta similarity index 100% rename from Runtime/ScaleAdjuster/CameraHooks.cs.meta rename to Runtime/ScaleAdjuster/ProxyManager.cs.meta diff --git a/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs b/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs index f53655ac..e4ab0e57 100644 --- a/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs +++ b/Runtime/ScaleAdjuster/ScaleAdjusterRenderer.cs @@ -1,300 +1,33 @@ #region +using UnityEngine; +using VRC.SDKBase; #if UNITY_EDITOR using UnityEditor; #endif -using System.Collections.Generic; -using System.Linq; -using nadena.dev.modular_avatar.JacksonDunstan.NativeCollections; -using UnityEngine; -using VRC.SDKBase; -using Object = System.Object; #endregion namespace nadena.dev.modular_avatar.core { + /// + /// Legacy component from early 1.9.x builds. + /// [ExecuteInEditMode] [AddComponentMenu("")] [RequireComponent(typeof(SkinnedMeshRenderer))] internal class ScaleAdjusterRenderer : MonoBehaviour, IEditorOnly { - internal static Dictionary originalParent = - new Dictionary(new ObjectIdentityComparer()); - - internal static Dictionary proxyObjects = new Dictionary( - new ObjectIdentityComparer()); - - internal static Dictionary originalObjects = - new Dictionary( - new ObjectIdentityComparer() - ); - - private static int RecreateHierarchyIndexCount = 0; - -#if UNITY_EDITOR - [InitializeOnLoadMethod] - static void Setup() - { - EditorApplication.hierarchyChanged += InvalidateAll; - } -#endif - - internal static void InvalidateAll() - { - RecreateHierarchyIndexCount++; - } - - private SkinnedMeshRenderer myRenderer; - private SkinnedMeshRenderer parentRenderer; - - private bool wasActive = false; - private bool redoBoneMappings = true; - private bool hasRelevantBones = false; - private int lastRecreateHierarchyIndex = -1; - - internal Dictionary BoneMappings = new Dictionary( - new ObjectIdentityComparer() - ); - - #if UNITY_EDITOR private void OnValidate() { if (PrefabUtility.IsPartOfPrefabAsset(this)) return; - redoBoneMappings = true; - + EditorApplication.delayCall += () => { - if (this == null) return; - - // We hide this in Harmony, not here, so it is eligible for click-to-select. - gameObject.hideFlags = HideFlags.DontSaveInBuild; - - if (BoneMappings == null) - { - BoneMappings = new Dictionary(); - } - - Configure(); + if (this != null) DestroyImmediate(gameObject); }; - - EditorApplication.playModeStateChanged += OnPlayModeStateChanged; - redoBoneMappings = true; - } - - private void OnPlayModeStateChanged(PlayModeStateChange change) - { - if (change == PlayModeStateChange.ExitingEditMode) - { - ClearHooks(); - } } #endif - - private Transform MapBone(Transform bone) - { - if (bone == null) return null; - if (BoneMappings.TryGetValue(bone, out var newBone) && newBone != null) return newBone; - return bone; - } - - private void OnDestroy() - { - ClearHooks(); - #if UNITY_EDITOR - EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; - #endif - } - - private void Configure() - { - if (originalParent.TryGetValue(this, out var prevParent) && transform.parent?.gameObject == prevParent) - { - return; - } - - if (prevParent != null) - { - ClearHooks(); - } - - if (!hasRelevantBones) - { - return; - } - - if (transform.parent == null) - { - return; - } - -#if UNITY_EDITOR - AssemblyReloadEvents.beforeAssemblyReload += ClearHooks; -#endif - - var parent = transform.parent.gameObject; - - proxyObjects[gameObject] = parent; - originalObjects[parent] = this; - originalParent[this] = parent; - - CheckBoneUsage(); // register the proxy if needed - } - - private void ClearHooks() - { - if (originalParent.TryGetValue(this, out var prevParent)) - { - if (parentRenderer != null) - { - CameraHooks.UnregisterProxy(parentRenderer); - } - - if ((Object)prevParent != null) - { - originalObjects.Remove(prevParent); - } - - originalParent.Remove(this); - if (gameObject != null) - { - proxyObjects.Remove(gameObject); - } - else - { - CleanDeadObjects(); - } - } - } - -#if UNITY_EDITOR - private void Update() - { - if (EditorApplication.isPlayingOrWillChangePlaymode) return; - if (transform.parent == null) - { - DestroyImmediate(gameObject); - return; - } - - if (myRenderer == null) - { - myRenderer = GetComponent(); - } - - if (parentRenderer == null) - { - parentRenderer = transform.parent.GetComponent(); - } - - Configure(); - - myRenderer.sharedMaterials = parentRenderer.sharedMaterials; - myRenderer.localBounds = parentRenderer.localBounds; - if (redoBoneMappings || lastRecreateHierarchyIndex != RecreateHierarchyIndexCount - || myRenderer.sharedMesh != parentRenderer.sharedMesh) - { - CleanDeadObjects(BoneMappings); - - if (BoneMappings.Count == 0) - { - #if UNITY_2022_3_OR_NEWER - DestroyImmediate(gameObject); - return; - #endif - } - - myRenderer.sharedMesh = parentRenderer.sharedMesh; - myRenderer.rootBone = MapBone(parentRenderer.rootBone); - myRenderer.bones = parentRenderer.bones.Select(MapBone).ToArray(); - redoBoneMappings = false; - lastRecreateHierarchyIndex = RecreateHierarchyIndexCount; - - CheckBoneUsage(); - } - - if (!hasRelevantBones) return; - - myRenderer.quality = parentRenderer.quality; - myRenderer.shadowCastingMode = parentRenderer.shadowCastingMode; - myRenderer.receiveShadows = parentRenderer.receiveShadows; - myRenderer.lightProbeUsage = parentRenderer.lightProbeUsage; - myRenderer.reflectionProbeUsage = parentRenderer.reflectionProbeUsage; - myRenderer.probeAnchor = parentRenderer.probeAnchor; - myRenderer.motionVectorGenerationMode = parentRenderer.motionVectorGenerationMode; - myRenderer.allowOcclusionWhenDynamic = parentRenderer.allowOcclusionWhenDynamic; - - if (myRenderer.sharedMesh != null) - { - var blendShapeCount = myRenderer.sharedMesh.blendShapeCount; - - for (int i = 0; i < blendShapeCount; i++) - { - myRenderer.SetBlendShapeWeight(i, parentRenderer.GetBlendShapeWeight(i)); - } - } - } -#endif - private void CheckBoneUsage() - { - hasRelevantBones = false; - if (myRenderer.sharedMesh != null) - { - var weights = myRenderer.sharedMesh.GetAllBoneWeights(); - var parentBones = parentRenderer.bones; - foreach (var weight in weights) - { - if (weight.weight < 0.0001f) continue; - if (weight.boneIndex < 0 || weight.boneIndex >= parentBones.Length) continue; - - var bone = parentBones[weight.boneIndex]; - if (BoneMappings.ContainsKey(bone)) - { - hasRelevantBones = true; - break; - } - } - } - - if (hasRelevantBones) - { - CameraHooks.RegisterProxy(parentRenderer, myRenderer); - } - else - { - CameraHooks.UnregisterProxy(parentRenderer); - myRenderer.enabled = false; - } - } - - public void ClearBoneCache() - { - redoBoneMappings = true; - } - - private static void CleanDeadObjects() - { - CleanDeadObjects(originalParent); - CleanDeadObjects(originalObjects); - CleanDeadObjects(proxyObjects); - } - - private static int lastCleanedFrame = 0; - private static void CleanDeadObjects(IDictionary dict) - where K: UnityEngine.Object - where V: UnityEngine.Object - { - // Avoid any O(n^2) behavior if we have lots of cleanup calls happening at the same instant - if (Time.frameCount == lastCleanedFrame) return; - lastCleanedFrame = Time.frameCount; - - var dead = dict.Where(kvp => kvp.Key == null || kvp.Value == null).ToList(); - - foreach (var kvp in dead) - { - dict.Remove(kvp.Key); - } - } } } \ No newline at end of file diff --git a/Runtime/ScaleAdjuster/ScaleProxy.cs b/Runtime/ScaleAdjuster/ScaleProxy.cs index 25cac20b..27dca2ef 100644 --- a/Runtime/ScaleAdjuster/ScaleProxy.cs +++ b/Runtime/ScaleAdjuster/ScaleProxy.cs @@ -16,13 +16,12 @@ namespace nadena.dev.modular_avatar.core #if UNITY_EDITOR void OnValidate() { - base.OnDestroy(); + base.OnValidate(); EditorApplication.delayCall += DeferredValidate; } void OnDestroy() { - ScaleAdjusterRenderer.InvalidateAll(); base.OnDestroy(); } @@ -30,44 +29,12 @@ namespace nadena.dev.modular_avatar.core { if (this == null) return; - if (GetComponent() == null) + // Avoid logspam on Unity 2019 + if (PrefabUtility.IsPartOfPrefabInstance(gameObject)) return; + + if (!ProxyManager.ShouldRetain(gameObject)) { - gameObject.AddComponent(); - } - - var avatar = ndmf.runtime.RuntimeUtil.FindAvatarInParents(transform); - ClearOverrides(avatar); - - gameObject.hideFlags = HideFlags.HideInHierarchy; - -#if MODULAR_AVATAR_DEBUG_HIDDEN - gameObject.hideFlags = HideFlags.None; -#endif - hideFlags = HideFlags.None; - - var parentObject = transform.parent; - var parentScaleAdjuster = - parentObject != null ? parentObject.GetComponent() : null; - - if (parentScaleAdjuster == null || parentScaleAdjuster.scaleProxy != transform) - { - if (PrefabUtility.IsPartOfPrefabAsset(this)) - { - var path = AssetDatabase.GetAssetPath(this); - var root = PrefabUtility.LoadPrefabContents(path); - - foreach (var obj in root.GetComponentsInChildren()) - { - obj.DeferredValidate(); - } - - PrefabUtility.SaveAsPrefabAsset(root, path); - PrefabUtility.UnloadPrefabContents(root); - } - else - { - SelfDestruct(); - } + SelfDestruct(); } } @@ -79,10 +46,9 @@ namespace nadena.dev.modular_avatar.core root = transform; while (root.parent != null) root = root.parent; } - + ClearOverrides(root); - // Avoid logspam on Unity 2019 if (PrefabUtility.IsPartOfPrefabInstance(gameObject)) return; DestroyImmediate(gameObject); From d15bbe86a22f9b311e2c7494b318e8603b21be2c Mon Sep 17 00:00:00 2001 From: kaikoga Date: Tue, 5 Mar 2024 17:20:43 +0900 Subject: [PATCH 2/4] chore: Skip ApplyAnimatorDefaultValuesPass when not VRChat avatar (#731) --- Editor/ApplyAnimatorDefaultValuesPass.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Editor/ApplyAnimatorDefaultValuesPass.cs b/Editor/ApplyAnimatorDefaultValuesPass.cs index 587042e8..f2554d13 100644 --- a/Editor/ApplyAnimatorDefaultValuesPass.cs +++ b/Editor/ApplyAnimatorDefaultValuesPass.cs @@ -15,6 +15,8 @@ namespace nadena.dev.modular_avatar.core.editor { protected override void Execute(ndmf.BuildContext context) { + if (!context.AvatarDescriptor) return; + var values = context.GetState()?.InitialValueOverrides ?? ImmutableDictionary.Empty; From 37b0f3c036714b340ad3e0b948a94d540d865f08 Mon Sep 17 00:00:00 2001 From: bd_ Date: Tue, 5 Mar 2024 00:26:30 -0800 Subject: [PATCH 3/4] 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 --- Runtime/ArmatureAwase/AllocationMap.cs | 149 ++++++ Runtime/ArmatureAwase/AllocationMap.cs.meta | 3 + .../ArmatureAwase/ArmatureLockController.cs | 3 +- Runtime/ArmatureAwase/ArmatureLockJob.cs | 6 +- .../ArmatureAwase/ArmatureLockJobAccessor.cs | 49 +- Runtime/ArmatureAwase/ArmatureLockOperator.cs | 426 ++++++++++++------ .../BidirectionalArmatureLock.cs | 55 ++- Runtime/ArmatureAwase/NativeMemoryManager.cs | 219 +++++++++ .../ArmatureAwase/NativeMemoryManager.cs.meta | 3 + Runtime/ArmatureAwase/OnewayArmatureLock.cs | 207 +++++---- Runtime/ArmatureAwase/TransformState.cs | 8 +- UnitTests~/ArmatureAwase.meta | 3 + UnitTests~/ArmatureAwase/AllocationMapTest.cs | 78 ++++ .../ArmatureAwase/AllocationMapTest.cs.meta | 3 + .../ArmatureAwase/NativeMemoryManagerTest.cs | 69 +++ .../NativeMemoryManagerTest.cs.meta | 3 + 16 files changed, 987 insertions(+), 297 deletions(-) create mode 100644 Runtime/ArmatureAwase/AllocationMap.cs create mode 100644 Runtime/ArmatureAwase/AllocationMap.cs.meta create mode 100644 Runtime/ArmatureAwase/NativeMemoryManager.cs create mode 100644 Runtime/ArmatureAwase/NativeMemoryManager.cs.meta create mode 100644 UnitTests~/ArmatureAwase.meta create mode 100644 UnitTests~/ArmatureAwase/AllocationMapTest.cs create mode 100644 UnitTests~/ArmatureAwase/AllocationMapTest.cs.meta create mode 100644 UnitTests~/ArmatureAwase/NativeMemoryManagerTest.cs create mode 100644 UnitTests~/ArmatureAwase/NativeMemoryManagerTest.cs.meta diff --git a/Runtime/ArmatureAwase/AllocationMap.cs b/Runtime/ArmatureAwase/AllocationMap.cs new file mode 100644 index 00000000..898359f2 --- /dev/null +++ b/Runtime/ArmatureAwase/AllocationMap.cs @@ -0,0 +1,149 @@ +#region + +using System; +using System.Collections.Generic; + +#endregion + +namespace nadena.dev.modular_avatar.core.armature_lock +{ + internal interface ISegment + { + AllocationMap.DefragmentCallback Defragment { get; set; } + int Offset { get; } + int Length { get; } + } + + internal class AllocationMap + { + public delegate void DefragmentCallback(int oldOffset, int newOffset, int length); + + // Visible for unit tests + internal class Segment : ISegment + { + public int _offset; + public int _length; + public bool _inUse; + + public AllocationMap.DefragmentCallback Defragment { get; set; } + public int Offset => _offset; + public int Length => _length; + + internal Segment(int offset, int length, bool inUse) + { + _offset = offset; + _length = length; + _inUse = inUse; + } + } + + /// + /// A list of allocated (and unallocated) segments. + /// + /// Invariant: The last element (if any) is always inUse. + /// Invariant: No two consecutive elements are free (inUse = false). + /// + /// + List segments = new List(); + + public ISegment Allocate(int requestedLength) + { + for (int i = 0; i < segments.Count; i++) + { + var segment = segments[i]; + if (segment._inUse) continue; + + if (segment._length == requestedLength) + { + segment._inUse = true; + return segment; + } + + if (segment._length > requestedLength) + { + var remaining = new Segment( + segment._offset + requestedLength, + segment._length - requestedLength, + false + ); + + segment._length = requestedLength; + segment._inUse = true; + segments.Insert(i + 1, remaining); + + return segment; + } + } + + // Add a new in-use segment at the end + var newSegment = new Segment( + segments.Count == 0 ? 0 : segments[segments.Count - 1]._offset + segments[segments.Count - 1]._length, + requestedLength, + true + ); + segments.Add(newSegment); + + return newSegment; + } + + public void FreeSegment(ISegment inputSegment) + { + var s = inputSegment as Segment; + if (s == null) throw new ArgumentException("Passed a foreign segment???"); + + int index = segments.BinarySearch(s, Comparer.Create((a, b) => a._offset.CompareTo(b._offset))); + if (index < 0 || segments[index] != s) throw new Exception("Segment not found in FreeSegment"); + + if (index == segments.Count - 1) + { + segments.RemoveAt(index); + return; + } + + if (index + 1 < segments.Count) + { + var next = segments[index + 1]; + if (!next._inUse) + { + next._offset = s._offset; + next._length += s._length; + segments.RemoveAt(index); + return; + } + } + + // Replace with a fresh segment object to avoid any issues with leaking old references to the segment + segments[index] = new Segment(s._offset, s._length, false); + } + + /// + /// Defragments all free space. When a segment is moved, the passed callback is called with the old and new offsets, + /// and then the callback associated with the segment (if any) is also invoked. + /// + /// + public void Defragment(AllocationMap.DefragmentCallback callback) + { + int offset = 0; + + for (int i = 0; i < segments.Count; i++) + { + var seg = segments[i]; + if (!seg._inUse) + { + segments.RemoveAt(i); + i--; + continue; + } + + if (seg._offset != offset) + { + callback(seg._offset, offset, seg._length); + seg.Defragment?.Invoke(seg._offset, offset, seg._length); + seg._offset = offset; + } + + offset += seg.Length; + } + } + } +} \ No newline at end of file diff --git a/Runtime/ArmatureAwase/AllocationMap.cs.meta b/Runtime/ArmatureAwase/AllocationMap.cs.meta new file mode 100644 index 00000000..32eb9ae7 --- /dev/null +++ b/Runtime/ArmatureAwase/AllocationMap.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1341ee6059a7410abe5a1170cdbf6355 +timeCreated: 1709527531 \ No newline at end of file diff --git a/Runtime/ArmatureAwase/ArmatureLockController.cs b/Runtime/ArmatureAwase/ArmatureLockController.cs index fc2b74bf..34873baa 100644 --- a/Runtime/ArmatureAwase/ArmatureLockController.cs +++ b/Runtime/ArmatureAwase/ArmatureLockController.cs @@ -196,8 +196,9 @@ namespace nadena.dev.modular_avatar.core.armature_lock break; } } - catch (Exception) + catch (Exception e) { + Debug.LogException(e); _job = null; return false; } diff --git a/Runtime/ArmatureAwase/ArmatureLockJob.cs b/Runtime/ArmatureAwase/ArmatureLockJob.cs index cedcbad1..09344312 100644 --- a/Runtime/ArmatureAwase/ArmatureLockJob.cs +++ b/Runtime/ArmatureAwase/ArmatureLockJob.cs @@ -23,8 +23,12 @@ namespace nadena.dev.modular_avatar.core.armature_lock internal ImmutableList<(Transform, Transform)> RecordedParents; internal ImmutableList<(Transform, Transform)> Transforms; - internal ArmatureLockJob(ImmutableList<(Transform, Transform)> transforms, Action dispose, Action update) + internal ISegment Segment { get; private set; } + + internal ArmatureLockJob(ISegment Segment, ImmutableList<(Transform, Transform)> transforms, Action dispose, + Action update) { + this.Segment = Segment; Transforms = transforms; RecordedParents = transforms.Select(((tuple, _) => (tuple.Item1.parent, tuple.Item2.parent))) .ToImmutableList(); diff --git a/Runtime/ArmatureAwase/ArmatureLockJobAccessor.cs b/Runtime/ArmatureAwase/ArmatureLockJobAccessor.cs index df282bdd..047a0093 100644 --- a/Runtime/ArmatureAwase/ArmatureLockJobAccessor.cs +++ b/Runtime/ArmatureAwase/ArmatureLockJobAccessor.cs @@ -17,45 +17,10 @@ namespace nadena.dev.modular_avatar.core.armature_lock /// internal struct ArmatureLockJobAccessor { - internal void Allocate(int nBones, int nWords) - { - _in_baseBone = new NativeArray(nBones, Allocator.Persistent); - _in_targetBone = new NativeArray(nBones, Allocator.Persistent); - _out_baseBone = new NativeArray(nBones, Allocator.Persistent); - _out_targetBone = new NativeArray(nBones, Allocator.Persistent); - _out_dirty_baseBone = new NativeArray(nBones, Allocator.Persistent); - _out_dirty_targetBone = new NativeArray(nBones, Allocator.Persistent); - _boneToJobIndex = new NativeArray(nBones, Allocator.Persistent); - _abortFlag = new NativeArray(nWords, Allocator.Persistent); - _didAnyWriteFlag = new NativeArray(nWords, Allocator.Persistent); - } - - internal void Destroy() - { - if (_in_baseBone.IsCreated) _in_baseBone.Dispose(); - _in_baseBone = default; - if (_in_targetBone.IsCreated) _in_targetBone.Dispose(); - _in_targetBone = default; - if (_out_baseBone.IsCreated) _out_baseBone.Dispose(); - _out_baseBone = default; - if (_out_targetBone.IsCreated) _out_targetBone.Dispose(); - _out_targetBone = default; - if (_out_dirty_baseBone.IsCreated) _out_dirty_baseBone.Dispose(); - _out_dirty_baseBone = default; - if (_out_dirty_targetBone.IsCreated) _out_dirty_targetBone.Dispose(); - _out_dirty_targetBone = default; - if (_boneToJobIndex.IsCreated) _boneToJobIndex.Dispose(); - _boneToJobIndex = default; - if (_abortFlag.IsCreated) _abortFlag.Dispose(); - _abortFlag = default; - if (_didAnyWriteFlag.IsCreated) _didAnyWriteFlag.Dispose(); - _didAnyWriteFlag = default; - } - /// /// Initial transform states /// - public NativeArray _in_baseBone, _in_targetBone; + public NativeArray _in_baseBone, _in_targetBone, _in_baseParentBone, _in_targetParentBone; /// /// Transform states to write out (if _out_dirty is set) @@ -65,8 +30,14 @@ namespace nadena.dev.modular_avatar.core.armature_lock /// /// Flags indicating whether the given bone should be written back to its transform /// - public NativeArray _out_dirty_baseBone, _out_dirty_targetBone; + public NativeArray _out_dirty_baseBone, _out_dirty_targetBone; + /// + /// Indicates whether this bone index is associated with any job at all. + /// + [FormerlySerializedAs("_in_boneIsValid")] + public NativeArray _in_boneInUse; + /// /// Indexed by the job index (via _boneToJobIndex). If set to a nonzero value, none of the bones in this /// particular job (e.g. a single MergeArmature component) will be committed. @@ -75,7 +46,7 @@ namespace nadena.dev.modular_avatar.core.armature_lock /// shouldn't read this value. /// [NativeDisableContainerSafetyRestriction] [NativeDisableParallelForRestriction] - public NativeArray _abortFlag; + public NativeArray _abortFlag; /// /// Indexed by the job index (via _boneToJobIndex). Should be set to a nonzero value when any bone in the job @@ -85,7 +56,7 @@ namespace nadena.dev.modular_avatar.core.armature_lock /// shouldn't read this value. /// [NativeDisableContainerSafetyRestriction] [NativeDisableParallelForRestriction] - public NativeArray _didAnyWriteFlag; + public NativeArray _didAnyWriteFlag; /// /// Maps from bone index to job index. diff --git a/Runtime/ArmatureAwase/ArmatureLockOperator.cs b/Runtime/ArmatureAwase/ArmatureLockOperator.cs index ab41d13b..b4e113d1 100644 --- a/Runtime/ArmatureAwase/ArmatureLockOperator.cs +++ b/Runtime/ArmatureAwase/ArmatureLockOperator.cs @@ -22,20 +22,57 @@ namespace nadena.dev.modular_avatar.core.armature_lock internal static readonly T Instance = new T(); private static long LastHierarchyChange = 0; - private ArmatureLockJobAccessor _accessor; - private TransformAccessArray _baseBones, _targetBones; + private TransformAccessArray _baseBones, _baseParentBones, _targetBones, _targetParentBones; private int _commitFilter; private bool _isDisposed = false; - private bool _isInit = false, _isValid = false; - private ImmutableList _jobs = ImmutableList.Empty; private JobHandle _lastJob; - private List _requestedJobs = new List(); + private List _jobs = new List(); private long LastCheckedHierarchy = -1; + protected readonly NativeMemoryManager _memoryManager = new NativeMemoryManager(); + + private bool _transformAccessDirty = true; + private Transform[] _baseTransforms = Array.Empty(), _targetTransforms = Array.Empty(); + + private Transform[] _baseParentTransforms = Array.Empty(), + _targetParentTransforms = Array.Empty(); + + protected Transform[] BaseTransforms => _baseTransforms; + protected Transform[] TargetTransforms => _targetTransforms; + + // Managed by _memoryManager + private NativeArrayRef _in_baseBone, _in_targetBone, _out_baseBone, _out_targetBone; + private NativeArrayRef _in_baseParentBone, _in_targetParentBone; + + private NativeArrayRef _out_dirty_baseBone, _out_dirty_targetBone; + private NativeArrayRef _boneToJobIndex; + + // Not managed by _memoryManager (since they're not indexed by bone) + private NativeArray _abortFlag, _didAnyWriteFlag; + + private ArmatureLockJobAccessor GetAccessor() + { + return new ArmatureLockJobAccessor() + { + _in_baseBone = _in_baseBone, + _in_targetBone = _in_targetBone, + _in_baseParentBone = _in_baseParentBone, + _in_targetParentBone = _in_targetParentBone, + _out_baseBone = _out_baseBone, + _out_targetBone = _out_targetBone, + _out_dirty_baseBone = _out_dirty_baseBone, + _out_dirty_targetBone = _out_dirty_targetBone, + _abortFlag = _abortFlag, + _didAnyWriteFlag = _didAnyWriteFlag, + _boneToJobIndex = _boneToJobIndex, + _in_boneInUse = _memoryManager.InUseMask, + }; + } + static ArmatureLockOperator() { Instance = new T(); @@ -49,6 +86,18 @@ namespace nadena.dev.modular_avatar.core.armature_lock #if UNITY_EDITOR AssemblyReloadEvents.beforeAssemblyReload += () => DeferDestroy.DestroyImmediate(this); #endif + _memoryManager.OnSegmentMove += MoveTransforms; + + _in_baseBone = _memoryManager.CreateArray(); + _in_targetBone = _memoryManager.CreateArray(); + _out_baseBone = _memoryManager.CreateArray(); + _out_targetBone = _memoryManager.CreateArray(); + _in_baseParentBone = _memoryManager.CreateArray(); + _in_targetParentBone = _memoryManager.CreateArray(); + + _out_dirty_baseBone = _memoryManager.CreateArray(); + _out_dirty_targetBone = _memoryManager.CreateArray(); + _boneToJobIndex = _memoryManager.CreateArray(); } protected abstract bool WritesBaseBones { get; } @@ -57,14 +106,17 @@ namespace nadena.dev.modular_avatar.core.armature_lock { if (_isDisposed) return; _isDisposed = true; - - if (!_isInit) return; - + _lastJob.Complete(); - DeferDestroy.DeferDestroyObj(_baseBones); - DeferDestroy.DeferDestroyObj(_targetBones); + if (_baseBones.isCreated) DeferDestroy.DeferDestroyObj(_baseBones); + if (_targetBones.isCreated) DeferDestroy.DeferDestroyObj(_targetBones); + if (_baseParentBones.isCreated) DeferDestroy.DeferDestroyObj(_baseParentBones); + if (_targetParentBones.isCreated) DeferDestroy.DeferDestroyObj(_targetParentBones); DerivedDispose(); - _accessor.Destroy(); + + _memoryManager.Dispose(); + if (_abortFlag.IsCreated) _abortFlag.Dispose(); + if (_didAnyWriteFlag.IsCreated) _didAnyWriteFlag.Dispose(); } #if UNITY_EDITOR @@ -72,7 +124,8 @@ namespace nadena.dev.modular_avatar.core.armature_lock { EditorApplication.hierarchyChanged += () => { LastHierarchyChange += 1; }; UpdateLoopController.UpdateCallbacks += Instance.Update; - ArmatureLockConfig.instance.OnGlobalEnableChange += Instance.Invalidate; + // TODO: On global enable, reset all jobs to init state? + //ArmatureLockConfig.instance.OnGlobalEnableChange += Instance.Invalidate; EditorApplication.playModeStateChanged += (change) => { @@ -87,10 +140,10 @@ namespace nadena.dev.modular_avatar.core.armature_lock #endif /// - /// Initialize the lock operator with a particular list of transforms. + /// (Re-)initialize state for a single job /// /// - protected abstract void Reinit(List<(Transform, Transform)> transforms, List problems); + protected abstract bool SetupJob(ISegment segment); /// /// Computes the new positions and status words for a given range of bones. @@ -103,92 +156,113 @@ namespace nadena.dev.modular_avatar.core.armature_lock public ArmatureLockJob RegisterLock(IEnumerable<(Transform, Transform)> transforms) { + if (_isDisposed) throw new ObjectDisposedException("ArmatureLockOperator"); + + var immutableTransforms = transforms.ToImmutableList(); + + var segment = _memoryManager.Allocate(immutableTransforms.Count()); + ArmatureLockJob job = null; job = new ArmatureLockJob( - transforms.ToImmutableList(), + segment, + immutableTransforms, () => RemoveJob(job), () => UpdateSingle(job) ); - _requestedJobs.Add(job); - Invalidate(); + EnsureTransformCapacity(_memoryManager.AllocatedLength); + + for (int i = 0; i < job.Transforms.Count(); i++) + { + var (baseBone, mergeBone) = job.Transforms[i]; + _baseTransforms[i + segment.Offset] = baseBone; + _baseParentTransforms[i + segment.Offset] = baseBone.parent; + _targetTransforms[i + segment.Offset] = mergeBone; + _targetParentTransforms[i + segment.Offset] = mergeBone.parent; + } + + int jobIndex = _jobs.IndexOf(null); + if (jobIndex >= 0) + { + _jobs[jobIndex] = job; + } + else + { + jobIndex = _jobs.Count(); + _jobs.Add(job); + } + + EnsureJobFlagCapacity(); + + for (int i = 0; i < segment.Length; i++) + { + _boneToJobIndex.Array[segment.Offset + i] = jobIndex; + } + + _transformAccessDirty = true; + + bool ok = false; + try + { + ok = SetupJob(segment); + } + finally + { + if (!ok) + { + // Initial setup failed; roll things back + job.IsValid = false; + RemoveJob(job); + } + } return job; } - private void Invalidate() + private void RemoveJob(ArmatureLockJob job) { - _isValid = false; + int index = _jobs.IndexOf(job); + + if (index < 0) return; + + _jobs[index] = null; + + _memoryManager.Free(job.Segment); } - private void MaybeRevalidate() + private void EnsureJobFlagCapacity() { - if (!_isValid) - { - // Do an update to make sure all the old jobs are in sync first, before we reset our state. - if (_isInit) SingleUpdate(null); - Reset(); - } + if (_abortFlag.IsCreated && _abortFlag.Length >= _jobs.Count) return; + + var priorLength = _abortFlag.Length; + + if (_abortFlag.IsCreated) _abortFlag.Dispose(); + if (_didAnyWriteFlag.IsCreated) _didAnyWriteFlag.Dispose(); + + int targetSize = Math.Max(Math.Max(16, _jobs.Count), (int)(priorLength * 1.5f)); + _abortFlag = new NativeArray(targetSize, Allocator.Persistent); + _didAnyWriteFlag = new NativeArray(targetSize, Allocator.Persistent); } - private void Reset() + private void EnsureTransformCapacity(int targetLength) { - if (_isDisposed) return; + if (targetLength == _baseTransforms.Length) return; - _lastJob.Complete(); - - if (_isInit) - { - _accessor.Destroy(); - _baseBones.Dispose(); - _targetBones.Dispose(); - } - - _isInit = true; - - // TODO: toposort? - int[] boneToJobIndex = null; - - List problems = new List(); - do - { - var failed = problems.Select(p => _jobs[boneToJobIndex[p]]).Distinct().ToList(); - foreach (var job in failed) - { - job.IsValid = false; - _requestedJobs.Remove(job); - } - - problems.Clear(); - - _jobs = _requestedJobs.ToImmutableList(); - - _accessor.Destroy(); - if (_baseBones.isCreated) _baseBones.Dispose(); - if (_targetBones.isCreated) _targetBones.Dispose(); - - _baseBones = _targetBones = default; - - var bones = _jobs.SelectMany(j => j.Transforms).ToList(); - boneToJobIndex = _jobs.SelectMany((i, j) => Enumerable.Repeat(j, i.Transforms.Count)).ToArray(); - - var baseBones = bones.Select(t => t.Item1).ToArray(); - var targetBones = bones.Select(t => t.Item2).ToArray(); - - _accessor.Allocate( - bones.Count, - _jobs.Count - ); - - _baseBones = new TransformAccessArray(baseBones); - _targetBones = new TransformAccessArray(targetBones); - - Reinit(_jobs.SelectMany(j => j.Transforms).ToList(), problems); - } while (problems.Count > 0); - - _isValid = true; + Array.Resize(ref _baseTransforms, targetLength); + Array.Resize(ref _baseParentTransforms, targetLength); + Array.Resize(ref _targetTransforms, targetLength); + Array.Resize(ref _targetParentTransforms, targetLength); } + private void MoveTransforms(int oldoffset, int newoffset, int length) + { + Array.Copy(_baseTransforms, oldoffset, _baseTransforms, newoffset, length); + Array.Copy(_baseParentTransforms, oldoffset, _baseParentTransforms, newoffset, length); + Array.Copy(_targetTransforms, oldoffset, _targetTransforms, newoffset, length); + Array.Copy(_targetParentTransforms, oldoffset, _targetParentTransforms, newoffset, length); + _transformAccessDirty = true; + } + public void Update() { InternalUpdate(); @@ -206,8 +280,6 @@ namespace nadena.dev.modular_avatar.core.armature_lock { if (_isDisposed) return; - MaybeRevalidate(); - SingleUpdate(jobIndex); } @@ -216,19 +288,54 @@ namespace nadena.dev.modular_avatar.core.armature_lock private void SingleUpdate(int? jobIndex) { - if (!_isInit || _jobs.Count == 0) return; + if (_jobs.Count == 0) return; + + if (_isDisposed) return; Profiler.BeginSample("InternalUpdate"); _lastJob.Complete(); + EnsureJobFlagCapacity(); + + if (_transformAccessDirty) + { + Profiler.BeginSample("RecreateTransformAccess"); + + if (_baseBones.isCreated && _baseBones.length == _baseTransforms.Length) + { + _baseBones.SetTransforms(_baseTransforms); + _baseParentBones.SetTransforms(_baseParentTransforms); + _targetBones.SetTransforms(_targetTransforms); + _targetParentBones.SetTransforms(_targetParentTransforms); + } + else + { + if (_baseBones.isCreated) _baseBones.Dispose(); + if (_targetBones.isCreated) _targetBones.Dispose(); + if (_baseParentBones.isCreated) _baseParentBones.Dispose(); + if (_targetParentBones.isCreated) _targetParentBones.Dispose(); + + _baseBones = new TransformAccessArray(_baseTransforms); + _baseParentBones = new TransformAccessArray(_baseParentTransforms); + _targetBones = new TransformAccessArray(_targetTransforms); + _targetParentBones = new TransformAccessArray(_targetParentTransforms); + } + + _transformAccessDirty = false; + + Profiler.EndSample(); + } + + var accessor = GetAccessor(); + for (int i = 0; i < _jobs.Count; i++) { - _accessor._abortFlag[i] = 0; - _accessor._didAnyWriteFlag[i] = 0; + accessor._abortFlag[i] = (_jobs[i] == null) || !_jobs[i].IsValid; + accessor._didAnyWriteFlag[i] = false; } _lastJob = ReadTransforms(jobIndex); - _lastJob = Compute(_accessor, jobIndex, _lastJob); + _lastJob = Compute(accessor, jobIndex, _lastJob); if (LastCheckedHierarchy != LastHierarchyChange) { @@ -245,10 +352,9 @@ namespace nadena.dev.modular_avatar.core.armature_lock var job = _jobs[_nextCheckIndex % _jobs.Count]; _nextCheckIndex = (1 + _nextCheckIndex) % _jobs.Count; - if (job.HierarchyChanged) + if (job != null && job.HierarchyChanged) { job.IsValid = false; - Invalidate(); } } while (_nextCheckIndex != startCheckIndex && !_lastJob.IsCompleted); @@ -269,19 +375,21 @@ namespace nadena.dev.modular_avatar.core.armature_lock bool anyDirty = false; for (int job = 0; job < _jobs.Count; job++) { + if (accessor._abortFlag[job]) continue; + int curBoneBase = boneBase; boneBase += _jobs[job].Transforms.Count; - if (_accessor._didAnyWriteFlag[job] == 0) continue; + if (!accessor._didAnyWriteFlag[job]) continue; for (int b = curBoneBase; b < boneBase; b++) { - if (_accessor._out_dirty_targetBone[b] != 0 || _accessor._out_dirty_baseBone[b] != 0) + if (accessor._out_dirty_targetBone[b] || accessor._out_dirty_baseBone[b]) { anyDirty = true; if (_jobs[job].BoneChanged(b - curBoneBase)) { - _accessor._abortFlag[job] = 1; + accessor._abortFlag[job] = true; _jobs[job].IsValid = false; break; } @@ -299,96 +407,139 @@ namespace nadena.dev.modular_avatar.core.armature_lock for (int i = 0; i < _jobs.Count; i++) { - if (_accessor._abortFlag[i] != 0) + if (_jobs[i] == null) continue; + + if (accessor._abortFlag[i]) { - Invalidate(); + _jobs[i].IsValid = false; } else { _jobs[i].MarkLoop(); + _jobs[i].WroteAny = accessor._didAnyWriteFlag[i]; } - - _jobs[i].WroteAny = _accessor._didAnyWriteFlag[i] != 0; - } - - if (!_isValid) - { - Reset(); } Profiler.EndSample(); } - private void RemoveJob(ArmatureLockJob job) + protected virtual void DerivedDispose() { - if (_requestedJobs.Remove(job)) Invalidate(); + // default no-op } - protected abstract void DerivedDispose(); - #region Job logic + [BurstCompile] + struct CopyTransformState : IJobParallelFor + { + [ReadOnly] public NativeArray _in; + [WriteOnly] public NativeArray _out; + + public void Execute(int index) + { + _out[index] = _in[index]; + } + } + [BurstCompile] struct ReadTransformsJob : IJobParallelForTransform { - public NativeArray _bone; - public NativeArray _bone2; + [WriteOnly] public NativeArray _bone; [ReadOnly] public NativeArray _boneToJobIndex; + [ReadOnly] public NativeArray _boneInUse; - [NativeDisableContainerSafetyRestriction] [NativeDisableParallelForRestriction] - public NativeArray _abortFlag; + [NativeDisableContainerSafetyRestriction] [NativeDisableParallelForRestriction] [WriteOnly] + public NativeArray _abortFlag; [BurstCompile] public void Execute(int index, TransformAccess transform) { + if (!_boneInUse[index]) return; + #if UNITY_2021_1_OR_NEWER if (!transform.isValid) { - _abortFlag[_boneToJobIndex[index]] = 1; + _abortFlag[_boneToJobIndex[index]] = true; return; } #endif - _bone[index] = _bone2[index] = new TransformState + _bone[index] = new TransformState { localPosition = transform.localPosition, localRotation = transform.localRotation, - localScale = transform.localScale + localScale = transform.localScale, + localToWorldMatrix = transform.localToWorldMatrix, }; } } JobHandle ReadTransforms(int? jobIndex) { + var accessor = GetAccessor(); + var baseRead = new ReadTransformsJob() { - _bone = _accessor._in_baseBone, - _bone2 = _accessor._out_baseBone, - _boneToJobIndex = _accessor._boneToJobIndex, - _abortFlag = _accessor._abortFlag + _bone = accessor._in_baseBone, + _boneToJobIndex = accessor._boneToJobIndex, + _abortFlag = accessor._abortFlag, + _boneInUse = accessor._in_boneInUse, }.ScheduleReadOnly(_baseBones, 32); + baseRead = new CopyTransformState() + { + _in = accessor._in_baseBone, + _out = accessor._out_baseBone + }.Schedule(accessor._in_baseBone.Length, 32, baseRead); + var targetRead = new ReadTransformsJob() { - _bone = _accessor._in_targetBone, - _bone2 = _accessor._out_targetBone, - _boneToJobIndex = _accessor._boneToJobIndex, - _abortFlag = _accessor._abortFlag - }.ScheduleReadOnly(_targetBones, 32, baseRead); + _bone = accessor._in_targetBone, + _boneToJobIndex = accessor._boneToJobIndex, + _abortFlag = accessor._abortFlag, + _boneInUse = accessor._in_boneInUse, + }.ScheduleReadOnly(_targetBones, 32); - return JobHandle.CombineDependencies(baseRead, targetRead); + targetRead = new CopyTransformState() + { + _in = accessor._in_targetBone, + _out = accessor._out_targetBone + }.Schedule(accessor._in_targetBone.Length, 32, targetRead); + + var baseParentRead = new ReadTransformsJob() + { + _bone = accessor._in_baseParentBone, + _boneToJobIndex = accessor._boneToJobIndex, + _abortFlag = accessor._abortFlag, + _boneInUse = accessor._in_boneInUse, + }.ScheduleReadOnly(_baseParentBones, 32); + + var targetParentRead = new ReadTransformsJob() + { + _bone = accessor._in_targetParentBone, + _boneToJobIndex = accessor._boneToJobIndex, + _abortFlag = accessor._abortFlag, + _boneInUse = accessor._in_boneInUse, + }.ScheduleReadOnly(_targetParentBones, 32); + + return JobHandle.CombineDependencies( + JobHandle.CombineDependencies(baseRead, targetRead), + JobHandle.CombineDependencies(baseParentRead, targetParentRead) + ); } [BurstCompile] struct CommitTransformsJob : IJobParallelForTransform { [ReadOnly] public NativeArray _boneState; - [ReadOnly] public NativeArray _dirtyBoneFlag; + [ReadOnly] public NativeArray _dirtyBoneFlag; [ReadOnly] public NativeArray _boneToJobIndex; + [ReadOnly] public NativeArray _boneInUse; [NativeDisableContainerSafetyRestriction] [NativeDisableParallelForRestriction] [ReadOnly] - public NativeArray _abortFlag; + public NativeArray _abortFlag; public int jobIndexFilter; @@ -398,11 +549,12 @@ namespace nadena.dev.modular_avatar.core.armature_lock #if UNITY_2021_1_OR_NEWER if (!transform.isValid) return; #endif + if (!_boneInUse[index]) return; var jobIndex = _boneToJobIndex[index]; if (jobIndexFilter >= 0 && jobIndex != jobIndexFilter) return; - if (_abortFlag[jobIndex] != 0) return; - if (_dirtyBoneFlag[index] == 0) return; + if (_abortFlag[jobIndex]) return; + if (!_dirtyBoneFlag[index]) return; transform.localPosition = _boneState[index].localPosition; transform.localRotation = _boneState[index].localRotation; @@ -412,24 +564,28 @@ namespace nadena.dev.modular_avatar.core.armature_lock JobHandle CommitTransforms(int? jobIndex, JobHandle prior) { + var accessor = GetAccessor(); + JobHandle job = new CommitTransformsJob() { - _boneState = _accessor._out_targetBone, - _dirtyBoneFlag = _accessor._out_dirty_targetBone, - _boneToJobIndex = _accessor._boneToJobIndex, - _abortFlag = _accessor._abortFlag, - jobIndexFilter = jobIndex ?? -1 + _boneState = accessor._out_targetBone, + _dirtyBoneFlag = accessor._out_dirty_targetBone, + _boneToJobIndex = accessor._boneToJobIndex, + _abortFlag = accessor._abortFlag, + jobIndexFilter = jobIndex ?? -1, + _boneInUse = accessor._in_boneInUse, }.Schedule(_targetBones, prior); if (WritesBaseBones) { var job2 = new CommitTransformsJob() { - _boneState = _accessor._out_baseBone, - _dirtyBoneFlag = _accessor._out_dirty_baseBone, - _boneToJobIndex = _accessor._boneToJobIndex, - _abortFlag = _accessor._abortFlag, - jobIndexFilter = jobIndex ?? -1 + _boneState = accessor._out_baseBone, + _dirtyBoneFlag = accessor._out_dirty_baseBone, + _boneToJobIndex = accessor._boneToJobIndex, + _abortFlag = accessor._abortFlag, + jobIndexFilter = jobIndex ?? -1, + _boneInUse = accessor._in_boneInUse, }.Schedule(_baseBones, prior); return JobHandle.CombineDependencies(job, job2); diff --git a/Runtime/ArmatureAwase/BidirectionalArmatureLock.cs b/Runtime/ArmatureAwase/BidirectionalArmatureLock.cs index cf6462db..6ed85105 100644 --- a/Runtime/ArmatureAwase/BidirectionalArmatureLock.cs +++ b/Runtime/ArmatureAwase/BidirectionalArmatureLock.cs @@ -1,11 +1,9 @@ #region -using System.Collections.Generic; using Unity.Burst; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using Unity.Jobs; -using UnityEngine; #endregion @@ -13,25 +11,31 @@ namespace nadena.dev.modular_avatar.core.armature_lock { internal class BidirectionalArmatureLockOperator : ArmatureLockOperator { - private NativeArray SavedState; + private NativeArrayRef SavedState; protected override bool WritesBaseBones => true; - protected override void Reinit(List<(Transform, Transform)> transforms, List problems) + public BidirectionalArmatureLockOperator() { - if (SavedState.IsCreated) SavedState.Dispose(); + SavedState = _memoryManager.CreateArray(); + } - SavedState = new NativeArray(transforms.Count, Allocator.Persistent); - - for (int i = 0; i < transforms.Count; i++) + protected override bool SetupJob(ISegment segment) + { + for (int i = 0; i < segment.Length; i++) { - var (baseBone, mergeBone) = transforms[i]; - SavedState[i] = TransformState.FromTransform(mergeBone); + int bone = i + segment.Offset; - if (TransformState.Differs(TransformState.FromTransform(baseBone), SavedState[i])) + var baseBone = BaseTransforms[bone]; + var targetBone = TargetTransforms[bone]; + + SavedState.Array[i] = TransformState.FromTransform(targetBone); + if (TransformState.Differs(TransformState.FromTransform(baseBone), SavedState.Array[i])) { - problems.Add(i); + return false; } } + + return true; } protected override JobHandle Compute(ArmatureLockJobAccessor accessor, int? jobIndex, JobHandle dependency) @@ -49,15 +53,12 @@ namespace nadena.dev.modular_avatar.core.armature_lock boneToJobIndex = accessor._boneToJobIndex, wroteAny = accessor._didAnyWriteFlag, + boneInUse = accessor._in_boneInUse, + singleJobIndex = jobIndex ?? -1 }.Schedule(accessor._in_baseBone.Length, 16, dependency); } - protected override void DerivedDispose() - { - SavedState.Dispose(); - } - [BurstCompile] private struct ComputeOperator : IJobParallelFor { @@ -67,15 +68,19 @@ namespace nadena.dev.modular_avatar.core.armature_lock public NativeArray SavedState; - [WriteOnly] public NativeArray baseDirty, mergeDirty; + [WriteOnly] public NativeArray baseDirty, mergeDirty; [ReadOnly] public NativeArray boneToJobIndex; [NativeDisableContainerSafetyRestriction] [NativeDisableParallelForRestriction] [WriteOnly] - public NativeArray wroteAny; + public NativeArray wroteAny; + + [ReadOnly] public NativeArray boneInUse; [BurstCompile] public void Execute(int index) { + if (!boneInUse[index]) return; + var jobIndex = boneToJobIndex[index]; if (singleJobIndex != -1 && jobIndex != singleJobIndex) return; @@ -86,21 +91,21 @@ namespace nadena.dev.modular_avatar.core.armature_lock if (TransformState.Differs(saved, mergeBone)) { - baseDirty[index] = 1; - mergeDirty[index] = 0; + baseDirty[index] = true; + mergeDirty[index] = false; SavedState[index] = base_out[index] = merge_in[index]; - wroteAny[jobIndex] = 1; + wroteAny[jobIndex] = true; } else if (TransformState.Differs(saved, baseBone)) { - mergeDirty[index] = 1; - baseDirty[index] = 0; + mergeDirty[index] = true; + baseDirty[index] = false; SavedState[index] = merge_out[index] = base_in[index]; - wroteAny[jobIndex] = 1; + wroteAny[jobIndex] = true; } } } diff --git a/Runtime/ArmatureAwase/NativeMemoryManager.cs b/Runtime/ArmatureAwase/NativeMemoryManager.cs new file mode 100644 index 00000000..5f38f96e --- /dev/null +++ b/Runtime/ArmatureAwase/NativeMemoryManager.cs @@ -0,0 +1,219 @@ +#region + +using System; +using System.Collections.Generic; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; + +#endregion + +namespace nadena.dev.modular_avatar.core.armature_lock +{ + internal class NativeArrayRef : INativeArrayRef where T : unmanaged + { + internal NativeArray Array; + + public static implicit operator NativeArray(NativeArrayRef arrayRef) => arrayRef.Array; + + public void Dispose() + { + Array.Dispose(); + } + + public void Resize(int n) + { + if (Array.Length == n) return; + + var newArray = new NativeArray(n, Allocator.Persistent); + + unsafe + { + UnsafeUtility.MemCpy(newArray.GetUnsafePtr(), Array.GetUnsafePtr(), + Math.Min(n, Array.Length) * UnsafeUtility.SizeOf()); + } + + /* + for (int i = 0; i < Math.Min(n, Array.Length); i++) + { + newArray[i] = Array[i]; + }*/ + + Array.Dispose(); + + Array = newArray; + } + + public void MemMove(int srcOffset, int dstOffset, int count) + { + if (srcOffset < 0 || dstOffset < 0 + || count < 0 + || srcOffset + count > Array.Length + || dstOffset + count > Array.Length + ) + { + throw new ArgumentOutOfRangeException(); + } + + + unsafe + { + UnsafeUtility.MemMove(((T*)Array.GetUnsafePtr()) + dstOffset, ((T*)Array.GetUnsafePtr()) + srcOffset, + count * UnsafeUtility.SizeOf()); + } + + /* + // We assume dstOffset < srcOffset + for (int i = 0; i < count; i++) + { + Array[dstOffset + i] = Array[srcOffset + i]; + }*/ + } + } + + internal interface INativeArrayRef : IDisposable + { + void Resize(int n); + void MemMove(int srcOffset, int dstOffset, int count); + } + + internal class NativeMemoryManager : IDisposable + { + private List arrays = new List(); + public NativeArrayRef InUseMask { get; private set; } + + public event AllocationMap.DefragmentCallback OnSegmentMove; + + private int _allocatedLength = 1; + public int AllocatedLength => _allocatedLength; + private AllocationMap _allocationMap = new AllocationMap(); + private bool _isDisposed; + + public NativeMemoryManager() + { + // Bootstrap + InUseMask = new NativeArrayRef() + { + Array = new NativeArray(1, Allocator.Persistent) + }; + arrays.Add(InUseMask); + } + + public NativeArrayRef CreateArray() where T : unmanaged + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(NativeMemoryManager)); + } + + var arrayRef = new NativeArrayRef() + { + Array = new NativeArray(_allocatedLength, Allocator.Persistent) + }; + + arrays.Add(arrayRef); + + return arrayRef; + } + + public void Dispose() + { + if (_isDisposed) return; + + _isDisposed = true; + + foreach (var array in arrays) + { + array.Dispose(); + } + } + + void SetInUseMask(int offset, int length, bool value) + { + unsafe + { + UnsafeUtility.MemSet((byte*)InUseMask.Array.GetUnsafePtr() + offset, value ? (byte)1 : (byte)0, length); + } + + /* + for (int i = 0; i < length; i++) + { + try + { + InUseMask.Array[offset + i] = value; + } + catch (Exception e) + { + throw; + } + }*/ + } + + public ISegment Allocate(int requested) + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(NativeMemoryManager)); + } + + var segment = _allocationMap.Allocate(requested); + + if (segment.Offset + segment.Length > _allocatedLength) + { + // Try defragmenting first. + + // First, deallocate that segment we just created, since it'll be beyond the end of the array and break + // the memmove operations we'll be doing. + _allocationMap.FreeSegment(segment); + + Defragment(); + + segment = _allocationMap.Allocate(requested); + } + + if (segment.Offset + segment.Length > _allocatedLength) + { + // We're still using more space than we have allocated, so allocate some more memory now + ResizeNativeArrays(segment.Offset + segment.Length); + } + + SetInUseMask(segment.Offset, segment.Length, true); + + return segment; + } + + private void Defragment() + { + _allocationMap.Defragment((src, dst, length) => + { + foreach (var array in arrays) + { + array.MemMove(src, dst, length); + } + + OnSegmentMove?.Invoke(src, dst, length); + }); + } + + + private void ResizeNativeArrays(int minimumLength) + { + int targetLength = Math.Max((int)(1.5 * _allocatedLength), minimumLength); + + foreach (var array in arrays) + { + array.Resize(targetLength); + } + + SetInUseMask(_allocatedLength, targetLength - _allocatedLength, false); + _allocatedLength = targetLength; + } + + public void Free(ISegment segment) + { + if (_isDisposed) return; + + _allocationMap.FreeSegment(segment); + SetInUseMask(segment.Offset, segment.Length, false); + } + } +} \ No newline at end of file diff --git a/Runtime/ArmatureAwase/NativeMemoryManager.cs.meta b/Runtime/ArmatureAwase/NativeMemoryManager.cs.meta new file mode 100644 index 00000000..a5862cb3 --- /dev/null +++ b/Runtime/ArmatureAwase/NativeMemoryManager.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b6602f940b944dc2b01f5f977fbc16a9 +timeCreated: 1709526526 \ No newline at end of file diff --git a/Runtime/ArmatureAwase/OnewayArmatureLock.cs b/Runtime/ArmatureAwase/OnewayArmatureLock.cs index 0e279ff3..498533a3 100644 --- a/Runtime/ArmatureAwase/OnewayArmatureLock.cs +++ b/Runtime/ArmatureAwase/OnewayArmatureLock.cs @@ -15,54 +15,102 @@ namespace nadena.dev.modular_avatar.core.armature_lock { internal class OnewayArmatureLockOperator : ArmatureLockOperator { - private Transform[] _baseBones, _mergeBones, _baseParentBones, _mergeParentBones; - private NativeArray _boneStaticData; - public NativeArray _mergeSavedState; + private NativeArrayRef _boneStaticData; + private NativeArrayRef _mergeSavedState; private List<(Transform, Transform)> _transforms; protected override bool WritesBaseBones => false; - protected override void Reinit(List<(Transform, Transform)> transforms, List problems) + public OnewayArmatureLockOperator() { - if (_boneStaticData.IsCreated) _boneStaticData.Dispose(); - if (_mergeSavedState.IsCreated) _mergeSavedState.Dispose(); - - _transforms = transforms; + _boneStaticData = _memoryManager.CreateArray(); + _mergeSavedState = _memoryManager.CreateArray(); + } - _boneStaticData = new NativeArray(transforms.Count, Allocator.Persistent); - - _baseBones = new Transform[_transforms.Count]; - _mergeBones = new Transform[_transforms.Count]; - _baseParentBones = new Transform[_transforms.Count]; - _mergeParentBones = new Transform[_transforms.Count]; - _mergeSavedState = new NativeArray(_transforms.Count, Allocator.Persistent); - - for (int i = 0; i < transforms.Count; i++) + protected override bool SetupJob(ISegment segment) + { + for (int i = 0; i < segment.Length; i++) { - var (baseBone, mergeBone) = transforms[i]; - var mergeParent = mergeBone.parent; - var baseParent = baseBone.parent; + int bone = segment.Offset + i; - if (mergeParent == null || baseParent == null) + var baseState = TransformState.FromTransform(BaseTransforms[bone]); + var mergeState = TransformState.FromTransform(TargetTransforms[bone]); + var baseParentState = TransformState.FromTransform(BaseTransforms[bone].parent); + var mergeParentState = TransformState.FromTransform(TargetTransforms[bone].parent); + + if (!new ComputePosition().SyncState(out var staticData, baseState, mergeState, baseParentState, + mergeParentState)) { - problems.Add(i); - continue; + return false; } - if (SmallScale(mergeParent.localScale) || SmallScale(mergeBone.localScale) || - SmallScale(baseBone.localScale)) + _boneStaticData.Array[bone] = staticData; + _mergeSavedState.Array[bone] = mergeState; + } + + return true; + } + + protected override JobHandle Compute(ArmatureLockJobAccessor accessor, int? jobIndex, JobHandle dependency) + { + return new ComputePosition() + { + _baseState = accessor._in_baseBone, + _mergeState = accessor._in_targetBone, + _baseParentState = accessor._in_baseParentBone, + _mergeParentState = accessor._in_baseParentBone, + _mergeSavedState = _mergeSavedState, + _boneStatic = _boneStaticData, + _fault = accessor._abortFlag, + _wroteAny = accessor._didAnyWriteFlag, + _wroteBone = accessor._out_dirty_targetBone, + jobIndexLimit = jobIndex ?? -1, + _boneToJobIndex = accessor._boneToJobIndex, + _outputState = accessor._out_targetBone, + _boneInUse = accessor._in_boneInUse, + }.Schedule(accessor._in_baseBone.Length, 32, dependency); + } + + struct BoneStaticData + { + public Matrix4x4 _mat_l, _mat_r; + } + + + [BurstCompile] + struct ComputePosition : IJobParallelFor + { + public NativeArray _boneStatic; + + [ReadOnly] public NativeArray _mergeState; + [ReadOnly] public NativeArray _baseState; + + [ReadOnly] public NativeArray _mergeParentState; + [ReadOnly] public NativeArray _baseParentState; + + public NativeArray _mergeSavedState; + public NativeArray _outputState; + public NativeArray _wroteBone; + + public int jobIndexLimit; + + [ReadOnly] public NativeArray _boneToJobIndex; + [ReadOnly] public NativeArray _boneInUse; + + // job indexed + [NativeDisableContainerSafetyRestriction] [NativeDisableParallelForRestriction] + public NativeArray _fault, _wroteAny; + + public bool SyncState(out BoneStaticData result, TransformState baseState, TransformState mergeState, + TransformState baseParentState, TransformState mergeParentState) + { + if (SmallScale(mergeParentState.localScale) || SmallScale(mergeState.localScale) || + SmallScale(baseState.localScale)) { - problems.Add(i); - continue; + result = default; + return false; } - _baseBones[i] = baseBone; - _mergeBones[i] = mergeBone; - _baseParentBones[i] = baseParent; - _mergeParentBones[i] = mergeParent; - - _mergeSavedState[i] = TransformState.FromTransform(mergeBone); - // We want to emulate the hierarchy: // baseParent // - baseBone @@ -76,80 +124,35 @@ namespace nadena.dev.modular_avatar.core.armature_lock // baseBone -> baseParent affine transform? // First, relative to baseBone, what is the local affine transform of mergeBone? - var mat_l = baseBone.worldToLocalMatrix * mergeBone.localToWorldMatrix; + var mat_l = baseState.worldToLocalMatrix * mergeState.localToWorldMatrix; // We also find parent -> mergeParent - var mat_r = mergeParent.worldToLocalMatrix * baseParent.localToWorldMatrix; + var mat_r = mergeParentState.worldToLocalMatrix * baseParentState.localToWorldMatrix; // Now we can multiply: // (baseParent -> mergeParent) * (baseBone -> baseParent) * (mergeBone -> baseBone) // = (baseParent -> mergeParent) * (mergeBone -> baseParent) // = (mergeBone -> mergeParent) - _boneStaticData[i] = new BoneStaticData() + result = new BoneStaticData() { _mat_l = mat_r, _mat_r = mat_l }; + + return true; } - } - private bool SmallScale(Vector3 scale) - { - var epsilon = 0.000001f; - - return (scale.x < epsilon || scale.y < epsilon || scale.z < epsilon); - } - - protected override JobHandle Compute(ArmatureLockJobAccessor accessor, int? jobIndex, JobHandle dependency) - { - return new ComputePosition() + private bool SmallScale(Vector3 scale) { - _baseState = accessor._in_baseBone, - _mergeState = accessor._in_targetBone, - _mergeSavedState = _mergeSavedState, - _boneStatic = _boneStaticData, - _fault = accessor._abortFlag, - _wroteAny = accessor._didAnyWriteFlag, - _wroteBone = accessor._out_dirty_targetBone, - jobIndexLimit = jobIndex ?? -1, - _boneToJobIndex = accessor._boneToJobIndex, - _outputState = accessor._out_targetBone, - }.Schedule(accessor._in_baseBone.Length, 32, dependency); - } - - protected override void DerivedDispose() - { - if (_boneStaticData.IsCreated) _boneStaticData.Dispose(); - if (_mergeSavedState.IsCreated) _mergeSavedState.Dispose(); - } - - struct BoneStaticData - { - public Matrix4x4 _mat_l, _mat_r; - } - - - [BurstCompile] - struct ComputePosition : IJobParallelFor - { - [ReadOnly] public NativeArray _boneStatic; - - [ReadOnly] public NativeArray _mergeState; - [ReadOnly] public NativeArray _baseState; - - public NativeArray _mergeSavedState; - public NativeArray _outputState; - public NativeArray _wroteBone; - - public int jobIndexLimit; - - [ReadOnly] public NativeArray _boneToJobIndex; - - // job indexed - [NativeDisableContainerSafetyRestriction] [NativeDisableParallelForRestriction] - public NativeArray _fault, _wroteAny; + var epsilon = 0.000001f; + return (scale.x < epsilon || scale.y < epsilon || scale.z < epsilon); + } + public void Execute(int index) { + if (!_boneInUse[index]) return; + _wroteBone[index] = false; + var jobIndex = _boneToJobIndex[index]; if (jobIndexLimit >= 0 && jobIndex >= jobIndexLimit) return; @@ -165,7 +168,21 @@ namespace nadena.dev.modular_avatar.core.armature_lock if (TransformState.Differs(mergeSaved, mergeState)) { - _fault[jobIndex] = 1; + // Reinitialize our transform matrices here, so we can continue to track on the next frame + if (SyncState(out var state, + _baseState[index], + _mergeState[index], + _baseParentState[index], + _mergeParentState[index])) + { + _boneStatic[index] = state; + } + else + { + _fault[jobIndex] = true; + } + + return; } var relTransform = boneStatic._mat_l * Matrix4x4.TRS(basePos, baseRot, baseScale) * boneStatic._mat_r; @@ -183,8 +200,8 @@ namespace nadena.dev.modular_avatar.core.armature_lock if (TransformState.Differs(mergeSaved, newState)) { - _wroteAny[jobIndex] = 1; - _wroteBone[index] = 1; + _wroteAny[jobIndex] = true; + _wroteBone[index] = true; _mergeSavedState[index] = newState; _outputState[index] = newState; } diff --git a/Runtime/ArmatureAwase/TransformState.cs b/Runtime/ArmatureAwase/TransformState.cs index 0f340f18..92194942 100644 --- a/Runtime/ArmatureAwase/TransformState.cs +++ b/Runtime/ArmatureAwase/TransformState.cs @@ -19,13 +19,19 @@ namespace nadena.dev.modular_avatar.core.armature_lock public Quaternion localRotation; public Vector3 localScale; + // Read on FromTransform, not written back in ToTransform + public Matrix4x4 localToWorldMatrix; + + public Matrix4x4 worldToLocalMatrix => localToWorldMatrix.inverse; + internal static TransformState FromTransform(Transform mergeBone) { return new TransformState { localPosition = mergeBone.localPosition, localRotation = mergeBone.localRotation, - localScale = mergeBone.localScale + localScale = mergeBone.localScale, + localToWorldMatrix = mergeBone.localToWorldMatrix, }; } diff --git a/UnitTests~/ArmatureAwase.meta b/UnitTests~/ArmatureAwase.meta new file mode 100644 index 00000000..9e05c96a --- /dev/null +++ b/UnitTests~/ArmatureAwase.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1efcc3ec2e0142c880fb6d44f651e239 +timeCreated: 1709536475 \ No newline at end of file diff --git a/UnitTests~/ArmatureAwase/AllocationMapTest.cs b/UnitTests~/ArmatureAwase/AllocationMapTest.cs new file mode 100644 index 00000000..a5104b38 --- /dev/null +++ b/UnitTests~/ArmatureAwase/AllocationMapTest.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using nadena.dev.modular_avatar.core.armature_lock; +using NUnit.Framework; + +namespace UnitTests.ArmatureAwase +{ + public class AllocationMapTest + { + [Test] + public void Test() + { + AllocationMap map = new AllocationMap(); + + ISegment s1 = map.Allocate(10); + AssertSegment(s1, 0, 10, true); + + ISegment s2 = map.Allocate(5); + AssertSegment(s2, 10, 5, true); + + map.FreeSegment(s1); + s1 = map.Allocate(5); + AssertSegment(s1, 0, 5, true); + + var s1a = map.Allocate(3); + AssertSegment(s1a, 5, 3, true); + + var s3 = map.Allocate(3); + AssertSegment(s3, 15, 3, true); + + List<(ISegment, int, int, int)> segmentDefrags = new List<(ISegment, int, int, int)>(); + List<(int, int, int)> globalDefrags = new List<(int, int, int)>(); + + s1.Defragment = (src, dst, length) => segmentDefrags.Add((s1, src, dst, length)); + s1a.Defragment = (src, dst, length) => segmentDefrags.Add((s1a, src, dst, length)); + s2.Defragment = (src, dst, length) => segmentDefrags.Add((s2, src, dst, length)); + s3.Defragment = (src, dst, length) => segmentDefrags.Add((s3, src, dst, length)); + + map.Defragment((src, dst, length) => globalDefrags.Add((src, dst, length))); + + Assert.AreEqual(segmentDefrags, new List<(ISegment, int, int, int)>() + { + (s2, 10, 8, 5), + (s3, 15, 13, 3), + }); + + Assert.AreEqual(globalDefrags, new List<(int, int, int)>() + { + (10, 8, 5), + (15, 13, 3), + }); + } + + [Test] + public void SegmentCoalescing() + { + var map = new AllocationMap(); + var s1 = map.Allocate(10); + var s2 = map.Allocate(10); + var s3 = map.Allocate(10); + + map.FreeSegment(s2); + map.FreeSegment(s1); + + var s4 = map.Allocate(20); + + AssertSegment(s4, 0, 20, true); + } + + private void AssertSegment(ISegment segment, int offset, int length, bool inUse) + { + var s = segment as AllocationMap.Segment; + + Assert.AreEqual(offset, segment.Offset); + Assert.AreEqual(length, segment.Length); + Assert.AreEqual(inUse, s._inUse); + } + } +} \ No newline at end of file diff --git a/UnitTests~/ArmatureAwase/AllocationMapTest.cs.meta b/UnitTests~/ArmatureAwase/AllocationMapTest.cs.meta new file mode 100644 index 00000000..c284f1b4 --- /dev/null +++ b/UnitTests~/ArmatureAwase/AllocationMapTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5d0c9fcd8a234b66b96dda10be362791 +timeCreated: 1709536483 \ No newline at end of file diff --git a/UnitTests~/ArmatureAwase/NativeMemoryManagerTest.cs b/UnitTests~/ArmatureAwase/NativeMemoryManagerTest.cs new file mode 100644 index 00000000..6593b911 --- /dev/null +++ b/UnitTests~/ArmatureAwase/NativeMemoryManagerTest.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using nadena.dev.modular_avatar.core.armature_lock; +using NUnit.Framework; +using Unity.Collections; + +namespace UnitTests.ArmatureAwase +{ + public class NativeMemoryManagerTest + { + [Test] + public void Test() + { + var mm = new NativeMemoryManager(); + var arr = mm.CreateArray(); + + var s1 = mm.Allocate(8); + SetRange(arr, s1, 101); + + var s2 = mm.Allocate(8); + SetRange(arr, s2, 102); + + mm.Free(s1); + AssertRange(mm.InUseMask, 0, 8, false); + AssertRange(mm.InUseMask, 8, 16, true); + AssertRange(mm.InUseMask, 16, -1, false); + + List<(int, int, int)> defragOps = new List<(int, int, int)>(); + mm.OnSegmentMove += (src, dst, length) => defragOps.Add((src, dst, length)); + var s3 = mm.Allocate(16); // Forces reallocation/defragment + Assert.AreEqual(s2.Offset, 0); + Assert.AreEqual(defragOps, new List<(int, int, int)>() + { + (8, 0, 8), + }); + SetRange(arr, s3, 103); + + AssertRange(arr, s2, 102); + + AssertRange(mm.InUseMask, s2, true); + AssertRange(mm.InUseMask, s3, true); + AssertRange(mm.InUseMask, s3.Offset, -1, false); + + mm.Dispose(); + + Assert.IsFalse(arr.Array.IsCreated); + } + + private void SetRange(NativeArray arr, ISegment segment, T value) where T : unmanaged + { + for (int i = 0; i < segment.Length; i++) + { + arr[i + segment.Offset] = value; + } + } + + private void AssertRange(NativeArray arr, ISegment segment, T value) where T : unmanaged + { + AssertRange(arr, segment.Offset, segment.Offset + segment.Length, value); + } + + private void AssertRange(NativeArray arr, int start, int end, T value) where T : unmanaged + { + for (int i = start; i < end; i++) + { + Assert.AreEqual(value, arr[i]); + } + } + } +} \ No newline at end of file diff --git a/UnitTests~/ArmatureAwase/NativeMemoryManagerTest.cs.meta b/UnitTests~/ArmatureAwase/NativeMemoryManagerTest.cs.meta new file mode 100644 index 00000000..a4b8de6b --- /dev/null +++ b/UnitTests~/ArmatureAwase/NativeMemoryManagerTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d371b34b4f1e45f6b945509d26f48cee +timeCreated: 1709536883 \ No newline at end of file From 42477aae45e5cbc9e458abee53f9a2c0d0eeb8f7 Mon Sep 17 00:00:00 2001 From: bd_ Date: Tue, 5 Mar 2024 19:13:28 +0900 Subject: [PATCH 4/4] 1.9.5-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 52cd87c7..271cfb74 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "nadena.dev.modular-avatar", "displayName": "Modular Avatar", - "version": "1.9.5-rc.0", + "version": "1.9.5-rc.1", "unity": "2019.4", "description": "A suite of tools for assembling your avatar out of reusable components", "author": {