diff --git a/BitsKit.Benchmarks/BitsKitBenchmark.BatchPrimitives.cs b/BitsKit.Benchmarks/BitsKitBenchmark.BatchPrimitives.cs new file mode 100644 index 0000000..f74a959 --- /dev/null +++ b/BitsKit.Benchmarks/BitsKitBenchmark.BatchPrimitives.cs @@ -0,0 +1,133 @@ +using BenchmarkDotNet.Attributes; +using BitsKit.Primitives; + +namespace BitsKit.Benchmarks; + +public partial class BitsKitBenchmark +{ + private const int BatchOperations = 4096; + private const int BatchBitOffset = 3; + private const int BatchBitCount = 12; + private const int BatchBitStride = 32; + + private readonly byte[] _batchReadBuffer = CreateBatchBuffer(); + private readonly byte[] _batchScalarWriteBuffer = CreateBatchBuffer(); + private readonly byte[] _batchWriteBuffer = CreateBatchBuffer(); + private readonly uint[] _batchScalarResults = new uint[BatchOperations]; + private readonly uint[] _batchResults = new uint[BatchOperations]; + private readonly uint[] _batchValues = CreateBatchValues(); + + [Benchmark(OperationsPerInvoke = BatchOperations)] + [BenchmarkCategory("BatchPrimitives", "ScalarLoop", "Read", "Contiguous", "LSB")] + public uint BatchReadUInt32LSBScalarLoop() + { + for (int i = 0; i < _batchScalarResults.Length; i++) + { + _batchScalarResults[i] = BitPrimitives.ReadUInt32LSB( + _batchReadBuffer, + BatchBitOffset + i * BatchBitCount, + BatchBitCount); + } + + return _batchScalarResults[BatchOperations - 1]; + } + + [Benchmark(OperationsPerInvoke = BatchOperations)] + [BenchmarkCategory("BatchPrimitives", "Batch", "Read", "Contiguous", "LSB")] + public uint BatchReadUInt32LSB() + { + BitBatchPrimitives.ReadUInt32LSB( + _batchReadBuffer, + BatchBitOffset, + BatchBitCount, + _batchResults); + return _batchResults[BatchOperations - 1]; + } + + [Benchmark(OperationsPerInvoke = BatchOperations)] + [BenchmarkCategory("BatchPrimitives", "GeneratedAccessor", "Read", "Contiguous", "LSB")] + public uint BatchGeneratedReadUInt32LSB() + { + GeneratedBatchAccessorModel.ReadValueBatch(_batchReadBuffer, _batchResults); + return _batchResults[BatchOperations - 1]; + } + + [Benchmark(OperationsPerInvoke = BatchOperations)] + [BenchmarkCategory("BatchPrimitives", "ScalarLoop", "Read", "Strided", "LSB")] + public uint BatchStridedReadUInt32LSBScalarLoop() + { + for (int i = 0; i < _batchScalarResults.Length; i++) + { + _batchScalarResults[i] = BitPrimitives.ReadUInt32LSB( + _batchReadBuffer, + BatchBitOffset + i * BatchBitStride, + BatchBitCount); + } + + return _batchScalarResults[BatchOperations - 1]; + } + + [Benchmark(OperationsPerInvoke = BatchOperations)] + [BenchmarkCategory("BatchPrimitives", "Batch", "Read", "Strided", "LSB")] + public uint BatchStridedReadUInt32LSB() + { + BitBatchPrimitives.ReadUInt32LSB( + _batchReadBuffer, + BatchBitOffset, + BatchBitCount, + BatchBitStride, + _batchResults); + return _batchResults[BatchOperations - 1]; + } + + [Benchmark(OperationsPerInvoke = BatchOperations)] + [BenchmarkCategory("BatchPrimitives", "ScalarLoop", "Write", "Contiguous", "LSB")] + public byte BatchWriteUInt32LSBScalarLoop() + { + for (int i = 0; i < _batchValues.Length; i++) + { + BitPrimitives.WriteUInt32LSB( + _batchScalarWriteBuffer, + BatchBitOffset + i * BatchBitCount, + _batchValues[i], + BatchBitCount); + } + + return _batchScalarWriteBuffer[0]; + } + + [Benchmark(OperationsPerInvoke = BatchOperations)] + [BenchmarkCategory("BatchPrimitives", "Batch", "Write", "Contiguous", "LSB")] + public byte BatchWriteUInt32LSB() + { + BitBatchPrimitives.WriteUInt32LSB( + _batchWriteBuffer, + BatchBitOffset, + BatchBitCount, + _batchValues); + return _batchWriteBuffer[0]; + } + + [Benchmark(OperationsPerInvoke = BatchOperations)] + [BenchmarkCategory("BatchPrimitives", "GeneratedAccessor", "Write", "Contiguous", "LSB")] + public byte BatchGeneratedWriteUInt32LSB() + { + GeneratedBatchAccessorModel.WriteValueBatch(_batchWriteBuffer, _batchValues); + return _batchWriteBuffer[0]; + } + + private static byte[] CreateBatchBuffer() + { + var buffer = new byte[((BatchOperations - 1) * BatchBitStride + BatchBitOffset + BatchBitCount + 7) / 8]; + FillBuffer(buffer); + return buffer; + } + + private static uint[] CreateBatchValues() + { + var values = new uint[BatchOperations]; + for (int i = 0; i < values.Length; i++) + values[i] = unchecked((uint)i * 0x9E3779B9u); + return values; + } +} diff --git a/BitsKit.Benchmarks/GeneratedAccessorBenchmarkModels.cs b/BitsKit.Benchmarks/GeneratedAccessorBenchmarkModels.cs index 6da7698..39b5470 100644 --- a/BitsKit.Benchmarks/GeneratedAccessorBenchmarkModels.cs +++ b/BitsKit.Benchmarks/GeneratedAccessorBenchmarkModels.cs @@ -54,6 +54,14 @@ public enum GeneratedAccessorKind : uint Seven } +[BitObject(BitOrder.LeastSignificant, GenerateBatchAccessors = true)] +public partial struct GeneratedBatchAccessorModel +{ + [BitField(3)] + [BitField("Value", 12, BitFieldType.UInt32)] + public Memory BackingField; +} + [BitObject(BitOrder.LeastSignificant)] public partial struct GeneratedAccessorMemoryModel { diff --git a/BitsKit.Generator/Models/BitFieldModel.cs b/BitsKit.Generator/Models/BitFieldModel.cs index d0d4559..f65a5f3 100644 --- a/BitsKit.Generator/Models/BitFieldModel.cs +++ b/BitsKit.Generator/Models/BitFieldModel.cs @@ -48,16 +48,7 @@ public BitFieldModel(AttributeData attributeData, TypeSymbolProcessor? typeSymbo public void GenerateCSharpSource(StringBuilder sb) { - string accessor = (Modifiers & BitFieldModifiers.AccessorMask) switch - { - BitFieldModifiers.Public => "public", - BitFieldModifiers.Private => "private", - BitFieldModifiers.Protected => "protected", - BitFieldModifiers.Internal => "internal", - BitFieldModifiers.ProtectedInternal => "protected internal", - BitFieldModifiers.PrivateProtected => "private protected", - _ => "public", - }; + string accessor = GetAccessor(); // property sb.AppendIndentedLine(2, @@ -103,6 +94,56 @@ public void GenerateCSharpSource(StringBuilder sb) .AppendLine(); } + public void GenerateBatchAccessors(StringBuilder sb) + { + string accessor = GetAccessor(); + string valueType = ReturnType ?? FieldType!.Value.ToString(); + string primitiveName = this is BooleanFieldModel ? "Bit" : FieldType!.Value.ToIntegralName(); + string bitCountArgument = this is BooleanFieldModel ? string.Empty : $", {BitCount}"; + string readDestination = this is EnumFieldModel + ? $"MemoryMarshal.Cast<{valueType}, {FieldType!.Value}>(destination)" + : "destination"; + string writeValues = this is EnumFieldModel + ? $"MemoryMarshal.Cast<{valueType}, {FieldType!.Value}>(values)" + : "values"; + + sb.AppendIndentedLine(2, + $"{accessor} static void Read{Name}Batch(ReadOnlySpan source, Span<{valueType}> destination) =>") + .AppendIndentedLine(3, + $"BitBatchPrimitives.Read{primitiveName}{BitOrder.ToShortName()}(source, {BitOffset}{bitCountArgument}, {readDestination});") + .AppendLine() + .AppendIndentedLine(2, + $"{accessor} static void Read{Name}Batch(ReadOnlySpan source, Int32 bitStride, Span<{valueType}> destination) =>") + .AppendIndentedLine(3, + $"BitBatchPrimitives.Read{primitiveName}{BitOrder.ToShortName()}(source, {BitOffset}{bitCountArgument}, bitStride, {readDestination});") + .AppendLine(); + + if (IsReadOnly()) + return; + + sb.AppendIndentedLine(2, + $"{accessor} static void Write{Name}Batch(Span destination, ReadOnlySpan<{valueType}> values) =>") + .AppendIndentedLine(3, + $"BitBatchPrimitives.Write{primitiveName}{BitOrder.ToShortName()}(destination, {BitOffset}{bitCountArgument}, {writeValues});") + .AppendLine() + .AppendIndentedLine(2, + $"{accessor} static void Write{Name}Batch(Span destination, Int32 bitStride, ReadOnlySpan<{valueType}> values) =>") + .AppendIndentedLine(3, + $"BitBatchPrimitives.Write{primitiveName}{BitOrder.ToShortName()}(destination, {BitOffset}{bitCountArgument}, bitStride, {writeValues});") + .AppendLine(); + } + + private string GetAccessor() => (Modifiers & BitFieldModifiers.AccessorMask) switch + { + BitFieldModifiers.Public => "public", + BitFieldModifiers.Private => "private", + BitFieldModifiers.Protected => "protected", + BitFieldModifiers.Internal => "internal", + BitFieldModifiers.ProtectedInternal => "protected internal", + BitFieldModifiers.PrivateProtected => "private protected", + _ => "public", + }; + /// /// Generates a template for the property accessors, type and name /// diff --git a/BitsKit.Generator/TypeSymbolProcessor.cs b/BitsKit.Generator/TypeSymbolProcessor.cs index cbf8ba8..52690e9 100644 --- a/BitsKit.Generator/TypeSymbolProcessor.cs +++ b/BitsKit.Generator/TypeSymbolProcessor.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; -using Microsoft.CodeAnalysis; +using System.Linq; using System.Text; using BitsKit.Generator.Models; -using System.Linq; +using Microsoft.CodeAnalysis; namespace BitsKit.Generator; @@ -13,6 +13,7 @@ internal sealed record TypeSymbolProcessor public BitOrder DefaultBitOrder { get; } public BitObjectAccessMode AccessMode { get; } + public bool GenerateBatchAccessors { get; } public bool IsStruct { get; } public bool IsInlineArray { get; } @@ -36,6 +37,7 @@ public TypeSymbolProcessor(INamedTypeSymbol typeSymbol, AttributeData attribute) DefaultBitOrder = (BitOrder)attribute.ConstructorArguments[0].Value!; AccessMode = GetAccessMode(attribute); + GenerateBatchAccessors = GetGenerateBatchAccessors(attribute); IsStruct = typeSymbol.TypeKind == TypeKind.Struct; IsInlineArray = HasInlineArrayAttribute(typeSymbol); @@ -53,6 +55,17 @@ private static BitObjectAccessMode GetAccessMode(AttributeData attribute) return BitObjectAccessMode.Checked; } + private static bool GetGenerateBatchAccessors(AttributeData attribute) + { + foreach (KeyValuePair argument in attribute.NamedArguments) + { + if (argument.Key == "GenerateBatchAccessors" && argument.Value.Value is bool value) + return value; + } + + return false; + } + public void GenerateCSharpSource(StringBuilder sb) { sb.AppendIndentedLine(1, @@ -64,6 +77,12 @@ public void GenerateCSharpSource(StringBuilder sb) foreach (BitFieldModel field in Fields) field.GenerateCSharpSource(sb); + if (GenerateBatchAccessors) + { + foreach (BitFieldModel field in Fields) + field.GenerateBatchAccessors(sb); + } + sb.RemoveLastLine() .AppendIndentedLine(1, "}") .AppendLine(); diff --git a/BitsKit.Tests/BatchGeneratedAccessorTests.cs b/BitsKit.Tests/BatchGeneratedAccessorTests.cs new file mode 100644 index 0000000..65e9488 --- /dev/null +++ b/BitsKit.Tests/BatchGeneratedAccessorTests.cs @@ -0,0 +1,92 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace BitsKit.Tests; + +[TestClass] +public class BatchGeneratedAccessorTests +{ + [TestMethod] + public void GeneratedStridedReadersUseDeclaredFieldLayouts() + { + const int RecordStride = 24; + const int RecordCount = 32; + var random = new Random(0x6E4A7); + var source = new byte[RecordCount * 3]; + random.NextBytes(source); + + var values = new ushort[RecordCount]; + var flags = new bool[RecordCount]; + var kinds = new TestEnum[RecordCount]; + var bigEndianValues = new uint[RecordCount]; + + BatchAccessorStruct.ReadValueBatch(source, RecordStride, values); + BatchAccessorStruct.ReadFlagBatch(source, RecordStride, flags); + BatchAccessorStruct.ReadKindBatch(source, RecordStride, kinds); + BatchAccessorStruct.ReadBigEndianValueBatch(source, RecordStride, bigEndianValues); + + for (int i = 0; i < RecordCount; i++) + { + int recordOffset = i * RecordStride; + Assert.AreEqual((ushort)Helpers.ReadBitsLSB(source, recordOffset + 3, 12), values[i]); + Assert.AreEqual(Helpers.ReadBitsLSB(source, recordOffset + 15, 1) != 0, flags[i]); + int rawKind = (int)Helpers.ReadBitsLSB(source, recordOffset + 16, 2); + Assert.AreEqual((TestEnum)((rawKind << 30) >> 30), kinds[i]); + Assert.AreEqual((uint)Helpers.ReadBitsMSB(source, recordOffset + 5, 11), bigEndianValues[i]); + } + } + + [TestMethod] + public void GeneratedStridedWritersUseDeclaredFieldLayouts() + { + const int RecordStride = 24; + const int RecordCount = 32; + var random = new Random(0xBA7C8); + var original = new byte[RecordCount * 3]; + random.NextBytes(original); + + var values = new ushort[RecordCount]; + var flags = new bool[RecordCount]; + var kinds = new TestEnum[RecordCount]; + for (int i = 0; i < RecordCount; i++) + { + values[i] = unchecked((ushort)random.Next()); + flags[i] = (i & 1) != 0; + kinds[i] = (TestEnum)(i & 3); + } + + var expected = (byte[])original.Clone(); + for (int i = 0; i < RecordCount; i++) + { + int recordOffset = i * RecordStride; + Helpers.WriteBitsLSB(expected, recordOffset + 3, values[i], 12); + Helpers.WriteBitsLSB(expected, recordOffset + 15, flags[i] ? 1UL : 0UL, 1); + Helpers.WriteBitsLSB(expected, recordOffset + 16, (ulong)kinds[i], 2); + } + + var actual = (byte[])original.Clone(); + BatchAccessorStruct.WriteValueBatch(actual, RecordStride, values); + BatchAccessorStruct.WriteFlagBatch(actual, RecordStride, flags); + BatchAccessorStruct.WriteKindBatch(actual, RecordStride, kinds); + + CollectionAssert.AreEqual(expected, actual); + } + + [TestMethod] + public void GeneratedPackedOverloadsAdvanceByFieldWidth() + { + const int ValueCount = 16; + var values = new ushort[ValueCount]; + for (int i = 0; i < values.Length; i++) + values[i] = (ushort)(i * 71); + + int bitLength = 3 + values.Length * 12; + var storage = new byte[(bitLength + 7) / 8]; + + BatchAccessorStruct.WriteValueBatch(storage, values); + + var actual = new ushort[ValueCount]; + BatchAccessorStruct.ReadValueBatch(storage, actual); + CollectionAssert.AreEqual(values, actual); + } +} diff --git a/BitsKit.Tests/BatchPrimitiveTests.cs b/BitsKit.Tests/BatchPrimitiveTests.cs new file mode 100644 index 0000000..76fdd59 --- /dev/null +++ b/BitsKit.Tests/BatchPrimitiveTests.cs @@ -0,0 +1,222 @@ +using System; +using BitsKit.Primitives; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace BitsKit.Tests; + +[TestClass] +public class BatchPrimitiveTests +{ + [TestMethod] + public void ContiguousUInt32BatchesMatchReferenceOperations() + { + const int BitOffset = 3; + const int BitCount = 12; + const int ValueCount = 32; + int byteCount = (BitOffset + BitCount * ValueCount + 7) / 8; + var random = new Random(0xBA7C4); + var source = new byte[byteCount]; + random.NextBytes(source); + + var actual = new uint[ValueCount]; + BitBatchPrimitives.ReadUInt32LSB(source, BitOffset, BitCount, actual); + + for (int i = 0; i < actual.Length; i++) + Assert.AreEqual((uint)Helpers.ReadBitsLSB(source, BitOffset + i * BitCount, BitCount), actual[i]); + + var values = new uint[ValueCount]; + for (int i = 0; i < values.Length; i++) + values[i] = unchecked((uint)random.NextInt64()); + + var expected = (byte[])source.Clone(); + var destination = (byte[])source.Clone(); + for (int i = 0; i < values.Length; i++) + Helpers.WriteBitsLSB(expected, BitOffset + i * BitCount, values[i], BitCount); + + BitBatchPrimitives.WriteUInt32LSB(destination, BitOffset, BitCount, values); + CollectionAssert.AreEqual(expected, destination); + } + + [TestMethod] + public void StridedUInt64MsbBatchesMatchReferenceOperations() + { + const int BitOffset = 5; + const int BitCount = 43; + const int BitStride = 64; + const int ValueCount = 20; + int byteCount = (BitOffset + (ValueCount - 1) * BitStride + BitCount + 7) / 8; + var random = new Random(0x57A1D); + var source = new byte[byteCount]; + random.NextBytes(source); + + var actual = new ulong[ValueCount]; + BitBatchPrimitives.ReadUInt64MSB(source, BitOffset, BitCount, BitStride, actual); + + for (int i = 0; i < actual.Length; i++) + Assert.AreEqual(Helpers.ReadBitsMSB(source, BitOffset + i * BitStride, BitCount), actual[i]); + + var values = new ulong[ValueCount]; + for (int i = 0; i < values.Length; i++) + values[i] = unchecked((ulong)random.NextInt64()); + + var expected = (byte[])source.Clone(); + var destination = (byte[])source.Clone(); + for (int i = 0; i < values.Length; i++) + Helpers.WriteBitsMSB(expected, BitOffset + i * BitStride, values[i], BitCount); + + BitBatchPrimitives.WriteUInt64MSB(destination, BitOffset, BitCount, BitStride, values); + CollectionAssert.AreEqual(expected, destination); + } + + [TestMethod] + public void SignedAndBooleanBatchesPreserveTheirSemantics() + { + const int ValueCount = 24; + var signedSource = new byte[48]; + var booleanSource = new byte[12]; + var random = new Random(0x519E0); + random.NextBytes(signedSource); + random.NextBytes(booleanSource); + + var signed = new short[ValueCount]; + BitBatchPrimitives.ReadInt16LSB(signedSource, 2, 9, 15, signed); + for (int i = 0; i < signed.Length; i++) + { + int raw = (int)Helpers.ReadBitsLSB(signedSource, 2 + i * 15, 9); + short expected = (short)((raw << 23) >> 23); + Assert.AreEqual(expected, signed[i]); + } + + var bits = new bool[ValueCount]; + BitBatchPrimitives.ReadBitMSB(booleanSource, 4, 3, bits); + for (int i = 0; i < bits.Length; i++) + Assert.AreEqual(Helpers.ReadBitsMSB(booleanSource, 4 + i * 3, 1) != 0, bits[i]); + + for (int i = 0; i < bits.Length; i++) + bits[i] = (i & 2) != 0; + + var expectedBits = (byte[])booleanSource.Clone(); + var actualBits = (byte[])booleanSource.Clone(); + for (int i = 0; i < bits.Length; i++) + Helpers.WriteBitsMSB(expectedBits, 4 + i * 3, bits[i] ? 1UL : 0UL, 1); + + BitBatchPrimitives.WriteBitMSB(actualBits, 4, 3, bits); + CollectionAssert.AreEqual(expectedBits, actualBits); + } + + [TestMethod] + public void BatchValidationRejectsInvalidRangesAndOverlappingWrites() + { + byte[] buffer = new byte[4]; + uint[] values = new uint[2]; + + Assert.ThrowsExactly(() => + BitBatchPrimitives.ReadUInt32LSB(buffer, -1, 8, values)); + Assert.ThrowsExactly(() => + BitBatchPrimitives.ReadUInt32LSB(buffer, 0, 33, values)); + Assert.ThrowsExactly(() => + BitBatchPrimitives.ReadUInt32LSB(buffer, 0, 8, 7, values)); + Assert.ThrowsExactly(() => + BitBatchPrimitives.ReadUInt32LSB(buffer, 17, 8, 8, values)); + Assert.ThrowsExactly(() => + BitBatchPrimitives.WriteUInt32LSB(buffer, 0, 8, 7, values)); + } + + [TestMethod] + public void EveryIntegralBatchTypeIsAvailableForBothBitOrders() + { + ReadOnlySpan source = []; + Span destination = []; + + BitBatchPrimitives.ReadInt8LSB(source, 0, 0, Span.Empty); + BitBatchPrimitives.ReadUInt8LSB(source, 0, 0, Span.Empty); + BitBatchPrimitives.ReadInt16LSB(source, 0, 0, Span.Empty); + BitBatchPrimitives.ReadUInt16LSB(source, 0, 0, Span.Empty); + BitBatchPrimitives.ReadInt32LSB(source, 0, 0, Span.Empty); + BitBatchPrimitives.ReadUInt32LSB(source, 0, 0, Span.Empty); + BitBatchPrimitives.ReadInt64LSB(source, 0, 0, Span.Empty); + BitBatchPrimitives.ReadUInt64LSB(source, 0, 0, Span.Empty); + BitBatchPrimitives.ReadIntPtrLSB(source, 0, 0, Span.Empty); + BitBatchPrimitives.ReadUIntPtrLSB(source, 0, 0, Span.Empty); + + BitBatchPrimitives.ReadInt8MSB(source, 0, 0, Span.Empty); + BitBatchPrimitives.ReadUInt8MSB(source, 0, 0, Span.Empty); + BitBatchPrimitives.ReadInt16MSB(source, 0, 0, Span.Empty); + BitBatchPrimitives.ReadUInt16MSB(source, 0, 0, Span.Empty); + BitBatchPrimitives.ReadInt32MSB(source, 0, 0, Span.Empty); + BitBatchPrimitives.ReadUInt32MSB(source, 0, 0, Span.Empty); + BitBatchPrimitives.ReadInt64MSB(source, 0, 0, Span.Empty); + BitBatchPrimitives.ReadUInt64MSB(source, 0, 0, Span.Empty); + BitBatchPrimitives.ReadIntPtrMSB(source, 0, 0, Span.Empty); + BitBatchPrimitives.ReadUIntPtrMSB(source, 0, 0, Span.Empty); + + BitBatchPrimitives.WriteInt8LSB(destination, 0, 0, ReadOnlySpan.Empty); + BitBatchPrimitives.WriteUInt8LSB(destination, 0, 0, ReadOnlySpan.Empty); + BitBatchPrimitives.WriteInt16LSB(destination, 0, 0, ReadOnlySpan.Empty); + BitBatchPrimitives.WriteUInt16LSB(destination, 0, 0, ReadOnlySpan.Empty); + BitBatchPrimitives.WriteInt32LSB(destination, 0, 0, ReadOnlySpan.Empty); + BitBatchPrimitives.WriteUInt32LSB(destination, 0, 0, ReadOnlySpan.Empty); + BitBatchPrimitives.WriteInt64LSB(destination, 0, 0, ReadOnlySpan.Empty); + BitBatchPrimitives.WriteUInt64LSB(destination, 0, 0, ReadOnlySpan.Empty); + BitBatchPrimitives.WriteIntPtrLSB(destination, 0, 0, ReadOnlySpan.Empty); + BitBatchPrimitives.WriteUIntPtrLSB(destination, 0, 0, ReadOnlySpan.Empty); + + BitBatchPrimitives.WriteInt8MSB(destination, 0, 0, ReadOnlySpan.Empty); + BitBatchPrimitives.WriteUInt8MSB(destination, 0, 0, ReadOnlySpan.Empty); + BitBatchPrimitives.WriteInt16MSB(destination, 0, 0, ReadOnlySpan.Empty); + BitBatchPrimitives.WriteUInt16MSB(destination, 0, 0, ReadOnlySpan.Empty); + BitBatchPrimitives.WriteInt32MSB(destination, 0, 0, ReadOnlySpan.Empty); + BitBatchPrimitives.WriteUInt32MSB(destination, 0, 0, ReadOnlySpan.Empty); + BitBatchPrimitives.WriteInt64MSB(destination, 0, 0, ReadOnlySpan.Empty); + BitBatchPrimitives.WriteUInt64MSB(destination, 0, 0, ReadOnlySpan.Empty); + BitBatchPrimitives.WriteIntPtrMSB(destination, 0, 0, ReadOnlySpan.Empty); + BitBatchPrimitives.WriteUIntPtrMSB(destination, 0, 0, ReadOnlySpan.Empty); + } + + [TestMethod] + public void EveryIntegralBatchReaderMatchesScalarPrimitives() + { + const int BitOffset = 7; + const int BitStride = 73; + const int ValueCount = 8; + var source = new byte[80]; + new Random(0xA11B47).NextBytes(source); + + var int8Lsb = new sbyte[ValueCount]; + var uint8Msb = new byte[ValueCount]; + var int16Msb = new short[ValueCount]; + var uint16Lsb = new ushort[ValueCount]; + var int32Lsb = new int[ValueCount]; + var uint32Msb = new uint[ValueCount]; + var int64Msb = new long[ValueCount]; + var uint64Lsb = new ulong[ValueCount]; + var intptrLsb = new nint[ValueCount]; + var uintptrMsb = new nuint[ValueCount]; + + BitBatchPrimitives.ReadInt8LSB(source, BitOffset, 5, BitStride, int8Lsb); + BitBatchPrimitives.ReadUInt8MSB(source, BitOffset, 7, BitStride, uint8Msb); + BitBatchPrimitives.ReadInt16MSB(source, BitOffset, 13, BitStride, int16Msb); + BitBatchPrimitives.ReadUInt16LSB(source, BitOffset, 15, BitStride, uint16Lsb); + BitBatchPrimitives.ReadInt32LSB(source, BitOffset, 29, BitStride, int32Lsb); + BitBatchPrimitives.ReadUInt32MSB(source, BitOffset, 31, BitStride, uint32Msb); + BitBatchPrimitives.ReadInt64MSB(source, BitOffset, 57, BitStride, int64Msb); + BitBatchPrimitives.ReadUInt64LSB(source, BitOffset, 61, BitStride, uint64Lsb); + BitBatchPrimitives.ReadIntPtrLSB(source, BitOffset, 29, BitStride, intptrLsb); + BitBatchPrimitives.ReadUIntPtrMSB(source, BitOffset, 29, BitStride, uintptrMsb); + + for (int i = 0; i < ValueCount; i++) + { + int offset = BitOffset + i * BitStride; + Assert.AreEqual(BitPrimitives.ReadInt8LSB(source, offset, 5), int8Lsb[i]); + Assert.AreEqual(BitPrimitives.ReadUInt8MSB(source, offset, 7), uint8Msb[i]); + Assert.AreEqual(BitPrimitives.ReadInt16MSB(source, offset, 13), int16Msb[i]); + Assert.AreEqual(BitPrimitives.ReadUInt16LSB(source, offset, 15), uint16Lsb[i]); + Assert.AreEqual(BitPrimitives.ReadInt32LSB(source, offset, 29), int32Lsb[i]); + Assert.AreEqual(BitPrimitives.ReadUInt32MSB(source, offset, 31), uint32Msb[i]); + Assert.AreEqual(BitPrimitives.ReadInt64MSB(source, offset, 57), int64Msb[i]); + Assert.AreEqual(BitPrimitives.ReadUInt64LSB(source, offset, 61), uint64Lsb[i]); + Assert.AreEqual(BitPrimitives.ReadIntPtrLSB(source, offset, 29), intptrLsb[i]); + Assert.AreEqual(BitPrimitives.ReadUIntPtrMSB(source, offset, 29), uintptrMsb[i]); + } + } +} diff --git a/BitsKit.Tests/GeneratorTests.Models.cs b/BitsKit.Tests/GeneratorTests.Models.cs index 6c8afec..fa4189c 100644 --- a/BitsKit.Tests/GeneratorTests.Models.cs +++ b/BitsKit.Tests/GeneratorTests.Models.cs @@ -183,6 +183,20 @@ public enum TestEnum C = 4, } +[BitObject(BitOrder.LeastSignificant, GenerateBatchAccessors = true)] +public partial struct BatchAccessorStruct +{ + [BitField(3)] + [BitField("Value", 12, BitFieldType.UInt16)] + [BooleanField("Flag")] + [EnumField("Kind", 2, typeof(TestEnum))] + public Memory Backing; + + [BitField(5)] + [BitField("BigEndianValue", 11, BitFieldType.UInt32, ReverseBitOrder = true)] + public ReadOnlyMemory ReadOnlyBacking; +} + [BitObject(BitOrder.LeastSignificant)] public partial struct OptimizedIntegralAccessorStruct { diff --git a/BitsKit/BitFields/BitObjectAttribute.cs b/BitsKit/BitFields/BitObjectAttribute.cs index 1efb2bd..2beece3 100644 --- a/BitsKit/BitFields/BitObjectAttribute.cs +++ b/BitsKit/BitFields/BitObjectAttribute.cs @@ -19,4 +19,13 @@ public sealed class BitObjectAttribute(BitOrder defaultBitOrder) : Attribute /// to provide non-empty backing storage large enough for each generated access width. /// public BitObjectAccessMode AccessMode { get; set; } = BitObjectAccessMode.Checked; + + /// + /// Generates static checked batch helpers for every named bit field. + /// + /// + /// Each field receives packed and record-strided read helpers and, when writable, matching + /// write helpers. The helpers operate on external byte spans and do not alter instance accessors. + /// + public bool GenerateBatchAccessors { get; set; } } diff --git a/BitsKit/Primitives/BitBatchPrimitives.cs b/BitsKit/Primitives/BitBatchPrimitives.cs new file mode 100644 index 0000000..200e8e7 --- /dev/null +++ b/BitsKit/Primitives/BitBatchPrimitives.cs @@ -0,0 +1,1409 @@ +using System.Runtime.InteropServices; + +namespace BitsKit.Primitives; + +/// +/// Reads or writes repeated packed and record-strided bit fields with one bounds validation. +/// +/// +/// The destination or values span determines the number of fields processed. A contiguous overload +/// advances by bitCount; a strided overload advances by bitStride. Strides smaller than +/// the field width are rejected so writes cannot overlap. +/// +public static class BitBatchPrimitives +{ + #region LSB integral batches + + /// Reads contiguous Int8 bit fields into . + public static void ReadInt8LSB(ReadOnlySpan source, int bitOffset, int bitCount, Span destination) => + ReadInt8LSB(source, bitOffset, bitCount, bitCount, destination); + + /// Reads strided Int8 bit fields into . + public static void ReadInt8LSB(ReadOnlySpan source, int bitOffset, int bitCount, int bitStride, Span destination) + { + ValidateBatch(source.Length, bitOffset, bitCount, bitStride, destination.Length, 8); + if (destination.IsEmpty) return; + if (bitCount == 0) { destination.Clear(); return; } + ref byte sourceReference = ref MemoryMarshal.GetReference(source); + int currentOffset = bitOffset; + int accessWidth = 4; + for (int i = 0; i < destination.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + destination[i] = byteOffset <= source.Length - accessWidth + ? ReadInt8LSBUnchecked(ref Unsafe.Add(ref sourceReference, byteOffset), bitInByte, bitCount) + : ReadTail(source.Slice(byteOffset), bitInByte, bitCount, BitOrder.LeastSignificant); + } + } + + /// Writes contiguous Int8 bit fields from . + public static void WriteInt8LSB(Span destination, int bitOffset, int bitCount, ReadOnlySpan values) => + WriteInt8LSB(destination, bitOffset, bitCount, bitCount, values); + + /// Writes strided Int8 bit fields from . + public static void WriteInt8LSB(Span destination, int bitOffset, int bitCount, int bitStride, ReadOnlySpan values) + { + ValidateBatch(destination.Length, bitOffset, bitCount, bitStride, values.Length, 8); + if (values.IsEmpty || bitCount == 0) return; + ref byte destinationReference = ref MemoryMarshal.GetReference(destination); + int currentOffset = bitOffset; + int accessWidth = 4; + for (int i = 0; i < values.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + if (byteOffset <= destination.Length - accessWidth) + UnsafeBitPrimitives.WriteInt8LSB(ref Unsafe.Add(ref destinationReference, byteOffset), bitInByte, values[i], bitCount); + else + WriteTail(destination.Slice(byteOffset), bitInByte, values[i], bitCount, BitOrder.LeastSignificant); + } + } + + /// Reads contiguous UInt8 bit fields into . + public static void ReadUInt8LSB(ReadOnlySpan source, int bitOffset, int bitCount, Span destination) => + ReadUInt8LSB(source, bitOffset, bitCount, bitCount, destination); + + /// Reads strided UInt8 bit fields into . + public static void ReadUInt8LSB(ReadOnlySpan source, int bitOffset, int bitCount, int bitStride, Span destination) + { + ValidateBatch(source.Length, bitOffset, bitCount, bitStride, destination.Length, 8); + if (destination.IsEmpty) return; + if (bitCount == 0) { destination.Clear(); return; } + ref byte sourceReference = ref MemoryMarshal.GetReference(source); + int currentOffset = bitOffset; + int accessWidth = 4; + for (int i = 0; i < destination.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + destination[i] = byteOffset <= source.Length - accessWidth + ? ReadUInt8LSBUnchecked(ref Unsafe.Add(ref sourceReference, byteOffset), bitInByte, bitCount) + : ReadTail(source.Slice(byteOffset), bitInByte, bitCount, BitOrder.LeastSignificant); + } + } + + /// Writes contiguous UInt8 bit fields from . + public static void WriteUInt8LSB(Span destination, int bitOffset, int bitCount, ReadOnlySpan values) => + WriteUInt8LSB(destination, bitOffset, bitCount, bitCount, values); + + /// Writes strided UInt8 bit fields from . + public static void WriteUInt8LSB(Span destination, int bitOffset, int bitCount, int bitStride, ReadOnlySpan values) + { + ValidateBatch(destination.Length, bitOffset, bitCount, bitStride, values.Length, 8); + if (values.IsEmpty || bitCount == 0) return; + ref byte destinationReference = ref MemoryMarshal.GetReference(destination); + int currentOffset = bitOffset; + int accessWidth = 4; + for (int i = 0; i < values.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + if (byteOffset <= destination.Length - accessWidth) + UnsafeBitPrimitives.WriteUInt8LSB(ref Unsafe.Add(ref destinationReference, byteOffset), bitInByte, values[i], bitCount); + else + WriteTail(destination.Slice(byteOffset), bitInByte, values[i], bitCount, BitOrder.LeastSignificant); + } + } + + /// Reads contiguous Int16 bit fields into . + public static void ReadInt16LSB(ReadOnlySpan source, int bitOffset, int bitCount, Span destination) => + ReadInt16LSB(source, bitOffset, bitCount, bitCount, destination); + + /// Reads strided Int16 bit fields into . + public static void ReadInt16LSB(ReadOnlySpan source, int bitOffset, int bitCount, int bitStride, Span destination) + { + ValidateBatch(source.Length, bitOffset, bitCount, bitStride, destination.Length, 16); + if (destination.IsEmpty) return; + if (bitCount == 0) { destination.Clear(); return; } + ref byte sourceReference = ref MemoryMarshal.GetReference(source); + int currentOffset = bitOffset; + int accessWidth = 4; + for (int i = 0; i < destination.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + destination[i] = byteOffset <= source.Length - accessWidth + ? ReadInt16LSBUnchecked(ref Unsafe.Add(ref sourceReference, byteOffset), bitInByte, bitCount) + : ReadTail(source.Slice(byteOffset), bitInByte, bitCount, BitOrder.LeastSignificant); + } + } + + /// Writes contiguous Int16 bit fields from . + public static void WriteInt16LSB(Span destination, int bitOffset, int bitCount, ReadOnlySpan values) => + WriteInt16LSB(destination, bitOffset, bitCount, bitCount, values); + + /// Writes strided Int16 bit fields from . + public static void WriteInt16LSB(Span destination, int bitOffset, int bitCount, int bitStride, ReadOnlySpan values) + { + ValidateBatch(destination.Length, bitOffset, bitCount, bitStride, values.Length, 16); + if (values.IsEmpty || bitCount == 0) return; + ref byte destinationReference = ref MemoryMarshal.GetReference(destination); + int currentOffset = bitOffset; + int accessWidth = 4; + for (int i = 0; i < values.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + if (byteOffset <= destination.Length - accessWidth) + UnsafeBitPrimitives.WriteInt16LSB(ref Unsafe.Add(ref destinationReference, byteOffset), bitInByte, values[i], bitCount); + else + WriteTail(destination.Slice(byteOffset), bitInByte, values[i], bitCount, BitOrder.LeastSignificant); + } + } + + /// Reads contiguous UInt16 bit fields into . + public static void ReadUInt16LSB(ReadOnlySpan source, int bitOffset, int bitCount, Span destination) => + ReadUInt16LSB(source, bitOffset, bitCount, bitCount, destination); + + /// Reads strided UInt16 bit fields into . + public static void ReadUInt16LSB(ReadOnlySpan source, int bitOffset, int bitCount, int bitStride, Span destination) + { + ValidateBatch(source.Length, bitOffset, bitCount, bitStride, destination.Length, 16); + if (destination.IsEmpty) return; + if (bitCount == 0) { destination.Clear(); return; } + ref byte sourceReference = ref MemoryMarshal.GetReference(source); + int currentOffset = bitOffset; + int accessWidth = 4; + for (int i = 0; i < destination.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + destination[i] = byteOffset <= source.Length - accessWidth + ? ReadUInt16LSBUnchecked(ref Unsafe.Add(ref sourceReference, byteOffset), bitInByte, bitCount) + : ReadTail(source.Slice(byteOffset), bitInByte, bitCount, BitOrder.LeastSignificant); + } + } + + /// Writes contiguous UInt16 bit fields from . + public static void WriteUInt16LSB(Span destination, int bitOffset, int bitCount, ReadOnlySpan values) => + WriteUInt16LSB(destination, bitOffset, bitCount, bitCount, values); + + /// Writes strided UInt16 bit fields from . + public static void WriteUInt16LSB(Span destination, int bitOffset, int bitCount, int bitStride, ReadOnlySpan values) + { + ValidateBatch(destination.Length, bitOffset, bitCount, bitStride, values.Length, 16); + if (values.IsEmpty || bitCount == 0) return; + ref byte destinationReference = ref MemoryMarshal.GetReference(destination); + int currentOffset = bitOffset; + int accessWidth = 4; + for (int i = 0; i < values.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + if (byteOffset <= destination.Length - accessWidth) + UnsafeBitPrimitives.WriteUInt16LSB(ref Unsafe.Add(ref destinationReference, byteOffset), bitInByte, values[i], bitCount); + else + WriteTail(destination.Slice(byteOffset), bitInByte, values[i], bitCount, BitOrder.LeastSignificant); + } + } + + /// Reads contiguous Int32 bit fields into . + public static void ReadInt32LSB(ReadOnlySpan source, int bitOffset, int bitCount, Span destination) => + ReadInt32LSB(source, bitOffset, bitCount, bitCount, destination); + + /// Reads strided Int32 bit fields into . + public static void ReadInt32LSB(ReadOnlySpan source, int bitOffset, int bitCount, int bitStride, Span destination) + { + ValidateBatch(source.Length, bitOffset, bitCount, bitStride, destination.Length, 32); + if (destination.IsEmpty) return; + if (bitCount == 0) { destination.Clear(); return; } + ref byte sourceReference = ref MemoryMarshal.GetReference(source); + int currentOffset = bitOffset; + int accessWidth = 8; + for (int i = 0; i < destination.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + destination[i] = byteOffset <= source.Length - accessWidth + ? ReadInt32LSBUnchecked(ref Unsafe.Add(ref sourceReference, byteOffset), bitInByte, bitCount) + : ReadTail(source.Slice(byteOffset), bitInByte, bitCount, BitOrder.LeastSignificant); + } + } + + /// Writes contiguous Int32 bit fields from . + public static void WriteInt32LSB(Span destination, int bitOffset, int bitCount, ReadOnlySpan values) => + WriteInt32LSB(destination, bitOffset, bitCount, bitCount, values); + + /// Writes strided Int32 bit fields from . + public static void WriteInt32LSB(Span destination, int bitOffset, int bitCount, int bitStride, ReadOnlySpan values) + { + ValidateBatch(destination.Length, bitOffset, bitCount, bitStride, values.Length, 32); + if (values.IsEmpty || bitCount == 0) return; + ref byte destinationReference = ref MemoryMarshal.GetReference(destination); + int currentOffset = bitOffset; + int accessWidth = 8; + for (int i = 0; i < values.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + if (byteOffset <= destination.Length - accessWidth) + UnsafeBitPrimitives.WriteInt32LSB(ref Unsafe.Add(ref destinationReference, byteOffset), bitInByte, values[i], bitCount); + else + WriteTail(destination.Slice(byteOffset), bitInByte, values[i], bitCount, BitOrder.LeastSignificant); + } + } + + /// Reads contiguous UInt32 bit fields into . + public static void ReadUInt32LSB(ReadOnlySpan source, int bitOffset, int bitCount, Span destination) => + ReadUInt32LSB(source, bitOffset, bitCount, bitCount, destination); + + /// Reads strided UInt32 bit fields into . + public static void ReadUInt32LSB(ReadOnlySpan source, int bitOffset, int bitCount, int bitStride, Span destination) + { + ValidateBatch(source.Length, bitOffset, bitCount, bitStride, destination.Length, 32); + if (destination.IsEmpty) return; + if (bitCount == 0) { destination.Clear(); return; } + ref byte sourceReference = ref MemoryMarshal.GetReference(source); + int currentOffset = bitOffset; + int accessWidth = 8; + for (int i = 0; i < destination.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + destination[i] = byteOffset <= source.Length - accessWidth + ? ReadUInt32LSBUnchecked(ref Unsafe.Add(ref sourceReference, byteOffset), bitInByte, bitCount) + : ReadTail(source.Slice(byteOffset), bitInByte, bitCount, BitOrder.LeastSignificant); + } + } + + /// Writes contiguous UInt32 bit fields from . + public static void WriteUInt32LSB(Span destination, int bitOffset, int bitCount, ReadOnlySpan values) => + WriteUInt32LSB(destination, bitOffset, bitCount, bitCount, values); + + /// Writes strided UInt32 bit fields from . + public static void WriteUInt32LSB(Span destination, int bitOffset, int bitCount, int bitStride, ReadOnlySpan values) + { + ValidateBatch(destination.Length, bitOffset, bitCount, bitStride, values.Length, 32); + if (values.IsEmpty || bitCount == 0) return; + ref byte destinationReference = ref MemoryMarshal.GetReference(destination); + int currentOffset = bitOffset; + int accessWidth = 8; + for (int i = 0; i < values.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + if (byteOffset <= destination.Length - accessWidth) + WriteUInt32LSBUnchecked(ref Unsafe.Add(ref destinationReference, byteOffset), bitInByte, values[i], bitCount); + else + WriteTail(destination.Slice(byteOffset), bitInByte, values[i], bitCount, BitOrder.LeastSignificant); + } + } + + /// Reads contiguous Int64 bit fields into . + public static void ReadInt64LSB(ReadOnlySpan source, int bitOffset, int bitCount, Span destination) => + ReadInt64LSB(source, bitOffset, bitCount, bitCount, destination); + + /// Reads strided Int64 bit fields into . + public static void ReadInt64LSB(ReadOnlySpan source, int bitOffset, int bitCount, int bitStride, Span destination) + { + ValidateBatch(source.Length, bitOffset, bitCount, bitStride, destination.Length, 64); + if (destination.IsEmpty) return; + if (bitCount == 0) { destination.Clear(); return; } + ref byte sourceReference = ref MemoryMarshal.GetReference(source); + int currentOffset = bitOffset; + int accessWidth = 16; + for (int i = 0; i < destination.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + destination[i] = byteOffset <= source.Length - accessWidth + ? ReadInt64LSBUnchecked(ref Unsafe.Add(ref sourceReference, byteOffset), bitInByte, bitCount) + : ReadTail(source.Slice(byteOffset), bitInByte, bitCount, BitOrder.LeastSignificant); + } + } + + /// Writes contiguous Int64 bit fields from . + public static void WriteInt64LSB(Span destination, int bitOffset, int bitCount, ReadOnlySpan values) => + WriteInt64LSB(destination, bitOffset, bitCount, bitCount, values); + + /// Writes strided Int64 bit fields from . + public static void WriteInt64LSB(Span destination, int bitOffset, int bitCount, int bitStride, ReadOnlySpan values) + { + ValidateBatch(destination.Length, bitOffset, bitCount, bitStride, values.Length, 64); + if (values.IsEmpty || bitCount == 0) return; + ref byte destinationReference = ref MemoryMarshal.GetReference(destination); + int currentOffset = bitOffset; + int accessWidth = 16; + for (int i = 0; i < values.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + if (byteOffset <= destination.Length - accessWidth) + UnsafeBitPrimitives.WriteInt64LSB(ref Unsafe.Add(ref destinationReference, byteOffset), bitInByte, values[i], bitCount); + else + WriteTail(destination.Slice(byteOffset), bitInByte, values[i], bitCount, BitOrder.LeastSignificant); + } + } + + /// Reads contiguous UInt64 bit fields into . + public static void ReadUInt64LSB(ReadOnlySpan source, int bitOffset, int bitCount, Span destination) => + ReadUInt64LSB(source, bitOffset, bitCount, bitCount, destination); + + /// Reads strided UInt64 bit fields into . + public static void ReadUInt64LSB(ReadOnlySpan source, int bitOffset, int bitCount, int bitStride, Span destination) + { + ValidateBatch(source.Length, bitOffset, bitCount, bitStride, destination.Length, 64); + if (destination.IsEmpty) return; + if (bitCount == 0) { destination.Clear(); return; } + ref byte sourceReference = ref MemoryMarshal.GetReference(source); + int currentOffset = bitOffset; + int accessWidth = 16; + for (int i = 0; i < destination.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + destination[i] = byteOffset <= source.Length - accessWidth + ? ReadUInt64LSBUnchecked(ref Unsafe.Add(ref sourceReference, byteOffset), bitInByte, bitCount) + : ReadTail(source.Slice(byteOffset), bitInByte, bitCount, BitOrder.LeastSignificant); + } + } + + /// Writes contiguous UInt64 bit fields from . + public static void WriteUInt64LSB(Span destination, int bitOffset, int bitCount, ReadOnlySpan values) => + WriteUInt64LSB(destination, bitOffset, bitCount, bitCount, values); + + /// Writes strided UInt64 bit fields from . + public static void WriteUInt64LSB(Span destination, int bitOffset, int bitCount, int bitStride, ReadOnlySpan values) + { + ValidateBatch(destination.Length, bitOffset, bitCount, bitStride, values.Length, 64); + if (values.IsEmpty || bitCount == 0) return; + ref byte destinationReference = ref MemoryMarshal.GetReference(destination); + int currentOffset = bitOffset; + int accessWidth = 16; + for (int i = 0; i < values.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + if (byteOffset <= destination.Length - accessWidth) + UnsafeBitPrimitives.WriteUInt64LSB(ref Unsafe.Add(ref destinationReference, byteOffset), bitInByte, values[i], bitCount); + else + WriteTail(destination.Slice(byteOffset), bitInByte, values[i], bitCount, BitOrder.LeastSignificant); + } + } + + /// Reads contiguous IntPtr bit fields into . + public static void ReadIntPtrLSB(ReadOnlySpan source, int bitOffset, int bitCount, Span destination) => + ReadIntPtrLSB(source, bitOffset, bitCount, bitCount, destination); + + /// Reads strided IntPtr bit fields into . + public static void ReadIntPtrLSB(ReadOnlySpan source, int bitOffset, int bitCount, int bitStride, Span destination) + { + ValidateBatch(source.Length, bitOffset, bitCount, bitStride, destination.Length, IntPtr.Size * 8); + if (destination.IsEmpty) return; + if (bitCount == 0) { destination.Clear(); return; } + ref byte sourceReference = ref MemoryMarshal.GetReference(source); + int currentOffset = bitOffset; + int accessWidth = IntPtr.Size == 8 ? 16 : 8; + for (int i = 0; i < destination.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + destination[i] = byteOffset <= source.Length - accessWidth + ? ReadIntPtrLSBUnchecked(ref Unsafe.Add(ref sourceReference, byteOffset), bitInByte, bitCount) + : ReadTail(source.Slice(byteOffset), bitInByte, bitCount, BitOrder.LeastSignificant); + } + } + + /// Writes contiguous IntPtr bit fields from . + public static void WriteIntPtrLSB(Span destination, int bitOffset, int bitCount, ReadOnlySpan values) => + WriteIntPtrLSB(destination, bitOffset, bitCount, bitCount, values); + + /// Writes strided IntPtr bit fields from . + public static void WriteIntPtrLSB(Span destination, int bitOffset, int bitCount, int bitStride, ReadOnlySpan values) + { + ValidateBatch(destination.Length, bitOffset, bitCount, bitStride, values.Length, IntPtr.Size * 8); + if (values.IsEmpty || bitCount == 0) return; + ref byte destinationReference = ref MemoryMarshal.GetReference(destination); + int currentOffset = bitOffset; + int accessWidth = IntPtr.Size == 8 ? 16 : 8; + for (int i = 0; i < values.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + if (byteOffset <= destination.Length - accessWidth) + UnsafeBitPrimitives.WriteIntPtrLSB(ref Unsafe.Add(ref destinationReference, byteOffset), bitInByte, values[i], bitCount); + else + WriteTail(destination.Slice(byteOffset), bitInByte, values[i], bitCount, BitOrder.LeastSignificant); + } + } + + /// Reads contiguous UIntPtr bit fields into . + public static void ReadUIntPtrLSB(ReadOnlySpan source, int bitOffset, int bitCount, Span destination) => + ReadUIntPtrLSB(source, bitOffset, bitCount, bitCount, destination); + + /// Reads strided UIntPtr bit fields into . + public static void ReadUIntPtrLSB(ReadOnlySpan source, int bitOffset, int bitCount, int bitStride, Span destination) + { + ValidateBatch(source.Length, bitOffset, bitCount, bitStride, destination.Length, IntPtr.Size * 8); + if (destination.IsEmpty) return; + if (bitCount == 0) { destination.Clear(); return; } + ref byte sourceReference = ref MemoryMarshal.GetReference(source); + int currentOffset = bitOffset; + int accessWidth = IntPtr.Size == 8 ? 16 : 8; + for (int i = 0; i < destination.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + destination[i] = byteOffset <= source.Length - accessWidth + ? ReadUIntPtrLSBUnchecked(ref Unsafe.Add(ref sourceReference, byteOffset), bitInByte, bitCount) + : ReadTail(source.Slice(byteOffset), bitInByte, bitCount, BitOrder.LeastSignificant); + } + } + + /// Writes contiguous UIntPtr bit fields from . + public static void WriteUIntPtrLSB(Span destination, int bitOffset, int bitCount, ReadOnlySpan values) => + WriteUIntPtrLSB(destination, bitOffset, bitCount, bitCount, values); + + /// Writes strided UIntPtr bit fields from . + public static void WriteUIntPtrLSB(Span destination, int bitOffset, int bitCount, int bitStride, ReadOnlySpan values) + { + ValidateBatch(destination.Length, bitOffset, bitCount, bitStride, values.Length, IntPtr.Size * 8); + if (values.IsEmpty || bitCount == 0) return; + ref byte destinationReference = ref MemoryMarshal.GetReference(destination); + int currentOffset = bitOffset; + int accessWidth = IntPtr.Size == 8 ? 16 : 8; + for (int i = 0; i < values.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + if (byteOffset <= destination.Length - accessWidth) + UnsafeBitPrimitives.WriteUIntPtrLSB(ref Unsafe.Add(ref destinationReference, byteOffset), bitInByte, values[i], bitCount); + else + WriteTail(destination.Slice(byteOffset), bitInByte, values[i], bitCount, BitOrder.LeastSignificant); + } + } + + #endregion + + #region MSB integral batches + + /// Reads contiguous Int8 bit fields into . + public static void ReadInt8MSB(ReadOnlySpan source, int bitOffset, int bitCount, Span destination) => + ReadInt8MSB(source, bitOffset, bitCount, bitCount, destination); + + /// Reads strided Int8 bit fields into . + public static void ReadInt8MSB(ReadOnlySpan source, int bitOffset, int bitCount, int bitStride, Span destination) + { + ValidateBatch(source.Length, bitOffset, bitCount, bitStride, destination.Length, 8); + if (destination.IsEmpty) return; + if (bitCount == 0) { destination.Clear(); return; } + ref byte sourceReference = ref MemoryMarshal.GetReference(source); + int currentOffset = bitOffset; + int accessWidth = 4; + for (int i = 0; i < destination.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + destination[i] = byteOffset <= source.Length - accessWidth + ? ReadInt8MSBUnchecked(ref Unsafe.Add(ref sourceReference, byteOffset), bitInByte, bitCount) + : ReadTail(source.Slice(byteOffset), bitInByte, bitCount, BitOrder.MostSignificant); + } + } + + /// Writes contiguous Int8 bit fields from . + public static void WriteInt8MSB(Span destination, int bitOffset, int bitCount, ReadOnlySpan values) => + WriteInt8MSB(destination, bitOffset, bitCount, bitCount, values); + + /// Writes strided Int8 bit fields from . + public static void WriteInt8MSB(Span destination, int bitOffset, int bitCount, int bitStride, ReadOnlySpan values) + { + ValidateBatch(destination.Length, bitOffset, bitCount, bitStride, values.Length, 8); + if (values.IsEmpty || bitCount == 0) return; + ref byte destinationReference = ref MemoryMarshal.GetReference(destination); + int currentOffset = bitOffset; + int accessWidth = 4; + for (int i = 0; i < values.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + if (byteOffset <= destination.Length - accessWidth) + UnsafeBitPrimitives.WriteInt8MSB(ref Unsafe.Add(ref destinationReference, byteOffset), bitInByte, values[i], bitCount); + else + WriteTail(destination.Slice(byteOffset), bitInByte, values[i], bitCount, BitOrder.MostSignificant); + } + } + + /// Reads contiguous UInt8 bit fields into . + public static void ReadUInt8MSB(ReadOnlySpan source, int bitOffset, int bitCount, Span destination) => + ReadUInt8MSB(source, bitOffset, bitCount, bitCount, destination); + + /// Reads strided UInt8 bit fields into . + public static void ReadUInt8MSB(ReadOnlySpan source, int bitOffset, int bitCount, int bitStride, Span destination) + { + ValidateBatch(source.Length, bitOffset, bitCount, bitStride, destination.Length, 8); + if (destination.IsEmpty) return; + if (bitCount == 0) { destination.Clear(); return; } + ref byte sourceReference = ref MemoryMarshal.GetReference(source); + int currentOffset = bitOffset; + int accessWidth = 4; + for (int i = 0; i < destination.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + destination[i] = byteOffset <= source.Length - accessWidth + ? ReadUInt8MSBUnchecked(ref Unsafe.Add(ref sourceReference, byteOffset), bitInByte, bitCount) + : ReadTail(source.Slice(byteOffset), bitInByte, bitCount, BitOrder.MostSignificant); + } + } + + /// Writes contiguous UInt8 bit fields from . + public static void WriteUInt8MSB(Span destination, int bitOffset, int bitCount, ReadOnlySpan values) => + WriteUInt8MSB(destination, bitOffset, bitCount, bitCount, values); + + /// Writes strided UInt8 bit fields from . + public static void WriteUInt8MSB(Span destination, int bitOffset, int bitCount, int bitStride, ReadOnlySpan values) + { + ValidateBatch(destination.Length, bitOffset, bitCount, bitStride, values.Length, 8); + if (values.IsEmpty || bitCount == 0) return; + ref byte destinationReference = ref MemoryMarshal.GetReference(destination); + int currentOffset = bitOffset; + int accessWidth = 4; + for (int i = 0; i < values.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + if (byteOffset <= destination.Length - accessWidth) + UnsafeBitPrimitives.WriteUInt8MSB(ref Unsafe.Add(ref destinationReference, byteOffset), bitInByte, values[i], bitCount); + else + WriteTail(destination.Slice(byteOffset), bitInByte, values[i], bitCount, BitOrder.MostSignificant); + } + } + + /// Reads contiguous Int16 bit fields into . + public static void ReadInt16MSB(ReadOnlySpan source, int bitOffset, int bitCount, Span destination) => + ReadInt16MSB(source, bitOffset, bitCount, bitCount, destination); + + /// Reads strided Int16 bit fields into . + public static void ReadInt16MSB(ReadOnlySpan source, int bitOffset, int bitCount, int bitStride, Span destination) + { + ValidateBatch(source.Length, bitOffset, bitCount, bitStride, destination.Length, 16); + if (destination.IsEmpty) return; + if (bitCount == 0) { destination.Clear(); return; } + ref byte sourceReference = ref MemoryMarshal.GetReference(source); + int currentOffset = bitOffset; + int accessWidth = 4; + for (int i = 0; i < destination.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + destination[i] = byteOffset <= source.Length - accessWidth + ? ReadInt16MSBUnchecked(ref Unsafe.Add(ref sourceReference, byteOffset), bitInByte, bitCount) + : ReadTail(source.Slice(byteOffset), bitInByte, bitCount, BitOrder.MostSignificant); + } + } + + /// Writes contiguous Int16 bit fields from . + public static void WriteInt16MSB(Span destination, int bitOffset, int bitCount, ReadOnlySpan values) => + WriteInt16MSB(destination, bitOffset, bitCount, bitCount, values); + + /// Writes strided Int16 bit fields from . + public static void WriteInt16MSB(Span destination, int bitOffset, int bitCount, int bitStride, ReadOnlySpan values) + { + ValidateBatch(destination.Length, bitOffset, bitCount, bitStride, values.Length, 16); + if (values.IsEmpty || bitCount == 0) return; + ref byte destinationReference = ref MemoryMarshal.GetReference(destination); + int currentOffset = bitOffset; + int accessWidth = 4; + for (int i = 0; i < values.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + if (byteOffset <= destination.Length - accessWidth) + UnsafeBitPrimitives.WriteInt16MSB(ref Unsafe.Add(ref destinationReference, byteOffset), bitInByte, values[i], bitCount); + else + WriteTail(destination.Slice(byteOffset), bitInByte, values[i], bitCount, BitOrder.MostSignificant); + } + } + + /// Reads contiguous UInt16 bit fields into . + public static void ReadUInt16MSB(ReadOnlySpan source, int bitOffset, int bitCount, Span destination) => + ReadUInt16MSB(source, bitOffset, bitCount, bitCount, destination); + + /// Reads strided UInt16 bit fields into . + public static void ReadUInt16MSB(ReadOnlySpan source, int bitOffset, int bitCount, int bitStride, Span destination) + { + ValidateBatch(source.Length, bitOffset, bitCount, bitStride, destination.Length, 16); + if (destination.IsEmpty) return; + if (bitCount == 0) { destination.Clear(); return; } + ref byte sourceReference = ref MemoryMarshal.GetReference(source); + int currentOffset = bitOffset; + int accessWidth = 4; + for (int i = 0; i < destination.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + destination[i] = byteOffset <= source.Length - accessWidth + ? ReadUInt16MSBUnchecked(ref Unsafe.Add(ref sourceReference, byteOffset), bitInByte, bitCount) + : ReadTail(source.Slice(byteOffset), bitInByte, bitCount, BitOrder.MostSignificant); + } + } + + /// Writes contiguous UInt16 bit fields from . + public static void WriteUInt16MSB(Span destination, int bitOffset, int bitCount, ReadOnlySpan values) => + WriteUInt16MSB(destination, bitOffset, bitCount, bitCount, values); + + /// Writes strided UInt16 bit fields from . + public static void WriteUInt16MSB(Span destination, int bitOffset, int bitCount, int bitStride, ReadOnlySpan values) + { + ValidateBatch(destination.Length, bitOffset, bitCount, bitStride, values.Length, 16); + if (values.IsEmpty || bitCount == 0) return; + ref byte destinationReference = ref MemoryMarshal.GetReference(destination); + int currentOffset = bitOffset; + int accessWidth = 4; + for (int i = 0; i < values.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + if (byteOffset <= destination.Length - accessWidth) + UnsafeBitPrimitives.WriteUInt16MSB(ref Unsafe.Add(ref destinationReference, byteOffset), bitInByte, values[i], bitCount); + else + WriteTail(destination.Slice(byteOffset), bitInByte, values[i], bitCount, BitOrder.MostSignificant); + } + } + + /// Reads contiguous Int32 bit fields into . + public static void ReadInt32MSB(ReadOnlySpan source, int bitOffset, int bitCount, Span destination) => + ReadInt32MSB(source, bitOffset, bitCount, bitCount, destination); + + /// Reads strided Int32 bit fields into . + public static void ReadInt32MSB(ReadOnlySpan source, int bitOffset, int bitCount, int bitStride, Span destination) + { + ValidateBatch(source.Length, bitOffset, bitCount, bitStride, destination.Length, 32); + if (destination.IsEmpty) return; + if (bitCount == 0) { destination.Clear(); return; } + ref byte sourceReference = ref MemoryMarshal.GetReference(source); + int currentOffset = bitOffset; + int accessWidth = 8; + for (int i = 0; i < destination.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + destination[i] = byteOffset <= source.Length - accessWidth + ? ReadInt32MSBUnchecked(ref Unsafe.Add(ref sourceReference, byteOffset), bitInByte, bitCount) + : ReadTail(source.Slice(byteOffset), bitInByte, bitCount, BitOrder.MostSignificant); + } + } + + /// Writes contiguous Int32 bit fields from . + public static void WriteInt32MSB(Span destination, int bitOffset, int bitCount, ReadOnlySpan values) => + WriteInt32MSB(destination, bitOffset, bitCount, bitCount, values); + + /// Writes strided Int32 bit fields from . + public static void WriteInt32MSB(Span destination, int bitOffset, int bitCount, int bitStride, ReadOnlySpan values) + { + ValidateBatch(destination.Length, bitOffset, bitCount, bitStride, values.Length, 32); + if (values.IsEmpty || bitCount == 0) return; + ref byte destinationReference = ref MemoryMarshal.GetReference(destination); + int currentOffset = bitOffset; + int accessWidth = 8; + for (int i = 0; i < values.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + if (byteOffset <= destination.Length - accessWidth) + UnsafeBitPrimitives.WriteInt32MSB(ref Unsafe.Add(ref destinationReference, byteOffset), bitInByte, values[i], bitCount); + else + WriteTail(destination.Slice(byteOffset), bitInByte, values[i], bitCount, BitOrder.MostSignificant); + } + } + + /// Reads contiguous UInt32 bit fields into . + public static void ReadUInt32MSB(ReadOnlySpan source, int bitOffset, int bitCount, Span destination) => + ReadUInt32MSB(source, bitOffset, bitCount, bitCount, destination); + + /// Reads strided UInt32 bit fields into . + public static void ReadUInt32MSB(ReadOnlySpan source, int bitOffset, int bitCount, int bitStride, Span destination) + { + ValidateBatch(source.Length, bitOffset, bitCount, bitStride, destination.Length, 32); + if (destination.IsEmpty) return; + if (bitCount == 0) { destination.Clear(); return; } + ref byte sourceReference = ref MemoryMarshal.GetReference(source); + int currentOffset = bitOffset; + int accessWidth = 8; + for (int i = 0; i < destination.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + destination[i] = byteOffset <= source.Length - accessWidth + ? ReadUInt32MSBUnchecked(ref Unsafe.Add(ref sourceReference, byteOffset), bitInByte, bitCount) + : ReadTail(source.Slice(byteOffset), bitInByte, bitCount, BitOrder.MostSignificant); + } + } + + /// Writes contiguous UInt32 bit fields from . + public static void WriteUInt32MSB(Span destination, int bitOffset, int bitCount, ReadOnlySpan values) => + WriteUInt32MSB(destination, bitOffset, bitCount, bitCount, values); + + /// Writes strided UInt32 bit fields from . + public static void WriteUInt32MSB(Span destination, int bitOffset, int bitCount, int bitStride, ReadOnlySpan values) + { + ValidateBatch(destination.Length, bitOffset, bitCount, bitStride, values.Length, 32); + if (values.IsEmpty || bitCount == 0) return; + ref byte destinationReference = ref MemoryMarshal.GetReference(destination); + int currentOffset = bitOffset; + int accessWidth = 8; + for (int i = 0; i < values.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + if (byteOffset <= destination.Length - accessWidth) + WriteUInt32MSBUnchecked(ref Unsafe.Add(ref destinationReference, byteOffset), bitInByte, values[i], bitCount); + else + WriteTail(destination.Slice(byteOffset), bitInByte, values[i], bitCount, BitOrder.MostSignificant); + } + } + + /// Reads contiguous Int64 bit fields into . + public static void ReadInt64MSB(ReadOnlySpan source, int bitOffset, int bitCount, Span destination) => + ReadInt64MSB(source, bitOffset, bitCount, bitCount, destination); + + /// Reads strided Int64 bit fields into . + public static void ReadInt64MSB(ReadOnlySpan source, int bitOffset, int bitCount, int bitStride, Span destination) + { + ValidateBatch(source.Length, bitOffset, bitCount, bitStride, destination.Length, 64); + if (destination.IsEmpty) return; + if (bitCount == 0) { destination.Clear(); return; } + ref byte sourceReference = ref MemoryMarshal.GetReference(source); + int currentOffset = bitOffset; + int accessWidth = 16; + for (int i = 0; i < destination.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + destination[i] = byteOffset <= source.Length - accessWidth + ? ReadInt64MSBUnchecked(ref Unsafe.Add(ref sourceReference, byteOffset), bitInByte, bitCount) + : ReadTail(source.Slice(byteOffset), bitInByte, bitCount, BitOrder.MostSignificant); + } + } + + /// Writes contiguous Int64 bit fields from . + public static void WriteInt64MSB(Span destination, int bitOffset, int bitCount, ReadOnlySpan values) => + WriteInt64MSB(destination, bitOffset, bitCount, bitCount, values); + + /// Writes strided Int64 bit fields from . + public static void WriteInt64MSB(Span destination, int bitOffset, int bitCount, int bitStride, ReadOnlySpan values) + { + ValidateBatch(destination.Length, bitOffset, bitCount, bitStride, values.Length, 64); + if (values.IsEmpty || bitCount == 0) return; + ref byte destinationReference = ref MemoryMarshal.GetReference(destination); + int currentOffset = bitOffset; + int accessWidth = 16; + for (int i = 0; i < values.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + if (byteOffset <= destination.Length - accessWidth) + UnsafeBitPrimitives.WriteInt64MSB(ref Unsafe.Add(ref destinationReference, byteOffset), bitInByte, values[i], bitCount); + else + WriteTail(destination.Slice(byteOffset), bitInByte, values[i], bitCount, BitOrder.MostSignificant); + } + } + + /// Reads contiguous UInt64 bit fields into . + public static void ReadUInt64MSB(ReadOnlySpan source, int bitOffset, int bitCount, Span destination) => + ReadUInt64MSB(source, bitOffset, bitCount, bitCount, destination); + + /// Reads strided UInt64 bit fields into . + public static void ReadUInt64MSB(ReadOnlySpan source, int bitOffset, int bitCount, int bitStride, Span destination) + { + ValidateBatch(source.Length, bitOffset, bitCount, bitStride, destination.Length, 64); + if (destination.IsEmpty) return; + if (bitCount == 0) { destination.Clear(); return; } + ref byte sourceReference = ref MemoryMarshal.GetReference(source); + int currentOffset = bitOffset; + int accessWidth = 16; + for (int i = 0; i < destination.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + destination[i] = byteOffset <= source.Length - accessWidth + ? ReadUInt64MSBUnchecked(ref Unsafe.Add(ref sourceReference, byteOffset), bitInByte, bitCount) + : ReadTail(source.Slice(byteOffset), bitInByte, bitCount, BitOrder.MostSignificant); + } + } + + /// Writes contiguous UInt64 bit fields from . + public static void WriteUInt64MSB(Span destination, int bitOffset, int bitCount, ReadOnlySpan values) => + WriteUInt64MSB(destination, bitOffset, bitCount, bitCount, values); + + /// Writes strided UInt64 bit fields from . + public static void WriteUInt64MSB(Span destination, int bitOffset, int bitCount, int bitStride, ReadOnlySpan values) + { + ValidateBatch(destination.Length, bitOffset, bitCount, bitStride, values.Length, 64); + if (values.IsEmpty || bitCount == 0) return; + ref byte destinationReference = ref MemoryMarshal.GetReference(destination); + int currentOffset = bitOffset; + int accessWidth = 16; + for (int i = 0; i < values.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + if (byteOffset <= destination.Length - accessWidth) + UnsafeBitPrimitives.WriteUInt64MSB(ref Unsafe.Add(ref destinationReference, byteOffset), bitInByte, values[i], bitCount); + else + WriteTail(destination.Slice(byteOffset), bitInByte, values[i], bitCount, BitOrder.MostSignificant); + } + } + + /// Reads contiguous IntPtr bit fields into . + public static void ReadIntPtrMSB(ReadOnlySpan source, int bitOffset, int bitCount, Span destination) => + ReadIntPtrMSB(source, bitOffset, bitCount, bitCount, destination); + + /// Reads strided IntPtr bit fields into . + public static void ReadIntPtrMSB(ReadOnlySpan source, int bitOffset, int bitCount, int bitStride, Span destination) + { + ValidateBatch(source.Length, bitOffset, bitCount, bitStride, destination.Length, IntPtr.Size * 8); + if (destination.IsEmpty) return; + if (bitCount == 0) { destination.Clear(); return; } + ref byte sourceReference = ref MemoryMarshal.GetReference(source); + int currentOffset = bitOffset; + int accessWidth = IntPtr.Size == 8 ? 16 : 8; + for (int i = 0; i < destination.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + destination[i] = byteOffset <= source.Length - accessWidth + ? ReadIntPtrMSBUnchecked(ref Unsafe.Add(ref sourceReference, byteOffset), bitInByte, bitCount) + : ReadTail(source.Slice(byteOffset), bitInByte, bitCount, BitOrder.MostSignificant); + } + } + + /// Writes contiguous IntPtr bit fields from . + public static void WriteIntPtrMSB(Span destination, int bitOffset, int bitCount, ReadOnlySpan values) => + WriteIntPtrMSB(destination, bitOffset, bitCount, bitCount, values); + + /// Writes strided IntPtr bit fields from . + public static void WriteIntPtrMSB(Span destination, int bitOffset, int bitCount, int bitStride, ReadOnlySpan values) + { + ValidateBatch(destination.Length, bitOffset, bitCount, bitStride, values.Length, IntPtr.Size * 8); + if (values.IsEmpty || bitCount == 0) return; + ref byte destinationReference = ref MemoryMarshal.GetReference(destination); + int currentOffset = bitOffset; + int accessWidth = IntPtr.Size == 8 ? 16 : 8; + for (int i = 0; i < values.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + if (byteOffset <= destination.Length - accessWidth) + UnsafeBitPrimitives.WriteIntPtrMSB(ref Unsafe.Add(ref destinationReference, byteOffset), bitInByte, values[i], bitCount); + else + WriteTail(destination.Slice(byteOffset), bitInByte, values[i], bitCount, BitOrder.MostSignificant); + } + } + + /// Reads contiguous UIntPtr bit fields into . + public static void ReadUIntPtrMSB(ReadOnlySpan source, int bitOffset, int bitCount, Span destination) => + ReadUIntPtrMSB(source, bitOffset, bitCount, bitCount, destination); + + /// Reads strided UIntPtr bit fields into . + public static void ReadUIntPtrMSB(ReadOnlySpan source, int bitOffset, int bitCount, int bitStride, Span destination) + { + ValidateBatch(source.Length, bitOffset, bitCount, bitStride, destination.Length, IntPtr.Size * 8); + if (destination.IsEmpty) return; + if (bitCount == 0) { destination.Clear(); return; } + ref byte sourceReference = ref MemoryMarshal.GetReference(source); + int currentOffset = bitOffset; + int accessWidth = IntPtr.Size == 8 ? 16 : 8; + for (int i = 0; i < destination.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + destination[i] = byteOffset <= source.Length - accessWidth + ? ReadUIntPtrMSBUnchecked(ref Unsafe.Add(ref sourceReference, byteOffset), bitInByte, bitCount) + : ReadTail(source.Slice(byteOffset), bitInByte, bitCount, BitOrder.MostSignificant); + } + } + + /// Writes contiguous UIntPtr bit fields from . + public static void WriteUIntPtrMSB(Span destination, int bitOffset, int bitCount, ReadOnlySpan values) => + WriteUIntPtrMSB(destination, bitOffset, bitCount, bitCount, values); + + /// Writes strided UIntPtr bit fields from . + public static void WriteUIntPtrMSB(Span destination, int bitOffset, int bitCount, int bitStride, ReadOnlySpan values) + { + ValidateBatch(destination.Length, bitOffset, bitCount, bitStride, values.Length, IntPtr.Size * 8); + if (values.IsEmpty || bitCount == 0) return; + ref byte destinationReference = ref MemoryMarshal.GetReference(destination); + int currentOffset = bitOffset; + int accessWidth = IntPtr.Size == 8 ? 16 : 8; + for (int i = 0; i < values.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + if (byteOffset <= destination.Length - accessWidth) + UnsafeBitPrimitives.WriteUIntPtrMSB(ref Unsafe.Add(ref destinationReference, byteOffset), bitInByte, values[i], bitCount); + else + WriteTail(destination.Slice(byteOffset), bitInByte, values[i], bitCount, BitOrder.MostSignificant); + } + } + + #endregion + + #region Boolean batches + + /// Reads contiguous Boolean bits into . + public static void ReadBitLSB(ReadOnlySpan source, int bitOffset, Span destination) => + ReadBitLSB(source, bitOffset, 1, destination); + + /// Reads strided Boolean bits into . + public static void ReadBitLSB(ReadOnlySpan source, int bitOffset, int bitStride, Span destination) + { + ValidateBatch(source.Length, bitOffset, 1, bitStride, destination.Length, 1); + if (destination.IsEmpty) return; + ref byte sourceReference = ref MemoryMarshal.GetReference(source); + int currentOffset = bitOffset; + for (int i = 0; i < destination.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + destination[i] = UnsafeBitPrimitives.ReadBitLSB(ref Unsafe.Add(ref sourceReference, byteOffset), bitInByte); + } + } + + /// Writes contiguous Boolean bits from . + public static void WriteBitLSB(Span destination, int bitOffset, ReadOnlySpan values) => + WriteBitLSB(destination, bitOffset, 1, values); + + /// Writes strided Boolean bits from . + public static void WriteBitLSB(Span destination, int bitOffset, int bitStride, ReadOnlySpan values) + { + ValidateBatch(destination.Length, bitOffset, 1, bitStride, values.Length, 1); + if (values.IsEmpty) return; + ref byte destinationReference = ref MemoryMarshal.GetReference(destination); + int currentOffset = bitOffset; + for (int i = 0; i < values.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + UnsafeBitPrimitives.WriteBitLSB(ref Unsafe.Add(ref destinationReference, byteOffset), bitInByte, values[i]); + } + } + + /// Reads contiguous Boolean bits into . + public static void ReadBitMSB(ReadOnlySpan source, int bitOffset, Span destination) => + ReadBitMSB(source, bitOffset, 1, destination); + + /// Reads strided Boolean bits into . + public static void ReadBitMSB(ReadOnlySpan source, int bitOffset, int bitStride, Span destination) + { + ValidateBatch(source.Length, bitOffset, 1, bitStride, destination.Length, 1); + if (destination.IsEmpty) return; + ref byte sourceReference = ref MemoryMarshal.GetReference(source); + int currentOffset = bitOffset; + for (int i = 0; i < destination.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + destination[i] = UnsafeBitPrimitives.ReadBitMSB(ref Unsafe.Add(ref sourceReference, byteOffset), bitInByte); + } + } + + /// Writes contiguous Boolean bits from . + public static void WriteBitMSB(Span destination, int bitOffset, ReadOnlySpan values) => + WriteBitMSB(destination, bitOffset, 1, values); + + /// Writes strided Boolean bits from . + public static void WriteBitMSB(Span destination, int bitOffset, int bitStride, ReadOnlySpan values) + { + ValidateBatch(destination.Length, bitOffset, 1, bitStride, values.Length, 1); + if (values.IsEmpty) return; + ref byte destinationReference = ref MemoryMarshal.GetReference(destination); + int currentOffset = bitOffset; + for (int i = 0; i < values.Length; i++, currentOffset += bitStride) + { + int byteOffset = (int)(currentOffset >> 3); + int bitInByte = (int)currentOffset & 7; + UnsafeBitPrimitives.WriteBitMSB(ref Unsafe.Add(ref destinationReference, byteOffset), bitInByte, values[i]); + } + } + + #endregion + + private static void ValidateBatch( + int byteLength, + int bitOffset, + int bitCount, + int bitStride, + int valueCount, + int maxBits) + { + if ((uint)bitCount > (uint)maxBits) + throw new ArgumentOutOfRangeException(nameof(bitCount)); + if (bitStride < bitCount) + throw new ArgumentOutOfRangeException(nameof(bitStride)); + if (bitOffset < 0) + throw new ArgumentOutOfRangeException(nameof(bitOffset)); + + long requiredBits = valueCount == 0 + ? bitOffset + : (long)bitOffset + ((long)valueCount - 1) * bitStride + bitCount; + + if (requiredBits > (long)byteLength * 8 || requiredBits > int.MaxValue) + throw new ArgumentOutOfRangeException(nameof(bitOffset), "The batch exceeds the available buffer."); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte ReadUInt8LSBUnchecked(ref byte source, int bitOffset, int bitCount) + { + uint value = Unsafe.ReadUnaligned(ref source); + return unchecked((byte)BitPrimitives.ReadValue32(value, bitOffset, bitCount, BitOrder.LeastSignificant)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static sbyte ReadInt8LSBUnchecked(ref byte source, int bitOffset, int bitCount) => + unchecked((sbyte)SignExtend(ReadUInt8LSBUnchecked(ref source, bitOffset, bitCount), bitCount)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ushort ReadUInt16LSBUnchecked(ref byte source, int bitOffset, int bitCount) + { + uint value = Unsafe.ReadUnaligned(ref source); + return unchecked((ushort)BitPrimitives.ReadValue32(value, bitOffset, bitCount, BitOrder.LeastSignificant)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static short ReadInt16LSBUnchecked(ref byte source, int bitOffset, int bitCount) => + unchecked((short)SignExtend(ReadUInt16LSBUnchecked(ref source, bitOffset, bitCount), bitCount)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int ReadInt32LSBUnchecked(ref byte source, int bitOffset, int bitCount) => + unchecked((int)SignExtend(ReadUInt32LSBUnchecked(ref source, bitOffset, bitCount), bitCount)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong ReadUInt64LSBUnchecked(ref byte source, int bitOffset, int bitCount) + { + UInt128 value = Unsafe.ReadUnaligned(ref source); + return bitCount + bitOffset > 64 + ? unchecked((ulong)BitPrimitives.ReadValue128(value, bitOffset, bitCount, BitOrder.LeastSignificant)) + : BitPrimitives.ReadValue64(unchecked((ulong)value), bitOffset, bitCount, BitOrder.LeastSignificant); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static long ReadInt64LSBUnchecked(ref byte source, int bitOffset, int bitCount) => + SignExtend(ReadUInt64LSBUnchecked(ref source, bitOffset, bitCount), bitCount); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static nint ReadIntPtrLSBUnchecked(ref byte source, int bitOffset, int bitCount) => + IntPtr.Size == 8 + ? unchecked((nint)ReadInt64LSBUnchecked(ref source, bitOffset, bitCount)) + : ReadInt32LSBUnchecked(ref source, bitOffset, bitCount); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static nuint ReadUIntPtrLSBUnchecked(ref byte source, int bitOffset, int bitCount) => + IntPtr.Size == 8 + ? unchecked((nuint)ReadUInt64LSBUnchecked(ref source, bitOffset, bitCount)) + : ReadUInt32LSBUnchecked(ref source, bitOffset, bitCount); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte ReadUInt8MSBUnchecked(ref byte source, int bitOffset, int bitCount) + { + uint value = Unsafe.ReadUnaligned(ref source); + return unchecked((byte)BitPrimitives.ReadValue32(value, bitOffset, bitCount, BitOrder.MostSignificant)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static sbyte ReadInt8MSBUnchecked(ref byte source, int bitOffset, int bitCount) => + unchecked((sbyte)SignExtend(ReadUInt8MSBUnchecked(ref source, bitOffset, bitCount), bitCount)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ushort ReadUInt16MSBUnchecked(ref byte source, int bitOffset, int bitCount) + { + uint value = Unsafe.ReadUnaligned(ref source); + return unchecked((ushort)BitPrimitives.ReadValue32(value, bitOffset, bitCount, BitOrder.MostSignificant)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static short ReadInt16MSBUnchecked(ref byte source, int bitOffset, int bitCount) => + unchecked((short)SignExtend(ReadUInt16MSBUnchecked(ref source, bitOffset, bitCount), bitCount)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int ReadInt32MSBUnchecked(ref byte source, int bitOffset, int bitCount) => + unchecked((int)SignExtend(ReadUInt32MSBUnchecked(ref source, bitOffset, bitCount), bitCount)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong ReadUInt64MSBUnchecked(ref byte source, int bitOffset, int bitCount) + { + UInt128 value = Unsafe.ReadUnaligned(ref source); + return bitCount + bitOffset > 64 + ? unchecked((ulong)BitPrimitives.ReadValue128(value, bitOffset, bitCount, BitOrder.MostSignificant)) + : BitPrimitives.ReadValue64(unchecked((ulong)value), bitOffset, bitCount, BitOrder.MostSignificant); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static long ReadInt64MSBUnchecked(ref byte source, int bitOffset, int bitCount) => + SignExtend(ReadUInt64MSBUnchecked(ref source, bitOffset, bitCount), bitCount); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static nint ReadIntPtrMSBUnchecked(ref byte source, int bitOffset, int bitCount) => + IntPtr.Size == 8 + ? unchecked((nint)ReadInt64MSBUnchecked(ref source, bitOffset, bitCount)) + : ReadInt32MSBUnchecked(ref source, bitOffset, bitCount); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static nuint ReadUIntPtrMSBUnchecked(ref byte source, int bitOffset, int bitCount) => + IntPtr.Size == 8 + ? unchecked((nuint)ReadUInt64MSBUnchecked(ref source, bitOffset, bitCount)) + : ReadUInt32MSBUnchecked(ref source, bitOffset, bitCount); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static long SignExtend(ulong value, int bitCount) => + bitCount == 0 ? 0 : unchecked((long)(value << (64 - bitCount))) >> (64 - bitCount); + + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint ReadUInt32LSBUnchecked(ref byte source, int bitOffset, int bitCount) + { + ulong value = Unsafe.ReadUnaligned(ref source); + return bitCount + bitOffset <= 32 + ? BitPrimitives.ReadValue32(unchecked((uint)value), bitOffset, bitCount, BitOrder.LeastSignificant) + : unchecked((uint)BitPrimitives.ReadValue64(value, bitOffset, bitCount, BitOrder.LeastSignificant)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint ReadUInt32MSBUnchecked(ref byte source, int bitOffset, int bitCount) + { + ulong value = Unsafe.ReadUnaligned(ref source); + return bitCount + bitOffset <= 32 + ? BitPrimitives.ReadValue32(unchecked((uint)value), bitOffset, bitCount, BitOrder.MostSignificant) + : unchecked((uint)BitPrimitives.ReadValue64(value, bitOffset, bitCount, BitOrder.MostSignificant)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteUInt32LSBUnchecked(ref byte destination, int bitOffset, uint value, int bitCount) + { + if (bitCount + bitOffset > 32) + BitPrimitives.WriteValue64(ref Unsafe.As(ref destination), bitOffset, value, bitCount, BitOrder.LeastSignificant); + else + BitPrimitives.WriteValue32(ref Unsafe.As(ref destination), bitOffset, value, bitCount, BitOrder.LeastSignificant); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteUInt32MSBUnchecked(ref byte destination, int bitOffset, uint value, int bitCount) + { + if (bitCount + bitOffset > 32) + BitPrimitives.WriteValue64(ref Unsafe.As(ref destination), bitOffset, value, bitCount, BitOrder.MostSignificant); + else + BitPrimitives.WriteValue32(ref Unsafe.As(ref destination), bitOffset, value, bitCount, BitOrder.MostSignificant); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static T ReadTail( + ReadOnlySpan source, + int bitOffset, + int bitCount, + BitOrder bitOrder) + where T : unmanaged + { + Span padded = stackalloc byte[16]; + source.CopyTo(padded); + return ReadUnchecked(ref MemoryMarshal.GetReference(padded), bitOffset, bitCount, bitOrder); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void WriteTail( + Span destination, + int bitOffset, + T value, + int bitCount, + BitOrder bitOrder) + where T : unmanaged + { + Span padded = stackalloc byte[16]; + destination.CopyTo(padded); + WriteUnchecked(ref MemoryMarshal.GetReference(padded), bitOffset, value, bitCount, bitOrder); + padded.Slice(0, destination.Length).CopyTo(destination); + } + + private static T ReadUnchecked(ref byte source, int bitOffset, int bitCount, BitOrder bitOrder) + where T : unmanaged + { + if (typeof(T) == typeof(sbyte)) + { + sbyte value = bitOrder == BitOrder.LeastSignificant + ? UnsafeBitPrimitives.ReadInt8LSB(ref source, bitOffset, bitCount) + : UnsafeBitPrimitives.ReadInt8MSB(ref source, bitOffset, bitCount); + return Unsafe.As(ref value); + } + + if (typeof(T) == typeof(byte)) + { + byte value = bitOrder == BitOrder.LeastSignificant + ? UnsafeBitPrimitives.ReadUInt8LSB(ref source, bitOffset, bitCount) + : UnsafeBitPrimitives.ReadUInt8MSB(ref source, bitOffset, bitCount); + return Unsafe.As(ref value); + } + + if (typeof(T) == typeof(short)) + { + short value = bitOrder == BitOrder.LeastSignificant + ? UnsafeBitPrimitives.ReadInt16LSB(ref source, bitOffset, bitCount) + : UnsafeBitPrimitives.ReadInt16MSB(ref source, bitOffset, bitCount); + return Unsafe.As(ref value); + } + + if (typeof(T) == typeof(ushort)) + { + ushort value = bitOrder == BitOrder.LeastSignificant + ? UnsafeBitPrimitives.ReadUInt16LSB(ref source, bitOffset, bitCount) + : UnsafeBitPrimitives.ReadUInt16MSB(ref source, bitOffset, bitCount); + return Unsafe.As(ref value); + } + + if (typeof(T) == typeof(int)) + { + int value = bitOrder == BitOrder.LeastSignificant + ? UnsafeBitPrimitives.ReadInt32LSB(ref source, bitOffset, bitCount) + : UnsafeBitPrimitives.ReadInt32MSB(ref source, bitOffset, bitCount); + return Unsafe.As(ref value); + } + + if (typeof(T) == typeof(uint)) + { + uint value = bitOrder == BitOrder.LeastSignificant + ? UnsafeBitPrimitives.ReadUInt32LSB(ref source, bitOffset, bitCount) + : UnsafeBitPrimitives.ReadUInt32MSB(ref source, bitOffset, bitCount); + return Unsafe.As(ref value); + } + + if (typeof(T) == typeof(long)) + { + long value = bitOrder == BitOrder.LeastSignificant + ? UnsafeBitPrimitives.ReadInt64LSB(ref source, bitOffset, bitCount) + : UnsafeBitPrimitives.ReadInt64MSB(ref source, bitOffset, bitCount); + return Unsafe.As(ref value); + } + + if (typeof(T) == typeof(ulong)) + { + ulong value = bitOrder == BitOrder.LeastSignificant + ? UnsafeBitPrimitives.ReadUInt64LSB(ref source, bitOffset, bitCount) + : UnsafeBitPrimitives.ReadUInt64MSB(ref source, bitOffset, bitCount); + return Unsafe.As(ref value); + } + + if (typeof(T) == typeof(nint)) + { + nint value = bitOrder == BitOrder.LeastSignificant + ? UnsafeBitPrimitives.ReadIntPtrLSB(ref source, bitOffset, bitCount) + : UnsafeBitPrimitives.ReadIntPtrMSB(ref source, bitOffset, bitCount); + return Unsafe.As(ref value); + } + + if (typeof(T) == typeof(nuint)) + { + nuint value = bitOrder == BitOrder.LeastSignificant + ? UnsafeBitPrimitives.ReadUIntPtrLSB(ref source, bitOffset, bitCount) + : UnsafeBitPrimitives.ReadUIntPtrMSB(ref source, bitOffset, bitCount); + return Unsafe.As(ref value); + } + + throw new NotSupportedException(); + } + + private static void WriteUnchecked(ref byte destination, int bitOffset, T value, int bitCount, BitOrder bitOrder) + where T : unmanaged + { + if (typeof(T) == typeof(sbyte)) + { + sbyte typedValue = Unsafe.As(ref value); + if (bitOrder == BitOrder.LeastSignificant) + UnsafeBitPrimitives.WriteInt8LSB(ref destination, bitOffset, typedValue, bitCount); + else + UnsafeBitPrimitives.WriteInt8MSB(ref destination, bitOffset, typedValue, bitCount); + return; + } + + if (typeof(T) == typeof(byte)) + { + byte typedValue = Unsafe.As(ref value); + if (bitOrder == BitOrder.LeastSignificant) + UnsafeBitPrimitives.WriteUInt8LSB(ref destination, bitOffset, typedValue, bitCount); + else + UnsafeBitPrimitives.WriteUInt8MSB(ref destination, bitOffset, typedValue, bitCount); + return; + } + + if (typeof(T) == typeof(short)) + { + short typedValue = Unsafe.As(ref value); + if (bitOrder == BitOrder.LeastSignificant) + UnsafeBitPrimitives.WriteInt16LSB(ref destination, bitOffset, typedValue, bitCount); + else + UnsafeBitPrimitives.WriteInt16MSB(ref destination, bitOffset, typedValue, bitCount); + return; + } + + if (typeof(T) == typeof(ushort)) + { + ushort typedValue = Unsafe.As(ref value); + if (bitOrder == BitOrder.LeastSignificant) + UnsafeBitPrimitives.WriteUInt16LSB(ref destination, bitOffset, typedValue, bitCount); + else + UnsafeBitPrimitives.WriteUInt16MSB(ref destination, bitOffset, typedValue, bitCount); + return; + } + + if (typeof(T) == typeof(int)) + { + int typedValue = Unsafe.As(ref value); + if (bitOrder == BitOrder.LeastSignificant) + UnsafeBitPrimitives.WriteInt32LSB(ref destination, bitOffset, typedValue, bitCount); + else + UnsafeBitPrimitives.WriteInt32MSB(ref destination, bitOffset, typedValue, bitCount); + return; + } + + if (typeof(T) == typeof(uint)) + { + uint typedValue = Unsafe.As(ref value); + if (bitOrder == BitOrder.LeastSignificant) + UnsafeBitPrimitives.WriteUInt32LSB(ref destination, bitOffset, typedValue, bitCount); + else + UnsafeBitPrimitives.WriteUInt32MSB(ref destination, bitOffset, typedValue, bitCount); + return; + } + + if (typeof(T) == typeof(long)) + { + long typedValue = Unsafe.As(ref value); + if (bitOrder == BitOrder.LeastSignificant) + UnsafeBitPrimitives.WriteInt64LSB(ref destination, bitOffset, typedValue, bitCount); + else + UnsafeBitPrimitives.WriteInt64MSB(ref destination, bitOffset, typedValue, bitCount); + return; + } + + if (typeof(T) == typeof(ulong)) + { + ulong typedValue = Unsafe.As(ref value); + if (bitOrder == BitOrder.LeastSignificant) + UnsafeBitPrimitives.WriteUInt64LSB(ref destination, bitOffset, typedValue, bitCount); + else + UnsafeBitPrimitives.WriteUInt64MSB(ref destination, bitOffset, typedValue, bitCount); + return; + } + + if (typeof(T) == typeof(nint)) + { + nint typedValue = Unsafe.As(ref value); + if (bitOrder == BitOrder.LeastSignificant) + UnsafeBitPrimitives.WriteIntPtrLSB(ref destination, bitOffset, typedValue, bitCount); + else + UnsafeBitPrimitives.WriteIntPtrMSB(ref destination, bitOffset, typedValue, bitCount); + return; + } + + if (typeof(T) == typeof(nuint)) + { + nuint typedValue = Unsafe.As(ref value); + if (bitOrder == BitOrder.LeastSignificant) + UnsafeBitPrimitives.WriteUIntPtrLSB(ref destination, bitOffset, typedValue, bitCount); + else + UnsafeBitPrimitives.WriteUIntPtrMSB(ref destination, bitOffset, typedValue, bitCount); + return; + } + + throw new NotSupportedException(); + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 68b34b9..b8b7699 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ Notable changes to the community-maintained fork are documented here. This proje - An explicit `BitObjectAccessMode.Unsafe` generator opt-in can remove byte-storage bounds checks for callers that guarantee padded, valid backing buffers; checked generation remains the default. - `UnsafeBitPrimitives` exposes the raw-reference operations used by unsafe generated accessors. +- `BitBatchPrimitives` provides allocation-free contiguous and record-strided reads and writes for every supported integral type, Boolean values, and both bit orders. +- `BitObjectAttribute.GenerateBatchAccessors` generates strongly typed packed and strided helpers for integral, Boolean, and enum fields. ## 1.5.0 - 2026-07-21 diff --git a/README.md b/README.md index 12e13d4..b4812a5 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Existing source code continues to use the original namespaces, for example `Bits ## Features - [Changes in this fork](#changes-in-this-fork) - [BitPrimitives](#bitprimitives) +- [Batch Processing](#batch-processing) - [Bit Fields](#bit-fields) - [IO Classes](#io-classes) - [Utility Methods](#utility-methods) @@ -39,6 +40,7 @@ This fork preserves the original public API while maintaining it for current .NE | Validation | Cross-platform CI, CodeQL, modern tests, package validation, release provenance, scheduled benchmarks, and an original-fork performance regression workflow. | | Distribution | Automated tagged GitHub releases and the maintained [`RejectKid.BitsKit`](https://www.nuget.org/packages/RejectKid.BitsKit) package. | | Optional unsafe access | Generated byte-storage accessors can explicitly skip safety checks for trusted, sufficiently padded buffers; checked behavior remains the default. | +| Batch processing | Packed and record-strided fields can be read or written in bulk directly or through opt-in generated helpers. | Migrating from the original package normally requires only changing the package reference: @@ -79,6 +81,49 @@ static uint ReverseBitOrder(uint value); 0b11001000_00111011 => 0b00010011_11011100 ``` +### Batch Processing + +`BitsKit.Primitives.BitBatchPrimitives` reads and writes repeated fields with one upfront range validation and no allocations. It supports signed and unsigned 8-, 16-, 32-, and 64-bit values, native integers, Booleans, and both LSB and MSB order. + +The destination span determines how many values are read. This example decodes 1,000 packed 12-bit samples: + +```c# +ReadOnlySpan packetData = GetPacketData(); +Span samples = new ushort[1_000]; + +BitBatchPrimitives.ReadUInt16LSB( + packetData, + bitOffset: 0, + bitCount: 12, + destination: samples); +``` + +The matching write API packs values back-to-back: + +```c# +ReadOnlySpan samples = GetSamples(); +Span packetData = GetOutputBuffer(); + +BitBatchPrimitives.WriteUInt16LSB( + packetData, + bitOffset: 0, + bitCount: 12, + values: samples); +``` + +Use `bitStride` when the same field appears in a repeated record. Here each record is 32 bits wide and the desired 11-bit field starts at bit 5: + +```c# +BitBatchPrimitives.ReadUInt16LSB( + packetData, + bitOffset: 5, + bitCount: 11, + bitStride: 32, + destination: values); +``` + +This reads bits 5–15, 37–47, 69–79, and so on. The stride must be at least the field width, so batch writes cannot overlap. The full batch is validated before any value is processed; an invalid offset, width, stride, count, or buffer throws `ArgumentOutOfRangeException`. The final accessed bit position is limited to `Int32.MaxValue`, matching the span-oriented primitive APIs. + ### Bit Fields BitsKit provides the ability to generate bit-fields within types and aims to be as feature complete as the C and C++ implementations. This is achieved through the use of attributes applied to backing fields, which describe the structure and layout. These are converted into properties via a source generator. @@ -144,12 +189,52 @@ The `BitFieldModifiers` enum allows alterations to the way that the source gener For valid fixed-width integral backing fields, generated LSB and MSB getters and setters use direct masks, shifts, and byte-order operations. Eligible memory, span, array, and byte inline-array layouts also use specialized generated accessors. Other layouts continue through `BitPrimitives` to preserve their established semantics. +#### Generated Batch Accessors + +Set `GenerateBatchAccessors = true` to generate static batch helpers from the field names, offsets, widths, types, and bit orders already declared on a bit object: + +```c# +public enum PacketKind : byte +{ + Data, + Control, + Acknowledgement +} + +[BitObject( + BitOrder.LeastSignificant, + GenerateBatchAccessors = true)] +public partial struct Packet +{ + [BitField("Id", 12, BitFieldType.UInt16)] + [BooleanField("Enabled")] + [EnumField("Kind", 3, typeof(PacketKind))] + private ushort _layout; +} +``` + +The generator adds packed and strided overloads such as: + +```c# +const int recordCount = 100; +Span records = new byte[recordCount * 2]; +Span ids = new ushort[recordCount]; +Span enabled = new bool[recordCount]; + +Packet.ReadIdBatch(records, bitStride: 16, destination: ids); +Packet.ReadEnabledBatch(records, bitStride: 16, destination: enabled); + +Packet.WriteIdBatch(records, bitStride: 16, values: ids); +``` + +Generated batch methods operate on external `Span`/`ReadOnlySpan` buffers; they do not construct a bit-object for every record. Methods use the corresponding property accessibility, omit writers for readonly fields, and expose strongly typed enum and Boolean spans. Batch generation is opt-in to avoid adding public methods to existing types. Generated batch helpers always use checked `BitBatchPrimitives`, even when instance accessors select `BitObjectAccessMode.Unsafe`. + #### Unsafe Access Mode Generated accessors are bounds-checked by default. No changes are required for existing bit objects: ```c# -[BitObject(BitOrder.LeastSignificantBit)] +[BitObject(BitOrder.LeastSignificant)] public partial struct CheckedPacket { [BitField(3)] @@ -162,7 +247,7 @@ Applications that control every backing buffer and have measured a meaningful be ```c# [BitObject( - BitOrder.LeastSignificantBit, + BitOrder.LeastSignificant, AccessMode = BitObjectAccessMode.Unsafe)] public partial class TrustedPacket { @@ -230,7 +315,7 @@ struct S ``` Converted to its BitsKit representation: ```c# -[BitObject(BitOrder.LeastSignificantBit)] +[BitObject(BitOrder.LeastSignificant)] public partial struct S { [BitField("b1", 3)] // 1st 3 bits (in 1st byte) are b1 @@ -257,7 +342,7 @@ public partial struct S #### Straddling Unit Boundaries Some C compilers support straddling storage-unit boundaries. An example of this would be the "b3" field in the above example occupying the last 2 bits in the first byte and the first 4 bits in the second byte. BitsKit enforces unit boundaries for integral types however memory types do allow this. ```c# -[BitObject(BitOrder.LeastSignificantBit)] +[BitObject(BitOrder.LeastSignificant)] public unsafe partial struct S { [BitField("b1", 3)] // 1st 3 bits (in 1st byte) are b1 @@ -348,7 +433,7 @@ static int Decode(uint value) ## Benchmarks -The [benchmark workflow](https://github.com/RejectKid/BitsKit/actions/workflows/benchmarks.yml) measures the library's features on .NET 10 every week and on demand. The report covers LSB/MSB bit primitives, generated scalar, memory, and inline-array accessors, and the array-, span-, and stream-backed readers and writers across supported bit widths. Relevant pull requests run one focused dry benchmark to validate the harness. Each run includes a readable, categorized results table in its workflow summary and downloadable Markdown, JSON, logs, and environment metadata for 90 days. +The [benchmark workflow](https://github.com/RejectKid/BitsKit/actions/workflows/benchmarks.yml) measures the library's features on .NET 10 every week and on demand. The report covers LSB/MSB bit primitives, direct and generated batch operations, generated scalar, memory, and inline-array accessors, and the array-, span-, and stream-backed readers and writers across supported bit widths. Relevant pull requests run one focused dry benchmark to validate the harness. Each run includes a readable, categorized results table in its workflow summary and downloadable Markdown, JSON, logs, and environment metadata for 90 days. The published `Mean` values are normalized to one library operation, even though each benchmark processes a larger batch internally for measurement stability. This keeps primitive reads and writes, generated accessors, and the reader/writer types on the same nanoseconds-per-operation scale. Use stable local hardware and attach its generated report when making a performance-regression claim. diff --git a/eng/Run-Benchmarks.ps1 b/eng/Run-Benchmarks.ps1 index a15a70c..2306c34 100644 --- a/eng/Run-Benchmarks.ps1 +++ b/eng/Run-Benchmarks.ps1 @@ -7,6 +7,7 @@ param( [string[]] $Category = @( 'BitsKit', + 'BatchPrimitives', 'GeneratedAccessor', 'BitReader', 'MemoryBitReader',