using System.Collections; using System.Collections.Generic; using System.Diagnostics; using Unity.Burst; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using Unity.Mathematics; using UnityEngine.Assertions; namespace Pathfinding.Util { /// /// Thread-safe hierarchical bitset. /// /// Stores an array of bits. Each bit can be set or cleared individually from any thread. /// /// Note: Setting the capacity is not thread-safe, nor is iterating over the bitset while it is being modified. /// [BurstCompile] public struct HierarchicalBitset { UnsafeSpan l1; UnsafeSpan l2; UnsafeSpan l3; Allocator allocator; const int Log64 = 6; public HierarchicalBitset (int size, Allocator allocator) { this.allocator = allocator; l1 = new UnsafeSpan(allocator, (size + 64 - 1) >> Log64); l2 = new UnsafeSpan(allocator, (size + (64*64 - 1)) >> Log64 >> Log64); l3 = new UnsafeSpan(allocator, (size + (64*64*64 - 1)) >> Log64 >> Log64 >> Log64); l1.FillZeros(); l2.FillZeros(); l3.FillZeros(); } public bool IsCreated => Capacity > 0; public void Dispose () { l1.Free(allocator); l2.Free(allocator); l3.Free(allocator); this = default; } public int Capacity { get { return l1.Length << Log64; } set { if (value < Capacity) throw new System.ArgumentException("Shrinking the bitset is not supported"); if (value == Capacity) return; var b = new HierarchicalBitset(value, allocator); // Copy the old data l1.CopyTo(b.l1); l2.CopyTo(b.l2); l3.CopyTo(b.l3); Dispose(); this = b; } } /// Number of set bits in the bitset public int Count () { int count = 0; for (int i = 0; i < l1.Length; i++) { count += math.countbits(l1[i]); } return count; } /// True if the bitset is empty public bool IsEmpty { get { for (int i = 0; i < l3.Length; i++) { if (l3[i] != 0) return false; } return true; } } /// Clear all bits public void Clear () { // TODO: Optimize? l1.FillZeros(); l2.FillZeros(); l3.FillZeros(); } public void GetIndices (NativeList result) { var buffer = new NativeArray(256, Allocator.Temp); var iter = GetIterator(buffer.AsUnsafeSpan()); while (iter.MoveNext()) { var span = iter.Current; for (int i = 0; i < span.Length; i++) { result.Add(span[i]); } } } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] static bool SetAtomic (ref UnsafeSpan span, int index) { var cellIndex = index >> Log64; var currentValue = span[cellIndex]; // Note: 1 << index will only use the lower 6 bits of index if ((currentValue & (1UL << index)) != 0) { // Bit already set return true; } // TODO: Use Interlocked.Or in newer .net versions while (true) { var actualValue = (ulong)System.Threading.Interlocked.CompareExchange(ref UnsafeUtility.As(ref span[cellIndex]), (long)(currentValue | (1UL << index)), (long)currentValue); if (actualValue != currentValue) currentValue = actualValue; else break; } return false; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] static bool ResetAtomic (ref UnsafeSpan span, int index) { var cellIndex = index >> Log64; var currentValue = span[cellIndex]; // Note: 1 << index will only use the lower 6 bits of index if ((currentValue & (1UL << index)) == 0) { // Bit already cleared return true; } // TODO: Use Interlocked.Or in newer .net versions while (true) { var actualValue = (ulong)System.Threading.Interlocked.CompareExchange(ref UnsafeUtility.As(ref span[cellIndex]), (long)(currentValue & ~(1UL << index)), (long)currentValue); if (actualValue != currentValue) currentValue = actualValue; else break; } return false; } /// Get the value of a bit [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public bool Get (int index) { // Note: 1 << index will only use the lower 6 bits of index return (l1[index >> Log64] & (1UL << index)) != 0; } /// Set a given bit to 1 [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void Set (int index) { if (SetAtomic(ref l1, index)) return; SetAtomic(ref l2, index >> Log64); SetAtomic(ref l3, index >> (2*Log64)); } /// Set a given bit to 0 [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] public void Reset (int index) { if (ResetAtomic(ref l1, index)) return; if (l1[index >> Log64] == 0) ResetAtomic(ref l2, index >> Log64); if (l2[index >> (2*Log64)] == 0) ResetAtomic(ref l3, index >> (2*Log64)); } /// Get an iterator over all set bits. /// A buffer to use for temporary storage. A slice of this buffer will be returned on each iteration, filled with the indices of the set bits. public Iterator GetIterator (UnsafeSpan scratchBuffer) { return new Iterator(this, scratchBuffer); } [BurstCompile] public struct Iterator : IEnumerator >, IEnumerable > { HierarchicalBitset bitSet; UnsafeSpan result; int resultCount; int l3index; int l3bitIndex; int l2bitIndex; public UnsafeSpan Current => result.Slice(0, resultCount); object IEnumerator.Current => throw new System.NotImplementedException(); public void Reset() => throw new System.NotImplementedException(); public void Dispose () {} public IEnumerator > GetEnumerator() => this; IEnumerator IEnumerable.GetEnumerator() => throw new System.NotImplementedException(); static int l2index(int l3index, int l3bitIndex) => (l3index << Log64) + l3bitIndex; static int l1index(int l2index, int l2bitIndex) => (l2index << Log64) + l2bitIndex; public Iterator (HierarchicalBitset bitSet, UnsafeSpan result) { this.bitSet = bitSet; this.result = result; resultCount = 0; l3index = 0; l3bitIndex = 0; l2bitIndex = 0; if (result.Length < 128) { // Minimum is actually 64, but that can be very inefficient throw new System.ArgumentException("Result array must be at least 128 elements long"); } } public bool MoveNext () { return MoveNextBurst(ref this); } [BurstCompile] public static bool MoveNextBurst (ref Iterator iter) { return iter.MoveNextInternal(); } // Inline [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] bool MoveNextInternal () { // Store various data in local variables to avoid writing them to memory every time they are updated uint resultCount = 0; int l3index = this.l3index; int l3bitIndex = this.l3bitIndex; int l2bitIndex = this.l2bitIndex; Assert.IsTrue(l2bitIndex < 64 && l3bitIndex < 64); for (; l3index < bitSet.l3.length; l3index++) { // Get the L3 cell, and mask out all bits we have already visited var l3cell = bitSet.l3[l3index] & (~0UL << l3bitIndex); if (l3cell == 0) continue; while (l3cell != 0) { // Find the next set bit in the L3 cell l3bitIndex = math.tzcnt(l3cell); // Nest check for level 2 int l2index = Iterator.l2index(l3index, l3bitIndex); // The l2 cell is guaranteed to be non-zero, even after masking out the bits we have already visited var l2cell = bitSet.l2[l2index] & (~0UL << l2bitIndex); Assert.AreNotEqual(0, l2cell); while (l2cell != 0) { l2bitIndex = math.tzcnt(l2cell); // Stop the loop if we have almost filled the result array // Each L1 cell may contain up to 64 set bits if (resultCount + 64 > result.Length) { this.resultCount = (int)resultCount; this.l3index = l3index; this.l3bitIndex = l3bitIndex; this.l2bitIndex = l2bitIndex; return true; } int l1index = Iterator.l1index(l2index, l2bitIndex); var l1cell = bitSet.l1[l1index]; int l1indexStart = l1index << Log64; Assert.AreNotEqual(0, l1cell); while (l1cell != 0) { var l1bitIndex = math.tzcnt(l1cell); l1cell &= l1cell - 1UL; // clear lowest bit int index = l1indexStart + l1bitIndex; Unity.Burst.CompilerServices.Hint.Assume(resultCount < (uint)result.Length); result[resultCount++] = index; } l2cell &= l2cell - 1UL; } // Skip a bit at the L3 level l3cell &= l3cell - 1UL; // clear lowest bit // Enter new L2 level l2bitIndex = 0; } l2bitIndex = 0; l3bitIndex = 0; } this.resultCount = (int)resultCount; this.l3index = l3index; this.l3bitIndex = l3bitIndex; this.l2bitIndex = l2bitIndex; return resultCount > 0; } } } }