diff --git a/CHANGELOG.md b/CHANGELOG.md index 9518633..d1d194a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] - yyyy-mm-dd ### Added - +- Added RcCircularBuffer [@ikpil](https://github.com/ikpil) + ### Fixed ### Changed -- Added DtPathCorridor.Init(int maxPath) function to allow setting the maximum path [@ikpil](https://github.com/ikpil) +- Changed DtPathCorridor.Init(int maxPath) function to allow setting the maximum path [@ikpil](https://github.com/ikpil) +- Changed from List to RcCyclicBuffer in DtCrowdTelemetry execution timing sampling [@wrenge](https://github.com/wrenge) ### Removed diff --git a/src/DotRecast.Core/Buffers/RcCyclicBuffer.cs b/src/DotRecast.Core/Buffers/RcCyclicBuffer.cs index 0dc6ffd..2846bfb 100644 --- a/src/DotRecast.Core/Buffers/RcCyclicBuffer.cs +++ b/src/DotRecast.Core/Buffers/RcCyclicBuffer.cs @@ -1,48 +1,244 @@ using System; +using System.Collections.Generic; +using System.Net.Security; namespace DotRecast.Core.Buffers { + // https://github.com/joaoportela/CircularBuffer-CSharp/blob/master/CircularBuffer/CircularBuffer.cs public class RcCyclicBuffer { - public int MinIndex { get; private set; } - public int MaxIndex { get; private set; } - public int Count => MaxIndex - MinIndex + 1; - public readonly int Size; - - public T this[int index] => Get(index); - private readonly T[] _buffer; - public RcCyclicBuffer(in int size) + private int _start; + private int _end; + private int _size; + + public RcCyclicBuffer(int capacity) + : this(capacity, new T[] { }) { - _buffer = new T[size]; - Size = size; - MinIndex = 0; - MaxIndex = -1; } - public void Add(in T item) + public RcCyclicBuffer(int capacity, T[] items) { - MaxIndex++; - var index = MaxIndex % Size; + if (capacity < 1) + { + throw new ArgumentException("RcCyclicBuffer cannot have negative or zero capacity.", nameof(capacity)); + } - if (MaxIndex >= Size) - MinIndex = MaxIndex - Size + 1; + if (items == null) + { + throw new ArgumentNullException(nameof(items)); + } - _buffer[index] = item; + if (items.Length > capacity) + { + throw new ArgumentException("Too many items to fit RcCyclicBuffer", nameof(items)); + } + + _buffer = new T[capacity]; + + Array.Copy(items, _buffer, items.Length); + _size = items.Length; + + _start = 0; + _end = _size == capacity ? 0 : _size; } - public T Get(in int index) - { - if (index < MinIndex || index > MaxIndex) - throw new ArgumentOutOfRangeException(); + public int Capacity => _buffer.Length; + public bool IsFull => Size == Capacity; + public bool IsEmpty => Size == 0; - return _buffer[index % Size]; + public int Size => _size; + + public T Front() + { + ThrowIfEmpty(); + return _buffer[_start]; } - public Span AsSpan() + public T Back() { - return _buffer.AsSpan(0, Count); + ThrowIfEmpty(); + return _buffer[(_end != 0 ? _end : Capacity) - 1]; + } + + public T this[int index] + { + get + { + if (IsEmpty) + { + throw new IndexOutOfRangeException($"Cannot access index {index}. Buffer is empty"); + } + + if (index >= _size) + { + throw new IndexOutOfRangeException($"Cannot access index {index}. Buffer size is {_size}"); + } + + int actualIndex = InternalIndex(index); + return _buffer[actualIndex]; + } + set + { + if (IsEmpty) + { + throw new IndexOutOfRangeException($"Cannot access index {index}. Buffer is empty"); + } + + if (index >= _size) + { + throw new IndexOutOfRangeException($"Cannot access index {index}. Buffer size is {_size}"); + } + + int actualIndex = InternalIndex(index); + _buffer[actualIndex] = value; + } + } + + public void PushBack(T item) + { + if (IsFull) + { + _buffer[_end] = item; + Increment(ref _end); + _start = _end; + } + else + { + _buffer[_end] = item; + Increment(ref _end); + ++_size; + } + } + + public void PushFront(T item) + { + if (IsFull) + { + Decrement(ref _start); + _end = _start; + _buffer[_start] = item; + } + else + { + Decrement(ref _start); + _buffer[_start] = item; + ++_size; + } + } + + public void PopBack() + { + ThrowIfEmpty("Cannot take elements from an empty buffer."); + Decrement(ref _end); + _buffer[_end] = default(T); + --_size; + } + + public void PopFront() + { + ThrowIfEmpty("Cannot take elements from an empty buffer."); + _buffer[_start] = default(T); + Increment(ref _start); + --_size; + } + + public void Clear() + { + // to clear we just reset everything. + _start = 0; + _end = 0; + _size = 0; + Array.Clear(_buffer, 0, _buffer.Length); + } + + public T[] ToArray() + { + int idx = 0; + T[] newArray = new T[Size]; + + ForEach(x => newArray[idx++] = x); + + return newArray; + } + + public void ForEach(Action action) + { + var spanOne = ArrayOne(); + foreach (var item in spanOne) + { + action.Invoke(item); + } + + var spanTwo = ArrayTwo(); + foreach (var item in spanTwo) + { + action.Invoke(item); + } + } + + private void ThrowIfEmpty(string message = "Cannot access an empty buffer.") + { + if (IsEmpty) + { + throw new InvalidOperationException(message); + } + } + + private void Increment(ref int index) + { + if (++index == Capacity) + { + index = 0; + } + } + + private void Decrement(ref int index) + { + if (index == 0) + { + index = Capacity; + } + + index--; + } + + private int InternalIndex(int index) + { + return _start + (index < (Capacity - _start) + ? index + : index - Capacity); + } + + private Span ArrayOne() + { + if (IsEmpty) + { + return new Span(Array.Empty()); + } + + if (_start < _end) + { + return new Span(_buffer, _start, _end - _start); + } + + return new Span(_buffer, _start, _buffer.Length - _start); + } + + private Span ArrayTwo() + { + if (IsEmpty) + { + return new Span(Array.Empty()); + } + + if (_start < _end) + { + return new Span(_buffer, _end, 0); + } + + return new Span(_buffer, 0, _end); } } } \ No newline at end of file diff --git a/src/DotRecast.Detour.Crowd/DtCrowdTelemetry.cs b/src/DotRecast.Detour.Crowd/DtCrowdTelemetry.cs index dbc3086..0347d00 100644 --- a/src/DotRecast.Detour.Crowd/DtCrowdTelemetry.cs +++ b/src/DotRecast.Detour.Crowd/DtCrowdTelemetry.cs @@ -19,12 +19,9 @@ freely, subject to the following restrictions: using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; -using System.Reflection.Emit; using DotRecast.Core; using DotRecast.Core.Buffers; -using DotRecast.Core.Numerics; namespace DotRecast.Detour.Crowd { @@ -86,25 +83,25 @@ namespace DotRecast.Detour.Crowd private void Stop(DtCrowdTimerLabel name) { long duration = RcFrequency.Ticks - _executionTimings[name]; - if (!_executionTimingSamples.TryGetValue(name, out var s)) + if (!_executionTimingSamples.TryGetValue(name, out var cb)) { - s = new RcCyclicBuffer(TIMING_SAMPLES); - _executionTimingSamples.Add(name, s); + cb = new RcCyclicBuffer(TIMING_SAMPLES); + _executionTimingSamples.Add(name, cb); } - s.Add(duration); - _executionTimings[name] = CalculateAverage(s.AsSpan()); + cb.PushBack(duration); + _executionTimings[name] = CalculateAverage(cb); } - private static long CalculateAverage(Span buffer) + private static long CalculateAverage(RcCyclicBuffer buffer) { long sum = 0L; - foreach (var item in buffer) + buffer.ForEach(item => { sum += item; - } + }); - return sum / buffer.Length; + return sum / buffer.Size; } } } \ No newline at end of file diff --git a/test/DotRecast.Core.Test/RcCyclicBufferTest.cs b/test/DotRecast.Core.Test/RcCyclicBufferTest.cs new file mode 100644 index 0000000..56a9c54 --- /dev/null +++ b/test/DotRecast.Core.Test/RcCyclicBufferTest.cs @@ -0,0 +1,293 @@ +using System; +using DotRecast.Core.Buffers; +using NUnit.Framework; + +namespace DotRecast.Core.Test; + +// https://github.com/joaoportela/CircularBuffer-CSharp/blob/master/CircularBuffer.Tests/CircularBufferTests.cs +public class RcCyclicBufferTests +{ + [Test] + public void RcCyclicBuffer_GetEnumeratorConstructorCapacity_ReturnsEmptyCollection() + { + var buffer = new RcCyclicBuffer(5); + Assert.That(buffer.ToArray(), Is.Empty); + } + + [Test] + public void RcCyclicBuffer_ConstructorSizeIndexAccess_CorrectContent() + { + var buffer = new RcCyclicBuffer(5, new[] { 0, 1, 2, 3 }); + + Assert.That(buffer.Capacity, Is.EqualTo(5)); + Assert.That(buffer.Size, Is.EqualTo(4)); + for (int i = 0; i < 4; i++) + { + Assert.That(buffer[i], Is.EqualTo(i)); + } + } + + [Test] + public void RcCyclicBuffer_Constructor_ExceptionWhenSourceIsLargerThanCapacity() + { + Assert.Throws(() => new RcCyclicBuffer(3, new[] { 0, 1, 2, 3 })); + } + + [Test] + public void RcCyclicBuffer_GetEnumeratorConstructorDefinedArray_CorrectContent() + { + var buffer = new RcCyclicBuffer(5, new[] { 0, 1, 2, 3 }); + + int x = 0; + buffer.ForEach(item => + { + Assert.That(item, Is.EqualTo(x)); + x++; + }); + } + + [Test] + public void RcCyclicBuffer_PushBack_CorrectContent() + { + var buffer = new RcCyclicBuffer(5); + + for (int i = 0; i < 5; i++) + { + buffer.PushBack(i); + } + + Assert.That(buffer.Front(), Is.EqualTo(0)); + for (int i = 0; i < 5; i++) + { + Assert.That(buffer[i], Is.EqualTo(i)); + } + } + + [Test] + public void RcCyclicBuffer_PushBackOverflowingBuffer_CorrectContent() + { + var buffer = new RcCyclicBuffer(5); + + for (int i = 0; i < 10; i++) + { + buffer.PushBack(i); + } + + Assert.That(buffer.ToArray(), Is.EqualTo(new[] { 5, 6, 7, 8, 9 })); + } + + [Test] + public void RcCyclicBuffer_GetEnumeratorOverflowedArray_CorrectContent() + { + var buffer = new RcCyclicBuffer(5); + + for (int i = 0; i < 10; i++) + { + buffer.PushBack(i); + } + + // buffer should have [5,6,7,8,9] + int x = 5; + buffer.ForEach(item => + { + Assert.That(item, Is.EqualTo(x)); + x++; + }); + } + + [Test] + public void RcCyclicBuffer_ToArrayConstructorDefinedArray_CorrectContent() + { + var buffer = new RcCyclicBuffer(5, new[] { 0, 1, 2, 3 }); + + Assert.That(buffer.ToArray(), Is.EqualTo(new[] { 0, 1, 2, 3 })); + } + + [Test] + public void RcCyclicBuffer_ToArrayOverflowedBuffer_CorrectContent() + { + var buffer = new RcCyclicBuffer(5); + + for (int i = 0; i < 10; i++) + { + buffer.PushBack(i); + } + + Assert.That(buffer.ToArray(), Is.EqualTo(new[] { 5, 6, 7, 8, 9 })); + } + + [Test] + public void RcCyclicBuffer_PushFront_CorrectContent() + { + var buffer = new RcCyclicBuffer(5); + + for (int i = 0; i < 5; i++) + { + buffer.PushFront(i); + } + + Assert.That(buffer.ToArray(), Is.EqualTo(new[] { 4, 3, 2, 1, 0 })); + } + + [Test] + public void RcCyclicBuffer_PushFrontAndOverflow_CorrectContent() + { + var buffer = new RcCyclicBuffer(5); + + for (int i = 0; i < 10; i++) + { + buffer.PushFront(i); + } + + Assert.That(buffer.ToArray(), Is.EqualTo(new[] { 9, 8, 7, 6, 5 })); + } + + [Test] + public void RcCyclicBuffer_Front_CorrectItem() + { + var buffer = new RcCyclicBuffer(5, new[] { 0, 1, 2, 3, 4 }); + + Assert.That(buffer.Front(), Is.EqualTo(0)); + } + + [Test] + public void RcCyclicBuffer_Back_CorrectItem() + { + var buffer = new RcCyclicBuffer(5, new[] { 0, 1, 2, 3, 4 }); + Assert.That(buffer.Back(), Is.EqualTo(4)); + } + + [Test] + public void RcCyclicBuffer_BackOfBufferOverflowByOne_CorrectItem() + { + var buffer = new RcCyclicBuffer(5, new[] { 0, 1, 2, 3, 4 }); + buffer.PushBack(42); + Assert.That(buffer.ToArray(), Is.EqualTo(new[] { 1, 2, 3, 4, 42 })); + Assert.That(buffer.Back(), Is.EqualTo(42)); + } + + [Test] + public void RcCyclicBuffer_Front_EmptyBufferThrowsException() + { + var buffer = new RcCyclicBuffer(5); + + Assert.Throws(() => buffer.Front()); + } + + [Test] + public void RcCyclicBuffer_Back_EmptyBufferThrowsException() + { + var buffer = new RcCyclicBuffer(5); + Assert.Throws(() => buffer.Back()); + } + + [Test] + public void RcCyclicBuffer_PopBack_RemovesBackElement() + { + var buffer = new RcCyclicBuffer(5, new[] { 0, 1, 2, 3, 4 }); + + Assert.That(buffer.Size, Is.EqualTo(5)); + + buffer.PopBack(); + + Assert.That(buffer.Size, Is.EqualTo(4)); + Assert.That(buffer.ToArray(), Is.EqualTo(new[] { 0, 1, 2, 3 })); + } + + [Test] + public void RcCyclicBuffer_PopBackInOverflowBuffer_RemovesBackElement() + { + var buffer = new RcCyclicBuffer(5, new[] { 0, 1, 2, 3, 4 }); + buffer.PushBack(5); + + Assert.That(buffer.Size, Is.EqualTo(5)); + Assert.That(buffer.ToArray(), Is.EqualTo(new[] { 1, 2, 3, 4, 5 })); + + buffer.PopBack(); + + Assert.That(buffer.Size, Is.EqualTo(4)); + Assert.That(buffer.ToArray(), Is.EqualTo(new[] { 1, 2, 3, 4 })); + } + + [Test] + public void RcCyclicBuffer_PopFront_RemovesBackElement() + { + var buffer = new RcCyclicBuffer(5, new[] { 0, 1, 2, 3, 4 }); + + Assert.That(buffer.Size, Is.EqualTo(5)); + + buffer.PopFront(); + + Assert.That(buffer.Size, Is.EqualTo(4)); + Assert.That(buffer.ToArray(), Is.EqualTo(new[] { 1, 2, 3, 4 })); + } + + [Test] + public void RcCyclicBuffer_PopFrontInOverflowBuffer_RemovesBackElement() + { + var buffer = new RcCyclicBuffer(5, new[] { 0, 1, 2, 3, 4 }); + buffer.PushFront(5); + + Assert.That(buffer.Size, Is.EqualTo(5)); + Assert.That(buffer.ToArray(), Is.EqualTo(new[] { 5, 0, 1, 2, 3 })); + + buffer.PopFront(); + + Assert.That(buffer.Size, Is.EqualTo(4)); + Assert.That(buffer.ToArray(), Is.EqualTo(new[] { 0, 1, 2, 3 })); + } + + [Test] + public void RcCyclicBuffer_SetIndex_ReplacesElement() + { + var buffer = new RcCyclicBuffer(5, new[] { 0, 1, 2, 3, 4 }); + + buffer[1] = 10; + buffer[3] = 30; + + Assert.That(buffer.ToArray(), Is.EqualTo(new[] { 0, 10, 2, 30, 4 })); + } + + [Test] + public void RcCyclicBuffer_WithDifferentSizeAndCapacity_BackReturnsLastArrayPosition() + { + // test to confirm this issue does not happen anymore: + // https://github.com/joaoportela/RcCyclicBuffer-CSharp/issues/2 + + var buffer = new RcCyclicBuffer(5, new[] { 0, 1, 2, 3, 4 }); + + buffer.PopFront(); // (make size and capacity different) + + Assert.That(buffer.Back(), Is.EqualTo(4)); + } + + [Test] + public void RcCyclicBuffer_Clear_ClearsContent() + { + var buffer = new RcCyclicBuffer(5, new[] { 4, 3, 2, 1, 0 }); + + buffer.Clear(); + + Assert.That(buffer.Size, Is.EqualTo(0)); + Assert.That(buffer.Capacity, Is.EqualTo(5)); + Assert.That(buffer.ToArray(), Is.EqualTo(new int[0])); + } + + [Test] + public void RcCyclicBuffer_Clear_WorksNormallyAfterClear() + { + var buffer = new RcCyclicBuffer(5, new[] { 4, 3, 2, 1, 0 }); + + buffer.Clear(); + for (int i = 0; i < 5; i++) + { + buffer.PushBack(i); + } + + Assert.That(buffer.Front(), Is.EqualTo(0)); + for (int i = 0; i < 5; i++) + { + Assert.That(buffer[i], Is.EqualTo(i)); + } + } +} \ No newline at end of file