using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Utilities; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; namespace Misaki.HighPerformance.LowLevel.Collections; internal class UnsafeBitSetDebugView { private readonly UnsafeBitSet _bitSet; public UnsafeBitSetDebugView(UnsafeBitSet bitSet) { _bitSet = bitSet; } [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] public bool[] Bits { get { var bits = new bool[_bitSet.Count]; for (var i = 0; i < bits.Length; i++) { bits[i] = _bitSet.IsSet(i); } return bits; } } } [DebuggerTypeProxy(typeof(UnsafeBitSetDebugView))] public unsafe struct UnsafeBitSet : IDisposable, IEquatable { public ref struct Iterator { private ref UnsafeBitSet _bitSet; private int _currentBit; public Iterator(ref UnsafeBitSet bitSet, int start) { _bitSet = ref bitSet; _currentBit = start - 1; } public bool Next(out int bitIndex) { _currentBit = _bitSet.NextSetBit(_currentBit + 1); bitIndex = _currentBit; return _currentBit != -1; } } internal const int _BIT_SIZE = sizeof(uint) * 8 - 1; // 31 internal const int _INDEX_SIZE = 5; // log_2(BitSize + 1) internal const int _MASK = (1 << 5) - 1; // 0x1F, the mask to get the bit index inside a uint internal static readonly int s_padding = Vector.Count; // The padding used for vectorization, the amount of uints required for being vectorized basically private UnsafeArray _bits; private int _highestBit; private int _max; /// /// The highest uint index in use inside the -array. /// public readonly int HighestIndex => _max; /// /// The highest bit set. /// public readonly int HighestBit => _highestBit; /// /// Gets the total number of bits represented by the current instance. /// public readonly int Count => _bits.Count << _INDEX_SIZE; public readonly bool IsCreated => _bits.IsCreated; /// /// Initializes a new instance of UnsafeBitSet with a default size of 1 and a persistent allocation handle. /// public UnsafeBitSet() { _bits = new UnsafeArray(1, AllocationHandle.Persistent, AllocationOption.None); } /// /// Initializes a new instance of the class. /// /// The minimal length in bits. /// The allocation handle. /// The allocation option. public UnsafeBitSet(int minimalLength, AllocationHandle handle, AllocationOption option = AllocationOption.None) { var uints = (minimalLength >> _INDEX_SIZE) + int.Sign(minimalLength & _BIT_SIZE); var length = RoundToPadding(uints); _bits = new UnsafeArray(length, handle, option); } /// /// Initializes a new instance of the class. /// [Obsolete("Use AllocationHandle instead.")] public UnsafeBitSet(int minimalLength, Allocator allocator, AllocationOption option = AllocationOption.None) : this(minimalLength, AllocationManager.GetAllocationHandle(allocator), option) { } /// /// Initializes a new instance of the class. /// [Obsolete("Use AllocationHandle instead.")] public UnsafeBitSet(Span bits, Allocator allocator) { _bits = new UnsafeArray(bits.Length, allocator, AllocationOption.None); _bits.CopyFrom(bits); _highestBit = 0; _max = _bits.Count * (_BIT_SIZE + 1) - 1; // Calculate the maximum index in use for (var i = 0; i < _bits.Count; i++) { if (_bits[i] != 0) { _highestBit = Math.Max(_highestBit, i * (_BIT_SIZE + 1) + BitOperations.Log2(_bits[i]) + 1); } } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int RoundToPadding(int length) { return (length + s_padding - 1) / s_padding * s_padding; } /// /// Determines the required length of an to hold the passed ID or bit. /// /// The ID or bit. /// A size of required s for the bitset. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int RequiredLength(int id) { return (id >> _INDEX_SIZE) + int.Sign(id & _BIT_SIZE); } [MethodImpl(MethodImplOptions.AggressiveInlining)] [UnscopedRef] public Iterator GetIterator(int start = 0) { return new Iterator(ref this, start); } /// /// Checks whether a bit is set at the index. /// /// The index. /// True if it is, otherwise false [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly bool IsSet(int index) { var b = index >> _INDEX_SIZE; if (b >= _bits.Count) { return false; } return (_bits[b] & 1 << (index & _BIT_SIZE)) != 0; } /// /// Sets a bit at the given index. /// Resizes its internal array if necessary. /// /// The index. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetBit(int index) { var b = index >> _INDEX_SIZE; if (b >= _bits.Count) { _bits.Resize(index); } // Track highest set bit _highestBit = Math.Max(_highestBit, index); _max = _highestBit / (_BIT_SIZE + 1) + 1; _bits[b] |= 1u << (index & _BIT_SIZE); } /// /// Clears the bit at the given index. /// /// The index. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void ClearBit(int index) { var b = index >> _INDEX_SIZE; if (b >= _bits.Count) { return; } _bits[b] &= ~(1u << (index & _BIT_SIZE)); } /// /// Sets all bits. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetAll() { _bits.AsSpan().Fill(0xffffffff); _highestBit = _bits.Count * (_BIT_SIZE + 1) - 1; _max = _highestBit / (_BIT_SIZE + 1) + 1; } /// /// Clears all set bits. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void ClearAll() { _bits.Clear(); _highestBit = 0; _max = 0; } /// /// Finds the next set bit at or after `startIndex`, or -1 if none. /// public readonly int NextSetBit(int startIndex) { var wordIndex = startIndex >> _BIT_SIZE; if (wordIndex >= _bits.Count) { return -1; } // Mask off bits below startIndex in the first word: var word = _bits[wordIndex] & ~0u << (startIndex & _MASK); while (true) { if (word != 0) { // get the least-significant set bit var bit = BitOperations.TrailingZeroCount(word); return (wordIndex << _BIT_SIZE) + bit; } wordIndex++; if (wordIndex >= _bits.Count) { return -1; } word = _bits[wordIndex]; } } public void Resize(int minimalLength, AllocationOption option = AllocationOption.None) { var oldSize = _bits.Count; var uints = (minimalLength >> _INDEX_SIZE) + int.Sign(minimalLength & _BIT_SIZE); var length = RoundToPadding(uints); _bits.Resize(length, option); if (!option.HasFlag(AllocationOption.Clear)) { _bits.AsSpan()[oldSize..].Clear(); } } /// /// Checks if all bits from this instance match those of the other instance. /// /// The other . /// True if they match, false if not. public readonly bool All(UnsafeBitSet other) { var min = Math.Min(Math.Min(_bits.Count, other._bits.Count), _max); if (!Vector.IsHardwareAccelerated || min < s_padding) { var bits = _bits.AsSpan(); var otherBits = other._bits.AsSpan(); // Bitwise and for (var i = 0; i < min; i++) { var bit = bits[i]; if ((bit & otherBits[i]) != bit) { return false; } } // Handle extra bits on our side that might just be all zero. for (var i = min; i < _max; i++) { if (bits[i] != 0) { return false; } } } else { // Vectorized bitwise and for (var i = 0; i < min; i += s_padding) { var vector = new Vector(_bits.AsSpan()[i..]); var otherVector = new Vector(other._bits.AsSpan()[i..]); var resultVector = Vector.BitwiseAnd(vector, otherVector); if (!Vector.EqualsAll(resultVector, vector)) { return false; } } // Handle extra bits on our side that might just be all zero. for (var i = min; i < _max; i += s_padding) { var vector = new Vector(_bits.AsSpan()[i..]); if (!Vector.EqualsAll(vector, Vector.Zero)) // Vectors are not zero bits[0] != 0 basically { return false; } } } return true; } /// /// Checks if any bits from this instance match those of the other instance. /// /// The other . /// True if they match, false if not. public readonly bool Any(UnsafeBitSet other) { var min = Math.Min(Math.Min(_bits.Count, other._bits.Count), _max); if (!Vector.IsHardwareAccelerated || min < s_padding) { var bits = _bits.AsSpan(); var otherBits = other._bits.AsSpan(); // Bitwise and, return true since any is met for (var i = 0; i < min; i++) { var bit = bits[i]; if ((bit & otherBits[i]) > 0) { return true; } } // Handle extra bits on our side that might just be all zero. for (var i = min; i < _max; i++) { if (bits[i] > 0) { return false; } } } else { // Vectorized bitwise and, return true since any is met for (var i = 0; i < min; i += s_padding) { var vector = new Vector(_bits.AsSpan()[i..]); var otherVector = new Vector(other._bits.AsSpan()[i..]); var resultVector = Vector.BitwiseAnd(vector, otherVector); if (!Vector.EqualsAll(resultVector, Vector.Zero)) { return true; } } // Handle extra bits on our side that might just be all zero. for (var i = min; i < _max; i += s_padding) { var vector = new Vector(_bits.AsSpan()[i..]); if (!Vector.EqualsAll(vector, Vector.Zero)) // Vectors are not zero bits[0] != 0 basically { return false; } } } return _highestBit <= 0; } /// /// Checks if none bits from this instance match those of the other instance. /// /// The other . /// True if none match, false if not. public readonly bool None(UnsafeBitSet other) { var min = Math.Min(Math.Min(_bits.Count, other._bits.Count), _max); if (!Vector.IsHardwareAccelerated || min < s_padding) { var bits = _bits.AsSpan(); var otherBits = other._bits.AsSpan(); // Bitwise and, return true since any is met for (var i = 0; i < min; i++) { var bit = bits[i]; if ((bit & otherBits[i]) != 0) { return false; } } } else { // Vectorized bitwise and, return true since any is met for (var i = 0; i < min; i += s_padding) { var vector = new Vector(_bits.AsSpan()[i..]); var otherVector = new Vector(other._bits.AsSpan()[i..]); var resultVector = Vector.BitwiseAnd(vector, otherVector); if (!Vector.EqualsAll(resultVector, Vector.Zero)) { return false; } } } return true; } /// /// Checks if exactly all bits from this instance match those of the other instance. /// /// The other . /// True if they match, false if not. public readonly bool Exclusive(UnsafeBitSet other) { var min = Math.Min(Math.Min(_bits.Count, other._bits.Count), _max); if (!Vector.IsHardwareAccelerated || min < s_padding) { var bits = _bits.AsSpan(); var otherBits = other._bits.AsSpan(); // Bitwise xor, if both are not totally equal, return false for (var i = 0; i < min; i++) { var bit = bits[i]; if ((bit ^ otherBits[i]) != 0) { return false; } } // handle extra bits on our side that might just be all zero for (var i = min; i < _max; i++) { if (bits[i] != 0) { return false; } } } else { // Vectorized bitwise xor, return true since any is met for (var i = 0; i < min; i += s_padding) { var vector = new Vector(_bits.AsSpan()[i..]); var otherVector = new Vector(other._bits.AsSpan()[i..]); var resultVector = Vector.Xor(vector, otherVector); if (!Vector.EqualsAll(resultVector, Vector.Zero)) { return false; } } // Handle extra bits on our side that might just be all zero. for (var i = min; i < _max; i += s_padding) { var vector = new Vector(_bits.AsSpan()[i..]); if (!Vector.EqualsAll(vector, Vector.Zero)) // Vectors are not zero bits[0] != 0 basically { return false; } } } return true; } /// /// Inverts all bits in the current vector, replacing each bit with its logical complement. /// public void Not() { var thisCount = _bits.Count; if (!Vector.IsHardwareAccelerated || thisCount < s_padding) { for (var i = 0; i < thisCount; i++) { _bits[i] = ~_bits[i]; } } else { var pThis = (byte*)_bits.GetUnsafePtr(); for (var i = 0; i < thisCount; i += s_padding) { var vector = new Vector(_bits.AsSpan()[i..]); var resultVector = ~vector; Unsafe.WriteUnaligned(pThis + i, resultVector); } } } /// /// Performs a bitwise AND operation between the current bit set and the specified bit set, updating the current bit /// set in place. /// /// The bit set to combine with the current bit set using a bitwise AND operation. Must have the same length as the current bit set. /// Thrown when does not have the same length as the current bit set. public void And(UnsafeBitSet other) { var thisCount = _bits.Count; if (thisCount != other._bits.Count) { throw new ArgumentException("Bitsets must be of the same length for AND operation."); } if (!Vector.IsHardwareAccelerated || thisCount < s_padding) { for (var i = 0; i < thisCount; i++) { _bits[i] &= other._bits[i]; } } else { var pThis = (byte*)_bits.GetUnsafePtr(); var pOther = (byte*)other._bits.GetUnsafePtr(); for (var i = 0; i < thisCount; i += s_padding) { var vectorLeft = Vector.Load(pThis + i); var vectorRight = Vector.Load(pOther + i); var resultVector = Vector.BitwiseAnd(vectorLeft, vectorRight); Unsafe.WriteUnaligned(pThis + i, resultVector); } } } /// /// Performs a bitwise NAND operation between the current bit set and the specified bit set, updating the current /// bit set in place. /// /// The bit set to combine with the current bit set using the NAND operation. Must have the same length as the current bit set. /// Thrown if does not have the same length as the current bit set. public void Nand(UnsafeBitSet other) { var thisCount = _bits.Count; if (thisCount != other._bits.Count) { throw new ArgumentException("Bitsets must be of the same length for AND operation."); } if (!Vector.IsHardwareAccelerated || thisCount < s_padding) { for (var i = 0; i < thisCount; i++) { _bits[i] = ~(_bits[i] & other._bits[i]); } } else { var pThis = (byte*)_bits.GetUnsafePtr(); var pOther = (byte*)other._bits.GetUnsafePtr(); for (var i = 0; i < thisCount; i += s_padding) { var vectorLeft = Vector.Load(pThis + i); var vectorRight = Vector.Load(pOther + i); var resultVector = ~Vector.BitwiseAnd(vectorLeft, vectorRight); Unsafe.WriteUnaligned(pThis + i, resultVector); } } } /// /// Performs a bitwise AND NOT operation between the current bit set and the specified bit set, updating the current /// bit set in place. /// /// The bit set whose bits will be inverted and ANDed with the current bit set. Must have the same length as the current bit set. /// Thrown when the specified bit set does not have the same length as the current bit set. public void ANDC(UnsafeBitSet other) { var thisCount = _bits.Count; if (thisCount != other._bits.Count) { throw new ArgumentException("Bitsets must be of the same length for AND operation."); } if (!Vector.IsHardwareAccelerated || thisCount < s_padding) { for (var i = 0; i < thisCount; i++) { _bits[i] &= ~other._bits[i]; } } else { var pThis = (byte*)_bits.GetUnsafePtr(); var pOther = (byte*)other._bits.GetUnsafePtr(); for (var i = 0; i < thisCount; i += s_padding) { var vectorLeft = Vector.Load(pThis + i); var vectorRight = Vector.Load(pOther + i); var resultVector = Vector.AndNot(vectorLeft, vectorRight); Unsafe.WriteUnaligned(pThis + i, resultVector); } } } /// /// Performs a bitwise OR operation between the current bit set and the specified bit set, updating the current set /// in place. /// /// The bit set to combine with the current set using a bitwise OR operation. Must have the same length as the current bit set. /// Thrown if does not have the same length as the current bit set. public void Or(UnsafeBitSet other) { var thisCount = _bits.Count; if (thisCount != other._bits.Count) { throw new ArgumentException("Bitsets must be of the same length for AND operation."); } if (!Vector.IsHardwareAccelerated || thisCount < s_padding) { for (var i = 0; i < thisCount; i++) { _bits[i] |= other._bits[i]; } } else { var pThis = (byte*)_bits.GetUnsafePtr(); var pOther = (byte*)other._bits.GetUnsafePtr(); for (var i = 0; i < thisCount; i += s_padding) { var vectorLeft = Vector.Load(pThis + i); var vectorRight = Vector.Load(pOther + i); var resultVector = Vector.BitwiseOr(vectorLeft, vectorRight); Unsafe.WriteUnaligned(pThis + i, resultVector); } } } /// /// Performs a bitwise exclusive OR (XOR) operation between the current bit set and the specified bit set. /// /// The bit set to XOR with the current instance. Must have the same length as the current bit set. /// Thrown if does not have the same length as the current bit set. public void Xor(UnsafeBitSet other) { var thisCount = _bits.Count; if (thisCount != other._bits.Count) { throw new ArgumentException("Bitsets must be of the same length for AND operation."); } if (!Vector.IsHardwareAccelerated || thisCount < s_padding) { for (var i = 0; i < thisCount; i++) { _bits[i] ^= other._bits[i]; } } else { var pThis = (byte*)_bits.GetUnsafePtr(); var pOther = (byte*)other._bits.GetUnsafePtr(); for (var i = 0; i < thisCount; i += s_padding) { var vectorLeft = Vector.Load(pThis + i); var vectorRight = Vector.Load(pOther + i); var resultVector = Vector.Xor(vectorLeft, vectorRight); Unsafe.WriteUnaligned(pThis + i, resultVector); } } } /// /// Creates a to access the . /// /// The . public readonly Span AsSpan() { var max = _highestBit / (_BIT_SIZE + 1) + 1; return _bits.AsSpan()[..max]; } /// /// Copies the bits into a and returns a slice containing the copied . /// /// The to copy into. /// If true, it will zero the unused space from the . /// The . public readonly Span AsSpan(Span span, bool zero = true) { // Copy everything thats possible from one to another var length = Math.Min(_bits.Count, span.Length); for (var index = 0; index < length; index++) { span[index] = _bits[index]; } // Zero the rest space which was not overriden due to the copy. for (var index = length; zero && index < span.Length; index++) { span[index] = 0; } return span[.._bits.Count]; } public readonly bool Equals(UnsafeBitSet other) { if (_bits.Count != other._bits.Count) { return false; } var bits = _bits.AsSpan(); var otherBits = other._bits.AsSpan(); if (!Vector.IsHardwareAccelerated || _bits.Count < s_padding) { for (var i = 0; i < _bits.Count; i++) { if (bits[i] != otherBits[i]) { return false; } } } else { for (var i = 0; i < _bits.Count; i += s_padding) { var vector = new Vector(bits[i..]); var otherVector = new Vector(otherBits[i..]); if (!Vector.EqualsAll(vector, otherVector)) { return false; } } } return true; } public override readonly bool Equals(object? obj) { return obj is UnsafeBitSet set && Equals(set); } public static bool operator ==(UnsafeBitSet left, UnsafeBitSet right) { return left.Equals(right); } public static bool operator !=(UnsafeBitSet left, UnsafeBitSet right) { return !(left == right); } public override readonly int GetHashCode() { var hash = new HashCode(); hash.AddBytes(MemoryMarshal.AsBytes(_bits.AsSpan())); return hash.ToHashCode(); } public override readonly string ToString() { // Convert uint to binary form for pretty printing var binaryBuilder = new StringBuilder(); foreach (var bit in _bits) { binaryBuilder.Append(Convert.ToString(bit, 2).PadLeft(32, '0')).Append(','); } binaryBuilder.Length--; return $"{nameof(_bits)}: {binaryBuilder}, {nameof(Count)}: {Count}"; } public void Dispose() { _bits.Dispose(); _highestBit = 0; _max = 0; } } /// /// The struct /// represents a non resizable collection of bits. /// Used to set, check and clear bits on a allocated or on the stack. /// public readonly ref struct SpanBitSet : IEquatable { public ref struct Iterator { private SpanBitSet _bitSet; private int _currentBit; public Iterator(SpanBitSet bitSet) { _bitSet = bitSet; _currentBit = -1; } public bool Next(out int bitIndex) { _currentBit = _bitSet.NextSetBit(_currentBit + 1); bitIndex = _currentBit; return _currentBit != -1; } } private const int _BIT_SIZE = sizeof(uint) * 8 - 1; // 31 // NOTE: Is a byte not 8 bits? private const int _BYTE_SIZE = 5; // log_2(BitSize + 1) /// /// The bits from the bitset. /// private readonly Span _bits; /// /// Initializes a new instance of the class. /// public SpanBitSet(Span bits) { _bits = bits; } public readonly Iterator GetIterator() { return new Iterator(this); } /// /// Checks whether a bit is set at the index. /// /// The index. /// True if it is, otherwise false public bool IsSet(int index) { var b = index >> _BYTE_SIZE; if (b >= _bits.Length) { return false; } return (_bits[b] & 1 << (index & _BIT_SIZE)) != 0; } /// /// Sets a bit at the given index. /// Resizes its internal array if necessary. /// /// The index. public void SetBit(int index) { var b = index >> _BYTE_SIZE; if (b >= _bits.Length) { return; } _bits[b] |= 1u << (index & _BIT_SIZE); } /// /// Clears the bit at the given index. /// /// The index. public void ClearBit(int index) { var b = index >> _BYTE_SIZE; if (b >= _bits.Length) { return; } _bits[b] &= ~(1u << (index & _BIT_SIZE)); } /// /// Sets all bits. /// public void SetAll() { var count = _bits.Length; for (var i = 0; i < count; i++) { _bits[i] = 0xffffffff; } } /// /// Clears all set bits. /// public void ClearAll() { _bits.Clear(); } public int NextSetBit(int startIndex) { var wordIndex = startIndex >> _BIT_SIZE; if (wordIndex >= _bits.Length) { return -1; } // Mask off bits below startIndex in the first word: var word = _bits[wordIndex] & ~0u << (startIndex & UnsafeBitSet._MASK); while (true) { if (word != 0) { // get the least-significant set bit var bit = BitOperations.TrailingZeroCount(word); return (wordIndex << _BIT_SIZE) + bit; } wordIndex++; if (wordIndex >= _bits.Length) { return -1; } word = _bits[wordIndex]; } } /// /// Creates a to access the . /// /// The hash. public Span AsSpan() { return _bits; } /// /// Copies the bits into a and returns a slice containing the copied . /// /// /// The hash. public Span AsSpan(Span span, bool zero = true) { // Prevent exception because target array is to small for copy operation var length = Math.Min(_bits.Length, span.Length); for (var index = 0; index < length; index++) { span[index] = _bits[index]; } // Zero the rest space which was not overriden due to the copy. for (var index = length; zero && index < span.Length; index++) { span[index] = 0; } return span[.._bits.Length]; } public bool Equals(SpanBitSet other) { if (_bits.Length != other._bits.Length) { return false; } for (var i = 0; i < _bits.Length; i++) { if (_bits[i] != other._bits[i]) { return false; } } return true; } public override int GetHashCode() { var hash = new HashCode(); hash.AddBytes(MemoryMarshal.AsBytes(_bits)); return hash.ToHashCode(); } public override string ToString() { // Convert uint to binary form for pretty printing var binaryBuilder = new StringBuilder(); foreach (var bit in _bits) { binaryBuilder.Append(Convert.ToString(bit, 2).PadLeft(32, '0')).Append(','); } binaryBuilder.Length--; return $"{nameof(_bits)}: {string.Join(",", binaryBuilder)}"; } }