using System; using System.Collections.Generic; using Unity.Burst; using Unity.Collections; using Unity.Jobs; using Unity.Mathematics; using UnityEngine.Jobs; using UnityEngine.Pool; using UnityEngine.Profiling; namespace UnityEngine.U2D.Animation { // This class is used to manage the transforms and their access for jobs. // The takes a list of transforms and creates a TransformAccessArray for them. // The TransformAccessArray is used to schedule jobs that require access to the transforms. // It can run 2 jobs internally, one to return localToWorld matrices and one to return worldToLocal matrices. internal class TransformAccessJob { public struct TransformData { public int transformIndex; public int refCount; public TransformData(int index) { transformIndex = index; refCount = 1; } } // This is an array of transforms that are passed to the job. // It must be an array because the TransformAccessArray requires an array of transforms. Transform[] m_Transform; TransformAccessArray m_TransformAccessArray; NativeHashMap m_TransformData; NativeArray m_TransformMatrix; NativeArray m_TransformChanged; bool m_Dirty; JobHandle m_JobHandle; public TransformAccessJob() { InitializeDataStructures(); m_Dirty = false; m_JobHandle = default(JobHandle); } public void Destroy() { ClearDataStructures(); } void InitializeDataStructures() { m_TransformMatrix = new NativeArray(1, Allocator.Persistent); m_TransformData = new NativeHashMap(1, Allocator.Persistent); m_Transform = Array.Empty(); } void ClearDataStructures() { if (m_TransformMatrix.IsCreated) m_TransformMatrix.Dispose(); if (m_TransformChanged.IsCreated) m_TransformChanged.Dispose(); if (m_TransformAccessArray.isCreated) m_TransformAccessArray.Dispose(); if (m_TransformData.IsCreated) m_TransformData.Dispose(); m_Transform = null; } public void ResetCache() { ClearDataStructures(); InitializeDataStructures(); } public NativeHashMap transformData => m_TransformData; // This array can hold localToWorld or worldToLocal matrices depending on the job that was scheduled. public NativeArray transformMatrix => m_TransformMatrix; public NativeArray transformChanged => m_TransformChanged; #if UNITY_INCLUDE_TESTS internal TransformAccessArray transformAccessArray => m_TransformAccessArray; #endif public void AddTransform(Transform t) { if (t == null || !m_TransformData.IsCreated) return; m_JobHandle.Complete(); int instanceId = t.GetInstanceID(); if (m_TransformData.ContainsKey(instanceId)) { TransformData transformData = m_TransformData[instanceId]; transformData.refCount += 1; m_TransformData[instanceId] = transformData; } else { m_TransformData.TryAdd(instanceId, new TransformData(-1)); ArrayAdd(ref m_Transform, t); m_Dirty = true; } } static void ArrayAdd(ref T[] array, T item) { int arraySize = array.Length; Array.Resize(ref array, arraySize + 1); array[arraySize] = item; } // if removing multiple items, it is more efficient to just set them to null and then call CompactArray. static void ArrayRemoveAt(ref T[] array, int index) { int length = array.Length; if (index >= length) throw new ArgumentOutOfRangeException(nameof(index)); // Shift elements up for (int i = index; i < length - 1; ++i) array[i] = array[i + 1]; // Resize array, chopping off the last element Array.Resize(ref array, length - 1); } // This method is used to remove real nulls from the array and resize it. static void CompactArray(ref T[] array) { // iterate over array and remove nulls int writeIndex = 0; for (int i = 0; i < array.Length; i++) { // we use 'is not null' to avoid removing destroyed transforms, which are handled elsewhere. if (array[i] is not null) { if (writeIndex != i) array[writeIndex] = array[i]; writeIndex++; } } // Resize the array to the new length if (writeIndex < array.Length) { Array.Resize(ref array, writeIndex); } } void UpdateTransformIndex() { if (!m_Dirty) return; m_Dirty = false; Profiler.BeginSample("UpdateTransformIndex"); // Always recreate matrix array when transform array changes to ensure clean state if (m_TransformMatrix.IsCreated) m_TransformMatrix.Dispose(); // Initialize with zero matrices. Note: Unity Transform.localToWorldMatrix can never be all zeros // due to homogeneous coordinate requirements (last row is always [0,0,0,1]), so zero initialization // ensures proper change detection. m_TransformMatrix = new NativeArray(m_Transform.Length, Allocator.Persistent, NativeArrayOptions.ClearMemory); if (!m_TransformAccessArray.isCreated) TransformAccessArray.Allocate(m_Transform.Length, -1, out m_TransformAccessArray); else if (m_TransformAccessArray.capacity != m_Transform.Length) m_TransformAccessArray.capacity = m_Transform.Length; m_TransformAccessArray.SetTransforms(m_Transform); for (int i = 0; i < m_Transform.Length; ++i) { if (m_Transform[i] != null) { int instanceId = m_Transform[i].GetInstanceID(); TransformData transformData = m_TransformData[instanceId]; transformData.transformIndex = i; m_TransformData[instanceId] = transformData; } } Profiler.EndSample(); } public JobHandle StartLocalToWorldAndChangeDetectionJob() { // No need for initialization as all values are overwritten each frame by LocalToWorldAndChangeDetectionTransformAccessJob NativeArrayHelpers.ResizeIfNeeded(ref m_TransformChanged, m_Transform.Length, Allocator.Persistent, NativeArrayOptions.UninitializedMemory); if (m_Transform.Length > 0) { m_JobHandle.Complete(); UpdateTransformIndex(); Profiler.BeginSample("StartLocalToWorldAndChangeDetectionJob"); LocalToWorldAndChangeDetectionTransformAccessJob job = new LocalToWorldAndChangeDetectionTransformAccessJob() { outMatrix = transformMatrix, hasChanged = transformChanged, }; m_JobHandle = job.ScheduleReadOnly(m_TransformAccessArray, 16); Profiler.EndSample(); return m_JobHandle; } return default(JobHandle); } public JobHandle StartWorldToLocalJob() { if (m_Transform.Length > 0) { m_JobHandle.Complete(); UpdateTransformIndex(); Profiler.BeginSample("StartWorldToLocalJob"); WorldToLocalTransformAccessJob job = new WorldToLocalTransformAccessJob() { outMatrix = transformMatrix, }; m_JobHandle = job.ScheduleReadOnly(m_TransformAccessArray, 16); Profiler.EndSample(); return m_JobHandle; } return default(JobHandle); } internal string GetDebugLog() { string log = ""; log += "TransformData Count: " + m_TransformData.Count + "\n"; log += "Transform Count: " + m_Transform.Length + "\n"; foreach (Transform ss in m_Transform) { log += ss == null ? "null" : ss.name + " " + ss.GetInstanceID(); log += "\n"; if (ss != null) { log += "RefCount: " + m_TransformData[ss.GetInstanceID()].refCount + "\n"; } log += "\n"; } return log; } // Remove any destroyed transforms from m_Transform and keep the index in sync internal int RemoveTransformsIfNull() { int count = 0; // process in reverse order to preserve array integrity on removal. for (int i = m_Transform.Length - 1; i >= 0; i--) { // Is this transform destroyed? if (!m_Transform[i]) { // remove from index, still safe to call GetInstanceID here. // todo:TransformData can't be removed because transform.GetInstanceID() will return zero after the transform is destroyed. m_TransformData.Remove(m_Transform[i].GetInstanceID()); // remove from transform array by assigning a real null. m_Transform[i] = null; count++; } } CompactArray(ref m_Transform); return count; } // Deformation manager calls this with a list of ids to remove // Note: the list passed in is also modified by this method. // Note: this method assumes the list is sorted. internal void RemoveTransformsByIds(List idsToRemove) { if (!m_TransformData.IsCreated) return; m_JobHandle.Complete(); // catch the indexes to remove from m_Transform here List indexesToRemove = ListPool.Get(); // Remove any ids that we do not know about // Reduce refcount on ids that we do know about. for (int i = idsToRemove.Count - 1; i >= 0; --i) { int id = idsToRemove[i]; // if we don't know about this id, remove it from the list then ignore if (!m_TransformData.ContainsKey(id)) { idsToRemove.Remove(id); continue; } TransformData transformData = m_TransformData[id]; // reduce refcount if it is > 1 if (transformData.refCount > 1) { transformData.refCount -= 1; m_TransformData[id] = transformData; idsToRemove.Remove(id); } else // refcount will become 0 so remove it from the index, and add it to the list of indexes to remove from m_Transform { m_TransformData.Remove(id); if (0 <= transformData.transformIndex) indexesToRemove.Add(transformData.transformIndex); } } if (indexesToRemove.Count > 0) { // remove the transforms from the transform array in reverse order // they appear to already be sorted however not sure we can assume that is always the case // so we sort them here to be safe indexesToRemove.Sort(); for (int i = indexesToRemove.Count - 1; i >= 0; i--) { int index = indexesToRemove[i]; // previous version of this code performed a linear search to find the index to remove // by matching GetInstanceID() of the transform. // it did not remove the transform from the transform array if there was no match // we do the same logic here by ignoring the index if it is out of bounds if (index < m_Transform.Length) m_Transform[index] = null; } CompactArray(ref m_Transform); } ListPool.Release(indexesToRemove); } internal void RemoveTransformById(int transformId) { if (!m_TransformData.IsCreated) return; m_JobHandle.Complete(); if (m_TransformData.TryGetValue(transformId, out TransformData transformData)) { if (transformData.refCount == 1) { m_TransformData.Remove(transformId); int index = Array.FindIndex(m_Transform, t => t.GetInstanceID() == transformId); if (index >= 0) { ArrayRemoveAt(ref m_Transform, index); } m_Dirty = true; } else { transformData.refCount -= 1; m_TransformData[transformId] = transformData; } } } } [BurstCompile] internal struct LocalToWorldAndChangeDetectionTransformAccessJob : IJobParallelForTransform { public NativeArray outMatrix; [WriteOnly] public NativeArray hasChanged; public void Execute(int index, TransformAccess transform) { if (transform.isValid) { float4x4 localToWorldMatrix = transform.localToWorldMatrix; hasChanged[index] = !outMatrix[index].Equals(localToWorldMatrix); outMatrix[index] = localToWorldMatrix; } } } [BurstCompile] internal struct WorldToLocalTransformAccessJob : IJobParallelForTransform { [WriteOnly] public NativeArray outMatrix; public void Execute(int index, TransformAccess transform) { if (transform.isValid) outMatrix[index] = transform.worldToLocalMatrix; } } }