using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Unity.Burst; using Unity.Collections.LowLevel.Unsafe; using Unity.Jobs; namespace Unity.Collections { [NativeContainer] [GenerateTestsForBurstCompatibility] internal unsafe struct NativeHashMapDispose { [NativeDisableUnsafePtrRestriction] internal UnsafeHashMap* m_HashMapData; internal AllocatorManager.AllocatorHandle m_Allocator; #if ENABLE_UNITY_COLLECTIONS_CHECKS internal AtomicSafetyHandle m_Safety; #endif internal void Dispose() { var hashMapData = (HashMapHelper*)m_HashMapData; HashMapHelper.Free(hashMapData); } } [BurstCompile] internal unsafe struct NativeHashMapDisposeJob : IJob { internal NativeHashMapDispose Data; public void Execute() { Data.Dispose(); } } /// /// A key-value pair. /// /// Used for enumerators. /// The type of the keys. /// The type of the values. [DebuggerDisplay("Key = {Key}, Value = {Value}")] [GenerateTestsForBurstCompatibility(GenericTypeArguments = new[] { typeof(int), typeof(int) })] public unsafe struct KVPair where TKey : unmanaged, IEquatable where TValue : unmanaged { internal HashMapHelper* m_Data; internal int m_Index; internal int m_Next; /// /// An invalid KeyValue. /// /// In a hash map enumerator's initial state, its value is Null. public static KVPair Null => new KVPair { m_Index = -1 }; /// /// The key. /// /// The key. If this KeyValue is Null, returns the default of TKey. public TKey Key { get { if (m_Index != -1) { return m_Data->Keys[m_Index]; } return default; } } /// /// Value of key/value pair. /// public ref TValue Value { get { #if ENABLE_UNITY_COLLECTIONS_CHECKS || UNITY_DOTS_DEBUG if (m_Index == -1) throw new ArgumentException("must be valid"); #endif return ref UnsafeUtility.AsRef(m_Data->Ptr + sizeof(TValue) * m_Index); } } /// /// Gets the key and the value. /// /// Outputs the key. If this KeyValue is Null, outputs the default of TKey. /// Outputs the value. If this KeyValue is Null, outputs the default of TValue. /// True if the key-value pair is valid. public bool GetKeyValue(out TKey key, out TValue value) { if (m_Index != -1) { key = m_Data->Keys[m_Index]; value = UnsafeUtility.ReadArrayElement(m_Data->Ptr, m_Index); return true; } key = default; value = default; return false; } } /// /// An unordered, expandable associative array. /// /// /// Not suitable for parallel write access. Use instead. /// /// The type of the keys. /// The type of the values. [StructLayout(LayoutKind.Sequential)] [NativeContainer] [DebuggerTypeProxy(typeof(NativeHashMapDebuggerTypeProxy<,>))] [GenerateTestsForBurstCompatibility(GenericTypeArguments = new[] { typeof(int), typeof(int) })] public unsafe struct NativeHashMap : INativeDisposable , IEnumerable> // Used by collection initializers. where TKey : unmanaged, IEquatable where TValue : unmanaged { [NativeDisableUnsafePtrRestriction] internal HashMapHelper* m_Data; #if ENABLE_UNITY_COLLECTIONS_CHECKS internal AtomicSafetyHandle m_Safety; static readonly SharedStatic s_staticSafetyId = SharedStatic.GetOrCreate>(); #endif /// /// Initializes and returns an instance of UnsafeHashMap. /// /// The number of key-value pairs that should fit in the initial allocation. /// The allocator to use. public NativeHashMap(int initialCapacity, AllocatorManager.AllocatorHandle allocator) { m_Data = HashMapHelper.Alloc(initialCapacity, sizeof(TValue), HashMapHelper.kMinimumCapacity, allocator); #if ENABLE_UNITY_COLLECTIONS_CHECKS m_Safety = CollectionHelper.CreateSafetyHandle(allocator); if (UnsafeUtility.IsNativeContainerType() || UnsafeUtility.IsNativeContainerType()) AtomicSafetyHandle.SetNestedContainer(m_Safety, true); CollectionHelper.SetStaticSafetyId>(ref m_Safety, ref s_staticSafetyId.Data); AtomicSafetyHandle.SetBumpSecondaryVersionOnScheduleWrite(m_Safety, true); #endif } /// /// Releases all resources (memory). /// public void Dispose() { #if ENABLE_UNITY_COLLECTIONS_CHECKS if (!AtomicSafetyHandle.IsDefaultValue(m_Safety)) { AtomicSafetyHandle.CheckExistsAndThrow(m_Safety); } #endif if (!IsCreated) { return; } #if ENABLE_UNITY_COLLECTIONS_CHECKS CollectionHelper.DisposeSafetyHandle(ref m_Safety); #endif HashMapHelper.Free(m_Data); m_Data = null; } /// /// Creates and schedules a job that will dispose this hash map. /// /// A job handle. The newly scheduled job will depend upon this handle. /// The handle of a new job that will dispose this hash map. public JobHandle Dispose(JobHandle inputDeps) { #if ENABLE_UNITY_COLLECTIONS_CHECKS if (!AtomicSafetyHandle.IsDefaultValue(m_Safety)) { AtomicSafetyHandle.CheckExistsAndThrow(m_Safety); } #endif if (!IsCreated) { return inputDeps; } #if ENABLE_UNITY_COLLECTIONS_CHECKS var jobHandle = new NativeHashMapDisposeJob { Data = new NativeHashMapDispose { m_HashMapData = (UnsafeHashMap*)m_Data, m_Safety = m_Safety } }.Schedule(inputDeps); AtomicSafetyHandle.Release(m_Safety); #else var jobHandle = new NativeHashMapDisposeJob { Data = new NativeHashMapDispose { m_HashMapData = (UnsafeHashMap*)m_Data } }.Schedule(inputDeps); #endif m_Data = null; return jobHandle; } /// /// Whether this hash map has been allocated (and not yet deallocated). /// /// True if this hash map has been allocated (and not yet deallocated). public readonly bool IsCreated { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => m_Data != null && m_Data->IsCreated; } /// /// Whether this hash map is empty. /// /// True if this hash map is empty or if the map has not been constructed. public readonly bool IsEmpty { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { if (!IsCreated) { return true; } CheckRead(); return m_Data->IsEmpty; } } /// /// The current number of key-value pairs in this hash map. /// /// The current number of key-value pairs in this hash map. public readonly int Count { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { CheckRead(); return m_Data->Count; } } /// /// The number of key-value pairs that fit in the current allocation. /// /// The number of key-value pairs that fit in the current allocation. /// A new capacity. Must be larger than the current capacity. public int Capacity { [MethodImpl(MethodImplOptions.AggressiveInlining)] readonly get { CheckRead(); return m_Data->Capacity; } set { CheckWrite(); m_Data->Resize(value); } } /// /// Removes all key-value pairs. /// /// Does not change the capacity. public void Clear() { CheckWrite(); m_Data->Clear(); } /// /// Adds a new key-value pair. /// /// If the key is already present, this method returns false without modifying the hash map. /// The key to add. /// The value to add. /// True if the key-value pair was added. public bool TryAdd(TKey key, TValue item) { CheckWrite(); var idx = m_Data->TryAdd(key); if (-1 != idx) { UnsafeUtility.WriteArrayElement(m_Data->Ptr, idx, item); return true; } return false; } /// /// Adds a new key-value pair. /// /// If the key is already present, this method throws without modifying the hash map. /// The key to add. /// The value to add. /// Thrown if the key was already present. public void Add(TKey key, TValue item) { var result = TryAdd(key, item); if (!result) { ThrowKeyAlreadyAdded(key); } } /// /// Removes a key-value pair. /// /// The key to remove. /// True if a key-value pair was removed. public bool Remove(TKey key) { CheckWrite(); return -1 != m_Data->TryRemove(key); } /// /// Returns the value associated with a key. /// /// The key to look up. /// Outputs the value associated with the key. Outputs default if the key was not present. /// True if the key was present. public bool TryGetValue(TKey key, out TValue item) { CheckRead(); return m_Data->TryGetValue(key, out item); } /// /// Returns true if a given key is present in this hash map. /// /// The key to look up. /// True if the key was present. public bool ContainsKey(TKey key) { CheckRead(); return -1 != m_Data->Find(key); } /// /// Sets the capacity to match what it would be if it had been originally initialized with all its entries. /// public void TrimExcess() { CheckWrite(); m_Data->TrimExcess(); } /// /// Gets and sets values by key. /// /// Getting a key that is not present will throw. Setting a key that is not already present will add the key. /// The key to look up. /// The value associated with the key. /// For getting, thrown if the key was not present. public TValue this[TKey key] { get { CheckRead(); TValue result; if (!m_Data->TryGetValue(key, out result)) { ThrowKeyNotPresent(key); } return result; } set { CheckWrite(); var idx = m_Data->Find(key); if (-1 == idx) { TryAdd(key, value); return; } UnsafeUtility.WriteArrayElement(m_Data->Ptr, idx, value); } } /// /// Returns an array with a copy of all this hash map's keys (in no particular order). /// /// The allocator to use. /// An array with a copy of all this hash map's keys (in no particular order). public NativeArray GetKeyArray(AllocatorManager.AllocatorHandle allocator) { CheckRead(); return m_Data->GetKeyArray(allocator); } /// /// Returns an array with a copy of all this hash map's values (in no particular order). /// /// The allocator to use. /// An array with a copy of all this hash map's values (in no particular order). public NativeArray GetValueArray(AllocatorManager.AllocatorHandle allocator) { CheckRead(); return m_Data->GetValueArray(allocator); } /// /// Returns a NativeKeyValueArrays with a copy of all this hash map's keys and values. /// /// The key-value pairs are copied in no particular order. For all `i`, `Values[i]` will be the value associated with `Keys[i]`. /// The allocator to use. /// A NativeKeyValueArrays with a copy of all this hash map's keys and values. public NativeKeyValueArrays GetKeyValueArrays(AllocatorManager.AllocatorHandle allocator) { CheckRead(); return m_Data->GetKeyValueArrays(allocator); } /// /// Returns an enumerator over the key-value pairs of this hash map. /// /// An enumerator over the key-value pairs of this hash map. public Enumerator GetEnumerator() { #if ENABLE_UNITY_COLLECTIONS_CHECKS AtomicSafetyHandle.CheckGetSecondaryDataPointerAndThrow(m_Safety); var ash = m_Safety; AtomicSafetyHandle.UseSecondaryVersion(ref ash); #endif return new Enumerator { #if ENABLE_UNITY_COLLECTIONS_CHECKS m_Safety = ash, #endif m_Enumerator = new HashMapHelper.Enumerator(m_Data), }; } /// /// This method is not implemented. Use instead. /// /// Throws NotImplementedException. /// Method is not implemented. IEnumerator> IEnumerable>.GetEnumerator() { throw new NotImplementedException(); } /// /// This method is not implemented. Use instead. /// /// Throws NotImplementedException. /// Method is not implemented. IEnumerator IEnumerable.GetEnumerator() { throw new NotImplementedException(); } /// /// An enumerator over the key-value pairs of a container. /// /// /// In an enumerator's initial state, is not valid to read. /// From this state, the first call advances the enumerator to the first key-value pair. /// [NativeContainer] [NativeContainerIsReadOnly] public struct Enumerator : IEnumerator> { [NativeDisableUnsafePtrRestriction] internal HashMapHelper.Enumerator m_Enumerator; #if ENABLE_UNITY_COLLECTIONS_CHECKS internal AtomicSafetyHandle m_Safety; #endif /// /// Does nothing. /// public void Dispose() { } /// /// Advances the enumerator to the next key-value pair. /// /// True if is valid to read after the call. [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool MoveNext() { #if ENABLE_UNITY_COLLECTIONS_CHECKS AtomicSafetyHandle.CheckReadAndThrow(m_Safety); #endif return m_Enumerator.MoveNext(); } /// /// Resets the enumerator to its initial state. /// public void Reset() { #if ENABLE_UNITY_COLLECTIONS_CHECKS AtomicSafetyHandle.CheckReadAndThrow(m_Safety); #endif m_Enumerator.Reset(); } /// /// The current key-value pair. /// /// The current key-value pair. public KVPair Current { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => m_Enumerator.GetCurrent(); } /// /// Gets the element at the current position of the enumerator in the container. /// object IEnumerator.Current => Current; } /// /// Returns a readonly version of this NativeHashMap instance. /// /// ReadOnly containers point to the same underlying data as the NativeHashMap it is made from. /// ReadOnly instance for this. public ReadOnly AsReadOnly() { return new ReadOnly(ref this); } /// /// A read-only alias for the value of a NativeHashMap. Does not have its own allocated storage. /// [NativeContainer] [NativeContainerIsReadOnly] [GenerateTestsForBurstCompatibility(GenericTypeArguments = new[] { typeof(int), typeof(int) })] public struct ReadOnly : IEnumerable> { [NativeDisableUnsafePtrRestriction] internal HashMapHelper* m_Data; #if ENABLE_UNITY_COLLECTIONS_CHECKS AtomicSafetyHandle m_Safety; internal static readonly SharedStatic s_staticSafetyId = SharedStatic.GetOrCreate(); #endif internal ReadOnly(ref NativeHashMap data) { m_Data = data.m_Data; #if ENABLE_UNITY_COLLECTIONS_CHECKS m_Safety = data.m_Safety; CollectionHelper.SetStaticSafetyId(ref m_Safety, ref s_staticSafetyId.Data); #endif } /// /// Whether this hash map has been allocated (and not yet deallocated). /// /// True if this hash map has been allocated (and not yet deallocated). public readonly bool IsCreated { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => m_Data != null && m_Data->IsCreated; } /// /// Whether this hash map is empty. /// /// True if this hash map is empty or if the map has not been constructed. public readonly bool IsEmpty { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { if (!IsCreated) { return true; } CheckRead(); return m_Data->IsEmpty; } } /// /// The current number of key-value pairs in this hash map. /// /// The current number of key-value pairs in this hash map. public readonly int Count { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { CheckRead(); return m_Data->Count; } } /// /// The number of key-value pairs that fit in the current allocation. /// /// The number of key-value pairs that fit in the current allocation. public readonly int Capacity { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { CheckRead(); return m_Data->Capacity; } } /// /// Returns the value associated with a key. /// /// The key to look up. /// Outputs the value associated with the key. Outputs default if the key was not present. /// True if the key was present. public readonly bool TryGetValue(TKey key, out TValue item) { CheckRead(); return m_Data->TryGetValue(key, out item); } /// /// Returns true if a given key is present in this hash map. /// /// The key to look up. /// True if the key was present. public readonly bool ContainsKey(TKey key) { CheckRead(); return -1 != m_Data->Find(key); } /// /// Gets values by key. /// /// Getting a key that is not present will throw. /// The key to look up. /// The value associated with the key. /// For getting, thrown if the key was not present. public readonly TValue this[TKey key] { get { CheckRead(); TValue result; if (!m_Data->TryGetValue(key, out result)) { ThrowKeyNotPresent(key); } return result; } } /// /// Returns an array with a copy of all this hash map's keys (in no particular order). /// /// The allocator to use. /// An array with a copy of all this hash map's keys (in no particular order). public readonly NativeArray GetKeyArray(AllocatorManager.AllocatorHandle allocator) { CheckRead(); return m_Data->GetKeyArray(allocator); } /// /// Returns an array with a copy of all this hash map's values (in no particular order). /// /// The allocator to use. /// An array with a copy of all this hash map's values (in no particular order). public readonly NativeArray GetValueArray(AllocatorManager.AllocatorHandle allocator) { CheckRead(); return m_Data->GetValueArray(allocator); } /// /// Returns a NativeKeyValueArrays with a copy of all this hash map's keys and values. /// /// The key-value pairs are copied in no particular order. For all `i`, `Values[i]` will be the value associated with `Keys[i]`. /// The allocator to use. /// A NativeKeyValueArrays with a copy of all this hash map's keys and values. public readonly NativeKeyValueArrays GetKeyValueArrays(AllocatorManager.AllocatorHandle allocator) { CheckRead(); return m_Data->GetKeyValueArrays(allocator); } /// /// Returns an enumerator over the key-value pairs of this hash map. /// /// An enumerator over the key-value pairs of this hash map. public readonly Enumerator GetEnumerator() { #if ENABLE_UNITY_COLLECTIONS_CHECKS AtomicSafetyHandle.CheckGetSecondaryDataPointerAndThrow(m_Safety); var ash = m_Safety; AtomicSafetyHandle.UseSecondaryVersion(ref ash); #endif return new Enumerator { #if ENABLE_UNITY_COLLECTIONS_CHECKS m_Safety = ash, #endif m_Enumerator = new HashMapHelper.Enumerator(m_Data), }; } /// /// This method is not implemented. Use instead. /// /// Throws NotImplementedException. /// Method is not implemented. IEnumerator> IEnumerable>.GetEnumerator() { throw new NotImplementedException(); } /// /// This method is not implemented. Use instead. /// /// Throws NotImplementedException. /// Method is not implemented. IEnumerator IEnumerable.GetEnumerator() { throw new NotImplementedException(); } [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] [MethodImpl(MethodImplOptions.AggressiveInlining)] readonly void CheckRead() { #if ENABLE_UNITY_COLLECTIONS_CHECKS AtomicSafetyHandle.CheckReadAndThrow(m_Safety); #endif } [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS"), Conditional("UNITY_DOTS_DEBUG")] readonly void ThrowKeyNotPresent(TKey key) { throw new ArgumentException($"Key: {key} is not present."); } } [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] [MethodImpl(MethodImplOptions.AggressiveInlining)] readonly void CheckRead() { #if ENABLE_UNITY_COLLECTIONS_CHECKS AtomicSafetyHandle.CheckReadAndThrow(m_Safety); #endif } [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] [MethodImpl(MethodImplOptions.AggressiveInlining)] void CheckWrite() { #if ENABLE_UNITY_COLLECTIONS_CHECKS AtomicSafetyHandle.CheckWriteAndBumpSecondaryVersion(m_Safety); #endif } [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS"), Conditional("UNITY_DOTS_DEBUG")] void ThrowKeyNotPresent(TKey key) { throw new ArgumentException($"Key: {key} is not present."); } [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS"), Conditional("UNITY_DOTS_DEBUG")] void ThrowKeyAlreadyAdded(TKey key) { throw new ArgumentException($"An item with the same key has already been added: {key}"); } } internal unsafe sealed class NativeHashMapDebuggerTypeProxy where TKey : unmanaged, IEquatable where TValue : unmanaged { HashMapHelper* Data; public NativeHashMapDebuggerTypeProxy(NativeHashMap target) { Data = target.m_Data; } public NativeHashMapDebuggerTypeProxy(NativeHashMap.ReadOnly target) { Data = target.m_Data; } public List> Items { get { if (Data == null) { return default; } var result = new List>(); using (var kva = Data->GetKeyValueArrays(Allocator.Temp)) { for (var i = 0; i < kva.Length; ++i) { result.Add(new Pair(kva.Keys[i], kva.Values[i])); } } return result; } } } }