From b125146b27b6a2a1beb7a6e6af9b758b6742fbb0 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Mon, 13 Dec 2021 09:31:02 -0800 Subject: [PATCH] fix: Adds BitArray support with codegen (#888) * Adds a custom BitArray class with the following added features: * ctor for creating BitArray against read only span * ctor for creating BitArray against BinaryReader * CopyTo to copy a BitArray to a Span * Adds BitArray to UO Primitive serialization so it can be codegenned. --- .../Rules/PrimitiveUOTypeMigrationRule.cs | 1 + .../SymbolMetadata/SymbolMetadata.UO.cs | 8 + Projects/Server/Collections/BitArray.cs | 1410 +++++++++++++++++ .../Collections/CollectionThrowStrings.cs | 48 +- .../Server/Serialization/BinaryFileReader.cs | 10 + Projects/Server/Serialization/BufferReader.cs | 14 + Projects/Server/Serialization/BufferWriter.cs | 11 + .../Server/Serialization/IGenericReader.cs | 3 + .../Server/Serialization/IGenericWriter.cs | 3 + 9 files changed, 1489 insertions(+), 19 deletions(-) create mode 100644 Projects/Server/Collections/BitArray.cs diff --git a/Projects/SerializationGenerator/SerializableMigration/Rules/PrimitiveUOTypeMigrationRule.cs b/Projects/SerializationGenerator/SerializableMigration/Rules/PrimitiveUOTypeMigrationRule.cs index ec3562317..9d60ca27e 100644 --- a/Projects/SerializationGenerator/SerializableMigration/Rules/PrimitiveUOTypeMigrationRule.cs +++ b/Projects/SerializationGenerator/SerializableMigration/Rules/PrimitiveUOTypeMigrationRule.cs @@ -43,6 +43,7 @@ namespace SerializableMigration _ when symbol.IsRectangle3D(compilation) => new[] { "Rect3D" }, _ when symbol.IsRace(compilation) => new[] { "Race" }, _ when symbol.IsMap(compilation) => new[] { "Map" }, + _ when symbol.IsBitArray(compilation) => new[] { "BitArray" }, _ => null }; diff --git a/Projects/SerializationGenerator/SourceGeneration/SymbolMetadata/SymbolMetadata.UO.cs b/Projects/SerializationGenerator/SourceGeneration/SymbolMetadata/SymbolMetadata.UO.cs index 9b88b7493..2fdf8793c 100644 --- a/Projects/SerializationGenerator/SourceGeneration/SymbolMetadata/SymbolMetadata.UO.cs +++ b/Projects/SerializationGenerator/SourceGeneration/SymbolMetadata/SymbolMetadata.UO.cs @@ -47,6 +47,8 @@ namespace SerializationGenerator public const string SERIALIZABLE_FIELD_SAVE_FLAG_ATTRIBUTE = "Server.SerializableFieldSaveFlagAttribute"; public const string SERIALIZABLE_FIELD_DEFAULT_ATTRIBUTE = "Server.SerializableFieldDefaultAttribute"; public const string RAW_SERIALIZABLE_INTERFACE = "Server.IRawSerializable"; + // ModernUO modified BitArray + public const string SERVER_BITARRAY_CLASS = "Server.Collections.BitArray"; public static bool IsTimerDrift(this AttributeData attr, Compilation compilation) => attr?.IsAttribute(compilation.GetTypeByMetadataName(TIMER_DRIFT_ATTRIBUTE)) == true; @@ -184,6 +186,12 @@ namespace SerializationGenerator SymbolEqualityComparer.Default ); + public static bool IsBitArray(this ISymbol symbol, Compilation compilation) => + symbol.Equals( + compilation.GetTypeByMetadataName(SERVER_BITARRAY_CLASS), + SymbolEqualityComparer.Default + ); + public static AttributeData? GetAttribute(this ISymbol symbol, ISymbol attrSymbol) => symbol .GetAttributes() diff --git a/Projects/Server/Collections/BitArray.cs b/Projects/Server/Collections/BitArray.cs new file mode 100644 index 000000000..9df8f5441 --- /dev/null +++ b/Projects/Server/Collections/BitArray.cs @@ -0,0 +1,1410 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Buffers.Binary; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +using System.Runtime.Intrinsics.Arm; +using System.Collections; +using System.IO; + +namespace Server.Collections; + +// A vector of bits. Use this to store bits efficiently, without having to do bit +// shifting yourself. +[System.Serializable] +public sealed class BitArray : ICollection, ICloneable +{ + private int[] m_array; // Do not rename (binary serialization) + private int m_length; // Do not rename (binary serialization) + private int _version; // Do not rename (binary serialization) + + private const int _ShrinkThreshold = 256; + + /*========================================================================= + ** Allocates space to hold length bit values. All of the values in the bit + ** array are set to defaultValue. + ** + ** Exceptions: ArgumentOutOfRangeException if length < 0. + =========================================================================*/ + public BitArray(int length, bool defaultValue = false) + { + if (length < 0) + { + throw new ArgumentOutOfRangeException(nameof(length), length, CollectionThrowStrings.ArgumentOutOfRange_NeedNonNegNum); + } + + m_array = new int[GetInt32ArrayLengthFromBitLength(length)]; + m_length = length; + + if (defaultValue) + { + Array.Fill(m_array, -1); + + // clear high bit values in the last int + Div32Rem(length, out int extraBits); + if (extraBits > 0) + { + m_array[^1] = (1 << extraBits) - 1; + } + } + + _version = 0; + } + + /*========================================================================= + ** Allocates space to hold the bit values in bytes. bytes[0] represents + ** bits 0 - 7, bytes[1] represents bits 8 - 15, etc. The LSB of each byte + ** represents the lowest index value; bytes[0] & 1 represents bit 0, + ** bytes[0] & 2 represents bit 1, bytes[0] & 4 represents bit 2, etc. + ** + ** Exceptions: ArgumentException if bytes == null. + =========================================================================*/ + public BitArray(byte[] bytes) + { + if (bytes == null) + { + throw new ArgumentNullException(nameof(bytes)); + } + + // this value is chosen to prevent overflow when computing m_length. + // m_length is of type int32 and is exposed as a property, so + // type of m_length can't be changed to accommodate. + if (bytes.Length > int.MaxValue / BitsPerByte) + { + throw new ArgumentException(string.Format(CollectionThrowStrings.Argument_ArrayTooLarge, BitsPerByte), nameof(bytes)); + } + + m_array = new int[GetInt32ArrayLengthFromByteLength(bytes.Length)]; + m_length = bytes.Length * BitsPerByte; + + uint totalCount = (uint)bytes.Length / 4; + + ReadOnlySpan byteSpan = bytes; + for (int i = 0; i < totalCount; i++) + { + m_array[i] = BinaryPrimitives.ReadInt32LittleEndian(byteSpan); + byteSpan = byteSpan[4..]; + } + + Debug.Assert(byteSpan.Length >= 0 && byteSpan.Length < 4); + + int last = 0; + switch (byteSpan.Length) + { + case 3: + last = byteSpan[2] << 16; + goto case 2; + // fall through + case 2: + last |= byteSpan[1] << 8; + goto case 1; + // fall through + case 1: + m_array[totalCount] = last | byteSpan[0]; + break; + } + + _version = 0; + } + + /*========================================================================= + ** Allocates space to hold the bit values in bytes. bytes[0] represents + ** bits 0 - 7, bytes[1] represents bits 8 - 15, etc. The LSB of each byte + ** represents the lowest index value; bytes[0] & 1 represents bit 0, + ** bytes[0] & 2 represents bit 1, bytes[0] & 4 represents bit 2, etc. + ** + ** Exceptions: ArgumentException if bytes == null. + =========================================================================*/ + public BitArray(ReadOnlySpan bytes) + { + if (bytes == null) + { + throw new ArgumentNullException(nameof(bytes)); + } + + // this value is chosen to prevent overflow when computing m_length. + // m_length is of type int32 and is exposed as a property, so + // type of m_length can't be changed to accommodate. + if (bytes.Length > int.MaxValue / BitsPerByte) + { + throw new ArgumentException(string.Format(CollectionThrowStrings.Argument_ArrayTooLarge, BitsPerByte), nameof(bytes)); + } + + m_array = new int[GetInt32ArrayLengthFromByteLength(bytes.Length)]; + m_length = bytes.Length * BitsPerByte; + + uint totalCount = (uint)bytes.Length / 4; + + ReadOnlySpan byteSpan = bytes; + for (int i = 0; i < totalCount; i++) + { + m_array[i] = BinaryPrimitives.ReadInt32LittleEndian(byteSpan); + byteSpan = byteSpan[4..]; + } + + Debug.Assert(byteSpan.Length >= 0 && byteSpan.Length < 4); + + int last = 0; + switch (byteSpan.Length) + { + case 3: + last = byteSpan[2] << 16; + goto case 2; + // fall through + case 2: + last |= byteSpan[1] << 8; + goto case 1; + // fall through + case 1: + m_array[totalCount] = last | byteSpan[0]; + break; + } + + _version = 0; + } + + /*========================================================================= + ** Allocates space to hold the bit values in bytes. bytes[0] represents + ** bits 0 - 7, bytes[1] represents bits 8 - 15, etc. The LSB of each byte + ** represents the lowest index value; bytes[0] & 1 represents bit 0, + ** bytes[0] & 2 represents bit 1, bytes[0] & 4 represents bit 2, etc. + ** + ** Exceptions: ArgumentException if bytes == null. + =========================================================================*/ + public BitArray(BinaryReader reader, int length) + { + if (reader == null) + { + throw new ArgumentNullException(nameof(reader)); + } + + // this value is chosen to prevent overflow when computing m_length. + // m_length is of type int32 and is exposed as a property, so + // type of m_length can't be changed to accommodate. + if (length > int.MaxValue / BitsPerByte) + { + throw new ArgumentException(string.Format(CollectionThrowStrings.Argument_ArrayTooLarge, BitsPerByte), nameof(reader)); + } + + m_array = new int[GetInt32ArrayLengthFromByteLength(length)]; + m_length = length * BitsPerByte; + + uint totalCount = (uint)length / 4; + + for (int i = 0; i < totalCount; i++) + { + m_array[i] = reader.ReadInt32(); + length -= 4; + } + + Debug.Assert(length >= 0 && length < 4); + + int last = 0; + switch (length) + { + case 3: + last = reader.ReadInt16(); + goto case 2; + // fall through + case 2: + last |= reader.ReadByte(); + goto case 1; + // fall through + case 1: + m_array[totalCount] = last | reader.ReadByte(); + break; + } + + _version = 0; + } + + private const uint Vector128ByteCount = 16; + private const uint Vector128IntCount = 4; + private const uint Vector256ByteCount = 32; + private const uint Vector256IntCount = 8; + public unsafe BitArray(bool[] values) + { + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + m_array = new int[GetInt32ArrayLengthFromBitLength(values.Length)]; + m_length = values.Length; + + uint i = 0; + + if (values.Length < Vector256.Count) + { + goto LessThan32; + } + + // Comparing with 1s would get rid of the final negation, however this would not work for some CLR bools + // (true for any non-zero values, false for 0) - any values between 2-255 will be interpreted as false. + // Instead, We compare with zeroes (== false) then negate the result to ensure compatibility. + + if (Avx2.IsSupported) + { + // JIT does not support code hoisting for SIMD yet + Vector256 zero = Vector256.Zero; + fixed (bool* ptr = values) + { + for (; i + Vector256ByteCount <= (uint)values.Length; i += Vector256ByteCount) + { + Vector256 vector = Avx.LoadVector256((byte*)ptr + i); + Vector256 isFalse = Avx2.CompareEqual(vector, zero); + int result = Avx2.MoveMask(isFalse); + m_array[i / 32u] = ~result; + } + } + } + else if (Sse2.IsSupported) + { + // JIT does not support code hoisting for SIMD yet + Vector128 zero = Vector128.Zero; + fixed (bool* ptr = values) + { + for (; i + Vector128ByteCount * 2u <= (uint)values.Length; i += Vector128ByteCount * 2u) + { + Vector128 lowerVector = Sse2.LoadVector128((byte*)ptr + i); + Vector128 lowerIsFalse = Sse2.CompareEqual(lowerVector, zero); + int lowerPackedIsFalse = Sse2.MoveMask(lowerIsFalse); + + Vector128 upperVector = Sse2.LoadVector128((byte*)ptr + i + Vector128.Count); + Vector128 upperIsFalse = Sse2.CompareEqual(upperVector, zero); + int upperPackedIsFalse = Sse2.MoveMask(upperIsFalse); + + m_array[i / 32u] = ~((upperPackedIsFalse << 16) | lowerPackedIsFalse); + } + } + } + else if (AdvSimd.Arm64.IsSupported) + { + // JIT does not support code hoisting for SIMD yet + // However comparison against zero can be replaced to cmeq against zero (vceqzq_s8) + // See dotnet/runtime#33972 for details + Vector128 zero = Vector128.Zero; + Vector128 bitMask128 = BitConverter.IsLittleEndian ? + Vector128.Create(0x80402010_08040201).AsByte() : + Vector128.Create(0x01020408_10204080).AsByte(); + + fixed (bool* ptr = values) + { + for (; i + Vector128ByteCount * 2u <= (uint)values.Length; i += Vector128ByteCount * 2u) + { + // Same logic as SSE2 path, however we lack MoveMask (equivalent) instruction + // As a workaround, mask out the relevant bit after comparison + // and combine by ORing all of them together (In this case, adding all of them does the same thing) + Vector128 lowerVector = AdvSimd.LoadVector128((byte*)ptr + i); + Vector128 lowerIsFalse = AdvSimd.CompareEqual(lowerVector, zero); + Vector128 bitsExtracted1 = AdvSimd.And(lowerIsFalse, bitMask128); + bitsExtracted1 = AdvSimd.Arm64.AddPairwise(bitsExtracted1, bitsExtracted1); + bitsExtracted1 = AdvSimd.Arm64.AddPairwise(bitsExtracted1, bitsExtracted1); + bitsExtracted1 = AdvSimd.Arm64.AddPairwise(bitsExtracted1, bitsExtracted1); + Vector128 lowerPackedIsFalse = bitsExtracted1.AsInt16(); + + Vector128 upperVector = AdvSimd.LoadVector128((byte*)ptr + i + Vector128.Count); + Vector128 upperIsFalse = AdvSimd.CompareEqual(upperVector, zero); + Vector128 bitsExtracted2 = AdvSimd.And(upperIsFalse, bitMask128); + bitsExtracted2 = AdvSimd.Arm64.AddPairwise(bitsExtracted2, bitsExtracted2); + bitsExtracted2 = AdvSimd.Arm64.AddPairwise(bitsExtracted2, bitsExtracted2); + bitsExtracted2 = AdvSimd.Arm64.AddPairwise(bitsExtracted2, bitsExtracted2); + Vector128 upperPackedIsFalse = bitsExtracted2.AsInt16(); + + int result = AdvSimd.Arm64.ZipLow(lowerPackedIsFalse, upperPackedIsFalse).AsInt32().ToScalar(); + if (!BitConverter.IsLittleEndian) + { + result = BinaryPrimitives.ReverseEndianness(result); + } + m_array[i / 32u] = ~result; + } + } + } + + LessThan32: + for (; i < (uint)values.Length; i++) + { + if (values[i]) + { + int elementIndex = Div32Rem((int)i, out int extraBits); + m_array[elementIndex] |= 1 << extraBits; + } + } + + _version = 0; + } + + /*========================================================================= + ** Allocates space to hold the bit values in values. values[0] represents + ** bits 0 - 31, values[1] represents bits 32 - 63, etc. The LSB of each + ** integer represents the lowest index value; values[0] & 1 represents bit + ** 0, values[0] & 2 represents bit 1, values[0] & 4 represents bit 2, etc. + ** + ** Exceptions: ArgumentException if values == null. + =========================================================================*/ + public BitArray(int[] values) + { + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + // this value is chosen to prevent overflow when computing m_length + if (values.Length > int.MaxValue / BitsPerInt32) + { + throw new ArgumentException(string.Format(CollectionThrowStrings.Argument_ArrayTooLarge, BitsPerInt32), nameof(values)); + } + + m_array = new int[values.Length]; + Array.Copy(values, m_array, values.Length); + m_length = values.Length * BitsPerInt32; + + _version = 0; + } + + /*========================================================================= + ** Allocates a new BitArray with the same length and bit values as bits. + ** + ** Exceptions: ArgumentException if bits == null. + =========================================================================*/ + public BitArray(BitArray bits) + { + if (bits == null) + { + throw new ArgumentNullException(nameof(bits)); + } + + int arrayLength = GetInt32ArrayLengthFromBitLength(bits.m_length); + + m_array = new int[arrayLength]; + + Debug.Assert(bits.m_array.Length <= arrayLength); + + Array.Copy(bits.m_array, m_array, arrayLength); + m_length = bits.m_length; + + _version = bits._version; + } + + public bool this[int index] + { + get => Get(index); + set => Set(index, value); + } + + /*========================================================================= + ** Returns the bit value at position index. + ** + ** Exceptions: ArgumentOutOfRangeException if index < 0 or + ** index >= GetLength(). + =========================================================================*/ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Get(int index) + { + if ((uint)index >= (uint)m_length) + { + ThrowArgumentOutOfRangeException(index); + } + + return (m_array[index >> 5] & (1 << index)) != 0; + } + + /*========================================================================= + ** Sets the bit value at position index to value. + ** + ** Exceptions: ArgumentOutOfRangeException if index < 0 or + ** index >= GetLength(). + =========================================================================*/ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Set(int index, bool value) + { + if ((uint)index >= (uint)m_length) + { + ThrowArgumentOutOfRangeException(index); + } + + int bitMask = 1 << index; + ref int segment = ref m_array[index >> 5]; + + if (value) + { + segment |= bitMask; + } + else + { + segment &= ~bitMask; + } + + _version++; + } + + /*========================================================================= + ** Sets all the bit values to value. + =========================================================================*/ + public void SetAll(bool value) + { + int arrayLength = GetInt32ArrayLengthFromBitLength(Length); + Span span = m_array.AsSpan(0, arrayLength); + if (value) + { + span.Fill(-1); + + // clear high bit values in the last int + Div32Rem(m_length, out int extraBits); + if (extraBits > 0) + { + span[^1] &= (1 << extraBits) - 1; + } + } + else + { + span.Clear(); + } + + _version++; + } + + /*========================================================================= + ** Returns a reference to the current instance ANDed with value. + ** + ** Exceptions: ArgumentException if value == null or + ** value.Length != this.Length. + =========================================================================*/ + public unsafe BitArray And(BitArray value) + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + // This method uses unsafe code to manipulate data in the BitArrays. To avoid issues with + // buggy code concurrently mutating these instances in a way that could cause memory corruption, + // we snapshot the arrays from both and then operate only on those snapshots, while also validating + // that the count we iterate to is within the bounds of both arrays. We don't care about such code + // corrupting the BitArray data in a way that produces incorrect answers, since BitArray is not meant + // to be thread-safe; we only care about avoiding buffer overruns. + int[] thisArray = m_array; + int[] valueArray = value.m_array; + + int count = GetInt32ArrayLengthFromBitLength(Length); + if (Length != value.Length || (uint)count > (uint)thisArray.Length || (uint)count > (uint)valueArray.Length) + { + throw new ArgumentException(CollectionThrowStrings.Arg_ArrayLengthsDiffer); + } + + // Unroll loop for count less than Vector256 size. + switch (count) + { + case 7: thisArray[6] &= valueArray[6]; goto case 6; + case 6: thisArray[5] &= valueArray[5]; goto case 5; + case 5: thisArray[4] &= valueArray[4]; goto case 4; + case 4: thisArray[3] &= valueArray[3]; goto case 3; + case 3: thisArray[2] &= valueArray[2]; goto case 2; + case 2: thisArray[1] &= valueArray[1]; goto case 1; + case 1: thisArray[0] &= valueArray[0]; goto Done; + case 0: goto Done; + } + + uint i = 0; + if (Avx2.IsSupported) + { + fixed (int* leftPtr = thisArray) + { + fixed (int* rightPtr = valueArray) + { + for (; i < (uint)count - (Vector256IntCount - 1u); i += Vector256IntCount) + { + Vector256 leftVec = Avx.LoadVector256(leftPtr + i); + Vector256 rightVec = Avx.LoadVector256(rightPtr + i); + Avx.Store(leftPtr + i, Avx2.And(leftVec, rightVec)); + } + } + } + } + else if (Sse2.IsSupported) + { + fixed (int* leftPtr = thisArray) + { + fixed (int* rightPtr = valueArray) + { + for (; i < (uint)count - (Vector128IntCount - 1u); i += Vector128IntCount) + { + Vector128 leftVec = Sse2.LoadVector128(leftPtr + i); + Vector128 rightVec = Sse2.LoadVector128(rightPtr + i); + Sse2.Store(leftPtr + i, Sse2.And(leftVec, rightVec)); + } + } + } + } + else if (AdvSimd.IsSupported) + { + fixed (int* leftPtr = thisArray) + { + fixed (int* rightPtr = valueArray) + { + for (; i < (uint)count - (Vector128IntCount - 1u); i += Vector128IntCount) + { + Vector128 leftVec = AdvSimd.LoadVector128(leftPtr + i); + Vector128 rightVec = AdvSimd.LoadVector128(rightPtr + i); + AdvSimd.Store(leftPtr + i, AdvSimd.And(leftVec, rightVec)); + } + } + } + } + + for (; i < (uint)count; i++) + { + thisArray[i] &= valueArray[i]; + } + + Done: + _version++; + return this; + } + + /*========================================================================= + ** Returns a reference to the current instance ORed with value. + ** + ** Exceptions: ArgumentException if value == null or + ** value.Length != this.Length. + =========================================================================*/ + public unsafe BitArray Or(BitArray value) + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + // This method uses unsafe code to manipulate data in the BitArrays. To avoid issues with + // buggy code concurrently mutating these instances in a way that could cause memory corruption, + // we snapshot the arrays from both and then operate only on those snapshots, while also validating + // that the count we iterate to is within the bounds of both arrays. We don't care about such code + // corrupting the BitArray data in a way that produces incorrect answers, since BitArray is not meant + // to be thread-safe; we only care about avoiding buffer overruns. + int[] thisArray = m_array; + int[] valueArray = value.m_array; + + int count = GetInt32ArrayLengthFromBitLength(Length); + if (Length != value.Length || (uint)count > (uint)thisArray.Length || (uint)count > (uint)valueArray.Length) + { + throw new ArgumentException(CollectionThrowStrings.Arg_ArrayLengthsDiffer); + } + + // Unroll loop for count less than Vector256 size. + switch (count) + { + case 7: thisArray[6] |= valueArray[6]; goto case 6; + case 6: thisArray[5] |= valueArray[5]; goto case 5; + case 5: thisArray[4] |= valueArray[4]; goto case 4; + case 4: thisArray[3] |= valueArray[3]; goto case 3; + case 3: thisArray[2] |= valueArray[2]; goto case 2; + case 2: thisArray[1] |= valueArray[1]; goto case 1; + case 1: thisArray[0] |= valueArray[0]; goto Done; + case 0: goto Done; + } + + uint i = 0; + if (Avx2.IsSupported) + { + fixed (int* leftPtr = thisArray) + { + fixed (int* rightPtr = valueArray) + { + for (; i < (uint)count - (Vector256IntCount - 1u); i += Vector256IntCount) + { + Vector256 leftVec = Avx.LoadVector256(leftPtr + i); + Vector256 rightVec = Avx.LoadVector256(rightPtr + i); + Avx.Store(leftPtr + i, Avx2.Or(leftVec, rightVec)); + } + } + } + } + else if (Sse2.IsSupported) + { + fixed (int* leftPtr = thisArray) + { + fixed (int* rightPtr = valueArray) + { + for (; i < (uint)count - (Vector128IntCount - 1u); i += Vector128IntCount) + { + Vector128 leftVec = Sse2.LoadVector128(leftPtr + i); + Vector128 rightVec = Sse2.LoadVector128(rightPtr + i); + Sse2.Store(leftPtr + i, Sse2.Or(leftVec, rightVec)); + } + } + } + } + else if (AdvSimd.IsSupported) + { + fixed (int* leftPtr = thisArray) + { + fixed (int* rightPtr = valueArray) + { + for (; i < (uint)count - (Vector128IntCount - 1u); i += Vector128IntCount) + { + Vector128 leftVec = AdvSimd.LoadVector128(leftPtr + i); + Vector128 rightVec = AdvSimd.LoadVector128(rightPtr + i); + AdvSimd.Store(leftPtr + i, AdvSimd.Or(leftVec, rightVec)); + } + } + } + } + + for (; i < (uint)count; i++) + { + thisArray[i] |= valueArray[i]; + } + + Done: + _version++; + return this; + } + + /*========================================================================= + ** Returns a reference to the current instance XORed with value. + ** + ** Exceptions: ArgumentException if value == null or + ** value.Length != this.Length. + =========================================================================*/ + public unsafe BitArray Xor(BitArray value) + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + // This method uses unsafe code to manipulate data in the BitArrays. To avoid issues with + // buggy code concurrently mutating these instances in a way that could cause memory corruption, + // we snapshot the arrays from both and then operate only on those snapshots, while also validating + // that the count we iterate to is within the bounds of both arrays. We don't care about such code + // corrupting the BitArray data in a way that produces incorrect answers, since BitArray is not meant + // to be thread-safe; we only care about avoiding buffer overruns. + int[] thisArray = m_array; + int[] valueArray = value.m_array; + + int count = GetInt32ArrayLengthFromBitLength(Length); + if (Length != value.Length || (uint)count > (uint)thisArray.Length || (uint)count > (uint)valueArray.Length) + { + throw new ArgumentException(CollectionThrowStrings.Arg_ArrayLengthsDiffer); + } + + // Unroll loop for count less than Vector256 size. + switch (count) + { + case 7: thisArray[6] ^= valueArray[6]; goto case 6; + case 6: thisArray[5] ^= valueArray[5]; goto case 5; + case 5: thisArray[4] ^= valueArray[4]; goto case 4; + case 4: thisArray[3] ^= valueArray[3]; goto case 3; + case 3: thisArray[2] ^= valueArray[2]; goto case 2; + case 2: thisArray[1] ^= valueArray[1]; goto case 1; + case 1: thisArray[0] ^= valueArray[0]; goto Done; + case 0: goto Done; + } + + uint i = 0; + if (Avx2.IsSupported) + { + fixed (int* leftPtr = m_array) + { + fixed (int* rightPtr = value.m_array) + { + for (; i < (uint)count - (Vector256IntCount - 1u); i += Vector256IntCount) + { + Vector256 leftVec = Avx.LoadVector256(leftPtr + i); + Vector256 rightVec = Avx.LoadVector256(rightPtr + i); + Avx.Store(leftPtr + i, Avx2.Xor(leftVec, rightVec)); + } + } + } + } + else if (Sse2.IsSupported) + { + fixed (int* leftPtr = thisArray) + { + fixed (int* rightPtr = valueArray) + { + for (; i < (uint)count - (Vector128IntCount - 1u); i += Vector128IntCount) + { + Vector128 leftVec = Sse2.LoadVector128(leftPtr + i); + Vector128 rightVec = Sse2.LoadVector128(rightPtr + i); + Sse2.Store(leftPtr + i, Sse2.Xor(leftVec, rightVec)); + } + } + } + } + else if (AdvSimd.IsSupported) + { + fixed (int* leftPtr = thisArray) + { + fixed (int* rightPtr = valueArray) + { + for (; i < (uint)count - (Vector128IntCount - 1u); i += Vector128IntCount) + { + Vector128 leftVec = AdvSimd.LoadVector128(leftPtr + i); + Vector128 rightVec = AdvSimd.LoadVector128(rightPtr + i); + AdvSimd.Store(leftPtr + i, AdvSimd.Xor(leftVec, rightVec)); + } + } + } + } + + for (; i < (uint)count; i++) + { + thisArray[i] ^= valueArray[i]; + } + + Done: + _version++; + return this; + } + + /*========================================================================= + ** Inverts all the bit values. On/true bit values are converted to + ** off/false. Off/false bit values are turned on/true. The current instance + ** is updated and returned. + =========================================================================*/ + public unsafe BitArray Not() + { + // This method uses unsafe code to manipulate data in the BitArray. To avoid issues with + // buggy code concurrently mutating this instance in a way that could cause memory corruption, + // we snapshot the array then operate only on this snapshot. We don't care about such code + // corrupting the BitArray data in a way that produces incorrect answers, since BitArray is not meant + // to be thread-safe; we only care about avoiding buffer overruns. + int[] thisArray = m_array; + + int count = GetInt32ArrayLengthFromBitLength(Length); + + // Unroll loop for count less than Vector256 size. + switch (count) + { + case 7: thisArray[6] = ~thisArray[6]; goto case 6; + case 6: thisArray[5] = ~thisArray[5]; goto case 5; + case 5: thisArray[4] = ~thisArray[4]; goto case 4; + case 4: thisArray[3] = ~thisArray[3]; goto case 3; + case 3: thisArray[2] = ~thisArray[2]; goto case 2; + case 2: thisArray[1] = ~thisArray[1]; goto case 1; + case 1: thisArray[0] = ~thisArray[0]; goto Done; + case 0: goto Done; + } + + uint i = 0; + if (Avx2.IsSupported) + { + Vector256 ones = Vector256.Create(-1); + fixed (int* ptr = thisArray) + { + for (; i < (uint)count - (Vector256IntCount - 1u); i += Vector256IntCount) + { + Vector256 vec = Avx.LoadVector256(ptr + i); + Avx.Store(ptr + i, Avx2.Xor(vec, ones)); + } + } + } + else if (Sse2.IsSupported) + { + Vector128 ones = Vector128.Create(-1); + fixed (int* ptr = thisArray) + { + for (; i < (uint)count - (Vector128IntCount - 1u); i += Vector128IntCount) + { + Vector128 vec = Sse2.LoadVector128(ptr + i); + Sse2.Store(ptr + i, Sse2.Xor(vec, ones)); + } + } + } + else if (AdvSimd.IsSupported) + { + fixed (int* leftPtr = thisArray) + { + for (; i < (uint)count - (Vector128IntCount - 1u); i += Vector128IntCount) + { + Vector128 leftVec = AdvSimd.LoadVector128(leftPtr + i); + AdvSimd.Store(leftPtr + i, AdvSimd.Not(leftVec)); + } + } + } + + for (; i < (uint)count; i++) + { + thisArray[i] = ~thisArray[i]; + } + + Done: + _version++; + return this; + } + + /*========================================================================= + ** Shift all the bit values to right on count bits. The current instance is + ** updated and returned. + * + ** Exceptions: ArgumentOutOfRangeException if count < 0 + =========================================================================*/ + public BitArray RightShift(int count) + { + if (count <= 0) + { + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count), count, CollectionThrowStrings.ArgumentOutOfRange_NeedNonNegNum); + } + + _version++; + return this; + } + + int toIndex = 0; + int ints = GetInt32ArrayLengthFromBitLength(m_length); + if (count < m_length) + { + // We can not use Math.DivRem without taking a dependency on System.Runtime.Extensions + int fromIndex = Div32Rem(count, out int shiftCount); + Div32Rem(m_length, out int extraBits); + if (shiftCount == 0) + { + unchecked + { + // Cannot use `(1u << extraBits) - 1u` as the mask + // because for extraBits == 0, we need the mask to be 111...111, not 0. + // In that case, we are shifting a uint by 32, which could be considered undefined. + // The result of a shift operation is undefined ... if the right operand + // is greater than or equal to the width in bits of the promoted left operand, + // https://docs.microsoft.com/en-us/cpp/c-language/bitwise-shift-operators?view=vs-2017 + // However, the compiler protects us from undefined behaviour by constraining the + // right operand to between 0 and width - 1 (inclusive), i.e. right_operand = (right_operand % width). + uint mask = uint.MaxValue >> (BitsPerInt32 - extraBits); + m_array[ints - 1] &= (int)mask; + } + Array.Copy(m_array, fromIndex, m_array, 0, ints - fromIndex); + toIndex = ints - fromIndex; + } + else + { + int lastIndex = ints - 1; + unchecked + { + while (fromIndex < lastIndex) + { + uint right = (uint)m_array[fromIndex] >> shiftCount; + int left = m_array[++fromIndex] << (BitsPerInt32 - shiftCount); + m_array[toIndex++] = left | (int)right; + } + uint mask = uint.MaxValue >> (BitsPerInt32 - extraBits); + mask &= (uint)m_array[fromIndex]; + m_array[toIndex++] = (int)(mask >> shiftCount); + } + } + } + + m_array.AsSpan(toIndex, ints - toIndex).Clear(); + _version++; + return this; + } + + /*========================================================================= + ** Shift all the bit values to left on count bits. The current instance is + ** updated and returned. + * + ** Exceptions: ArgumentOutOfRangeException if count < 0 + =========================================================================*/ + public BitArray LeftShift(int count) + { + if (count <= 0) + { + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count), count, CollectionThrowStrings.ArgumentOutOfRange_NeedNonNegNum); + } + + _version++; + return this; + } + + int lengthToClear; + if (count < m_length) + { + int lastIndex = (m_length - 1) >> BitShiftPerInt32; // Divide by 32. + + // We can not use Math.DivRem without taking a dependency on System.Runtime.Extensions + lengthToClear = Div32Rem(count, out int shiftCount); + + if (shiftCount == 0) + { + Array.Copy(m_array, 0, m_array, lengthToClear, lastIndex + 1 - lengthToClear); + } + else + { + int fromindex = lastIndex - lengthToClear; + unchecked + { + while (fromindex > 0) + { + int left = m_array[fromindex] << shiftCount; + uint right = (uint)m_array[--fromindex] >> (BitsPerInt32 - shiftCount); + m_array[lastIndex] = left | (int)right; + lastIndex--; + } + m_array[lastIndex] = m_array[fromindex] << shiftCount; + } + } + } + else + { + lengthToClear = GetInt32ArrayLengthFromBitLength(m_length); // Clear all + } + + m_array.AsSpan(0, lengthToClear).Clear(); + _version++; + return this; + } + + public int Length + { + get + { + return m_length; + } + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), value, CollectionThrowStrings.ArgumentOutOfRange_NeedNonNegNum); + } + + int newints = GetInt32ArrayLengthFromBitLength(value); + if (newints > m_array.Length || newints + _ShrinkThreshold < m_array.Length) + { + // grow or shrink (if wasting more than _ShrinkThreshold ints) + Array.Resize(ref m_array, newints); + } + + if (value > m_length) + { + // clear high bit values in the last int + int last = (m_length - 1) >> BitShiftPerInt32; + Div32Rem(m_length, out int bits); + if (bits > 0) + { + m_array[last] &= (1 << bits) - 1; + } + + // clear remaining int values + m_array.AsSpan(last + 1, newints - last - 1).Clear(); + } + + m_length = value; + _version++; + } + } + + public void CopyTo(Span span) + { + int arrayLength = GetByteArrayLengthFromBitLength(m_length); + if (span.Length < arrayLength) + { + throw new ArgumentException(CollectionThrowStrings.Argument_InvalidOffLen); + } + + // equivalent to m_length % BitsPerByte, since BitsPerByte is a power of 2 + uint extraBits = (uint)m_length & (BitsPerByte - 1); + if (extraBits > 0) + { + // last byte is not aligned, we will directly copy one less byte + arrayLength -= 1; + } + + int quotient = Div4Rem(arrayLength, out int remainder); + for (int i = 0; i < quotient; i++) + { + BinaryPrimitives.WriteInt32LittleEndian(span, m_array[i]); + span = span[4..]; + } + + if (extraBits > 0) + { + Debug.Assert(span.Length > 0); + Debug.Assert(m_array.Length > quotient); + // mask the final byte + span[remainder] = (byte)((m_array[quotient] >> (remainder * 8)) & ((1 << (int)extraBits) - 1)); + } + + switch (remainder) + { + case 3: + span[2] = (byte)(m_array[quotient] >> 16); + goto case 2; + // fall through + case 2: + span[1] = (byte)(m_array[quotient] >> 8); + goto case 1; + // fall through + case 1: + span[0] = (byte)m_array[quotient]; + break; + } + } + + public unsafe void CopyTo(Array array, int index) + { + if (array == null) + { + throw new ArgumentNullException(nameof(array)); + } + + if (index < 0) + { + throw new ArgumentOutOfRangeException(nameof(index), index, CollectionThrowStrings.ArgumentOutOfRange_NeedNonNegNum); + } + + if (array.Rank != 1) + { + throw new ArgumentException(CollectionThrowStrings.Arg_RankMultiDimNotSupported, nameof(array)); + } + + if (array is int[] intArray) + { + Div32Rem(m_length, out int extraBits); + + if (extraBits == 0) + { + // we have perfect bit alignment, no need to sanitize, just copy + Array.Copy(m_array, 0, intArray, index, m_array.Length); + } + else + { + int last = (m_length - 1) >> BitShiftPerInt32; + // do not copy the last int, as it is not completely used + Array.Copy(m_array, 0, intArray, index, last); + + // the last int needs to be masked + intArray[index + last] = m_array[last] & unchecked((1 << extraBits) - 1); + } + } + else if (array is byte[] byteArray) + { + int arrayLength = GetByteArrayLengthFromBitLength(m_length); + if (array.Length - index < arrayLength) + { + throw new ArgumentException(CollectionThrowStrings.Argument_InvalidOffLen); + } + + // equivalent to m_length % BitsPerByte, since BitsPerByte is a power of 2 + uint extraBits = (uint)m_length & (BitsPerByte - 1); + if (extraBits > 0) + { + // last byte is not aligned, we will directly copy one less byte + arrayLength -= 1; + } + + Span span = byteArray.AsSpan(index); + + int quotient = Div4Rem(arrayLength, out int remainder); + for (int i = 0; i < quotient; i++) + { + BinaryPrimitives.WriteInt32LittleEndian(span, m_array[i]); + span = span[4..]; + } + + if (extraBits > 0) + { + Debug.Assert(span.Length > 0); + Debug.Assert(m_array.Length > quotient); + // mask the final byte + span[remainder] = (byte)((m_array[quotient] >> (remainder * 8)) & ((1 << (int)extraBits) - 1)); + } + + switch (remainder) + { + case 3: + span[2] = (byte)(m_array[quotient] >> 16); + goto case 2; + // fall through + case 2: + span[1] = (byte)(m_array[quotient] >> 8); + goto case 1; + // fall through + case 1: + span[0] = (byte)m_array[quotient]; + break; + } + } + else if (array is bool[] boolArray) + { + if (array.Length - index < m_length) + { + throw new ArgumentException(CollectionThrowStrings.Argument_InvalidOffLen); + } + + uint i = 0; + + if (m_length < BitsPerInt32) + { + goto LessThan32; + } + + // The mask used when shuffling a single int into Vector128/256. + // On little endian machines, the lower 8 bits of int belong in the first byte, next lower 8 in the second and so on. + // We place the bytes that contain the bits to its respective byte so that we can mask out only the relevant bits later. + Vector128 lowerShuffleMask_CopyToBoolArray = Vector128.Create(0, 0x01010101_01010101).AsByte(); + Vector128 upperShuffleMask_CopyToBoolArray = Vector128.Create(0x02020202_02020202, 0x03030303_03030303).AsByte(); + + if (Avx2.IsSupported) + { + Vector256 shuffleMask = Vector256.Create(lowerShuffleMask_CopyToBoolArray, upperShuffleMask_CopyToBoolArray); + Vector256 bitMask = Vector256.Create(0x80402010_08040201).AsByte(); + Vector256 ones = Vector256.Create((byte)1); + + fixed (bool* destination = &boolArray[index]) + { + for (; i + Vector256ByteCount <= (uint)m_length; i += Vector256ByteCount) + { + int bits = m_array[i / BitsPerInt32]; + Vector256 scalar = Vector256.Create(bits); + Vector256 shuffled = Avx2.Shuffle(scalar.AsByte(), shuffleMask); + Vector256 extracted = Avx2.And(shuffled, bitMask); + + // The extracted bits can be anywhere between 0 and 255, so we normalise the value to either 0 or 1 + // to ensure compatibility with "C# bool" (0 for false, 1 for true, rest undefined) + Vector256 normalized = Avx2.Min(extracted, ones); + Avx.Store((byte*)destination + i, normalized); + } + } + } + else if (Ssse3.IsSupported) + { + Vector128 lowerShuffleMask = lowerShuffleMask_CopyToBoolArray; + Vector128 upperShuffleMask = upperShuffleMask_CopyToBoolArray; + Vector128 ones = Vector128.Create((byte)1); + Vector128 bitMask128 = BitConverter.IsLittleEndian ? + Vector128.Create(0x80402010_08040201).AsByte() : + Vector128.Create(0x01020408_10204080).AsByte(); + + fixed (bool* destination = &boolArray[index]) + { + for (; i + Vector128ByteCount * 2u <= (uint)m_length; i += Vector128ByteCount * 2u) + { + int bits = m_array[i / BitsPerInt32]; + Vector128 scalar = Vector128.CreateScalarUnsafe(bits); + + Vector128 shuffledLower = Ssse3.Shuffle(scalar.AsByte(), lowerShuffleMask); + Vector128 extractedLower = Sse2.And(shuffledLower, bitMask128); + Vector128 normalizedLower = Sse2.Min(extractedLower, ones); + Sse2.Store((byte*)destination + i, normalizedLower); + + Vector128 shuffledHigher = Ssse3.Shuffle(scalar.AsByte(), upperShuffleMask); + Vector128 extractedHigher = Sse2.And(shuffledHigher, bitMask128); + Vector128 normalizedHigher = Sse2.Min(extractedHigher, ones); + Sse2.Store((byte*)destination + i + Vector128.Count, normalizedHigher); + } + } + } + else if (AdvSimd.IsSupported) + { + Vector128 ones = Vector128.Create((byte)1); + Vector128 bitMask128 = BitConverter.IsLittleEndian ? + Vector128.Create(0x80402010_08040201).AsByte() : + Vector128.Create(0x01020408_10204080).AsByte(); + + fixed (bool* destination = &boolArray[index]) + { + for (; i + Vector128ByteCount * 2u <= (uint)m_length; i += Vector128ByteCount * 2u) + { + int bits = m_array[i / BitsPerInt32]; + // Same logic as SSSE3 path, except we do not have Shuffle instruction. + // (TableVectorLookup could be an alternative - dotnet/runtime#1277) + // Instead we use chained ZIP1/2 instructions: + // (A0 is the byte containing LSB, A3 is the byte containing MSB) + // bits (on Big endian) - A3 A2 A1 A0 + // bits (Little endian) / Byte reversal - A0 A1 A2 A3 + // v1 = Vector128.Create - A0 A1 A2 A3 A0 A1 A2 A3 A0 A1 A2 A3 A0 A1 A2 A3 + // v2 = ZipLow(v1, v1) - A0 A0 A1 A1 A2 A2 A3 A3 A0 A0 A1 A1 A2 A2 A3 A3 + // v3 = ZipLow(v2, v2) - A0 A0 A0 A0 A1 A1 A1 A1 A2 A2 A2 A2 A3 A3 A3 A3 + // shuffledLower = ZipLow(v3, v3) - A0 A0 A0 A0 A0 A0 A0 A0 A1 A1 A1 A1 A1 A1 A1 A1 + // shuffledHigher = ZipHigh(v3, v3) - A2 A2 A2 A2 A2 A2 A2 A2 A3 A3 A3 A3 A3 A3 A3 A3 + if (!BitConverter.IsLittleEndian) + { + bits = BinaryPrimitives.ReverseEndianness(bits); + } + Vector128 vector = Vector128.Create(bits).AsByte(); + vector = AdvSimd.Arm64.ZipLow(vector, vector); + vector = AdvSimd.Arm64.ZipLow(vector, vector); + + Vector128 shuffledLower = AdvSimd.Arm64.ZipLow(vector, vector); + Vector128 extractedLower = AdvSimd.And(shuffledLower, bitMask128); + Vector128 normalizedLower = AdvSimd.Min(extractedLower, ones); + AdvSimd.Store((byte*)destination + i, normalizedLower); + + Vector128 shuffledHigher = AdvSimd.Arm64.ZipHigh(vector, vector); + Vector128 extractedHigher = AdvSimd.And(shuffledHigher, bitMask128); + Vector128 normalizedHigher = AdvSimd.Min(extractedHigher, ones); + AdvSimd.Store((byte*)destination + i + Vector128.Count, normalizedHigher); + } + } + } + + LessThan32: + for (; i < (uint)m_length; i++) + { + int elementIndex = Div32Rem((int)i, out int extraBits); + boolArray[(uint)index + i] = ((m_array[elementIndex] >> extraBits) & 0x00000001) != 0; + } + } + else + { + throw new ArgumentException(CollectionThrowStrings.Arg_BitArrayTypeUnsupported, nameof(array)); + } + } + + public int Count => m_length; + + public object SyncRoot => this; + + public bool IsSynchronized => false; + + public bool IsReadOnly => false; + + public object Clone() => new BitArray(this); + + public IEnumerator GetEnumerator() => new BitArrayEnumeratorSimple(this); + + // XPerY=n means that n Xs can be stored in 1 Y. + private const int BitsPerInt32 = 32; + private const int BitsPerByte = 8; + + private const int BitShiftPerInt32 = 5; + private const int BitShiftPerByte = 3; + private const int BitShiftForBytesPerInt32 = 2; + + /// + /// Used for conversion between different representations of bit array. + /// Returns (n + (32 - 1)) / 32, rearranged to avoid arithmetic overflow. + /// For example, in the bit to int case, the straightforward calc would + /// be (n + 31) / 32, but that would cause overflow. So instead it's + /// rearranged to ((n - 1) / 32) + 1. + /// Due to sign extension, we don't need to special case for n == 0, if we use + /// bitwise operations (since ((n - 1) >> 5) + 1 = 0). + /// This doesn't hold true for ((n - 1) / 32) + 1, which equals 1. + /// + /// Usage: + /// GetArrayLength(77): returns how many ints must be + /// allocated to store 77 bits. + /// + /// + /// how many ints are required to store n bytes + private static int GetInt32ArrayLengthFromBitLength(int n) + { + Debug.Assert(n >= 0); + return (int)((uint)(n - 1 + (1 << BitShiftPerInt32)) >> BitShiftPerInt32); + } + + private static int GetInt32ArrayLengthFromByteLength(int n) + { + Debug.Assert(n >= 0); + // Due to sign extension, we don't need to special case for n == 0, since ((n - 1) >> 2) + 1 = 0 + // This doesn't hold true for ((n - 1) / 4) + 1, which equals 1. + return (int)((uint)(n - 1 + (1 << BitShiftForBytesPerInt32)) >> BitShiftForBytesPerInt32); + } + + public static int GetByteArrayLengthFromBitLength(int n) + { + Debug.Assert(n >= 0); + // Due to sign extension, we don't need to special case for n == 0, since ((n - 1) >> 3) + 1 = 0 + // This doesn't hold true for ((n - 1) / 8) + 1, which equals 1. + return (int)((uint)(n - 1 + (1 << BitShiftPerByte)) >> BitShiftPerByte); + } + + private static int Div32Rem(int number, out int remainder) + { + uint quotient = (uint)number / 32; + remainder = number & (32 - 1); // equivalent to number % 32, since 32 is a power of 2 + return (int)quotient; + } + + private static int Div4Rem(int number, out int remainder) + { + uint quotient = (uint)number / 4; + remainder = number & (4 - 1); // equivalent to number % 4, since 4 is a power of 2 + return (int)quotient; + } + + private static void ThrowArgumentOutOfRangeException(int index) + { + throw new ArgumentOutOfRangeException(nameof(index), index, CollectionThrowStrings.ArgumentOutOfRange_Index); + } + + private sealed class BitArrayEnumeratorSimple : IEnumerator, ICloneable + { + private readonly BitArray _bitArray; + private int _index; + private readonly int _version; + private bool _currentElement; + + internal BitArrayEnumeratorSimple(BitArray bitArray) + { + _bitArray = bitArray; + _index = -1; + _version = bitArray._version; + } + + public object Clone() => MemberwiseClone(); + + public bool MoveNext() + { + if (_version != _bitArray._version) + { + throw new InvalidOperationException(CollectionThrowStrings.InvalidOperation_EnumFailedVersion); + } + + if (_index < _bitArray.m_length - 1) + { + _index++; + _currentElement = _bitArray.Get(_index); + return true; + } + else + { + _index = _bitArray.m_length; + } + + return false; + } + + public object Current + { + get + { + if ((uint)_index >= (uint)_bitArray.m_length) + { + throw GetInvalidOperationException(_index); + } + + return _currentElement; + } + } + + public void Reset() + { + if (_version != _bitArray._version) + { + throw new InvalidOperationException(CollectionThrowStrings.InvalidOperation_EnumFailedVersion); + } + + _index = -1; + } + + private InvalidOperationException GetInvalidOperationException(int index) + { + if (index == -1) + { + return new InvalidOperationException(CollectionThrowStrings.InvalidOperation_EnumNotStarted); + } + + Debug.Assert(index >= _bitArray.m_length); + return new InvalidOperationException(CollectionThrowStrings.InvalidOperation_EnumEnded); + } + } +} diff --git a/Projects/Server/Collections/CollectionThrowStrings.cs b/Projects/Server/Collections/CollectionThrowStrings.cs index 41feda828..1730c6334 100644 --- a/Projects/Server/Collections/CollectionThrowStrings.cs +++ b/Projects/Server/Collections/CollectionThrowStrings.cs @@ -13,33 +13,43 @@ * along with this program. If not, see . * *************************************************************************/ -namespace Server.Collections +namespace Server.Collections; + +public static class CollectionThrowStrings { - public static class CollectionThrowStrings - { - public const string ArgumentOutOfRange_Index = - "Index was out of range. Must be non-negative and less than the size of the collection."; + public const string ArgumentOutOfRange_Index = + "Index was out of range. Must be non-negative and less than the size of the collection."; - public const string ArgumentOutOfRange_NeedNonNegNum = "Non-negative number required."; + public const string ArgumentOutOfRange_NeedNonNegNum = "Non-negative number required."; - public const string Argument_InvalidOffLen = - "Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection."; + public const string Argument_InvalidOffLen = + "Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection."; - public const string Argument_AddingDuplicate = "An item with the same value has already been added. Value: {0}"; + public const string Argument_AddingDuplicate = "An item with the same value has already been added. Value: {0}"; - public const string Arg_ArrayPlusOffTooSmall = - "Destination array is not long enough to copy all the items in the collection. Check array index and length."; + public const string Arg_ArrayPlusOffTooSmall = + "Destination array is not long enough to copy all the items in the collection. Check array index and length."; - public const string InvalidOperation_ConcurrentOperationsNotSupported = - "Operations that change non-concurrent collections must have exclusive access. A concurrent update was performed on this collection and corrupted its state. The collection's state is no longer correct."; + public const string InvalidOperation_ConcurrentOperationsNotSupported = + "Operations that change non-concurrent collections must have exclusive access. A concurrent update was performed on this collection and corrupted its state. The collection's state is no longer correct."; - public const string InvalidOperation_EnumFailedVersion = - "Collection was modified after the enumerator was instantiated."; + public const string InvalidOperation_EnumFailedVersion = + "Collection was modified after the enumerator was instantiated."; - public const string InvalidOperation_EmptyQueue = "Queue empty."; + public const string InvalidOperation_EmptyQueue = "Queue empty."; - public const string InvalidOperation_EnumNotStarted = "Enumeration has not started. Call MoveNext."; + public const string InvalidOperation_EnumNotStarted = "Enumeration has not started. Call MoveNext."; - public const string InvalidOperation_EnumEnded = "Enumeration already finished."; - } + public const string InvalidOperation_EnumEnded = "Enumeration already finished."; + + public const string Argument_ArrayTooLarge = + "The input array length must not exceed Int32.MaxValue / {0}. Otherwise BitArray.Length would exceed Int32.MaxValue."; + + public const string Arg_ArrayLengthsDiffer = "Array lengths must be the same."; + + public const string Arg_RankMultiDimNotSupported = + "Only single dimensional arrays are supported for the requested action."; + + public const string Arg_BitArrayTypeUnsupported = + "Only supported array types for CopyTo on BitArrays are Boolean[], Int32[] and Byte[]."; } diff --git a/Projects/Server/Serialization/BinaryFileReader.cs b/Projects/Server/Serialization/BinaryFileReader.cs index 81064faa3..e7ba33642 100644 --- a/Projects/Server/Serialization/BinaryFileReader.cs +++ b/Projects/Server/Serialization/BinaryFileReader.cs @@ -16,6 +16,7 @@ using System; using System.IO; using System.Runtime.CompilerServices; +using Server.Collections; namespace Server { @@ -78,6 +79,15 @@ namespace Server [MethodImpl(MethodImplOptions.AggressiveInlining)] public int Read(Span buffer) => _reader.Read(buffer); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public BitArray ReadBitArray() + { + var length = ((IGenericReader)this).ReadEncodedInt(); + + // BinaryReader doesn't expose a Span slice of the buffer, so we use a custom ctor + return new BitArray(_reader, length); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public long Seek(long offset, SeekOrigin origin) => _reader.BaseStream.Seek(offset, origin); diff --git a/Projects/Server/Serialization/BufferReader.cs b/Projects/Server/Serialization/BufferReader.cs index 309ac26f4..91641c343 100644 --- a/Projects/Server/Serialization/BufferReader.cs +++ b/Projects/Server/Serialization/BufferReader.cs @@ -19,6 +19,7 @@ using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; using System.Text; +using Server.Collections; using Server.Text; namespace Server @@ -156,6 +157,19 @@ namespace Server return length; } + public BitArray ReadBitArray() + { + var length = ((IGenericReader)this).ReadEncodedInt(); + if (length > _buffer.Length - _position) + { + throw new OutOfMemoryException(); + } + + var bitArray = new BitArray(_buffer.AsSpan(_position, length)); + _position += length; + return bitArray; + } + public virtual long Seek(long offset, SeekOrigin origin) { Debug.Assert( diff --git a/Projects/Server/Serialization/BufferWriter.cs b/Projects/Server/Serialization/BufferWriter.cs index 208039916..167d24edb 100644 --- a/Projects/Server/Serialization/BufferWriter.cs +++ b/Projects/Server/Serialization/BufferWriter.cs @@ -18,6 +18,7 @@ using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; using System.Text; +using Server.Collections; using Server.Text; namespace Server @@ -134,6 +135,16 @@ namespace Server } } + public void Write(BitArray bitArray) + { + var byteLength = BitArray.GetByteArrayLengthFromBitLength(bitArray.Length); + FlushIfNeeded(byteLength + 4); + + ((IGenericWriter)this).WriteEncodedInt(byteLength); + bitArray.CopyTo(_buffer.AsSpan((int)Index, byteLength)); + Index += byteLength; + } + public virtual long Seek(long offset, SeekOrigin origin) { Debug.Assert( diff --git a/Projects/Server/Serialization/IGenericReader.cs b/Projects/Server/Serialization/IGenericReader.cs index 67e27764e..5abf069aa 100644 --- a/Projects/Server/Serialization/IGenericReader.cs +++ b/Projects/Server/Serialization/IGenericReader.cs @@ -16,6 +16,7 @@ using System; using System.IO; using System.Net; +using Server.Collections; namespace Server { @@ -116,6 +117,8 @@ namespace Server return new Guid(bytes); } + BitArray ReadBitArray(); + long Seek(long offset, SeekOrigin origin); } } diff --git a/Projects/Server/Serialization/IGenericWriter.cs b/Projects/Server/Serialization/IGenericWriter.cs index 95aa9d92f..8eeec249f 100644 --- a/Projects/Server/Serialization/IGenericWriter.cs +++ b/Projects/Server/Serialization/IGenericWriter.cs @@ -16,6 +16,7 @@ using System; using System.IO; using System.Net; +using Server.Collections; namespace Server { @@ -160,6 +161,8 @@ namespace Server Write(stack); } + void Write(BitArray bitArray); + long Seek(long offset, SeekOrigin origin); } }