Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<long, int> AliasCounts = new();

private static Dictionary<long, int> 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;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,15 +97,32 @@ public static IReadOnlyList<RequiredFieldDescriptor> GetRequiredFields(Type type
// Per-type memo for GetRequiredFields — the reflected field set is stable until a domain reload clears statics.
private static readonly Dictionary<Type, IReadOnlyList<RequiredFieldDescriptor>> 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)
{
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('.'))
Expand Down
Loading