diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Extensions/SerializeReferenceHelpers.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Extensions/SerializeReferenceHelpers.cs index 0078aeff..816fa88d 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Extensions/SerializeReferenceHelpers.cs +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Extensions/SerializeReferenceHelpers.cs @@ -940,21 +940,38 @@ public static bool HasSharedReference(SerializedProperty property) if (property.managedReferenceValue is null) return false; var id = property.managedReferenceId; - var shared = false; - var path = property.propertyPath; - TraverseManagedReferences(property.serializedObject, other => - { - if (other.propertyPath != path && other.managedReferenceId == id) - { - shared = true; - return true; - } + // GetHeight and Draw each ask this for every managed-reference field, every IMGUI repaint, so a naive + // per-property full-object walk is 2·N walks per frame. Instead the object's managed-reference id → use-count + // is built once per object per frame and reused: an id used by more than one field is aliased. + return GetReferenceIdCounts(property.serializedObject).TryGetValue(id, out var count) && count > 1; + } + // Per-object, per-frame memo of how many managed-reference fields carry each id. Built by a single full-object + // walk and shared across every HasSharedReference call in the same repaint (keyed by the SerializedObject and the + // current IMGUI frame), collapsing the 2·N walks GetHeight + Draw would otherwise do into one walk per object. + private static int _aliasFrame = -1; + private static SerializedObject _aliasSerializedObject; + private static readonly Dictionary AliasCounts = new(); + + private static Dictionary GetReferenceIdCounts(SerializedObject serializedObject) + { + var frame = Time.frameCount; + if (_aliasFrame == frame && ReferenceEquals(_aliasSerializedObject, serializedObject)) + return AliasCounts; + + AliasCounts.Clear(); + TraverseManagedReferences(serializedObject, other => + { + var id = other.managedReferenceId; + AliasCounts.TryGetValue(id, out var count); + AliasCounts[id] = count + 1; return false; }); - return shared; + _aliasFrame = frame; + _aliasSerializedObject = serializedObject; + return AliasCounts; } /// diff --git a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Extensions/SerializeReferenceRequiredGate.cs b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Extensions/SerializeReferenceRequiredGate.cs index ba991d12..714875c9 100644 --- a/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Extensions/SerializeReferenceRequiredGate.cs +++ b/Aspid.FastTools/Packages/tech.aspid.fasttools/Unity/Editor/Scripts/SerializeReferences/Extensions/SerializeReferenceRequiredGate.cs @@ -97,6 +97,11 @@ public static IReadOnlyList GetRequiredFields(Type type // Per-type memo for GetRequiredFields — the reflected field set is stable until a domain reload clears statics. private static readonly Dictionary> RequiredFieldCache = new(); + // Memoises ResolveFieldInfo by (target type, property path): the reflected field is stable for a given type and + // path until a domain reload clears statics, so IsViolation/TryGetRequired — called from both GetHeight and Draw + // every IMGUI repaint — reflect each path only once instead of re-walking it on every frame. + private static readonly Dictionary<(Type, string), FieldInfo> ResolvedFieldCache = new(); + // Walks the property path against the target object's type to find the declared field (which carries the // attribute). For a list/array element the field is the collection itself, matching PropertyDrawer.fieldInfo. private static FieldInfo ResolveFieldInfo(SerializedProperty property) @@ -104,8 +109,20 @@ private static FieldInfo ResolveFieldInfo(SerializedProperty property) var type = property.serializedObject?.targetObject?.GetType(); if (type is null) return null; + var cacheKey = (type, property.propertyPath); + if (ResolvedFieldCache.TryGetValue(cacheKey, out var cachedField)) return cachedField; + + var field = ResolveFieldInfoUncached(type, property.propertyPath); + ResolvedFieldCache[cacheKey] = field; + return field; + } + + private static FieldInfo ResolveFieldInfoUncached(Type targetType, string propertyPath) + { + var type = targetType; + // "_slots.Array.data[0]._weapon" -> "_slots[0]._weapon" - var path = property.propertyPath.Replace(".Array.data[", "["); + var path = propertyPath.Replace(".Array.data[", "["); FieldInfo field = null; foreach (var rawSegment in path.Split('.'))