From bfa71b11932e899f4c4d6a5541eb4cc649cef405 Mon Sep 17 00:00:00 2001 From: Jeremy Kuhne Date: Tue, 23 Jan 2024 11:31:09 -0800 Subject: [PATCH] Add SpanWriter and RunLengthEncoder Add SpanWriter and RunLengthEncoder. Tweak SpanReader API. --- src/thirtytwo/Support/RunLengthEncoder.cs | 97 +++++++++++++++ src/thirtytwo/Support/SpanReader.cs | 51 +++++++- src/thirtytwo/Support/SpanWriter.cs | 112 ++++++++++++++++++ .../Support/RunLengthEncoderTests.cs | 44 +++++++ .../Support/SpanReaderTests.cs | 49 ++++---- .../Support/SpanWriterTests.cs | 87 ++++++++++++++ 6 files changed, 415 insertions(+), 25 deletions(-) create mode 100644 src/thirtytwo/Support/RunLengthEncoder.cs create mode 100644 src/thirtytwo/Support/SpanWriter.cs create mode 100644 src/thirtytwo_tests/Support/RunLengthEncoderTests.cs create mode 100644 src/thirtytwo_tests/Support/SpanWriterTests.cs diff --git a/src/thirtytwo/Support/RunLengthEncoder.cs b/src/thirtytwo/Support/RunLengthEncoder.cs new file mode 100644 index 0000000..0d9a5ab --- /dev/null +++ b/src/thirtytwo/Support/RunLengthEncoder.cs @@ -0,0 +1,97 @@ +// Copyright (c) Jeremy W. Kuhne. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Windows.Support; + +/// +/// Simple run length encoder (RLE) that works on spans. +/// +/// +/// +/// Format used is a byte for the count, followed by a byte for the value. +/// +/// +internal static class RunLengthEncoder +{ + /// + /// Get the encoded length, in bytes, of the given data. + /// + public static int GetEncodedLength(ReadOnlySpan data) + { + SpanReader reader = new(data); + + int length = 0; + while (reader.TryRead(out byte value)) + { + int count = reader.AdvancePast(value) + 1; + while (count > 0) + { + // 1 byte for the count, 1 byte for the value + length += 2; + count -= 0xFF; + } + } + + return length; + } + + /// + /// Get the decoded length, in bytes, of the given encoded data. + /// + public static int GetDecodedLength(ReadOnlySpan encoded) + { + int length = 0; + for (int i = 0; i < encoded.Length; i += 2) + { + length += encoded[i]; + } + + return length; + } + + /// + /// Encode the given data into the given span. + /// + /// + /// if the span was not large enough to hold the encoded data. + /// + public static bool TryEncode(ReadOnlySpan data, Span encoded, out int written) + { + SpanReader reader = new(data); + SpanWriter writer = new(encoded); + + while (reader.TryRead(out byte value)) + { + int count = reader.AdvancePast(value) + 1; + while (count > 0) + { + if (!writer.TryWrite((byte)Math.Min(count, 0xFF)) || !writer.TryWrite(value)) + { + written = writer.Position; + return false; + } + + count -= 0xFF; + } + } + + written = writer.Position; + return true; + } + + public static bool TryDecode(ReadOnlySpan encoded, Span data) + { + SpanReader reader = new(encoded); + SpanWriter writer = new(data); + + while (reader.TryRead(out byte count)) + { + if (!reader.TryRead(out byte value) || !writer.TryWrite(count, value)) + { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/src/thirtytwo/Support/SpanReader.cs b/src/thirtytwo/Support/SpanReader.cs index 7dd6063..28b47ef 100644 --- a/src/thirtytwo/Support/SpanReader.cs +++ b/src/thirtytwo/Support/SpanReader.cs @@ -19,7 +19,14 @@ public unsafe ref struct SpanReader(ReadOnlySpan span) where T : unmanaged { private ReadOnlySpan _unread = span; public ReadOnlySpan Span { get; } = span; - public readonly ReadOnlySpan UnreadSpan => _unread; + + public int Position + { + readonly get => Span.Length - _unread.Length; + set => _unread = Span[value..]; + } + + public readonly int Length => Span.Length; /// /// Try to read everything up to the given . Advances the reader past the @@ -54,6 +61,7 @@ public bool TryReadTo(T delimiter, bool advancePastDelimiter, out ReadOnlySpan(int count, out ReadOnlySpan value) where TVa return success; } + /// + /// Advance the reader if the given values are next. + /// + /// The span to compare the next items to. + /// if the values were found and the reader advanced. + public bool TryAdvancePast(ReadOnlySpan next) + { + bool success = false; + if (_unread.StartsWith(next)) + { + UnsafeAdvance(next.Length); + success = true; + } + + return success; + } + + /// + /// Advance the reader past consecutive instances of the given . + /// + /// How many positions the reader has been advanced + public int AdvancePast(T value) + { + int count = 0; + + int index = _unread.IndexOfAnyExcept(value); + if (index == -1) + { + // Everything left is the value + count = _unread.Length; + _unread = default; + } + else if (index != 0) + { + count = index; + UnsafeAdvance(index); + } + + return count; + } + /// /// Advance the reader by the given . /// diff --git a/src/thirtytwo/Support/SpanWriter.cs b/src/thirtytwo/Support/SpanWriter.cs new file mode 100644 index 0000000..aad705d --- /dev/null +++ b/src/thirtytwo/Support/SpanWriter.cs @@ -0,0 +1,112 @@ +// Copyright (c) Jeremy W. Kuhne. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Windows.Support; + +/// +/// Fast stack based writer. +/// +internal unsafe ref struct SpanWriter(Span span) where T : unmanaged, IEquatable +{ + private Span _unwritten = span; + public Span Span { get; } = span; + + public int Position + { + readonly get => Span.Length - _unwritten.Length; + set => _unwritten = Span[value..]; + } + + public readonly int Length => Span.Length; + + /// + /// Try to write the given value. + /// + public bool TryWrite(T value) + { + bool success = false; + + if (!_unwritten.IsEmpty) + { + success = true; + _unwritten[0] = value; + UnsafeAdvance(1); + } + + return success; + } + + /// + /// Try to write the given value. + /// + public bool TryWrite(ReadOnlySpan values) + { + bool success = false; + + if (_unwritten.Length >= values.Length) + { + success = true; + values.CopyTo(_unwritten); + UnsafeAdvance(values.Length); + } + + return success; + } + + /// + /// Try to write the given value times. + /// + public bool TryWrite(int count, T value) + { + bool success = false; + + if (_unwritten.Length >= count) + { + success = true; + _unwritten[..count].Fill(value); + UnsafeAdvance(count); + } + + return success; + } + + /// + /// Advance the writer by the given . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Advance(int count) => _unwritten = _unwritten[count..]; + + /// + /// Rewind the writer by the given . + /// + public void Rewind(int count) => _unwritten = Span[(Span.Length - _unwritten.Length - count)..]; + + /// + /// Reset the reader to the beginning of the span. + /// + public void Reset() => _unwritten = Span; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void UnsafeAdvance(int count) + { + Debug.Assert((uint)count <= (uint)_unwritten.Length); + UncheckedSlice(ref _unwritten, count, _unwritten.Length - count); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void UncheckedSliceTo(ref Span span, int length) + { + Debug.Assert((uint)length <= (uint)span.Length); + span = MemoryMarshal.CreateSpan(ref MemoryMarshal.GetReference(span), length); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void UncheckedSlice(ref Span span, int start, int length) + { + Debug.Assert((uint)start <= (uint)span.Length && (uint)length <= (uint)(span.Length - start)); + span = MemoryMarshal.CreateSpan(ref Unsafe.Add(ref MemoryMarshal.GetReference(span), (nint)(uint)start), length); + } +} \ No newline at end of file diff --git a/src/thirtytwo_tests/Support/RunLengthEncoderTests.cs b/src/thirtytwo_tests/Support/RunLengthEncoderTests.cs new file mode 100644 index 0000000..2747c0f --- /dev/null +++ b/src/thirtytwo_tests/Support/RunLengthEncoderTests.cs @@ -0,0 +1,44 @@ +// Copyright (c) Jeremy W. Kuhne. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Windows.Support; + +public class RunLengthEncoderTests +{ + [Fact] + public void RunLengthEncoder_TryEncode() + { + ReadOnlySpan data = [1, 1, 1, 2, 2, 3, 3, 3, 3]; + Span encoded = new byte[RunLengthEncoder.GetEncodedLength(data)]; + RunLengthEncoder.TryEncode(data, encoded, out int written).Should().BeTrue(); + written.Should().Be(6); + encoded.ToArray().Should().BeEquivalentTo([3, 1, 2, 2, 4, 3]); + } + + [Theory] + [InlineData(0, 0)] + [InlineData(1, 2)] + [InlineData(255, 2)] + [InlineData(256, 4)] + public void RunLengthEncoder_GetEncodedLength(int count, int expectedLength) + { + Span data = new byte[count]; + Span encoded = new byte[RunLengthEncoder.GetEncodedLength(data)]; + RunLengthEncoder.TryEncode(data, encoded, out int written).Should().BeTrue(); + written.Should().Be(expectedLength); + encoded.Length.Should().Be(expectedLength); + } + + [Fact] + public void RunLengthEncoder_RoundTrip() + { + ReadOnlySpan data = [1, 1, 1, 2, 2, 3, 3, 3, 3]; + Span encoded = new byte[RunLengthEncoder.GetEncodedLength(data)]; + RunLengthEncoder.TryEncode(data, encoded, out int written).Should().BeTrue(); + written.Should().Be(6); + + Span decoded = new byte[RunLengthEncoder.GetDecodedLength(encoded)]; + RunLengthEncoder.TryDecode(encoded, decoded).Should().BeTrue(); + decoded.ToArray().Should().BeEquivalentTo(data.ToArray()); + } +} diff --git a/src/thirtytwo_tests/Support/SpanReaderTests.cs b/src/thirtytwo_tests/Support/SpanReaderTests.cs index 5b20a0c..38a5cf6 100644 --- a/src/thirtytwo_tests/Support/SpanReaderTests.cs +++ b/src/thirtytwo_tests/Support/SpanReaderTests.cs @@ -10,51 +10,51 @@ public class SpanReaderTests [Fact] public void SpanReader_TryReadTo_SkipDelimiter() { - ReadOnlySpan span = new byte[] { 1, 2, 3, 4, 5 }; + ReadOnlySpan span = [1, 2, 3, 4, 5]; SpanReader reader = new(span); reader.TryReadTo(3, out var read).Should().BeTrue(); read.ToArray().Should().BeEquivalentTo([1, 2]); - reader.UnreadSpan.ToArray().Should().BeEquivalentTo([4, 5]); + reader.Position.Should().Be(3); reader.TryReadTo(5, out read).Should().BeTrue(); read.ToArray().Should().BeEquivalentTo([4]); - reader.UnreadSpan.ToArray().Should().BeEmpty(); + reader.Position.Should().Be(5); } [Fact] - public void SpanReader_TryReadTo_DontSkipDelimiter() + public void SpanReader_TryReadTo_DoNotSkipDelimiter() { - ReadOnlySpan span = new byte[] { 1, 2, 3, 4, 5 }; + ReadOnlySpan span = [1, 2, 3, 4, 5]; SpanReader reader = new(span); reader.TryReadTo(3, advancePastDelimiter: false, out var read).Should().BeTrue(); read.ToArray().Should().BeEquivalentTo([1, 2]); - reader.UnreadSpan.ToArray().Should().BeEquivalentTo([3, 4, 5]); + reader.Position.Should().Be(2); reader.TryReadTo(5, advancePastDelimiter: false, out read).Should().BeTrue(); read.ToArray().Should().BeEquivalentTo([3, 4]); - reader.UnreadSpan.ToArray().Should().BeEquivalentTo([5]); + reader.Position.Should().Be(4); reader.TryReadTo(5, advancePastDelimiter: false, out read).Should().BeTrue(); read.ToArray().Should().BeEmpty(); - reader.UnreadSpan.ToArray().Should().BeEquivalentTo([5]); + reader.Position.Should().Be(4); } [Fact] public void SpanReader_Advance() { - ReadOnlySpan span = new byte[] { 1, 2, 3, 4, 5 }; + ReadOnlySpan span = [1, 2, 3, 4, 5]; SpanReader reader = new(span); reader.Advance(2); - reader.UnreadSpan.ToArray().Should().BeEquivalentTo([3, 4, 5]); + reader.Position.Should().Be(2); reader.Advance(2); - reader.UnreadSpan.ToArray().Should().BeEquivalentTo([5]); + reader.Position.Should().Be(4); reader.Advance(1); - reader.UnreadSpan.ToArray().Should().BeEmpty(); + reader.Position.Should().Be(5); try { @@ -70,14 +70,14 @@ public void SpanReader_Advance() [Fact] public void SpanReader_Rewind() { - ReadOnlySpan span = new byte[] { 1, 2, 3, 4, 5 }; + ReadOnlySpan span = [1, 2, 3, 4, 5]; SpanReader reader = new(span); reader.Advance(2); - reader.UnreadSpan.ToArray().Should().BeEquivalentTo([3, 4, 5]); + reader.Position.Should().Be(2); reader.Rewind(1); - reader.UnreadSpan.ToArray().Should().BeEquivalentTo([2, 3, 4, 5]); + reader.Position.Should().Be(1); try { @@ -90,13 +90,13 @@ public void SpanReader_Rewind() } reader.Rewind(1); - reader.UnreadSpan.ToArray().Should().BeEquivalentTo([1, 2, 3, 4, 5]); + reader.Position.Should().Be(0); } [Fact] public void SpanReader_TryRead_ReadPoints() { - ReadOnlySpan span = new uint[] { 1, 2, 3, 4, 5 }; + ReadOnlySpan span = [1, 2, 3, 4, 5]; SpanReader reader = new(span); reader.TryRead(out Point value).Should().BeTrue(); @@ -106,13 +106,14 @@ public void SpanReader_TryRead_ReadPoints() value.Should().Be(new Point(3, 4)); reader.TryRead(out value).Should().BeFalse(); - reader.UnreadSpan.ToArray().Should().BeEquivalentTo([5]); + value.Should().Be(default(Point)); + reader.Position.Should().Be(4); } [Fact] public void SpanReader_TryRead_ReadPointCounts() { - ReadOnlySpan span = new uint[] { 1, 2, 3, 4, 5 }; + ReadOnlySpan span = [1, 2, 3, 4, 5]; SpanReader reader = new(span); reader.TryRead(2, out ReadOnlySpan value).Should().BeTrue(); @@ -141,18 +142,18 @@ public void SpanReader_TryRead_ReadPointFCounts_NotEnoughBuffer(int bufferSize, [Fact] public void SpanReader_TryRead_Count() { - ReadOnlySpan span = new uint[] { 1, 2, 3, 4, 5 }; + ReadOnlySpan span = [1, 2, 3, 4, 5]; SpanReader reader = new(span); reader.TryRead(2, out var read).Should().BeTrue(); read.ToArray().Should().BeEquivalentTo([1, 2]); - reader.UnreadSpan.ToArray().Should().BeEquivalentTo([3, 4, 5]); + reader.Position.Should().Be(2); reader.TryRead(2, out read).Should().BeTrue(); read.ToArray().Should().BeEquivalentTo([3, 4]); - reader.UnreadSpan.ToArray().Should().BeEquivalentTo([5]); + reader.Position.Should().Be(4); - reader.TryRead(2, out read).Should().BeFalse(); - reader.UnreadSpan.ToArray().Should().BeEquivalentTo([5]); + reader.TryRead(2, out _).Should().BeFalse(); + reader.Position.Should().Be(4); } } diff --git a/src/thirtytwo_tests/Support/SpanWriterTests.cs b/src/thirtytwo_tests/Support/SpanWriterTests.cs new file mode 100644 index 0000000..6087ca0 --- /dev/null +++ b/src/thirtytwo_tests/Support/SpanWriterTests.cs @@ -0,0 +1,87 @@ +// Copyright (c) Jeremy W. Kuhne. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Drawing; + +namespace Windows.Support; + +public class SpanWriterTests +{ + [Fact] + public void SpanWriter_TryWrite() + { + Span span = new byte[5]; + SpanWriter writer = new(span); + + writer.TryWrite(1).Should().BeTrue(); + span.ToArray().Should().BeEquivalentTo([1, 0, 0, 0, 0]); + + writer.TryWrite(2).Should().BeTrue(); + span.ToArray().Should().BeEquivalentTo([1, 2, 0, 0, 0]); + + writer.TryWrite(3).Should().BeTrue(); + span.ToArray().Should().BeEquivalentTo([1, 2, 3, 0, 0]); + + writer.TryWrite(4).Should().BeTrue(); + span.ToArray().Should().BeEquivalentTo([1, 2, 3, 4, 0]); + + writer.TryWrite(5).Should().BeTrue(); + span.ToArray().Should().BeEquivalentTo([1, 2, 3, 4, 5]); + + writer.TryWrite(6).Should().BeFalse(); + } + + [Fact] + public void SpanWriter_TryWrite_Spans() + { + Span span = new byte[5]; + SpanWriter writer = new(span); + + writer.TryWrite([1, 2]).Should().BeTrue(); + span.ToArray().Should().BeEquivalentTo([1, 2, 0, 0, 0]); + + writer.TryWrite([3, 4]).Should().BeTrue(); + span.ToArray().Should().BeEquivalentTo([1, 2, 3, 4, 0]); + + writer.TryWrite([5]).Should().BeTrue(); + span.ToArray().Should().BeEquivalentTo([1, 2, 3, 4, 5]); + + writer.TryWrite([6]).Should().BeFalse(); + } + + [Fact] + public void SpanWriter_TryWrite_Count() + { + Span span = new int[5]; + SpanWriter writer = new(span); + + writer.TryWrite(2, 1).Should().BeTrue(); + span.ToArray().Should().BeEquivalentTo([1, 1, 0, 0, 0]); + + writer.TryWrite(2, 2).Should().BeTrue(); + span.ToArray().Should().BeEquivalentTo([1, 1, 2, 2, 0]); + + writer.TryWrite(1, 3).Should().BeTrue(); + span.ToArray().Should().BeEquivalentTo([1, 1, 2, 2, 3]); + + writer.TryWrite(1, 4).Should().BeFalse(); + } + + [Fact] + public void SpanWriter_TryWrite_CountPoints() + { + Span span = new Point[5]; + SpanWriter writer = new(span); + + writer.TryWrite(2, new Point(1, 2)).Should().BeTrue(); + span.ToArray().Should().BeEquivalentTo([new Point(1, 2), new Point(1, 2), default, default, default]); + + writer.TryWrite(2, new Point(3, 4)).Should().BeTrue(); + span.ToArray().Should().BeEquivalentTo([new Point(1, 2), new Point(1, 2), new Point(3, 4), new Point(3, 4), default]); + + writer.TryWrite(1, new Point(5, 6)).Should().BeTrue(); + span.ToArray().Should().BeEquivalentTo([new Point(1, 2), new Point(1, 2), new Point(3, 4), new Point(3, 4), new Point(5, 6)]); + + writer.TryWrite(1, new Point(7, 8)).Should().BeFalse(); + } +}